diff --git a/backend/config/settings.py b/backend/config/settings.py index 2fc07a8..b01bd9b 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -84,6 +84,21 @@ MIDDLEWARE = [ "django_structlog.middlewares.RequestMiddleware", ] +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] ROOT_URLCONF = "config.urls" diff --git a/backend/letters/tasks.py b/backend/letters/tasks.py index d794018..a822b68 100644 --- a/backend/letters/tasks.py +++ b/backend/letters/tasks.py @@ -3,8 +3,10 @@ from datetime import UTC, datetime import structlog from apscheduler.schedulers.background import BackgroundScheduler from django.core.mail import send_mail +from django.template.loader import render_to_string from config import settings +from config.settings import FRONTEND_URLS from letters.models import Letter logger = structlog.get_logger(__name__) @@ -23,9 +25,26 @@ def notify_unlocked_letter(letter): """ author = letter.user.get_username() try: - send_mail(subject="", message="", from_email=settings.FROM_EMAIL, recipient_list=[author], fail_silently=False) + letter_link = f"{FRONTEND_URLS[0]}/read/{letter.public_id}" + subject = "A letter. Written for this exact moment." + context = { + "pen_name": letter.user.first_name, + "cta": {"title": "View what you wrote", "link": letter_link}, + "footnote": True, + } + plaint_content = render_to_string("email/vault_unlock.txt", context=context) + html_content = render_to_string("email/vault_unlock.html", context=context) + send_mail( + subject=subject, + message=plaint_content, + from_email=settings.FROM_EMAIL, + recipient_list=[author], + fail_silently=False, + html_message=html_content, + ) letter.notified_at = datetime.now(UTC) letter.save() + logger.info(f"Successfully notified {author} of unlocked letter") except Exception: logger.exception(f"Failed to notify {author} of unlocked letter") diff --git a/backend/letters/tests.py b/backend/letters/tests.py index 3f9f7ad..04e66d1 100644 --- a/backend/letters/tests.py +++ b/backend/letters/tests.py @@ -396,6 +396,7 @@ class LetterTaskTest(TestCase): from_email=settings.FROM_EMAIL, recipient_list=[self.user.email], fail_silently=False, + html_message=ANY, ) self.assertIsNotNone(letter_to_notify1.notified_at) diff --git a/backend/templates/email/activation.html b/backend/templates/email/activation.html new file mode 100644 index 0000000..5135d5f --- /dev/null +++ b/backend/templates/email/activation.html @@ -0,0 +1,22 @@ +{% extends 'email/base.html' %} + +{% block content %} +
+

{{ pen_name }},

+

+ Your destination is one train away. +

+

I've been keeping a place for your words.
+ Come when you're ready.

+
+{% endblock %} + +{% block footnote %} + This link expires in 24 hours.
+ I'm patient, but not endlessly so. +{% endblock %} + +{% block footer %} + Didn't write to me? Then someone else did.
+ Ignore this. I'll forget you were ever here. +{% endblock %} \ No newline at end of file diff --git a/backend/templates/email/activation.txt b/backend/templates/email/activation.txt new file mode 100644 index 0000000..d115ffd --- /dev/null +++ b/backend/templates/email/activation.txt @@ -0,0 +1,21 @@ +pi. ku. +------------------------------------------- + +{{pen_name}}, + +Your destination is one train away. + +I've been keeping a place for your words. +Come when you're ready. + +{{ cta.title }} -> {{ cta.link }} + +------------------------------------------- + +This link expires in 24 hours. +I'm patient, but not endlessly so. + +------------------------------------------- + +Didn't write to me? Then someone else did. +Ignore this. I'll forget you were ever here. \ No newline at end of file diff --git a/backend/templates/email/base.html b/backend/templates/email/base.html new file mode 100644 index 0000000..09460b9 --- /dev/null +++ b/backend/templates/email/base.html @@ -0,0 +1,100 @@ + + + + + + + + pi. ku. + + + + + + + +
+ + + {# Logo #} + + + + + {# Body #} + + + + + {# CTA #} + {% if cta %} + + + + {% endif %} + + {% if footnote %} + + + + {% endif %} + + {# Footer #} + + + + + + + +
+ Pi.Ku +
+ {% block content %} + {% endblock %} +
+ + + + +
+ + {{ cta.title }} + +
+
+ {% block footnote %} + {% endblock %} +
 
+ {% block footer %} + {% endblock %} +
+
+ + + \ No newline at end of file diff --git a/backend/templates/email/vault_unlock.html b/backend/templates/email/vault_unlock.html new file mode 100644 index 0000000..8f8f0f4 --- /dev/null +++ b/backend/templates/email/vault_unlock.html @@ -0,0 +1,20 @@ +{% extends 'email/base.html' %} + +{% block content %} +

+ Time has a way of making things clearer.
+ Or heavier. Sometimes both. +

+

+ You had something to say at this exact moment.
+ I kept it exactly as you left it.
+ Not a word changed. Not a word read. +

+{% endblock %} + +{% block footnote %} +

+ You're ready now. Or maybe you're still not.
+ Open it anyway. You won't regret it. +

+{% endblock %} \ No newline at end of file diff --git a/backend/templates/email/vault_unlock.txt b/backend/templates/email/vault_unlock.txt new file mode 100644 index 0000000..0b93a64 --- /dev/null +++ b/backend/templates/email/vault_unlock.txt @@ -0,0 +1,17 @@ +pi. ku. +------------------------------------------- + +{{pen_name}}, + +Time has a way of making things clearer. +Or heavier. Sometimes both. + +You had something to say at this exact moment. +I kept it exactly as you left it. +Not a word changed. Not a word read. + +{{ cta.title }} -> {{ cta.link }} + +------------------------------------------- +You're ready now. Or maybe you're still not. +Open it anyway. You won't regret it. \ No newline at end of file diff --git a/backend/users/utils.py b/backend/users/utils.py index 38d302c..9251f33 100644 --- a/backend/users/utils.py +++ b/backend/users/utils.py @@ -1,6 +1,7 @@ from django.conf import settings from django.contrib.auth.tokens import default_token_generator from django.core.mail import send_mail +from django.template.loader import render_to_string from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode @@ -9,16 +10,25 @@ def send_activation_email(user): token = default_token_generator.make_token(user) uid = urlsafe_base64_encode(force_bytes(user.public_id)) activation_url = f"{settings.FRONTEND_URLS[0]}/activate/{uid}/{token}" - subject = "Activate Your Piku Account" - message = f"""Hi {user.full_name}, - - Welcome to Pi Ku. - - Please click the link below to activate your account: - >> {activation_url} - - If you did not create this account, please ignore this email.""" - send_mail(subject, message, settings.FROM_EMAIL, [user.email], fail_silently=False) + subject = "Activate your pi. ku. account" + context = { + "pen_name": user.full_name, + "footnote": True, + "cta": { + "title": "Onboard", + "link": activation_url, + }, + } + html_content = render_to_string("email/activation.html", context) + plain_content = render_to_string("email/activation.txt", context) + send_mail( + subject=subject, + message=plain_content, + from_email=settings.FROM_EMAIL, + recipient_list=[user.email], + fail_silently=False, + html_message=html_content, + ) return True diff --git a/frontend/e2e/letter.spec.ts b/frontend/e2e/letter.spec.ts index c635ab3..7f2c30b 100644 --- a/frontend/e2e/letter.spec.ts +++ b/frontend/e2e/letter.spec.ts @@ -185,7 +185,7 @@ test.describe("Letter Drafting (Real Backend)", () => { await expect(page.getByText(/your letter is sealed/i)).toBeVisible({ timeout: 10000, }); - await page.getByRole("button", { name: /keep it/i }).click(); + await page.getByRole("button", { name: /keep it to myself/i }).click(); // Open "Kept" section - search for the section with id='kept' and click its toggle button logger.info(">> [Drawer] Opening Kept section..."); diff --git a/frontend/e2e/utils/auth.ts b/frontend/e2e/utils/auth.ts index 78a4614..a9d373e 100644 --- a/frontend/e2e/utils/auth.ts +++ b/frontend/e2e/utils/auth.ts @@ -14,13 +14,13 @@ const logger = pino({ /** * Completes the full registration -> activation -> login cycle. */ -export async function registerAndLogin( +async function registerAndLogin( page: Page, email: string, fullName: string, password: string, ) { - // 1. Registration + // Register the User logger.info(`[Auth] Registering user: ${email}`); await page.goto("/onboard"); await page.getByLabel(/pen name/i).fill(fullName); @@ -31,7 +31,7 @@ export async function registerAndLogin( await expect(page).toHaveURL(/\/verify-email/); - // 2. Activation via Mailpit + // Get activation URL from Mailpit and activate user logger.info(`[Auth] Polling Mailpit for activation email...`); const activationLink = await MailpitHelper.getActivationLink(email); @@ -40,11 +40,11 @@ export async function registerAndLogin( await expect(page.getByText(/account activated/i)).toBeVisible(); await page.getByRole("button", { name: /start writing/i }).click(); - // 3. Login + // Dismiss the Welcom Modal and Perform Login logger.info(`[Auth] Logging in...`); await expect(page).toHaveURL(/\/login/); - const welcomeButton = page.getByRole("button", { name: /i understand/i }); + const welcomeButton = page.getByRole("button", { name: /I'll remember/i }); await welcomeButton.waitFor({ state: "visible", timeout: 10000 }); await welcomeButton.click(); await expect(welcomeButton).toBeHidden(); @@ -56,6 +56,4 @@ export async function registerAndLogin( await expect(page).toHaveURL(/\/drawer/); logger.info(`[Auth] Successfully authenticated ${email}`); } - -// Maintain backward compatibility if needed, or update callers export const AuthHelper = { registerAndLogin }; diff --git a/frontend/e2e/utils/mailpit.ts b/frontend/e2e/utils/mailpit.ts index e8408f1..0351937 100644 --- a/frontend/e2e/utils/mailpit.ts +++ b/frontend/e2e/utils/mailpit.ts @@ -31,8 +31,8 @@ export const MailpitHelper = { ); const details = await detailRes.json(); - const body = details.HTML || details.Text || ""; - const match = body.match(/https?:\/\/\S+activate\/\S+/); + const body = details.Text || ""; + const match = body.match(/https?:\/\/\S*activate\S*/); if (match) return match[0]; } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 96e5738..77d9593 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -31,7 +31,7 @@ export default function App() { return ( -
+
}> } /> diff --git a/frontend/src/assets/noise.gif b/frontend/src/assets/noise.gif new file mode 100644 index 0000000..6b76b19 Binary files /dev/null and b/frontend/src/assets/noise.gif differ diff --git a/frontend/src/assets/sf_mini.png b/frontend/src/assets/sf_mini.png new file mode 100644 index 0000000..198533b Binary files /dev/null and b/frontend/src/assets/sf_mini.png differ diff --git a/frontend/src/components/Logo.tsx b/frontend/src/components/Logo.tsx index d7874a6..dbe19b6 100644 --- a/frontend/src/components/Logo.tsx +++ b/frontend/src/components/Logo.tsx @@ -1,7 +1,7 @@ import { DotIcon } from "@phosphor-icons/react"; import "@fontsource/knewave/400.css"; -export default function Logo({ scale = 2 }) { +export default function Logo({ scale = 1 }) { return (
-  Pi + Pi -  Ku +  Ku
diff --git a/frontend/src/components/drawer/LetterItem.tsx b/frontend/src/components/drawer/LetterItem.tsx index 0c1cd58..418b521 100644 --- a/frontend/src/components/drawer/LetterItem.tsx +++ b/frontend/src/components/drawer/LetterItem.tsx @@ -2,6 +2,15 @@ import { LockIcon, LockKeyOpenIcon } from "@phosphor-icons/react"; import { useNavigate } from "react-router-dom"; import { PATHS } from "../../config/routes"; +interface LetterItemProps { + preview: string; + timestamp: string; + id: string; + status: "DRAFT" | "SEALED" | "BURNED"; + unlock_at?: string; + isLocked?: boolean; +} + export function LetterItem({ preview, timestamp, @@ -9,14 +18,7 @@ export function LetterItem({ status, unlock_at, isLocked = false, -}: { - preview: string; - timestamp: string; - id: string; - status: "DRAFT" | "SEALED" | "BURNED"; - unlock_at?: string; - isLocked?: boolean; -}) { +}: LetterItemProps) { const navigate = useNavigate(); function handleNavigate(): void { if (isLocked) return; diff --git a/frontend/src/components/editor/PostSealModal.tsx b/frontend/src/components/editor/PostSealModal.tsx index 1f1c2de..792cead 100644 --- a/frontend/src/components/editor/PostSealModal.tsx +++ b/frontend/src/components/editor/PostSealModal.tsx @@ -5,11 +5,13 @@ import { PATHS, ROUTES } from "../../config/routes"; interface PostSealModalProps { sealedTargetId: string | null; navigate: NavigateFunction; + type: "KEPT" | "VAULT"; } export function PostSealModal({ sealedTargetId, navigate, + type = "KEPT", }: PostSealModalProps) { if (!sealedTargetId) return null; return ( @@ -20,33 +22,61 @@ export function PostSealModal({

It's encrypted and always safe in your drawer.

-

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

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

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

+ ) : ( +

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

+ )}
- - + {type === "KEPT" ? ( + <> + + + + ) : ( + + )}
diff --git a/frontend/src/components/editor/ToolBar.tsx b/frontend/src/components/editor/ToolBar.tsx index efe4c3d..061b6ce 100644 --- a/frontend/src/components/editor/ToolBar.tsx +++ b/frontend/src/components/editor/ToolBar.tsx @@ -102,10 +102,24 @@ export function ToolBar({ ); @@ -136,21 +150,23 @@ export function VaultConfirmModal({ setUnlockDate, }: VaultConfirmModalProps) { return ( -
-
+
+
-

Vault this letter?

+

Take it away, then?

- Vaulting locks the letter permanently and will be{" "} - mailed to you - automatically on the unlock date. + By vaulting this letter, you ask me to hold on to this.
- - You cannot edit or view the contents of the letter until then. + I'll remember to mail you this on the unlock date. +
+ + {" "} + But I won't let you read or rewrite this letter until then. +

{ @@ -158,11 +174,13 @@ export function VaultConfirmModal({ const formData = new FormData(e.currentTarget); const unlockDateStr = formData.get("vault-date") as string; const newUnlockDate = new Date(unlockDateStr); + console.log(newUnlockDate); setUnlockDate(newUnlockDate); await onSave("VAULT", newUnlockDate); setConfirmModal(null); }} id="vault-form" + className="min-w-75" >
Set an unlock date @@ -173,21 +191,22 @@ export function VaultConfirmModal({ className="input input-bordered w-full" name="vault-date" /> - - - +
+ + +
diff --git a/frontend/src/components/reader/BurnModal.tsx b/frontend/src/components/reader/BurnModal.tsx index 4fdb372..e5e5154 100644 --- a/frontend/src/components/reader/BurnModal.tsx +++ b/frontend/src/components/reader/BurnModal.tsx @@ -1,12 +1,19 @@ import { CampfireIcon, FlameIcon, XCircleIcon } from "@phosphor-icons/react"; import { useEffect, useState } from "react"; +interface BurnModalProps { + burnLetter: () => void; + isBurning: boolean; + setShowBurnModal: (show: boolean) => void; + setRevealState: (state: "sealed" | "revealed" | "burning" | "burned") => void; +} + export function BurnModal({ burnLetter, isBurning, setShowBurnModal, setRevealState, -}) { +}: BurnModalProps) { const [flameOn, setFlameOn] = useState(0); const [rotate, setRotate] = useState(0); const [burnClicked, setBurnClicked] = useState(false); diff --git a/frontend/src/components/reader/EnvelopeReveal.tsx b/frontend/src/components/reader/EnvelopeReveal.tsx index beaebed..dc62736 100644 --- a/frontend/src/components/reader/EnvelopeReveal.tsx +++ b/frontend/src/components/reader/EnvelopeReveal.tsx @@ -9,6 +9,7 @@ export interface EnvelopeRevealProps { onRevealComplete: () => void; ignite: boolean; isFlip?: boolean; + isInteractive?: boolean; } export function EnvelopeReveal({ @@ -17,6 +18,7 @@ export function EnvelopeReveal({ onRevealComplete, ignite, isFlip, + isInteractive = true, }: EnvelopeRevealProps) { const [revealLetter, setRevealLetter] = useState(false); const [isFlipped, setIsFlipped] = useState(!!isFlip); @@ -67,6 +69,7 @@ export function EnvelopeReveal({ type="checkbox" className="transition checkbox absolute h-full w-full text-transparent bg-transparent z-100" ref={flapCheckbox} + disabled={!isInteractive} />
setIsFlipped((prev) => !prev)} > diff --git a/frontend/src/components/reader/PostActionOverlay.tsx b/frontend/src/components/reader/PostActionOverlay.tsx index 4959fed..2b0caf3 100644 --- a/frontend/src/components/reader/PostActionOverlay.tsx +++ b/frontend/src/components/reader/PostActionOverlay.tsx @@ -1,7 +1,11 @@ import { useNavigate } from "react-router-dom"; import { ROUTES } from "../../config/routes"; -export function PostActionOverlay({ revealState }) { +interface PostActionOverlayProps { + revealState: "sealed" | "revealed" | "burning" | "burned"; +} + +export function PostActionOverlay({ revealState }: PostActionOverlayProps) { const navigate = useNavigate(); return (
void; +} + +export function ShareModal({ shareLink, setShareLink }: ShareModalProps) { const copyToClipboard = async () => { if (!shareLink) return; await navigator.clipboard.writeText(shareLink); diff --git a/frontend/src/components/ui/FormField.tsx b/frontend/src/components/ui/FormField.tsx index e6d42a6..367cd77 100644 --- a/frontend/src/components/ui/FormField.tsx +++ b/frontend/src/components/ui/FormField.tsx @@ -6,6 +6,7 @@ interface FormFieldProps { placeholder?: string; registration: UseFormRegisterReturn; error?: string; + handleFocus?: () => void; } export default function FormField({ @@ -14,6 +15,7 @@ export default function FormField({ placeholder, registration, error, + handleFocus, }: FormFieldProps) { return (
@@ -31,6 +33,7 @@ export default function FormField({ className={`input input-bordered focus:input-primary ${ error ? "input-error" : "" }`} + onFocus={handleFocus} /> {error &&

{error}

}
diff --git a/frontend/src/components/ui/Saajan.tsx b/frontend/src/components/ui/Saajan.tsx new file mode 100644 index 0000000..0f973ee --- /dev/null +++ b/frontend/src/components/ui/Saajan.tsx @@ -0,0 +1,53 @@ +import { useEffect, useState } from "react"; +import sf_mini from "../../assets/sf_mini.png"; + +interface SaajanProps { + message: string; + position?: "top" | "left" | "right" | "bottom"; +} + +export default function Saajan({ message, position = "right" }: SaajanProps) { + const [animate, setAnimate] = useState(false); + const [tooltipPosition, setTooltipPosition] = + useState("tooltip-right"); + const [alignment, setAlignment] = useState("justify-start"); + + useEffect(() => { + setAnimate(true); + const timeout = setTimeout(() => { + setAnimate(false); + }, 1000); + + return () => { + clearTimeout(timeout); + }; + }, []); + + useEffect(() => { + setTooltipPosition(`tooltip-${position}`); + if (position === "top") { + setAlignment("justify-center"); + } + if (position === "right") { + setAlignment("justify-start"); + } + if (position === "left") { + setAlignment("justify-end"); + } + }, [position]); + + return ( +
+
+ saajan +
+
+ ); +} diff --git a/frontend/src/hooks/useLetters.tsx b/frontend/src/hooks/useLetters.tsx index be33475..5f8e657 100644 --- a/frontend/src/hooks/useLetters.tsx +++ b/frontend/src/hooks/useLetters.tsx @@ -86,7 +86,7 @@ export function useLetters() { return { drafts: letters.filter((l) => l.status === "DRAFT"), kept: letters.filter((l) => l.type === "KEPT" && l.status === "SEALED"), - vault: letters.filter((l) => l.type === "VAULT"), + vault: letters.filter((l) => l.type === "VAULT" && l.status === "SEALED"), sent: letters.filter((l) => l.type === "SENT" && l.status === "SEALED"), }; }, [letters]); diff --git a/frontend/src/index.css b/frontend/src/index.css index d2a940c..8071edf 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -47,10 +47,7 @@ --font-display: "Playwrite HR Lijeva Variable", cursive; --font-sans: "Jost Variable", sans-serif; --font-serif: "Playfair Display Variable", serif; - --color-glass-bg: rgba(28, - 22, - 16, - 0.45); + --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); diff --git a/frontend/src/pages/Activate.tsx b/frontend/src/pages/Activate.tsx index d81d9ae..740b79e 100644 --- a/frontend/src/pages/Activate.tsx +++ b/frontend/src/pages/Activate.tsx @@ -16,8 +16,6 @@ export default function Activate() { useEffect(() => { if (!(uidb64 && token) || hasCalled.current) return; - - // prevent double api calls hasCalled.current = true; const activateAccount = async () => { @@ -46,7 +44,7 @@ export default function Activate() { )} {status === "success" && ( -
+
Account Activated! -

- Welcome to +

+ Welcome to
Your identity is now verified and ready for timeless letters.

-
- +

Activation Failed

-

+

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

+
)} diff --git a/frontend/src/pages/Drawer.tsx b/frontend/src/pages/Drawer.tsx index d49b509..2692568 100644 --- a/frontend/src/pages/Drawer.tsx +++ b/frontend/src/pages/Drawer.tsx @@ -5,6 +5,7 @@ import { DrawerSection } from "../components/drawer/DrawerSection.tsx"; import { LetterItem } from "../components/drawer/LetterItem.tsx"; import { PasskeyModal } from "../components/drawer/PasskeyModal.tsx"; import Logo from "../components/Logo"; +import Saajan from "../components/ui/Saajan.tsx"; import { PATHS } from "../config/routes"; import { useAuth } from "../hooks/useAuth"; import { useLetters } from "../hooks/useLetters"; @@ -165,6 +166,12 @@ export default function Drawer() {
For your unsaid.
+
+ +
); } diff --git a/frontend/src/pages/Editor.test.tsx b/frontend/src/pages/Editor.test.tsx index f1dc66e..d1e63c4 100644 --- a/frontend/src/pages/Editor.test.tsx +++ b/frontend/src/pages/Editor.test.tsx @@ -83,11 +83,9 @@ describe("Editor Page", () => { expect(screen.queryByText(/Opening your draft/i)).not.toBeInTheDocument(); }); - // Initial state: DRAFT (not read-only) const canvas = screen.getByTestId("canvas"); expect(canvas.getAttribute("data-readonly")).toBe("false"); - // Click Seal in the main toolbar (it's in the div with id="writer-toolbar") const toolbar = container.querySelector("#writer-toolbar"); const sealBtn = toolbar?.querySelector(".btn-primary"); if (!sealBtn) throw new Error("Seal button not found"); diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index 4d81ea6..c481516 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -293,7 +293,7 @@ export default function Editor() { setLetterStatus(status); setLastSavedPulseTick((prev) => prev + 1); - if (status === "SEALED") { + if (status === "SEALED" || status === "VAULT") { setSealedTargetId(targetId); } setSaveOverlay("saved"); @@ -419,7 +419,11 @@ export default function Editor() { /> )} {sealedTargetId && ( - + )}
diff --git a/frontend/src/pages/Login.test.tsx b/frontend/src/pages/Login.test.tsx index 7ff13a9..c6f4af3 100644 --- a/frontend/src/pages/Login.test.tsx +++ b/frontend/src/pages/Login.test.tsx @@ -14,16 +14,6 @@ describe("Login Page", () => { server.resetHandlers(); }); - it("should render the sign-in form correctly", () => { - render( - - - , - ); - - expect(screen.getByText("Sign in to")).toBeInTheDocument(); - }); - it("should display a technical issues message when the server is down", async () => { server.use( http.post(`${API_URL}${endpoints.LOGIN}`, () => diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 7574f75..b5b0240 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,5 +1,9 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { ShieldCheckIcon, WarningIcon } from "@phosphor-icons/react"; +import { + HandPalmIcon, + ShieldCheckIcon, + WarningIcon, +} from "@phosphor-icons/react"; import axios from "axios"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -8,6 +12,7 @@ import { z } from "zod"; import { api, publicApi } from "../api/apiClient"; import Logo from "../components/Logo"; import FormField from "../components/ui/FormField"; +import Saajan from "../components/ui/Saajan"; import { endpoints } from "../config/endpoints"; import { ROUTES } from "../config/routes"; import { useAuth } from "../hooks/useAuth"; @@ -20,10 +25,19 @@ const loginSchema = z.object({ type LoginInputs = z.infer; -function WelcomeModal({ setShowWelcome }) { +function WelcomeModal({ + setShowWelcome, +}: { + setShowWelcome: (show: boolean) => void; +}) { return (
-
+
+ +
+

- Welcome to ! + Welcome to   +  !

- To ensure complete privacy, all - your letters are{" "} - - sealed with your password - - , which only you have access to. + Before we begin, let me make a small promise. + +


- - The server never sees it, and it's a solemn promise! - + 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.

@@ -53,6 +70,19 @@ function WelcomeModal({ setShowWelcome }) {

If you ever happen to forget your password, your letters are lost to time, forever. +
+ + I highly, highly recommend storing this password in your{" "} + + password manager + {" "} + or somewhere safe to remember it. +

@@ -62,7 +92,7 @@ function WelcomeModal({ setShowWelcome }) { onClick={() => setShowWelcome(false)} className="btn btn-primary w-full shadow-lg" > - I understand + I'll remember
@@ -78,6 +108,9 @@ export default function Login() { 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 { @@ -125,12 +158,13 @@ export default function Login() { }; return ( -
+
+ {!showWelcome && } {showWelcome && }

- Sign in to + Enter Archive

{apiError && ( @@ -142,9 +176,10 @@ export default function Login() { setSaajanMessage("I remember you.")} /> + setSaajanMessage("The one thing I cannot know for you.") + } />
diff --git a/frontend/src/pages/Reader.tsx b/frontend/src/pages/Reader.tsx index ef09c98..537334b 100644 --- a/frontend/src/pages/Reader.tsx +++ b/frontend/src/pages/Reader.tsx @@ -44,7 +44,7 @@ export default function Reader() { const [isDecrypting, setIsDecrypting] = useState(true); const [revealState, setRevealState] = useState< - "sealed" | "revealed" | "burned" + "sealed" | "revealed" | "burned" | "burning" >("sealed"); const [error, setError] = useState<{ message: string; diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index 662b814..7a769db 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -8,6 +8,7 @@ import { z } from "zod"; import { publicApi } from "../api/apiClient"; import Logo from "../components/Logo"; import FormField from "../components/ui/FormField"; +import Saajan from "../components/ui/Saajan"; import { endpoints } from "../config/endpoints"; import { ROUTES } from "../config/routes"; import { CryptoUtils } from "../utils/crypto"; @@ -31,6 +32,9 @@ 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 { register, @@ -41,6 +45,7 @@ export default function Register() { }); const onSubmit = async (data: RegisterInputs) => { + setSaajanMessage("Good. I'll remember that."); setIsLoading(true); setApiError(null); try { @@ -68,74 +73,96 @@ export default function Register() { }; return ( -
- -

- Create a Account -

- - {apiError && ( -
- {apiError} +
+ +
+ +
+ 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.", + ) + } + /> - {/* Warning */} -
- -

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

-
+ + setSaajanMessage( + "Just once? Trust me, \nsome things are worth repeating twice.", + ) + } + /> -
- -
- + {/* Warning */} +
+ +

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

+
+ +
+ +
+ +
); } diff --git a/frontend/src/pages/VerifyEmail.tsx b/frontend/src/pages/VerifyEmail.tsx index 3c0c3be..03145c0 100644 --- a/frontend/src/pages/VerifyEmail.tsx +++ b/frontend/src/pages/VerifyEmail.tsx @@ -1,41 +1,55 @@ import { EnvelopeSimpleOpenIcon } from "@phosphor-icons/react"; import Logo from "../components/Logo"; +import Saajan from "../components/ui/Saajan"; export default function VerifyEmail() { return ( -
-
- +
+ + +
+
+ +
+ +
+

+ Check Your Mailbox +

+

+ You're one train away from starting your {" "} + journey. +

+
+ +
+ +
+

+ Nothing yet? Sometimes letters take the wrong train. Check your spam + folder. +
+ + The link expires in 24 hours. + +
I'm patient... but not endlessly so +

+
+ +
- -
-

Check Your Email

-

- We've sent an activation link to your inbox.
- Please click it to verify your account. -

-
- -
- -
-

- Didn't receive it? Check your spam folder or wait for a few minutes. - The link will expire in 24 hours. -

-
- -
); }