Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 33 additions & 30 deletions apps/website/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Analytics } from "@vercel/analytics/react";
import { AuthProvider } from "./hooks/useAuth";
import { ToastProvider } from "./hooks/useToast";
import { SplashProvider } from "./contexts/SplashProvider";
import Layout from "./components/Layout";
import AuthGuard from "./components/AuthGuard";
import LandingPage from "./pages/LandingPage";
Expand All @@ -15,39 +16,41 @@ import DocsPage from "./pages/DocsPage";
export default function App() {
return (
<BrowserRouter>
<ToastProvider>
<AuthProvider>
<Routes>
<Route element={<Layout />}>
{/* Public landing page */}
<Route path="/" element={<LandingPage />} />
<SplashProvider>
<ToastProvider>
<AuthProvider>
<Routes>
<Route element={<Layout />}>
{/* Public landing page */}
<Route path="/" element={<LandingPage />} />

{/* Documentation */}
<Route path="/docs" element={<DocsPage />} />
<Route path="/docs/*" element={<DocsPage />} />
{/* Documentation */}
<Route path="/docs" element={<DocsPage />} />
<Route path="/docs/*" element={<DocsPage />} />

{/* Auth */}
<Route path="/auth/login" element={<LoginPage />} />
<Route path="/auth/signup" element={<SignupPage />} />
<Route
path="/auth/reset-password"
element={<ResetPasswordPage />}
/>
<Route path="/auth/logout" element={<LogoutPage />} />
{/* Auth */}
<Route path="/auth/login" element={<LoginPage />} />
<Route path="/auth/signup" element={<SignupPage />} />
<Route
path="/auth/reset-password"
element={<ResetPasswordPage />}
/>
<Route path="/auth/logout" element={<LogoutPage />} />

{/* Early Access page - requires auth */}
<Route
path="/app"
element={
<AuthGuard>
<EarlyAccessPage />
</AuthGuard>
}
/>
</Route>
</Routes>
</AuthProvider>
</ToastProvider>
{/* Early Access page - requires auth */}
<Route
path="/app"
element={
<AuthGuard>
<EarlyAccessPage />
</AuthGuard>
}
/>
</Route>
</Routes>
</AuthProvider>
</ToastProvider>
</SplashProvider>
<Analytics />
</BrowserRouter>
);
Expand Down
38 changes: 27 additions & 11 deletions apps/website/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Link, Outlet, useLocation } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
import { useSplash } from "../hooks/useSplash";
import { ToastContainer } from "../hooks/useToast";
import { LogOut, Github } from "lucide-react";

