diff --git a/frontend/src/components/drawer/WelcomeLetterOverlay.tsx b/frontend/src/components/drawer/WelcomeLetterOverlay.tsx index 8422e08..c0ac222 100644 --- a/frontend/src/components/drawer/WelcomeLetterOverlay.tsx +++ b/frontend/src/components/drawer/WelcomeLetterOverlay.tsx @@ -6,73 +6,73 @@ import { type CanvasTools, ComposeCanvas } from "../editor/ComposeCanvas"; import { EnvelopeReveal } from "../reader/EnvelopeReveal"; export interface WelcomeLetterOverlayProps { - onComplete: () => void; - userName: string; + onComplete: () => void; + userName: string; } export function WelcomeLetterOverlay({ - onComplete, - userName, + onComplete, + userName, }: WelcomeLetterOverlayProps) { - const [revealState, setRevealState] = useState<"SEALED" | "REVEALED">( - "SEALED", - ); - const canvasRef = useRef(null); + const [revealState, setRevealState] = useState<"SEALED" | "REVEALED">( + "SEALED", + ); + const canvasRef = useRef(null); - useEffect(() => { - if (revealState === "REVEALED" && canvasRef.current) { - const welcomeContent = getWelcomeLetterContent(userName); - canvasRef.current.loadData(welcomeContent); - } - }, [revealState, userName]); + useEffect(() => { + if (revealState === "REVEALED" && canvasRef.current) { + const welcomeContent = getWelcomeLetterContent(userName); + canvasRef.current.loadData(welcomeContent); + } + }, [revealState, userName]); - return ( -
-
+ return ( +
+
-
- - {revealState === "SEALED" && ( - - setRevealState("REVEALED")} - ignite={false} - /> - - )} - -
-
-
- -
+
+ + {revealState === "SEALED" && ( + + setRevealState("REVEALED")} + ignite={false} + /> + + )} + +
+
+
+ +
-
- -
-
-
+
+ +
- ); +
+
+ ); } diff --git a/frontend/src/pages/Activate.tsx b/frontend/src/pages/Activate.tsx index 0ca4895..2f53023 100644 --- a/frontend/src/pages/Activate.tsx +++ b/frontend/src/pages/Activate.tsx @@ -7,96 +7,99 @@ import { endpoints, replacePathParams } from "../config/endpoints"; import { ROUTES } from "../config/routes"; export default function Activate() { - const { uidb64, token } = useParams(); - const [status, setStatus] = useState<"loading" | "success" | "error">( - "loading", - ); - const hasCalled = useRef(false); - const navigate = useNavigate(); + const { uidb64, token } = useParams(); + const [status, setStatus] = useState<"loading" | "success" | "error">( + "loading", + ); + const hasCalled = useRef(false); + const navigate = useNavigate(); - useEffect(() => { - if (!(uidb64 && token) || hasCalled.current) return; - hasCalled.current = true; + useEffect(() => { + if (!(uidb64 && token) || hasCalled.current) return; + hasCalled.current = true; - const activateAccount = async () => { - try { - const url = replacePathParams(endpoints.ACTIVATE, { - uidb64, - token, - }); - await publicApi.get(url); - setStatus("success"); - } catch (_err) { - setStatus("error"); - } - }; + const activateAccount = async () => { + try { + const url = replacePathParams(endpoints.ACTIVATE, { + uidb64, + token, + }); + await publicApi.get(url); + setStatus("success"); + } catch (_err) { + setStatus("error"); + } + }; - activateAccount(); - }, [uidb64, token]); + activateAccount(); + }, [uidb64, token]); - return ( -
- {status === "loading" && ( -
- -

Activating your account...

-
- )} - - {status === "success" && ( -
-
- -
-

- You're in. -

-

- Welcome to -
- Just one more step and you can start writing timeless letters. -

-
- -
- )} - - {status === "error" && ( -
-
- -
-

Activation Failed

-

- The link might be expired or already used. Please try registering - again. -

-
- -
- )} + return ( +
+ {status === "loading" && ( +
+ +

Activating your account...

- ); + )} + + {status === "success" && ( +
+
+ +
+

+ You're in. +

+

+ Welcome to +
+ Just one more step and you can start writing timeless letters. +

+
+ +
+ )} + + {status === "error" && ( +
+
+ +
+

Activation Failed

+

+ The link might be expired or already used. Please try registering + again. +

+
+ +
+ )} +
+ ); } diff --git a/frontend/src/pages/Drawer.test.tsx b/frontend/src/pages/Drawer.test.tsx index fed79fb..b79aed3 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, - }); - - 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 cabinet sections and empty state message", () => { - render( - - - , - ); + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: false, + isAuthRequired: false, + }); + }); - 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 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, }); - it("renders the loading state", () => { - vi.mocked(useLetters).mockReturnValue({ - drafts: [], - kept: [], - sent: [], - vault: [], - loading: true, - isAuthRequired: false, - }); + render( + + + , + ); - render( - - - , - ); + expect(screen.getByText(/Opening your cabinet/i)).toBeInTheDocument(); + }); - expect(screen.getByText(/Opening your cabinet/i)).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.getByText(/You've been away a while./i)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument(); + }); - expect(screen.getByText(/You've been away a while./i)).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/Register.tsx b/frontend/src/pages/Register.tsx index e1c5bc1..a7e9fb4 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. +

+
+ +
+ +
+ +
+
+ ); }