mirror of
https://github.com/hanzoai/sign.git
synced 2026-11-02 23:26:15 +00:00
Emails, the signing page's title, the sealed PDF's reason, the sign-in refusal and the dialogs still said Hanzo Sign. The catalogs change with the strings, message for message, so every translation keeps its id.
204 lines
7.2 KiB
TypeScript
204 lines
7.2 KiB
TypeScript
import { Trans } from '@lingui/react/macro';
|
||
import { FileIcon } from 'lucide-react';
|
||
import { DateTime } from 'luxon';
|
||
import { Link, redirect } from 'react-router';
|
||
|
||
import { useOptionalSession } from '@hanzo/esign-lib/client-only/providers/session';
|
||
import { getPublicProfileByUrl } from '@hanzo/esign-lib/server-only/profile/get-public-profile-by-url';
|
||
import { formatAvatarUrl } from '@hanzo/esign-lib/utils/avatars';
|
||
import { extractInitials } from '@hanzo/esign-lib/utils/recipient-formatter';
|
||
import { formatDirectTemplatePath } from '@hanzo/esign-lib/utils/templates';
|
||
import { Avatar, AvatarFallback, AvatarImage } from '@hanzo/esign-ui/primitives/avatar';
|
||
import { Button } from '@hanzo/esign-ui/primitives/button';
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from '@hanzo/esign-ui/primitives/table';
|
||
import { Tooltip, TooltipContent, TooltipTrigger } from '@hanzo/esign-ui/primitives/tooltip';
|
||
|
||
import type { Route } from './+types/p.$url';
|
||
|
||
const BADGE_DATA = {
|
||
Premium: {
|
||
imageSrc: '/static/premium-user-badge.svg',
|
||
name: 'Premium',
|
||
},
|
||
EarlySupporter: {
|
||
imageSrc: '/static/early-supporter-badge.svg',
|
||
name: 'Early supporter',
|
||
},
|
||
};
|
||
|
||
export async function loader({ params }: Route.LoaderArgs) {
|
||
const { url: profileUrl } = params;
|
||
|
||
if (!profileUrl) {
|
||
throw redirect('/');
|
||
}
|
||
|
||
const publicProfile = await getPublicProfileByUrl({
|
||
profileUrl,
|
||
}).catch(() => null);
|
||
|
||
if (!publicProfile || !publicProfile.profile.enabled) {
|
||
throw new Response('Not Found', { status: 404 });
|
||
}
|
||
|
||
return {
|
||
publicProfile,
|
||
};
|
||
}
|
||
|
||
export default function PublicProfilePage({ loaderData }: Route.ComponentProps) {
|
||
const { publicProfile } = loaderData;
|
||
|
||
const { profile, templates } = publicProfile;
|
||
|
||
const { sessionData } = useOptionalSession();
|
||
const user = sessionData?.user;
|
||
|
||
return (
|
||
<div className="flex flex-col items-center justify-center py-4 sm:py-32">
|
||
<div className="flex flex-col items-center">
|
||
<Avatar className="h-24 w-24 border-2 border-solid dark:border-border">
|
||
{publicProfile.avatarImageId && (
|
||
<AvatarImage src={formatAvatarUrl(publicProfile.avatarImageId)} />
|
||
)}
|
||
|
||
<AvatarFallback className="text-sm text-gray-400">
|
||
{extractInitials(publicProfile.name)}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
|
||
<div className="mt-4 flex flex-row items-center justify-center">
|
||
<h2 className="text-xl font-semibold md:text-2xl">{publicProfile.name}</h2>
|
||
|
||
{publicProfile.badge && (
|
||
<Tooltip>
|
||
<TooltipTrigger>
|
||
<img
|
||
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">
|
||
<img
|
||
className="mt-0.5"
|
||
alt="Profile badge"
|
||
src={BADGE_DATA[publicProfile.badge.type].imageSrc}
|
||
height={24}
|
||
width={24}
|
||
/>
|
||
|
||
<div className="ml-2">
|
||
<p className="text-base font-semibold text-foreground">
|
||
{BADGE_DATA[publicProfile.badge.type].name}
|
||
</p>
|
||
<p className="mt-0.5 text-sm text-muted-foreground">
|
||
<Trans>
|
||
Since {DateTime.fromJSDate(publicProfile.badge.since).toFormat('LLL ‘yy')}
|
||
</Trans>
|
||
</p>
|
||
</div>
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
)}
|
||
</div>
|
||
|
||
<div className="mt-4 space-y-1 text-muted-foreground">
|
||
{(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>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{templates.length === 0 && (
|
||
<div className="mt-4 w-full max-w-xl border-t pt-4">
|
||
<p className="max-w-[60ch] whitespace-pre-wrap break-words text-center text-sm leading-relaxed text-muted-foreground">
|
||
<Trans>
|
||
It looks like {publicProfile.name} hasn't added any documents to their profile yet.
|
||
</Trans>{' '}
|
||
{!user?.id && (
|
||
<span className="mt-2 inline-block">
|
||
<Trans>
|
||
While waiting for them to do so you can create your own Hanzo eSign account and get
|
||
started with document signing right away.
|
||
</Trans>
|
||
</span>
|
||
)}
|
||
{'userId' in profile && user?.id === profile.userId && (
|
||
<span className="mt-2 inline-block">
|
||
<Trans>
|
||
Go to your{' '}
|
||
<Link to="/settings/public-profile" className="underline">
|
||
public profile settings
|
||
</Link>{' '}
|
||
to add documents.
|
||
</Trans>
|
||
</span>
|
||
)}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{templates.length > 0 && (
|
||
<div className="mt-8 w-full max-w-xl rounded-md border">
|
||
<Table className="w-full" overflowHidden>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className="w-full rounded-tl-md bg-neutral-50 dark:bg-neutral-700">
|
||
<Trans>Documents</Trans>
|
||
</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{templates.map((template) => (
|
||
<TableRow key={template.id}>
|
||
<TableCell className="flex flex-col justify-between overflow-hidden text-sm text-muted-foreground sm:flex-row">
|
||
<div className="flex flex-1 items-start justify-start gap-2">
|
||
<FileIcon
|
||
className="h-8 w-8 flex-shrink-0 text-muted-foreground/40"
|
||
strokeWidth={1.5}
|
||
/>
|
||
|
||
<div className="flex flex-1 flex-col gap-4 overflow-hidden md:flex-row md:items-start md:justify-between">
|
||
<div>
|
||
<p className="text-sm font-semibold leading-none text-foreground">
|
||
{template.publicTitle}
|
||
</p>
|
||
<p className="mt-1 line-clamp-3 max-w-[70ch] whitespace-normal text-xs text-muted-foreground">
|
||
{template.publicDescription}
|
||
</p>
|
||
</div>
|
||
|
||
<Button asChild className="w-fit">
|
||
<Link to={formatDirectTemplatePath(template.directLink.token)}>
|
||
<Trans>Sign</Trans>
|
||
</Link>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|