style: revamp homepage and drawer
Co-authored-by: me <ramvignesh-b@github.com> Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 34 KiB |
+17
-10
@@ -1,6 +1,6 @@
|
|||||||
import { lazy, Suspense, useEffect, useRef } from "react";
|
import { lazy, Suspense, useEffect, useRef } from "react";
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||||
import { ProtectedRoute, PublicRoute } from "./components/RouteGuards";
|
import { AutoRedirectRoute, ProtectedRoute } from "./components/RouteGuards";
|
||||||
import SplashScreen from "./components/SplashScreen";
|
import SplashScreen from "./components/SplashScreen";
|
||||||
import { ROUTES } from "./config/routes";
|
import { ROUTES } from "./config/routes";
|
||||||
import { useAuth } from "./hooks/useAuth";
|
import { useAuth } from "./hooks/useAuth";
|
||||||
@@ -34,38 +34,45 @@ export default function App() {
|
|||||||
<main className="relative min-h-screen min-w-screen flex items-center justify-center w-full bg-base-200 before:absolute before:top-0 before:left-0 before:w-full before:h-full before:content-[''] before:opacity-[0.03] before:z-50 before:pointer-events-none before:bg-[url('assets/noise.gif')]">
|
<main className="relative min-h-screen min-w-screen flex items-center justify-center w-full bg-base-200 before:absolute before:top-0 before:left-0 before:w-full before:h-full before:content-[''] before:opacity-[0.03] before:z-50 before:pointer-events-none before:bg-[url('assets/noise.gif')]">
|
||||||
<Suspense fallback={<SplashScreen />}>
|
<Suspense fallback={<SplashScreen />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={ROUTES.HOME} element={<Home />} />
|
<Route
|
||||||
|
path={ROUTES.HOME}
|
||||||
|
element={
|
||||||
|
<AutoRedirectRoute>
|
||||||
|
<Home />
|
||||||
|
</AutoRedirectRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={ROUTES.ONBOARD}
|
path={ROUTES.ONBOARD}
|
||||||
element={
|
element={
|
||||||
<PublicRoute>
|
<AutoRedirectRoute>
|
||||||
<Register />
|
<Register />
|
||||||
</PublicRoute>
|
</AutoRedirectRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={ROUTES.LOGIN}
|
path={ROUTES.LOGIN}
|
||||||
element={
|
element={
|
||||||
<PublicRoute>
|
<AutoRedirectRoute>
|
||||||
<Login />
|
<Login />
|
||||||
</PublicRoute>
|
</AutoRedirectRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={ROUTES.VERIFY_EMAIL}
|
path={ROUTES.VERIFY_EMAIL}
|
||||||
element={
|
element={
|
||||||
<PublicRoute>
|
<AutoRedirectRoute>
|
||||||
<VerifyEmail />
|
<VerifyEmail />
|
||||||
</PublicRoute>
|
</AutoRedirectRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={ROUTES.ACTIVATE}
|
path={ROUTES.ACTIVATE}
|
||||||
element={
|
element={
|
||||||
<PublicRoute>
|
<AutoRedirectRoute>
|
||||||
<Activate />
|
<Activate />
|
||||||
</PublicRoute>
|
</AutoRedirectRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ import "@fontsource/knewave/400.css";
|
|||||||
|
|
||||||
interface LogoProps {
|
interface LogoProps {
|
||||||
scale?: number;
|
scale?: number;
|
||||||
type?: "inline" | "mono" | "logo";
|
type?: "inline" | "mono" | "logo" | null;
|
||||||
|
ul?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Logo({ scale = 1, type = "logo" }: LogoProps) {
|
export default function Logo({
|
||||||
|
scale = 1,
|
||||||
|
type = null,
|
||||||
|
ul = false,
|
||||||
|
}: LogoProps) {
|
||||||
if (type === "inline") {
|
if (type === "inline") {
|
||||||
return (
|
return (
|
||||||
<span className={"text-accent font-display italic "}>
|
<span className={"text-accent font-display italic "}>
|
||||||
@@ -24,24 +29,35 @@ export default function Logo({ scale = 1, type = "logo" }: LogoProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "logo") {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src="/logo.svg"
|
||||||
|
alt="Pi. Ku. logo"
|
||||||
|
className="mx-4"
|
||||||
|
width={scale * 100}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="img"
|
role="img"
|
||||||
aria-label="Pi. Ku. logo"
|
aria-label="Pi. Ku. logo"
|
||||||
className="inline-flex items-baseline justify-center leading-none select-none"
|
className={`inline-flex items-baseline justify-center leading-none select-none ${ul ? "ul-wavy" : ""}`}
|
||||||
style={{ fontFamily: "'Knewave', serif", scale }}
|
style={{ fontFamily: "'Knewave', serif", scale }}
|
||||||
>
|
>
|
||||||
<span className={`text-3xl font-light text-accent`}>Pi</span>
|
<span className="text-3xl font-light text-accent">Pi</span>
|
||||||
<DotIcon
|
<DotIcon
|
||||||
weight="fill"
|
weight="fill"
|
||||||
size={12}
|
size={12}
|
||||||
className={`text-primary translate-y-1 -mx-px`}
|
className="text-primary translate-y-1 -mx-px"
|
||||||
/>
|
/>
|
||||||
<span className={`text-3xl font-light text-accent`}> Ku</span>
|
<span className="text-3xl font-light text-accent"> Ku</span>
|
||||||
<DotIcon
|
<DotIcon
|
||||||
weight="fill"
|
weight="fill"
|
||||||
size={12}
|
size={12}
|
||||||
className={`text-primary translate-y-1 -mx-px`}
|
className="text-primary translate-y-1 -mx-px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,14 +3,20 @@ import { MemoryRouter, Route, Routes } from "react-router-dom";
|
|||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { mockUser } from "../../test/fixtures/user.fixture";
|
import { mockUser } from "../../test/fixtures/user.fixture";
|
||||||
import { useAuthStore } from "../store/useAuthStore";
|
import { useAuthStore } from "../store/useAuthStore";
|
||||||
import { ProtectedRoute, PublicRoute } from "./RouteGuards";
|
import { AutoRedirectRoute, ProtectedRoute } from "./RouteGuards";
|
||||||
|
|
||||||
function renderGuard(ui: React.ReactNode, mountPath: "/protected" | "/public") {
|
function renderGuard(ui: React.ReactNode, mountPath: "/protected" | "/public") {
|
||||||
return render(
|
return render(
|
||||||
<MemoryRouter initialEntries={[mountPath]}>
|
<MemoryRouter initialEntries={[mountPath]}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<div data-testid="login-page">Login Page</div>} />
|
<Route
|
||||||
<Route path="/drawer" element={<div data-testid="drawer-page">Drawer Page</div>} />
|
path="/login"
|
||||||
|
element={<div data-testid="login-page">Login Page</div>}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/drawer"
|
||||||
|
element={<div data-testid="drawer-page">Drawer Page</div>}
|
||||||
|
/>
|
||||||
<Route path="/protected" element={ui} />
|
<Route path="/protected" element={ui} />
|
||||||
<Route path="/public" element={ui} />
|
<Route path="/public" element={ui} />
|
||||||
</Routes>
|
</Routes>
|
||||||
@@ -85,9 +91,9 @@ describe("PublicRoute", () => {
|
|||||||
user: null,
|
user: null,
|
||||||
});
|
});
|
||||||
renderGuard(
|
renderGuard(
|
||||||
<PublicRoute>
|
<AutoRedirectRoute>
|
||||||
<div data-testid="mock-login-page">Login Page</div>
|
<div data-testid="mock-login-page">Login Page</div>
|
||||||
</PublicRoute>,
|
</AutoRedirectRoute>,
|
||||||
"/public",
|
"/public",
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId("splash-screen")).toBeInTheDocument();
|
expect(screen.getByTestId("splash-screen")).toBeInTheDocument();
|
||||||
@@ -101,9 +107,9 @@ describe("PublicRoute", () => {
|
|||||||
user: mockUser,
|
user: mockUser,
|
||||||
});
|
});
|
||||||
renderGuard(
|
renderGuard(
|
||||||
<PublicRoute>
|
<AutoRedirectRoute>
|
||||||
<div data-testid="login-form">Login Form</div>
|
<div data-testid="login-form">Login Form</div>
|
||||||
</PublicRoute>,
|
</AutoRedirectRoute>,
|
||||||
"/public",
|
"/public",
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId("drawer-page")).toBeInTheDocument();
|
expect(screen.getByTestId("drawer-page")).toBeInTheDocument();
|
||||||
@@ -117,9 +123,9 @@ describe("PublicRoute", () => {
|
|||||||
user: null,
|
user: null,
|
||||||
});
|
});
|
||||||
renderGuard(
|
renderGuard(
|
||||||
<PublicRoute>
|
<AutoRedirectRoute>
|
||||||
<div data-testid="login-form">Login Form</div>
|
<div data-testid="login-form">Login Form</div>
|
||||||
</PublicRoute>,
|
</AutoRedirectRoute>,
|
||||||
"/public",
|
"/public",
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId("login-form")).toBeInTheDocument();
|
expect(screen.getByTestId("login-form")).toBeInTheDocument();
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ export function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public - auth route guard.
|
* Auto-redirect - auth route guard.
|
||||||
* If authenticated, redirect all the auth related flows to the drawer
|
* If authenticated, redirect all the auth related flows to the drawer
|
||||||
*/
|
*/
|
||||||
export function PublicRoute({ children }: { children: React.ReactNode }) {
|
export function AutoRedirectRoute({ children }: { children: React.ReactNode }) {
|
||||||
const { isAuthenticated, isInitializing } = useAuth();
|
const { isAuthenticated, isInitializing } = useAuth();
|
||||||
|
|
||||||
if (isInitializing) return <SplashScreen />;
|
if (isInitializing) return <SplashScreen />;
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import Logo from "./Logo";
|
|||||||
|
|
||||||
export default function SplashScreen() {
|
export default function SplashScreen() {
|
||||||
return (
|
return (
|
||||||
<div data-testid="splash-screen" className="fixed w-screen h-screen inset-0 flex flex-col items-center justify-center z-9999 before:absolute before:top-0 before:left-0 before:w-full before:h-full before:content-[''] before:opacity-[0.03] before:z-10 before:pointer-events-none before:bg-[url('assets/noise.gif')">
|
<div
|
||||||
|
data-testid="splash-screen"
|
||||||
|
className="fixed w-screen h-screen inset-0 flex flex-col items-center justify-center z-9999 before:absolute before:top-0 before:left-0 before:w-full before:h-full before:content-[''] before:opacity-[0.03] before:z-10 before:pointer-events-none before:bg-[url('assets/noise.gif')"
|
||||||
|
>
|
||||||
<div className="flex flex-col items-center gap-6 animate-pulse">
|
<div className="flex flex-col items-center gap-6 animate-pulse">
|
||||||
<Logo />
|
<Logo />
|
||||||
|
|
||||||
|
|||||||
@@ -3,19 +3,23 @@ import { GearFineIcon } from "@phosphor-icons/react";
|
|||||||
interface DrawerSectionProps {
|
interface DrawerSectionProps {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
count: string;
|
count: number;
|
||||||
|
subtext: string;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
icon: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DrawerSection({
|
export function DrawerSection({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
count,
|
count,
|
||||||
|
subtext,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
|
icon,
|
||||||
}: DrawerSectionProps) {
|
}: DrawerSectionProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -23,20 +27,32 @@ export function DrawerSection({
|
|||||||
className={`join-item group flex flex-col transition-colors duration-3000 ease-in-out ${isOpen ? "bg-base-300/30" : ""}`}
|
className={`join-item group flex flex-col transition-colors duration-3000 ease-in-out ${isOpen ? "bg-base-300/30" : ""}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`transition-all duration-1500 ease-in-out bg-neutral/10 ${
|
className={`bg-neutral/10 transition-all duration-1000 ease-in-out overflow-visible ${isOpen ? "max-h-125" : "max-h-0 pointer-events-none"}`}
|
||||||
isOpen
|
|
||||||
? "max-h-125 opacity-100 py-3 border-b border-base-content/5 overflow-visible"
|
|
||||||
: "max-h-0 opacity-0 pointer-events-none"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{children}
|
<div
|
||||||
|
className={`transition-opacity ease-in-out ${
|
||||||
|
isOpen
|
||||||
|
? "opacity-100 py-3 border-b border-base-content/5 duration-700 delay-500"
|
||||||
|
: "opacity-0 duration-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{count === 0 && (
|
||||||
|
<p
|
||||||
|
data-testid={`empty-drawer-message-${id}`}
|
||||||
|
className="text-center text-base-content/20 mt-4"
|
||||||
|
>
|
||||||
|
This drawer remains silent
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
data-testid={`drawer-section-${id}`}
|
data-testid={`drawer-section-${id}`}
|
||||||
className={`w-full p-[24px_28px] cursor-pointer flex items-center gap-5 transition-all duration-2000 ease-in-out outline-none focus-visible:ring-2 focus-visible:ring-primary/50 border border-base-content/10 text-left bg-linear-to-r from-transparent to-base-100/40`}
|
className="w-full relative p-[24px_28px] cursor-pointer flex items-center gap-5 transition-all duration-2000 ease-in-out outline-none focus-visible:ring-2 overflow-hidden focus-visible:ring-primary/50 border border-base-content/10 text-left bg-linear-to-r from-transparent to-base-100/40"
|
||||||
>
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div
|
<div
|
||||||
@@ -49,8 +65,14 @@ export function DrawerSection({
|
|||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-sans text-[0.6rem] text-base-content/20 mt-1">
|
<div className="font-sans text-xs text-base-content/20 mt-1">
|
||||||
{count}
|
<span className="font-mono text-xs md:text-base -mt-1 absolute text-primary/30">
|
||||||
|
{count}
|
||||||
|
</span>{" "}
|
||||||
|
<span className="ml-3">{subtext}</span>
|
||||||
|
</div>
|
||||||
|
<div className="absolute right-5 -translate-y-15 text-base-content/4">
|
||||||
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function LetterItem({
|
|||||||
data-testid={`letter-item-${id}`}
|
data-testid={`letter-item-${id}`}
|
||||||
className={`${isLocked ? "pointer-events-none" : ""} p-4 border-base-content/3 flex items-start gap-4 hover:bg-base-300 transition-all delay-75 duration-100 group text-left cursor-pointer w-9/12 mx-auto hover:scale-120 hover:h-24 hover:-translate-y-3 hover:pb-4 hover:border-x-5 hover:border-t-5 border-t-2 hover:-mb-2`}
|
className={`${isLocked ? "pointer-events-none" : ""} p-4 border-base-content/3 flex items-start gap-4 hover:bg-base-300 transition-all delay-75 duration-100 group text-left cursor-pointer w-9/12 mx-auto hover:scale-120 hover:h-24 hover:-translate-y-3 hover:pb-4 hover:border-x-5 hover:border-t-5 border-t-2 hover:-mb-2`}
|
||||||
>
|
>
|
||||||
<div className="text-[0.85rem] italic text-base-content/40 flex-1 truncate group-hover:text-base-content/60 transition-none animate-[opacity_200ms_linear_forwards]">
|
<div className="text-sm italic text-base-content/40 flex-1 truncate group-hover:text-base-content/60">
|
||||||
{preview}
|
{preview}
|
||||||
</div>
|
</div>
|
||||||
{unlock_at ? (
|
{unlock_at ? (
|
||||||
@@ -53,7 +53,7 @@ export function LetterItem({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="font-sans text-[0.6rem] text-base-content/20 transition-none">
|
<div className="font-sans text-xs text-base-content/20">
|
||||||
{timestamp}
|
{timestamp}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ export function PasskeyModal() {
|
|||||||
className="text-primary mx-auto mb-8 animate-pulse"
|
className="text-primary mx-auto mb-8 animate-pulse"
|
||||||
weight="duotone"
|
weight="duotone"
|
||||||
/>
|
/>
|
||||||
<h3 data-testid="passkey-modal-title" className="font-bold text-lg font-display text-primary">
|
<h3
|
||||||
|
data-testid="passkey-modal-title"
|
||||||
|
className="font-bold text-lg font-display text-primary"
|
||||||
|
>
|
||||||
You've been away a while.
|
You've been away a while.
|
||||||
</h3>
|
</h3>
|
||||||
<p className="py-4 font-sans">
|
<p className="py-4 font-sans">
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export function ComposeCanvas({
|
|||||||
// re-calculates height based on content and applies the zoom transform
|
// re-calculates height based on content and applies the zoom transform
|
||||||
const syncViewport = useCallback(() => {
|
const syncViewport = useCallback(() => {
|
||||||
if (!(fabricRef.current && wrapperRef.current)) return;
|
if (!(fabricRef.current && wrapperRef.current)) return;
|
||||||
|
textboxRef.current.initDimensions();
|
||||||
|
|
||||||
const minHeight = initialData?.canvasHeight ?? DEFAULT_LOGICAL_HEIGHT;
|
const minHeight = initialData?.canvasHeight ?? DEFAULT_LOGICAL_HEIGHT;
|
||||||
logicalSizeRef.current.height = measureLogicalContentHeight(
|
logicalSizeRef.current.height = measureLogicalContentHeight(
|
||||||
|
|||||||
@@ -123,7 +123,12 @@ export function EnvelopeReveal({
|
|||||||
<span className={"text-neutral-content/60 font-xs font-display"}>
|
<span className={"text-neutral-content/60 font-xs font-display"}>
|
||||||
to
|
to
|
||||||
</span>
|
</span>
|
||||||
<h1 data-testid="envelope-recipient" className="text-3xl font-bold text-base-content">{recipient}</h1>
|
<h1
|
||||||
|
data-testid="envelope-recipient"
|
||||||
|
className="text-3xl font-bold text-base-content"
|
||||||
|
>
|
||||||
|
{recipient}
|
||||||
|
</h1>
|
||||||
<p className="text-base-content/60 font-display mt-8">{date}</p>
|
<p className="text-base-content/60 font-display mt-8">{date}</p>
|
||||||
<img
|
<img
|
||||||
src={stamp}
|
src={stamp}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@
|
|||||||
--color-neutral: oklch(38% 0.02 45);
|
--color-neutral: oklch(38% 0.02 45);
|
||||||
--color-neutral-content: oklch(80% 0.015 60);
|
--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-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-success-content: oklch(16% 0.03 150);
|
||||||
--color-warning: oklch(68% 0.08 72);
|
--color-warning: oklch(68% 0.08 72);
|
||||||
--color-warning-content: oklch(18% 0.03 60);
|
--color-warning-content: oklch(18% 0.03 60);
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.glass-card {
|
.glass-card {
|
||||||
@apply bg-glass-bg backdrop-blur-xl border border-white/5 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 {
|
.ul-wavy {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe("Drawer Page", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the cabinet sections and empty state message", () => {
|
it("renders the drawer sections and empty state message", () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Drawer />
|
<Drawer />
|
||||||
@@ -49,9 +49,13 @@ describe("Drawer Page", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByTestId("drawer-section-drafts")).toBeInTheDocument();
|
expect(screen.getByTestId("drawer-section-drafts")).toBeInTheDocument();
|
||||||
expect(screen.getAllByTestId("drawer-section-title").length).toBeGreaterThanOrEqual(1);
|
expect(
|
||||||
|
screen.getAllByTestId("drawer-section-title").length,
|
||||||
|
).toBeGreaterThanOrEqual(1);
|
||||||
expect(screen.getByTestId("drawer-section-vault")).toBeInTheDocument();
|
expect(screen.getByTestId("drawer-section-vault")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("empty-drawer-message-drafts")).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByTestId("empty-drawer-message-drafts"),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the loading state", () => {
|
it("renders the loading state", () => {
|
||||||
|
|||||||
+184
-177
@@ -1,4 +1,10 @@
|
|||||||
import { FeatherIcon } from "@phosphor-icons/react";
|
import {
|
||||||
|
ArchiveIcon,
|
||||||
|
FeatherIcon,
|
||||||
|
FileDashedIcon,
|
||||||
|
PaperPlaneTiltIcon,
|
||||||
|
VaultIcon,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useLocation, 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";
|
||||||
@@ -11,190 +17,191 @@ import { PATHS } from "../config/routes";
|
|||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
import { useLetters } from "../hooks/useLetters";
|
import { useLetters } from "../hooks/useLetters";
|
||||||
import {
|
import {
|
||||||
formatRelativeDate,
|
formatRelativeDate,
|
||||||
formatRelativeDateWithoutTime,
|
formatRelativeDateWithoutTime,
|
||||||
} from "../utils/dateFormat.ts";
|
} from "../utils/dateFormat.ts";
|
||||||
|
|
||||||
export default function Drawer() {
|
export default function Drawer() {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
const [openSection, setOpenSection] = useState<string | null>(null);
|
const [openSection, setOpenSection] = useState<string | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [showWelcomeLetter, setShowWelcomeLetter] = useState(
|
const [showWelcomeLetter, setShowWelcomeLetter] = useState(
|
||||||
!!location.state?.firstTime,
|
!!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;
|
||||||
|
|
||||||
const toggleSection = (id: string) =>
|
const toggleSection = (id: string) =>
|
||||||
setOpenSection(openSection === id ? null : id);
|
setOpenSection(openSection === id ? null : id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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 && (
|
{showWelcomeLetter && (
|
||||||
<WelcomeLetterOverlay
|
<WelcomeLetterOverlay
|
||||||
userName={user.full_name}
|
userName={user.full_name}
|
||||||
onComplete={() => {
|
onComplete={() => {
|
||||||
setShowWelcomeLetter(false);
|
setShowWelcomeLetter(false);
|
||||||
navigate(location.pathname, { replace: true, state: {} });
|
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 />
|
||||||
<div className="font-sans text-xs tracking-widester uppercase text-base-content/40 mt-2">
|
<div className="font-sans text-xs tracking-widester uppercase text-base-content/40 mt-2">
|
||||||
Personal Archive
|
Personal Archive
|
||||||
</div>
|
|
||||||
<div className="mt-6 font-sans text-sm text-base-content flex items-center justify-center gap-2 opacity-60 hover:opacity-100 transition-opacity">
|
|
||||||
Welcome Back{" "}
|
|
||||||
<span className="font-semibold text-primary">{user.full_name}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={logout}
|
|
||||||
className="ml-3 cursor-pointer underline underline-offset-4 text-xs hover:text-primary transition-colors"
|
|
||||||
>
|
|
||||||
Sign Out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="join join-vertical w-full max-w-120 bg-base-200 border border-base-content/10 shadow-2xl z-10 rounded-sm duration-500 delay-200 min-h-64 flex flex-col">
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex-1 flex flex-col items-center justify-center p-12 gap-4">
|
|
||||||
<span className="loading loading-ring loading-lg text-primary opacity-20"></span>
|
|
||||||
<span data-testid="drawer-loading-state" className="text-xxs uppercase tracking-widester font-sans text-base-content/20 animate-pulse">
|
|
||||||
Opening your cabinet...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<DrawerSection
|
|
||||||
id="drafts"
|
|
||||||
title="Drafts"
|
|
||||||
count={`${drafts.length} unfinished whispers`}
|
|
||||||
isOpen={openSection === "drafts"}
|
|
||||||
onClick={() => toggleSection("drafts")}
|
|
||||||
>
|
|
||||||
{drafts.map((draft) => (
|
|
||||||
<LetterItem
|
|
||||||
id={draft.public_id}
|
|
||||||
status={draft.status}
|
|
||||||
key={draft.public_id}
|
|
||||||
preview={draft.metadata?.recipient || "Untitled Draft"}
|
|
||||||
timestamp={formatRelativeDate(draft.updated_at)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{drafts.length === 0 && (
|
|
||||||
<p data-testid="empty-drawer-message-drafts" className="text-center text-base-content/20 mt-4">
|
|
||||||
This drawer remains silent
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</DrawerSection>
|
|
||||||
|
|
||||||
<DrawerSection
|
|
||||||
id="kept"
|
|
||||||
title="Kept"
|
|
||||||
count={`${kept.length} private letters`}
|
|
||||||
isOpen={openSection === "kept"}
|
|
||||||
onClick={() => toggleSection("kept")}
|
|
||||||
>
|
|
||||||
{kept.map((letter) => (
|
|
||||||
<LetterItem
|
|
||||||
id={letter.public_id}
|
|
||||||
status={letter.status}
|
|
||||||
key={letter.public_id}
|
|
||||||
preview={letter.metadata?.recipient || "Someone dear..."}
|
|
||||||
timestamp={formatRelativeDate(letter.updated_at)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</DrawerSection>
|
|
||||||
<DrawerSection
|
|
||||||
id="sent"
|
|
||||||
title="Sent"
|
|
||||||
count={`${sent.length} shared truths`}
|
|
||||||
isOpen={openSection === "sent"}
|
|
||||||
onClick={() => toggleSection("sent")}
|
|
||||||
>
|
|
||||||
{sent.map((letter) => (
|
|
||||||
<LetterItem
|
|
||||||
key={letter.public_id}
|
|
||||||
status={letter.status}
|
|
||||||
id={letter.public_id}
|
|
||||||
preview={letter.metadata?.recipient || "Someone dear..."}
|
|
||||||
timestamp={formatRelativeDate(letter.updated_at)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{sent.length === 0 && (
|
|
||||||
<p className="empty-drawer-message-sent text-center text-base-content/20 mt-4">
|
|
||||||
This drawer remains silent
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</DrawerSection>
|
|
||||||
<DrawerSection
|
|
||||||
id="vault"
|
|
||||||
title="Vault"
|
|
||||||
count={`${vault.length} things locked;not lost;in time`}
|
|
||||||
isOpen={openSection === "vault"}
|
|
||||||
onClick={() => toggleSection("vault")}
|
|
||||||
>
|
|
||||||
{vault.map((letter) => (
|
|
||||||
<LetterItem
|
|
||||||
key={letter.public_id}
|
|
||||||
status={letter.status}
|
|
||||||
id={letter.public_id}
|
|
||||||
preview={letter.metadata?.recipient || "Future Self"}
|
|
||||||
timestamp={formatRelativeDate(letter.updated_at)}
|
|
||||||
unlock_at={formatRelativeDateWithoutTime(
|
|
||||||
letter.unlock_at || "",
|
|
||||||
)}
|
|
||||||
isLocked={letter.unlock_at > new Date().toISOString()}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</DrawerSection>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="write-letter-btn"
|
|
||||||
data-testid="write-letter-btn"
|
|
||||||
className="group mt-15 z-10 bg-transparent border border-dashed border-base-content/10 px-8 py-4 text-base-content/40 italic cursor-pointer transition-all hover:border-primary/40 hover:text-base-content/60 hover:bg-primary/5 hover:-translate-y-0.5 flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-primary/50 duration-500"
|
|
||||||
onClick={() => navigate(PATHS.write(""))}
|
|
||||||
>
|
|
||||||
<FeatherIcon
|
|
||||||
size={18}
|
|
||||||
weight="duotone"
|
|
||||||
className="text-primary/30 transition-all duration-300 group-hover:text-primary"
|
|
||||||
/>
|
|
||||||
Write something{" "}
|
|
||||||
<span className="relative inline-flex">
|
|
||||||
<span className="transition-opacity duration-500 opacity-80 group-hover:opacity-0">
|
|
||||||
. . . . . .
|
|
||||||
</span>
|
|
||||||
<span className="absolute inset-0 text-primary transition-opacity duration-300 opacity-0 group-hover:opacity-100">
|
|
||||||
unsaid
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<footer className="mt-25 font-sans text-[0.6rem] tracking-widester uppercase text-base-content/10 z-10">
|
|
||||||
For your unsaid.
|
|
||||||
</footer>
|
|
||||||
{!showWelcomeLetter && (
|
|
||||||
<div className="absolute bottom-0 z-50 font-sans">
|
|
||||||
<Saajan
|
|
||||||
message={`Good to see you again, ${user.full_name}.\nWhat's on your mind today?`}
|
|
||||||
position="top"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="mt-6 font-sans text-sm text-base-content flex items-center justify-center gap-2 opacity-60 hover:opacity-100 transition-opacity">
|
||||||
|
Welcome Back{" "}
|
||||||
|
<span className="font-semibold text-primary">{user.full_name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={logout}
|
||||||
|
className="ml-3 cursor-pointer underline underline-offset-4 text-xs hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="join join-vertical w-full max-w-120 bg-base-200 border border-base-content/10 shadow-2xl z-10 rounded-sm duration-500 delay-200 min-h-64 flex flex-col">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center p-12 gap-4">
|
||||||
|
<span className="loading loading-ring loading-lg text-primary opacity-20"></span>
|
||||||
|
<span
|
||||||
|
data-testid="drawer-loading-state"
|
||||||
|
className="text-xxs uppercase tracking-widester font-sans text-base-content/20 animate-pulse"
|
||||||
|
>
|
||||||
|
Opening your cabinet...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<DrawerSection
|
||||||
|
id="drafts"
|
||||||
|
title="Drafts"
|
||||||
|
count={drafts.length}
|
||||||
|
subtext="unfinished whispers"
|
||||||
|
isOpen={openSection === "drafts"}
|
||||||
|
onClick={() => toggleSection("drafts")}
|
||||||
|
icon={<FileDashedIcon weight="thin" size={128} />}
|
||||||
|
>
|
||||||
|
{drafts.map((draft) => (
|
||||||
|
<LetterItem
|
||||||
|
id={draft.public_id}
|
||||||
|
status={draft.status}
|
||||||
|
key={draft.public_id}
|
||||||
|
preview={draft.metadata?.recipient || "Untitled Draft"}
|
||||||
|
timestamp={formatRelativeDate(draft.updated_at)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DrawerSection>
|
||||||
|
|
||||||
|
<DrawerSection
|
||||||
|
id="kept"
|
||||||
|
title="Kept"
|
||||||
|
count={kept.length}
|
||||||
|
subtext="private letters"
|
||||||
|
isOpen={openSection === "kept"}
|
||||||
|
onClick={() => toggleSection("kept")}
|
||||||
|
icon={<ArchiveIcon weight="thin" size={128} />}
|
||||||
|
>
|
||||||
|
{kept.map((letter) => (
|
||||||
|
<LetterItem
|
||||||
|
id={letter.public_id}
|
||||||
|
status={letter.status}
|
||||||
|
key={letter.public_id}
|
||||||
|
preview={letter.metadata?.recipient || "Someone dear..."}
|
||||||
|
timestamp={formatRelativeDate(letter.updated_at)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DrawerSection>
|
||||||
|
<DrawerSection
|
||||||
|
id="sent"
|
||||||
|
title="Sent"
|
||||||
|
count={sent.length}
|
||||||
|
subtext="shared truths"
|
||||||
|
isOpen={openSection === "sent"}
|
||||||
|
onClick={() => toggleSection("sent")}
|
||||||
|
icon={<PaperPlaneTiltIcon weight="thin" size={128} />}
|
||||||
|
>
|
||||||
|
{sent.map((letter) => (
|
||||||
|
<LetterItem
|
||||||
|
key={letter.public_id}
|
||||||
|
status={letter.status}
|
||||||
|
id={letter.public_id}
|
||||||
|
preview={letter.metadata?.recipient || "Someone dear..."}
|
||||||
|
timestamp={formatRelativeDate(letter.updated_at)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DrawerSection>
|
||||||
|
<DrawerSection
|
||||||
|
id="vault"
|
||||||
|
title="Vault"
|
||||||
|
count={vault.length}
|
||||||
|
subtext="things locked—not lost—in time"
|
||||||
|
isOpen={openSection === "vault"}
|
||||||
|
onClick={() => toggleSection("vault")}
|
||||||
|
icon={<VaultIcon weight="thin" size={128} />}
|
||||||
|
>
|
||||||
|
{vault.map((letter) => (
|
||||||
|
<LetterItem
|
||||||
|
key={letter.public_id}
|
||||||
|
status={letter.status}
|
||||||
|
id={letter.public_id}
|
||||||
|
preview={letter.metadata?.recipient || "Future Self"}
|
||||||
|
timestamp={formatRelativeDate(letter.updated_at)}
|
||||||
|
unlock_at={formatRelativeDateWithoutTime(
|
||||||
|
letter.unlock_at || "",
|
||||||
|
)}
|
||||||
|
isLocked={letter.unlock_at > new Date().toISOString()}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DrawerSection>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="write-letter-btn"
|
||||||
|
data-testid="write-letter-btn"
|
||||||
|
className="group mt-15 z-10 bg-transparent border border-dashed border-base-content/10 px-8 py-4 text-base-content/40 italic cursor-pointer transition-all hover:border-primary/40 hover:text-base-content/60 hover:bg-primary/5 hover:-translate-y-0.5 flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-primary/50 duration-500"
|
||||||
|
onClick={() => navigate(PATHS.write(""))}
|
||||||
|
>
|
||||||
|
<FeatherIcon
|
||||||
|
size={18}
|
||||||
|
weight="duotone"
|
||||||
|
className="text-primary/30 transition-all duration-300 group-hover:text-primary"
|
||||||
|
/>
|
||||||
|
Write something{" "}
|
||||||
|
<span className="relative inline-flex">
|
||||||
|
<span className="transition-opacity duration-500 opacity-80 group-hover:opacity-0">
|
||||||
|
. . . . . .
|
||||||
|
</span>
|
||||||
|
<span className="absolute inset-0 text-primary transition-opacity duration-300 opacity-0 group-hover:opacity-100">
|
||||||
|
unsaid
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<footer className="mt-25 font-sans text-[0.6rem] tracking-widester uppercase text-base-content/10 z-10">
|
||||||
|
For your unsaid.
|
||||||
|
</footer>
|
||||||
|
{!showWelcomeLetter && (
|
||||||
|
<div className="absolute bottom-0 z-50 font-sans">
|
||||||
|
<Saajan
|
||||||
|
message={`Good to see you again, ${user.full_name}.\nWhat's on your mind today?`}
|
||||||
|
position="top"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { HttpResponse, http } from "msw";
|
||||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
@@ -79,7 +84,9 @@ describe("Editor Page", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Wait for initial load to complete
|
// 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");
|
const canvas = screen.getByTestId("canvas");
|
||||||
expect(canvas.getAttribute("data-readonly")).toBe("false");
|
expect(canvas.getAttribute("data-readonly")).toBe("false");
|
||||||
@@ -136,7 +143,9 @@ describe("Editor Page", () => {
|
|||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => screen.queryByTestId("opening-draft-overlay"));
|
await waitForElementToBeRemoved(() =>
|
||||||
|
screen.queryByTestId("opening-draft-overlay"),
|
||||||
|
);
|
||||||
|
|
||||||
const canvas = screen.getByTestId("canvas");
|
const canvas = screen.getByTestId("canvas");
|
||||||
|
|
||||||
|
|||||||
+17
-14
@@ -14,6 +14,9 @@ import Saajan from "../components/ui/Saajan.tsx";
|
|||||||
import { ROUTES } from "../config/routes.ts";
|
import { ROUTES } from "../config/routes.ts";
|
||||||
import { formatDate } from "../utils/dateFormat.ts";
|
import { formatDate } from "../utils/dateFormat.ts";
|
||||||
|
|
||||||
|
import "@fontsource/space-mono/index.css";
|
||||||
|
import "@fontsource/architects-daughter/index.css";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const sectionContainer1 = useRef<HTMLDivElement>(null);
|
const sectionContainer1 = useRef<HTMLDivElement>(null);
|
||||||
const { scrollYProgress } = useScroll({
|
const { scrollYProgress } = useScroll({
|
||||||
@@ -53,7 +56,7 @@ export default function Home() {
|
|||||||
<ReactLenis root options={{ lerp: 0.1, duration: 1.5, smoothWheel: true }}>
|
<ReactLenis root options={{ lerp: 0.1, duration: 1.5, smoothWheel: true }}>
|
||||||
<section
|
<section
|
||||||
ref={sectionContainer1}
|
ref={sectionContainer1}
|
||||||
className="relative w-full h-[850vh] bg-base-100 font-serif"
|
className="relative w-full h-[850vh] bg-base-100 font-serif text-neutral-content/90"
|
||||||
>
|
>
|
||||||
<div className="sticky top-0 h-screen w-full flex flex-col items-center justify-center overflow-hidden">
|
<div className="sticky top-0 h-screen w-full flex flex-col items-center justify-center overflow-hidden">
|
||||||
{/* Intro */}
|
{/* Intro */}
|
||||||
@@ -64,12 +67,12 @@ export default function Home() {
|
|||||||
scale: useTransform(scrollYProgress, [0, 0.12], [1, 10]),
|
scale: useTransform(scrollYProgress, [0, 0.12], [1, 10]),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h1 className="text-neutral-content/40 text-4xl md:text-6xl text-center px-6">
|
<h1 className="text-neutral text-4xl md:text-6xl text-center px-6">
|
||||||
You've been carrying something
|
You've been carrying something
|
||||||
</h1>
|
</h1>
|
||||||
<h2 className="text-primary text-5xl md:text-7xl font-extralight mt-4 italic font-display animate-pulse">
|
<motion.h2 className="text-primary text-5xl md:text-7xl mt-4 italic font-display font-light">
|
||||||
unsaid
|
unsaid
|
||||||
</h2>
|
</motion.h2>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -97,9 +100,9 @@ export default function Home() {
|
|||||||
}}
|
}}
|
||||||
transition={{ delay: 4 }}
|
transition={{ delay: 4 }}
|
||||||
>
|
>
|
||||||
<Logo scale={2} />
|
<Logo type="logo" scale={1.5} ul={true} />
|
||||||
<motion.div
|
<motion.div
|
||||||
className="mt-6 text-4xl md:text-6xl text-base-content/60 "
|
className="font-serif italic font-extralight mt-6 text-4xl md:text-6xl text-neutral "
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(
|
opacity: useTransform(
|
||||||
scrollYProgress,
|
scrollYProgress,
|
||||||
@@ -113,14 +116,14 @@ export default function Home() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
is a{" "}
|
is a{" "}
|
||||||
<span className="font-display text-primary font-extralight">
|
<span className="font-display text-primary font-extralight">
|
||||||
safe space
|
safe space
|
||||||
</span>
|
</span>
|
||||||
,<br />
|
,<br />
|
||||||
<motion.span
|
<motion.span
|
||||||
className="opacity-0 text-3xl md:text-5xl"
|
className="opacity-0 text-2xl md:text-4xl font-hand tracking-widest italic text-neutral"
|
||||||
transition={{ delay: 3 }}
|
transition={{ delay: 5 }}
|
||||||
whileInView={{ opacity: 1 }}
|
whileInView={{ opacity: 1 }}
|
||||||
viewport={{ once: false, amount: 0.3 }}
|
viewport={{ once: false, amount: 0.3 }}
|
||||||
>
|
>
|
||||||
@@ -168,11 +171,11 @@ export default function Home() {
|
|||||||
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
|
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
|
||||||
>
|
>
|
||||||
seal it{" "}
|
seal it{" "}
|
||||||
<span className="text-secondary font-display italic font-extralight">
|
<span className="text-success font-mono tracking-tighter font-extrabold">
|
||||||
secure
|
secure
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
and{" "}
|
and{" "}
|
||||||
<span className="text-secondary font-display font-extralight italic">
|
<span className="text-info font-mono tracking-tighter italic">
|
||||||
private
|
private
|
||||||
</span>
|
</span>
|
||||||
.
|
.
|
||||||
@@ -252,7 +255,7 @@ export default function Home() {
|
|||||||
{/* Outro */}
|
{/* Outro */}
|
||||||
<motion.h2
|
<motion.h2
|
||||||
className={
|
className={
|
||||||
"italic absolute text-4xl md:text-6xl text-center px-10 leading-tight"
|
"italic absolute text-4xl md:text-6xl text-center px-10 leading-tight text-neutral-content/50"
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(scrollYProgress, [0.9, 1], [0, 1]),
|
opacity: useTransform(scrollYProgress, [0.9, 1], [0, 1]),
|
||||||
@@ -264,7 +267,7 @@ export default function Home() {
|
|||||||
{/* CTA */}
|
{/* CTA */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className={
|
className={
|
||||||
"z-100 absolute -bottom-12 md:bottom-0 font-display flex flex-wrap md:flex-nowrap gap-4 md:gap-12 justify-center"
|
"z-100 absolute -bottom-12 md:bottom-0 font-hand flex flex-wrap md:flex-nowrap gap-4 md:gap-12 justify-center"
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(scrollYProgress, [0.98, 1], [0, 1]),
|
opacity: useTransform(scrollYProgress, [0.98, 1], [0, 1]),
|
||||||
@@ -288,7 +291,7 @@ export default function Home() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={
|
className={
|
||||||
"md:opacity-50 hover:opacity-100 btn rounded-full btn-primary btn-wide md:btn-xl md:grayscale hover:grayscale-0 hover:-translate-y-1 transition-all duration-1000"
|
"md:opacity-50 hover:opacity-100 btn rounded-full btn-primary btn-wide md:btn-xl md:grayscale-50 hover:grayscale-0 focus:grayscale-0 active:grayscale-0 hover:-translate-y-1 transition-all duration-1000"
|
||||||
}
|
}
|
||||||
type={"button"}
|
type={"button"}
|
||||||
onClick={() => navigate(ROUTES.ONBOARD, { replace: true })}
|
onClick={() => navigate(ROUTES.ONBOARD, { replace: true })}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ describe("Login Page", () => {
|
|||||||
await userEvent.type(screen.getByLabelText(/password/i), "password123");
|
await userEvent.type(screen.getByLabelText(/password/i), "password123");
|
||||||
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
|
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([
|
it.each([
|
||||||
@@ -73,8 +75,14 @@ describe("Login Page", () => {
|
|||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/drawer" element={<div data-testid="drawer-page">Drawer</div>} />
|
<Route
|
||||||
<Route path="/read/:publicId" element={<div data-testid="reader-page">Reader</div>} />
|
path="/drawer"
|
||||||
|
element={<div data-testid="drawer-page">Drawer</div>}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/read/:publicId"
|
||||||
|
element={<div data-testid="reader-page">Reader</div>}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
@@ -83,7 +91,8 @@ describe("Login Page", () => {
|
|||||||
await userEvent.type(screen.getByLabelText(/password/i), "password123");
|
await userEvent.type(screen.getByLabelText(/password/i), "password123");
|
||||||
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
|
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();
|
expect(await screen.findByTestId(expectedTestId)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ export default function Login() {
|
|||||||
{showWelcome && <WelcomeModal setShowWelcome={setShowWelcome} />}
|
{showWelcome && <WelcomeModal setShowWelcome={setShowWelcome} />}
|
||||||
<div className="glass-card w-full max-w-sm p-2 transition-all duration-500 hover:shadow-2xl fade-zoom">
|
<div className="glass-card w-full max-w-sm p-2 transition-all duration-500 hover:shadow-2xl fade-zoom">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="card-body gap-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="card-body gap-4">
|
||||||
<h1 className="card-title font-display text-2xl justify-center text-primary/80 tracking-tight">
|
<h1 className="flex items-center font-display text-2xl justify-center text-primary/80 tracking-tight">
|
||||||
Enter <Logo /> Archive
|
Enter <Logo type="logo" scale={0.7} /> Archive
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{apiError && (
|
{apiError && (
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ describe("Reader Page", () => {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
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 () => {
|
it("should display an error message if the server request fails", async () => {
|
||||||
@@ -97,9 +99,9 @@ describe("Reader Page", () => {
|
|||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(await screen.findByTestId("log-modal-message")).toHaveTextContent(
|
||||||
await screen.findByTestId("log-modal-message"),
|
/Failed to load letter/i,
|
||||||
).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 () => {
|
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 () => {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default function Register() {
|
|||||||
<div className="glass-card w-full max-w-sm p-2 transition-all duration-500 hover:shadow-2xl fade-zoom">
|
<div className="glass-card w-full max-w-sm p-2 transition-all duration-500 hover:shadow-2xl fade-zoom">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="card-body gap-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="card-body gap-4">
|
||||||
<div className="card-title font-display text-2xl justify-center text-primary/80 tracking-tight whitespace-nowrap">
|
<div className="card-title font-display text-2xl justify-center text-primary/80 tracking-tight whitespace-nowrap">
|
||||||
Create a <Logo /> Account
|
Create a <Logo type="logo" scale={0.7} /> Account
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{apiError && (
|
{apiError && (
|
||||||
|
|||||||
Reference in New Issue
Block a user