wip: refresh design

This commit is contained in:
Mythie
2023-06-09 18:21:18 +10:00
parent 76b2fb5edd
commit 159bcade7b
432 changed files with 19640 additions and 29359 deletions

View File

@ -0,0 +1,151 @@
'use client';
import { Variants, motion } from 'framer-motion';
import { Plus } from 'lucide-react';
import { useDropzone } from 'react-dropzone';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
const DocumentDropzoneContainerVariants: Variants = {
initial: {
scale: 1,
},
animate: {
scale: 1,
},
hover: {
transition: {
staggerChildren: 0.05,
},
},
};
const DocumentDropzoneCardLeftVariants: Variants = {
initial: {
x: 40,
y: -10,
rotate: -14,
},
animate: {
x: 40,
y: -10,
rotate: -14,
},
hover: {
x: -25,
y: -25,
rotate: -22,
},
};
const DocumentDropzoneCardRightVariants: Variants = {
initial: {
x: -40,
y: -10,
rotate: 14,
},
animate: {
x: -40,
y: -10,
rotate: 14,
},
hover: {
x: 25,
y: -25,
rotate: 22,
},
};
const DocumentDropzoneCardCenterVariants: Variants = {
initial: {
x: 0,
y: 0,
},
animate: {
x: 0,
y: 0,
},
hover: {
x: 0,
y: -25,
},
};
export type DocumentDropzoneProps = {
className: string;
onDrop?: (_file: File) => void | Promise<void>;
[key: string]: unknown;
};
export const DocumentDropzone = ({ className, onDrop, ...props }: DocumentDropzoneProps) => {
const { getRootProps, getInputProps } = useDropzone({
accept: {
'application/pdf': ['.pdf'],
},
multiple: false,
onDrop: ([acceptedFile]) => {
if (acceptedFile && onDrop) {
onDrop(acceptedFile);
}
},
});
return (
<motion.div
className={cn('flex', className)}
variants={DocumentDropzoneContainerVariants}
initial="initial"
animate="animate"
whileHover="hover"
>
<Card
role="button"
className={cn('flex flex-1 cursor-pointer flex-col items-center justify-center', className)}
gradient={true}
degrees={120}
{...getRootProps()}
{...props}
>
<CardContent className="text-muted-foreground/40 flex flex-col items-center justify-center p-6">
{/* <FilePlus strokeWidth="1px" className="h-16 w-16"/> */}
<div className="flex">
<motion.div
className="border-muted-foreground/20 group-hover:border-primary/80 z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneCardLeftVariants}
>
<div className="bg-muted-foreground/20 group-hover:bg-primary h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-primary h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-primary h-2 w-full rounded-[2px]" />
</motion.div>
<motion.div
className="border-muted-foreground/20 group-hover:border-primary/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneCardCenterVariants}
>
<Plus
strokeWidth="2px"
className="text-muted-foreground/20 group-hover:text-primary h-12 w-12"
/>
</motion.div>
<motion.div
className="border-muted-foreground/20 group-hover:border-primary/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneCardRightVariants}
>
<div className="bg-muted-foreground/20 group-hover:bg-primary h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-primary h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-primary h-2 w-full rounded-[2px]" />
</motion.div>
</div>
<input {...getInputProps()} />
<p className="group-hover:text-primary mt-8 font-medium">Add a document</p>
<p className="mt-1 text-sm">Drag & drop your document here.</p>
</CardContent>
</Card>
</motion.div>
);
};

View File

@ -0,0 +1,43 @@
'use client';
import { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@documenso/ui/lib/utils';
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
const pathname = usePathname();
return (
<div className={cn('ml-8 hidden flex-1 gap-x-6 md:flex', className)} {...props}>
<Link
href="/dashboard"
className={cn('font-medium leading-5 text-[#A1A1AA] hover:opacity-80', {
'text-primary-foreground': pathname?.startsWith('/dashboard'),
})}
>
Dashboard
</Link>
<Link
href="/documents"
className={cn('font-medium leading-5 text-[#A1A1AA] hover:opacity-80', {
'text-primary-foreground': pathname?.startsWith('/documents'),
})}
>
Documents
</Link>
{/* <Link
href="/settings/profile"
className={cn('font-medium leading-5 text-[#A1A1AA] hover:opacity-80', {
'text-primary-foreground': pathname?.startsWith('/settings'),
})}
>
Settings
</Link> */}
</div>
);
};

