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