From 8cca16a0f9bb32f7549c846a90ed9d12921cac28 Mon Sep 17 00:00:00 2001 From: me Date: Thu, 7 May 2026 04:59:29 +0530 Subject: [PATCH 01/11] refactor: rename PublicRoute with AutoRedirectRoute --- frontend/src/components/RouteGuards.test.tsx | 200 +++++++++---------- frontend/src/components/RouteGuards.tsx | 30 +-- 2 files changed, 115 insertions(+), 115 deletions(-) diff --git a/frontend/src/components/RouteGuards.test.tsx b/frontend/src/components/RouteGuards.test.tsx index c0d59a7..946d1bb 100644 --- a/frontend/src/components/RouteGuards.test.tsx +++ b/frontend/src/components/RouteGuards.test.tsx @@ -3,125 +3,125 @@ import { MemoryRouter, Route, Routes } from "react-router-dom"; import { beforeEach, describe, expect, it } from "vitest"; import { mockUser } from "../../test/fixtures/user.fixture"; import { useAuthStore } from "../store/useAuthStore"; -import { ProtectedRoute, PublicRoute } from "./RouteGuards"; +import { ProtectedRoute, AutoRedirectRoute } from "./RouteGuards"; function renderGuard(ui: React.ReactNode, mountPath: "/protected" | "/public") { - return render( - - - Login Page} /> - Drawer Page} /> - - - - , - ); + return render( + + + Login Page} /> + Drawer Page} /> + + + + , + ); } beforeEach(() => { - useAuthStore.setState({ - accessToken: null, - user: null, - isInitializing: true, - }); + useAuthStore.setState({ + accessToken: null, + user: null, + isInitializing: true, + }); }); describe("ProtectedRoute", () => { - it("should show SplashScreen while auth is initializing", () => { - useAuthStore.setState({ - isInitializing: true, - accessToken: null, - user: null, + it("should show SplashScreen while auth is initializing", () => { + useAuthStore.setState({ + isInitializing: true, + accessToken: null, + user: null, + }); + renderGuard( + +
Secret
+
, + "/protected", + ); + + expect(screen.getByText(/Unsealing/i)).toBeInTheDocument(); + expect(screen.queryByText("Secret")).not.toBeInTheDocument(); }); - renderGuard( - -
Secret
-
, - "/protected", - ); - expect(screen.getByText(/Unsealing/i)).toBeInTheDocument(); - expect(screen.queryByText("Secret")).not.toBeInTheDocument(); - }); - - it("should redirect unauthenticated users to /login", () => { - useAuthStore.setState({ - isInitializing: false, - accessToken: null, - user: null, + it("should redirect unauthenticated users to /login", () => { + useAuthStore.setState({ + isInitializing: false, + accessToken: null, + user: null, + }); + renderGuard( + +
Secret
+
, + "/protected", + ); + expect(screen.getByText("Login Page")).toBeInTheDocument(); + expect(screen.queryByText("Secret")).not.toBeInTheDocument(); }); - renderGuard( - -
Secret
-
, - "/protected", - ); - expect(screen.getByText("Login Page")).toBeInTheDocument(); - expect(screen.queryByText("Secret")).not.toBeInTheDocument(); - }); - it("should render page for authenticated users", () => { - useAuthStore.setState({ - isInitializing: false, - accessToken: "token", - user: mockUser, + it("should render page for authenticated users", () => { + useAuthStore.setState({ + isInitializing: false, + accessToken: "token", + user: mockUser, + }); + renderGuard( + +
Secret
+
, + "/protected", + ); + + expect(screen.getByText("Secret")).toBeInTheDocument(); }); - renderGuard( - -
Secret
-
, - "/protected", - ); - - expect(screen.getByText("Secret")).toBeInTheDocument(); - }); }); describe("PublicRoute", () => { - it("should show SplashScreen while auth is initializing", () => { - useAuthStore.setState({ - isInitializing: true, - accessToken: null, - user: null, + it("should show SplashScreen while auth is initializing", () => { + useAuthStore.setState({ + isInitializing: true, + accessToken: null, + user: null, + }); + renderGuard( + +
Login Page
+
, + "/public", + ); + expect(screen.getByText(/Unsealing/i)).toBeInTheDocument(); + expect(screen.queryByText("Login Page")).not.toBeInTheDocument(); }); - renderGuard( - -
Login Page
-
, - "/public", - ); - expect(screen.getByText(/Unsealing/i)).toBeInTheDocument(); - expect(screen.queryByText("Login Page")).not.toBeInTheDocument(); - }); - it("should redirect authenticated users to /drawer", () => { - useAuthStore.setState({ - isInitializing: false, - accessToken: "token", - user: mockUser, + it("should redirect authenticated users to /drawer", () => { + useAuthStore.setState({ + isInitializing: false, + accessToken: "token", + user: mockUser, + }); + renderGuard( + +
Login Form
+
, + "/public", + ); + expect(screen.getByText("Drawer Page")).toBeInTheDocument(); + expect(screen.queryByText("Login Form")).not.toBeInTheDocument(); }); - renderGuard( - -
Login Form
-
, - "/public", - ); - expect(screen.getByText("Drawer Page")).toBeInTheDocument(); - expect(screen.queryByText("Login Form")).not.toBeInTheDocument(); - }); - it("should render page for unauthenticated users", () => { - useAuthStore.setState({ - isInitializing: false, - accessToken: null, - user: null, + it("should render page for unauthenticated users", () => { + useAuthStore.setState({ + isInitializing: false, + accessToken: null, + user: null, + }); + renderGuard( + +
Login Form
+
, + "/public", + ); + expect(screen.getByText("Login Form")).toBeInTheDocument(); }); - renderGuard( - -
Login Form
-
, - "/public", - ); - expect(screen.getByText("Login Form")).toBeInTheDocument(); - }); }); diff --git a/frontend/src/components/RouteGuards.tsx b/frontend/src/components/RouteGuards.tsx index 5c4d53e..59e9b23 100644 --- a/frontend/src/components/RouteGuards.tsx +++ b/frontend/src/components/RouteGuards.tsx @@ -9,30 +9,30 @@ import SplashScreen from "./SplashScreen"; * state so the Login component can link them back after sign-in */ export function ProtectedRoute({ children }: { children: React.ReactNode }) { - const { isAuthenticated, isInitializing } = useAuth(); - const location = useLocation(); + const { isAuthenticated, isInitializing } = useAuth(); + const location = useLocation(); - if (isInitializing) return ; + if (isInitializing) return ; - if (!isAuthenticated) { - return ; - } + if (!isAuthenticated) { + return ; + } - return <>{children}; + return <>{children}; } /** - * Public - auth route guard. + * Auto-redirect - auth route guard. * If authenticated, redirect all the auth related flows to the drawer */ -export function PublicRoute({ children }: { children: React.ReactNode }) { - const { isAuthenticated, isInitializing } = useAuth(); +export function AutoRedirectRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated, isInitializing } = useAuth(); - if (isInitializing) return ; + if (isInitializing) return ; - if (isAuthenticated) { - return ; - } + if (isAuthenticated) { + return ; + } - return <>{children}; + return <>{children}; } -- 2.52.0 From 167b1d2875fbaf805a03fbdcc33529c769e1095a Mon Sep 17 00:00:00 2001 From: me Date: Thu, 7 May 2026 04:59:55 +0530 Subject: [PATCH 02/11] feat: use autoredirect for homepage --- frontend/src/App.tsx | 150 ++++++++++++++++++++++--------------------- 1 file changed, 77 insertions(+), 73 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4db6ed3..9d03587 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { lazy, Suspense, useEffect, useRef } from "react"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; -import { ProtectedRoute, PublicRoute } from "./components/RouteGuards"; +import { ProtectedRoute, AutoRedirectRoute } from "./components/RouteGuards"; import SplashScreen from "./components/SplashScreen"; import { ROUTES } from "./config/routes"; import { useAuth } from "./hooks/useAuth"; @@ -16,81 +16,85 @@ const VerifyEmail = lazy(() => import("./pages/VerifyEmail")); const About = lazy(() => import("./pages/About")); export default function App() { - const { initialize, isInitializing } = useAuth(); - const authInitialized = useRef(false); + const { initialize, isInitializing } = useAuth(); + const authInitialized = useRef(false); - useEffect(() => { - if (authInitialized.current) return; - authInitialized.current = true; - initialize().then(); - }, [initialize]); + useEffect(() => { + if (authInitialized.current) return; + authInitialized.current = true; + initialize().then(); + }, [initialize]); - if (isInitializing) { - return ; - } + if (isInitializing) { + return ; + } - return ( - -
- }> - - } /> + return ( + +
+ }> + + + + + } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - - - - } - /> - - - - } - /> - } /> - } /> - } /> - - -
-
- ); + + + + } + /> + + + + } + /> + } /> + } /> + } /> +
+
+
+
+ ); } -- 2.52.0 From 06f5c2d11942f53f75d7a74902746be5ee0caf37 Mon Sep 17 00:00:00 2001 From: me Date: Thu, 7 May 2026 05:52:52 +0530 Subject: [PATCH 03/11] refactor: add image logo variant and subtle texts on home --- frontend/src/components/Logo.tsx | 85 ++-- frontend/src/pages/Home.tsx | 755 ++++++++++++++++--------------- 2 files changed, 425 insertions(+), 415 deletions(-) diff --git a/frontend/src/components/Logo.tsx b/frontend/src/components/Logo.tsx index 27151c5..80a16f9 100644 --- a/frontend/src/components/Logo.tsx +++ b/frontend/src/components/Logo.tsx @@ -2,47 +2,54 @@ import { DotIcon } from "@phosphor-icons/react"; import "@fontsource/knewave/400.css"; interface LogoProps { - scale?: number; - type?: "inline" | "mono" | "logo"; + scale?: number; + type?: "inline" | "mono" | "logo" | null; + ul?: boolean; } -export default function Logo({ scale = 1, type = "logo" }: LogoProps) { - if (type === "inline") { - return ( - - pi. ku - .  - - ); - } +export default function Logo({ scale = 1, type = null, ul = false }: LogoProps) { + if (type === "inline") { + return ( + + pi. ku + .  + + ); + } - if (type === "mono") { - return ( - - pi. ku. - - ); - } + if (type === "mono") { + return ( + + pi. ku. + + ); + } - return ( -
- Pi - -  Ku - -
- ); + if (type === "logo") { + return ( + Pi. Ku. logo + ); + } + + return ( +
+ Pi + +  Ku + +
+ ); } diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 697f8d7..0382c07 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,10 +1,10 @@ import { InfoIcon } from "@phosphor-icons/react"; import { ReactLenis } from "lenis/react"; import { - motion, - useMotionValueEvent, - useScroll, - useTransform, + motion, + useMotionValueEvent, + useScroll, + useTransform, } from "motion/react"; import { useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -14,384 +14,387 @@ import Saajan from "../components/ui/Saajan.tsx"; import { ROUTES } from "../config/routes.ts"; import { formatDate } from "../utils/dateFormat.ts"; +import "@fontsource/space-mono/index.css"; +import "@fontsource/architects-daughter/index.css"; + export default function Home() { - const sectionContainer1 = useRef(null); - const { scrollYProgress } = useScroll({ - target: sectionContainer1, - }); - const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true); - const [flapOpen, setFlapOpen] = useState(false); - const [recipient, setRecipient] = useState("someone dear"); - const [ignite, setIgnite] = useState(false); + const sectionContainer1 = useRef(null); + const { scrollYProgress } = useScroll({ + target: sectionContainer1, + }); + const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true); + const [flapOpen, setFlapOpen] = useState(false); + const [recipient, setRecipient] = useState("someone dear"); + const [ignite, setIgnite] = useState(false); - const navigate = useNavigate(); + const navigate = useNavigate(); - useMotionValueEvent(scrollYProgress, "change", (latestScrollValue) => { - if (latestScrollValue > 0.54) { - setFlapOpen(false); - } else { - setFlapOpen(true); - } - if (latestScrollValue <= 0.6) { - setIsEnvelopeFlipped(true); - } else { - setIsEnvelopeFlipped(false); - } - if (latestScrollValue > 0.68) { - setRecipient("future me"); - } else { - setRecipient("someone dear"); - } - if (latestScrollValue > 0.77) { - setIgnite(true); - } else { - setIgnite(false); - } - }); + useMotionValueEvent(scrollYProgress, "change", (latestScrollValue) => { + if (latestScrollValue > 0.54) { + setFlapOpen(false); + } else { + setFlapOpen(true); + } + if (latestScrollValue <= 0.6) { + setIsEnvelopeFlipped(true); + } else { + setIsEnvelopeFlipped(false); + } + if (latestScrollValue > 0.68) { + setRecipient("future me"); + } else { + setRecipient("someone dear"); + } + if (latestScrollValue > 0.77) { + setIgnite(true); + } else { + setIgnite(false); + } + }); - return ( - -
-
- {/* Intro */} - -

- You've been carrying something -

-

- unsaid -

-
+ return ( + +
+
+ {/* Intro */} + +

+ You've been carrying something +

+ + unsaid + +
- -
- and that's okay... -
-
- {/* pi. ku. */} - - - - is a{" "} - - safe space - - ,
- - where you can - -
-
+ +
+ and that's okay... +
+
+ {/* pi. ku. */} + + + + is a{" "} + + safe space + + ,
+ + where you can + +
+
-
- - pen down your unsaid words into{" "} - - letters - - . - - {/* Seal */} - - seal it{" "} - - secure - {" "} - and{" "} - - private - - . - - {/* Send / vault */} - - send it to{" "} - - someone dear - - - - {" "} - or{" "} - - - yourself in the future - - . - - - {/* Burn */} - - and even burn it{" "} - to release the burden. - - {/* Outro */} - - You've been carrying it long enough. - - {/* CTA */} - - - - -
+
+ + pen down your unsaid words into{" "} + + letters + + . + + {/* Seal */} + + seal it{" "} + + secure + {" "} + and{" "} + + private + + . + + {/* Send / vault */} + + send it to{" "} + + someone dear + + + + {" "} + or{" "} + + + yourself in the future + + . + + + {/* Burn */} + + and even burn it{" "} + to release the burden. + + {/* Outro */} + + You've been carrying it long enough. + + {/* CTA */} + + + + +
-
- -
-
-
- letter +
+ +
+
+
+ letter +
+
+
+ {/* Envelope */} + + { }} + isFlip={isEnvelopeFlipped} + openFlap={flapOpen} + /> + + {/* Saajan */} + + + + {/* Orb */} + +
+
-
- - {/* Envelope */} - - {}} - isFlip={isEnvelopeFlipped} - openFlap={flapOpen} - /> - - {/* Saajan */} - - - - {/* Orb */} - -
-
-
-
-
- ); +
+
+ ); } -- 2.52.0 From b91d2a45418e75e185e188f9924eb1a19b86d241 Mon Sep 17 00:00:00 2001 From: me Date: Thu, 7 May 2026 15:23:20 +0530 Subject: [PATCH 04/11] refactor: whitespace fixes --- frontend/src/App.tsx | 157 ++-- frontend/src/components/Logo.tsx | 101 +-- frontend/src/components/RouteGuards.test.tsx | 200 ++--- frontend/src/components/RouteGuards.tsx | 26 +- frontend/src/pages/Home.tsx | 752 +++++++++---------- 5 files changed, 624 insertions(+), 612 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9d03587..bf5cc26 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { lazy, Suspense, useEffect, useRef } from "react"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; -import { ProtectedRoute, AutoRedirectRoute } from "./components/RouteGuards"; +import { AutoRedirectRoute, ProtectedRoute } from "./components/RouteGuards"; import SplashScreen from "./components/SplashScreen"; import { ROUTES } from "./config/routes"; import { useAuth } from "./hooks/useAuth"; @@ -16,85 +16,88 @@ const VerifyEmail = lazy(() => import("./pages/VerifyEmail")); const About = lazy(() => import("./pages/About")); export default function App() { - const { initialize, isInitializing } = useAuth(); - const authInitialized = useRef(false); + const { initialize, isInitializing } = useAuth(); + const authInitialized = useRef(false); - useEffect(() => { - if (authInitialized.current) return; - authInitialized.current = true; - initialize().then(); - }, [initialize]); + useEffect(() => { + if (authInitialized.current) return; + authInitialized.current = true; + initialize().then(); + }, [initialize]); - if (isInitializing) { - return ; - } + if (isInitializing) { + return ; + } - return ( - -
- }> - - - - - } /> + return ( + +
+ }> + + + + + } + /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - - - - } - /> - - - - } - /> - } /> - } /> - } /> - - -
-
- ); + + + + } + /> + + + + } + /> + } /> + } /> + } /> +
+
+
+
+ ); } diff --git a/frontend/src/components/Logo.tsx b/frontend/src/components/Logo.tsx index 80a16f9..1ff3e79 100644 --- a/frontend/src/components/Logo.tsx +++ b/frontend/src/components/Logo.tsx @@ -2,54 +2,63 @@ import { DotIcon } from "@phosphor-icons/react"; import "@fontsource/knewave/400.css"; interface LogoProps { - scale?: number; - type?: "inline" | "mono" | "logo" | null; - ul?: boolean; + scale?: number; + type?: "inline" | "mono" | "logo" | null; + ul?: boolean; } -export default function Logo({ scale = 1, type = null, ul = false }: LogoProps) { - if (type === "inline") { - return ( - - pi. ku - .  - - ); - } - - if (type === "mono") { - return ( - - pi. ku. - - ); - } - - if (type === "logo") { - return ( - Pi. Ku. logo - ); - } - +export default function Logo({ + scale = 1, + type = null, + ul = false, +}: LogoProps) { + if (type === "inline") { return ( -
- Pi - -  Ku - -
+ + pi. ku + .  + ); + } + + if (type === "mono") { + return ( + + pi. ku. + + ); + } + + if (type === "logo") { + return ( + Pi. Ku. logo + ); + } + + return ( +
+ Pi + +  Ku + +
+ ); } diff --git a/frontend/src/components/RouteGuards.test.tsx b/frontend/src/components/RouteGuards.test.tsx index 946d1bb..5cb34a3 100644 --- a/frontend/src/components/RouteGuards.test.tsx +++ b/frontend/src/components/RouteGuards.test.tsx @@ -3,125 +3,125 @@ import { MemoryRouter, Route, Routes } from "react-router-dom"; import { beforeEach, describe, expect, it } from "vitest"; import { mockUser } from "../../test/fixtures/user.fixture"; import { useAuthStore } from "../store/useAuthStore"; -import { ProtectedRoute, AutoRedirectRoute } from "./RouteGuards"; +import { AutoRedirectRoute, ProtectedRoute } from "./RouteGuards"; function renderGuard(ui: React.ReactNode, mountPath: "/protected" | "/public") { - return render( - - - Login Page} /> - Drawer Page} /> - - - - , - ); + return render( + + + Login Page} /> + Drawer Page} /> + + + + , + ); } beforeEach(() => { - useAuthStore.setState({ - accessToken: null, - user: null, - isInitializing: true, - }); + useAuthStore.setState({ + accessToken: null, + user: null, + isInitializing: true, + }); }); describe("ProtectedRoute", () => { - it("should show SplashScreen while auth is initializing", () => { - useAuthStore.setState({ - isInitializing: true, - accessToken: null, - user: null, - }); - renderGuard( - -
Secret
-
, - "/protected", - ); - - expect(screen.getByText(/Unsealing/i)).toBeInTheDocument(); - expect(screen.queryByText("Secret")).not.toBeInTheDocument(); + it("should show SplashScreen while auth is initializing", () => { + useAuthStore.setState({ + isInitializing: true, + accessToken: null, + user: null, }); + renderGuard( + +
Secret
+
, + "/protected", + ); - it("should redirect unauthenticated users to /login", () => { - useAuthStore.setState({ - isInitializing: false, - accessToken: null, - user: null, - }); - renderGuard( - -
Secret
-
, - "/protected", - ); - expect(screen.getByText("Login Page")).toBeInTheDocument(); - expect(screen.queryByText("Secret")).not.toBeInTheDocument(); + expect(screen.getByText(/Unsealing/i)).toBeInTheDocument(); + expect(screen.queryByText("Secret")).not.toBeInTheDocument(); + }); + + it("should redirect unauthenticated users to /login", () => { + useAuthStore.setState({ + isInitializing: false, + accessToken: null, + user: null, }); + renderGuard( + +
Secret
+
, + "/protected", + ); + expect(screen.getByText("Login Page")).toBeInTheDocument(); + expect(screen.queryByText("Secret")).not.toBeInTheDocument(); + }); - it("should render page for authenticated users", () => { - useAuthStore.setState({ - isInitializing: false, - accessToken: "token", - user: mockUser, - }); - renderGuard( - -
Secret
-
, - "/protected", - ); - - expect(screen.getByText("Secret")).toBeInTheDocument(); + it("should render page for authenticated users", () => { + useAuthStore.setState({ + isInitializing: false, + accessToken: "token", + user: mockUser, }); + renderGuard( + +
Secret
+
, + "/protected", + ); + + expect(screen.getByText("Secret")).toBeInTheDocument(); + }); }); describe("PublicRoute", () => { - it("should show SplashScreen while auth is initializing", () => { - useAuthStore.setState({ - isInitializing: true, - accessToken: null, - user: null, - }); - renderGuard( - -
Login Page
-
, - "/public", - ); - expect(screen.getByText(/Unsealing/i)).toBeInTheDocument(); - expect(screen.queryByText("Login Page")).not.toBeInTheDocument(); + it("should show SplashScreen while auth is initializing", () => { + useAuthStore.setState({ + isInitializing: true, + accessToken: null, + user: null, }); + renderGuard( + +
Login Page
+
, + "/public", + ); + expect(screen.getByText(/Unsealing/i)).toBeInTheDocument(); + expect(screen.queryByText("Login Page")).not.toBeInTheDocument(); + }); - it("should redirect authenticated users to /drawer", () => { - useAuthStore.setState({ - isInitializing: false, - accessToken: "token", - user: mockUser, - }); - renderGuard( - -
Login Form
-
, - "/public", - ); - expect(screen.getByText("Drawer Page")).toBeInTheDocument(); - expect(screen.queryByText("Login Form")).not.toBeInTheDocument(); + it("should redirect authenticated users to /drawer", () => { + useAuthStore.setState({ + isInitializing: false, + accessToken: "token", + user: mockUser, }); + renderGuard( + +
Login Form
+
, + "/public", + ); + expect(screen.getByText("Drawer Page")).toBeInTheDocument(); + expect(screen.queryByText("Login Form")).not.toBeInTheDocument(); + }); - it("should render page for unauthenticated users", () => { - useAuthStore.setState({ - isInitializing: false, - accessToken: null, - user: null, - }); - renderGuard( - -
Login Form
-
, - "/public", - ); - expect(screen.getByText("Login Form")).toBeInTheDocument(); + it("should render page for unauthenticated users", () => { + useAuthStore.setState({ + isInitializing: false, + accessToken: null, + user: null, }); + renderGuard( + +
Login Form
+
, + "/public", + ); + expect(screen.getByText("Login Form")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/RouteGuards.tsx b/frontend/src/components/RouteGuards.tsx index 59e9b23..9fa20af 100644 --- a/frontend/src/components/RouteGuards.tsx +++ b/frontend/src/components/RouteGuards.tsx @@ -9,16 +9,16 @@ import SplashScreen from "./SplashScreen"; * state so the Login component can link them back after sign-in */ export function ProtectedRoute({ children }: { children: React.ReactNode }) { - const { isAuthenticated, isInitializing } = useAuth(); - const location = useLocation(); + const { isAuthenticated, isInitializing } = useAuth(); + const location = useLocation(); - if (isInitializing) return ; + if (isInitializing) return ; - if (!isAuthenticated) { - return ; - } + if (!isAuthenticated) { + return ; + } - return <>{children}; + return <>{children}; } /** @@ -26,13 +26,13 @@ export function ProtectedRoute({ children }: { children: React.ReactNode }) { * If authenticated, redirect all the auth related flows to the drawer */ export function AutoRedirectRoute({ children }: { children: React.ReactNode }) { - const { isAuthenticated, isInitializing } = useAuth(); + const { isAuthenticated, isInitializing } = useAuth(); - if (isInitializing) return ; + if (isInitializing) return ; - if (isAuthenticated) { - return ; - } + if (isAuthenticated) { + return ; + } - return <>{children}; + return <>{children}; } diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 0382c07..142d7da 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,10 +1,10 @@ import { InfoIcon } from "@phosphor-icons/react"; import { ReactLenis } from "lenis/react"; import { - motion, - useMotionValueEvent, - useScroll, - useTransform, + motion, + useMotionValueEvent, + useScroll, + useTransform, } from "motion/react"; import { useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -18,383 +18,383 @@ import "@fontsource/space-mono/index.css"; import "@fontsource/architects-daughter/index.css"; export default function Home() { - const sectionContainer1 = useRef(null); - const { scrollYProgress } = useScroll({ - target: sectionContainer1, - }); - const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true); - const [flapOpen, setFlapOpen] = useState(false); - const [recipient, setRecipient] = useState("someone dear"); - const [ignite, setIgnite] = useState(false); + const sectionContainer1 = useRef(null); + const { scrollYProgress } = useScroll({ + target: sectionContainer1, + }); + const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true); + const [flapOpen, setFlapOpen] = useState(false); + const [recipient, setRecipient] = useState("someone dear"); + const [ignite, setIgnite] = useState(false); - const navigate = useNavigate(); + const navigate = useNavigate(); - useMotionValueEvent(scrollYProgress, "change", (latestScrollValue) => { - if (latestScrollValue > 0.54) { - setFlapOpen(false); - } else { - setFlapOpen(true); - } - if (latestScrollValue <= 0.6) { - setIsEnvelopeFlipped(true); - } else { - setIsEnvelopeFlipped(false); - } - if (latestScrollValue > 0.68) { - setRecipient("future me"); - } else { - setRecipient("someone dear"); - } - if (latestScrollValue > 0.77) { - setIgnite(true); - } else { - setIgnite(false); - } - }); + useMotionValueEvent(scrollYProgress, "change", (latestScrollValue) => { + if (latestScrollValue > 0.54) { + setFlapOpen(false); + } else { + setFlapOpen(true); + } + if (latestScrollValue <= 0.6) { + setIsEnvelopeFlipped(true); + } else { + setIsEnvelopeFlipped(false); + } + if (latestScrollValue > 0.68) { + setRecipient("future me"); + } else { + setRecipient("someone dear"); + } + if (latestScrollValue > 0.77) { + setIgnite(true); + } else { + setIgnite(false); + } + }); - return ( - -
+
+
+ {/* Intro */} + +

+ You've been carrying something +

+ + unsaid + +
+ + +
+ and that's okay... +
+
+ {/* pi. ku. */} + + + -
- {/* Intro */} - -

- You've been carrying something -

- - unsaid - -
+ is a{" "} + + safe space + + ,
+ + where you can + + + - -
- and that's okay... -
-
- {/* pi. ku. */} - - - - is a{" "} - - safe space - - ,
- - where you can - -
-
+
+ + pen down your unsaid words into{" "} + + letters + + . + + {/* Seal */} + + seal it{" "} + + secure + {" "} + and{" "} + + private + + . + + {/* Send / vault */} + + send it to{" "} + + someone dear + + + + {" "} + or{" "} + + + yourself in the future + + . + + + {/* Burn */} + + and even burn it{" "} + to release the burden. + + {/* Outro */} + + You've been carrying it long enough. + + {/* CTA */} + + + + +
-
- - pen down your unsaid words into{" "} - - letters - - . - - {/* Seal */} - - seal it{" "} - - secure - {" "} - and{" "} - - private - - . - - {/* Send / vault */} - - send it to{" "} - - someone dear - - - - {" "} - or{" "} - - - yourself in the future - - . - - - {/* Burn */} - - and even burn it{" "} - to release the burden. - - {/* Outro */} - - You've been carrying it long enough. - - {/* CTA */} - - - - -
- -
- -
-
-
- letter -
-
-
- {/* Envelope */} - - { }} - isFlip={isEnvelopeFlipped} - openFlap={flapOpen} - /> - - {/* Saajan */} - - - - {/* Orb */} - -
-
+
+ +
+
+
+ letter
-
- - ); + + + {/* Envelope */} + + {}} + isFlip={isEnvelopeFlipped} + openFlap={flapOpen} + /> + + {/* Saajan */} + + + + {/* Orb */} + +
+
+ +
+
+ ); } -- 2.52.0 From 61f005fddb81b663519863b19b7dcdb45102c579 Mon Sep 17 00:00:00 2001 From: me Date: Thu, 7 May 2026 16:25:31 +0530 Subject: [PATCH 05/11] feat: initialize textbox dimensions during viewport sync --- frontend/src/components/editor/ComposeCanvas.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/editor/ComposeCanvas.tsx b/frontend/src/components/editor/ComposeCanvas.tsx index 6c97b1e..6284ce5 100644 --- a/frontend/src/components/editor/ComposeCanvas.tsx +++ b/frontend/src/components/editor/ComposeCanvas.tsx @@ -122,6 +122,7 @@ export function ComposeCanvas({ // re-calculates height based on content and applies the zoom transform const syncViewport = useCallback(() => { if (!(fabricRef.current && wrapperRef.current)) return; + textboxRef.current.initDimensions(); const minHeight = initialData?.canvasHeight ?? DEFAULT_LOGICAL_HEIGHT; logicalSizeRef.current.height = measureLogicalContentHeight( -- 2.52.0 From aeead98570662149ef5c376e55011e4d773f1ba6 Mon Sep 17 00:00:00 2001 From: me Date: Thu, 7 May 2026 18:08:21 +0530 Subject: [PATCH 06/11] feat: redesign drawer component with icon integration and refactor count section --- .../src/components/drawer/DrawerSection.tsx | 39 ++++++++++++++----- frontend/src/components/drawer/LetterItem.tsx | 4 +- frontend/src/pages/Drawer.tsx | 33 ++++++++++------ 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/drawer/DrawerSection.tsx b/frontend/src/components/drawer/DrawerSection.tsx index f189bf3..cdea2c1 100644 --- a/frontend/src/components/drawer/DrawerSection.tsx +++ b/frontend/src/components/drawer/DrawerSection.tsx @@ -3,19 +3,23 @@ import { GearFineIcon } from "@phosphor-icons/react"; interface DrawerSectionProps { id: string; title: string; - count: string; + count: number; + subtext: string; isOpen: boolean; onClick: () => void; children: React.ReactNode; + icon: React.ReactNode; } export function DrawerSection({ id, title, count, + subtext, isOpen, onClick, children, + icon, }: DrawerSectionProps) { return (
- {children} +
+ {children} + {count === 0 && ( +
+ This drawer remains silent... +
+ )} +
-
+
For your unsaid.
{!showWelcomeLetter && ( -- 2.52.0 From b29218b6e63463b4a3c97ee0124bbc537e1bca74 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 8 May 2026 05:22:51 +0530 Subject: [PATCH 07/11] Merge branch 'refactor/foritify_tests' of https://git.ramvignesh.dev/me/pi-ku into feature/home_revamp --- frontend/src/components/RouteGuards.test.tsx | 36 +- frontend/src/components/SplashScreen.tsx | 2 +- .../src/components/drawer/DrawerSection.tsx | 166 ++++---- .../src/components/drawer/PasskeyModal.tsx | 2 +- .../src/components/reader/EnvelopeReveal.tsx | 2 +- frontend/src/components/ui/LogModal.tsx | 2 +- frontend/src/pages/Drawer.test.tsx | 188 ++++----- frontend/src/pages/Drawer.tsx | 358 +++++++++--------- frontend/src/pages/Editor.test.tsx | 18 +- frontend/src/pages/Login.test.tsx | 9 +- frontend/src/pages/Login.tsx | 2 +- frontend/src/pages/Reader.test.tsx | 10 +- 12 files changed, 392 insertions(+), 403 deletions(-) diff --git a/frontend/src/components/RouteGuards.test.tsx b/frontend/src/components/RouteGuards.test.tsx index 5cb34a3..7eea6d4 100644 --- a/frontend/src/components/RouteGuards.test.tsx +++ b/frontend/src/components/RouteGuards.test.tsx @@ -9,8 +9,8 @@ function renderGuard(ui: React.ReactNode, mountPath: "/protected" | "/public") { return render( - Login Page
} /> - Drawer Page} /> + Login Page} /> + Drawer Page} /> @@ -35,13 +35,13 @@ describe("ProtectedRoute", () => { }); renderGuard( -
Secret
+
Secret
, "/protected", ); - expect(screen.getByText(/Unsealing/i)).toBeInTheDocument(); - expect(screen.queryByText("Secret")).not.toBeInTheDocument(); + expect(screen.getByTestId("splash-screen")).toBeInTheDocument(); + expect(screen.queryByTestId("secret-page")).not.toBeInTheDocument(); }); it("should redirect unauthenticated users to /login", () => { @@ -52,12 +52,12 @@ describe("ProtectedRoute", () => { }); renderGuard( -
Secret
+
Secret
, "/protected", ); - expect(screen.getByText("Login Page")).toBeInTheDocument(); - expect(screen.queryByText("Secret")).not.toBeInTheDocument(); + expect(screen.getByTestId("login-page")).toBeInTheDocument(); + expect(screen.queryByTestId("secret-page")).not.toBeInTheDocument(); }); it("should render page for authenticated users", () => { @@ -68,12 +68,12 @@ describe("ProtectedRoute", () => { }); renderGuard( -
Secret
+
Secret
, "/protected", ); - expect(screen.getByText("Secret")).toBeInTheDocument(); + expect(screen.getByTestId("secret-page")).toBeInTheDocument(); }); }); @@ -86,12 +86,12 @@ describe("PublicRoute", () => { }); renderGuard( -
Login Page
+
Login Page
, "/public", ); - expect(screen.getByText(/Unsealing/i)).toBeInTheDocument(); - expect(screen.queryByText("Login Page")).not.toBeInTheDocument(); + expect(screen.getByTestId("splash-screen")).toBeInTheDocument(); + expect(screen.queryByTestId("mock-login-page")).not.toBeInTheDocument(); }); it("should redirect authenticated users to /drawer", () => { @@ -102,12 +102,12 @@ describe("PublicRoute", () => { }); renderGuard( -
Login Form
+
Login Form
, "/public", ); - expect(screen.getByText("Drawer Page")).toBeInTheDocument(); - expect(screen.queryByText("Login Form")).not.toBeInTheDocument(); + expect(screen.getByTestId("drawer-page")).toBeInTheDocument(); + expect(screen.queryByTestId("login-form")).not.toBeInTheDocument(); }); it("should render page for unauthenticated users", () => { @@ -118,10 +118,10 @@ describe("PublicRoute", () => { }); renderGuard( -
Login Form
+
Login Form
, "/public", ); - expect(screen.getByText("Login Form")).toBeInTheDocument(); + expect(screen.getByTestId("login-form")).toBeInTheDocument(); }); }); diff --git a/frontend/src/components/SplashScreen.tsx b/frontend/src/components/SplashScreen.tsx index ca6ce1a..7c4297e 100644 --- a/frontend/src/components/SplashScreen.tsx +++ b/frontend/src/components/SplashScreen.tsx @@ -3,7 +3,7 @@ import Logo from "./Logo"; export default function SplashScreen() { return ( -
+
diff --git a/frontend/src/components/drawer/DrawerSection.tsx b/frontend/src/components/drawer/DrawerSection.tsx index cdea2c1..7fe6ff0 100644 --- a/frontend/src/components/drawer/DrawerSection.tsx +++ b/frontend/src/components/drawer/DrawerSection.tsx @@ -1,97 +1,95 @@ import { GearFineIcon } from "@phosphor-icons/react"; interface DrawerSectionProps { - id: string; - title: string; - count: number; - subtext: string; - isOpen: boolean; - onClick: () => void; - children: React.ReactNode; - icon: React.ReactNode; + id: string; + title: string; + count: number; + subtext: string; + isOpen: boolean; + onClick: () => void; + children: React.ReactNode; + icon: React.ReactNode; } export function DrawerSection({ - id, - title, - count, - subtext, - isOpen, - onClick, - children, - icon, + id, + title, + count, + subtext, + isOpen, + onClick, + children, + icon, }: DrawerSectionProps) { - return ( -
-
+ return (
- {children} - {count === 0 && ( -
- This drawer remains silent... +
+
+ {children} + {count === 0 && ( +

+ This drawer remains silent +

+ )} +
- )} -
-
- -
- ); + {id === "vault" ? ( + + ) : ( +
+
+
+ )} + +
+ ); } diff --git a/frontend/src/components/drawer/PasskeyModal.tsx b/frontend/src/components/drawer/PasskeyModal.tsx index bf1b5ad..04ba0ed 100644 --- a/frontend/src/components/drawer/PasskeyModal.tsx +++ b/frontend/src/components/drawer/PasskeyModal.tsx @@ -12,7 +12,7 @@ export function PasskeyModal() { className="text-primary mx-auto mb-8 animate-pulse" weight="duotone" /> -

+

You've been away a while.

diff --git a/frontend/src/components/reader/EnvelopeReveal.tsx b/frontend/src/components/reader/EnvelopeReveal.tsx index c76cf32..a830307 100644 --- a/frontend/src/components/reader/EnvelopeReveal.tsx +++ b/frontend/src/components/reader/EnvelopeReveal.tsx @@ -123,7 +123,7 @@ export function EnvelopeReveal({ to -

{recipient}

+

{recipient}

{date}

)} - {message} + {message} {log && ( <>
diff --git a/frontend/src/pages/Drawer.test.tsx b/frontend/src/pages/Drawer.test.tsx index b79aed3..ae4ddd6 100644 --- a/frontend/src/pages/Drawer.test.tsx +++ b/frontend/src/pages/Drawer.test.tsx @@ -9,116 +9,116 @@ import Drawer from "./Drawer"; vi.mock("../hooks/useLetters"); vi.mock("../components/drawer/WelcomeLetterOverlay", () => ({ - WelcomeLetterOverlay: ({ onComplete }: WelcomeLetterOverlayProps) => ( -
- -
- ), + WelcomeLetterOverlay: ({ onComplete }: WelcomeLetterOverlayProps) => ( +
+ +
+ ), })); describe("Drawer Page", () => { - beforeEach(() => { - // Setup authenticated state for the test - useAuthStore.setState({ - user: mockUser, - accessToken: "fake-token", - isInitializing: false, + beforeEach(() => { + // Setup authenticated state for the test + useAuthStore.setState({ + user: mockUser, + accessToken: "fake-token", + isInitializing: false, + }); + + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: false, + isAuthRequired: false, + }); }); - vi.mocked(useLetters).mockReturnValue({ - drafts: [], - kept: [], - sent: [], - vault: [], - loading: false, - isAuthRequired: false, - }); - }); + it("renders the drawer sections and empty state message", () => { + render( + + + , + ); - it("renders the cabinet sections and empty state message", () => { - render( - - - , - ); - - expect(screen.getByText(/Drafts/i)).toBeInTheDocument(); - expect(screen.getAllByText(/Kept/i).length).toBeGreaterThanOrEqual(1); - expect(screen.getByText(/Vault/i)).toBeInTheDocument(); - expect(screen.getByText(/This drawer remains silent/i)).toBeInTheDocument(); - }); - - it("renders the loading state", () => { - vi.mocked(useLetters).mockReturnValue({ - drafts: [], - kept: [], - sent: [], - vault: [], - loading: true, - isAuthRequired: false, + expect(screen.getByTestId("drawer-section-drafts")).toBeInTheDocument(); + expect(screen.getAllByTestId("drawer-section-title").length).toBeGreaterThanOrEqual(1); + expect(screen.getByTestId("drawer-section-vault")).toBeInTheDocument(); + expect(screen.getByTestId("empty-drawer-message-drafts")).toBeInTheDocument(); }); - render( - - - , - ); + it("renders the loading state", () => { + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: true, + isAuthRequired: false, + }); - expect(screen.getByText(/Opening your cabinet/i)).toBeInTheDocument(); - }); + render( + + + , + ); - it("renders the authentication required modal when api requires auth", () => { - vi.mocked(useLetters).mockReturnValue({ - drafts: [], - kept: [], - sent: [], - vault: [], - loading: false, - isAuthRequired: true, + expect(screen.getByTestId("drawer-loading-state")).toBeInTheDocument(); }); - render( - - - , - ); + it("renders the authentication required modal when api requires auth", () => { + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: false, + isAuthRequired: true, + }); - expect(screen.getByText(/You've been away a while./i)).toBeInTheDocument(); - expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument(); - }); + render( + + + , + ); - it("renders the welcome letter when firstTime state is present", () => { - render( - - - , - ); + expect(screen.getByTestId("passkey-modal-title")).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument(); + }); - expect(screen.getByTestId("welcome-letter-overlay")).toBeInTheDocument(); - }); + it("renders the welcome letter when firstTime state is present", () => { + render( + + + , + ); - it("renders the drawer content when the letter is closed", () => { - render( - - - , - ); + expect(screen.getByTestId("welcome-letter-overlay")).toBeInTheDocument(); + }); - const completeButton = screen.getByTestId("overlay-exit-button"); - fireEvent.click(completeButton); + it("renders the drawer content when the letter is closed", () => { + render( + + + , + ); - expect( - screen.queryByTestId("welcome-letter-overlay"), - ).not.toBeInTheDocument(); - }); + const completeButton = screen.getByTestId("overlay-exit-button"); + fireEvent.click(completeButton); + + expect( + screen.queryByTestId("welcome-letter-overlay"), + ).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/pages/Drawer.tsx b/frontend/src/pages/Drawer.tsx index 284d626..425956a 100644 --- a/frontend/src/pages/Drawer.tsx +++ b/frontend/src/pages/Drawer.tsx @@ -1,9 +1,9 @@ import { - ArchiveIcon, - FeatherIcon, - FileDashedIcon, - PaperPlaneTiltIcon, - VaultIcon, + ArchiveIcon, + FeatherIcon, + FileDashedIcon, + PaperPlaneTiltIcon, + VaultIcon, } from "@phosphor-icons/react"; import { useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; @@ -17,188 +17,188 @@ import { PATHS } from "../config/routes"; import { useAuth } from "../hooks/useAuth"; import { useLetters } from "../hooks/useLetters"; import { - formatRelativeDate, - formatRelativeDateWithoutTime, + formatRelativeDate, + formatRelativeDateWithoutTime, } from "../utils/dateFormat.ts"; export default function Drawer() { - const { user, logout } = useAuth(); + const { user, logout } = useAuth(); - const [openSection, setOpenSection] = useState(null); - const navigate = useNavigate(); - const location = useLocation(); - const [showWelcomeLetter, setShowWelcomeLetter] = useState( - !!location.state?.firstTime, - ); - const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters(); + const [openSection, setOpenSection] = useState(null); + const navigate = useNavigate(); + const location = useLocation(); + const [showWelcomeLetter, setShowWelcomeLetter] = useState( + !!location.state?.firstTime, + ); + const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters(); - if (!user) return null; + if (!user) return null; - const toggleSection = (id: string) => - setOpenSection(openSection === id ? null : id); + const toggleSection = (id: string) => + setOpenSection(openSection === id ? null : id); - return ( -
-
+ return ( +
+
- {showWelcomeLetter && ( - { - setShowWelcomeLetter(false); - navigate(location.pathname, { replace: true, state: {} }); - }} - /> - )} + {showWelcomeLetter && ( + { + setShowWelcomeLetter(false); + navigate(location.pathname, { replace: true, state: {} }); + }} + /> + )} - {isAuthRequired && } -
- -
- Personal Archive + {isAuthRequired && } +
+ +
+ Personal Archive +
+
+ Welcome Back{" "} + {user.full_name} + +
+
+ +
+ {loading ? ( +
+ + + Opening your cabinet... + +
+ ) : ( + <> + toggleSection("drafts")} + icon={} + > + {drafts.map((draft) => ( + + ))} + + + toggleSection("kept")} + icon={} + > + {kept.map((letter) => ( + + ))} + + toggleSection("sent")} + icon={} + > + {sent.map((letter) => ( + + ))} + + toggleSection("vault")} + icon={} + > + {vault.map((letter) => ( + new Date().toISOString()} + /> + ))} + + + )} +
+ + + +
+ For your unsaid. +
+ {!showWelcomeLetter && ( +
+ +
+ )}
-
- Welcome Back{" "} - {user.full_name} - -
-
- -
- {loading ? ( -
- - - Opening your cabinet... - -
- ) : ( - <> - toggleSection("drafts")} - icon={} - > - {drafts.map((draft) => ( - - ))} - - - toggleSection("kept")} - icon={} - > - {kept.map((letter) => ( - - ))} - - toggleSection("sent")} - icon={} - > - {sent.map((letter) => ( - - ))} - - toggleSection("vault")} - icon={} - > - {vault.map((letter) => ( - new Date().toISOString()} - /> - ))} - - - )} -
- - - -
- For your unsaid. -
- {!showWelcomeLetter && ( -
- -
- )} -
- ); + ); } diff --git a/frontend/src/pages/Editor.test.tsx b/frontend/src/pages/Editor.test.tsx index d1e63c4..6b32945 100644 --- a/frontend/src/pages/Editor.test.tsx +++ b/frontend/src/pages/Editor.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { fireEvent, render, screen, waitForElementToBeRemoved } from "@testing-library/react"; import { HttpResponse, http } from "msw"; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -79,9 +79,7 @@ describe("Editor Page", () => { ); // Wait for initial load to complete - await waitFor(() => { - expect(screen.queryByText(/Opening your draft/i)).not.toBeInTheDocument(); - }); + await waitForElementToBeRemoved(() => screen.queryByTestId("opening-draft-overlay")); const canvas = screen.getByTestId("canvas"); expect(canvas.getAttribute("data-readonly")).toBe("false"); @@ -107,9 +105,7 @@ describe("Editor Page", () => { fireEvent.click(confirmVaultBtn); // Wait for save to complete and check readOnly - await waitFor(() => { - expect(screen.getByText(/Your letter is saved/i)).toBeInTheDocument(); - }); + expect(await screen.findByTestId("save-success-toast")).toBeInTheDocument(); expect(canvas.getAttribute("data-readonly")).toBe("true"); expect(screen.getByLabelText(/recipient/i)).toBeDisabled(); @@ -140,9 +136,7 @@ describe("Editor Page", () => { , ); - await waitFor(() => { - expect(screen.queryByText(/Opening your draft/i)).not.toBeInTheDocument(); - }); + await waitForElementToBeRemoved(() => screen.queryByTestId("opening-draft-overlay")); const canvas = screen.getByTestId("canvas"); @@ -156,9 +150,7 @@ describe("Editor Page", () => { if (!secondarySealBtn) throw new Error("Secondary seal button not found"); fireEvent.click(secondarySealBtn); - await waitFor(() => { - expect(screen.getByText(/Your letter is saved/i)).toBeInTheDocument(); - }); + expect(await screen.findByTestId("save-success-toast")).toBeInTheDocument(); expect(canvas.getAttribute("data-readonly")).toBe("true"); expect(screen.getByLabelText(/recipient/i)).toBeDisabled(); diff --git a/frontend/src/pages/Login.test.tsx b/frontend/src/pages/Login.test.tsx index c6f4af3..7f58b75 100644 --- a/frontend/src/pages/Login.test.tsx +++ b/frontend/src/pages/Login.test.tsx @@ -31,7 +31,7 @@ describe("Login Page", () => { await userEvent.type(screen.getByLabelText(/password/i), "password123"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); - expect(await screen.findByText(/technical issues/i)).toBeInTheDocument(); + expect(await screen.findByTestId("login-error-message")).toHaveTextContent(/technical issues/i); }); it.each([ @@ -73,8 +73,8 @@ describe("Login Page", () => { > } /> - Drawer
} /> - Reader
} /> + Drawer
} /> + Reader
} /> , ); @@ -83,6 +83,7 @@ describe("Login Page", () => { await userEvent.type(screen.getByLabelText(/password/i), "password123"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); - expect(await screen.findByText(nextRoute)).toBeInTheDocument(); + const expectedTestId = nextRoute.toLowerCase() === "drawer" ? "drawer-page" : "reader-page"; + expect(await screen.findByTestId(expectedTestId)).toBeInTheDocument(); }); }); diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index c5de9c9..4ed7089 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -89,7 +89,7 @@ export default function Login() { {apiError && (
- {apiError} + {apiError}
)} diff --git a/frontend/src/pages/Reader.test.tsx b/frontend/src/pages/Reader.test.tsx index 8078e1b..50164b1 100644 --- a/frontend/src/pages/Reader.test.tsx +++ b/frontend/src/pages/Reader.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { HttpResponse, http } from "msw"; import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -76,9 +76,7 @@ describe("Reader Page", () => { , ); - await waitFor(() => { - expect(screen.getByText(/Guest/i)).toBeInTheDocument(); - }); + expect(await screen.findByTestId("envelope-recipient")).toHaveTextContent(/Guest/i); }); it("should display an error message if the server request fails", async () => { @@ -100,8 +98,8 @@ describe("Reader Page", () => { ); expect( - await screen.findByText(/Failed to load letter/i), - ).toBeInTheDocument(); + await screen.findByTestId("log-modal-message"), + ).toHaveTextContent(/Failed to load letter/i); }); it("should navigate to the login page with redirect url when the letter has no sharing key and the user is not logged in", async () => { -- 2.52.0 From a2eb22bdbe1a33a5e70f00491c00a907c03f7bbf Mon Sep 17 00:00:00 2001 From: me Date: Fri, 8 May 2026 05:40:36 +0530 Subject: [PATCH 08/11] style: update glass-card border color --- frontend/src/index.css | 102 ++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index f3f211a..ae1cb96 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,77 +2,75 @@ @plugin "daisyui"; @plugin "daisyui/theme" { - name: "piku"; - default: true; - prefersdark: true; - color-scheme: dark; + name: "piku"; + default: true; + prefersdark: true; + color-scheme: dark; - --color-base-100: oklch(14% 0.012 35); - --color-base-200: oklch(18% 0.014 33); - --color-base-300: oklch(22% 0.016 32); - --color-base-content: oklch(82% 0.02 70); + --color-base-100: oklch(14% 0.012 35); + --color-base-200: oklch(18% 0.014 33); + --color-base-300: oklch(22% 0.016 32); + --color-base-content: oklch(82% 0.02 70); - --color-primary: oklch(67% 0.11 78); - --color-primary-content: oklch(15% 0.03 70); + --color-primary: oklch(67% 0.11 78); + --color-primary-content: oklch(15% 0.03 70); - --color-secondary: oklch(48% 0.08 305); - --color-secondary-content: oklch(92% 0.01 305); + --color-secondary: oklch(48% 0.08 305); + --color-secondary-content: oklch(92% 0.01 305); - --color-accent: oklch(55% 0.06 325); - --color-accent-content: oklch(18% 0.03 295); + --color-accent: oklch(55% 0.06 325); + --color-accent-content: oklch(18% 0.03 295); - --color-neutral: oklch(38% 0.02 45); - --color-neutral-content: oklch(80% 0.015 60); + --color-neutral: oklch(38% 0.02 45); + --color-neutral-content: oklch(80% 0.015 60); - --color-info: oklch(60% 0.07 240); - --color-info-content: oklch(95% 0.01 240); - --color-success: oklch(60% 0.08 150); - --color-success-content: oklch(16% 0.03 150); - --color-warning: oklch(68% 0.08 72); - --color-warning-content: oklch(18% 0.03 60); - --color-error: oklch(55% 0.1 22); - --color-error-content: oklch(92% 0.01 22); + --color-info: oklch(60% 0.07 240); + --color-info-content: oklch(95% 0.01 240); + --color-success: oklch(60% 0.08 150); + --color-success-content: oklch(16% 0.03 150); + --color-warning: oklch(68% 0.08 72); + --color-warning-content: oklch(18% 0.03 60); + --color-error: oklch(55% 0.1 22); + --color-error-content: oklch(92% 0.01 22); - --radius-selector: 0.5rem; - --radius-field: 0.375rem; - --radius-box: 0.5rem; + --radius-selector: 0.5rem; + --radius-field: 0.375rem; + --radius-box: 0.5rem; - --depth: 1; - --noise: 0.03; + --depth: 1; + --noise: 0.03; - --border: 1px; + --border: 1px; } @theme { - --font-display: "Playwrite HR Lijeva Variable", cursive; - --font-sans: "Jost Variable", sans-serif; - --font-serif: "Playfair Display Variable", serif; - --font-mono: "Space Mono", monospace; - --font-ink: "Kavivanar", sans-serif; - --font-redact: "Redacted Script", cursive; - --font-slab: "Cutive Mono", monospace; - --font-hand: "Architects Daughter", cursive; - --color-glass-bg: rgba(28, 22, 16, 0.45); - --shadow-warm: 0 20px 50px -12px rgba(30, 20, 12, 0.6); - --radius-xl: 1.5rem; - --color-paper: oklch(97% 0.008 80); - --text-xxs: 10px; - --tracking-widester: 0.5em; - --background-image-vig: radial-gradient( - circle at center, - transparent 0%, - rgba(0, 0, 0, 0.4) 100% - ); + --font-display: "Playwrite HR Lijeva Variable", cursive; + --font-sans: "Jost Variable", sans-serif; + --font-serif: "Playfair Display Variable", serif; + --font-mono: "Space Mono", monospace; + --font-ink: "Kavivanar", sans-serif; + --font-redact: "Redacted Script", cursive; + --font-slab: "Cutive Mono", monospace; + --font-hand: "Architects Daughter", cursive; + --color-glass-bg: rgba(28, 22, 16, 0.45); + --shadow-warm: 0 20px 50px -12px rgba(30, 20, 12, 0.6); + --radius-xl: 1.5rem; + --color-paper: oklch(97% 0.008 80); + --text-xxs: 10px; + --tracking-widester: 0.5em; + --background-image-vig: radial-gradient(circle at center, + transparent 0%, + rgba(0, 0, 0, 0.4) 100%); } .glass-card { - @apply bg-glass-bg backdrop-blur-xl border border-white/5 shadow-warm rounded-xl m-4; + @apply bg-glass-bg backdrop-blur-xl border border-neutral-content/10 shadow-warm rounded-xl m-4; } .ul-wavy { - @apply decoration-primary/40 underline decoration-wavy underline-offset-4; + @apply decoration-primary/40 underline decoration-wavy underline-offset-4; } a { - @apply text-primary underline decoration-base-content/20 underline-offset-4 hover:decoration-primary/60 transition-colors; + @apply text-primary underline decoration-base-content/20 underline-offset-4 hover:decoration-primary/60 transition-colors; } -- 2.52.0 From 27ab3ad4b1b1486c9ebd078da18de8a3a5078eb5 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 8 May 2026 06:06:19 +0530 Subject: [PATCH 09/11] refactor: update logo assets --- frontend/public/logo.svg | 1 + frontend/src/components/Logo.tsx | 104 +++++------ frontend/src/index.css | 6 +- frontend/src/pages/Login.tsx | 234 ++++++++++++------------- frontend/src/pages/Register.tsx | 288 +++++++++++++++---------------- 5 files changed, 317 insertions(+), 316 deletions(-) create mode 100644 frontend/public/logo.svg diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg new file mode 100644 index 0000000..5e4619f --- /dev/null +++ b/frontend/public/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/Logo.tsx b/frontend/src/components/Logo.tsx index 1ff3e79..17bdf03 100644 --- a/frontend/src/components/Logo.tsx +++ b/frontend/src/components/Logo.tsx @@ -2,63 +2,63 @@ import { DotIcon } from "@phosphor-icons/react"; import "@fontsource/knewave/400.css"; interface LogoProps { - scale?: number; - type?: "inline" | "mono" | "logo" | null; - ul?: boolean; + scale?: number; + type?: "inline" | "mono" | "logo" | null; + ul?: boolean; } export default function Logo({ - scale = 1, - type = null, - ul = false, + scale = 1, + type = null, + ul = false, }: LogoProps) { - if (type === "inline") { - return ( - - pi. ku - .  - - ); - } + if (type === "inline") { + return ( + + pi. ku + .  + + ); + } - if (type === "mono") { - return ( - - pi. ku. - - ); - } + if (type === "mono") { + return ( + + pi. ku. + + ); + } - if (type === "logo") { - return ( - Pi. Ku. logo - ); - } + if (type === "logo") { + return ( + Pi. Ku. logo + ); + } - return ( -
- Pi - -  Ku - -
- ); + return ( +
+ Pi + +  Ku + +
+ ); } diff --git a/frontend/src/index.css b/frontend/src/index.css index ae1cb96..b2cec31 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -24,9 +24,9 @@ --color-neutral: oklch(38% 0.02 45); --color-neutral-content: oklch(80% 0.015 60); - --color-info: oklch(60% 0.07 240); + --color-info: oklch(60% 0.06 250); --color-info-content: oklch(95% 0.01 240); - --color-success: oklch(60% 0.08 150); + --color-success: oklch(65% 0.05 140); --color-success-content: oklch(16% 0.03 150); --color-warning: oklch(68% 0.08 72); --color-warning-content: oklch(18% 0.03 60); @@ -64,7 +64,7 @@ } .glass-card { - @apply bg-glass-bg backdrop-blur-xl border border-neutral-content/10 shadow-warm rounded-xl m-4; + @apply bg-glass-bg max-w-xs md:max-w-sm backdrop-blur-xl border border-neutral-content/10 shadow-warm rounded-xl m-4; } .ul-wavy { diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 4ed7089..5183c37 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -16,135 +16,135 @@ import { useAuth } from "../hooks/useAuth"; import { CryptoUtils } from "../utils/crypto"; const loginSchema = z.object({ - email: z.email("Please enter a valid email"), - password: z.string().min(1, "Password is required"), + email: z.email("Please enter a valid email"), + password: z.string().min(1, "Password is required"), }); type LoginInputs = z.infer; export default function Login() { - const navigate = useNavigate(); - const location = useLocation(); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(null); - const { setAuthStore } = useAuth(); - const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime); - const [saajanMessage, setSaajanMessage] = useState( - "I was wondering when you'd return.", - ); - const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER; + const navigate = useNavigate(); + const location = useLocation(); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const { setAuthStore } = useAuth(); + const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime); + const [saajanMessage, setSaajanMessage] = useState( + "I was wondering when you'd return.", + ); + const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER; - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(loginSchema), - }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + }); - const onSubmit = async (data: LoginInputs) => { - setIsLoading(true); - setApiError(null); - try { - // client side key derivation for e2e encryption - const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle( - data.password, - data.email, - ); + const onSubmit = async (data: LoginInputs) => { + setIsLoading(true); + setApiError(null); + try { + // client side key derivation for e2e encryption + const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle( + data.password, + data.email, + ); - // send just the authHash as the password to the server - const { data: authData } = await publicApi.post(endpoints.LOGIN, { - email: data.email, - password: authHash, - }); + // send just the authHash as the password to the server + const { data: authData } = await publicApi.post(endpoints.LOGIN, { + email: data.email, + password: authHash, + }); - const { data: userData } = await api.get(endpoints.ME, { - headers: { Authorization: `Bearer ${authData.access}` }, - }); + const { data: userData } = await api.get(endpoints.ME, { + headers: { Authorization: `Bearer ${authData.access}` }, + }); - await setAuthStore(authData.access, userData, masterKey); + await setAuthStore(authData.access, userData, masterKey); - navigate(nextRoute, { replace: true, state: location.state }); - } catch (err) { - let message = - "Sorry, we're experiencing technical issues.\nPlease try again later."; - if (axios.isAxiosError(err) && err.response?.status !== 500) { - message = err.response?.data?.detail || err.response?.data?.message; - } - setApiError(message); - } finally { - setIsLoading(false); - } - }; - - return ( -
- {!showWelcome && } - {showWelcome && } -
-
-

- Enter Archive -

- - {apiError && ( -
- {apiError} -
- )} - - setSaajanMessage("I remember you.")} - /> - - - setSaajanMessage("The one thing I cannot know for you.") + navigate(nextRoute, { replace: true, state: location.state }); + } catch (err) { + let message = + "Sorry, we're experiencing technical issues.\nPlease try again later."; + if (axios.isAxiosError(err) && err.response?.status !== 500) { + message = err.response?.data?.detail || err.response?.data?.message; } - /> + setApiError(message); + } finally { + setIsLoading(false); + } + }; -
- -
+ return ( +
+ {!showWelcome && } + {showWelcome && } +
+ +

+   Enter Archive +

-
- Don't have an account?{" "} - -
- -
-
- ); + {apiError && ( +
+ {apiError} +
+ )} + + setSaajanMessage("I remember you.")} + /> + + + setSaajanMessage("The one thing I cannot know for you.") + } + /> + +
+ +
+ +
+ Don't have an account?{" "} + +
+ +
+
+ ); } diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index a7e9fb4..174da9e 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -14,158 +14,158 @@ import { ROUTES } from "../config/routes"; import { CryptoUtils } from "../utils/crypto"; const registerSchema = z - .object({ - full_name: z.string().min(2, "Name must be at least 2 characters"), - email: z.email("Please enter a valid email"), - password: z.string().min(8, "Password must be at least 8 characters"), - confirm_password: z.string(), - }) - .refine((data) => data.password === data.confirm_password, { - message: "Passwords don't match", - path: ["confirm_password"], - }); + .object({ + full_name: z.string().min(2, "Name must be at least 2 characters"), + email: z.email("Please enter a valid email"), + password: z.string().min(8, "Password must be at least 8 characters"), + confirm_password: z.string(), + }) + .refine((data) => data.password === data.confirm_password, { + message: "Passwords don't match", + path: ["confirm_password"], + }); type RegisterInputs = z.infer; export default function Register() { - const navigate = useNavigate(); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(null); - const [saajanMessage, setSaajanMessage] = useState( - "I didn't think I'd be here either.\nAnd yet, here we are.", - ); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const [saajanMessage, setSaajanMessage] = useState( + "I didn't think I'd be here either.\nAnd yet, here we are.", + ); - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(registerSchema), - }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerSchema), + }); - const onSubmit = async (data: RegisterInputs) => { - setSaajanMessage("Good. I'll remember that."); - setIsLoading(true); - setApiError(null); - try { - // we generate the key bundle here to get the authHash (password) to be haSHed and stored in the db. - const { authHash } = await CryptoUtils.deriveKeyBundle( - data.password, - data.email, - ); + const onSubmit = async (data: RegisterInputs) => { + setSaajanMessage("Good. I'll remember that."); + setIsLoading(true); + setApiError(null); + try { + // we generate the key bundle here to get the authHash (password) to be haSHed and stored in the db. + const { authHash } = await CryptoUtils.deriveKeyBundle( + data.password, + data.email, + ); - await publicApi.post(endpoints.REGISTER, { - full_name: data.full_name, - email: data.email, - password: authHash, - }); - navigate(ROUTES.VERIFY_EMAIL, { replace: true }); - } catch (err) { - let message = "Registration failed. Please try again."; - if (axios.isAxiosError(err)) { - message = err.response?.data?.message || message; - } - setApiError(message); - } finally { - setIsLoading(false); - } - }; + await publicApi.post(endpoints.REGISTER, { + full_name: data.full_name, + email: data.email, + password: authHash, + }); + navigate(ROUTES.VERIFY_EMAIL, { replace: true }); + } catch (err) { + let message = "Registration failed. Please try again."; + if (axios.isAxiosError(err)) { + message = err.response?.data?.message || message; + } + setApiError(message); + } finally { + setIsLoading(false); + } + }; - return ( -
- -
-
-
- Create a Account -
+ return ( +
+ +
+ +
+ Create a Account +
- {apiError && ( -
- {apiError} + {apiError && ( +
+ {apiError} +
+ )} + + + setSaajanMessage("Hello friend. What should I call you?") + } + /> + + + setSaajanMessage( + "Where should I send your letters?\nNo empty lunchboxes, please.", + ) + } + /> + + + setSaajanMessage( + "Something only you know.\nI have one of those too.", + ) + } + /> + + + setSaajanMessage( + "Just once? Trust me, \nsome things are worth repeating twice.", + ) + } + /> + +
+ +

+ Choose a password you won't forget.
+ Just like life,{" "} + there is no reset{" "} + here. If you lose it, your letters cannot be recovered. +

+
+ +
+ +
+
- )} - - - setSaajanMessage("Hello friend. What should I call you?") - } - /> - - - setSaajanMessage( - "Where should I send your letters?\nNo empty lunchboxes, please.", - ) - } - /> - - - setSaajanMessage( - "Something only you know.\nI have one of those too.", - ) - } - /> - - - setSaajanMessage( - "Just once? Trust me, \nsome things are worth repeating twice.", - ) - } - /> - -
- -

