From 8f048142a57a8d8554d9d7e2c7df09e9f68efad2 Mon Sep 17 00:00:00 2001 From: peace node Date: Fri, 4 Sep 2026 15:30:14 -0400 Subject: [PATCH 1/2] feat(auth): add frontend-only animated auth screens --- app/(auth)/layout.tsx | 11 +- app/(auth)/login/page.tsx | 27 +-- app/(auth)/signup/page.tsx | 12 +- components/console/LoginPage.tsx | 196 +------------------- components/console/auth/AuthMediaRing.tsx | 210 ++++++++++++++++++++++ components/console/auth/AuthPanel.tsx | 127 +++++++++++++ 6 files changed, 358 insertions(+), 225 deletions(-) create mode 100644 components/console/auth/AuthMediaRing.tsx create mode 100644 components/console/auth/AuthPanel.tsx diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx index 7834ef9..c19285c 100644 --- a/app/(auth)/layout.tsx +++ b/app/(auth)/layout.tsx @@ -1,8 +1,7 @@ import type { Metadata } from "next"; -import { AuthProvider } from "@/components/console/AuthContext"; export const metadata: Metadata = { - title: "Sign in — Livepeer Early Access", + title: "Livepeer Early Access", description: "Sign in or create an account to access Livepeer Early Access.", }; @@ -12,10 +11,8 @@ export default function ConsoleAuthLayout({ children: React.ReactNode; }) { return ( - -
- {children} -
-
+
+ {children} +
); } diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 6302b46..5f78248 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,27 +1,8 @@ -import { redirect } from "next/navigation"; - -import { auth0 } from "@/lib/auth0"; +import type { Metadata } from "next"; import LoginPage from "@/components/console/LoginPage"; -const MCP_CALLBACK_PATH = "/api/mcp/oauth/callback"; - -export default async function LoginRoute({ - searchParams -}: { - searchParams: Promise<{ - mcp_oauth?: string; - }>; -}) { - const params = await searchParams; - const mcpOauth = params.mcp_oauth === "1"; - - const session = await auth0.getSession(); - if (session) { - if (mcpOauth) { - redirect(MCP_CALLBACK_PATH); - } - redirect("/home"); - } +export const metadata: Metadata = { title: "Sign in — Livepeer Early Access" }; - return ; +export default function LoginRoute() { + return ; } diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx index eb72ea9..ee78f22 100644 --- a/app/(auth)/signup/page.tsx +++ b/app/(auth)/signup/page.tsx @@ -1,12 +1,8 @@ -import { redirect } from "next/navigation"; -import { auth0 } from "@/lib/auth0"; +import type { Metadata } from "next"; import LoginPage from "@/components/console/LoginPage"; -export default async function SignupRoute() { - const session = await auth0.getSession(); - if (session) { - redirect("/home"); - } +export const metadata: Metadata = { title: "Sign up — Livepeer Early Access" }; - return ; +export default function SignupRoute() { + return ; } diff --git a/components/console/LoginPage.tsx b/components/console/LoginPage.tsx index 10aaa50..60a000e 100644 --- a/components/console/LoginPage.tsx +++ b/components/console/LoginPage.tsx @@ -1,197 +1,19 @@ "use client"; -import Link from "next/link"; -import { motion, AnimatePresence } from "framer-motion"; -import { LivepeerWordmark } from "@/components/design-system/LivepeerLogo"; - -function GoogleIcon({ className }: { className?: string }) { - return ( - - ); -} +import { AuthPanel, type AuthMode } from "@/components/console/auth/AuthPanel"; +import { AuthMediaRing } from "@/components/console/auth/AuthMediaRing"; interface LoginPageProps { - /** Which mode the page renders in. Driven by route — `/login` - * passes `"signin"`, `/signup` passes `"signup"`. The footer - * toggle navigates between the two routes so the URL always matches the - * visible mode. */ - initialMode?: "signin" | "signup"; - /** Auth0 returnTo. MCP login uses the complete route, not /home. */ - returnTo?: string; + mode?: AuthMode; } -export default function LoginPage({ - initialMode = "signin", - returnTo = "/home", -}: LoginPageProps = {}) { - const mode = initialMode; - const encodedReturnTo = encodeURIComponent(returnTo); - const loginHref = - mode === "signup" - ? `/auth/login?screen_hint=signup&returnTo=${encodedReturnTo}` - : `/auth/login?returnTo=${encodedReturnTo}`; - const googleHref = - mode === "signup" - ? `/auth/login?screen_hint=signup&connection=google-oauth2&returnTo=${encodedReturnTo}` - : `/auth/login?connection=google-oauth2&returnTo=${encodedReturnTo}`; - - const oauthButtonClass = - "inline-flex h-10 w-full items-center justify-center gap-2.5 rounded-full border border-hairline bg-transparent px-4 text-[13px] font-medium text-fg-strong transition-colors hover:border-subtle hover:bg-hover hover:text-fg focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-strong"; - +export default function LoginPage({ mode = "signin" }: LoginPageProps = {}) { return ( -
-
- -
- -
- - - - {mode === "signin" - ? "Log in to Livepeer Early Access" - : "Get started with Livepeer"} - - - - - -

- {mode === "signin" ? ( - <> - Don't have an account?{" "} - - Sign up - - - ) : ( - <> - Already have an account?{" "} - - Log in - - - )} -

- - - {mode === "signup" && ( - - By creating an account, you agree to our{" "} - - Terms - {" "} - and{" "} - - Privacy Policy - - . - - )} - -
+
+ +
+
- - -
+ ); } diff --git a/components/console/auth/AuthMediaRing.tsx b/components/console/auth/AuthMediaRing.tsx new file mode 100644 index 0000000..fbf22ac --- /dev/null +++ b/components/console/auth/AuthMediaRing.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { motion } from "framer-motion"; + +type MediaRingItem = { + src: string; + alt: string; +}; + +const RING = { + cardWidth: 286, + cardAspectRatio: 4 / 3, + radiusX: 620, + radiusY: 360, + spinSpeed: 2.5, + cardTiltDegrees: 5, + pathTiltDegrees: -18, + introDurationMs: 1100, +} as const; + +export const DEFAULT_MEDIA_RING_ITEMS: MediaRingItem[] = [ + { + src: "/images/console/explore/stable-video-diffusion.webp", + alt: "Stable Video Diffusion preview", + }, + { + src: "/images/console/explore/img2img-sdxl.webp", + alt: "Image editing preview", + }, + { + src: "/images/console/explore/live-video-to-video.webp", + alt: "Live video preview", + }, + { + src: "/images/console/explore/flux-schnell.webp", + alt: "Flux Schnell preview", + }, + { src: "/images/console/daydream.png", alt: "Daydream preview" }, + { src: "/images/console/explore/sdxl-turbo.webp", alt: "SDXL Turbo preview" }, + { + src: "/images/console/explore/real-esrgan-4x.webp", + alt: "Image upscale preview", + }, +]; + +interface AuthMediaRingProps { + items?: MediaRingItem[]; +} + +export function AuthMediaRing({ + items = DEFAULT_MEDIA_RING_ITEMS, +}: AuthMediaRingProps) { + const rootRef = useRef(null); + const cardRefs = useRef>([]); + const loadedImagesRef = useRef(new Set()); + const rotationRef = useRef(-16); + const [isReady, setIsReady] = useState(false); + + function markImageReady(index: number) { + loadedImagesRef.current.add(index); + if (loadedImagesRef.current.size >= items.length) setIsReady(true); + } + + useEffect(() => { + const fallback = window.setTimeout(() => setIsReady(true), 900); + return () => window.clearTimeout(fallback); + }, []); + + useEffect(() => { + if (!isReady) return; + + const prefersReducedMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)" + ).matches; + const cardHeight = RING.cardWidth * RING.cardAspectRatio; + const pathTilt = (RING.pathTiltDegrees * Math.PI) / 180; + let animationFrame = 0; + let introStartTime = 0; + let lastTime = 0; + + const render = (rotation: number, introProgress: number) => { + const root = rootRef.current; + if (!root || items.length === 0) return; + + const viewportScale = Math.min( + 1.16, + Math.max(0.7, root.clientWidth / 1120) + ); + const width = RING.cardWidth * viewportScale; + const height = cardHeight * viewportScale; + + cardRefs.current.forEach((card, index) => { + if (!card) return; + const radians = + (index / items.length) * Math.PI * 2 + (rotation * Math.PI) / 180; + const depth = (Math.sin(radians) + 1) / 2; + const rawX = + Math.cos(radians) * + RING.radiusX * + viewportScale * + (0.74 + depth * 0.26); + const rawY = + Math.sin(radians) * + RING.radiusY * + viewportScale * + (0.88 + depth * 0.12); + const targetX = rawX * Math.cos(pathTilt) - rawY * Math.sin(pathTilt); + const targetY = rawX * Math.sin(pathTilt) + rawY * Math.cos(pathTilt); + const targetScale = 0.7 + depth * 0.32; + const x = targetX * introProgress; + const y = targetY * introProgress; + const scale = 0.42 + (targetScale - 0.42) * introProgress; + const blur = (1 - depth) * 8 + (1 - introProgress) * 4; + const image = card.firstElementChild as HTMLElement | null; + + card.style.width = `${width}px`; + card.style.height = `${height}px`; + card.style.marginLeft = `${-width / 2}px`; + card.style.marginTop = `${-height / 2}px`; + card.style.opacity = `${Math.min(1, introProgress * 1.2)}`; + card.style.zIndex = `${Math.round(depth * 1000)}`; + card.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale}) rotate(${RING.cardTiltDegrees}deg)`; + card.style.boxShadow = `0 ${8 + depth * 14}px ${22 + depth * 28}px rgba(0,0,0,${0.04 + depth * 0.08})`; + + if (image) { + image.style.filter = `blur(${blur}px)`; + image.style.transform = `scale(${1 + blur * 0.012})`; + } + }); + }; + + const renderFinalFrame = () => render(rotationRef.current, 1); + + if (prefersReducedMotion) { + renderFinalFrame(); + window.addEventListener("resize", renderFinalFrame); + return () => window.removeEventListener("resize", renderFinalFrame); + } + + const tick = (now: number) => { + if (!introStartTime) introStartTime = now; + if (!lastTime) lastTime = now; + const deltaSeconds = (now - lastTime) / 1000; + const linearProgress = Math.min( + 1, + (now - introStartTime) / RING.introDurationMs + ); + const introProgress = 1 - Math.pow(1 - linearProgress, 3); + lastTime = now; + rotationRef.current += RING.spinSpeed * deltaSeconds; + render(rotationRef.current, introProgress); + animationFrame = window.requestAnimationFrame(tick); + }; + + window.addEventListener("resize", renderFinalFrame); + animationFrame = window.requestAnimationFrame(tick); + return () => { + window.cancelAnimationFrame(animationFrame); + window.removeEventListener("resize", renderFinalFrame); + }; + }, [isReady, items]); + + return ( +