diff --git a/screenshots/scanning-done.png b/screenshots/scanning-done.png new file mode 100644 index 0000000..cd346f1 Binary files /dev/null and b/screenshots/scanning-done.png differ diff --git a/screenshots/scanning-mid.png b/screenshots/scanning-mid.png new file mode 100644 index 0000000..b2de965 Binary files /dev/null and b/screenshots/scanning-mid.png differ diff --git a/src/app/globals.css b/src/app/globals.css index 403f16f..6e8c839 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -249,6 +249,55 @@ h1, h2, h3, h4, h5, h6 { .transition-card:hover { transform: translateY(-3px); } +/* Scanning avatar glow rings */ +.scan-avatar-ring { + position: relative; + border-radius: 9999px; +} +.scan-avatar-ring::before, +.scan-avatar-ring::after { + content: ''; + position: absolute; + inset: -4px; + border-radius: 9999px; + border: 1.5px solid rgba(0, 255, 135, 0.25); + animation: scanPulse 2s ease-in-out infinite; +} +.scan-avatar-ring::after { + inset: -10px; + border-color: rgba(0, 255, 135, 0.12); + animation-delay: 0.5s; +} + +/* Scanning sweep line */ +.scan-line { + position: relative; + overflow: hidden; +} +.scan-line::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 2px; + background: linear-gradient(90deg, transparent, #00FF87, transparent); + animation: scanSweep 3s ease-in-out infinite; +} + +@keyframes scanSweep { + 0% { left: -100%; } + 100% { left: 100%; } +} + +/* Scanning status card glass */ +.scan-card { + background: rgba(0, 0, 0, 0.5); + border: 1px solid rgba(255, 255, 255, 0.06); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); +} + html[data-ms-theme="light"] { filter: invert(1) hue-rotate(180deg); } html[data-ms-theme="light"] img, html[data-ms-theme="light"] video, diff --git a/src/app/onboarding/analyze/onboarding-client.tsx b/src/app/onboarding/analyze/onboarding-client.tsx new file mode 100644 index 0000000..c3ea352 --- /dev/null +++ b/src/app/onboarding/analyze/onboarding-client.tsx @@ -0,0 +1,209 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import Image from 'next/image'; +import { motion } from 'framer-motion'; +import { CheckCircle2, Loader2, Circle } from 'lucide-react'; + +type TaskStatus = 'pending' | 'active' | 'completed'; + +interface Task { + id: number; + label: string; + completedDuration: string | null; +} + +const TASKS: Task[] = [ + { id: 1, label: 'Reading public profile', completedDuration: '0.3s' }, + { id: 2, label: 'Fetching repo history', completedDuration: '1.1s' }, + { id: 3, label: 'Counting merged PRs', completedDuration: '0.8s' }, + { id: 4, label: 'Evaluating contribution quality', completedDuration: null }, + { id: 5, label: 'Calculating trust score', completedDuration: null }, + { id: 6, label: 'Assigning placement level', completedDuration: null }, +]; + +const INITIAL_STATUSES: TaskStatus[] = [ + 'completed', + 'completed', + 'completed', + 'active', + 'pending', + 'pending', +]; + +function generateProcessId(): string { + const hex = Array.from({ length: 6 }, () => Math.floor(Math.random() * 16).toString(16)).join(''); + return `Running process_id_${hex}...`; +} + +export function OnboardingClient({ + avatarUrl, + githubHandle, +}: { + avatarUrl: string | null; + githubHandle: string; +}) { + const router = useRouter(); + const [processId] = useState(generateProcessId); + const [taskStatuses, setTaskStatuses] = useState(INITIAL_STATUSES); + const [progress, setProgress] = useState(50); + + useEffect(() => { + const t1 = setTimeout(() => { + setTaskStatuses((prev) => { + const next = [...prev]; + next[3] = 'completed'; + next[4] = 'active'; + return next; + }); + setProgress(66); + }, 1500); + + const t2 = setTimeout(() => { + setTaskStatuses((prev) => { + const next = [...prev]; + next[4] = 'completed'; + next[5] = 'active'; + return next; + }); + setProgress(83); + }, 3000); + + const t3 = setTimeout(() => { + setTaskStatuses((prev) => { + const next = [...prev]; + next[5] = 'completed'; + return next; + }); + setProgress(100); + }, 4200); + + const t4 = setTimeout(() => { + router.push('/dashboard'); + }, 5000); + + return () => { + clearTimeout(t1); + clearTimeout(t2); + clearTimeout(t3); + clearTimeout(t4); + }; + }, [router]); + + return ( +
+
+ MERGESHIP +
+ +
+ + Step 2 of 3 + +
+ +
+
+ +
+
+ {avatarUrl ? ( + {githubHandle} + ) : ( + + {githubHandle.substring(0, 2).toUpperCase()} + + )} +
+
+ + +
+ + SCANNING + + {processId} +
+ +
+ {TASKS.map((task, index) => { + const status: TaskStatus = taskStatuses[index] ?? 'pending'; + return ( + +
+ + + {task.label} + +
+ + + +
+ ); + })} +
+
+ +

This usually takes under 10 seconds.

+
+ ); +} + +function TaskIcon({ status }: { status: TaskStatus }) { + if (status === 'completed') { + return ; + } + if (status === 'active') { + return ; + } + return ; +} + +function TaskDuration({ + status, + completedDuration, +}: { + status: TaskStatus; + completedDuration: string | null; +}) { + if (status === 'active') return ...; + if (status === 'pending') return --; + return {completedDuration ?? '0.0s'}; +} diff --git a/src/app/onboarding/analyze/page.tsx b/src/app/onboarding/analyze/page.tsx new file mode 100644 index 0000000..c614f9d --- /dev/null +++ b/src/app/onboarding/analyze/page.tsx @@ -0,0 +1,54 @@ +import { redirect } from 'next/navigation'; +import { getServerSupabase } from '@/lib/supabase/server'; +import { getServiceSupabase } from '@/lib/supabase/service'; +import { OnboardingClient } from './onboarding-client'; + +export const dynamic = 'force-dynamic'; + +export default async function AnalyzePage() { + const sb = await getServerSupabase(); + if (!sb) { + return ; + } + + const { + data: { user }, + } = await sb.auth.getUser(); + if (!user) redirect('/'); + + const identity = user.identities?.find((i) => i.provider === 'github'); + const avatarUrl = (identity?.identity_data?.['avatar_url'] as string) ?? null; + const githubHandle = (identity?.identity_data?.['user_name'] as string) ?? null; + + if (!githubHandle) redirect('/'); + + const service = getServiceSupabase(); + if (service) { + const { data: profile } = await service + .from('profiles') + .select('audit_completed') + .eq('id', user.id) + .maybeSingle(); + + if (profile?.audit_completed) { + redirect('/dashboard'); + } + } + + return ( +
+ +
+ ); +} + +function DemoRender() { + return ( +
+ +
+ ); +} diff --git a/src/middleware.ts b/src/middleware.ts index f741963..949fda2 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -17,6 +17,7 @@ import { readSupabaseEnv } from '@/lib/supabase/env'; const GATE_BYPASS_PREFIXES = [ '/install', + '/onboarding', '/api/auth', '/api/webhooks', '/api/inngest', diff --git a/tailwind.config.js b/tailwind.config.js index 795e25f..64bf94e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -19,6 +19,11 @@ module.exports = { amber: "#F59E0B", red: "#EF4444", }, + neon: { + green: "#00FF87", + "green-muted": "rgba(0,255,135,0.15)", + "green-glow": "rgba(0,255,135,0.4)", + }, dark: { 900: "#060611", 800: "#0D0D1A", @@ -51,6 +56,9 @@ module.exports = { "glow": "glow 2s ease-in-out infinite alternate", "slide-in": "slideIn 0.3s ease-out", "fade-in": "fadeIn 0.4s ease-out", + "scan-avatar": "scanAvatar 2s ease-in-out infinite alternate", + "scan-progress": "scanProgress 2s ease-in-out infinite", + "scan-pulse": "scanPulse 2s ease-in-out infinite", }, keyframes: { float: { @@ -69,6 +77,18 @@ module.exports = { from: { opacity: "0", transform: "translateY(8px)" }, to: { opacity: "1", transform: "translateY(0)" }, }, + scanAvatar: { + "0%": { boxShadow: "0 0 15px rgba(0,255,135,0.3), 0 0 30px rgba(0,255,135,0.1)" }, + "100%": { boxShadow: "0 0 25px rgba(0,255,135,0.6), 0 0 50px rgba(0,255,135,0.3), 0 0 80px rgba(0,255,135,0.1)" }, + }, + scanProgress: { + "0%": { backgroundPosition: "200% 0" }, + "100%": { backgroundPosition: "-200% 0" }, + }, + scanPulse: { + "0%, 100%": { opacity: "1" }, + "50%": { opacity: "0.5" }, + }, }, }, }, diff --git a/tsconfig.json b/tsconfig.json index a5be647..0b01aea 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,13 +15,23 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, - "plugins": [{ "name": "next" }], + "plugins": [ + { + "name": "next" + } + ], "paths": { "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], "exclude": ["node_modules", "dist", ".next", "supabase/migrations/**/*.sql"] }