-
-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #61 from complexdatacollective/feature/participant…
…-urls Feature/participant urls
- Loading branch information
Showing
15 changed files
with
690 additions
and
37 deletions.
There are no files selected for viewing
101 changes: 101 additions & 0 deletions
101
app/(dashboard)/dashboard/_components/ParticipantSelectionDropdown.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { useEffect, useState } from 'react'; | ||
|
||
import { Button } from '~/components/ui/Button'; | ||
import { | ||
DropdownMenu, | ||
DropdownMenuCheckboxItem, | ||
DropdownMenuContent, | ||
DropdownMenuLabel, | ||
DropdownMenuSeparator, | ||
DropdownMenuTrigger, | ||
} from '~/components/ui/dropdown-menu'; | ||
import type { Participant } from '@prisma/client'; | ||
import { ScrollArea } from '~/components/ui/scroll-area'; | ||
|
||
type ParticipantSelectionDropdownProps = { | ||
participants: Participant[]; | ||
disabled: boolean; | ||
setParticipantsToExport: (participants: Participant[]) => void; | ||
}; | ||
|
||
export function ParticipantSelectionDropdown({ | ||
participants, | ||
disabled, | ||
setParticipantsToExport, | ||
}: ParticipantSelectionDropdownProps) { | ||
const [selectedParticipants, setSelectedParticipants] = useState< | ||
Participant[] | ||
>([]); | ||
|
||
useEffect(() => { | ||
setSelectedParticipants(participants); | ||
}, [participants, setParticipantsToExport]); | ||
|
||
useEffect(() => { | ||
setParticipantsToExport(selectedParticipants); | ||
}, [selectedParticipants, setParticipantsToExport]); | ||
|
||
return ( | ||
<DropdownMenu> | ||
<DropdownMenuTrigger asChild> | ||
<Button disabled={disabled} variant="outline"> | ||
{selectedParticipants.length === participants.length | ||
? 'All Participants Selected' | ||
: `${selectedParticipants.length} Participants Selected`} | ||
</Button> | ||
</DropdownMenuTrigger> | ||
<DropdownMenuContent className="w-auto"> | ||
<ScrollArea className="h-72 w-auto"> | ||
<DropdownMenuLabel>Participants</DropdownMenuLabel> | ||
<div className="flex flex-row gap-2 p-2"> | ||
<div className="text-sm"> | ||
{selectedParticipants.length} selected | ||
</div> | ||
<Button | ||
size="xs" | ||
onClick={() => setSelectedParticipants(participants)} | ||
> | ||
Select All | ||
</Button> | ||
<Button | ||
variant="destructive" | ||
size="xs" | ||
onClick={() => setSelectedParticipants([])} | ||
> | ||
Clear | ||
</Button> | ||
</div> | ||
|
||
<DropdownMenuSeparator /> | ||
|
||
{/* loop through all participants and render a dropdown menu checkbox item for them */} | ||
{participants.map((participant) => { | ||
return ( | ||
<DropdownMenuCheckboxItem | ||
key={participant.id} | ||
checked={selectedParticipants.includes(participant)} | ||
onCheckedChange={(checked) => { | ||
if (checked) { | ||
setSelectedParticipants([ | ||
...selectedParticipants, | ||
participant, | ||
]); | ||
} else { | ||
setSelectedParticipants( | ||
selectedParticipants.filter( | ||
(selectedParticipant) => | ||
selectedParticipant !== participant, | ||
), | ||
); | ||
} | ||
}} | ||
> | ||
{participant.identifier} | ||
</DropdownMenuCheckboxItem> | ||
); | ||
})} | ||
</ScrollArea> | ||
</DropdownMenuContent> | ||
</DropdownMenu> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
app/(dashboard)/dashboard/_components/ParticipantsTable/GetParticipantURLButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
'use client'; | ||
import type { Participant, Protocol } from '@prisma/client'; | ||
import { useState, useEffect } from 'react'; | ||
import { | ||
Dialog, | ||
DialogContent, | ||
DialogDescription, | ||
DialogHeader, | ||
DialogTitle, | ||
DialogTrigger, | ||
} from '~/components/ui/dialog'; | ||
import { | ||
Select, | ||
SelectContent, | ||
SelectItem, | ||
SelectTrigger, | ||
SelectValue, | ||
} from '~/components/ui/select'; | ||
|
||
import { Button } from '~/components/ui/Button'; | ||
import { api } from '~/trpc/client'; | ||
import { getBaseUrl } from '~/trpc/shared'; | ||
import { useToast } from '~/components/ui/use-toast'; | ||
|
||
export const GetParticipantURLButton = ({ | ||
participant, | ||
}: { | ||
participant: Participant; | ||
}) => { | ||
const { data: protocolData, isLoading: isLoadingProtocols } = | ||
api.protocol.get.all.useQuery(); | ||
const [protocols, setProtocols] = useState<Protocol[]>([]); | ||
const [selectedProtocol, setSelectedProtocol] = useState<Protocol>(); | ||
const [dialogOpen, setDialogOpen] = useState(false); | ||
|
||
useEffect(() => { | ||
if (protocolData) { | ||
setProtocols(protocolData); | ||
} | ||
}, [protocolData]); | ||
|
||
const { toast } = useToast(); | ||
|
||
const handleCopy = (url: string) => { | ||
if (url) { | ||
navigator.clipboard | ||
.writeText(url) | ||
.then(() => { | ||
toast({ | ||
description: 'Copied to clipboard', | ||
variant: 'success', | ||
duration: 3000, | ||
}); | ||
}) | ||
.catch(() => { | ||
toast({ | ||
title: 'Error', | ||
description: 'Could not copy text', | ||
variant: 'destructive', | ||
}); | ||
}); | ||
} | ||
setDialogOpen(false); | ||
}; | ||
|
||
return ( | ||
<Dialog open={dialogOpen}> | ||
<DialogTrigger asChild onClick={() => setDialogOpen(true)}> | ||
<Button size="sm">Get Participation URL</Button> | ||
</DialogTrigger> | ||
<DialogContent> | ||
<DialogHeader> | ||
<DialogTitle>Get Participation URL</DialogTitle> | ||
<DialogDescription> | ||
Generate a URL that can be shared with a participant to allow them | ||
to participate for a selected protocol. | ||
</DialogDescription> | ||
</DialogHeader> | ||
<> | ||
<div> | ||
<Select | ||
onValueChange={(value) => { | ||
const protocol = protocols.find( | ||
(protocol) => protocol.id === value, | ||
); | ||
|
||
setSelectedProtocol(protocol); | ||
handleCopy( | ||
`${getBaseUrl()}/onboard/${protocol?.id}/participantId=${ | ||
participant.id | ||
}`, | ||
); | ||
}} | ||
value={selectedProtocol?.id} | ||
disabled={isLoadingProtocols} | ||
> | ||
<SelectTrigger> | ||
<SelectValue placeholder="Select a Protocol..." /> | ||
</SelectTrigger> | ||
<SelectContent> | ||
{protocols?.map((protocol) => ( | ||
<SelectItem key={protocol.id} value={protocol.id}> | ||
{protocol.name} | ||
</SelectItem> | ||
))} | ||
</SelectContent> | ||
</Select> | ||
</div> | ||
</> | ||
</DialogContent> | ||
</Dialog> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
app/(dashboard)/dashboard/_components/ProtocolsTable/AnonymousRecruitmentSection.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
'use client'; | ||
import RecruitmentSwitch from '~/components/RecruitmentSwitch'; | ||
|
||
import { AnonymousRecruitmentModal } from '~/app/(dashboard)/dashboard/protocols/_components/AnonymousRecruitmentModal'; | ||
export const AnonymousRecruitmentSection = () => { | ||
return ( | ||
<div className="flex w-1/3 flex-col gap-4 rounded-lg border border-solid p-6"> | ||
<div> | ||
<h1 className="pb-2 text-xl">Anonymous Recruitment</h1> | ||
<p className="text-sm text-muted-foreground"> | ||
If anonymous recruitment is enabled, you may generate an anonymous | ||
participation URL. This URL can be shared with participants to allow | ||
them to self-enroll in your study. | ||
</p> | ||
</div> | ||
|
||
<div className="flex justify-between"> | ||
<p>Allow anonymous recruitment?</p> | ||
<RecruitmentSwitch /> | ||
</div> | ||
<AnonymousRecruitmentModal /> | ||
</div> | ||
); | ||
}; |
52 changes: 52 additions & 0 deletions
52
app/(dashboard)/dashboard/_components/ProtocolsTable/AnonymousRecruitmentURLButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
'use client'; | ||
|
||
import { Badge } from '~/components/ui/badge'; | ||
import { getBaseUrl } from '~/trpc/shared'; | ||
import { useToast } from '~/components/ui/use-toast'; | ||
import { Copy } from 'lucide-react'; | ||
import { api } from '~/trpc/client'; | ||
|
||
export const AnonymousRecruitmentURLButton = ({ | ||
protocolId, | ||
}: { | ||
protocolId: string; | ||
}) => { | ||
const { data: appSettings } = api.appSettings.get.useQuery(); | ||
const allowAnonymousRecruitment = !!appSettings?.allowAnonymousRecruitment; | ||
|
||
const { toast } = useToast(); | ||
const url = `${getBaseUrl()}/onboard/${protocolId}`; | ||
const handleCopyClick = () => { | ||
navigator.clipboard | ||
.writeText(url) | ||
.then(() => { | ||
toast({ | ||
description: 'Copied to clipboard', | ||
variant: 'success', | ||
duration: 3000, | ||
}); | ||
}) | ||
.catch((error) => { | ||
// eslint-disable-next-line no-console | ||
console.error('Could not copy text: ', error); | ||
toast({ | ||
title: 'Error', | ||
description: 'Could not copy text', | ||
variant: 'destructive', | ||
}); | ||
}); | ||
}; | ||
|
||
return ( | ||
<> | ||
{allowAnonymousRecruitment ? ( | ||
<Badge onClick={handleCopyClick} className="cursor-pointer"> | ||
<p className="w-36 truncate">{url}</p> | ||
<Copy className="ml-2 h-4 w-4" /> | ||
</Badge> | ||
) : ( | ||
<Badge variant="destructive">Disabled</Badge> | ||
)} | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.