This commit is contained in:
Mythie
2025-01-02 15:33:37 +11:00
committed by David Nguyen
parent 9183f668d3
commit f7a98180d7
413 changed files with 29538 additions and 1606 deletions

View File

@ -0,0 +1,62 @@
import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { GetSignerConversionMonthlyResult } from '@documenso/lib/server-only/user/get-signer-conversion';
export type AdminStatsSignerConversionChartProps = {
className?: string;
title: string;
cummulative?: boolean;
data: GetSignerConversionMonthlyResult;
};
export const AdminStatsSignerConversionChart = ({
className,
data,
title,
cummulative = false,
}: AdminStatsSignerConversionChartProps) => {
const formattedData = [...data].reverse().map(({ month, count, cume_count }) => {
return {
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'),
count: Number(count),
signed_count: Number(cume_count),
};
});
return (
<div className={className}>
<div className="border-border flex flex-1 flex-col justify-center rounded-2xl border p-6 pl-2">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">{title}</h3>
</div>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
formatter={(value, name) => [
Number(value).toLocaleString('en-US'),
name === 'Recipients',
]}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/>
<Bar
dataKey={cummulative ? 'signed_count' : 'count'}
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label="Recipients"
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};

View File

@ -0,0 +1,93 @@
import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { TooltipProps } from 'recharts';
import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
export type AdminStatsUsersWithDocumentsChartProps = {
className?: string;
title: string;
data: GetUserWithDocumentMonthlyGrowth;
completed?: boolean;
tooltip?: string;
};
const CustomTooltip = ({
active,
payload,
label,
tooltip,
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
if (active && payload && payload.length) {
return (
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
<p className="">{label}</p>
<p className="text-documenso">
{`${tooltip} : `}
<span className="text-black">{payload[0].value}</span>
</p>
</div>
);
}
return null;
};
export const AdminStatsUsersWithDocumentsChart = ({
className,
data,
title,
completed = false,
tooltip,
}: AdminStatsUsersWithDocumentsChartProps) => {
const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => {
return [...data].reverse().map(({ month, count, signed_count }) => {
const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL');
if (completed) {
return {
month: formattedMonth,
count: Number(signed_count),
};
} else {
return {
month: formattedMonth,
count: Number(count),
};
}
});
};
return (
<div className={className}>
<div className="border-border flex flex-1 flex-col justify-center rounded-2xl border p-6 pl-2">
<div className="mb-6 flex h-12 px-4">
<h3 className="text-lg font-semibold">{title}</h3>
</div>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData(data, completed)}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip
content={<CustomTooltip tooltip={tooltip} />}
labelStyle={{
color: 'hsl(var(--primary-foreground))',
}}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/>
<Bar
dataKey="count"
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label={tooltip}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};

View File

@ -0,0 +1,85 @@
import * as React from 'react';
import { Trans } from '@lingui/macro';
import { Role } from '@prisma/client';
import { Check, ChevronsUpDown } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from '@documenso/ui/primitives/command';
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
type ComboboxProps = {
listValues: string[];
onChange: (_values: string[]) => void;
};
const MultiSelectRoleCombobox = ({ listValues, onChange }: ComboboxProps) => {
const [open, setOpen] = React.useState(false);
const [selectedValues, setSelectedValues] = React.useState<string[]>([]);
const dbRoles = Object.values(Role);
React.useEffect(() => {
setSelectedValues(listValues);
}, [listValues]);
const allRoles = [...new Set([...dbRoles, ...selectedValues])];
const handleSelect = (currentValue: string) => {
let newSelectedValues;
if (selectedValues.includes(currentValue)) {
newSelectedValues = selectedValues.filter((value) => value !== currentValue);
} else {
newSelectedValues = [...selectedValues, currentValue];
}
setSelectedValues(newSelectedValues);
onChange(newSelectedValues);
setOpen(false);
};
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-[200px] justify-between"
>
{selectedValues.length > 0 ? selectedValues.join(', ') : 'Select values...'}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder={selectedValues.join(', ')} />
<CommandEmpty>
<Trans>No value found.</Trans>
</CommandEmpty>
<CommandGroup>
{allRoles.map((value: string, i: number) => (
<CommandItem key={i} onSelect={() => handleSelect(value)}>
<Check
className={cn(
'mr-2 h-4 w-4',
selectedValues.includes(value) ? 'opacity-100' : 'opacity-0',
)}
/>
{value}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
};
export { MultiSelectRoleCombobox };

View File

@ -0,0 +1,34 @@
import type { HTMLAttributes } from 'react';
import { Trans } from '@lingui/macro';
import { Link } from 'react-router';
import { cn } from '@documenso/ui/lib/utils';
export type SigningDisclosureProps = HTMLAttributes<HTMLParagraphElement>;
export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProps) => {
return (
<p className={cn('text-muted-foreground text-xs', className)} {...props}>
<Trans>
By proceeding with your electronic signature, you acknowledge and consent that it will be
used to sign the given document and holds the same legal validity as a handwritten
signature. By completing the electronic signing process, you affirm your understanding and
acceptance of these conditions.
</Trans>
<span className="mt-2 block">
<Trans>
Read the full{' '}
<Link
className="text-documenso-700 underline"
href="/articles/signature-disclosure"
target="_blank"
>
signature disclosure
</Link>
.
</Trans>
</span>
</p>
);
};

View File

@ -0,0 +1,55 @@
import { useCallback, useEffect, useState } from 'react';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { ClaimPublicProfileDialogForm } from '~/components/forms/public-profile-claim-dialog';
import { useAuth } from '~/providers/auth';
export const UpcomingProfileClaimTeaser = () => {
const { user } = useAuth();
const { _ } = useLingui();
const { toast } = useToast();
const [open, setOpen] = useState(false);
const [claimed, setClaimed] = useState(false);
const onOpenChange = useCallback(
(open: boolean) => {
if (!open && !claimed) {
toast({
title: _(msg`Claim your profile later`),
description: _(
msg`You can claim your profile later on by going to your profile settings!`,
),
});
}
setOpen(open);
localStorage.setItem('app.hasShownProfileClaimDialog', 'true');
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[claimed, toast],
);
useEffect(() => {
const hasShownProfileClaimDialog =
localStorage.getItem('app.hasShownProfileClaimDialog') === 'true';
if (!user.url && !hasShownProfileClaimDialog) {
onOpenChange(true);
}
}, [onOpenChange, user.url]);
return (
<ClaimPublicProfileDialogForm
open={open}
onOpenChange={onOpenChange}
onClaimed={() => setClaimed(true)}
user={user}
/>
);
};