View File

@ -0,0 +1,48 @@
'use client';
import { HTMLAttributes } from 'react';
import Link from 'next/link';
import { Menu } from 'lucide-react';
import { User } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Logo } from '~/components/branding/logo';
import { DesktopNav } from './desktop-nav';
import { ProfileDropdown } from './profile-dropdown';
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
user: User;
};
export const Header = ({ className, user, ...props }: HeaderProps) => {
return (
<header
className={cn(
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-40 flex h-16 w-full items-center border-b backdrop-blur',
className,
)}
{...props}
>
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
<Link href="/">
<Logo className="h-6 w-auto" />
</Link>
<DesktopNav />
<div className="flex gap-x-4">
<ProfileDropdown user={user} />
<Button variant="outline" size="sm" className="h-10 w-10 p-0.5 md:hidden">
<Menu className="h-6 w-6" />
</Button>
</div>
</div>
</header>
);
};

View File

@ -0,0 +1,93 @@
'use client';
import Link from 'next/link';
import { CreditCard, Github, Key, LogOut, User as LucideUser } from 'lucide-react';
import { signOut } from 'next-auth/react';
import { IS_SUBSCRIPTIONS_ENABLED } from '@documenso/lib/constants/features';
import { User } from '@documenso/prisma/client';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
export type ProfileDropdownProps = {
user: User;
};
export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
const initials =
user.name
?.split(' ')
.map((name) => name.slice(0, 1).toUpperCase())
.slice(0, 2)
.join('') ?? 'UK';
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-10 w-10 rounded-full">
<Avatar className="h-10 w-10">
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel>Account</DropdownMenuLabel>
<DropdownMenuItem asChild>
<Link href="/settings/profile" className="cursor-pointer">
<LucideUser className="mr-2 h-4 w-4" />
Profile
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/settings/password" className="cursor-pointer">
<Key className="mr-2 h-4 w-4" />
Password
</Link>
</DropdownMenuItem>
{IS_SUBSCRIPTIONS_ENABLED && (
<DropdownMenuItem asChild>
<Link href="/settings/billing" className="cursor-pointer">
<CreditCard className="mr-2 h-4 w-4" />
Billing
</Link>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="https://github.com/documenso/documenso" className="cursor-pointer">
<Github className="mr-2 h-4 w-4" />
Star on Github
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() =>
signOut({
callbackUrl: '/',
})
}
>
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@ -0,0 +1,30 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
export type CardMetricProps = {
icon?: LucideIcon;
title: string;
value: string | number;
className?: string;
};
export const CardMetric = ({ icon: Icon, title, value, className }: CardMetricProps) => {
return (
<div className={cn('overflow-hidden rounded-lg border border-slate-200 bg-white')}>
<div className="px-4 pb-6 pt-4 sm:px-4 sm:pb-8 sm:pt-4">
<div className="flex items-start">
{Icon && <Icon className="mr-2 h-4 w-4 text-slate-500" />}
<h3 className="flex items-end text-sm font-medium text-slate-500">{title}</h3>
</div>
<p className="mt-6 text-4xl font-semibold leading-8 text-gray-900 md:mt-8">
{typeof value === 'number' ? value.toLocaleString('en-US') : value}
</p>
</div>
</div>
);
};

View File

@ -0,0 +1,87 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { Loader } from 'lucide-react';
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import { cn } from '@documenso/ui/lib/utils';
type LoadedPDFDocument = pdfjs.PDFDocumentProxy;
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.js',
import.meta.url,
).toString();
export type PDFViewerProps = {
className?: string;
document: string;
[key: string]: unknown;
};
export const PDFViewer = ({ className, document, ...props }: PDFViewerProps) => {
const $el = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
const [numPages, setNumPages] = useState(0);
const onDocumentLoaded = (doc: LoadedPDFDocument) => {
setNumPages(doc.numPages);
};
useEffect(() => {
if ($el.current) {
const $current = $el.current;
const { width } = $current.getBoundingClientRect();
setWidth(width);
const onResize = () => {
const { width } = $current.getBoundingClientRect();
setWidth(width);
};
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
};
}
}, []);
return (
<div ref={$el} className={cn('overflow-hidden', className)}>
<PDFDocument
file={`data:application/pdf;base64,${document}`}
className="w-full overflow-hidden rounded"
onLoadSuccess={(d) => onDocumentLoaded(d)}
externalLinkTarget="_blank"
loading={
<div className="flex min-h-[80vh] flex-col items-center justify-center bg-white/50">
<Loader className="h-12 w-12 animate-spin text-slate-500" />
<p className="mt-4 text-slate-500">Loading document...</p>
</div>
}
>
{Array(numPages)
.fill(null)
.map((_, i) => (
<div
key={i}
className="border-t-primary mt-8 border-t pt-8 first:mt-0 first:border-t-0 first:pt-0"
>
<PDFPage pageNumber={i + 1} width={width} />
</div>
))}
</PDFDocument>
</div>
);
};
export default PDFViewer;

