Skip to content
Open
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
Binary file added screenshots/scanning-done.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/scanning-mid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
209 changes: 209 additions & 0 deletions src/app/onboarding/analyze/onboarding-client.tsx
Original file line number Diff line number Diff line change
@@ -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<TaskStatus[]>(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 (
<div className="flex w-full max-w-md flex-col items-center gap-8">
<div className="flex w-full items-center">
<span className="font-display text-xl font-bold tracking-wider text-white">MERGESHIP</span>
</div>

<div className="flex w-full flex-col gap-2">
<span className="text-[11px] font-medium uppercase tracking-[0.2em] text-zinc-500">
Step 2 of 3
</span>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-zinc-800">
<motion.div
className="h-full rounded-full"
style={{
background: 'linear-gradient(90deg, #00FF87, #00FF87, #00CC6A)',
boxShadow: '0 0 8px rgba(0,255,135,0.4)',
}}
initial={{ width: '50%' }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.6, ease: 'easeOut' }}
/>
</div>
</div>

<div className="scan-avatar-ring flex items-center justify-center">
<div className="flex h-20 w-20 animate-scan-avatar items-center justify-center overflow-hidden rounded-full bg-zinc-800 ring-2 ring-neon-green/30">
{avatarUrl ? (
<Image
src={avatarUrl}
alt={githubHandle}
width={80}
height={80}
className="h-full w-full object-cover"
unoptimized
/>
) : (
<span className="font-display text-xl font-bold text-zinc-500">
{githubHandle.substring(0, 2).toUpperCase()}
</span>
)}
</div>
</div>

<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}
className="scan-card w-full overflow-hidden rounded-xl"
>
<div className="flex items-center justify-between border-b border-zinc-800/60 px-5 py-3">
<span className="inline-flex items-center rounded-full bg-neon-green/15 px-2.5 py-0.5 text-[11px] font-bold uppercase tracking-wider text-neon-green">
SCANNING
</span>
<span className="font-mono text-[11px] text-zinc-500">{processId}</span>
</div>

<div className="px-5 py-3">
{TASKS.map((task, index) => {
const status: TaskStatus = taskStatuses[index] ?? 'pending';
return (
<motion.div
key={task.id}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: index * 0.08 }}
className="flex items-center justify-between py-2.5"
>
<div className="flex items-center gap-3">
<TaskIcon status={status} />
<span
className={`text-sm ${
status === 'pending'
? 'text-zinc-600'
: status === 'active'
? 'text-zinc-200'
: 'text-zinc-300'
}`}
>
{task.label}
</span>
</div>
<span className="font-mono text-xs tabular-nums text-zinc-500">
<TaskDuration status={status} completedDuration={task.completedDuration} />
</span>
</motion.div>
);
})}
</div>
</motion.div>

<p className="text-[13px] text-zinc-600">This usually takes under 10 seconds.</p>
</div>
);
}

function TaskIcon({ status }: { status: TaskStatus }) {
if (status === 'completed') {
return <CheckCircle2 className="h-4 w-4 text-neon-green" />;
}
if (status === 'active') {
return <Loader2 className="h-4 w-4 animate-spin text-amber-400" />;
}
return <Circle className="h-4 w-4 text-zinc-700" />;
}

function TaskDuration({
status,
completedDuration,
}: {
status: TaskStatus;
completedDuration: string | null;
}) {
if (status === 'active') return <span>...</span>;
if (status === 'pending') return <span>--</span>;
return <span>{completedDuration ?? '0.0s'}</span>;
}
54 changes: 54 additions & 0 deletions src/app/onboarding/analyze/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <DemoRender />;
}

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 (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#111318] px-4">
<OnboardingClient avatarUrl={avatarUrl} githubHandle={githubHandle} />
</div>
);
}

function DemoRender() {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#111318] px-4">
<OnboardingClient
avatarUrl="https://avatars.githubusercontent.com/u/90404176?v=4"
githubHandle="PranavAgarkar07"
/>
</div>
);
}
1 change: 1 addition & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { readSupabaseEnv } from '@/lib/supabase/env';

const GATE_BYPASS_PREFIXES = [
'/install',
'/onboarding',
'/api/auth',
'/api/webhooks',
'/api/inngest',
Expand Down
20 changes: 20 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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: {
Expand All @@ -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" },
},
},
},
},
Expand Down
16 changes: 13 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
Loading