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" : ""}`}
>
<div
className={`transition-all duration-2000 ease-in-out bg-neutral/10 ${
className={`transition-all duration-1500 ease-in-out bg-neutral/10 ${
isOpen
? "max-h-125 opacity-100 py-3 border-b border-base-content/5 overflow-visible"
: "max-h-0 opacity-0 pointer-events-none"
+25 -21
View File
@@ -8,22 +8,17 @@ import { PATHS } from "../config/routes";
import { useAuth } from "../hooks/useAuth";
import { useLetters } from "../hooks/useLetters";
import {
formateRelativeDateWithoutTime,
formatRelativeDate,
formatRelativeDateWithoutTime,
} from "../utils/dateFormat.ts";
export default function Drawer() {
const { user, logout, unlock } = useAuth();
interface PasskeyModalProps {
onUnlock: (password: string) => Promise<void>;
}
const [openSection, setOpenSection] = useState<string | null>(null);
const navigate = useNavigate();
const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters();
if (!user) return null;
function PasskeyModal() {
function PasskeyModal({ onUnlock }: PasskeyModalProps) {
return (
<div className="modal modal-open bg-base-100/20 backdrop-blur-md">
<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}
@@ -33,7 +28,7 @@ export default function Drawer() {
Authentication Required
</h3>
<p className="py-4 font-sans">
We need your passkey to open your letters
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">
@@ -47,7 +42,7 @@ export default function Drawer() {
const formData = new FormData(e.currentTarget);
const password = formData.get("password") as string;
if (!password) return;
await unlock(password);
await onUnlock(password);
}}
>
<input
@@ -68,6 +63,15 @@ export default function Drawer() {
);
}
export default function Drawer() {
const { user, logout, unlock } = useAuth();
const [openSection, setOpenSection] = useState<string | null>(null);
const navigate = useNavigate();
const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters();
if (!user) return null;
const toggleSection = (id: string) =>
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="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 />}
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-1000">
{isAuthRequired && <PasskeyModal onUnlock={unlock} />}
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-500">
<Logo />
<div className="font-sans text-xs tracking-[0.3em] uppercase text-base-content/40 mt-2">
Personal Archive
@@ -94,7 +98,7 @@ export default function Drawer() {
</div>
</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 ? (
<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>
@@ -175,7 +179,7 @@ export default function Drawer() {
id={letter.public_id}
preview={letter.metadata?.recipient || "Future Self"}
timestamp={formatRelativeDate(letter.updated_at)}
unlock_at={formateRelativeDateWithoutTime(
unlock_at={formatRelativeDateWithoutTime(
letter.unlock_at || "",
)}
isLocked={letter.unlock_at > new Date().toISOString()}
@@ -188,20 +192,20 @@ export default function Drawer() {
<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(""))}
>
<FeatherIcon
size={18}
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{" "}
<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 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
</span>
</span>
+270 -232
View File
@@ -43,6 +43,241 @@ const toPlaceholderList = [
"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() {
const navigate = useNavigate();
const navigateRef = useRef<NavigateFunction>(navigate);
@@ -76,26 +311,17 @@ export default function Editor() {
const [recipient, setRecipient] = useState("");
const [unlockDate, setUnlockDate] = useState<Date | null>(null);
const [placeholderIndex, setPlaceholderIndex] = useState(0);
const { masterKey } = useKeyStore();
const canvasRef = useRef<CanvasTools>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const recipientInputRef = useRef<HTMLInputElement>(null);
// Placeholder rotation
useEffect(() => {
const interval = setInterval(() => {
if (recipientInputRef.current) {
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)];
}
setPlaceholderIndex((prev) => (prev + 1) % toPlaceholderList.length);
}, 4000);
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 (
<>
<Navbar
@@ -533,13 +547,13 @@ export default function Editor() {
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">
Last Save
</span>
<br />
<span className="italic">{lastSaved}</span>
</p>
</div>
<ClockIcon
size={16}
weight="bold"
@@ -631,8 +645,16 @@ export default function Editor() {
</div>
)}
{confirmModal === "VAULT" && <VaultConfirm />}
{sealedTargetId && <SealedModal />}
{confirmModal === "VAULT" && (
<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="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
id="recipient"
type="text"
ref={recipientInputRef}
placeholder={toPlaceholderList[0]}
data-offset={"0"}
placeholder={toPlaceholderList[placeholderIndex]}
value={recipient}
disabled={status !== "DRAFT"}
onChange={(e) => setRecipient(e.target.value)}
@@ -658,7 +678,25 @@ export default function Editor() {
<DateDisplay />
</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"} />
</div>
+1 -1
View File
@@ -38,7 +38,7 @@ export function formatRelativeDate(input: Date | string | number) {
return dateTimeFormatter.format(date);
}
export function formateRelativeDateWithoutTime(input: Date | string | number) {
export function formatRelativeDateWithoutTime(input: Date | string | number) {
if (!input) return "";
const date = new Date(input);
const now = new Date();