- Choose a password you won't forget.
- Just like life,{" "} - there is no reset{" "} - here. If you lose it, your letters cannot be recovered. -

-
- -
- -
- -
-
- ); +
+ ); } -- 2.52.0 From e55f4acec5306d4e3660a88a3f505c218d37e7f0 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 8 May 2026 06:07:10 +0530 Subject: [PATCH 10/11] style: update home font tracking --- frontend/src/pages/Home.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 142d7da..8164748 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -56,7 +56,7 @@ export default function Home() {
{/* Intro */} @@ -102,7 +102,7 @@ export default function Home() { > - is a{" "} + is a{" "} safe space ,
{" "} and{" "} - + private . -- 2.52.0 From 566ba369ecaa6f4899093cacc79df9e400fbfe32 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 8 May 2026 06:07:22 +0530 Subject: [PATCH 11/11] refactor: lint --- frontend/src/components/Logo.tsx | 106 ++--- frontend/src/components/RouteGuards.test.tsx | 10 +- frontend/src/components/SplashScreen.tsx | 5 +- .../src/components/drawer/DrawerSection.tsx | 172 +++++---- .../src/components/drawer/PasskeyModal.tsx | 5 +- .../src/components/reader/EnvelopeReveal.tsx | 7 +- frontend/src/index.css | 102 ++--- frontend/src/pages/Drawer.test.tsx | 192 +++++----- frontend/src/pages/Drawer.tsx | 361 +++++++++--------- frontend/src/pages/Editor.test.tsx | 15 +- frontend/src/pages/Login.test.tsx | 17 +- frontend/src/pages/Login.tsx | 234 ++++++------ frontend/src/pages/Reader.test.tsx | 10 +- frontend/src/pages/Register.tsx | 288 +++++++------- 14 files changed, 788 insertions(+), 736 deletions(-) diff --git a/frontend/src/components/Logo.tsx b/frontend/src/components/Logo.tsx index 17bdf03..aa9bf44 100644 --- a/frontend/src/components/Logo.tsx +++ b/frontend/src/components/Logo.tsx @@ -2,63 +2,63 @@ import { DotIcon } from "@phosphor-icons/react"; import "@fontsource/knewave/400.css"; interface LogoProps { - scale?: number; - type?: "inline" | "mono" | "logo" | null; - ul?: boolean; + scale?: number; + type?: "inline" | "mono" | "logo" | null; + ul?: boolean; } export default function Logo({ - scale = 1, - type = null, - ul = false, + scale = 1, + type = null, + ul = false, }: LogoProps) { - if (type === "inline") { - return ( - - pi. ku - .  - - ); - } - - if (type === "mono") { - return ( - - pi. ku. - - ); - } - - if (type === "logo") { - return ( - Pi. Ku. logo - ); - } - + if (type === "inline") { return ( -
- Pi - -  Ku - -
+ + pi. ku + .  + ); + } + + if (type === "mono") { + return ( + + pi. ku. + + ); + } + + if (type === "logo") { + return ( + Pi. Ku. logo + ); + } + + return ( +
+ Pi + +  Ku + +
+ ); } diff --git a/frontend/src/components/RouteGuards.test.tsx b/frontend/src/components/RouteGuards.test.tsx index 7eea6d4..79606f9 100644 --- a/frontend/src/components/RouteGuards.test.tsx +++ b/frontend/src/components/RouteGuards.test.tsx @@ -9,8 +9,14 @@ function renderGuard(ui: React.ReactNode, mountPath: "/protected" | "/public") { return render( - Login Page
} /> - Drawer Page
} /> + Login Page
} + /> + Drawer Page
} + /> diff --git a/frontend/src/components/SplashScreen.tsx b/frontend/src/components/SplashScreen.tsx index 7c4297e..0874d18 100644 --- a/frontend/src/components/SplashScreen.tsx +++ b/frontend/src/components/SplashScreen.tsx @@ -3,7 +3,10 @@ import Logo from "./Logo"; export default function SplashScreen() { return ( -
+
diff --git a/frontend/src/components/drawer/DrawerSection.tsx b/frontend/src/components/drawer/DrawerSection.tsx index 7fe6ff0..14874e4 100644 --- a/frontend/src/components/drawer/DrawerSection.tsx +++ b/frontend/src/components/drawer/DrawerSection.tsx @@ -1,95 +1,101 @@ import { GearFineIcon } from "@phosphor-icons/react"; interface DrawerSectionProps { - id: string; - title: string; - count: number; - subtext: string; - isOpen: boolean; - onClick: () => void; - children: React.ReactNode; - icon: React.ReactNode; + id: string; + title: string; + count: number; + subtext: string; + isOpen: boolean; + onClick: () => void; + children: React.ReactNode; + icon: React.ReactNode; } export function DrawerSection({ - id, - title, - count, - subtext, - isOpen, - onClick, - children, - icon, + id, + title, + count, + subtext, + isOpen, + onClick, + children, + icon, }: DrawerSectionProps) { - return ( + return ( +
+
-
-
- {children} - {count === 0 && ( -

- This drawer remains silent -

- )} -
-
- - + This drawer remains silent +

+ )}
- ); +
+ + +
+ ); } diff --git a/frontend/src/components/drawer/PasskeyModal.tsx b/frontend/src/components/drawer/PasskeyModal.tsx index 04ba0ed..e33a38b 100644 --- a/frontend/src/components/drawer/PasskeyModal.tsx +++ b/frontend/src/components/drawer/PasskeyModal.tsx @@ -12,7 +12,10 @@ export function PasskeyModal() { className="text-primary mx-auto mb-8 animate-pulse" weight="duotone" /> -

+

You've been away a while.

diff --git a/frontend/src/components/reader/EnvelopeReveal.tsx b/frontend/src/components/reader/EnvelopeReveal.tsx index a830307..9ac9f14 100644 --- a/frontend/src/components/reader/EnvelopeReveal.tsx +++ b/frontend/src/components/reader/EnvelopeReveal.tsx @@ -123,7 +123,12 @@ export function EnvelopeReveal({ to -

{recipient}

+

+ {recipient} +

{date}

({ - WelcomeLetterOverlay: ({ onComplete }: WelcomeLetterOverlayProps) => ( -
- -
- ), + WelcomeLetterOverlay: ({ onComplete }: WelcomeLetterOverlayProps) => ( +
+ +
+ ), })); describe("Drawer Page", () => { - beforeEach(() => { - // Setup authenticated state for the test - useAuthStore.setState({ - user: mockUser, - accessToken: "fake-token", - isInitializing: false, - }); - - vi.mocked(useLetters).mockReturnValue({ - drafts: [], - kept: [], - sent: [], - vault: [], - loading: false, - isAuthRequired: false, - }); + beforeEach(() => { + // Setup authenticated state for the test + useAuthStore.setState({ + user: mockUser, + accessToken: "fake-token", + isInitializing: false, }); - it("renders the drawer sections and empty state message", () => { - render( - - - , - ); + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: false, + isAuthRequired: false, + }); + }); - expect(screen.getByTestId("drawer-section-drafts")).toBeInTheDocument(); - expect(screen.getAllByTestId("drawer-section-title").length).toBeGreaterThanOrEqual(1); - expect(screen.getByTestId("drawer-section-vault")).toBeInTheDocument(); - expect(screen.getByTestId("empty-drawer-message-drafts")).toBeInTheDocument(); + it("renders the drawer sections and empty state message", () => { + render( + + + , + ); + + expect(screen.getByTestId("drawer-section-drafts")).toBeInTheDocument(); + expect( + screen.getAllByTestId("drawer-section-title").length, + ).toBeGreaterThanOrEqual(1); + expect(screen.getByTestId("drawer-section-vault")).toBeInTheDocument(); + expect( + screen.getByTestId("empty-drawer-message-drafts"), + ).toBeInTheDocument(); + }); + + it("renders the loading state", () => { + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: true, + isAuthRequired: false, }); - it("renders the loading state", () => { - vi.mocked(useLetters).mockReturnValue({ - drafts: [], - kept: [], - sent: [], - vault: [], - loading: true, - isAuthRequired: false, - }); + render( + + + , + ); - render( - - - , - ); + expect(screen.getByTestId("drawer-loading-state")).toBeInTheDocument(); + }); - expect(screen.getByTestId("drawer-loading-state")).toBeInTheDocument(); + it("renders the authentication required modal when api requires auth", () => { + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: false, + isAuthRequired: true, }); - it("renders the authentication required modal when api requires auth", () => { - vi.mocked(useLetters).mockReturnValue({ - drafts: [], - kept: [], - sent: [], - vault: [], - loading: false, - isAuthRequired: true, - }); + render( + + + , + ); - render( - - - , - ); + expect(screen.getByTestId("passkey-modal-title")).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument(); + }); - expect(screen.getByTestId("passkey-modal-title")).toBeInTheDocument(); - expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument(); - }); + it("renders the welcome letter when firstTime state is present", () => { + render( + + + , + ); - it("renders the welcome letter when firstTime state is present", () => { - render( - - - , - ); + expect(screen.getByTestId("welcome-letter-overlay")).toBeInTheDocument(); + }); - expect(screen.getByTestId("welcome-letter-overlay")).toBeInTheDocument(); - }); + it("renders the drawer content when the letter is closed", () => { + render( + + + , + ); - it("renders the drawer content when the letter is closed", () => { - render( - - - , - ); + const completeButton = screen.getByTestId("overlay-exit-button"); + fireEvent.click(completeButton); - const completeButton = screen.getByTestId("overlay-exit-button"); - fireEvent.click(completeButton); - - expect( - screen.queryByTestId("welcome-letter-overlay"), - ).not.toBeInTheDocument(); - }); + expect( + screen.queryByTestId("welcome-letter-overlay"), + ).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/pages/Drawer.tsx b/frontend/src/pages/Drawer.tsx index 425956a..0a61541 100644 --- a/frontend/src/pages/Drawer.tsx +++ b/frontend/src/pages/Drawer.tsx @@ -1,9 +1,9 @@ import { - ArchiveIcon, - FeatherIcon, - FileDashedIcon, - PaperPlaneTiltIcon, - VaultIcon, + ArchiveIcon, + FeatherIcon, + FileDashedIcon, + PaperPlaneTiltIcon, + VaultIcon, } from "@phosphor-icons/react"; import { useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; @@ -17,188 +17,191 @@ import { PATHS } from "../config/routes"; import { useAuth } from "../hooks/useAuth"; import { useLetters } from "../hooks/useLetters"; import { - formatRelativeDate, - formatRelativeDateWithoutTime, + formatRelativeDate, + formatRelativeDateWithoutTime, } from "../utils/dateFormat.ts"; export default function Drawer() { - const { user, logout } = useAuth(); + const { user, logout } = useAuth(); - const [openSection, setOpenSection] = useState(null); - const navigate = useNavigate(); - const location = useLocation(); - const [showWelcomeLetter, setShowWelcomeLetter] = useState( - !!location.state?.firstTime, - ); - const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters(); + const [openSection, setOpenSection] = useState(null); + const navigate = useNavigate(); + const location = useLocation(); + const [showWelcomeLetter, setShowWelcomeLetter] = useState( + !!location.state?.firstTime, + ); + const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters(); - if (!user) return null; + if (!user) return null; - const toggleSection = (id: string) => - setOpenSection(openSection === id ? null : id); + const toggleSection = (id: string) => + setOpenSection(openSection === id ? null : id); - return ( -
-
+ return ( +
+
- {showWelcomeLetter && ( - { - setShowWelcomeLetter(false); - navigate(location.pathname, { replace: true, state: {} }); - }} - /> - )} + {showWelcomeLetter && ( + { + setShowWelcomeLetter(false); + navigate(location.pathname, { replace: true, state: {} }); + }} + /> + )} - {isAuthRequired && } -
- -
- Personal Archive -
-
- Welcome Back{" "} - {user.full_name} - -
-
- -
- {loading ? ( -
- - - Opening your cabinet... - -
- ) : ( - <> - toggleSection("drafts")} - icon={} - > - {drafts.map((draft) => ( - - ))} - - - toggleSection("kept")} - icon={} - > - {kept.map((letter) => ( - - ))} - - toggleSection("sent")} - icon={} - > - {sent.map((letter) => ( - - ))} - - toggleSection("vault")} - icon={} - > - {vault.map((letter) => ( - new Date().toISOString()} - /> - ))} - - - )} -
- - - -
- For your unsaid. -
- {!showWelcomeLetter && ( -
- -
- )} + {isAuthRequired && } +
+ +
+ Personal Archive
- ); +
+ Welcome Back{" "} + {user.full_name} + +
+
+ +
+ {loading ? ( +
+ + + Opening your cabinet... + +
+ ) : ( + <> + toggleSection("drafts")} + icon={} + > + {drafts.map((draft) => ( + + ))} + + + toggleSection("kept")} + icon={} + > + {kept.map((letter) => ( + + ))} + + toggleSection("sent")} + icon={} + > + {sent.map((letter) => ( + + ))} + + toggleSection("vault")} + icon={} + > + {vault.map((letter) => ( + new Date().toISOString()} + /> + ))} + + + )} +
+ + + +
+ For your unsaid. +
+ {!showWelcomeLetter && ( +
+ +
+ )} +
+ ); } diff --git a/frontend/src/pages/Editor.test.tsx b/frontend/src/pages/Editor.test.tsx index 6b32945..7b8ad34 100644 --- a/frontend/src/pages/Editor.test.tsx +++ b/frontend/src/pages/Editor.test.tsx @@ -1,4 +1,9 @@ -import { fireEvent, render, screen, waitForElementToBeRemoved } from "@testing-library/react"; +import { + fireEvent, + render, + screen, + waitForElementToBeRemoved, +} from "@testing-library/react"; import { HttpResponse, http } from "msw"; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -79,7 +84,9 @@ describe("Editor Page", () => { ); // Wait for initial load to complete - await waitForElementToBeRemoved(() => screen.queryByTestId("opening-draft-overlay")); + await waitForElementToBeRemoved(() => + screen.queryByTestId("opening-draft-overlay"), + ); const canvas = screen.getByTestId("canvas"); expect(canvas.getAttribute("data-readonly")).toBe("false"); @@ -136,7 +143,9 @@ describe("Editor Page", () => { , ); - await waitForElementToBeRemoved(() => screen.queryByTestId("opening-draft-overlay")); + await waitForElementToBeRemoved(() => + screen.queryByTestId("opening-draft-overlay"), + ); const canvas = screen.getByTestId("canvas"); diff --git a/frontend/src/pages/Login.test.tsx b/frontend/src/pages/Login.test.tsx index 7f58b75..3b77068 100644 --- a/frontend/src/pages/Login.test.tsx +++ b/frontend/src/pages/Login.test.tsx @@ -31,7 +31,9 @@ describe("Login Page", () => { await userEvent.type(screen.getByLabelText(/password/i), "password123"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); - expect(await screen.findByTestId("login-error-message")).toHaveTextContent(/technical issues/i); + expect(await screen.findByTestId("login-error-message")).toHaveTextContent( + /technical issues/i, + ); }); it.each([ @@ -73,8 +75,14 @@ describe("Login Page", () => { > } /> - Drawer
} /> - Reader
} /> + Drawer
} + /> + Reader
} + /> , ); @@ -83,7 +91,8 @@ describe("Login Page", () => { await userEvent.type(screen.getByLabelText(/password/i), "password123"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); - const expectedTestId = nextRoute.toLowerCase() === "drawer" ? "drawer-page" : "reader-page"; + const expectedTestId = + nextRoute.toLowerCase() === "drawer" ? "drawer-page" : "reader-page"; expect(await screen.findByTestId(expectedTestId)).toBeInTheDocument(); }); }); diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 5183c37..2f4b32e 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -16,135 +16,135 @@ import { useAuth } from "../hooks/useAuth"; import { CryptoUtils } from "../utils/crypto"; const loginSchema = z.object({ - email: z.email("Please enter a valid email"), - password: z.string().min(1, "Password is required"), + email: z.email("Please enter a valid email"), + password: z.string().min(1, "Password is required"), }); type LoginInputs = z.infer; export default function Login() { - const navigate = useNavigate(); - const location = useLocation(); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(null); - const { setAuthStore } = useAuth(); - const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime); - const [saajanMessage, setSaajanMessage] = useState( - "I was wondering when you'd return.", - ); - const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER; + const navigate = useNavigate(); + const location = useLocation(); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const { setAuthStore } = useAuth(); + const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime); + const [saajanMessage, setSaajanMessage] = useState( + "I was wondering when you'd return.", + ); + const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER; - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(loginSchema), - }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + }); - const onSubmit = async (data: LoginInputs) => { - setIsLoading(true); - setApiError(null); - try { - // client side key derivation for e2e encryption - const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle( - data.password, - data.email, - ); + const onSubmit = async (data: LoginInputs) => { + setIsLoading(true); + setApiError(null); + try { + // client side key derivation for e2e encryption + const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle( + data.password, + data.email, + ); - // send just the authHash as the password to the server - const { data: authData } = await publicApi.post(endpoints.LOGIN, { - email: data.email, - password: authHash, - }); + // send just the authHash as the password to the server + const { data: authData } = await publicApi.post(endpoints.LOGIN, { + email: data.email, + password: authHash, + }); - const { data: userData } = await api.get(endpoints.ME, { - headers: { Authorization: `Bearer ${authData.access}` }, - }); + const { data: userData } = await api.get(endpoints.ME, { + headers: { Authorization: `Bearer ${authData.access}` }, + }); - await setAuthStore(authData.access, userData, masterKey); + await setAuthStore(authData.access, userData, masterKey); - navigate(nextRoute, { replace: true, state: location.state }); - } catch (err) { - let message = - "Sorry, we're experiencing technical issues.\nPlease try again later."; - if (axios.isAxiosError(err) && err.response?.status !== 500) { - message = err.response?.data?.detail || err.response?.data?.message; - } - setApiError(message); - } finally { - setIsLoading(false); - } - }; + navigate(nextRoute, { replace: true, state: location.state }); + } catch (err) { + let message = + "Sorry, we're experiencing technical issues.\nPlease try again later."; + if (axios.isAxiosError(err) && err.response?.status !== 500) { + message = err.response?.data?.detail || err.response?.data?.message; + } + setApiError(message); + } finally { + setIsLoading(false); + } + }; - return ( -
- {!showWelcome && } - {showWelcome && } -
-
-

-   Enter Archive -

+ return ( +
+ {!showWelcome && } + {showWelcome && } +
+ +

+   Enter Archive +

- {apiError && ( -
- {apiError} -
- )} - - setSaajanMessage("I remember you.")} - /> - - - setSaajanMessage("The one thing I cannot know for you.") - } - /> - -
- -
- -
- Don't have an account?{" "} - -
- + {apiError && ( +
+ {apiError}
-
- ); + )} + + setSaajanMessage("I remember you.")} + /> + + + setSaajanMessage("The one thing I cannot know for you.") + } + /> + +
+ +
+ +
+ Don't have an account?{" "} + +
+ +
+
+ ); } diff --git a/frontend/src/pages/Reader.test.tsx b/frontend/src/pages/Reader.test.tsx index 50164b1..ec19415 100644 --- a/frontend/src/pages/Reader.test.tsx +++ b/frontend/src/pages/Reader.test.tsx @@ -76,7 +76,9 @@ describe("Reader Page", () => { , ); - expect(await screen.findByTestId("envelope-recipient")).toHaveTextContent(/Guest/i); + expect(await screen.findByTestId("envelope-recipient")).toHaveTextContent( + /Guest/i, + ); }); it("should display an error message if the server request fails", async () => { @@ -97,9 +99,9 @@ describe("Reader Page", () => { , ); - expect( - await screen.findByTestId("log-modal-message"), - ).toHaveTextContent(/Failed to load letter/i); + expect(await screen.findByTestId("log-modal-message")).toHaveTextContent( + /Failed to load letter/i, + ); }); it("should navigate to the login page with redirect url when the letter has no sharing key and the user is not logged in", async () => { diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index 174da9e..b28403a 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -14,158 +14,158 @@ import { ROUTES } from "../config/routes"; import { CryptoUtils } from "../utils/crypto"; const registerSchema = z - .object({ - full_name: z.string().min(2, "Name must be at least 2 characters"), - email: z.email("Please enter a valid email"), - password: z.string().min(8, "Password must be at least 8 characters"), - confirm_password: z.string(), - }) - .refine((data) => data.password === data.confirm_password, { - message: "Passwords don't match", - path: ["confirm_password"], - }); + .object({ + full_name: z.string().min(2, "Name must be at least 2 characters"), + email: z.email("Please enter a valid email"), + password: z.string().min(8, "Password must be at least 8 characters"), + confirm_password: z.string(), + }) + .refine((data) => data.password === data.confirm_password, { + message: "Passwords don't match", + path: ["confirm_password"], + }); type RegisterInputs = z.infer; export default function Register() { - const navigate = useNavigate(); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(null); - const [saajanMessage, setSaajanMessage] = useState( - "I didn't think I'd be here either.\nAnd yet, here we are.", - ); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const [saajanMessage, setSaajanMessage] = useState( + "I didn't think I'd be here either.\nAnd yet, here we are.", + ); - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(registerSchema), - }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerSchema), + }); - const onSubmit = async (data: RegisterInputs) => { - setSaajanMessage("Good. I'll remember that."); - setIsLoading(true); - setApiError(null); - try { - // we generate the key bundle here to get the authHash (password) to be haSHed and stored in the db. - const { authHash } = await CryptoUtils.deriveKeyBundle( - data.password, - data.email, - ); + const onSubmit = async (data: RegisterInputs) => { + setSaajanMessage("Good. I'll remember that."); + setIsLoading(true); + setApiError(null); + try { + // we generate the key bundle here to get the authHash (password) to be haSHed and stored in the db. + const { authHash } = await CryptoUtils.deriveKeyBundle( + data.password, + data.email, + ); - await publicApi.post(endpoints.REGISTER, { - full_name: data.full_name, - email: data.email, - password: authHash, - }); - navigate(ROUTES.VERIFY_EMAIL, { replace: true }); - } catch (err) { - let message = "Registration failed. Please try again."; - if (axios.isAxiosError(err)) { - message = err.response?.data?.message || message; - } - setApiError(message); - } finally { - setIsLoading(false); - } - }; + await publicApi.post(endpoints.REGISTER, { + full_name: data.full_name, + email: data.email, + password: authHash, + }); + navigate(ROUTES.VERIFY_EMAIL, { replace: true }); + } catch (err) { + let message = "Registration failed. Please try again."; + if (axios.isAxiosError(err)) { + message = err.response?.data?.message || message; + } + setApiError(message); + } finally { + setIsLoading(false); + } + }; - return ( -
- -
-
-
- Create a Account -
+ return ( +
+ +
+ +
+ Create a Account +
- {apiError && ( -
- {apiError} -
- )} - - - setSaajanMessage("Hello friend. What should I call you?") - } - /> - - - setSaajanMessage( - "Where should I send your letters?\nNo empty lunchboxes, please.", - ) - } - /> - - - setSaajanMessage( - "Something only you know.\nI have one of those too.", - ) - } - /> - - - setSaajanMessage( - "Just once? Trust me, \nsome things are worth repeating twice.", - ) - } - /> - -
- -

- Choose a password you won't forget.
- Just like life,{" "} - there is no reset{" "} - here. If you lose it, your letters cannot be recovered. -

-
- -
- -
- + {apiError && ( +
+ {apiError}
-
- ); + )} + + + setSaajanMessage("Hello friend. What should I call you?") + } + /> + + + setSaajanMessage( + "Where should I send your letters?\nNo empty lunchboxes, please.", + ) + } + /> + + + setSaajanMessage( + "Something only you know.\nI have one of those too.", + ) + } + /> + + + setSaajanMessage( + "Just once? Trust me, \nsome things are worth repeating twice.", + ) + } + /> + +
+ +

+ Choose a password you won't forget.
+ Just like life,{" "} + there is no reset{" "} + here. If you lose it, your letters cannot be recovered. +

+
+ +
+ +
+ +
+
+ ); } -- 2.52.0