Style/Revamp #4

Merged
me merged 12 commits from feature/home_revamp into main 2026-05-08 03:16:16 +00:00
5 changed files with 624 additions and 612 deletions
Showing only changes of commit b91d2a4541 - Show all commits
+80 -77
View File
@@ -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, AutoRedirectRoute } 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";
@@ -16,85 +16,88 @@ const VerifyEmail = lazy(() => import("./pages/VerifyEmail"));
const About = lazy(() => import("./pages/About")); const About = lazy(() => import("./pages/About"));
export default function App() { export default function App() {
const { initialize, isInitializing } = useAuth(); const { initialize, isInitializing } = useAuth();
const authInitialized = useRef<boolean>(false); const authInitialized = useRef<boolean>(false);
useEffect(() => { useEffect(() => {
if (authInitialized.current) return; if (authInitialized.current) return;
authInitialized.current = true; authInitialized.current = true;
initialize().then(); initialize().then();
}, [initialize]); }, [initialize]);
if (isInitializing) { if (isInitializing) {
return <SplashScreen />; return <SplashScreen />;
} }
return ( return (
<BrowserRouter> <BrowserRouter>
<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={ <Route
<AutoRedirectRoute> path={ROUTES.HOME}
<Home /> element={
</AutoRedirectRoute> <AutoRedirectRoute>
} /> <Home />
</AutoRedirectRoute>
}
/>
<Route <Route
path={ROUTES.ONBOARD} path={ROUTES.ONBOARD}
element={ element={
<AutoRedirectRoute> <AutoRedirectRoute>
<Register /> <Register />
</AutoRedirectRoute> </AutoRedirectRoute>
} }
/> />
<Route <Route
path={ROUTES.LOGIN} path={ROUTES.LOGIN}
element={ element={
<AutoRedirectRoute> <AutoRedirectRoute>
<Login /> <Login />
</AutoRedirectRoute> </AutoRedirectRoute>
} }
/> />
<Route <Route
path={ROUTES.VERIFY_EMAIL} path={ROUTES.VERIFY_EMAIL}
element={ element={
<AutoRedirectRoute> <AutoRedirectRoute>
<VerifyEmail /> <VerifyEmail />
</AutoRedirectRoute> </AutoRedirectRoute>
} }
/> />
<Route <Route
path={ROUTES.ACTIVATE} path={ROUTES.ACTIVATE}
element={ element={
<AutoRedirectRoute> <AutoRedirectRoute>
<Activate /> <Activate />
</AutoRedirectRoute> </AutoRedirectRoute>
} }
/> />
<Route <Route
path={ROUTES.DRAWER} path={ROUTES.DRAWER}
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Drawer /> <Drawer />
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route <Route
path={ROUTES.WRITE} path={ROUTES.WRITE}
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Editor /> <Editor />
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route path={ROUTES.READ} element={<Reader />} /> <Route path={ROUTES.READ} element={<Reader />} />
<Route path={ROUTES.ABOUT} element={<About />} /> <Route path={ROUTES.ABOUT} element={<About />} />
<Route path="*" element={<Navigate to={ROUTES.HOME} replace />} /> <Route path="*" element={<Navigate to={ROUTES.HOME} replace />} />
</Routes> </Routes>
</Suspense> </Suspense>
</main> </main>
</BrowserRouter> </BrowserRouter>
); );
} }
+55 -46
View File
@@ -2,54 +2,63 @@ import { DotIcon } from "@phosphor-icons/react";
import "@fontsource/knewave/400.css"; import "@fontsource/knewave/400.css";
interface LogoProps { interface LogoProps {
scale?: number; scale?: number;
type?: "inline" | "mono" | "logo" | null; type?: "inline" | "mono" | "logo" | null;
ul?: boolean; ul?: boolean;
} }
export default function Logo({ scale = 1, type = null, ul = false }: LogoProps) { export default function Logo({
if (type === "inline") { scale = 1,
return ( type = null,
<span className={"text-accent font-display italic "}> ul = false,
pi<span className="text-primary">.</span>&nbsp;ku }: LogoProps) {
<span className="text-primary">.</span>&nbsp; if (type === "inline") {
</span>
);
}
if (type === "mono") {
return (
<span className="font-display italic font-bold border-b-3 border-dashed border-stone-800/50">
pi. ku.
</span>
);
}
if (type === "logo") {
return (
<img src="/android-chrome-512x512.png" alt="Pi. Ku. logo" className="mx-auto" width={scale * 100} />
);
}
return ( return (
<div <span className={"text-accent font-display italic "}>
role="img" pi<span className="text-primary">.</span>&nbsp;ku
aria-label="Pi. Ku. logo" <span className="text-primary">.</span>&nbsp;
className={`inline-flex items-baseline justify-center leading-none select-none ${ul ? "ul-wavy" : ""}`} </span>
style={{ fontFamily: "'Knewave', serif", scale }}
>
<span className="text-3xl font-light text-accent">Pi</span>
<DotIcon
weight="fill"
size={12}
className="text-primary translate-y-1 -mx-px"
/>
<span className="text-3xl font-light text-accent">&nbsp;Ku</span>
<DotIcon
weight="fill"
size={12}
className="text-primary translate-y-1 -mx-px"
/>
</div>
); );
}
if (type === "mono") {
return (
<span className="font-display italic font-bold border-b-3 border-dashed border-stone-800/50">
pi. ku.
</span>
);
}
if (type === "logo") {
return (
<img
src="/android-chrome-512x512.png"
alt="Pi. Ku. logo"
className="mx-auto"
width={scale * 100}
/>
);
}
return (
<div
role="img"
aria-label="Pi. Ku. logo"
className={`inline-flex items-baseline justify-center leading-none select-none ${ul ? "ul-wavy" : ""}`}
style={{ fontFamily: "'Knewave', serif", scale }}
>
<span className="text-3xl font-light text-accent">Pi</span>
<DotIcon
weight="fill"
size={12}
className="text-primary translate-y-1 -mx-px"
/>
<span className="text-3xl font-light text-accent">&nbsp;Ku</span>
<DotIcon
weight="fill"
size={12}
className="text-primary translate-y-1 -mx-px"
/>
</div>
);
} }
+100 -100
View File
@@ -3,125 +3,125 @@ 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, AutoRedirectRoute } 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>Login Page</div>} /> <Route path="/login" element={<div>Login Page</div>} />
<Route path="/drawer" element={<div>Drawer Page</div>} /> <Route path="/drawer" element={<div>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>
</MemoryRouter>, </MemoryRouter>,
); );
} }
beforeEach(() => { beforeEach(() => {
useAuthStore.setState({ useAuthStore.setState({
accessToken: null, accessToken: null,
user: null, user: null,
isInitializing: true, isInitializing: true,
}); });
}); });
describe("ProtectedRoute", () => { describe("ProtectedRoute", () => {
it("should show SplashScreen while auth is initializing", () => { it("should show SplashScreen while auth is initializing", () => {
useAuthStore.setState({ useAuthStore.setState({
isInitializing: true, isInitializing: true,
accessToken: null, accessToken: null,
user: null, user: null,
});
renderGuard(
<ProtectedRoute>
<div>Secret</div>
</ProtectedRoute>,
"/protected",
);
expect(screen.getByText(/Unsealing/i)).toBeInTheDocument();
expect(screen.queryByText("Secret")).not.toBeInTheDocument();
}); });
renderGuard(
<ProtectedRoute>
<div>Secret</div>
</ProtectedRoute>,
"/protected",
);
it("should redirect unauthenticated users to /login", () => { expect(screen.getByText(/Unsealing/i)).toBeInTheDocument();
useAuthStore.setState({ expect(screen.queryByText("Secret")).not.toBeInTheDocument();
isInitializing: false, });
accessToken: null,
user: null, it("should redirect unauthenticated users to /login", () => {
}); useAuthStore.setState({
renderGuard( isInitializing: false,
<ProtectedRoute> accessToken: null,
<div>Secret</div> user: null,
</ProtectedRoute>,
"/protected",
);
expect(screen.getByText("Login Page")).toBeInTheDocument();
expect(screen.queryByText("Secret")).not.toBeInTheDocument();
}); });
renderGuard(
<ProtectedRoute>
<div>Secret</div>
</ProtectedRoute>,
"/protected",
);
expect(screen.getByText("Login Page")).toBeInTheDocument();
expect(screen.queryByText("Secret")).not.toBeInTheDocument();
});
it("should render page for authenticated users", () => { it("should render page for authenticated users", () => {
useAuthStore.setState({ useAuthStore.setState({
isInitializing: false, isInitializing: false,
accessToken: "token", accessToken: "token",
user: mockUser, user: mockUser,
});
renderGuard(
<ProtectedRoute>
<div>Secret</div>
</ProtectedRoute>,
"/protected",
);
expect(screen.getByText("Secret")).toBeInTheDocument();
}); });
renderGuard(
<ProtectedRoute>
<div>Secret</div>
</ProtectedRoute>,
"/protected",
);
expect(screen.getByText("Secret")).toBeInTheDocument();
});
}); });
describe("PublicRoute", () => { describe("PublicRoute", () => {
it("should show SplashScreen while auth is initializing", () => { it("should show SplashScreen while auth is initializing", () => {
useAuthStore.setState({ useAuthStore.setState({
isInitializing: true, isInitializing: true,
accessToken: null, accessToken: null,
user: null, user: null,
});
renderGuard(
<AutoRedirectRoute>
<div>Login Page</div>
</AutoRedirectRoute>,
"/public",
);
expect(screen.getByText(/Unsealing/i)).toBeInTheDocument();
expect(screen.queryByText("Login Page")).not.toBeInTheDocument();
}); });
renderGuard(
<AutoRedirectRoute>
<div>Login Page</div>
</AutoRedirectRoute>,
"/public",
);
expect(screen.getByText(/Unsealing/i)).toBeInTheDocument();
expect(screen.queryByText("Login Page")).not.toBeInTheDocument();
});
it("should redirect authenticated users to /drawer", () => { it("should redirect authenticated users to /drawer", () => {
useAuthStore.setState({ useAuthStore.setState({
isInitializing: false, isInitializing: false,
accessToken: "token", accessToken: "token",
user: mockUser, user: mockUser,
});
renderGuard(
<AutoRedirectRoute>
<div>Login Form</div>
</AutoRedirectRoute>,
"/public",
);
expect(screen.getByText("Drawer Page")).toBeInTheDocument();
expect(screen.queryByText("Login Form")).not.toBeInTheDocument();
}); });
renderGuard(
<AutoRedirectRoute>
<div>Login Form</div>
</AutoRedirectRoute>,
"/public",
);
expect(screen.getByText("Drawer Page")).toBeInTheDocument();
expect(screen.queryByText("Login Form")).not.toBeInTheDocument();
});
it("should render page for unauthenticated users", () => { it("should render page for unauthenticated users", () => {
useAuthStore.setState({ useAuthStore.setState({
isInitializing: false, isInitializing: false,
accessToken: null, accessToken: null,
user: null, user: null,
});
renderGuard(
<AutoRedirectRoute>
<div>Login Form</div>
</AutoRedirectRoute>,
"/public",
);
expect(screen.getByText("Login Form")).toBeInTheDocument();
}); });
renderGuard(
<AutoRedirectRoute>
<div>Login Form</div>
</AutoRedirectRoute>,
"/public",
);
expect(screen.getByText("Login Form")).toBeInTheDocument();
});
}); });
+13 -13
View File
@@ -9,16 +9,16 @@ import SplashScreen from "./SplashScreen";
* state so the Login component can link them back after sign-in * state so the Login component can link them back after sign-in
*/ */
export function ProtectedRoute({ children }: { children: React.ReactNode }) { export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isInitializing } = useAuth(); const { isAuthenticated, isInitializing } = useAuth();
const location = useLocation(); const location = useLocation();
if (isInitializing) return <SplashScreen />; if (isInitializing) return <SplashScreen />;
if (!isAuthenticated) { if (!isAuthenticated) {
return <Navigate to={ROUTES.LOGIN} state={{ from: location }} replace />; return <Navigate to={ROUTES.LOGIN} state={{ from: location }} replace />;
} }
return <>{children}</>; return <>{children}</>;
} }
/** /**
@@ -26,13 +26,13 @@ export function ProtectedRoute({ children }: { children: React.ReactNode }) {
* If authenticated, redirect all the auth related flows to the drawer * If authenticated, redirect all the auth related flows to the drawer
*/ */
export function AutoRedirectRoute({ 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 />;
if (isAuthenticated) { if (isAuthenticated) {
return <Navigate to={ROUTES.DRAWER} replace />; return <Navigate to={ROUTES.DRAWER} replace />;
} }
return <>{children}</>; return <>{children}</>;
} }
+376 -376
View File
@@ -1,10 +1,10 @@
import { InfoIcon } from "@phosphor-icons/react"; import { InfoIcon } from "@phosphor-icons/react";
import { ReactLenis } from "lenis/react"; import { ReactLenis } from "lenis/react";
import { import {
motion, motion,
useMotionValueEvent, useMotionValueEvent,
useScroll, useScroll,
useTransform, useTransform,
} from "motion/react"; } from "motion/react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -18,383 +18,383 @@ import "@fontsource/space-mono/index.css";
import "@fontsource/architects-daughter/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({
target: sectionContainer1, target: sectionContainer1,
}); });
const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true); const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true);
const [flapOpen, setFlapOpen] = useState(false); const [flapOpen, setFlapOpen] = useState(false);
const [recipient, setRecipient] = useState("someone dear"); const [recipient, setRecipient] = useState("someone dear");
const [ignite, setIgnite] = useState(false); const [ignite, setIgnite] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
useMotionValueEvent(scrollYProgress, "change", (latestScrollValue) => { useMotionValueEvent(scrollYProgress, "change", (latestScrollValue) => {
if (latestScrollValue > 0.54) { if (latestScrollValue > 0.54) {
setFlapOpen(false); setFlapOpen(false);
} else { } else {
setFlapOpen(true); setFlapOpen(true);
} }
if (latestScrollValue <= 0.6) { if (latestScrollValue <= 0.6) {
setIsEnvelopeFlipped(true); setIsEnvelopeFlipped(true);
} else { } else {
setIsEnvelopeFlipped(false); setIsEnvelopeFlipped(false);
} }
if (latestScrollValue > 0.68) { if (latestScrollValue > 0.68) {
setRecipient("future me"); setRecipient("future me");
} else { } else {
setRecipient("someone dear"); setRecipient("someone dear");
} }
if (latestScrollValue > 0.77) { if (latestScrollValue > 0.77) {
setIgnite(true); setIgnite(true);
} else { } else {
setIgnite(false); setIgnite(false);
} }
}); });
return ( return (
<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"
>
<div className="sticky top-0 h-screen w-full flex flex-col items-center justify-center overflow-hidden">
{/* Intro */}
<motion.div
className="absolute flex flex-col items-center justify-center pointer-events-none"
style={{
opacity: useTransform(scrollYProgress, [0, 0.12, 1], [1, 0, 0]),
scale: useTransform(scrollYProgress, [0, 0.12], [1, 10]),
}}
>
<h1 className="text-neutral text-4xl md:text-6xl text-center px-6">
You've been carrying something
</h1>
<motion.h2 className="text-primary text-5xl md:text-7xl mt-4 italic font-display font-light">
unsaid
</motion.h2>
</motion.div>
<motion.div
className="absolute text-center"
style={{
opacity: useTransform(scrollYProgress, [0, 0.15, 0.2], [0, 1, 0]),
y: useTransform(scrollYProgress, [0, 0.15, 0.2], [40, 0, -40]),
scale: useTransform(scrollYProgress, [0, 0.15, 0.2], [0.8, 1, 3]),
}}
>
<div className="mt-6 text-4xl md:text-6xl text-base-content/60 italic">
and that's okay...
</div>
</motion.div>
{/* pi. ku. */}
<motion.div
className="absolute text-center px-6"
style={{
opacity: useTransform(
scrollYProgress,
[0.18, 0.25, 0.3],
[0, 1, 0],
),
y: useTransform(scrollYProgress, [0.18, 0.25, 0.3], [20, 0, -20]),
}}
transition={{ delay: 4 }}
>
<Logo type="logo" scale={1.5} ul={true} />
<motion.div
className="font-serif font-extralight mt-6 text-4xl md:text-6xl text-neutral "
style={{
opacity: useTransform(
scrollYProgress,
[0.22, 0.25, 0.35, 0.4],
[0, 1, 1, 0],
),
y: useTransform(
scrollYProgress,
[0.25, 0.3, 0.35, 0.4],
[20, 0, 0, -20],
),
}}
> >
<div className="sticky top-0 h-screen w-full flex flex-col items-center justify-center overflow-hidden"> is a{" "}
{/* Intro */} <span className="font-display text-primary font-extralight">
<motion.div safe space
className="absolute flex flex-col items-center justify-center pointer-events-none" </span>
style={{ ,<br />
opacity: useTransform(scrollYProgress, [0, 0.12, 1], [1, 0, 0]), <motion.span
scale: useTransform(scrollYProgress, [0, 0.12], [1, 10]), className="opacity-0 text-2xl md:text-4xl font-hand tracking-[0.4em] text-neutral"
}} transition={{ delay: 5 }}
> whileInView={{ opacity: 1 }}
<h1 className="text-neutral text-4xl md:text-6xl text-center px-6"> viewport={{ once: false, amount: 0.3 }}
You've been carrying something >
</h1> where you can
<motion.h2 className="text-primary text-5xl md:text-7xl mt-4 italic font-display font-light"> </motion.span>
unsaid </motion.div>
</motion.h2> </motion.div>
</motion.div>
<motion.div <div className="relative w-full max-w-5xl h-1/2 flex items-center justify-center mt-20">
className="absolute text-center" <motion.h2
style={{ style={{
opacity: useTransform(scrollYProgress, [0, 0.15, 0.2], [0, 1, 0]), opacity: useTransform(
y: useTransform(scrollYProgress, [0, 0.15, 0.2], [40, 0, -40]), scrollYProgress,
scale: useTransform(scrollYProgress, [0, 0.15, 0.2], [0.8, 1, 3]), [0.3, 0.35, 0.4, 0.45],
}} [0, 1, 1, 0],
> ),
<div className="mt-6 text-4xl md:text-6xl text-base-content/60 italic"> y: useTransform(
and that's okay... scrollYProgress,
</div> [0.3, 0.35, 0.4, 0.45],
</motion.div> [40, 0, 0, -40],
{/* pi. ku. */} ),
<motion.div }}
className="absolute text-center px-6" className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
style={{ >
opacity: useTransform( pen down your unsaid words into{" "}
scrollYProgress, <span className="font-display text-primary font-extralight">
[0.18, 0.25, 0.3], letters
[0, 1, 0], </span>
), .
y: useTransform(scrollYProgress, [0.18, 0.25, 0.3], [20, 0, -20]), </motion.h2>
}} {/* Seal */}
transition={{ delay: 4 }} <motion.h2
> style={{
<Logo type="logo" scale={1.5} ul={true} /> opacity: useTransform(
<motion.div scrollYProgress,
className="font-serif font-extralight mt-6 text-4xl md:text-6xl text-neutral " [0.45, 0.5, 0.55, 0.6],
style={{ [0, 1, 1, 0],
opacity: useTransform( ),
scrollYProgress, y: useTransform(
[0.22, 0.25, 0.35, 0.4], scrollYProgress,
[0, 1, 1, 0], [0.45, 0.5, 0.55, 0.6],
), [40, 0, 0, -40],
y: useTransform( ),
scrollYProgress, }}
[0.25, 0.3, 0.35, 0.4], className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
[20, 0, 0, -20], >
), seal it{" "}
}} <span className="text-success font-mono tracking-tighter font-extrabold">
> secure
is a{" "} </span>{" "}
<span className="font-display text-primary font-extralight"> and{" "}
safe space <span className="text-info font-mono tracking-tighter">
</span> private
,<br /> </span>
<motion.span .
className="opacity-0 text-2xl md:text-4xl font-hand tracking-[0.4em] text-neutral" </motion.h2>
transition={{ delay: 5 }} {/* Send / vault */}
whileInView={{ opacity: 1 }} <motion.h2
viewport={{ once: false, amount: 0.3 }} style={{
> opacity: useTransform(
where you can scrollYProgress,
</motion.span> [0.6, 0.63, 0.72, 0.75],
</motion.div> [0, 1, 1, 0],
</motion.div> ),
y: useTransform(
scrollYProgress,
[0.6, 0.63, 0.72, 0.75],
[40, 0, 0, -40],
),
}}
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
>
send it to{" "}
<motion.span
className="font-display text-accent"
style={{
color: useTransform(
scrollYProgress,
[0.67, 1],
["var(--color-accent)", "var(--color-neutral)"],
),
}}
>
someone dear
</motion.span>
<motion.span
style={{
opacity: useTransform(scrollYProgress, [0.66, 0.7], [0, 1]),
}}
>
<motion.span
className="font-display text-accent"
style={{
color: useTransform(
scrollYProgress,
[0.67, 1],
["var(--color-accent)", "var(--color-neutral)"],
),
}}
>
{" "}
or{" "}
</motion.span>
<span className="font-display text-success">
yourself in the future
</span>
.
</motion.span>
</motion.h2>
{/* Burn */}
<motion.h2
style={{
opacity: useTransform(
scrollYProgress,
[0.75, 0.8, 0.85, 0.9],
[0, 1, 1, 0],
),
y: useTransform(
scrollYProgress,
[0.75, 0.8, 0.85, 0.9],
[40, 0, 0, -40],
),
}}
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
>
and even <span className="font-display text-error">burn it</span>{" "}
to release the burden.
</motion.h2>
{/* Outro */}
<motion.h2
className={
"italic absolute text-4xl md:text-6xl text-center px-10 leading-tight text-neutral-content/50"
}
style={{
opacity: useTransform(scrollYProgress, [0.9, 1], [0, 1]),
y: useTransform(scrollYProgress, [0.9, 1], [80, 0]),
}}
>
You've been carrying it long enough.
</motion.h2>
{/* CTA */}
<motion.div
className={
"z-100 absolute -bottom-12 md:bottom-0 font-hand flex flex-wrap md:flex-nowrap gap-4 md:gap-12 justify-center"
}
style={{
opacity: useTransform(scrollYProgress, [0.98, 1], [0, 1]),
y: useTransform(scrollYProgress, [0.98, 1], [80, 0]),
display: useTransform(
scrollYProgress,
[0.96, 1],
["none", "flex"],
),
}}
>
<button
className={
"md:opacity-50 hover:opacity-100 btn btn-ghost btn-wide md:btn-xl rounded-full font-extralight md:grayscale hover:grayscale-0 hover:-translate-y-1 transition-all duration-1000"
}
type={"button"}
onClick={() => navigate(ROUTES.ABOUT, { replace: true })}
>
<InfoIcon className={"text-primary"} />
Tell me More
</button>
<button
className={
"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"}
onClick={() => navigate(ROUTES.ONBOARD, { replace: true })}
>
I'm ready
</button>
</motion.div>
</div>
<div className="relative w-full max-w-5xl h-1/2 flex items-center justify-center mt-20"> <div className="relative h-1/4 w-full flex flex-col items-center justify-center pointer-events-none">
<motion.h2 <motion.div
style={{ className={"z-21 absolute"}
opacity: useTransform( style={{
scrollYProgress, opacity: useTransform(
[0.3, 0.35, 0.4, 0.45], scrollYProgress,
[0, 1, 1, 0], [0.3, 0.4, 0.5, 0.52],
), [0, 1, 0.1, 0],
y: useTransform( ),
scrollYProgress, y: useTransform(
[0.3, 0.35, 0.4, 0.45], scrollYProgress,
[40, 0, 0, -40], [0.3, 0.45, 0.5],
), [300, 0, 200],
}} ),
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight" scale: useTransform(
> scrollYProgress,
pen down your unsaid words into{" "} [0.3, 0.4, 0.5],
<span className="font-display text-primary font-extralight"> [1, 1, 0.6],
letters ),
</span> }}
. >
</motion.h2> <div className="mockup-phone w-[75vw] border-primary">
{/* Seal */} <div className="mockup-phone-camera"></div>
<motion.h2 <div className="mockup-phone-display">
style={{ <img alt="letter" src="/screenshots/letter.webp" />
opacity: useTransform(
scrollYProgress,
[0.45, 0.5, 0.55, 0.6],
[0, 1, 1, 0],
),
y: useTransform(
scrollYProgress,
[0.45, 0.5, 0.55, 0.6],
[40, 0, 0, -40],
),
}}
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
>
seal it{" "}
<span className="text-success font-mono tracking-tighter font-extrabold">
secure
</span>{" "}
and{" "}
<span className="text-info font-mono tracking-tighter">
private
</span>
.
</motion.h2>
{/* Send / vault */}
<motion.h2
style={{
opacity: useTransform(
scrollYProgress,
[0.6, 0.63, 0.72, 0.75],
[0, 1, 1, 0],
),
y: useTransform(
scrollYProgress,
[0.6, 0.63, 0.72, 0.75],
[40, 0, 0, -40],
),
}}
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
>
send it to{" "}
<motion.span
className="font-display text-accent"
style={{
color: useTransform(
scrollYProgress,
[0.67, 1],
["var(--color-accent)", "var(--color-neutral)"],
),
}}
>
someone dear
</motion.span>
<motion.span
style={{
opacity: useTransform(scrollYProgress, [0.66, 0.7], [0, 1]),
}}
>
<motion.span
className="font-display text-accent"
style={{
color: useTransform(
scrollYProgress,
[0.67, 1],
["var(--color-accent)", "var(--color-neutral)"],
),
}}
>
{" "}
or{" "}
</motion.span>
<span className="font-display text-success">
yourself in the future
</span>
.
</motion.span>
</motion.h2>
{/* Burn */}
<motion.h2
style={{
opacity: useTransform(
scrollYProgress,
[0.75, 0.8, 0.85, 0.9],
[0, 1, 1, 0],
),
y: useTransform(
scrollYProgress,
[0.75, 0.8, 0.85, 0.9],
[40, 0, 0, -40],
),
}}
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
>
and even <span className="font-display text-error">burn it</span>{" "}
to release the burden.
</motion.h2>
{/* Outro */}
<motion.h2
className={
"italic absolute text-4xl md:text-6xl text-center px-10 leading-tight text-neutral-content/50"
}
style={{
opacity: useTransform(scrollYProgress, [0.9, 1], [0, 1]),
y: useTransform(scrollYProgress, [0.9, 1], [80, 0]),
}}
>
You've been carrying it long enough.
</motion.h2>
{/* CTA */}
<motion.div
className={
"z-100 absolute -bottom-12 md:bottom-0 font-hand flex flex-wrap md:flex-nowrap gap-4 md:gap-12 justify-center"
}
style={{
opacity: useTransform(scrollYProgress, [0.98, 1], [0, 1]),
y: useTransform(scrollYProgress, [0.98, 1], [80, 0]),
display: useTransform(
scrollYProgress,
[0.96, 1],
["none", "flex"],
),
}}
>
<button
className={
"md:opacity-50 hover:opacity-100 btn btn-ghost btn-wide md:btn-xl rounded-full font-extralight md:grayscale hover:grayscale-0 hover:-translate-y-1 transition-all duration-1000"
}
type={"button"}
onClick={() => navigate(ROUTES.ABOUT, { replace: true })}
>
<InfoIcon className={"text-primary"} />
Tell me More
</button>
<button
className={
"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"}
onClick={() => navigate(ROUTES.ONBOARD, { replace: true })}
>
I'm ready
</button>
</motion.div>
</div>
<div className="relative h-1/4 w-full flex flex-col items-center justify-center pointer-events-none">
<motion.div
className={"z-21 absolute"}
style={{
opacity: useTransform(
scrollYProgress,
[0.3, 0.4, 0.5, 0.52],
[0, 1, 0.1, 0],
),
y: useTransform(
scrollYProgress,
[0.3, 0.45, 0.5],
[300, 0, 200],
),
scale: useTransform(
scrollYProgress,
[0.3, 0.4, 0.5],
[1, 1, 0.6],
),
}}
>
<div className="mockup-phone w-[75vw] border-primary">
<div className="mockup-phone-camera"></div>
<div className="mockup-phone-display">
<img alt="letter" src="/screenshots/letter.webp" />
</div>
</div>
</motion.div>
{/* Envelope */}
<motion.div
className="absolute scale-50 md:scale-80 z-10"
style={{
opacity: useTransform(
scrollYProgress,
[0.4, 0.45, 0.5, 0.7, 0.9, 1],
[0, 0.6, 1, 1, 0.3, 0],
),
y: useTransform(scrollYProgress, [0.45, 0.5, 1], [600, 200, 0]),
}}
>
<EnvelopeReveal
isInteractive={false}
ignite={ignite}
recipient={recipient}
date={formatDate(new Date().toISOString())}
onRevealComplete={() => { }}
isFlip={isEnvelopeFlipped}
openFlap={flapOpen}
/>
</motion.div>
{/* Saajan */}
<motion.div
className="fixed bottom-0 z-10 font-sans -mb-6 scale-85 md:scale-100 md:mb-0"
style={{
opacity: useTransform(
scrollYProgress,
[0.98, 0.995, 1],
[0, 0.5, 1],
),
y: useTransform(scrollYProgress, [0.98, 1], [50, -10]),
}}
>
<Saajan
message={
"I think we forget things\nif there is nobody to tell them."
}
position={"top"}
/>
</motion.div>
{/* Orb */}
<motion.div
className="w-48 z-100 h-48 rounded-full blur-3xl opacity-20"
transition={{
backgroundColor: { ease: "easeIn", duration: 2 },
}}
style={{
backgroundColor: useTransform(
scrollYProgress,
[0.45, 0.5, 0.7, 0.75, 1],
[
"var(--color-primary)",
"var(--color-secondary)",
"var(--color-accent)",
"var(--color-success)",
"var(--color-error)",
],
),
scale: useTransform(scrollYProgress, [0, 1], [0.6, 2.5]),
}}
/>
<div className="absolute border border-primary/5 w-64 h-64 rounded-full backdrop-blur-[1px]" />
</div>
</div> </div>
</section> </div>
</ReactLenis> </motion.div>
); {/* Envelope */}
<motion.div
className="absolute scale-50 md:scale-80 z-10"
style={{
opacity: useTransform(
scrollYProgress,
[0.4, 0.45, 0.5, 0.7, 0.9, 1],
[0, 0.6, 1, 1, 0.3, 0],
),
y: useTransform(scrollYProgress, [0.45, 0.5, 1], [600, 200, 0]),
}}
>
<EnvelopeReveal
isInteractive={false}
ignite={ignite}
recipient={recipient}
date={formatDate(new Date().toISOString())}
onRevealComplete={() => {}}
isFlip={isEnvelopeFlipped}
openFlap={flapOpen}
/>
</motion.div>
{/* Saajan */}
<motion.div
className="fixed bottom-0 z-10 font-sans -mb-6 scale-85 md:scale-100 md:mb-0"
style={{
opacity: useTransform(
scrollYProgress,
[0.98, 0.995, 1],
[0, 0.5, 1],
),
y: useTransform(scrollYProgress, [0.98, 1], [50, -10]),
}}
>
<Saajan
message={
"I think we forget things\nif there is nobody to tell them."
}
position={"top"}
/>
</motion.div>
{/* Orb */}
<motion.div
className="w-48 z-100 h-48 rounded-full blur-3xl opacity-20"
transition={{
backgroundColor: { ease: "easeIn", duration: 2 },
}}
style={{
backgroundColor: useTransform(
scrollYProgress,
[0.45, 0.5, 0.7, 0.75, 1],
[
"var(--color-primary)",
"var(--color-secondary)",
"var(--color-accent)",
"var(--color-success)",
"var(--color-error)",
],
),
scale: useTransform(scrollYProgress, [0, 1], [0.6, 2.5]),
}}
/>
<div className="absolute border border-primary/5 w-64 h-64 rounded-full backdrop-blur-[1px]" />
</div>
</div>
</section>
</ReactLenis>
);
} }