mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-11 22:54:45 -05:00
refactor(dashboard, participants, sidebar, users-tab, email): Clean up and enhance component logic and UI
- Removed unused code and comments from the Dashboard component to streamline functionality. - Updated the ParticipantsList component by removing unnecessary skeleton loaders and table headers for improved clarity. - Adjusted the Sidebar component to redirect to the main dashboard instead of the studies page when no study is selected. - Enhanced the UsersTab component to conditionally fetch invitations based on user permissions and improved role management UI. - Revamped the invitation email template for better presentation and clarity, including a more structured HTML format and improved messaging. - Updated role descriptions in the seed script for better accuracy and clarity.
This commit is contained in:
@@ -34,17 +34,6 @@ export default function Dashboard() {
|
|||||||
studyCount: data.length,
|
studyCount: data.length,
|
||||||
activeInvitationCount: 0
|
activeInvitationCount: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there's only one study and we're on the main dashboard, select it
|
|
||||||
if (data.length === 1) {
|
|
||||||
const study = {
|
|
||||||
...data[0],
|
|
||||||
createdAt: new Date(data[0].createdAt),
|
|
||||||
updatedAt: data[0].updatedAt ? new Date(data[0].updatedAt) : null
|
|
||||||
};
|
|
||||||
setActiveStudy(study);
|
|
||||||
router.push(`/dashboard/studies/${study.id}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching stats:", error);
|
console.error("Error fetching stats:", error);
|
||||||
toast({
|
toast({
|
||||||
@@ -55,7 +44,7 @@ export default function Dashboard() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [toast, router, setActiveStudy]);
|
}, [toast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStats();
|
fetchStats();
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ export default function ParticipantsList() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead><Skeleton className="h-4 w-[40px]" /></TableHead>
|
|
||||||
<TableHead><Skeleton className="h-4 w-[120px]" /></TableHead>
|
<TableHead><Skeleton className="h-4 w-[120px]" /></TableHead>
|
||||||
<TableHead><Skeleton className="h-4 w-[100px]" /></TableHead>
|
<TableHead><Skeleton className="h-4 w-[100px]" /></TableHead>
|
||||||
<TableHead className="w-[100px]"><Skeleton className="h-4 w-[60px]" /></TableHead>
|
<TableHead className="w-[100px]"><Skeleton className="h-4 w-[60px]" /></TableHead>
|
||||||
@@ -175,7 +174,6 @@ export default function ParticipantsList() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{[1, 2, 3].map((i) => (
|
{[1, 2, 3].map((i) => (
|
||||||
<TableRow key={i}>
|
<TableRow key={i}>
|
||||||
<TableCell><Skeleton className="h-4 w-[30px]" /></TableCell>
|
|
||||||
<TableCell><Skeleton className="h-4 w-[150px]" /></TableCell>
|
<TableCell><Skeleton className="h-4 w-[150px]" /></TableCell>
|
||||||
<TableCell><Skeleton className="h-4 w-[100px]" /></TableCell>
|
<TableCell><Skeleton className="h-4 w-[100px]" /></TableCell>
|
||||||
<TableCell><Skeleton className="h-8 w-8" /></TableCell>
|
<TableCell><Skeleton className="h-8 w-8" /></TableCell>
|
||||||
@@ -250,7 +248,6 @@ export default function ParticipantsList() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>ID</TableHead>
|
|
||||||
<TableHead>Name</TableHead>
|
<TableHead>Name</TableHead>
|
||||||
<TableHead>Added</TableHead>
|
<TableHead>Added</TableHead>
|
||||||
{canDeleteParticipant && <TableHead className="w-[100px]">Actions</TableHead>}
|
{canDeleteParticipant && <TableHead className="w-[100px]">Actions</TableHead>}
|
||||||
@@ -259,7 +256,6 @@ export default function ParticipantsList() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{participants.map((participant) => (
|
{participants.map((participant) => (
|
||||||
<TableRow key={participant.id}>
|
<TableRow key={participant.id}>
|
||||||
<TableCell>{participant.id}</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{canViewNames ? participant.name : `Participant ${participant.id}`}
|
{canViewNames ? participant.name : `Participant ${participant.id}`}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export function Sidebar() {
|
|||||||
const handleStudyChange = (value: string) => {
|
const handleStudyChange = (value: string) => {
|
||||||
if (value === "all") {
|
if (value === "all") {
|
||||||
setActiveStudy(null);
|
setActiveStudy(null);
|
||||||
router.push("/dashboard/studies");
|
router.push("/dashboard");
|
||||||
} else {
|
} else {
|
||||||
const study = studies.find(s => s.id.toString() === value);
|
const study = studies.find(s => s.id.toString() === value);
|
||||||
if (study) {
|
if (study) {
|
||||||
@@ -131,7 +131,6 @@ export function Sidebar() {
|
|||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{activeStudy?.title || "All Studies"}
|
{activeStudy?.title || "All Studies"}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
||||||
</div>
|
</div>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="sidebar-dropdown-content">
|
<SelectContent className="sidebar-dropdown-content">
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
|||||||
const fetchInvitations = useCallback(async () => {
|
const fetchInvitations = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/invitations?studyId=${studyId}`);
|
const response = await fetch(`/api/invitations?studyId=${studyId}`);
|
||||||
|
if (response.status === 403) {
|
||||||
|
// Silently handle 403 errors as they're expected for researchers
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!response.ok) throw new Error("Failed to fetch invitations");
|
if (!response.ok) throw new Error("Failed to fetch invitations");
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setInvitations(data.data || []);
|
setInvitations(data.data || []);
|
||||||
@@ -109,9 +113,7 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
|||||||
const response = await fetch("/api/roles");
|
const response = await fetch("/api/roles");
|
||||||
if (!response.ok) throw new Error("Failed to fetch roles");
|
if (!response.ok) throw new Error("Failed to fetch roles");
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setRoles(data.filter((role: Role) =>
|
setRoles(data);
|
||||||
!['admin'].includes(role.name)
|
|
||||||
));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching roles:", error);
|
console.error("Error fetching roles:", error);
|
||||||
toast({
|
toast({
|
||||||
@@ -125,15 +127,15 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
|||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
const promises = [fetchUsers(), fetchRoles()];
|
||||||
fetchUsers(),
|
if (canManageRoles) {
|
||||||
fetchInvitations(),
|
promises.push(fetchInvitations());
|
||||||
fetchRoles(),
|
}
|
||||||
]);
|
await Promise.all(promises);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [fetchUsers, fetchInvitations, fetchRoles]);
|
}, [fetchUsers, fetchInvitations, fetchRoles, canManageRoles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -218,10 +220,15 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle>Study Members</CardTitle>
|
<CardTitle>Study Members</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Manage users and their roles in this study
|
{canManageRoles ? 'Manage users and their roles in this study' : 'View study members'}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{canManageRoles && <InviteUserDialog studyId={studyId} onInviteSent={fetchInvitations} />}
|
{canManageRoles && (
|
||||||
|
<InviteUserDialog
|
||||||
|
studyId={studyId}
|
||||||
|
onInviteSent={() => canManageRoles && fetchInvitations()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -248,14 +255,15 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{user.email}</TableCell>
|
<TableCell>{user.email}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{canManageRoles ? (
|
<Select
|
||||||
<Select
|
value={user.roles[0]?.id.toString()}
|
||||||
value={user.roles[0]?.id.toString()}
|
onValueChange={(value) => handleRoleChange(user.id, value)}
|
||||||
onValueChange={(value) => handleRoleChange(user.id, value)}
|
disabled={!canManageRoles}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
{canManageRoles && (
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{roles.map((role) => (
|
{roles.map((role) => (
|
||||||
<SelectItem key={role.id} value={role.id.toString()}>
|
<SelectItem key={role.id} value={role.id.toString()}>
|
||||||
@@ -263,10 +271,8 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
)}
|
||||||
) : (
|
</Select>
|
||||||
<span>{formatRoleName(user.roles[0]?.name || '')}</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
@@ -275,7 +281,7 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{pendingInvitations.length > 0 && (
|
{canManageRoles && pendingInvitations.length > 0 && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Pending Invitations</CardTitle>
|
<CardTitle>Pending Invitations</CardTitle>
|
||||||
@@ -290,7 +296,7 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
|||||||
<TableHead>Email</TableHead>
|
<TableHead>Email</TableHead>
|
||||||
<TableHead>Role</TableHead>
|
<TableHead>Role</TableHead>
|
||||||
<TableHead>Expires</TableHead>
|
<TableHead>Expires</TableHead>
|
||||||
{canManageRoles && <TableHead className="w-[100px]">Actions</TableHead>}
|
<TableHead className="w-[100px]">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -299,33 +305,31 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
|||||||
<TableCell>{invitation.email}</TableCell>
|
<TableCell>{invitation.email}</TableCell>
|
||||||
<TableCell>{formatRoleName(invitation.roleName)}</TableCell>
|
<TableCell>{formatRoleName(invitation.roleName)}</TableCell>
|
||||||
<TableCell>{new Date(invitation.expiresAt).toLocaleDateString()}</TableCell>
|
<TableCell>{new Date(invitation.expiresAt).toLocaleDateString()}</TableCell>
|
||||||
{canManageRoles && (
|
<TableCell>
|
||||||
<TableCell>
|
<AlertDialog>
|
||||||
<AlertDialog>
|
<AlertDialogTrigger asChild>
|
||||||
<AlertDialogTrigger asChild>
|
<Button variant="ghost" size="icon">
|
||||||
<Button variant="ghost" size="icon">
|
<Trash2Icon className="h-4 w-4" />
|
||||||
<Trash2Icon className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
</AlertDialogTrigger>
|
||||||
</AlertDialogTrigger>
|
<AlertDialogContent>
|
||||||
<AlertDialogContent>
|
<AlertDialogHeader>
|
||||||
<AlertDialogHeader>
|
<AlertDialogTitle>Delete Invitation</AlertDialogTitle>
|
||||||
<AlertDialogTitle>Delete Invitation</AlertDialogTitle>
|
<AlertDialogDescription>
|
||||||
<AlertDialogDescription>
|
Are you sure you want to delete this invitation? This action cannot be undone.
|
||||||
Are you sure you want to delete this invitation? This action cannot be undone.
|
</AlertDialogDescription>
|
||||||
</AlertDialogDescription>
|
</AlertDialogHeader>
|
||||||
</AlertDialogHeader>
|
<AlertDialogFooter>
|
||||||
<AlertDialogFooter>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogAction
|
||||||
<AlertDialogAction
|
onClick={() => handleDeleteInvitation(invitation.id)}
|
||||||
onClick={() => handleDeleteInvitation(invitation.id)}
|
>
|
||||||
>
|
Delete
|
||||||
Delete
|
</AlertDialogAction>
|
||||||
</AlertDialogAction>
|
</AlertDialogFooter>
|
||||||
</AlertDialogFooter>
|
</AlertDialogContent>
|
||||||
</AlertDialogContent>
|
</AlertDialog>
|
||||||
</AlertDialog>
|
</TableCell>
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ function getRoleDescription(roleKey: string): string {
|
|||||||
PRINCIPAL_INVESTIGATOR:
|
PRINCIPAL_INVESTIGATOR:
|
||||||
"Lead researcher responsible for study design and oversight",
|
"Lead researcher responsible for study design and oversight",
|
||||||
RESEARCHER:
|
RESEARCHER:
|
||||||
"Study team member with data collection and analysis capabilities",
|
"Study team member with access to anonymized data and experiment monitoring capabilities",
|
||||||
WIZARD: "Operator controlling robot behavior during experiments",
|
WIZARD: "Operator controlling robot behavior during experiments",
|
||||||
OBSERVER: "Team member observing and annotating experiments",
|
OBSERVER: "Team member observing and annotating experiments",
|
||||||
ASSISTANT: "Support staff with limited view access",
|
ASSISTANT: "Support staff with limited view access",
|
||||||
|
|||||||
183
src/lib/email.ts
183
src/lib/email.ts
@@ -33,32 +33,179 @@ export async function sendInvitationEmail({
|
|||||||
token,
|
token,
|
||||||
}: SendInvitationEmailParams) {
|
}: SendInvitationEmailParams) {
|
||||||
const inviteUrl = `${process.env.NEXT_PUBLIC_APP_URL}/invite/accept/${token}`;
|
const inviteUrl = `${process.env.NEXT_PUBLIC_APP_URL}/invite/accept/${token}`;
|
||||||
|
const roleDisplay = role
|
||||||
|
.split('_')
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<h2>You've been invited to join HRIStudio</h2>
|
<!DOCTYPE html>
|
||||||
<p>${inviterName} has invited you to join their study "${studyTitle}" as a ${role}.</p>
|
<html>
|
||||||
<p>HRIStudio is a platform for managing human-robot interaction studies and Wizard-of-Oz experiments.</p>
|
<head>
|
||||||
<p>Click the button below to accept the invitation and join the study:</p>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<a href="${inviteUrl}" style="
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
display: inline-block;
|
<title>HRIStudio Invitation</title>
|
||||||
background-color: #0070f3;
|
<style>
|
||||||
color: white;
|
@media only screen and (max-width: 620px) {
|
||||||
padding: 12px 24px;
|
.content {
|
||||||
text-decoration: none;
|
padding: 24px !important;
|
||||||
border-radius: 6px;
|
}
|
||||||
margin: 16px 0;
|
}
|
||||||
">Accept Invitation</a>
|
</style>
|
||||||
<p>Or copy and paste this URL into your browser:</p>
|
</head>
|
||||||
<p>${inviteUrl}</p>
|
<body style="
|
||||||
<p>This invitation will expire in 7 days.</p>
|
background-color: #f3f4f6;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="margin: 0; padding: 20px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
">
|
||||||
|
<!-- Logo -->
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 24px;">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; margin-bottom: 8px;">
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="#6b7280"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
style="margin-right: 4px;"
|
||||||
|
>
|
||||||
|
<path d="M12 8V4H8" />
|
||||||
|
<rect width="16" height="12" x="4" y="8" rx="2" />
|
||||||
|
<path d="M2 14h2" />
|
||||||
|
<path d="M20 14h2" />
|
||||||
|
<path d="M15 13v2" />
|
||||||
|
<path d="M9 13v2" />
|
||||||
|
</svg>
|
||||||
|
<h1 style="
|
||||||
|
color: #111827;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
">
|
||||||
|
<span style="font-weight: 800;">HRI</span>
|
||||||
|
<span style="font-weight: 400;">Studio</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<p style="
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
">A platform for managing human-robot interaction studies</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Main Content -->
|
||||||
|
<tr>
|
||||||
|
<td class="content" style="
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
">
|
||||||
|
<h2 style="
|
||||||
|
color: #111827;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
">You've been invited to join a research study</h2>
|
||||||
|
|
||||||
|
<p style="margin: 16px 0; color: #374151;">
|
||||||
|
${inviterName} has invited you to join <strong>"${studyTitle}"</strong> as a ${roleDisplay}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin: 16px 0; color: #374151;">
|
||||||
|
HRIStudio helps research teams manage human-robot interaction studies and conduct Wizard-of-Oz experiments efficiently.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Button -->
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 24px 0;">
|
||||||
|
<a href="${inviteUrl}" style="
|
||||||
|
background-color: #2563eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #ffffff;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 12px 24px;
|
||||||
|
text-decoration: none;
|
||||||
|
">Accept Invitation</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="
|
||||||
|
margin: 16px 0 0;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
">
|
||||||
|
If you're having trouble with the button above, copy and paste the URL below into your web browser:
|
||||||
|
</p>
|
||||||
|
<p style="
|
||||||
|
margin: 8px 0;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
">
|
||||||
|
${inviteUrl}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="
|
||||||
|
margin: 24px 0 0;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: italic;
|
||||||
|
">
|
||||||
|
This invitation will expire in 7 days.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 24px; text-align: center;">
|
||||||
|
<p style="
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
">
|
||||||
|
This is an automated message from HRIStudio. Please do not reply to this email.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const text = `
|
const text = `
|
||||||
You've been invited to join HRIStudio
|
You've been invited to join HRIStudio
|
||||||
|
|
||||||
${inviterName} has invited you to join their study "${studyTitle}" as a ${role}.
|
${inviterName} has invited you to join "${studyTitle}" as a ${roleDisplay}.
|
||||||
|
|
||||||
HRIStudio is a platform for managing human-robot interaction studies and Wizard-of-Oz experiments.
|
HRIStudio helps research teams manage human-robot interaction studies and conduct Wizard-of-Oz experiments efficiently.
|
||||||
|
|
||||||
To accept the invitation, visit this URL:
|
To accept the invitation, visit this URL:
|
||||||
${inviteUrl}
|
${inviteUrl}
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ export const ROLE_PERMISSIONS: Record<RoleCode, Array<keyof typeof PERMISSIONS>>
|
|||||||
|
|
||||||
RESEARCHER: [
|
RESEARCHER: [
|
||||||
'VIEW_STUDY',
|
'VIEW_STUDY',
|
||||||
'CREATE_PARTICIPANT',
|
|
||||||
'EDIT_PARTICIPANT',
|
|
||||||
'VIEW_ROBOT_STATUS',
|
'VIEW_ROBOT_STATUS',
|
||||||
'VIEW_EXPERIMENT',
|
'VIEW_EXPERIMENT',
|
||||||
'VIEW_EXPERIMENT_DATA',
|
'VIEW_EXPERIMENT_DATA',
|
||||||
|
|||||||
Reference in New Issue
Block a user