export default function Layout() {
const { user, supabaseConfigured, loading } = useAuth();
const { isSplashActive } = useSplash();
const location = useLocation();

// Determine if we're on a landing page route
Expand All @@ -23,16 +25,30 @@ export default function Layout() {
return (
<div className="min-h-screen flex flex-col bg-white font-sans text-foreground antialiased">
{/* Navigation — frosted glass */}
<header className="sticky top-0 z-50 border-b border-border bg-white/80 backdrop-blur-lg">
<div className="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
<Link to="/" className="clerk-logo" id="clerk-logo">
<header
className={`sticky top-0 z-50 border-b border-border bg-white/80 backdrop-blur-lg transition-all duration-700 ${
isSplashActive ? "splash-active" : ""
}`}
>
<div className="max-w-5xl mx-auto px-4 py-3 sm:px-6 sm:py-4 flex items-center justify-between">
<Link
to="/"
className={`clerk-logo transition-opacity duration-500 ${
isSplashActive ? "opacity-0" : "opacity-100"
}`}
id="clerk-logo"
>
<span className="clerk-dot" />
<span className="clerk-dot" />
<span className="clerk-dot" />
<span className="clerk-dot" />
<span className="clerk-dot" />
</Link>
<nav className="flex items-center gap-2">
<nav
className={`flex items-center justify-end gap-0.5 sm:gap-1.5 transition-all duration-500 ${
isSplashActive ? "blur-nav" : ""
}`}
>
{isLandingRoute ? (
// Landing page navigation
<>
Expand All @@ -48,24 +64,24 @@ export default function Layout() {
<Github className="w-4 h-4" />
</a>
{loading ? (
<div className="flex items-center justify-center ml-2 w-8 h-8">
<div className="flex items-center justify-center ml-1 w-8 h-8">
<span className="pulse-dot" />
</div>
) : user ? (
<Link to="/app" className="btn btn-primary btn-sm ml-2">
<Link to="/app" className="btn btn-primary btn-sm ml-1">
App
</Link>
) : (
<>
<Link
to="/auth/login"
className="btn btn-ghost btn-sm ml-2"
className="btn btn-ghost btn-sm ml-1 hidden sm:inline-flex"
>
Sign In
</Link>
<Link
to="/auth/signup"
className="btn btn-primary btn-sm ml-2"
className="btn btn-primary btn-sm ml-1"
>
Sign Up
</Link>
Expand Down Expand Up @@ -104,7 +120,7 @@ export default function Layout() {
</>
)} */}
{loading ? (
<div className="flex items-center justify-center ml-2 w-8 h-8">
<div className="flex items-center justify-center ml-1 w-8 h-8">
<span className="pulse-dot" />
</div>
) : supabaseConfigured ? (
Expand All @@ -114,7 +130,7 @@ export default function Layout() {
{/* Settings link disabled for launch */}
{/* <Link
to="/settings"
className="btn btn-ghost btn-sm ml-2 px-2"
className="btn btn-ghost btn-sm ml-1 px-2"
title="Account Settings"
>
<Settings className="w-4 h-4" />
Expand All @@ -131,7 +147,7 @@ export default function Layout() {
!isLandingRoute && (
<Link
to="/auth/login"
className="btn btn-ghost btn-sm ml-2"
className="btn btn-ghost btn-sm ml-1"
>
Sign In
</Link>
Expand Down
85 changes: 80 additions & 5 deletions apps/website/src/components/LifecycleAnimation.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useEffect, useRef } from "react";

interface LifecycleAnimationProps {
onFlyStart?: () => void;
onComplete?: () => void;
}

export default function LifecycleAnimation({ onComplete }: LifecycleAnimationProps) {
export default function LifecycleAnimation({ onFlyStart, onComplete }: LifecycleAnimationProps) {
const rootRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand Down Expand Up @@ -60,6 +61,7 @@ export default function LifecycleAnimation({ onComplete }: LifecycleAnimationPro
}

const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let cleanupTimer: number | null = null;

/* ---------- tuning ---------- */
const DUR = 18.2;
Expand Down Expand Up @@ -527,8 +529,80 @@ export default function LifecycleAnimation({ onComplete }: LifecycleAnimationPro
score.setAttribute("fill", "#130DDD");
phaseWord.textContent = "Assemble";
phaseLine.textContent = "Execute · Evaluate · Improve · Complete";
const doneTimer = window.setTimeout(() => onComplete?.(), 2500);
return () => window.clearTimeout(doneTimer);
cleanupTimer = window.setTimeout(() => flyToLogo(), 2500);
return;
}

/* ---------- fly the five dots to the nav logo ---------- */
function flyToLogo() {
onFlyStart?.();

const logo = document.getElementById("clerk-logo");
const logoDots = logo
? Array.from(logo.querySelectorAll(".clerk-dot"))
: [];
if (logoDots.length !== 5) {
onComplete?.();
return;
}

const svgRect = svg.getBoundingClientRect();
const unitX = 820 / svgRect.width;

// Reveal only the dots for the flight
scene.style.opacity = "1";
score.style.opacity = "0";
head.style.opacity = "0";
guide.style.opacity = "0";
sweep.style.opacity = "0";
gauge.style.opacity = "0";
delta.style.opacity = "0";
pulse1.style.opacity = "0";
track.style.opacity = "0";
packetG.style.opacity = "0";
ghost1.style.opacity = "0";
ghost2.style.opacity = "0";
svg.style.overflow = "visible";

const targets = logoDots.map((ld) => {
const r = ld.getBoundingClientRect();
const targetCx = (r.left + r.width / 2 - svgRect.left) * unitX;
const targetCy = (r.top + r.height / 2 - svgRect.top) * unitX;
const targetScale = (r.width / 2) / (26 / unitX);
return { x: targetCx, y: targetCy, scale: targetScale };
});

dots.forEach((dot, i) => {
const cp = circPos(i);
const target = targets[i];
dot.style.opacity = "1";
dot.animate(
[
{
transform: `translate(${cp.x.toFixed(1)}px, ${cp.y.toFixed(1)}px) scale(0.867)`,
},
{
transform: `translate(${target.x.toFixed(1)}px, ${target.y.toFixed(1)}px) scale(${target.scale.toFixed(3)})`,
},
],
{
duration: 900,
easing: "cubic-bezier(0.77, 0, 0.175, 1)",
fill: "forwards",
},
);
});

cleanupTimer = window.setTimeout(() => {
onComplete?.();
dots.forEach((dot) => {
dot.animate([{ opacity: 1 }, { opacity: 0 }], {
duration: 200,
easing: "ease-in",
fill: "forwards",
});
});
}, 900);
}

/* ---------- loop driver: play once on load ---------- */
Expand All @@ -549,7 +623,7 @@ export default function LifecycleAnimation({ onComplete }: LifecycleAnimationPro
if (acc >= DUR) {
done = true;
stop();
onComplete?.();
flyToLogo();
return;
}
raf = requestAnimationFrame(tick);
Expand All @@ -570,8 +644,9 @@ export default function LifecycleAnimation({ onComplete }: LifecycleAnimationPro

return () => {
stop();
if (cleanupTimer) window.clearTimeout(cleanupTimer);
};
}, [onComplete]);
}, [onComplete, onFlyStart]);

return (
<div ref={rootRef} className="stage">
Expand Down
11 changes: 11 additions & 0 deletions apps/website/src/contexts/SplashContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext } from "react";

export interface SplashContextType {
isSplashActive: boolean;
setSplashActive: (active: boolean) => void;
}

export const SplashContext = createContext<SplashContextType>({
isSplashActive: false,
setSplashActive: () => {},
});
11 changes: 11 additions & 0 deletions apps/website/src/contexts/SplashProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useState, type ReactNode } from "react";
import { SplashContext } from "./SplashContext";

export function SplashProvider({ children }: { children: ReactNode }) {
const [isSplashActive, setSplashActive] = useState(false);
return (
<SplashContext.Provider value={{ isSplashActive, setSplashActive }}>
{children}
</SplashContext.Provider>
);
}
7 changes: 7 additions & 0 deletions apps/website/src/hooks/useSplash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useContext } from "react";
import { SplashContext } from "../contexts/SplashContext";
import type { SplashContextType } from "../contexts/SplashContext";

export function useSplash(): SplashContextType {
return useContext(SplashContext);
}
23 changes: 23 additions & 0 deletions apps/website/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,18 @@ textarea.input {
background: rgba(19, 13, 221, 0.06);
}

@media (max-width: 639px) {
.nav-link {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}

header .btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
}
}

/* ── Kit filter toggle ── */
.kit-filter-toggle {
display: inline-flex;
Expand Down Expand Up @@ -935,4 +947,15 @@ textarea.input {

@media (prefers-reduced-motion: reduce) {
.landing #ring-guide { animation: none; }
}

/* ── Splash state for Layout during landing intro ── */
.splash-active {
border-bottom-color: transparent;
}

.blur-nav {
filter: blur(6px);
opacity: 0.25;
pointer-events: none;
}
Loading
Loading