Files
sign/apps/remix/app/routes/_profile+/p.$url.tsx

204 lines
7.1 KiB
TypeScript
Raw Normal View History

2025-01-02 15:33:37 +11:00
import { Trans } from '@lingui/react/macro';
2024-06-06 14:46:48 +10:00
import { FileIcon } from 'lucide-react';
2024-06-08 13:22:51 +10:00
import { DateTime } from 'luxon';
2025-01-02 15:33:37 +11:00
import { Link, redirect } from 'react-router';
2024-06-06 14:46:48 +10:00
2025-01-02 15:33:37 +11:00
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
2024-06-06 14:46:48 +10:00
import { getPublicProfileByUrl } from '@documenso/lib/server-only/profile/get-public-profile-by-url';
2025-01-02 15:33:37 +11:00
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
2024-06-08 13:22:51 +10:00
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
2024-06-06 14:46:48 +10:00
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
2024-06-27 21:50:42 +10:00
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
2024-06-06 14:46:48 +10:00
import { Button } from '@documenso/ui/primitives/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@documenso/ui/primitives/table';
2024-06-08 13:22:51 +10:00
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
2024-06-06 14:46:48 +10:00
2025-01-02 15:33:37 +11:00
import type { Route } from './+types/p.$url';
2024-06-06 14:46:48 +10:00
2024-06-08 13:22:51 +10:00
const BADGE_DATA = {
Premium: {
imageSrc: '/static/premium-user-badge.svg',
name: 'Premium',
},
EarlySupporter: {
imageSrc: '/static/early-supporter-badge.svg',
name: 'Early supporter',
},
};
2025-01-02 15:33:37 +11:00
export async function loader({ params }: Route.LoaderArgs) {
2024-06-06 14:46:48 +10:00
const { url: profileUrl } = params;
if (!profileUrl) {
2025-01-02 15:33:37 +11:00
throw redirect('/');
2024-06-06 14:46:48 +10:00
}
const publicProfile = await getPublicProfileByUrl({
profileUrl,
}).catch(() => null);
if (!publicProfile || !publicProfile.profile.enabled) {
2025-01-02 15:33:37 +11:00
throw new Response('Not Found', { status: 404 });
2024-06-06 14:46:48 +10:00
}
2025-01-02 15:33:37 +11:00
return {
publicProfile,
};
}
export default function PublicProfilePage({ loaderData }: Route.ComponentProps) {
const { publicProfile } = loaderData;
2024-06-27 21:50:42 +10:00
2024-06-06 14:46:48 +10:00
const { profile, templates } = publicProfile;
2025-02-17 22:46:36 +11:00
const { sessionData } = useOptionalSession();
const user = sessionData?.user;
2025-01-02 15:33:37 +11:00
2024-06-06 14:46:48 +10:00
return (
<div className="flex flex-col items-center justify-center py-4 sm:py-32">
<div className="flex flex-col items-center">
2024-06-08 13:22:51 +10:00
<Avatar className="dark:border-border h-24 w-24 border-2 border-solid">
2024-06-27 21:50:42 +10:00
{publicProfile.avatarImageId && (
2025-01-02 15:33:37 +11:00
<AvatarImage src={formatAvatarUrl(publicProfile.avatarImageId)} />
2024-06-27 21:50:42 +10:00
)}
2024-06-10 20:07:32 +10:00
<AvatarFallback className="text-sm text-gray-400">
2024-06-08 13:22:51 +10:00
{extractInitials(publicProfile.name)}
2024-06-06 14:46:48 +10:00
</AvatarFallback>
</Avatar>
<div className="mt-4 flex flex-row items-center justify-center">
2024-06-27 21:50:42 +10:00
<h2 className="text-xl font-semibold md:text-2xl">{publicProfile.name}</h2>
2024-06-06 14:46:48 +10:00
{publicProfile.badge && (
2024-06-08 13:22:51 +10:00
<Tooltip>
<TooltipTrigger>
2025-01-02 15:33:37 +11:00
<img
2024-06-08 13:22:51 +10:00
className="ml-2 flex items-center justify-center"
alt="Profile badge"
src={BADGE_DATA[publicProfile.badge.type].imageSrc}
height={24}
width={24}
/>
</TooltipTrigger>
<TooltipContent className="flex flex-row items-start py-2 !pl-3 !pr-3.5">
2025-01-02 15:33:37 +11:00
<img
2024-06-08 13:22:51 +10:00
className="mt-0.5"
alt="Profile badge"
src={BADGE_DATA[publicProfile.badge.type].imageSrc}
height={24}
width={24}
/>
<div className="ml-2">
2024-06-27 21:50:42 +10:00
<p className="text-foreground text-base font-semibold">
2024-06-08 13:22:51 +10:00
{BADGE_DATA[publicProfile.badge.type].name}
</p>
<p className="text-muted-foreground mt-0.5 text-sm">
2024-08-27 20:34:39 +09:00
<Trans>
Since {DateTime.fromJSDate(publicProfile.badge.since).toFormat('LLL yy')}
</Trans>
2024-06-08 13:22:51 +10:00
</p>
</div>
</TooltipContent>
</Tooltip>
2024-06-06 14:46:48 +10:00
)}
</div>
2024-06-27 21:50:42 +10:00
<div className="text-muted-foreground mt-4 space-y-1">
{(profile.bio ?? '').split('\n').map((line, index) => (
<p
key={index}
className="max-w-[60ch] whitespace-pre-wrap break-words text-center text-sm"
>
{line}
</p>
))}
2024-06-06 14:46:48 +10:00
</div>
</div>
2024-06-27 21:50:42 +10:00
{templates.length === 0 && (
<div className="mt-4 w-full max-w-xl border-t pt-4">
<p className="text-muted-foreground max-w-[60ch] whitespace-pre-wrap break-words text-center text-sm leading-relaxed">
2024-08-27 20:34:39 +09:00
<Trans>
It looks like {publicProfile.name} hasn't added any documents to their profile yet.
</Trans>{' '}
2024-06-27 21:50:42 +10:00
{!user?.id && (
<span className="mt-2 inline-block">
2024-08-27 20:34:39 +09:00
<Trans>
While waiting for them to do so you can create your own Documenso account and get
started with document signing right away.
</Trans>
2024-06-27 21:50:42 +10:00
</span>
)}
{'userId' in profile && user?.id === profile.userId && (
<span className="mt-2 inline-block">
2024-08-27 20:34:39 +09:00
<Trans>
Go to your{' '}
2025-01-02 15:33:37 +11:00
<Link to="/settings/public-profile" className="underline">
2024-08-27 20:34:39 +09:00
public profile settings
</Link>{' '}
to add documents.
</Trans>
2024-06-27 21:50:42 +10:00
</span>
)}
</p>
</div>
)}
2024-06-06 14:46:48 +10:00
{templates.length > 0 && (
2024-06-27 21:50:42 +10:00
<div className="mt-8 w-full max-w-xl rounded-md border">
2024-06-06 14:46:48 +10:00
<Table className="w-full" overflowHidden>
<TableHeader>
<TableRow>
<TableHead className="w-full rounded-tl-md bg-neutral-50 dark:bg-neutral-700">
2024-08-27 20:34:39 +09:00
<Trans>Documents</Trans>
2024-06-06 14:46:48 +10:00
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{templates.map((template) => (
<TableRow key={template.id}>
<TableCell className="text-muted-foreground flex flex-col justify-between overflow-hidden text-sm sm:flex-row">
2024-06-27 21:50:42 +10:00
<div className="flex flex-1 items-start justify-start gap-2">
2024-06-06 14:46:48 +10:00
<FileIcon
className="text-muted-foreground/40 h-8 w-8 flex-shrink-0"
strokeWidth={1.5}
/>
2024-06-27 21:50:42 +10:00
<div className="flex flex-1 flex-col gap-4 overflow-hidden md:flex-row md:items-start md:justify-between">
2024-06-06 14:46:48 +10:00
<div>
2024-06-27 21:50:42 +10:00
<p className="text-foreground text-sm font-semibold leading-none">
{template.publicTitle}
</p>
<p className="text-muted-foreground mt-1 line-clamp-3 max-w-[70ch] whitespace-normal text-xs">
2024-06-06 14:46:48 +10:00
{template.publicDescription}
</p>
</div>
<Button asChild className="w-20">
2025-01-02 15:33:37 +11:00
<Link to={formatDirectTemplatePath(template.directLink.token)}>
2024-08-27 20:34:39 +09:00
<Trans>Sign</Trans>
2024-06-06 14:46:48 +10:00
</Link>
</Button>
</div>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
);
}