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.
| 59b6fb2 | 1 | /** |
| 2 | * Onboarding flow — guided setup for new users. | |
| 80bed05 | 3 | * |
| 4 | * Goal: get a fresh user from 0 to first repo in <60 seconds. | |
| 5 | * Headline + 1-line value prop + 3 concrete next-step CTAs + skip-to-dashboard. | |
| 59b6fb2 | 6 | */ |
| 7 | ||
| 8 | import { Hono } from "hono"; | |
| 9 | import { Layout } from "../views/layout"; | |
| 10 | import { softAuth, requireAuth } from "../middleware/auth"; | |
| 11 | import type { AuthEnv } from "../middleware/auth"; | |
| 12 | import { eq, sql } from "drizzle-orm"; | |
| 13 | import { db } from "../db"; | |
| 14 | import { repositories, sshKeys, apiTokens, users } from "../db/schema"; | |
| ea52715 | 15 | import { config } from "../lib/config"; |
| 3e8f8e8 | 16 | import { |
| 17 | Container, | |
| 18 | WelcomeHero, | |
| 19 | StepIndicator, | |
| 20 | Card, | |
| 21 | Flex, | |
| 22 | Text, | |
| 23 | LinkButton, | |
| 24 | CopyBlock, | |
| 25 | Kbd, | |
| 26 | Spacer, | |
| 27 | } from "../views/ui"; | |
| 59b6fb2 | 28 | |
| 29 | const onboardingRoutes = new Hono<AuthEnv>(); | |
| 30 | ||
| c63b860 | 31 | // P3 — `/onboarding` is the canonical post-register landing. Alias the |
| 32 | // existing `/getting-started` handler so both URLs work; new users hit | |
| 33 | // /onboarding?welcome=1 and see a celebration banner. | |
| 34 | const gettingStartedHandler = async (c: any) => { | |
| 59b6fb2 | 35 | const user = c.get("user")!; |
| c63b860 | 36 | const welcome = c.req.query("welcome") === "1"; |
| 59b6fb2 | 37 | |
| 38 | // Check what the user has done | |
| 39 | let repoCount = 0; | |
| 40 | let hasKeys = false; | |
| 41 | let hasTokens = false; | |
| 42 | ||
| 43 | try { | |
| 44 | const [repos] = await db | |
| 45 | .select({ count: sql<number>`count(*)` }) | |
| 46 | .from(repositories) | |
| 47 | .where(eq(repositories.ownerId, user.id)); | |
| 48 | repoCount = repos?.count ?? 0; | |
| 49 | ||
| 50 | const [keys] = await db | |
| 51 | .select({ count: sql<number>`count(*)` }) | |
| 52 | .from(sshKeys) | |
| 53 | .where(eq(sshKeys.userId, user.id)); | |
| 54 | hasKeys = (keys?.count ?? 0) > 0; | |
| 55 | ||
| 56 | const [tokens] = await db | |
| 57 | .select({ count: sql<number>`count(*)` }) | |
| 58 | .from(apiTokens) | |
| 59 | .where(eq(apiTokens.userId, user.id)); | |
| 60 | hasTokens = (tokens?.count ?? 0) > 0; | |
| 61 | } catch { /* DB may not be ready */ } | |
| 62 | ||
| 80bed05 | 63 | const firstRun = repoCount === 0; |
| 59b6fb2 | 64 | |
| 65 | return c.html( | |
| 66 | <Layout title="Getting Started" user={user}> | |
| 80bed05 | 67 | <Container maxWidth={760}> |
| c63b860 | 68 | {welcome && ( |
| 69 | <div | |
| 70 | data-onboarding-welcome="1" | |
| 71 | style="margin: 12px 0 20px; padding: 14px 18px; border-radius: 12px; background: linear-gradient(135deg, rgba(140,109,255,0.18) 0%, rgba(54,197,214,0.18) 100%); border: 1px solid rgba(140,109,255,0.45); font-size: 14px; color: var(--text-strong)" | |
| 72 | > | |
| 73 | 🎉 Welcome to Gluecron! Let's get you set up. | |
| 74 | </div> | |
| 75 | )} | |
| 80bed05 | 76 | {/* ─── Welcome headline + 1-line value prop ─── */} |
| 77 | <WelcomeHero | |
| 78 | title={firstRun ? `Welcome, ${user.username}` : "Finish setting up"} | |
| 79 | subtitle="Ship safer code with AI-native hosting, automated CI, and push-time gates." | |
| 80 | /> | |
| 81 | ||
| 82 | {/* ─── Three concrete next-step CTAs — the 60-second path ─── */} | |
| 83 | {firstRun && ( | |
| 84 | <div class="panel" style="margin-bottom:20px"> | |
| 85 | <div class="panel-item" style="flex-direction:column;align-items:stretch;gap:4px;padding:16px"> | |
| 86 | <div style="display:flex;justify-content:space-between;align-items:center;gap:12px"> | |
| 87 | <div style="flex:1"> | |
| 88 | <div style="font-size:15px;font-weight:600">Create a new repository</div> | |
| 89 | <div style="font-size:13px;color:var(--text-muted);margin-top:2px"> | |
| 90 | Start from scratch. Green-ecosystem defaults, branch protection, labels, CODEOWNERS — all wired on day one. | |
| 91 | </div> | |
| 92 | </div> | |
| 93 | <a href="/new" class="btn btn-primary">Create repo</a> | |
| 94 | </div> | |
| 95 | </div> | |
| 96 | <div class="panel-item" style="flex-direction:column;align-items:stretch;gap:4px;padding:16px"> | |
| 97 | <div style="display:flex;justify-content:space-between;align-items:center;gap:12px"> | |
| 98 | <div style="flex:1"> | |
| 99 | <div style="font-size:15px;font-weight:600">Import from GitHub</div> | |
| 100 | <div style="font-size:13px;color:var(--text-muted);margin-top:2px"> | |
| 101 | Mirror an existing repo by URL. History, branches, and tags come across on the first sync. | |
| 102 | </div> | |
| 103 | </div> | |
| 104 | <a href="/import" class="btn">Import repo</a> | |
| 105 | </div> | |
| 106 | </div> | |
| 107 | <div class="panel-item" style="flex-direction:column;align-items:stretch;gap:4px;padding:16px"> | |
| 108 | <div style="display:flex;justify-content:space-between;align-items:center;gap:12px"> | |
| 109 | <div style="flex:1"> | |
| 110 | <div style="font-size:15px;font-weight:600">Browse public repos</div> | |
| 111 | <div style="font-size:13px;color:var(--text-muted);margin-top:2px"> | |
| 112 | See what others are building. Fork or star without leaving the platform. | |
| 113 | </div> | |
| 114 | </div> | |
| 115 | <a href="/explore" class="btn">Browse</a> | |
| 116 | </div> | |
| 117 | </div> | |
| 118 | </div> | |
| 59b6fb2 | 119 | )} |
| 120 | ||
| 80bed05 | 121 | {/* ─── Existing users: show remaining setup as a compact checklist ─── */} |
| 122 | {!firstRun && ( | |
| 123 | <div class="panel" style="margin-bottom:20px"> | |
| 124 | <div class="panel-item" style="justify-content:space-between;padding:14px 16px"> | |
| 125 | <div> | |
| 126 | <div style="font-size:14px;font-weight:600"> | |
| 127 | {"✓"} You have {repoCount} repositor{repoCount === 1 ? "y" : "ies"} | |
| 128 | </div> | |
| 129 | <div style="font-size:12px;color:var(--text-muted);margin-top:2px"> | |
| 130 | Push code, open issues, review PRs. | |
| 131 | </div> | |
| 132 | </div> | |
| 133 | <a href="/dashboard" class="btn btn-sm">Open dashboard</a> | |
| 134 | </div> | |
| 135 | <div class="panel-item" style="justify-content:space-between;padding:14px 16px"> | |
| 136 | <div> | |
| 137 | <div style="font-size:14px;font-weight:600"> | |
| 138 | {hasKeys ? "✓ SSH key added" : "Add an SSH key"} | |
| 139 | </div> | |
| 140 | <div style="font-size:12px;color:var(--text-muted);margin-top:2px"> | |
| 141 | {hasKeys ? "Push without passwords." : "Push without entering a password every time."} | |
| 142 | </div> | |
| 143 | </div> | |
| 144 | {!hasKeys && <a href="/settings/keys" class="btn btn-sm">Add key</a>} | |
| 145 | </div> | |
| 146 | <div class="panel-item" style="justify-content:space-between;padding:14px 16px"> | |
| 147 | <div> | |
| 148 | <div style="font-size:14px;font-weight:600"> | |
| 149 | {hasTokens ? "✓ API token ready" : "Create an API token"} | |
| 150 | </div> | |
| 151 | <div style="font-size:12px;color:var(--text-muted);margin-top:2px"> | |
| 152 | {hasTokens ? "Use it for CI, CLI, and automation." : "Authenticate scripts, CI, and the CLI."} | |
| 153 | </div> | |
| 154 | </div> | |
| 155 | {!hasTokens && <a href="/settings/tokens" class="btn btn-sm">Create token</a>} | |
| 156 | </div> | |
| 157 | </div> | |
| 59b6fb2 | 158 | )} |
| 159 | ||
| 80bed05 | 160 | {/* ─── Push snippet (only once the user has at least one repo) ─── */} |
| 161 | {!firstRun && ( | |
| 162 | <Card style="padding:16px;margin-bottom:20px"> | |
| 163 | <h3 style="font-size:14px;margin:0 0 8px 0">Push an existing project</h3> | |
| 164 | <CopyBlock | |
| ea52715 | 165 | text={`git remote add gluecron ${config.appBaseUrl}/${user.username}/your-repo.git\ngit push -u gluecron main`} |
| 80bed05 | 166 | label="Commands" |
| 167 | /> | |
| 168 | </Card> | |
| 169 | )} | |
| 59b6fb2 | 170 | |
| 80bed05 | 171 | {/* ─── All done celebration ─── */} |
| 59b6fb2 | 172 | {repoCount > 0 && hasKeys && hasTokens && ( |
| 80bed05 | 173 | <Card style="text-align:center;padding:32px 0;border-color:var(--green);margin-bottom:20px;background:rgba(63,185,80,0.05)"> |
| 174 | <div style="font-size:40px;margin-bottom:8px">🎉</div> | |
| 175 | <h2 style="margin:0">You're all set.</h2> | |
| 176 | <Text size={13} muted style="display:block;margin-top:6px"> | |
| 177 | Setup complete. Start building. | |
| 178 | </Text> | |
| 179 | <Flex gap={12} justify="center" style="margin-top:16px"> | |
| 180 | <LinkButton href="/dashboard" variant="primary">Open dashboard</LinkButton> | |
| 3e8f8e8 | 181 | <LinkButton href="/explore">Discover repos</LinkButton> |
| 182 | </Flex> | |
| 183 | </Card> | |
| 59b6fb2 | 184 | )} |
| 185 | ||
| 80bed05 | 186 | {/* ─── Skip-to-dashboard + help ─── */} |
| 187 | <div style="text-align:center;padding:16px 0 32px 0"> | |
| 188 | <a href="/dashboard" style="font-size:13px;color:var(--text-muted);text-decoration:underline"> | |
| 189 | Skip to dashboard {"→"} | |
| 190 | </a> | |
| 191 | <div style="margin-top:12px"> | |
| 192 | <Text size={12} muted> | |
| 193 | Need help? See the <a href="/api/docs">API docs</a> or press <Kbd>?</Kbd> for shortcuts. | |
| 194 | </Text> | |
| 195 | </div> | |
| 59b6fb2 | 196 | </div> |
| 3e8f8e8 | 197 | </Container> |
| 59b6fb2 | 198 | </Layout> |
| 199 | ); | |
| c63b860 | 200 | }; |
| 201 | ||
| 202 | onboardingRoutes.get("/getting-started", softAuth, requireAuth, gettingStartedHandler); | |
| 203 | onboardingRoutes.get("/onboarding", softAuth, requireAuth, gettingStartedHandler); | |
| 59b6fb2 | 204 | |
| 205 | export default onboardingRoutes; |