Skip to content
File

Blob: src/client/pages/landing-page.tsx

typescript269 lines
1import { useAuth } from "@/client/auth";
2import { StatusPill } from "@/client/components";
3import { Badge, ButtonLink } from "@/client/components/ui";
4 
5const valueProps = [
6 {
7 title: "Pipeline config lives in the repo",
8 description: "anvil reads `.anvil.yml` from your branch — review it, revert it, override it per branch.",
9 },
10 {
11 title: "Serialized runs, no extra locking",
12 description: "One active run per project, everything else in a durable FIFO queue.",
13 },
14 {
15 title: "Logs stream while the run is live",
16 description: "See what's running, what failed, and what's queued next — during the run, not after.",
17 },
18 {
19 title: "Runs on Cloudflare end-to-end",
20 description: "Durable Objects, Queues, D1, and Sandbox SDK — no glue services in between.",
21 },
22];
23 
24const workflow = [
25 {
26 step: "01",
27 title: "Connect a repo",
28 description: "Sign in through tessera, then point anvil at a repository and choose the default branch.",
29 },
30 {
31 step: "02",
32 title: "Commit `.anvil.yml`",
33 description:
34 "Define steps in a YAML file checked into the repo. Branch it, review it, revert it — same workflow as the rest of your code.",
35 },
36 {
37 step: "03",
38 title: "Trigger and watch",
39 description:
40 "Start a run manually or via webhook. Queue position, active steps, and the live log tail all show up in one view.",
41 },
42];
43 
44const previewRuns = [
45 {
46 id: "#218",
47 name: "deploy-docs",
48 detail: "manual trigger by rachel",
49 status: "running" as const,
50 },
51 {
52 id: "#219",
53 name: "smoke-check",
54 detail: "queued from push to main",
55 status: "queued" as const,
56 },
57 {
58 id: "#220",
59 name: "release-verify",
60 detail: "queued from release branch",
61 status: "queued" as const,
62 },
63];
64 
65const previewLogs = [
66 "$ git fetch origin main --depth=1",
67 "$ anvil run deploy-docs",
68 "build passed in 18.4s",
69 "test passed in 43.1s",
70 "deploy uploading assets to edge...",
71];
72 
73const LandingActions = ({ isAuthenticated }: { isAuthenticated: boolean }) =>
74 isAuthenticated ? (
75 <div className="flex flex-wrap items-center gap-3">
76 <ButtonLink to="/app/projects" variant="primary">
77 Go to Dashboard
78 </ButtonLink>
79 <ButtonLink to="/app/projects/new" variant="secondary">
80 New Project
81 </ButtonLink>
82 </div>
83 ) : (
84 <div className="flex flex-wrap items-center gap-3">
85 <ButtonLink to="/app/login" variant="primary">
86 Sign in
87 </ButtonLink>
88 </div>
89 );
90 
91const ProductPreview = () => (
92 <div className="relative overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950">
93 {/* Terminal header */}
94 <div className="flex items-center justify-between border-b border-zinc-800/60 bg-zinc-900/50 px-4 py-2.5">
95 <div className="flex items-center gap-2">
96 <span className="flex gap-1.5 min-w-[54px]">
97 <span className="h-3 w-3 rounded-full bg-zinc-700" />
98 <span className="h-3 w-3 rounded-full bg-zinc-700" />
99 <span className="h-3 w-3 rounded-full bg-zinc-700" />
100 </span>
101 <span className="ml-4 font-mono text-[10px] uppercase tracking-widest text-zinc-500">
102 anvil exec --tail edge-docs
103 </span>
104 </div>
105 <Badge variant="accent" className="font-mono text-[10px]">
106 branch: main
107 </Badge>
108 </div>
109
110 {/* Terminal Output Area */}
111 <div className="grid divide-y divide-zinc-800/60 lg:grid-cols-[minmax(300px,380px)_1fr] lg:divide-x lg:divide-y-0">
112 {/* Queue status */}
113 <div className="bg-zinc-900/20 p-5">
114 <div className="mb-6 flex items-center justify-between">
115 <p className="font-display text-xs font-semibold uppercase tracking-widest text-zinc-100">Queue Status</p>
116 <span className="font-mono text-[10px] text-zinc-500">SERIALIZED FIFO</span>
117 </div>
118
119 <div className="space-y-3">
120 {previewRuns.map((run) => (
121 <div
122 key={run.id}
123 className="group relative -mx-2 flex items-start gap-3 rounded-lg border border-transparent p-2 transition-colors hover:border-zinc-800 hover:bg-zinc-900/40"
124 >
125 <StatusPill status={run.status} className="mt-0.5 shrink-0" />
126 <div className="min-w-0 flex-1">
127 <div className="flex items-center justify-between">
128 <p className="font-mono text-sm text-zinc-200 transition-colors group-hover:text-accent-400">
129 {run.id} {run.name}
130 </p>
131 </div>
132 <p className="mt-0.5 truncate text-xs text-zinc-500">{run.detail}</p>
133 </div>
134 </div>
135 ))}
136 </div>
137 </div>
138
139 {/* Live Stream */}
140 <div className="bg-zinc-950 p-5 font-mono text-xs leading-relaxed text-zinc-400 lg:p-6">
141 <div className="mb-6 flex items-center justify-between">
142 <span className="inline-flex items-center gap-2 text-[10px] uppercase tracking-widest text-emerald-500">
143 <span className="h-2 w-2 animate-pulse rounded-full bg-emerald-500" />
144 Live Output
145 </span>
146 <span className="text-[10px] uppercase tracking-widest text-zinc-600">ID: RUN_01HQK...</span>
147 </div>
148 <div className="space-y-1.5">
149 {previewLogs.map((line, i) => {
150 const isCmd = line.startsWith("$");
151 const isActive = line.includes("active") || line.includes("uploading");
152 return (
153 <p
154 key={i}
155 className={
156 isCmd ? "mb-2 mt-4 font-medium text-zinc-100" : isActive ? "text-accent-400" : "text-zinc-400"
157 }
158 >
159 {line}
160 </p>
161 );
162 })}
163 <div className="flex gap-2">
164 <span className="animate-pulse text-accent-400">_</span>
165 </div>
166 </div>
167 </div>
168 </div>
169 </div>
170);
171 
172export const LandingPage = () => {
173 const { isAuthenticated } = useAuth();
174 
175 return (
176 <div className="pb-16 pt-8">
177 {/* Hero Section */}
178 <section className="mx-auto max-w-7xl px-4 py-12 sm:px-6 sm:py-24">
179 <div className="relative">
180 <Badge
181 variant="accent"
182 className="mb-6 inline-flex border-accent-500/30 font-mono text-[10px] uppercase tracking-widest"
183 >
184 System Active
185 </Badge>
186 <div className="grid gap-12 lg:grid-cols-[1fr_auto]">
187 <div className="animate-slide-up max-w-4xl">
188 <h1 className="font-display text-5xl font-bold tracking-tight text-zinc-100 sm:text-7xl lg:text-[5rem] lg:leading-[0.95]">
189 <span className="block text-zinc-500">EDGE-NATIVE CI/CD.</span>
190 <span className="block">BUILT FOR VELOCITY.</span>
191 </h1>
192 </div>
193 <div className="animate-slide-up flex flex-col justify-end lg:pb-4" style={{ animationDelay: "120ms" }}>
194 <p className="max-w-sm text-base leading-7 text-zinc-400">
195 A streamlined, serialized execution engine built on Cloudflare Durable Objects. No glue services. No
196 yaml bloat.
197 </p>
198 <div className="mt-8">
199 <LandingActions isAuthenticated={isAuthenticated} />
200 </div>
201 </div>
202 </div>
203 </div>
204
205 <div className="animate-slide-up mt-16 sm:mt-24" style={{ animationDelay: "240ms" }}>
206 <ProductPreview />
207 </div>
208 </section>
209
210 {/* Why anvil section */}
211 <section className="mx-auto max-w-7xl border-t border-zinc-800/40 px-4 py-16 sm:px-6 lg:py-24">
212 <div className="max-w-3xl">
213 <h2 className="font-display text-3xl font-bold tracking-tight text-zinc-100 sm:text-4xl">
214 A CI designed for personal project velocity.
215 </h2>
216 <p className="mt-4 max-w-xl text-lg text-zinc-400">
217 No complex DAGs. No infrastructure mapping. Just a single execution queue connected to your Cloudflare edge
218 infrastructure.
219 </p>
220 </div>
221
222 <div className="mt-16 grid gap-12 sm:grid-cols-2 lg:grid-cols-4 lg:gap-8">
223 {valueProps.map((feature, i) => (
224 <div
225 key={feature.title}
226 className="group relative animate-slide-up"
227 style={{ animationDelay: `${i * 60}ms` }}
228 >
229 <div className="font-display text-6xl font-black text-zinc-800/50 transition-colors group-hover:text-zinc-700/60">
230 0{i + 1}
231 </div>
232 <h3 className="mt-4 font-display text-lg font-semibold tracking-wide text-zinc-100">{feature.title}</h3>
233 <p className="mt-3 text-sm leading-6 text-zinc-400">{feature.description}</p>
234 </div>
235 ))}
236 </div>
237 </section>
238
239 {/* How it works section */}
240 <section className="mx-auto max-w-7xl border-t border-zinc-800/40 px-4 py-16 sm:px-6 lg:py-24">
241 <div className="grid gap-16 lg:grid-cols-[1fr_1.5fr]">
242 <div className="max-w-md">
243 <h2 className="font-display text-3xl font-bold tracking-tight text-zinc-100 sm:text-4xl">
244 Implementation path.
245 </h2>
246 <p className="mt-4 leading-relaxed text-zinc-400">
247 Go from an empty queue to a live edge deployment in under three minutes, entirely defined in your git
248 history.
249 </p>
250 </div>
251
252 <div className="relative space-y-16 border-l border-zinc-800/60 pl-8">
253 {workflow.map((item, i) => (
254 <div key={item.step} className="relative animate-slide-up" style={{ animationDelay: `${i * 60}ms` }}>
255 <div className="absolute -left-[37px] top-1.5 rounded-full bg-canvas p-1">
256 <div className="h-2 w-2 rounded-full bg-accent-500" />
257 </div>
258 <p className="mb-2 font-mono text-xs uppercase tracking-widest text-accent-500">{item.step}</p>
259 <h3 className="font-display text-xl font-bold tracking-tight text-zinc-100">{item.title}</h3>
260 <p className="mt-3 max-w-xl text-sm leading-6 text-zinc-400">{item.description}</p>
261 </div>
262 ))}
263 </div>
264 </div>
265 </section>
266 </div>
267 );
268};