Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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.tsxBlame200 lines · 1 contributor
59b6fb2Claude1/**
2 * Onboarding flow — guided setup for new users.
3 */
4
5import { Hono } from "hono";
6import { Layout } from "../views/layout";
7import { softAuth, requireAuth } from "../middleware/auth";
8import type { AuthEnv } from "../middleware/auth";
9import { eq, sql } from "drizzle-orm";
10import { db } from "../db";
11import { repositories, sshKeys, apiTokens, users } from "../db/schema";
12
13const onboardingRoutes = new Hono<AuthEnv>();
14
15onboardingRoutes.get("/getting-started", softAuth, requireAuth, async (c) => {
16 const user = c.get("user")!;
17
18 // Check what the user has done
19 let repoCount = 0;
20 let hasKeys = false;
21 let hasTokens = false;
22
23 try {
24 const [repos] = await db
25 .select({ count: sql<number>`count(*)` })
26 .from(repositories)
27 .where(eq(repositories.ownerId, user.id));
28 repoCount = repos?.count ?? 0;
29
30 const [keys] = await db
31 .select({ count: sql<number>`count(*)` })
32 .from(sshKeys)
33 .where(eq(sshKeys.userId, user.id));
34 hasKeys = (keys?.count ?? 0) > 0;
35
36 const [tokens] = await db
37 .select({ count: sql<number>`count(*)` })
38 .from(apiTokens)
39 .where(eq(apiTokens.userId, user.id));
40 hasTokens = (tokens?.count ?? 0) > 0;
41 } catch { /* DB may not be ready */ }
42
43 const steps = [
44 { label: "Create account", completed: true, active: false },
45 { label: "Create a repository", completed: repoCount > 0, active: repoCount === 0 },
46 { label: "Push your code", completed: false, active: repoCount > 0 },
47 { label: "Set up SSH key", completed: hasKeys, active: !hasKeys && repoCount > 0 },
48 { label: "Create API token", completed: hasTokens, active: !hasTokens && hasKeys },
49 ];
50
51 const activeStep = steps.findIndex((s) => s.active);
52
53 return c.html(
54 <Layout title="Getting Started" user={user}>
55 <div style="max-width:700px;margin:0 auto">
56 <div style="text-align:center;padding:40px 0 32px">
57 <h1 style="font-size:32px;margin-bottom:8px">Welcome to gluecron</h1>
58 <p style="font-size:16px;color:var(--text-muted)">Let's get you set up in a few steps.</p>
59 </div>
60
61 <div style="display:flex;align-items:center;justify-content:center;gap:0;margin-bottom:40px">
62 {steps.map((step, i) => (
63 <>
64 {i > 0 && (
65 <div style={`flex:1;height:2px;background:${step.completed || steps[i - 1].completed ? "var(--green)" : "var(--border)"};min-width:30px`} />
66 )}
67 <div style="display:flex;flex-direction:column;align-items:center;gap:6px">
68 <div style={`width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;${step.completed ? "background:var(--green);border:2px solid var(--green);color:#fff" : step.active ? "border:2px solid var(--accent);color:var(--accent);background:transparent" : "border:2px solid var(--border);color:var(--text-muted);background:transparent"}`}>
69 {step.completed ? "\u2713" : i + 1}
70 </div>
71 <span style={`font-size:11px;white-space:nowrap;${step.active ? "color:var(--text);font-weight:500" : "color:var(--text-muted)"}`}>
72 {step.label}
73 </span>
74 </div>
75 </>
76 ))}
77 </div>
78
79 {/* Step 1: Create repository */}
80 {activeStep <= 1 && repoCount === 0 && (
81 <StepCard
82 number={1}
83 title="Create your first repository"
84 description="A repository contains all your project files, including the revision history."
85 active
86 >
87 <a href="/new" class="btn btn-primary" style="margin-top:12px">Create repository</a>
88 </StepCard>
89 )}
90
91 {/* Step 2: Push code */}
92 {repoCount > 0 && (
93 <StepCard
94 number={2}
95 title="Push your code"
96 description="Connect your local repository and push your first commit."
97 >
98 <pre style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:16px;font-family:var(--font-mono);font-size:13px;margin-top:12px;line-height:1.8;overflow-x:auto">{`# Add the remote
99git remote add gluecron http://localhost:3000/${user.username}/your-repo.git
100
101# Push your code
102git push -u gluecron main`}</pre>
103 <button type="button" class="btn btn-sm" data-clipboard={`git remote add gluecron http://localhost:3000/${user.username}/your-repo.git\ngit push -u gluecron main`} style="margin-top:8px">
104 Copy commands
105 </button>
106 </StepCard>
107 )}
108
109 {/* Step 3: SSH key */}
110 <StepCard
111 number={3}
112 title={hasKeys ? "SSH key added \u2713" : "Add an SSH key"}
113 description={hasKeys ? "Your SSH key is configured." : "SSH keys let you push code securely without entering your password."}
114 completed={hasKeys}
115 >
116 {!hasKeys && (
117 <>
118 <pre style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:16px;font-family:var(--font-mono);font-size:13px;margin-top:12px;line-height:1.8">{`# Generate a key (if you don't have one)
119ssh-keygen -t ed25519 -C "your@email.com"
120
121# Copy your public key
122cat ~/.ssh/id_ed25519.pub`}</pre>
123 <a href="/settings/keys" class="btn btn-primary btn-sm" style="margin-top:12px">Add SSH key</a>
124 </>
125 )}
126 </StepCard>
127
128 {/* Step 4: API token */}
129 <StepCard
130 number={4}
131 title={hasTokens ? "API token created \u2713" : "Create an API token"}
132 description={hasTokens ? "You have an API token configured." : "API tokens let you automate workflows and integrate with CI/CD."}
133 completed={hasTokens}
134 >
135 {!hasTokens && (
136 <>
137 <p style="font-size:14px;color:var(--text-muted);margin-top:12px">
138 Use tokens to authenticate with the gluecron API for scripting and automation.
139 </p>
140 <a href="/settings/tokens" class="btn btn-primary btn-sm" style="margin-top:12px">Create token</a>
141 </>
142 )}
143 </StepCard>
144
145 {/* All done */}
146 {repoCount > 0 && hasKeys && hasTokens && (
147 <div style="text-align:center;padding:40px 0;border:1px solid var(--green);border-radius:var(--radius);margin-top:24px;background:rgba(63,185,80,0.05)">
148 <div style="font-size:48px;margin-bottom:12px">&#127881;</div>
149 <h2>You're all set!</h2>
150 <p style="color:var(--text-muted);margin-top:8px">You've completed the setup. Start building something great.</p>
151 <div style="display:flex;gap:12px;justify-content:center;margin-top:20px">
152 <a href="/" class="btn btn-primary">Go to dashboard</a>
153 <a href="/api/docs" class="btn">Explore the API</a>
154 <a href="/explore" class="btn">Discover repos</a>
155 </div>
156 </div>
157 )}
158
159 <div style="text-align:center;padding:32px 0;color:var(--text-muted);font-size:13px">
160 <p>Need help? Check the <a href="/api/docs">API documentation</a> or press <kbd class="kbd">?</kbd> for keyboard shortcuts.</p>
161 </div>
162 </div>
163 </Layout>
164 );
165});
166
167const StepCard = ({
168 number,
169 title,
170 description,
171 active,
172 completed,
173 children,
174}: {
175 number: number;
176 title: string;
177 description: string;
178 active?: boolean;
179 completed?: boolean;
180 children?: any;
181}) => (
182 <div
183 style={`border:1px solid ${completed ? "var(--green)" : active ? "var(--accent)" : "var(--border)"};border-radius:var(--radius);padding:20px;margin-bottom:16px;${completed ? "opacity:0.7;" : ""}`}
184 >
185 <div style="display:flex;align-items:flex-start;gap:12px">
186 <div
187 style={`width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;flex-shrink:0;${completed ? "background:var(--green);color:#fff" : "background:var(--bg-tertiary);color:var(--text-muted)"}`}
188 >
189 {completed ? "\u2713" : number}
190 </div>
191 <div style="flex:1">
192 <h3 style="font-size:16px;margin-bottom:4px">{title}</h3>
193 <p style="font-size:14px;color:var(--text-muted)">{description}</p>
194 {children}
195 </div>
196 </div>
197 </div>
198);
199
200export default onboardingRoutes;