From 3e5dbbe3f334849d5244a2c5640a84f99e3c51e8 Mon Sep 17 00:00:00 2001 From: ramvignesh-b Date: Tue, 14 Apr 2026 21:00:30 +0530 Subject: [PATCH] feat: add Navbar component and integrate into Editor page --- frontend/src/components/ui/Navbar.tsx | 33 +++ frontend/src/pages/Editor.tsx | 411 +++++++++++++++++--------- 2 files changed, 298 insertions(+), 146 deletions(-) create mode 100644 frontend/src/components/ui/Navbar.tsx diff --git a/frontend/src/components/ui/Navbar.tsx b/frontend/src/components/ui/Navbar.tsx new file mode 100644 index 0000000..9526187 --- /dev/null +++ b/frontend/src/components/ui/Navbar.tsx @@ -0,0 +1,33 @@ +import { ArrowArcLeftIcon } from "@phosphor-icons/react"; +import { useNavigate } from "react-router-dom"; +import { ROUTES } from "../../config/routes"; + +export const Navbar = ({ child }: { child?: React.ReactNode }) => { + const navigate = useNavigate(); + return ( + + ); +}; diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index 64091da..055d07e 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -1,9 +1,12 @@ import { + ClockIcon, DownloadSimpleIcon, ImageIcon, LockIcon, SpinnerGapIcon, TrayIcon, + XCircleIcon, + XIcon, } from "@phosphor-icons/react"; import { useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; @@ -13,21 +16,33 @@ import { ComposeCanvas, } from "../components/ui/ComposeCanvas"; import DateDisplay from "../components/ui/DateDisplay"; +import { Navbar } from "../components/ui/Navbar"; import { endpoints } from "../config/endpoints"; import { PATHS } from "../config/routes"; import { useKeyStore } from "../store/useKeyStore"; import { CryptoUtils } from "../utils/crypto"; +import { formatRelativeDate } from "../utils/dateFormat"; import { decryptCanvasImages, encryptCanvasImages } from "../utils/letterLogic"; +type SaveOverlay = "idle" | "saving" | "saved" | "error"; + +const OVERLAY_FADE_MS = 250; +const SAVED_VISIBLE_MS = 1400; +const ERROR_VISIBLE_MS = 2400; + export default function Editor() { const navigate = useNavigate(); const { public_id } = useParams(); const letterIdRef = useRef(public_id ?? ""); const [isInitialLoading, setIsInitialLoading] = useState(false); - const [isSealing, setIsSealing] = useState(false); - const [isSaveSuccess, setIsSaveSuccess] = useState(false); const [shareLink, setShareLink] = useState(null); + const [lastSaved, setLastSaved] = useState(null); + const [isSaveDatePulsing, setIsSaveDatePulsing] = useState(false); + const [lastSavedPulseTick, setLastSavedPulseTick] = useState(0); + + const [saveOverlay, setSaveOverlay] = useState("idle"); + const [showSaveOverlay, setShowSaveOverlay] = useState(false); const [recipient, setRecipient] = useState(""); const { masterKey } = useKeyStore(); @@ -46,6 +61,8 @@ export default function Editor() { const res = await api.get(`${endpoints.LETTERS}${public_id}/`); const letterData = res.data; + setLastSaved(formatRelativeDate(new Date(letterData.updated_at))); + const metadata = await cryptoUtils.decryptMetadata( { encrypted_content: letterData.encrypted_metadata, @@ -85,7 +102,42 @@ export default function Editor() { loadExistingLetter(); }, [public_id, masterKey]); - // -------------------------------------------------------------------------------------- + useEffect(() => { + if (lastSavedPulseTick === 0) return; + + setIsSaveDatePulsing(true); + + const timer = setTimeout(() => { + setIsSaveDatePulsing(false); + }, 10000); + + return () => clearTimeout(timer); + }, [lastSavedPulseTick]); + + useEffect(() => { + if (saveOverlay === "idle" || saveOverlay === "saving") return; + + const visibleTimer = setTimeout( + () => { + setShowSaveOverlay(false); + }, + saveOverlay === "saved" ? SAVED_VISIBLE_MS : ERROR_VISIBLE_MS, + ); + + const unmountTimer = setTimeout( + () => { + setSaveOverlay("idle"); + }, + (saveOverlay === "saved" ? SAVED_VISIBLE_MS : ERROR_VISIBLE_MS) + + OVERLAY_FADE_MS, + ); + + return () => { + clearTimeout(visibleTimer); + clearTimeout(unmountTimer); + }; + }, [saveOverlay]); + const handleImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -102,8 +154,10 @@ export default function Editor() { letterIdRef.current = public_id; } - if (isSealing || !masterKey) return; - setIsSealing(true); + if (saveOverlay === "saving" || !masterKey) return; + + setSaveOverlay("saving"); + setShowSaveOverlay(true); const cryptoUtils = new CryptoUtils(); await cryptoUtils.initialize(); @@ -145,178 +199,243 @@ export default function Editor() { }); await api.put(`${endpoints.LETTERS}${letterIdRef.current}/`, formData); - setIsSaveSuccess(true); + + setLastSaved(formatRelativeDate(new Date())); + setLastSavedPulseTick((prev) => prev + 1); if (status === "SEALED" && encrypted_letter.sharingKey) { - const link = `${window.location.origin}${PATHS.read(letterIdRef.current)}#${encrypted_letter.sharingKey}`; + const link = `${window.location.origin}${PATHS.read( + letterIdRef.current, + )}#${encrypted_letter.sharingKey}`; setShareLink(link); + setShowSaveOverlay(false); + setTimeout(() => setSaveOverlay("idle"), OVERLAY_FADE_MS); + } else { + setSaveOverlay("saved"); + setShowSaveOverlay(true); } - - setTimeout(() => setIsSaveSuccess(false), 5000); } catch (_error) { - } finally { - setIsSealing(false); + setSaveOverlay("error"); + setShowSaveOverlay(true); } }; const copyToClipboard = async () => { if (!shareLink) return; - try { - await navigator.clipboard.writeText(shareLink); - } catch (_err) {} + await navigator.clipboard.writeText(shareLink); }; return ( -
- {isInitialLoading && ( -
-
- + + -

- Opening your draft... +

+ + Last Save + +
+ {lastSaved}

-
- )} + } + /> - {shareLink && ( -
-
- -
-
- -
-
-

Sealed & Ready

-

- This letter is now encrypted. Share this secret link with your - recipient. -

-
- -
- - -
- -

- Zero-Knowledge: The key is in the link, not our servers. +

+ {isInitialLoading && ( +
+
+ +

+ Opening your draft...

-
- )} + )} - {isSaveSuccess && !shareLink && ( -
-
- -

- Your letter is saved! -

+ {shareLink && ( +
+
+ +
+
+ +
+
+

Sealed & Ready

+

+ This letter is now encrypted. Share this secret link with + your recipient. +

+
+ +
+ + +
+ +

+ Zero-Knowledge: The key is in the link, not our servers. +

+
+
-
- )} + )} - {isSealing && ( -
-
- -

- Securing your letter... -

+ {saveOverlay !== "idle" && !shareLink && ( +
+
+ {saveOverlay === "saving" && ( +
+ + Securing your letter... +
+ )} + + {saveOverlay === "saved" && ( +
+ + Your letter is saved! +
+ )} + + {saveOverlay === "error" && ( +
+ + Failed to save letter +
+ )} +
-
- )} + )} -
-
-
- - setRecipient(e.target.value)} - className="bg-transparent border-none outline-none text-2xl md:text-3xl lg:text-4xl font-serif text-base-content placeholder:text-base-content/10 w-full" - /> -
- -
- -
-
- - +
+
+
+ + setRecipient(e.target.value)} + className="bg-transparent border-none outline-none text-2xl md:text-3xl lg:text-4xl font-serif text-base-content placeholder:text-base-content/10 w-full" + /> +
+
-
- +
+
+ + +
-
+
+ - +
+ + +
+ +
- -
-
+ + ); }