refactor: prevent re-renders by extracting components out

This commit is contained in:
ramvignesh-b
2026-04-24 16:30:13 +05:30
parent ae52a79bd0
commit 2f3d5161ed
4 changed files with 332 additions and 290 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ export function DrawerSection({
className={`join-item group flex flex-col transition-colors duration-3000 ease-in-out ${isOpen ? "bg-base-300/30" : ""}`} className={`join-item group flex flex-col transition-colors duration-3000 ease-in-out ${isOpen ? "bg-base-300/30" : ""}`}
> >
<div <div
className={`transition-all duration-2000 ease-in-out bg-neutral/10 ${ className={`transition-all duration-1500 ease-in-out bg-neutral/10 ${
isOpen isOpen
? "max-h-125 opacity-100 py-3 border-b border-base-content/5 overflow-visible" ? "max-h-125 opacity-100 py-3 border-b border-base-content/5 overflow-visible"
: "max-h-0 opacity-0 pointer-events-none" : "max-h-0 opacity-0 pointer-events-none"
+60 -56
View File
@@ -8,10 +8,61 @@ import { PATHS } from "../config/routes";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import { useLetters } from "../hooks/useLetters"; import { useLetters } from "../hooks/useLetters";
import { import {
formateRelativeDateWithoutTime,
formatRelativeDate, formatRelativeDate,
formatRelativeDateWithoutTime,
} from "../utils/dateFormat.ts"; } from "../utils/dateFormat.ts";
interface PasskeyModalProps {
onUnlock: (password: string) => Promise<void>;
}
function PasskeyModal({ onUnlock }: PasskeyModalProps) {
return (
<div className="modal modal-open bg-base-100/20 backdrop-blur-md z-100">
<div className="modal-box p-12 flex flex-col items-center">
<LockKeyIcon
size={48}
className="text-primary mx-auto mb-8 animate-pulse"
/>
<h3 className="font-bold text-lg font-display text-primary">
Authentication Required
</h3>
<p className="py-4 font-sans">
We need you to re-enter your passkey to open your letters
</p>
<div className="divider w-1/2 mx-auto text-xs text-neutral-content/30 mt-0"></div>
<p className="text-xs text-neutral-content/30 font-mono italic">
P.S. We don't validate your input at the moment.
</p>
<div className="modal-action items-center gap-4">
<form
className="form-control w-full inline-flex"
onSubmit={async (e: React.SubmitEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const password = formData.get("password") as string;
if (!password) return;
await onUnlock(password);
}}
>
<input
name="password"
required
type="password"
placeholder="password"
className="font-sans validator input input-bordered rounded-r-none"
/>
<div className="validator-message text-xs text-error"></div>
<button type="submit" className="btn btn-primary rounded-l-none">
Unlock
</button>
</form>
</div>
</div>
</div>
);
}
export default function Drawer() { export default function Drawer() {
const { user, logout, unlock } = useAuth(); const { user, logout, unlock } = useAuth();
@@ -21,53 +72,6 @@ export default function Drawer() {
if (!user) return null; if (!user) return null;
function PasskeyModal() {
return (
<div className="modal modal-open bg-base-100/20 backdrop-blur-md">
<div className="modal-box p-12 flex flex-col items-center">
<LockKeyIcon
size={48}
className="text-primary mx-auto mb-8 animate-pulse"
/>
<h3 className="font-bold text-lg font-display text-primary">
Authentication Required
</h3>
<p className="py-4 font-sans">
We need your passkey to open your letters
</p>
<div className="divider w-1/2 mx-auto text-xs text-neutral-content/30 mt-0"></div>
<p className="text-xs text-neutral-content/30 font-mono italic">
P.S. We don't validate your input at the moment.
</p>
<div className="modal-action items-center gap-4">
<form
className="form-control w-full inline-flex"
onSubmit={async (e: React.SubmitEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const password = formData.get("password") as string;
if (!password) return;
await unlock(password);
}}
>
<input
name="password"
required
type="password"
placeholder="password"
className="font-sans validator input input-bordered rounded-r-none"
/>
<div className="validator-message text-xs text-error"></div>
<button type="submit" className="btn btn-primary rounded-l-none">
Unlock
</button>
</form>
</div>
</div>
</div>
);
}
const toggleSection = (id: string) => const toggleSection = (id: string) =>
setOpenSection(openSection === id ? null : id); setOpenSection(openSection === id ? null : id);
@@ -75,8 +79,8 @@ export default function Drawer() {
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors"> <div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.5)_100%)] pointer-events-none z-0" /> <div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.5)_100%)] pointer-events-none z-0" />
{isAuthRequired && <PasskeyModal />} {isAuthRequired && <PasskeyModal onUnlock={unlock} />}
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-1000"> <header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-500">
<Logo /> <Logo />
<div className="font-sans text-xs tracking-[0.3em] uppercase text-base-content/40 mt-2"> <div className="font-sans text-xs tracking-[0.3em] uppercase text-base-content/40 mt-2">
Personal Archive Personal Archive
@@ -94,7 +98,7 @@ export default function Drawer() {
</div> </div>
</header> </header>
<div className="join join-vertical w-full max-w-120 bg-base-200 border border-base-content/10 shadow-2xl z-10 rounded-sm duration-1000 delay-200 min-h-64 flex flex-col"> <div className="join join-vertical w-full max-w-120 bg-base-200 border border-base-content/10 shadow-2xl z-10 rounded-sm duration-500 delay-200 min-h-64 flex flex-col">
{loading ? ( {loading ? (
<div className="flex-1 flex flex-col items-center justify-center p-12 gap-4"> <div className="flex-1 flex flex-col items-center justify-center p-12 gap-4">
<span className="loading loading-ring loading-lg text-primary opacity-20"></span> <span className="loading loading-ring loading-lg text-primary opacity-20"></span>
@@ -175,7 +179,7 @@ export default function Drawer() {
id={letter.public_id} id={letter.public_id}
preview={letter.metadata?.recipient || "Future Self"} preview={letter.metadata?.recipient || "Future Self"}
timestamp={formatRelativeDate(letter.updated_at)} timestamp={formatRelativeDate(letter.updated_at)}
unlock_at={formateRelativeDateWithoutTime( unlock_at={formatRelativeDateWithoutTime(
letter.unlock_at || "", letter.unlock_at || "",
)} )}
isLocked={letter.unlock_at > new Date().toISOString()} isLocked={letter.unlock_at > new Date().toISOString()}
@@ -188,20 +192,20 @@ export default function Drawer() {
<button <button
type="button" type="button"
className="group mt-15 z-10 bg-transparent border border-dashed border-base-content/10 px-8 py-4 text-base-content/40 italic cursor-pointer transition-all hover:border-primary/40 hover:text-base-content/60 hover:bg-primary/5 hover:-translate-y-0.5 flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-primary/50 duration-1000" className="group mt-15 z-10 bg-transparent border border-dashed border-base-content/10 px-8 py-4 text-base-content/40 italic cursor-pointer transition-all hover:border-primary/40 hover:text-base-content/60 hover:bg-primary/5 hover:-translate-y-0.5 flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-primary/50 duration-500"
onClick={() => navigate(PATHS.write(""))} onClick={() => navigate(PATHS.write(""))}
> >
<FeatherIcon <FeatherIcon
size={18} size={18}
weight="duotone" weight="duotone"
className="text-primary/30 transition-all duration-700 group-hover:text-primary" className="text-primary/30 transition-all duration-300 group-hover:text-primary"
/> />
Write something{" "} Write something{" "}
<span className="relative inline-flex"> <span className="relative inline-flex">
<span className="transition-opacity duration-1500 opacity-80 group-hover:opacity-0"> <span className="transition-opacity duration-500 opacity-80 group-hover:opacity-0">
. . . . . . . . . . . .
</span> </span>
<span className="absolute inset-0 text-primary transition-opacity duration-1000 opacity-0 group-hover:opacity-100"> <span className="absolute inset-0 text-primary transition-opacity duration-300 opacity-0 group-hover:opacity-100">
unsaid unsaid
</span> </span>
</span> </span>
+270 -232
View File
@@ -43,6 +43,241 @@ const toPlaceholderList = [
"Something to bear...", "Something to bear...",
]; ];
interface SealedModalProps {
sealedTargetId: string | null;
navigate: NavigateFunction;
}
function SealedModal({ sealedTargetId, navigate }: SealedModalProps) {
if (!sealedTargetId) return null;
return (
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md z-1000">
<div className="modal-box flex flex-col items-center text-center gap-6">
<LockIcon size={32} weight="duotone" className="text-primary mt-3" />
<h3 className="font-serif text-2xl">Your letter is sealed</h3>
<p className="text-base-content/60">
It's encrypted and always safe in your drawer.
</p>
<p className="text-base-content font-sans">
When you're ready,
<br />
you can{" "}
<span className="text-primary font-bold font-display">read</span> it,{" "}
<span className="text-accent font-bold font-display">send</span> it to
someone, or{" "}
<span className="text-error font-bold font-display">burn</span> it to
release
</p>
<div className="modal-action w-full justify-center gap-3 mt-4 mb-4">
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => navigate(ROUTES.DRAWER)}
>
Keep it to myself
</button>
<button
type="button"
className="btn btn-primary btn-sm"
onClick={() =>
navigate(PATHS.read(sealedTargetId), { replace: true })
}
>
View letter
</button>
</div>
</div>
</div>
);
}
interface ToolBarProps {
fileInputRef: React.RefObject<HTMLInputElement | null>;
sealBtnClicked: boolean;
setSealBtnClicked: (v: boolean) => void;
onSave: (status: "SEALED" | "DRAFT" | "VAULT", date?: Date) => Promise<void>;
setConfirmModal: (v: "VAULT" | "SEAL" | null) => void;
}
function ToolBar({
fileInputRef,
sealBtnClicked,
setSealBtnClicked,
onSave,
setConfirmModal,
}: ToolBarProps) {
return (
<div
id="writer-toolbar"
className="flex items-center justify-between mb-8 h-14 bg-base-100/50 backdrop-blur-md rounded-full border border-base-content/5 px-6"
>
<div className="flex gap-4">
<button
type="button"
className="btn btn-ghost btn-sm group"
onClick={() => fileInputRef.current?.click()}
>
<ImageIcon size={18} weight="bold" />
<span className="hidden md:inline group-hover:inline transition-all duration-1000">
Add Image
</span>
</button>
</div>
<div className="flex items-center gap-2">
<button
type="button"
className="btn btn-ghost btn-sm text-[10px] group tracking-[0.2em] uppercase font-bold text-base-content/60 hover:text-base-content"
title="Store in your private drawer"
onClick={() => onSave("DRAFT")}
>
<TrayIcon size={18} weight="bold" />
<span className="hidden md:inline group-hover:inline transition-all duration-1000">
Draft
</span>
</button>
<div className="w-px h-4 bg-base-content/10 mx-2" />
<button
type="button"
className={`btn btn-primary btn-sm rounded-full px-6 group ${sealBtnClicked ? "invisible" : "visible"}`}
onClick={() => setSealBtnClicked(true)}
>
<StampIcon
size={16}
weight="fill"
className="mr-1 group-hover:animate-bounce"
/>
<span
className={`hidden md:inline ${sealBtnClicked ? "inline" : ""} group-hover:inline transition-all duration-1000`}
>
Seal
</span>
</button>
</div>
<div
className={`flex-col items-center gap-2 absolute right-0 z-100000 bg-primary/20 rounded-full p-8 -m-2 ${sealBtnClicked ? "" : "hidden"}`}
>
<button
type="button"
className="btn btn-accent btn-sm rounded-full px-6 group"
onClick={() => onSave("SEALED")}
>
<StampIcon
size={16}
weight="fill"
className="mr-1 group-hover:animate-bounce"
/>
<span className="transition-all duration-1000">Seal</span>
</button>
<div className="w-full divider text-neutral-content/60 mt-2 mb-2">
or
</div>
<button
type="button"
className="btn btn-neutral btn-sm rounded-full px-6 group"
onClick={() => setConfirmModal("VAULT")}
>
<VaultIcon size={16} weight="fill" className="mr-1" />
<span className="transition-all duration-1000">Vault</span>
</button>
</div>
<button
type="button"
onClick={() => setSealBtnClicked(false)}
className={`bg-transparent cursor-pointer -mt-2 absolute z-1000001 right-0 text-primary ${sealBtnClicked ? "" : "hidden"}`}
>
<QuestionIcon weight="duotone" size={20} className={""} />
</button>
</div>
);
}
function LetterHead() {
return (
<div className="flex items-center justify-center mb-8 h-14">
<div className="badge badge-outline border-primary/20 bg-primary/5 text-primary gap-2 p-4 rounded-full">
<LockIcon size={14} weight="fill" />
<span className="text-[10px] uppercase tracking-widest font-bold">
Sealed & View Only
</span>
</div>
</div>
);
}
interface VaultConfirmProps {
onSave: (status: "SEALED" | "DRAFT" | "VAULT", date?: Date) => Promise<void>;
setConfirmModal: (v: "VAULT" | "SEAL" | null) => void;
setUnlockDate: (d: Date | null) => void;
}
function VaultConfirm({
onSave,
setConfirmModal,
setUnlockDate,
}: VaultConfirmProps) {
return (
<div className={"modal modal-open bg-base-100/20 backdrop-blur-md"}>
<div className="modal-box p-12 flex flex-col items-center">
<VaultIcon
size={48}
className="text-primary mx-auto mb-8 animate-pulse"
/>
<h3 className="font-serif text-3xl">Vault this letter?</h3>
<p className="text-base-content/60 text-sm text-center mt-4">
Vaulting locks the letter permanently and will be{" "}
<span className={"font-bold text-primary"}>mailed</span> to you
automatically on the unlock date.
<br />
<span className={"underline"}>
You cannot edit or view the contents of the letter until then.
</span>
</p>
<form
onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const unlockDateStr = formData.get("vault-date") as string;
const newUnlockDate = new Date(unlockDateStr);
setUnlockDate(newUnlockDate);
await onSave("VAULT", newUnlockDate);
setConfirmModal(null);
}}
id="vault-form"
>
<div className={"divider tracking-tightest font-display text-sm"}>
Set an unlock date
</div>
<input
required
type="date"
className="input input-bordered w-full"
name="vault-date"
/>
<button
className="btn btn-primary mt-4"
type="submit"
form="vault-form"
>
Vault
</button>
<button
type="button"
className="btn btn-ghost mt-4"
onClick={() => setConfirmModal(null)}
>
Cancel
</button>
</form>
</div>
</div>
);
}
export default function Editor() { export default function Editor() {
const navigate = useNavigate(); const navigate = useNavigate();
const navigateRef = useRef<NavigateFunction>(navigate); const navigateRef = useRef<NavigateFunction>(navigate);
@@ -76,26 +311,17 @@ export default function Editor() {
const [recipient, setRecipient] = useState(""); const [recipient, setRecipient] = useState("");
const [unlockDate, setUnlockDate] = useState<Date | null>(null); const [unlockDate, setUnlockDate] = useState<Date | null>(null);
const [placeholderIndex, setPlaceholderIndex] = useState(0);
const { masterKey } = useKeyStore(); const { masterKey } = useKeyStore();
const canvasRef = useRef<CanvasTools>(null); const canvasRef = useRef<CanvasTools>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const recipientInputRef = useRef<HTMLInputElement>(null);
// Placeholder rotation
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
if (recipientInputRef.current) { setPlaceholderIndex((prev) => (prev + 1) % toPlaceholderList.length);
let currentOffset = parseInt(
recipientInputRef.current.dataset.offset || "0",
);
recipientInputRef.current.dataset.offset = (
(currentOffset + 1) %
toPlaceholderList.length
).toString();
recipientInputRef.current.placeholder =
toPlaceholderList[parseInt(recipientInputRef.current.dataset.offset)];
}
}, 4000); }, 4000);
return () => clearInterval(interval); return () => clearInterval(interval);
@@ -312,218 +538,6 @@ export default function Editor() {
} }
}; };
function SealedModal() {
if (!sealedTargetId) return null;
return (
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md z-1000">
<div className="modal-box flex flex-col items-center text-center gap-6">
<LockIcon size={32} weight="duotone" className="text-primary mt-3" />
<h3 className="font-serif text-2xl">Your letter is sealed</h3>
<p className="text-base-content/60">
It's encrypted and always safe in your drawer.
</p>
<p className="text-base-content font-sans">
When you're ready,
<br />
you can{" "}
<span className="text-primary font-bold font-display">read</span>{" "}
it, <span className="text-accent font-bold font-display">send</span>{" "}
it to someone, or{" "}
<span className="text-error font-bold font-display">burn</span> it
to release
</p>
<div className="modal-action w-full justify-center gap-3 mt-4 mb-4">
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => navigate(ROUTES.DRAWER)}
>
Keep it to myself
</button>
<button
type="button"
className="btn btn-primary btn-sm"
onClick={() =>
navigate(PATHS.read(sealedTargetId), { replace: true })
}
>
View letter
</button>
</div>
</div>
</div>
);
}
function ToolBar() {
return (
<div
id="writer-toolbar"
className="flex items-center justify-between mb-8 h-14 bg-base-100/50 backdrop-blur-md rounded-full border border-base-content/5 px-6"
>
<div className="flex gap-4">
<button
type="button"
className="btn btn-ghost btn-sm group"
onClick={() => fileInputRef.current?.click()}
>
<ImageIcon size={18} weight="bold" />
<span className="hidden md:inline group-hover:inline transition-all duration-1000">
Add Image
</span>
</button>
<input
type="file"
ref={fileInputRef}
onChange={handleImageUpload}
accept="image/*"
className="hidden"
/>
</div>
<div className="flex items-center gap-2">
<button
type="button"
className="btn btn-ghost btn-sm text-[10px] group tracking-[0.2em] uppercase font-bold text-base-content/60 hover:text-base-content"
title="Store in your private drawer"
onClick={() => handleSave("DRAFT")}
>
<TrayIcon size={18} weight="bold" />
<span className="hidden md:inline group-hover:inline transition-all duration-1000">
Draft
</span>
</button>
<div className="w-px h-4 bg-base-content/10 mx-2" />
<button
type="button"
className={`btn btn-primary btn-sm rounded-full px-6 group ${sealBtnClicked ? "invisible" : "visible"}`}
onClick={() => setSealBtnClicked(true)}
>
<StampIcon
size={16}
weight="fill"
className="mr-1 group-hover:animate-bounce"
/>
<span
className={`hidden md:inline ${sealBtnClicked ? "inline" : ""} group-hover:inline transition-all duration-1000`}
>
Seal
</span>
</button>
</div>
<div
className={`flex-col items-center gap-2 absolute right-0 z-100000 bg-primary/20 rounded-full p-8 -m-2 ${sealBtnClicked ? "" : "hidden"}`}
>
<button
type="button"
className="btn btn-accent btn-sm rounded-full px-6 group"
onClick={() => handleSave("SEALED")}
>
<StampIcon
size={16}
weight="fill"
className="mr-1 group-hover:animate-bounce"
/>
<span className="transition-all duration-1000">Seal</span>
</button>
<div className="w-full divider text-neutral-content/60 mt-2 mb-2">
or
</div>
<button
type="button"
className="btn btn-neutral btn-sm rounded-full px-6 group"
onClick={() => setConfirmModal("VAULT")}
>
<VaultIcon size={16} weight="fill" className="mr-1" />
<span className="transition-all duration-1000">Vault</span>
</button>
</div>
<button
onClick={() => setSealBtnClicked(false)}
className={`bg-transparent cursor-pointer -mt-2 absolute z-1000001 right-0 text-primary ${sealBtnClicked ? "" : "hidden"}`}
>
<QuestionIcon weight="duotone" size={20} className={""} />
</button>
</div>
);
}
function LetterHead() {
return (
<div className="flex items-center justify-center mb-8 h-14">
<div className="badge badge-outline border-primary/20 bg-primary/5 text-primary gap-2 p-4 rounded-full">
<LockIcon size={14} weight="fill" />
<span className="text-[10px] uppercase tracking-widest font-bold">
Sealed & View Only
</span>
</div>
</div>
);
}
function VaultConfirm() {
return (
<div className={"modal modal-open bg-base-100/20 backdrop-blur-md"}>
<div className="modal-box p-12 flex flex-col items-center">
<VaultIcon
size={48}
className="text-primary mx-auto mb-8 animate-pulse"
/>
<h3 className="font-serif text-3xl">Vault this letter?</h3>
<p className="text-base-content/60 text-sm text-center mt-4">
Vaulting locks the letter permanently and will be{" "}
<span className={"font-bold text-primary"}>mailed</span> to you
automatically on the unlock date.
<br />
<span className={"underline"}>
You cannot edit or view the contents of the letter until then.
</span>
</p>
<form
onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const unlockDateStr = formData.get("vault-date") as string;
const newUnlockDate = new Date(unlockDateStr);
setUnlockDate(newUnlockDate);
await handleSave("VAULT", newUnlockDate);
setConfirmModal(null);
}}
id="vault-form"
>
<div className={"divider tracking-tightest font-display text-sm"}>
Set an unlock date
</div>
<input
required
type="date"
className="input input-bordered w-full"
name="vault-date"
/>
<button
className="btn btn-primary mt-4"
type="submit"
form="vault-form"
>
Vault
</button>
<button
type={"submit"}
className="btn btn-ghost mt-4"
onClick={() => setConfirmModal(null)}
>
Cancel
</button>
</form>
</div>
</div>
);
}
return ( return (
<> <>
<Navbar <Navbar
@@ -533,13 +547,13 @@ export default function Editor() {
isSaveDatePulsing ? "animate-pulse" : "" isSaveDatePulsing ? "animate-pulse" : ""
}`} }`}
> >
<p className="text-sm text-neutral-content/30 flex-col justify-end leading-none text-right"> <div className="text-sm text-neutral-content/30 flex-col justify-end leading-none text-right">
<span className="text-[10px] uppercase tracking-widest font-bold"> <span className="text-[10px] uppercase tracking-widest font-bold">
Last Save Last Save
</span> </span>
<br /> <br />
<span className="italic">{lastSaved}</span> <span className="italic">{lastSaved}</span>
</p> </div>
<ClockIcon <ClockIcon
size={16} size={16}
weight="bold" weight="bold"
@@ -631,8 +645,16 @@ export default function Editor() {
</div> </div>
)} )}
{confirmModal === "VAULT" && <VaultConfirm />} {confirmModal === "VAULT" && (
{sealedTargetId && <SealedModal />} <VaultConfirm
onSave={handleSave}
setConfirmModal={setConfirmModal}
setUnlockDate={setUnlockDate}
/>
)}
{sealedTargetId && (
<SealedModal sealedTargetId={sealedTargetId} navigate={navigate} />
)}
<div className="max-w-180 mx-auto px-1 md:px-0"> <div className="max-w-180 mx-auto px-1 md:px-0">
<div className="flex justify-between items-end mb-16 border-b border-base-content/5 pb-8 px-0"> <div className="flex justify-between items-end mb-16 border-b border-base-content/5 pb-8 px-0">
@@ -646,9 +668,7 @@ export default function Editor() {
<input <input
id="recipient" id="recipient"
type="text" type="text"
ref={recipientInputRef} placeholder={toPlaceholderList[placeholderIndex]}
placeholder={toPlaceholderList[0]}
data-offset={"0"}
value={recipient} value={recipient}
disabled={status !== "DRAFT"} disabled={status !== "DRAFT"}
onChange={(e) => setRecipient(e.target.value)} onChange={(e) => setRecipient(e.target.value)}
@@ -658,7 +678,25 @@ export default function Editor() {
<DateDisplay /> <DateDisplay />
</div> </div>
{status === "DRAFT" ? <ToolBar /> : <LetterHead />} {status === "DRAFT" ? (
<ToolBar
fileInputRef={fileInputRef}
sealBtnClicked={sealBtnClicked}
setSealBtnClicked={setSealBtnClicked}
onSave={handleSave}
setConfirmModal={setConfirmModal}
/>
) : (
<LetterHead />
)}
<input
type="file"
ref={fileInputRef}
onChange={handleImageUpload}
accept="image/*"
className="hidden"
/>
<ComposeCanvas ref={canvasRef} readOnly={status !== "DRAFT"} /> <ComposeCanvas ref={canvasRef} readOnly={status !== "DRAFT"} />
</div> </div>
+1 -1
View File
@@ -38,7 +38,7 @@ export function formatRelativeDate(input: Date | string | number) {
return dateTimeFormatter.format(date); return dateTimeFormatter.format(date);
} }
export function formateRelativeDateWithoutTime(input: Date | string | number) { export function formatRelativeDateWithoutTime(input: Date | string | number) {
if (!input) return ""; if (!input) return "";
const date = new Date(input); const date = new Date(input);
const now = new Date(); const now = new Date();