From faee0b45d6f847ff65bec2a0b92836b71e2c37d0 Mon Sep 17 00:00:00 2001 From: ramvignesh-b Date: Tue, 28 Apr 2026 22:52:06 +0530 Subject: [PATCH] refactor: implement reusable Modal component --- .../src/components/drawer/PasskeyModal.tsx | 81 ++++++------ .../src/components/editor/PostSealModal.tsx | 111 ++++++++--------- frontend/src/components/editor/ToolBar.tsx | 116 +++++++++--------- frontend/src/components/reader/BurnModal.tsx | 21 ++-- .../components/reader/PostActionOverlay.tsx | 8 +- frontend/src/components/reader/ShareModal.tsx | 41 +++---- frontend/src/components/ui/LogModal.tsx | 53 +++----- frontend/src/components/ui/Modal.tsx | 30 +++++ frontend/src/pages/Editor.tsx | 97 +++++++-------- frontend/src/pages/Login.tsx | 18 +-- frontend/src/pages/Reader.tsx | 63 +++++----- 11 files changed, 316 insertions(+), 323 deletions(-) create mode 100644 frontend/src/components/ui/Modal.tsx diff --git a/frontend/src/components/drawer/PasskeyModal.tsx b/frontend/src/components/drawer/PasskeyModal.tsx index 16376bb..97940d9 100644 --- a/frontend/src/components/drawer/PasskeyModal.tsx +++ b/frontend/src/components/drawer/PasskeyModal.tsx @@ -1,4 +1,5 @@ import { LockKeyIcon } from "@phosphor-icons/react"; +import { Modal } from "../ui/Modal"; interface PasskeyModalProps { onUnlock: (password: string) => Promise; @@ -6,47 +7,45 @@ interface PasskeyModalProps { export function PasskeyModal({ onUnlock }: PasskeyModalProps) { return ( -
-
- -

- Authentication Required -

-

- We need your passkey to open your letters -

-
-

- Your passkey is used to decrypt your data locally. -

-
-
) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const password = formData.get("password") as string; - if (!password) return; - await onUnlock(password); - }} - > - -
- -
-
+ + +

+ Authentication Required +

+

+ We need your passkey to open your letters +

+
+

+ Your passkey is used to decrypt your data locally. +

+
+
) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const password = formData.get("password") as string; + if (!password) return; + await onUnlock(password); + }} + > + +
+ +
-
+ ); } diff --git a/frontend/src/components/editor/PostSealModal.tsx b/frontend/src/components/editor/PostSealModal.tsx index 792cead..6bb8c51 100644 --- a/frontend/src/components/editor/PostSealModal.tsx +++ b/frontend/src/components/editor/PostSealModal.tsx @@ -1,6 +1,7 @@ import { LockIcon } from "@phosphor-icons/react"; import type { NavigateFunction } from "react-router-dom"; import { PATHS, ROUTES } from "../../config/routes"; +import { Modal } from "../ui/Modal"; interface PostSealModalProps { sealedTargetId: string | null; @@ -13,72 +14,68 @@ export function PostSealModal({ navigate, type = "KEPT", }: PostSealModalProps) { - if (!sealedTargetId) return null; return ( -
-
- -

Your letter is sealed

-

- It's encrypted and always safe in your drawer. + + +

Your letter is sealed

+

+ It's encrypted and always safe in your drawer. +

+ {type === "KEPT" ? ( +

+ When you're ready, +
+ you can{" "} + read it,{" "} + send it to + someone, or{" "} + burn it to + release

+ ) : ( +

+ Be assured that the letter will find you when the time is right. +
+ Till then,{" "} + + take a deep breath + + , manifest + , and{" "} + + let it rest + + . +

+ )} +
{type === "KEPT" ? ( -

- When you're ready, -
- you can{" "} - read{" "} - it, send{" "} - it to someone, or{" "} - burn it - to release -

- ) : ( -

- Be assured that the letter will find you when the time is right. -
- Till then,{" "} - - take a deep breath - - ,{" "} - manifest - , and{" "} - - let it rest - - . -

- )} -
- {type === "KEPT" ? ( - <> - - - - ) : ( + <> - )} -
+ + + ) : ( + + )}
-
+ ); } diff --git a/frontend/src/components/editor/ToolBar.tsx b/frontend/src/components/editor/ToolBar.tsx index 061b6ce..087544e 100644 --- a/frontend/src/components/editor/ToolBar.tsx +++ b/frontend/src/components/editor/ToolBar.tsx @@ -6,6 +6,7 @@ import { TrayIcon, VaultIcon, } from "@phosphor-icons/react"; +import { Modal } from "../ui/Modal"; interface ToolBarProps { fileInputRef: React.RefObject; @@ -150,65 +151,62 @@ export function VaultConfirmModal({ setUnlockDate, }: VaultConfirmModalProps) { return ( -
-
- + +

Take it away, then?

+

+ By vaulting this letter, you ask me to hold on to this. +
+ I'll remember to mail you this on the unlock date. +
+ + {" "} + But I won't let you read or rewrite this letter until then. + +
+

+
{ + 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" + className="min-w-75" + > +
+ Set an unlock date +
+ -

Take it away, then?

-

- By vaulting this letter, you ask me to hold on to this. -
- I'll remember to mail you this on the unlock date. -
- - {" "} - But I won't let you read or rewrite this letter until then. - -
-

- { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const unlockDateStr = formData.get("vault-date") as string; - const newUnlockDate = new Date(unlockDateStr); - console.log(newUnlockDate); - setUnlockDate(newUnlockDate); - await onSave("VAULT", newUnlockDate); - setConfirmModal(null); - }} - id="vault-form" - className="min-w-75" - > -
- Set an unlock date -
- -
- - -
-
-
-
+
+ + +
+ + ); } diff --git a/frontend/src/components/reader/BurnModal.tsx b/frontend/src/components/reader/BurnModal.tsx index e5e5154..1d9a303 100644 --- a/frontend/src/components/reader/BurnModal.tsx +++ b/frontend/src/components/reader/BurnModal.tsx @@ -1,11 +1,12 @@ -import { CampfireIcon, FlameIcon, XCircleIcon } from "@phosphor-icons/react"; +import { CampfireIcon, FlameIcon } from "@phosphor-icons/react"; import { useEffect, useState } from "react"; +import { Modal } from "../ui/Modal"; interface BurnModalProps { burnLetter: () => void; isBurning: boolean; setShowBurnModal: (show: boolean) => void; - setRevealState: (state: "sealed" | "revealed" | "burning" | "burned") => void; + setRevealState: (state: "SEALED" | "REVEALED" | "BURNING" | "BURNED") => void; } export function BurnModal({ @@ -20,7 +21,7 @@ export function BurnModal({ useEffect(() => { if (!burnClicked) return; if (flameOn === 100) { - setRevealState("sealed"); + setRevealState("SEALED"); burnLetter(); } const interval = setInterval(() => { @@ -33,23 +34,15 @@ export function BurnModal({ const burnStyle = flameOn < 30 ? "" : `contrast(${flameOn / 30})`; return ( -
+ setShowBurnModal(false)}>
-
-
+ ); } diff --git a/frontend/src/components/reader/PostActionOverlay.tsx b/frontend/src/components/reader/PostActionOverlay.tsx index 2b0caf3..5f07a55 100644 --- a/frontend/src/components/reader/PostActionOverlay.tsx +++ b/frontend/src/components/reader/PostActionOverlay.tsx @@ -2,22 +2,22 @@ import { useNavigate } from "react-router-dom"; import { ROUTES } from "../../config/routes"; interface PostActionOverlayProps { - revealState: "sealed" | "revealed" | "burning" | "burned"; + revealState: "SEALED" | "REVEALED" | "BURNING" | "BURNED"; } export function PostActionOverlay({ revealState }: PostActionOverlayProps) { const navigate = useNavigate(); return (

It is done

May your soul find diff --git a/frontend/src/components/reader/ShareModal.tsx b/frontend/src/components/reader/ShareModal.tsx index 4c094b2..38396c6 100644 --- a/frontend/src/components/reader/ShareModal.tsx +++ b/frontend/src/components/reader/ShareModal.tsx @@ -1,8 +1,6 @@ -import { - EyeSlashIcon, - PaperPlaneTiltIcon, - XCircleIcon, -} from "@phosphor-icons/react"; +import { EyeSlashIcon, PaperPlaneTiltIcon } from "@phosphor-icons/react"; +import { Modal } from "../ui/Modal"; +import Saajan from "../ui/Saajan"; interface ShareModalProps { shareLink: string | null; @@ -15,16 +13,8 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) { await navigator.clipboard.writeText(shareLink); }; return ( -

-
- + <> + setShareLink(null)}>

Send this letter

- You've carried these words long enough. Send your letter now, and - let the unsaid{" "} - finally find its home. + You've carried these words long enough. +
+ Send your letter now, and let the{" "} + unsaid finally + find its home.

- The recipient will have the same viewing experience like you do - now. + They'll receive it exactly as you're seeing it now. +
+ Nothing more, nothing less.
@@ -69,7 +62,13 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {

+ +
+
-
+ ); } diff --git a/frontend/src/components/ui/LogModal.tsx b/frontend/src/components/ui/LogModal.tsx index 4540bd1..9199772 100644 --- a/frontend/src/components/ui/LogModal.tsx +++ b/frontend/src/components/ui/LogModal.tsx @@ -1,4 +1,5 @@ -import { WarningIcon, XCircleIcon, XIcon } from "@phosphor-icons/react"; +import { WarningIcon } from "@phosphor-icons/react"; +import { Modal } from "./Modal"; interface LogModalContent { status: "WARN" | "ERROR" | "RESET" | "SUCCESS"; @@ -15,40 +16,24 @@ export const LogModal = ({ onClose, status, }: LogModalContent) => { - return status === "RESET" || !isOpen ? ( -
- ) : ( -
-
-
- {status === "WARN" && ( - - )} - {status === "ERROR" && ( - - )} - {message} -
- Error Stack -
-
-
-              {String(log)}
-            
-
-
- -
+ return ( + +
+ {status === "WARN" && ( + + )} + {message} +
+ Error Stack +
+
+
+            {String(log)}
+          
-
+ ); }; diff --git a/frontend/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx new file mode 100644 index 0000000..8ac0dba --- /dev/null +++ b/frontend/src/components/ui/Modal.tsx @@ -0,0 +1,30 @@ +import { XCircleIcon } from "@phosphor-icons/react"; +import type { ReactNode } from "react"; + +interface ModalProps { + isOpen: boolean; + onClose?: () => void; + children: ReactNode; +} + +export function Modal({ isOpen, onClose, children }: ModalProps) { + if (!isOpen) return null; + + return ( +
+
+ {onClose && ( + + )} + {children} +
+
+ ); +} diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index c481516..2de8ce2 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -23,6 +23,7 @@ import { } from "../components/editor/ToolBar"; import DateDisplay from "../components/ui/DateDisplay"; import { LogModal } from "../components/ui/LogModal"; +import { Modal } from "../components/ui/Modal"; import { Navbar } from "../components/ui/Navbar"; import { endpoints } from "../config/endpoints"; @@ -356,59 +357,53 @@ export default function Editor() { )} {saveOverlay !== "idle" && ( -
-
- {saveOverlay === "saving" && ( -
- - Securing your letter... -
- )} + + {saveOverlay === "saving" && ( +
+ + Securing your letter... +
+ )} - {saveOverlay === "saved" && ( -
- - Your letter is saved! -
- )} + {saveOverlay === "saved" && ( +
+ + Your letter is saved! +
+ )} - {saveOverlay === "error" && ( -
- - Failed to save letter -
- )} -
-
+ {saveOverlay === "error" && ( +
+ + Failed to save letter +
+ )} + )} {confirmModal === "VAULT" && ( diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index b5b0240..bebf1a7 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -12,6 +12,7 @@ import { z } from "zod"; import { api, publicApi } from "../api/apiClient"; import Logo from "../components/Logo"; import FormField from "../components/ui/FormField"; +import { Modal } from "../components/ui/Modal"; import Saajan from "../components/ui/Saajan"; import { endpoints } from "../config/endpoints"; import { ROUTES } from "../config/routes"; @@ -31,13 +32,8 @@ function WelcomeModal({ setShowWelcome: (show: boolean) => void; }) { return ( -
-
- -
-
+ <> +
+
+
+
-
+ ); } diff --git a/frontend/src/pages/Reader.tsx b/frontend/src/pages/Reader.tsx index 537334b..a86be58 100644 --- a/frontend/src/pages/Reader.tsx +++ b/frontend/src/pages/Reader.tsx @@ -33,6 +33,7 @@ interface LetterMetadata { updated_at?: string; } +const WAIT_FOR_BURN_MS = 18000; export default function Reader() { const { public_id } = useParams(); const location = useLocation(); @@ -44,13 +45,10 @@ export default function Reader() { const [isDecrypting, setIsDecrypting] = useState(true); const [revealState, setRevealState] = useState< - "sealed" | "revealed" | "burned" | "burning" - >("sealed"); - const [error, setError] = useState<{ - message: string; - log: string; - } | null>(null); - const [warning, setWarning] = useState<{ + "SEALED" | "REVEALED" | "BURNED" | "BURNING" + >("SEALED"); + const [logTrace, setLogTrace] = useState<{ + type: "WARN" | "ERROR"; message: string; log: string; } | null>(null); @@ -92,8 +90,8 @@ export default function Reader() { setShowBurnModal(false); setIgnite(true); setTimeout(() => { - setRevealState("burned"); - }, 13000); + setRevealState("BURNED"); + }, WAIT_FOR_BURN_MS); } }; @@ -180,17 +178,19 @@ export default function Reader() { ); } } catch (err) { - setWarning({ + setLogTrace({ message: "Failed to decrypt elements. Images might not render in the letter as intended.", log: err instanceof Error ? err.message : "Unknown error", + type: "WARN", }); } setDecryptedCanvasData(canvasData); } catch (err) { - setError({ - message: `Failed to load letter :(`, + setLogTrace({ + message: `Failed to load letter ☹`, log: err instanceof Error ? err.message : "Unknown error", + type: "ERROR", }); } finally { setIsDecrypting(false); @@ -203,7 +203,7 @@ export default function Reader() { useEffect(() => { if ( !isDecrypting && - revealState === "revealed" && + revealState === "REVEALED" && decryptedCanvasData && canvasRef.current ) { @@ -214,12 +214,12 @@ export default function Reader() { if (isDecrypting) { return (
-
+
-

+

Breaking the seal...

@@ -228,14 +228,17 @@ export default function Reader() { ); } - if (error) { + if (logTrace) { return ( (window.location.href = "/")} - message={error.message} - log={error.log} - status="ERROR" + isOpen={!!logTrace} + onClose={() => { + if (logTrace.type === "ERROR") window.location.href = "/"; + setLogTrace(null); + }} + message={logTrace.message} + log={logTrace.log} + status={logTrace.type} /> ); } @@ -245,12 +248,12 @@ export default function Reader() {
- {revealState === "sealed" && ( + {revealState === "SEALED" && (
setRevealState("revealed")} + onRevealComplete={() => setRevealState("REVEALED")} ignite={ignite} />
@@ -270,15 +273,7 @@ export default function Reader() { {ignite && } - setWarning(null)} - message={warning?.message || ""} - log={warning?.log || ""} - status="WARN" - /> - - {revealState === "revealed" && ( + {revealState === "REVEALED" && (
@@ -309,7 +304,7 @@ export default function Reader() { /> )} - {isAuthor && revealState !== "burned" && ( + {isAuthor && revealState !== "BURNED" && (