Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

onboarding.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

onboarding.tsxBlame687 lines · 2 contributors
59b6fb2Claude1/**
2 * Onboarding flow — guided setup for new users.
80bed05Claude3 *
4 * Goal: get a fresh user from 0 to first repo in <60 seconds.
f0b5874Claude5 * Headline + 1-line value prop + 4 concrete numbered step cards +
6 * skip-to-dashboard. 2026 polish — gradient hairline hero, orb, eyebrow,
7 * gradient verb in the headline, numbered step cards. All scoped under
8 * `.onb-*`.
59b6fb2Claude9 */
10
11import { Hono } from "hono";
12import { Layout } from "../views/layout";
13import { softAuth, requireAuth } from "../middleware/auth";
14import type { AuthEnv } from "../middleware/auth";
15import { eq, sql } from "drizzle-orm";
16import { db } from "../db";
f0b5874Claude17import { repositories, sshKeys, apiTokens } from "../db/schema";
ea52715copilot-swe-agent[bot]18import { config } from "../lib/config";
59b6fb2Claude19
20const onboardingRoutes = new Hono<AuthEnv>();
21
f0b5874Claude22/* ─────────────────────────────────────────────────────────────────────────
23 * Scoped CSS — every class prefixed `.onb-` so this surface can't bleed
24 * into other pages. Mirrors the gradient-hairline hero + section card
25 * patterns from admin-integrations.tsx, admin-ops.tsx, error-page.tsx.
26 * ───────────────────────────────────────────────────────────────────── */
27const styles = `
eed4684Claude28 .onb-wrap { max-width: 1680px; margin: 0 auto; padding: var(--space-6, 32px) var(--space-4, 24px); }
f0b5874Claude29
30 /* ─── Hero ─── */
31 .onb-hero {
32 position: relative;
33 margin-bottom: var(--space-5);
34 padding: clamp(28px, 4vw, 48px) clamp(24px, 4vw, 48px);
35 background: var(--bg-elevated);
36 border: 1px solid var(--border);
37 border-radius: 18px;
38 overflow: hidden;
39 }
40 .onb-hero::before {
41 content: '';
42 position: absolute;
43 top: 0; left: 0; right: 0;
44 height: 2px;
6fd5915Claude45 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
f0b5874Claude46 opacity: 0.75;
47 pointer-events: none;
48 }
49 .onb-hero-orb {
50 position: absolute;
51 inset: -30% -10% auto auto;
52 width: 460px; height: 460px;
6fd5915Claude53 background: radial-gradient(circle, rgba(91,110,232,0.22), rgba(95,143,160,0.10) 45%, transparent 70%);
f0b5874Claude54 filter: blur(80px);
55 opacity: 0.7;
56 pointer-events: none;
57 z-index: 0;
58 }
59 .onb-hero-inner { position: relative; z-index: 1; max-width: 720px; }
60 .onb-eyebrow {
61 display: inline-flex;
62 align-items: center;
63 gap: 8px;
64 font-family: var(--font-mono);
65 font-size: 11px;
66 letter-spacing: 0.18em;
67 text-transform: uppercase;
68 color: var(--text-muted);
69 font-weight: 600;
70 margin-bottom: 16px;
71 }
72 .onb-eyebrow-dot {
73 width: 8px; height: 8px;
74 border-radius: 9999px;
6fd5915Claude75 background: linear-gradient(135deg, #5b6ee8, #5f8fa0);
76 box-shadow: 0 0 0 3px rgba(91,110,232,0.18);
f0b5874Claude77 }
78 .onb-eyebrow strong { color: var(--accent); font-weight: 600; letter-spacing: 0.04em; }
79 .onb-title {
80 font-family: var(--font-display);
81 font-size: clamp(32px, 5vw, 48px);
82 font-weight: 800;
83 letter-spacing: -0.030em;
84 line-height: 1.05;
85 margin: 0 0 var(--space-3);
86 color: var(--text-strong);
87 }
88 .onb-title-grad {
6fd5915Claude89 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
f0b5874Claude90 -webkit-background-clip: text;
91 background-clip: text;
92 -webkit-text-fill-color: transparent;
93 color: transparent;
94 }
95 .onb-sub {
96 font-size: 16px;
97 color: var(--text-muted);
98 margin: 0;
99 line-height: 1.55;
100 max-width: 600px;
101 }
102
103 /* ─── Welcome banner ─── */
104 .onb-welcome {
105 margin-bottom: var(--space-5);
106 padding: 14px 18px;
107 border-radius: 12px;
6fd5915Claude108 background: linear-gradient(135deg, rgba(91,110,232,0.16), rgba(95,143,160,0.10));
109 border: 1px solid rgba(91,110,232,0.40);
f0b5874Claude110 font-size: 14px;
111 color: var(--text-strong);
112 display: flex;
113 align-items: center;
114 gap: 10px;
115 }
116 .onb-welcome-spark {
117 width: 10px; height: 10px;
118 border-radius: 9999px;
6fd5915Claude119 background: linear-gradient(135deg, #5b6ee8, #5f8fa0);
120 box-shadow: 0 0 0 4px rgba(91,110,232,0.18);
f0b5874Claude121 flex-shrink: 0;
122 }
123
124 /* ─── Step cards grid ─── */
125 .onb-steps {
126 display: grid;
127 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
128 gap: var(--space-3);
129 margin-bottom: var(--space-5);
130 }
131 .onb-step {
132 position: relative;
133 padding: var(--space-4);
134 background: var(--bg-elevated);
135 border: 1px solid var(--border);
136 border-radius: 14px;
137 display: flex;
138 flex-direction: column;
139 gap: 10px;
140 transition: border-color 150ms ease, transform 150ms ease, box-shadow 150ms ease;
141 }
142 .onb-step:hover {
6fd5915Claude143 border-color: rgba(91,110,232,0.45);
f0b5874Claude144 transform: translateY(-2px);
6fd5915Claude145 box-shadow: 0 10px 28px -10px rgba(91,110,232,0.30);
f0b5874Claude146 }
147 .onb-step.is-done {
148 border-color: rgba(52,211,153,0.40);
149 background: linear-gradient(180deg, rgba(52,211,153,0.04), var(--bg-elevated) 60%);
150 }
151 .onb-step-head {
152 display: flex;
153 align-items: center;
154 gap: 12px;
155 }
156 .onb-step-num {
157 display: inline-flex;
158 align-items: center;
159 justify-content: center;
160 width: 30px; height: 30px;
161 border-radius: 9999px;
6fd5915Claude162 background: linear-gradient(135deg, rgba(91,110,232,0.20), rgba(95,143,160,0.14));
f0b5874Claude163 color: #c5b3ff;
6fd5915Claude164 border: 1px solid rgba(91,110,232,0.40);
f0b5874Claude165 font-family: var(--font-display);
166 font-weight: 700;
167 font-size: 14px;
168 flex-shrink: 0;
169 }
170 .onb-step.is-done .onb-step-num {
171 background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
172 color: #062b1f;
173 border-color: rgba(52,211,153,0.55);
174 }
175 .onb-step-icon {
176 display: inline-flex;
177 align-items: center;
178 justify-content: center;
179 width: 30px; height: 30px;
180 border-radius: 9px;
6fd5915Claude181 background: rgba(91,110,232,0.12);
182 color: #5b6ee8;
183 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.32);
f0b5874Claude184 margin-left: auto;
185 flex-shrink: 0;
186 }
187 .onb-step-title {
188 font-family: var(--font-display);
189 font-size: 15px;
190 font-weight: 700;
191 color: var(--text-strong);
192 margin: 0;
193 letter-spacing: -0.012em;
194 }
195 .onb-step-desc {
196 font-size: 13px;
197 color: var(--text-muted);
198 margin: 0;
199 line-height: 1.5;
200 flex: 1;
201 }
202 .onb-step-foot {
203 display: flex;
204 align-items: center;
205 gap: 10px;
206 margin-top: 4px;
207 }
208 .onb-btn {
209 display: inline-flex;
210 align-items: center;
211 justify-content: center;
212 gap: 6px;
213 padding: 8px 14px;
214 border-radius: 9px;
215 font-size: 13px;
216 font-weight: 600;
217 text-decoration: none;
218 border: 1px solid transparent;
219 cursor: pointer;
220 font: inherit;
221 transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease, border-color 120ms ease;
222 line-height: 1;
223 }
224 .onb-btn-primary {
6fd5915Claude225 background: linear-gradient(135deg, #5b6ee8 0%, #5f8fa0 100%);
f0b5874Claude226 color: #ffffff;
6fd5915Claude227 box-shadow: 0 6px 16px -6px rgba(91,110,232,0.50), inset 0 1px 0 rgba(255,255,255,0.16);
f0b5874Claude228 }
229 .onb-btn-primary:hover {
230 transform: translateY(-1px);
6fd5915Claude231 box-shadow: 0 10px 22px -8px rgba(91,110,232,0.60), inset 0 1px 0 rgba(255,255,255,0.20);
f0b5874Claude232 color: #ffffff;
233 text-decoration: none;
234 }
235 .onb-btn-ghost {
236 background: transparent;
237 color: var(--text);
238 border-color: var(--border-strong, var(--border));
239 }
240 .onb-btn-ghost:hover {
6fd5915Claude241 background: rgba(91,110,232,0.06);
242 border-color: rgba(91,110,232,0.45);
f0b5874Claude243 color: var(--text-strong);
244 text-decoration: none;
245 }
246 .onb-skip {
247 background: transparent;
248 color: var(--text-muted);
249 border: none;
250 padding: 6px 4px;
251 font-size: 12px;
252 text-decoration: underline;
253 text-decoration-style: dotted;
254 text-underline-offset: 3px;
255 }
256 .onb-skip:hover { color: var(--text); text-decoration-style: solid; }
257 .onb-step-done-badge {
258 display: inline-flex;
259 align-items: center;
260 gap: 6px;
261 padding: 4px 10px;
262 border-radius: 9999px;
263 font-size: 11px;
264 font-weight: 600;
265 letter-spacing: 0.04em;
266 text-transform: uppercase;
267 background: rgba(52,211,153,0.14);
268 color: #6ee7b7;
269 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
270 }
271 .onb-step-done-badge .dot { width: 6px; height: 6px; border-radius: 9999px; background: currentColor; }
272
273 /* ─── Push snippet card ─── */
274 .onb-section {
275 margin-bottom: var(--space-5);
276 background: var(--bg-elevated);
277 border: 1px solid var(--border);
278 border-radius: 14px;
279 overflow: hidden;
280 }
281 .onb-section-head {
282 padding: var(--space-4) var(--space-5);
283 border-bottom: 1px solid var(--border);
284 }
285 .onb-section-title {
286 margin: 0;
287 font-family: var(--font-display);
288 font-size: 16px;
289 font-weight: 700;
290 color: var(--text-strong);
291 letter-spacing: -0.014em;
292 }
293 .onb-section-sub {
294 margin: 4px 0 0;
295 font-size: 12.5px;
296 color: var(--text-muted);
297 }
298 .onb-section-body { padding: var(--space-4) var(--space-5); }
299 .onb-code {
300 margin: 0;
301 padding: 14px 16px;
302 background: var(--bg-secondary, rgba(0,0,0,0.20));
303 border: 1px solid var(--border-subtle, var(--border));
304 border-radius: 10px;
305 font-family: var(--font-mono);
306 font-size: 12.5px;
307 line-height: 1.6;
308 color: var(--text);
309 overflow-x: auto;
310 white-space: pre;
311 }
312
313 /* ─── Empty / "all done" celebration ─── */
314 .onb-empty {
315 position: relative;
316 margin-bottom: var(--space-5);
317 padding: clamp(28px, 4vw, 44px) clamp(20px, 4vw, 40px);
318 text-align: center;
319 background: var(--bg-elevated);
6fd5915Claude320 border: 1px dashed rgba(91,110,232,0.40);
f0b5874Claude321 border-radius: 16px;
322 overflow: hidden;
323 }
324 .onb-empty-orb {
325 position: absolute;
326 inset: -40% -20% auto auto;
327 width: 320px; height: 320px;
6fd5915Claude328 background: radial-gradient(circle, rgba(52,211,153,0.20), rgba(95,143,160,0.10) 45%, transparent 70%);
f0b5874Claude329 filter: blur(60px);
330 opacity: 0.7;
331 pointer-events: none;
332 z-index: 0;
333 }
334 .onb-empty-inner { position: relative; z-index: 1; }
335 .onb-empty-glyph {
336 display: inline-flex;
337 align-items: center;
338 justify-content: center;
339 width: 56px; height: 56px;
340 border-radius: 9999px;
6fd5915Claude341 background: linear-gradient(135deg, rgba(91,110,232,0.18), rgba(95,143,160,0.12));
f0b5874Claude342 color: #c5b3ff;
6fd5915Claude343 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.35);
f0b5874Claude344 margin-bottom: 14px;
345 }
346 .onb-empty-title {
347 font-family: var(--font-display);
348 font-size: 22px;
349 font-weight: 700;
350 margin: 0 0 8px;
351 color: var(--text-strong);
352 letter-spacing: -0.018em;
353 }
354 .onb-empty-sub {
355 font-size: 14px;
356 color: var(--text-muted);
357 margin: 0 auto 18px;
358 max-width: 480px;
359 line-height: 1.55;
360 }
361 .onb-empty-actions {
362 display: flex;
363 gap: 10px;
364 justify-content: center;
365 flex-wrap: wrap;
366 }
367
368 /* ─── Skip + help foot ─── */
369 .onb-foot {
370 text-align: center;
371 padding: var(--space-3) 0 var(--space-6);
372 color: var(--text-muted);
373 font-size: 13px;
374 }
375 .onb-foot a { color: var(--accent); text-decoration: none; }
376 .onb-foot a:hover { text-decoration: underline; }
377 .onb-foot-kbd {
378 display: inline-flex;
379 align-items: center;
380 justify-content: center;
381 min-width: 18px;
382 padding: 1px 6px;
383 margin: 0 2px;
384 border-radius: 4px;
385 border: 1px solid var(--border);
386 background: rgba(255,255,255,0.04);
387 font-family: var(--font-mono);
388 font-size: 11px;
389 color: var(--text);
390 }
391`;
392
393/* Inline SVG icons used in the step cards. Small, monochrome, currentColor. */
394function IconConnect() {
395 return (
396 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
397 <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
398 <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
399 </svg>
400 );
401}
402function IconImport() {
403 return (
404 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
405 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
406 <polyline points="7 10 12 15 17 10" />
407 <line x1="12" y1="15" x2="12" y2="3" />
408 </svg>
409 );
410}
411function IconRun() {
412 return (
413 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
414 <polygon points="5 3 19 12 5 21 5 3" />
415 </svg>
416 );
417}
418function IconShip() {
419 return (
420 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
421 <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
422 <path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
423 <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
424 <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
425 </svg>
426 );
427}
428function IconKey() {
429 return (
430 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
431 <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
432 </svg>
433 );
434}
435function IconToken() {
436 return (
437 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
438 <rect x="3" y="11" width="18" height="11" rx="2" />
439 <path d="M7 11V7a5 5 0 0 1 10 0v4" />
440 </svg>
441 );
442}
443function IconSparkle() {
444 return (
445 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
446 <path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3z" />
447 </svg>
448 );
449}
450
c63b860Claude451// P3 — `/onboarding` is the canonical post-register landing. Alias the
452// existing `/getting-started` handler so both URLs work; new users hit
453// /onboarding?welcome=1 and see a celebration banner.
454const gettingStartedHandler = async (c: any) => {
59b6fb2Claude455 const user = c.get("user")!;
c63b860Claude456 const welcome = c.req.query("welcome") === "1";
59b6fb2Claude457
458 // Check what the user has done
459 let repoCount = 0;
460 let hasKeys = false;
461 let hasTokens = false;
462
463 try {
464 const [repos] = await db
465 .select({ count: sql<number>`count(*)` })
466 .from(repositories)
467 .where(eq(repositories.ownerId, user.id));
468 repoCount = repos?.count ?? 0;
469
470 const [keys] = await db
471 .select({ count: sql<number>`count(*)` })
472 .from(sshKeys)
473 .where(eq(sshKeys.userId, user.id));
474 hasKeys = (keys?.count ?? 0) > 0;
475
476 const [tokens] = await db
477 .select({ count: sql<number>`count(*)` })
478 .from(apiTokens)
479 .where(eq(apiTokens.userId, user.id));
480 hasTokens = (tokens?.count ?? 0) > 0;
481 } catch { /* DB may not be ready */ }
482
80bed05Claude483 const firstRun = repoCount === 0;
f0b5874Claude484 const allDone = repoCount > 0 && hasKeys && hasTokens;
485
486 // The 4 steps. For new users: Connect / Import / Run / Ship.
487 // For existing users with some setup, mark completed cards as done.
488 const steps = firstRun
489 ? [
490 {
491 n: 1,
492 title: "Create a repository",
493 desc: "Green-ecosystem defaults, branch protection, labels, CODEOWNERS — wired on day one.",
494 cta: { href: "/new", label: "Create repo", primary: true },
495 skip: null,
496 icon: <IconConnect />,
497 done: false,
498 },
499 {
500 n: 2,
501 title: "Import from GitHub",
502 desc: "Mirror an existing repo by URL. History, branches, and tags come across on the first sync.",
503 cta: { href: "/import", label: "Import repo", primary: false },
504 skip: { href: "/dashboard", label: "Skip" },
505 icon: <IconImport />,
506 done: false,
507 },
508 {
509 n: 3,
510 title: "Run the gates",
511 desc: "Push a commit and watch GateTest, AI review, and CI run automatically.",
512 cta: { href: "/explore", label: "Browse repos", primary: false },
513 skip: { href: "/dashboard", label: "Skip" },
514 icon: <IconRun />,
515 done: false,
516 },
517 {
518 n: 4,
519 title: "Ship to production",
520 desc: "Configure auto-merge + deploy webhooks. Your push lands live in ~25 seconds.",
521 cta: { href: "/help", label: "Read the guide", primary: false },
522 skip: { href: "/dashboard", label: "Skip" },
523 icon: <IconShip />,
524 done: false,
525 },
526 ]
527 : [
528 {
529 n: 1,
530 title: "Your repositories",
531 desc: `You have ${repoCount} repositor${repoCount === 1 ? "y" : "ies"}. Push code, open issues, review PRs.`,
532 cta: { href: "/dashboard", label: "Open dashboard", primary: true },
533 skip: null,
534 icon: <IconConnect />,
535 done: repoCount > 0,
536 },
537 {
538 n: 2,
539 title: hasKeys ? "SSH key added" : "Add an SSH key",
540 desc: hasKeys
541 ? "Push without entering a password every time."
542 : "Generate a public key on your machine and paste it in to push without a password.",
543 cta: hasKeys
544 ? { href: "/settings/keys", label: "Manage keys", primary: false }
545 : { href: "/settings/keys", label: "Add key", primary: true },
546 skip: hasKeys ? null : { href: "/dashboard", label: "Skip" },
547 icon: <IconKey />,
548 done: hasKeys,
549 },
550 {
551 n: 3,
552 title: hasTokens ? "API token ready" : "Create an API token",
553 desc: hasTokens
554 ? "Use it for CI, CLI, and automation — same scopes as the web."
555 : "Authenticate scripts, CI, and the CLI. Scoped to your account, revocable any time.",
556 cta: hasTokens
557 ? { href: "/settings/tokens", label: "Manage tokens", primary: false }
558 : { href: "/settings/tokens", label: "Create token", primary: true },
559 skip: hasTokens ? null : { href: "/dashboard", label: "Skip" },
560 icon: <IconToken />,
561 done: hasTokens,
562 },
563 {
564 n: 4,
565 title: "Ship to production",
566 desc: "Configure auto-merge + deploy webhooks. Your push lands live in ~25 seconds.",
567 cta: { href: "/help", label: "Read the guide", primary: false },
568 skip: { href: "/dashboard", label: "Skip" },
569 icon: <IconShip />,
570 done: false,
571 },
572 ];
59b6fb2Claude573
574 return c.html(
575 <Layout title="Getting Started" user={user}>
f0b5874Claude576 <style dangerouslySetInnerHTML={{ __html: styles }} />
577 <div class="onb-wrap">
c63b860Claude578 {welcome && (
f0b5874Claude579 <div class="onb-welcome" data-onboarding-welcome="1">
580 <span class="onb-welcome-spark" aria-hidden="true" />
581 Welcome to Gluecron, <strong>@{user.username}</strong> — let's get you set up.
c63b860Claude582 </div>
583 )}
f0b5874Claude584
585 {/* ─── Hero ─── */}
586 <section class="onb-hero">
587 <div class="onb-hero-orb" aria-hidden="true" />
588 <div class="onb-hero-inner">
589 <div class="onb-eyebrow">
590 <span class="onb-eyebrow-dot" aria-hidden="true" />
591 Onboarding · <strong>@{user.username}</strong>
80bed05Claude592 </div>
f0b5874Claude593 <h1 class="onb-title">
594 <span class="onb-title-grad">{firstRun ? "Get started." : "Finish setup."}</span>
595 </h1>
596 <p class="onb-sub">
597 Ship safer code with AI-native hosting, automated CI, and push-time
598 gates. Four short steps — under a minute end-to-end.
599 </p>
80bed05Claude600 </div>
f0b5874Claude601 </section>
59b6fb2Claude602
f0b5874Claude603 {/* ─── Numbered step cards ─── */}
604 <div class="onb-steps">
605 {steps.map((s) => (
606 <div class={"onb-step" + (s.done ? " is-done" : "")}>
607 <div class="onb-step-head">
608 <span class="onb-step-num">{s.done ? "✓" : s.n}</span>
609 <span class="onb-step-icon" aria-hidden="true">{s.icon}</span>
80bed05Claude610 </div>
f0b5874Claude611 <h3 class="onb-step-title">{s.title}</h3>
612 <p class="onb-step-desc">{s.desc}</p>
613 <div class="onb-step-foot">
614 {s.done ? (
615 <span class="onb-step-done-badge">
616 <span class="dot" aria-hidden="true" />
617 Done
618 </span>
619 ) : (
620 <a
621 href={s.cta.href}
622 class={"onb-btn " + (s.cta.primary ? "onb-btn-primary" : "onb-btn-ghost")}
623 >
624 {s.cta.label}
625 </a>
626 )}
627 {s.skip && !s.done && (
628 <a href={s.skip.href} class="onb-skip">
629 {s.skip.label}
630 </a>
631 )}
80bed05Claude632 </div>
633 </div>
f0b5874Claude634 ))}
635 </div>
59b6fb2Claude636
80bed05Claude637 {/* ─── Push snippet (only once the user has at least one repo) ─── */}
638 {!firstRun && (
f0b5874Claude639 <section class="onb-section">
640 <header class="onb-section-head">
641 <h3 class="onb-section-title">Push an existing project</h3>
642 <p class="onb-section-sub">
643 Add Gluecron as a git remote and push. We'll pick up the history on the first sync.
644 </p>
645 </header>
646 <div class="onb-section-body">
647 <pre class="onb-code">{`git remote add gluecron ${config.appBaseUrl}/${user.username}/your-repo.git
648git push -u gluecron main`}</pre>
649 </div>
650 </section>
80bed05Claude651 )}
59b6fb2Claude652
f0b5874Claude653 {/* ─── All-done empty state — celebration card ─── */}
654 {allDone && (
655 <section class="onb-empty">
656 <div class="onb-empty-orb" aria-hidden="true" />
657 <div class="onb-empty-inner">
658 <span class="onb-empty-glyph" aria-hidden="true"><IconSparkle /></span>
659 <h2 class="onb-empty-title">You're all set.</h2>
660 <p class="onb-empty-sub">
661 Setup complete. Start building, browsing, or invite a teammate.
662 </p>
663 <div class="onb-empty-actions">
664 <a href="/dashboard" class="onb-btn onb-btn-primary">Open dashboard</a>
665 <a href="/explore" class="onb-btn onb-btn-ghost">Discover repos</a>
666 </div>
667 </div>
668 </section>
59b6fb2Claude669 )}
670
f0b5874Claude671 {/* ─── Skip-to-dashboard + help foot ─── */}
672 <div class="onb-foot">
673 <a href="/dashboard">Skip to dashboard &rarr;</a>
674 <div style="margin-top:10px">
675 Need help? See the <a href="/api/docs">API docs</a> or press{" "}
676 <span class="onb-foot-kbd">?</span> for shortcuts.
80bed05Claude677 </div>
59b6fb2Claude678 </div>
f0b5874Claude679 </div>
59b6fb2Claude680 </Layout>
681 );
c63b860Claude682};
683
684onboardingRoutes.get("/getting-started", softAuth, requireAuth, gettingStartedHandler);
685onboardingRoutes.get("/onboarding", softAuth, requireAuth, gettingStartedHandler);
59b6fb2Claude686
687export default onboardingRoutes;