mirror of
https://github.com/documenso/documenso.git
synced 2025-11-17 02:01:33 +10:00
feat: document authoring
This commit is contained in:
@ -12,7 +12,7 @@ export type StackAvatarProps = {
|
||||
first?: boolean;
|
||||
zIndex?: string;
|
||||
fallbackText?: string;
|
||||
type: 'unsigned' | 'waiting' | 'completed';
|
||||
type: 'unsigned' | 'waiting' | 'opened' | 'completed';
|
||||
};
|
||||
|
||||
export const StackAvatar = ({ first, zIndex, fallbackText, type }: StackAvatarProps) => {
|
||||
@ -28,6 +28,9 @@ export const StackAvatar = ({ first, zIndex, fallbackText, type }: StackAvatarPr
|
||||
case 'unsigned':
|
||||
classes = 'bg-dawn-200 text-dawn-900';
|
||||
break;
|
||||
case 'opened':
|
||||
classes = 'bg-yellow-200 text-yellow-700';
|
||||
break;
|
||||
case 'waiting':
|
||||
classes = 'bg-water text-water-700';
|
||||
break;
|
||||
|
||||
@ -13,15 +13,19 @@ import { StackAvatars } from './stack-avatars';
|
||||
|
||||
export const StackAvatarsWithTooltip = ({ recipients }: { recipients: Recipient[] }) => {
|
||||
const waitingRecipients = recipients.filter(
|
||||
(recipient) => recipient.sendStatus === 'SENT' && recipient.signingStatus === 'NOT_SIGNED',
|
||||
(recipient) => getRecipientType(recipient) === 'waiting',
|
||||
);
|
||||
|
||||
const openedRecipients = recipients.filter(
|
||||
(recipient) => getRecipientType(recipient) === 'opened',
|
||||
);
|
||||
|
||||
const completedRecipients = recipients.filter(
|
||||
(recipient) => recipient.sendStatus === 'SENT' && recipient.signingStatus === 'SIGNED',
|
||||
(recipient) => getRecipientType(recipient) === 'completed',
|
||||
);
|
||||
|
||||
const uncompletedRecipients = recipients.filter(
|
||||
(recipient) => recipient.sendStatus === 'NOT_SENT' && recipient.signingStatus === 'NOT_SIGNED',
|
||||
(recipient) => getRecipientType(recipient) === 'unsigned',
|
||||
);
|
||||
|
||||
return (
|
||||
@ -66,6 +70,23 @@ export const StackAvatarsWithTooltip = ({ recipients }: { recipients: Recipient[
|
||||
</div>
|
||||
)}
|
||||
|
||||
{openedRecipients.length > 0 && (
|
||||
<div>
|
||||
<h1 className="text-base font-medium">Opened</h1>
|
||||
{openedRecipients.map((recipient: Recipient) => (
|
||||
<div key={recipient.id} className="my-1 flex items-center gap-2">
|
||||
<StackAvatar
|
||||
first={true}
|
||||
key={recipient.id}
|
||||
type={getRecipientType(recipient)}
|
||||
fallbackText={initials(recipient.name)}
|
||||
/>
|
||||
<span className="text-sm text-gray-500">{recipient.email}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uncompletedRecipients.length > 0 && (
|
||||
<div>
|
||||
<h1 className="text-base font-medium">Uncompleted</h1>
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { Loader } from 'lucide-react';
|
||||
|
||||
export const LazyPDFViewer = dynamic(
|
||||
async () => import('~/components/(dashboard)/pdf-viewer/pdf-viewer'),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="dark:bg-background flex min-h-[80vh] flex-col items-center justify-center bg-white/50">
|
||||
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
||||
|
||||
<p className="text-muted-foreground mt-4">Loading document...</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export const RefreshOnFocus = () => {
|
||||
const { refresh } = useRouter();
|
||||
|
||||
const onFocus = useCallback(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('focus', onFocus);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('focus', onFocus);
|
||||
};
|
||||
}, [onFocus]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import React, { HTMLAttributes } from 'react';
|
||||
|
||||
import { Loader } from 'lucide-react';
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Trash } from 'lucide-react';
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
import React, { createContext, useRef } from 'react';
|
||||
|
||||
import { OnPDFViewerPageClick } from '~/components/(dashboard)/pdf-viewer/pdf-viewer';
|
||||
|
||||
type EditFormContextValue = {
|
||||
firePageClickEvent: OnPDFViewerPageClick;
|
||||
registerPageClickHandler: (_handler: OnPDFViewerPageClick) => void;
|
||||
unregisterPageClickHandler: (_handler: OnPDFViewerPageClick) => void;
|
||||
} | null;
|
||||
|
||||
const EditFormContext = createContext<EditFormContextValue>(null);
|
||||
|
||||
export type EditFormProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const useEditForm = () => {
|
||||
const context = React.useContext(EditFormContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useEditForm must be used within a EditFormProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const EditFormProvider = ({ children }: EditFormProviderProps) => {
|
||||
const handlers = useRef(new Set<OnPDFViewerPageClick>());
|
||||
|
||||
const firePageClickEvent: OnPDFViewerPageClick = (event) => {
|
||||
handlers.current.forEach((handler) => handler(event));
|
||||
};
|
||||
|
||||
const registerPageClickHandler = (handler: OnPDFViewerPageClick) => {
|
||||
handlers.current.add(handler);
|
||||
};
|
||||
|
||||
const unregisterPageClickHandler = (handler: OnPDFViewerPageClick) => {
|
||||
handlers.current.delete(handler);
|
||||
};
|
||||
|
||||
return (
|
||||
<EditFormContext.Provider
|
||||
value={{
|
||||
firePageClickEvent,
|
||||
registerPageClickHandler,
|
||||
unregisterPageClickHandler,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</EditFormContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
@ -30,6 +32,8 @@ export type ProfileFormProps = {
|
||||
};
|
||||
|
||||
export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const {
|
||||
@ -59,6 +63,8 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
||||
description: 'Your profile has been updated successfully.',
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
toast({
|
||||
|
||||
7
apps/web/src/components/motion.tsx
Normal file
7
apps/web/src/components/motion.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export * from 'framer-motion';
|
||||
|
||||
export const MotionDiv = motion.div;
|
||||
@ -24,7 +24,12 @@ export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChang
|
||||
onChange?: (_signatureDataUrl: string | null) => void;
|
||||
};
|
||||
|
||||
export const SignaturePad = ({ className, onChange, ...props }: SignaturePadProps) => {
|
||||
export const SignaturePad = ({
|
||||
className,
|
||||
defaultValue,
|
||||
onChange,
|
||||
...props
|
||||
}: SignaturePadProps) => {
|
||||
const $el = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const [isPressed, setIsPressed] = useState(false);
|
||||
@ -127,7 +132,7 @@ export const SignaturePad = ({ className, onChange, ...props }: SignaturePadProp
|
||||
setPoints(newPoints);
|
||||
}
|
||||
|
||||
if ($el.current) {
|
||||
if ($el.current && newPoints.length > 0) {
|
||||
const ctx = $el.current.getContext('2d');
|
||||
|
||||
if (ctx) {
|
||||
@ -188,6 +193,23 @@ export const SignaturePad = ({ className, onChange, ...props }: SignaturePadProp
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log({ defaultValue });
|
||||
if ($el.current && typeof defaultValue === 'string') {
|
||||
const ctx = $el.current.getContext('2d');
|
||||
|
||||
const { width, height } = $el.current;
|
||||
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height));
|
||||
};
|
||||
|
||||
img.src = defaultValue;
|
||||
}
|
||||
}, [defaultValue]);
|
||||
|
||||
return (
|
||||
<div className="relative block">
|
||||
<canvas
|
||||
@ -202,10 +224,10 @@ export const SignaturePad = ({ className, onChange, ...props }: SignaturePadProp
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="absolute bottom-2 right-2">
|
||||
<div className="absolute bottom-4 right-4">
|
||||
<button
|
||||
type="button"
|
||||
className="focus-visible:ring-ring ring-offset-background rounded-full p-2 text-xs text-slate-500 focus-visible:outline-none focus-visible:ring-2"
|
||||
className="focus-visible:ring-ring ring-offset-background rounded-full p-0 text-xs text-slate-500 focus-visible:outline-none focus-visible:ring-2"
|
||||
onClick={() => onClearClick()}
|
||||
>
|
||||
Clear Signature
|
||||
|
||||
Reference in New Issue
Block a user