View File

@ -0,0 +1,59 @@
'use client';
import { useMemo } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { isPeriodSelectorValue } from './types';
export const PeriodSelector = () => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
const period = useMemo(() => {
const p = searchParams?.get('period') ?? '';
return isPeriodSelectorValue(p) ? p : '';
}, [searchParams]);
const onPeriodChange = (newPeriod: string) => {
if (!pathname) {
return;
}
const params = new URLSearchParams(searchParams?.toString());
params.set('period', newPeriod);
if (newPeriod === '') {
params.delete('period');
}
router.push(`${pathname}?${params.toString()}`);
};
return (
<Select defaultValue={period} onValueChange={onPeriodChange}>
<SelectTrigger className="max-w-[200px] text-slate-500">
<SelectValue />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="">All Time</SelectItem>
<SelectItem value="7d">Last 7 days</SelectItem>
<SelectItem value="14d">Last 14 days</SelectItem>
<SelectItem value="30d">Last 30 days</SelectItem>
</SelectContent>
</Select>
);
};

View File

@ -0,0 +1,5 @@
export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
export const isPeriodSelectorValue = (value: unknown): value is PeriodSelectorValue => {
return ['', '7d', '14d', '30d'].includes(value as string);
};

View File

@ -0,0 +1,63 @@
'use client';
import { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { CreditCard, Key, User } from 'lucide-react';
import { IS_SUBSCRIPTIONS_ENABLED } from '@documenso/lib/constants/features';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
const pathname = usePathname();
return (
<div className={cn('flex flex-col gap-y-2', className)} {...props}>
<Link href="/settings/profile">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/profile') && 'bg-secondary',
)}
>
<User className="mr-2 h-5 w-5" />
Profile
</Button>
</Link>
<Link href="/settings/password">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/password') && 'bg-secondary',
)}
>
<Key className="mr-2 h-5 w-5" />
Password
</Button>
</Link>
{IS_SUBSCRIPTIONS_ENABLED && (
<Link href="/settings/billing">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/billing') && 'bg-secondary',
)}
>
<CreditCard className="mr-2 h-5 w-5" />
Billing
</Button>
</Link>
)}
</div>
);
};

View File

@ -0,0 +1,66 @@
'use client';
import { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { CreditCard, Key, User } from 'lucide-react';
import { IS_SUBSCRIPTIONS_ENABLED } from '@documenso/lib/constants/features';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
export type MobileNavProps = HTMLAttributes<HTMLDivElement>;
export const MobileNav = ({ className, ...props }: MobileNavProps) => {
const pathname = usePathname();
return (
<div
className={cn('flex flex-wrap items-center justify-start gap-x-2 gap-y-4', className)}
{...props}
>
<Link href="/settings/profile">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/profile') && 'bg-secondary',
)}
>
<User className="mr-2 h-5 w-5" />
Profile
</Button>
</Link>
<Link href="/settings/password">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/password') && 'bg-secondary',
)}
>
<Key className="mr-2 h-5 w-5" />
Password
</Button>
</Link>
{IS_SUBSCRIPTIONS_ENABLED && (
<Link href="/settings/billing">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/billing') && 'bg-secondary',
)}
>
<CreditCard className="mr-2 h-5 w-5" />
Billing
</Button>
</Link>
)}
</div>
);
};