feat/welcome-letter integration #2

Merged
me merged 5 commits from feature/welcome-letter into main 2026-05-06 16:46:53 +00:00
4 changed files with 127 additions and 70 deletions
Showing only changes of commit 9800174df1 - Show all commits
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

@@ -1,6 +1,6 @@
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { getWelcomeContent } from "../../config/welcomeLetter"; import { getWelcomeLetterContent } from "../../config/welcomeLetter";
import { formatDate } from "../../utils/dateFormat"; import { formatDate } from "../../utils/dateFormat";
import { type CanvasTools, ComposeCanvas } from "../editor/ComposeCanvas"; import { type CanvasTools, ComposeCanvas } from "../editor/ComposeCanvas";
import { EnvelopeReveal } from "../reader/EnvelopeReveal"; import { EnvelopeReveal } from "../reader/EnvelopeReveal";
@@ -21,7 +21,7 @@ export function WelcomeLetterOverlay({
useEffect(() => { useEffect(() => {
if (revealState === "REVEALED" && canvasRef.current) { if (revealState === "REVEALED" && canvasRef.current) {
const welcomeContent = getWelcomeContent(userName); const welcomeContent = getWelcomeLetterContent(userName);
canvasRef.current.loadData(welcomeContent); canvasRef.current.loadData(welcomeContent);
} }
}, [revealState, userName]); }, [revealState, userName]);
+101 -61
View File
@@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { mockUser } from "../../test/fixtures/user.fixture"; import { mockUser } from "../../test/fixtures/user.fixture";
@@ -7,77 +7,117 @@ import { useAuthStore } from "../store/useAuthStore";
import Drawer from "./Drawer"; import Drawer from "./Drawer";
vi.mock("../hooks/useLetters"); vi.mock("../hooks/useLetters");
vi.mock("../components/drawer/WelcomeLetterOverlay", () => ({
WelcomeLetterOverlay: ({ onComplete }: any) => (
<div data-testid="welcome-letter-overlay">
<button
type="button"
data-testid="overlay-exit-button"
onClick={onComplete}
>
I'll see you
</button>
</div>
),
}));
describe("Drawer Page", () => { describe("Drawer Page", () => {
beforeEach(() => { beforeEach(() => {
// Setup authenticated state for the test // Setup authenticated state for the test
useAuthStore.setState({ useAuthStore.setState({
user: mockUser, user: mockUser,
accessToken: "fake-token", accessToken: "fake-token",
isInitializing: false, isInitializing: false,
});
vi.mocked(useLetters).mockReturnValue({
drafts: [],
kept: [],
sent: [],
vault: [],
loading: false,
isAuthRequired: false,
});
}); });
it("renders the cabinet sections and empty state message", () => { vi.mocked(useLetters).mockReturnValue({
render( drafts: [],
<MemoryRouter> kept: [],
<Drawer /> sent: [],
</MemoryRouter>, vault: [],
); loading: false,
isAuthRequired: false,
});
});
expect(screen.getByText(/Drafts/i)).toBeInTheDocument(); it("renders the cabinet sections and empty state message", () => {
expect(screen.getAllByText(/Kept/i).length).toBeGreaterThanOrEqual(1); render(
expect(screen.getByText(/Vault/i)).toBeInTheDocument(); <MemoryRouter>
expect(screen.getByText(/This drawer remains silent/i)).toBeInTheDocument(); <Drawer />
</MemoryRouter>,
);
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", () => { render(
vi.mocked(useLetters).mockReturnValue({ <MemoryRouter>
drafts: [], <Drawer />
kept: [], </MemoryRouter>,
sent: [], );
vault: [],
loading: true,
isAuthRequired: false,
});
render( expect(screen.getByText(/Opening your cabinet/i)).toBeInTheDocument();
<MemoryRouter> });
<Drawer />
</MemoryRouter>,
);
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", () => { render(
vi.mocked(useLetters).mockReturnValue({ <MemoryRouter>
drafts: [], <Drawer />
kept: [], </MemoryRouter>,
sent: [], );
vault: [],
loading: false,
isAuthRequired: true,
});
render( expect(screen.getByText(/You've been away a while./i)).toBeInTheDocument();
<MemoryRouter> expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument();
<Drawer /> });
</MemoryRouter>,
);
expect( it("renders the welcome letter when firstTime state is present", () => {
screen.getByText(/You've been away a while./i), render(
).toBeInTheDocument(); <MemoryRouter
expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument(); initialEntries={[{ pathname: "/drawer", state: { firstTime: true } }]}
}); >
<Drawer />
</MemoryRouter>,
);
expect(screen.getByTestId("welcome-letter-overlay")).toBeInTheDocument();
});
it("renders the drawer content when the letter is closed", () => {
render(
<MemoryRouter
initialEntries={[{ pathname: "/drawer", state: { firstTime: true } }]}
>
<Drawer />
</MemoryRouter>,
);
const completeButton = screen.getByTestId("overlay-exit-button");
fireEvent.click(completeButton);
expect(
screen.queryByTestId("welcome-letter-overlay"),
).not.toBeInTheDocument();
});
}); });
+24 -7
View File
@@ -1,9 +1,10 @@
import { FeatherIcon } from "@phosphor-icons/react"; import { FeatherIcon } from "@phosphor-icons/react";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { DrawerSection } from "../components/drawer/DrawerSection.tsx"; import { DrawerSection } from "../components/drawer/DrawerSection.tsx";
import { LetterItem } from "../components/drawer/LetterItem.tsx"; import { LetterItem } from "../components/drawer/LetterItem.tsx";
import { PasskeyModal } from "../components/drawer/PasskeyModal.tsx"; import { PasskeyModal } from "../components/drawer/PasskeyModal.tsx";
import { WelcomeLetterOverlay } from "../components/drawer/WelcomeLetterOverlay.tsx";
import Logo from "../components/Logo"; import Logo from "../components/Logo";
import Saajan from "../components/ui/Saajan.tsx"; import Saajan from "../components/ui/Saajan.tsx";
import { PATHS } from "../config/routes"; import { PATHS } from "../config/routes";
@@ -19,6 +20,10 @@ export default function Drawer() {
const [openSection, setOpenSection] = useState<string | null>(null); const [openSection, setOpenSection] = useState<string | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const [showWelcomeLetter, setShowWelcomeLetter] = useState(
!!location.state?.firstTime,
);
const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters(); const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters();
if (!user) return null; if (!user) return null;
@@ -30,6 +35,16 @@ export default function Drawer() {
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors"> <div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
<div className="fixed inset-0 bg-vig pointer-events-none z-0" /> <div className="fixed inset-0 bg-vig pointer-events-none z-0" />
{showWelcomeLetter && (
<WelcomeLetterOverlay
userName={user.full_name}
onComplete={() => {
setShowWelcomeLetter(false);
navigate(location.pathname, { replace: true, state: {} });
}}
/>
)}
{isAuthRequired && <PasskeyModal />} {isAuthRequired && <PasskeyModal />}
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-500"> <header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-500">
<Logo /> <Logo />
@@ -166,12 +181,14 @@ export default function Drawer() {
<footer className="mt-25 font-sans text-[0.6rem] tracking-widester uppercase text-base-content/10 z-10"> <footer className="mt-25 font-sans text-[0.6rem] tracking-widester uppercase text-base-content/10 z-10">
For your unsaid. For your unsaid.
</footer> </footer>
<div className="absolute bottom-0 z-50 font-sans"> {!showWelcomeLetter && (
<Saajan <div className="absolute bottom-0 z-50 font-sans">
message={`Good to see you again, ${user.full_name}.\nWhat's on your mind today?`} <Saajan
position="top" message={`Good to see you again, ${user.full_name}.\nWhat's on your mind today?`}
/> position="top"
</div> />
</div>
)}
</div> </div>
); );
} }