setConfirmModal("VAULT")}
>
@@ -269,8 +270,7 @@ export function VaultConfirmModal({
I'll remember to mail you this on the unlock date.
- {" "}
- But I won't let you read or rewrite this letter until then.
+ But I won't let you read or rewrite this letter until then.
@@ -299,6 +299,7 @@ export function VaultConfirmModal({
setConfirmModal(null)}
>
@@ -307,6 +308,7 @@ export function VaultConfirmModal({
Take it
diff --git a/frontend/src/components/login/WelcomeModal.tsx b/frontend/src/components/login/WelcomeModal.tsx
index 97dd30d..97e259f 100644
--- a/frontend/src/components/login/WelcomeModal.tsx
+++ b/frontend/src/components/login/WelcomeModal.tsx
@@ -15,7 +15,7 @@ export default function WelcomeModal({
return (
<>
-
+
- Welcome to
- !
+ Welcome to
+
-
+
Before we begin, let me make a small promise.
-
- Everything you write here is sealed with your password,{" "}
+
+ Everything you write here is sealed with your password,
cryptographically
, before it leaves your hands.
- A fancy way of saying, I couldn't if I tried.
+
+ A fancy way of saying, no one else can read them without your
+ key—not even me.
-
-
-
+
+
+
If you ever happen to forget your password, your letters are lost
to time, forever.
-
-
- I highly, highly recommend storing this password in your{" "}
+
+ I highly, highly
+ recommend storing this password in your
password manager
- {" "}
- or somewhere safe to remember it.
+
+ or somewhere safe to remember it.
@@ -74,9 +76,9 @@ export default function WelcomeModal({
-
+
diff --git a/frontend/src/components/reader/BurnModal.tsx b/frontend/src/components/reader/BurnModal.tsx
index 1d9a303..f07768c 100644
--- a/frontend/src/components/reader/BurnModal.tsx
+++ b/frontend/src/components/reader/BurnModal.tsx
@@ -58,8 +58,8 @@ export function BurnModal({
Let the echoes of your unsaid be finally released.
- Press and{" "}
- hold the{" "}
+ Press and
+ hold the
flame to proceed.
diff --git a/frontend/src/components/reader/PostActionOverlay.tsx b/frontend/src/components/reader/PostActionOverlay.tsx
index 5f07a55..6bed2d6 100644
--- a/frontend/src/components/reader/PostActionOverlay.tsx
+++ b/frontend/src/components/reader/PostActionOverlay.tsx
@@ -23,8 +23,8 @@ export function PostActionOverlay({ revealState }: PostActionOverlayProps) {
May your soul find
solace,
- just like your unsaid{" "}
- words did.
+ just like your unsaid
+ words did.
You've carried these words long enough.
- Send your letter now, and let the{" "}
+ Send your letter now, and let the
unsaid finally
find its home.
@@ -59,8 +59,8 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
The key never leaves your or the recipient's browser.
@@ -68,7 +68,7 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
-
+
diff --git a/frontend/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx
index 6849390..4a39115 100644
--- a/frontend/src/components/ui/Modal.tsx
+++ b/frontend/src/components/ui/Modal.tsx
@@ -1,5 +1,6 @@
import { XCircleIcon } from "@phosphor-icons/react";
import type { ReactNode } from "react";
+import { createPortal } from "react-dom";
interface ModalProps {
isOpen: boolean;
@@ -15,13 +16,15 @@ export function Modal({
"data-testid": testId,
}: ModalProps) {
if (!isOpen) return null;
-
- return (
+ // render the modal top of all elements and position them to document viewport (/ the main wrapper).
+ // NOTE: this is recommended approach for modals as it shouldn't be bound to the parent box.
+ const mainContainer = document.querySelector("main") || document.body;
+ return createPortal(
- Your letters.{" "}
+ Your letters.Nobody else's.
- When you write or upload anything{" "}
+ When you write or upload anything
(yes, even images) here, it gets
encrypted in your browser before anything leaves your device. What
reaches the server is something unreadable—and the server has no
@@ -224,31 +224,35 @@ function SpecsSection() {
Specs
-
-
- uses{" "}
- Zero Knowledge{" "}
-
+
+
+ uses
+ Zero Knowledge
+
E
-
+
nd—
2
-
+
—
E
-
+
nd
E
-
+
ncryption
- {" "}
- for your letters, with{" "}
+
+ for your
+ letters, with
Envelope Encryption
- {" "}
- for the keys.
+
+ for the keys.
-
- This means, both the{" "}
- encryption and{" "}
+
+ This means, both the
+ encryption and
decryption runs on
your device, in your browser.
-
+
- Every letter has a{" "}
+ Every letter has a
unique key
- {" "}
- which is derived from your original password.
+
+ which is derived from your original password.
Both the letter and the key are encrypted securely and sent to the
server.
- Now, the server holds{" "}
- the envelope,{" "}
- the seal and{" "}
+ Now, the server holds
+ the envelope
+ ,
+ the seal
+ and
another locked box
@@ -288,7 +294,7 @@ function SpecsSection() {
But you—
only you—hold the very thing
- that opens that box,{" "}
+ that opens that box,
your password.
@@ -296,17 +302,18 @@ function SpecsSection() {
Nothing on the server is readable without your actual password.
Even if someone were to breach in, all they'd find is encrypted
- noise and ain't no way they crackin' it.{" "}
+ noise and ain't no way they crackin'
+
- (unless this happens)
+ (unless this happens)
-
+
- Of course, this level of{" "}
+ Of course, this level of
privacy comes with a
- catch. No password reset{" "}
- for you.
+ catch. No password reset
+ for you.
Your original password is never stored
@@ -350,26 +357,27 @@ function SpecsSection() {
function OSSSection() {
return (
-
+
- is{" "}
+ is
private
only for
- your letters {" "}
+ your letters
- {" "}
+
+
open source !
-
+
is
...uhhh... pretty
@@ -377,12 +385,14 @@ function OSSSection() {
about privacy and encryption is publicly available in the code so you
don't have to take my word at it.
-
- You can also{" "}
- Self-host{" "}
- in just 4 steps.
+
@@ -533,7 +548,7 @@ function StorySection() {
postscript; a note written after the letter is signed.
- "the most honest thing was always in the{" "}
+ "the most honest thing was always in the
பி. கு."
@@ -559,7 +574,7 @@ function StorySection() {
is an abbreviated transliteration of the
தமிழ் (Tamil) word
- for{" "}
+ for
.
- {" "}
- —the thing you add after you've already signed your name, what
- you write when you thought you were finished, but weren't.
+
+ —the thing you add after you've already signed your
+ name, what you write when you thought you were finished, but
+ weren't.
@@ -594,7 +610,7 @@ function StorySection() {
It sits in drafts , in half-written notes, in the pause before we
change the subject.
- Those words{" "}
+ Those words
don't just disappear. They
- {" "}
- stay{" "}
+
+ stay
unsaid
@@ -640,10 +656,9 @@ function ForWhoSection() {
wasn't built for one kind of person, but a
particular kind of feeling—
- {" "}
- the one that lingers very quietly
- {" "}
- —fragile, yet never breaks.
+ the one that lingers very quietly
+
+ —fragile, yet never breaks.
@@ -672,7 +687,7 @@ function ArchetypesSection() {
>
The Archetypes
-
of writing
+
of writing
-
- {" "}
- To someone you can't reach anymore.
+
+
+ To someone you can't reach anymore.
@@ -692,7 +707,8 @@ function ArchetypesSection() {
finished.
-
+
+
Write the letter anyway. Keep it close.
@@ -712,8 +728,8 @@ function ArchetypesSection() {
weight="duotone"
className="text-accent"
size={32}
- />{" "}
- To someone who's still here.
+ />
+ To someone who's still here.
@@ -722,7 +738,8 @@ function ArchetypesSection() {
noise of a conversation already in motion. A letter slows it
down.
-
+
+
Give people their due flowers while they can still smell them.
+
Set a date and let a letter surprise you when you've long
forgotten writing it.
@@ -775,8 +794,8 @@ function ArchetypesSection() {
name="my-accordion-det-1"
>
- {" "}
- For liberation.
+
+ For liberation.
@@ -785,7 +804,8 @@ function ArchetypesSection() {
putting it somewhere outside of yourself.
That's sometimes enough.
-
+
+
Say it once. All of it. Then let it fade.
@@ -864,16 +884,16 @@ function AttributionSection() {
took a while to exist.
- This started as a{" "}
+ This started as a
CS50W
- {" "}
- capstone—one I kept postponing until I ran out of excuses.
- When I sat down to build it, it felt heavier than a typical
+
+ capstone—one I kept postponing until I ran out of
+ excuses. When I sat down to build it, it felt heavier than a typical
assignment—not just because things were difficult. It had to
be something that outlasted the grade. I wanted to make this one
count more than anything else I'd ever made. Something as close to
@@ -885,19 +905,19 @@ function AttributionSection() {
Of course, frustrations, id-exisi crises, crept in from time to
time. But helped me re-kindle the love for
the odd hours spent obsessing over the tiniest UX decisions and
- endlessly polishing the UI{" "}
+ endlessly polishing the UI
(only if I could've just made my mind up on one design system
sooner, instead of paddling in a sea of muses, muses everywhere)
- . I know I've shared the nuts and bolts of {" "}
- here—the core philosophies, how it all works—but the
- heart of it is really something you have to find by exploring it
+ . I know I've shared the nuts and bolts of
+ here—the core philosophies, how it all works—but
+ the heart of it is really something you have to find by exploring it
yourself.
The "why" behind all of this didn't just appear out of nowhere. For
- a while, I kept coming back to{" "}
+ a while, I kept coming back to
setHover((h) => ({ ...h, visible: false }))}
>
Saajan
- {" "}
- from{" "}
+
+ from
The Lunchbox
- {" "}
- —brought to life with such subtle brilliance by{" "}
+
+ —brought to life with such subtle brilliance by
Irrfan Khan
- {" "}
+
+
—the quiet emotional weight he carries through a lonely and
mechanized life, right up until those letters arrive and something
- inside him finally loosens. The ending feels like a deep sigh of{" "}
+ inside him finally loosens. The ending feels like a deep sigh
+ of
"it is what it is"
@@ -947,15 +969,15 @@ function AttributionSection() {
that a lot.
- There's a lot that goes{" "}
+ There's a lot that goes
unsaid
- {" "}
- these days. Not for a lack of feeling, not for the lack of time, but
- because the ways we reach each other have quietly changed. We're
- always reachable digitally, yet
- somehow the things that actually matter most end up staying
- inside—a trapped one at that.
+
+ these days. Not for a lack of feeling, not for the lack of
+ time, but because the ways we reach each other have quietly changed.
+ We're always reachable digitally,
+ yet somehow the things that actually matter most end up
+ staying inside—a trapped one at that.
Maybe writing can/will help. Maybe putting words somewhere
deliberate makes them feel less like a weight you're carrying alone.
@@ -973,7 +995,7 @@ function AttributionSection() {
P.S. And just so we're clear—I wrote every word of this
- myself—as I continue to back{" "}
+ myself—as I continue to back
Em DASH
- . Why should AI get to have all the fun with 'em em dashes?{" "}
+ . Why should AI get to have all the fun with 'em em dashes?
(get it?)
@@ -990,10 +1012,10 @@ function AttributionSection() {
weight="duotone"
size={48}
className="rotate-180 text-neutral-content"
- />{" "}
- I think we forget things if there is nobody to tell them.
+ />
+ I think we forget things if there is nobody to tell them.
- ~ Saajan Fernandes,{" "}
+ ~ Saajan Fernandes,
The Lunchbox
diff --git a/frontend/src/pages/Activate.tsx b/frontend/src/pages/Activate.tsx
index ccdb492..15597fa 100644
--- a/frontend/src/pages/Activate.tsx
+++ b/frontend/src/pages/Activate.tsx
@@ -59,7 +59,8 @@ export default function Activate() {
You're in.
- Welcome to
+ Welcome to
+
Just one more step and you can start writing timeless letters.
diff --git a/frontend/src/pages/Drawer.test.tsx b/frontend/src/pages/Drawer.test.tsx
index 3001514..b8acdcd 100644
--- a/frontend/src/pages/Drawer.test.tsx
+++ b/frontend/src/pages/Drawer.test.tsx
@@ -94,7 +94,7 @@ describe("Drawer Page", () => {
);
expect(screen.getByTestId("passkey-modal-title")).toBeInTheDocument();
- expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument();
+ expect(screen.getByTestId("passkey-input")).toBeInTheDocument();
});
it("renders the welcome letter when firstTime state is present", () => {
diff --git a/frontend/src/pages/Drawer.tsx b/frontend/src/pages/Drawer.tsx
index 0be1e0c..5f05d3f 100644
--- a/frontend/src/pages/Drawer.tsx
+++ b/frontend/src/pages/Drawer.tsx
@@ -58,7 +58,7 @@ export default function Drawer() {
Personal Archive
- Welcome Back{" "}
+ Welcome Back
{user.full_name}
- Write something{" "}
+ Write something
. . . . . .
diff --git a/frontend/src/pages/Editor.test.tsx b/frontend/src/pages/Editor.test.tsx
index 7b8ad34..98b20f7 100644
--- a/frontend/src/pages/Editor.test.tsx
+++ b/frontend/src/pages/Editor.test.tsx
@@ -97,25 +97,22 @@ describe("Editor Page", () => {
fireEvent.click(sealBtn);
// Click Vault to show confirm modal
- const vaultBtn = screen.getByRole("button", { name: /vault/i });
+ const vaultBtn = screen.getByTestId("vault-trigger-btn");
fireEvent.click(vaultBtn);
// Set date and submit vault form
- const dateInput = container.querySelector('input[name="vault-date"]');
+ const dateInput = document.body.querySelector('input[name="vault-date"]');
if (!dateInput) throw new Error("Date input not found");
fireEvent.change(dateInput, { target: { value: "2026-12-31" } });
- const confirmVaultBtn = container.querySelector(
- 'button[form="vault-form"]',
- );
- if (!confirmVaultBtn) throw new Error("Confirm vault button not found");
+ const confirmVaultBtn = screen.getByTestId("vault-confirm-btn");
fireEvent.click(confirmVaultBtn);
// Wait for save to complete and check readOnly
expect(await screen.findByTestId("save-success-toast")).toBeInTheDocument();
expect(canvas.getAttribute("data-readonly")).toBe("true");
- expect(screen.getByLabelText(/recipient/i)).toBeDisabled();
+ expect(screen.getByTestId("recipient-input")).toBeDisabled();
});
it("should set canvas to readOnly when status is SEALED", async () => {
@@ -135,7 +132,7 @@ describe("Editor Page", () => {
}),
);
- const { container } = render(
+ render(
} />
@@ -149,19 +146,17 @@ describe("Editor Page", () => {
const canvas = screen.getByTestId("canvas");
- const toolbar = container.querySelector("#writer-toolbar");
- const sealBtn = toolbar?.querySelector(".btn-primary");
- if (!sealBtn) throw new Error("Seal button not found");
+ const sealBtn = screen.getByTestId("seal-trigger-btn");
fireEvent.click(sealBtn);
// The secondary seal button appears (it has btn-accent class)
- const secondarySealBtn = container.querySelector(".btn-accent");
+ const secondarySealBtn = screen.getByTestId("seal-confirm-btn");
if (!secondarySealBtn) throw new Error("Secondary seal button not found");
fireEvent.click(secondarySealBtn);
expect(await screen.findByTestId("save-success-toast")).toBeInTheDocument();
expect(canvas.getAttribute("data-readonly")).toBe("true");
- expect(screen.getByLabelText(/recipient/i)).toBeDisabled();
+ expect(screen.getByTestId("recipient-input")).toBeDisabled();
});
});
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index 5dd3d0f..53cd4ba 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -149,7 +149,7 @@ export default function Home() {
}}
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
>
- pen down your unsaid words into{" "}
+ pen down your unsaid words into
letters
@@ -171,11 +171,11 @@ export default function Home() {
}}
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
>
- seal it{" "}
+ seal it
secure
- {" "}
- and{" "}
+
+ and
private
@@ -197,7 +197,7 @@ export default function Home() {
}}
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
>
- send it to{" "}
+ send it to
- {" "}
- or{" "}
+ or
yourself in the future
@@ -250,8 +249,8 @@ export default function Home() {
}}
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
>
- and even burn it{" "}
- to release the burden.
+ and even burn it
+ to release the burden.
{/* Outro */}
{
,
);
- await userEvent.type(screen.getByLabelText(/email/i), "test@example.com");
- await userEvent.type(screen.getByLabelText(/password/i), "password123");
- await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
+ await userEvent.type(screen.getByTestId("email-input"), "test@example.com");
+ await userEvent.type(screen.getByTestId("password-input"), "password123");
+ await userEvent.click(screen.getByTestId("login-submit-btn"));
expect(await screen.findByTestId("login-error-message")).toHaveTextContent(
/technical issues/i,
@@ -87,9 +87,9 @@ describe("Login Page", () => {
,
);
- await userEvent.type(screen.getByLabelText(/email/i), "test@example.com");
- await userEvent.type(screen.getByLabelText(/password/i), "password123");
- await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
+ await userEvent.type(screen.getByTestId("email-input"), "test@example.com");
+ await userEvent.type(screen.getByTestId("password-input"), "password123");
+ await userEvent.click(screen.getByTestId("login-submit-btn"));
const expectedTestId =
nextRoute.toLowerCase() === "drawer" ? "drawer-page" : "reader-page";
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
index 2f4b32e..98b319f 100644
--- a/frontend/src/pages/Login.tsx
+++ b/frontend/src/pages/Login.tsx
@@ -120,28 +120,29 @@ export default function Login() {
type="submit"
name="login"
disabled={isLoading}
- aria-label="Sign In"
data-testid="login-submit-btn"
className="btn btn-primary w-full shadow-lg"
>
{isLoading ? (
) : (
- "Sign In"
+ "Continue"
)}
-
-
- Don't have an account?{" "}
+
or
+
+ New to
+ ?
navigate(ROUTES.ONBOARD)}
- className="link link-primary no-underline hover:underline font-bold"
+ className="link link-primary"
>
- Register
+ Start here
+ .
diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx
index b28403a..f7b82a7 100644
--- a/frontend/src/pages/Register.tsx
+++ b/frontend/src/pages/Register.tsx
@@ -77,7 +77,8 @@ export default function Register() {