| 1 | import { Icon } from "@iconify/react"; |
| 2 | import { useEffect, useRef, useState } from "react"; |
| 3 | import { highlightTS } from "../marketing/highlightTS"; |
| 4 | |
| 5 | /** |
| 6 | * Showcase for the "Compose Effects and Layers..." (Effect-pilled) section. |
| 7 | * |
| 8 | * Single terminal-chromed card with a tab strip in the header and one |
| 9 | * focused example per tab. Each panel pairs a trimmed code snippet (left) |
| 10 | * with a stylized "generated artifact" panel (right) that visually proves |
| 11 | * what Alchemy wired up. Tabs auto-cycle while in view; tap to pin. |
| 12 | * |
| 13 | * Tabs: |
| 14 | * - IAM bindings — `S3.GetObject(...)` → permissions + env vars |
| 15 | * - Event sources — `DynamoDB.stream(...).process(...)` → EventSourceMapping |
| 16 | * - Durable Objects — `Cloudflare.DurableObject` |
| 17 | * - Containers — `Cloudflare.Container` |
| 18 | * - Workflows — `Cloudflare.Workflow` |
| 19 | * - Layers — `Layer.effect(JobStorage, ...)` |
| 20 | */ |
| 21 | |
| 22 | type Tab = "iam" | "stream" | "do" | "container" | "workflow" | "layer"; |
| 23 | |
| 24 | const TABS: { id: Tab; label: string }[] = [ |
| 25 | { id: "iam", label: "IAM bindings" }, |
| 26 | { id: "stream", label: "Event sources" }, |
| 27 | { id: "do", label: "Durable Objects" }, |
| 28 | { id: "container", label: "Containers" }, |
| 29 | { id: "workflow", label: "Workflows" }, |
| 30 | { id: "layer", label: "Layers" }, |
| 31 | ]; |
| 32 | |
| 33 | const CYCLE_MS = 6000; |
| 34 | /** Longer dwell on the Layer tab so both impls (ddb / d1) get airtime. */ |
| 35 | const LAYER_CYCLE_MS = 13000; |
| 36 | /** How long each Layer impl is "held" before the breathing cross-fade swaps. */ |
| 37 | const LAYER_IMPL_SWAP_MS = 3500; |
| 38 | |
| 39 | const IAM_CODE = `export default AWS.Lambda.Function( |
| 40 | "JobApi", |
| 41 | Effect.gen(function* () { |
| 42 | const getPhoto = yield* S3.GetObject(Photos); |
| 43 | const putJob = yield* DynamoDB.PutItem(Jobs); |
| 44 | |
| 45 | return { |
| 46 | fetch: Effect.gen(function* () { |
| 47 | const req = yield* HttpServerRequest; |
| 48 | return yield* getPhoto({ key: req.url.slice(1) }); |
| 49 | }), |
| 50 | }; |
| 51 | }), |
| 52 | );`; |
| 53 | |
| 54 | const STREAM_CODE = `export default AWS.Lambda.Function( |
| 55 | "JobsConsumer", |
| 56 | Effect.gen(function* () { |
| 57 | yield* DynamoDB.stream(Jobs).process((stream) => |
| 58 | stream.pipe( |
| 59 | Stream.map((r) => r.dynamodb.NewImage), |
| 60 | Stream.tap((job) => Effect.log(\`new job: \${job.id.S}\`)), |
| 61 | Stream.runDrain, |
| 62 | ), |
| 63 | ); |
| 64 | }), |
| 65 | );`; |
| 66 | |
| 67 | const DO_CODE = `export default class Room extends Cloudflare.DurableObject<Room>()( |
| 68 | "Rooms", |
| 69 | Effect.gen(function* () { |
| 70 | const state = yield* Cloudflare.DurableObjectState; |
| 71 | return Effect.gen(function* () { |
| 72 | const sessions = new Map<string, Cloudflare.WebSocket>(); |
| 73 | |
| 74 | return { |
| 75 | fetch: Effect.gen(function* () { |
| 76 | const [response, socket] = yield* Cloudflare.upgrade(); |
| 77 | sessions.set(crypto.randomUUID(), socket); |
| 78 | return response; |
| 79 | }), |
| 80 | }; |
| 81 | }); |
| 82 | }), |
| 83 | ) {}`; |
| 84 | |
| 85 | const CONTAINER_CODE = `export class Sandbox extends Cloudflare.Container<Sandbox, { |
| 86 | exec: (cmd: string) => Effect.Effect<ExecResult>; |
| 87 | }>()("Sandbox") {} |
| 88 | |
| 89 | export const SandboxLive = Sandbox.make( |
| 90 | Stack.useSync((stack) => ({ |
| 91 | main: import.meta.url, |
| 92 | instanceType: stack.stage === "prod" ? "standard-1" : "dev", |
| 93 | })), |
| 94 | Effect.gen(function* () { |
| 95 | const cp = yield* ChildProcessSpawner; |
| 96 | return Sandbox.of({ |
| 97 | exec: (cmd) => cp.spawn(ChildProcess.make(cmd, { shell: true })), |
| 98 | }); |
| 99 | }), |
| 100 | );`; |
| 101 | |
| 102 | const WORKFLOW_CODE = `export default class Notifier extends Cloudflare.Workflow<Notifier>()( |
| 103 | "Notifier", |
| 104 | Effect.gen(function* () { |
| 105 | const rooms = yield* Room; |
| 106 | return Effect.gen(function* () { |
| 107 | const { roomId, message } = (yield* Cloudflare.Workflows.WorkflowEvent).payload; |
| 108 | yield* Cloudflare.Workflows.task("store", rooms.getByName(roomId).store(message)); |
| 109 | yield* Cloudflare.Workflows.task("process", processMessage(message)); |
| 110 | yield* Cloudflare.Workflows.task("broadcast", rooms.getByName(roomId).broadcast(message)); |
| 111 | yield* Cloudflare.Workflows.sleep("cooldown", "2 seconds"); |
| 112 | yield* Cloudflare.Workflows.task("finalize", rooms.getByName(roomId).cleanup()); |
| 113 | }); |
| 114 | }), |
| 115 | ) {}`; |
| 116 | |
| 117 | type LayerImpl = "ddb" | "d1"; |
| 118 | |
| 119 | /** |
| 120 | * The Layer tab renders a structural diff between JobStorageDynamoDB and |
| 121 | * JobStorageD1. The two snippets are split into segments: "common" lines |
| 122 | * stay static; each "diff" segment overlays the ddb and d1 variants in the |
| 123 | * same grid cell so the inactive one fully hides (opacity 0) and the active |
| 124 | * one occupies that space — crossfading smoothly between them. |
| 125 | * |
| 126 | * Structurally parallel layout keeps the diff readable column-for-column. |
| 127 | */ |
| 128 | type LayerSegment = |
| 129 | | { kind: "common"; lines: string[] } |
| 130 | | { kind: "diff"; ddb: string[]; d1: string[] }; |
| 131 | |
| 132 | const LAYER_SEGMENTS: LayerSegment[] = [ |
| 133 | { |
| 134 | kind: "common", |
| 135 | lines: [ |
| 136 | "export class JobStorage extends Context.Service<JobStorage, {", |
| 137 | " putJob: (job: Job) => Effect.Effect<Job, PutJobError>;", |
| 138 | " getJob: (id: string) => Effect.Effect<Job | undefined, GetJobError>;", |
| 139 | '}>()("JobStorage") {}', |
| 140 | "", |
| 141 | ], |
| 142 | }, |
| 143 | { |
| 144 | kind: "diff", |
| 145 | ddb: [ |
| 146 | "export const JobStorageDynamoDB = Layer.effect(JobStorage, Effect.gen(function* () {", |
| 147 | ' const table = yield* DynamoDB.Table("JobsTable", { partitionKey: "id" });', |
| 148 | " const putItem = yield* DynamoDB.PutItem(table);", |
| 149 | " const getItem = yield* DynamoDB.GetItem.bind(table);", |
| 150 | ], |
| 151 | d1: [ |
| 152 | "export const JobStorageD1 = Layer.effect(JobStorage, Effect.gen(function* () {", |
| 153 | ' const db = yield* Cloudflare.D1.Database("JobsDB");', |
| 154 | " const conn = yield* Cloudflare.D1.QueryDatabase(db);", |
| 155 | "", |
| 156 | ], |
| 157 | }, |
| 158 | { |
| 159 | kind: "common", |
| 160 | lines: [" return JobStorage.of({"], |
| 161 | }, |
| 162 | { |
| 163 | kind: "diff", |
| 164 | ddb: [ |
| 165 | " putJob: (job) => putItem({ Item: { id: { S: job.id } } }),", |
| 166 | " getJob: (id) => getItem({ Key: { id: { S: id } } }),", |
| 167 | ], |
| 168 | d1: [ |
| 169 | " putJob: (job) => conn.exec(`INSERT INTO jobs (id) VALUES ('${job.id}')`),", |
| 170 | " getJob: (id) => conn.exec(`SELECT * FROM jobs WHERE id = '${id}'`),", |
| 171 | ], |
| 172 | }, |
| 173 | { |
| 174 | kind: "common", |
| 175 | lines: [" });", "}));"], |
| 176 | }, |
| 177 | ]; |
| 178 | |
| 179 | const CODE_BY_TAB: Record<Exclude<Tab, "layer">, string> = { |
| 180 | iam: IAM_CODE, |
| 181 | stream: STREAM_CODE, |
| 182 | do: DO_CODE, |
| 183 | container: CONTAINER_CODE, |
| 184 | workflow: WORKFLOW_CODE, |
| 185 | }; |
| 186 | |
| 187 | export default function EffectPilledShowcase() { |
| 188 | const [tab, setTab] = useState<Tab>("iam"); |
| 189 | const [pinned, setPinned] = useState(false); |
| 190 | const [layerImpl, setLayerImpl] = useState<LayerImpl>("ddb"); |
| 191 | const wrapRef = useRef<HTMLDivElement>(null); |
| 192 | |
| 193 | // Tab auto-cycle. Dwell longer on the Layer tab so both impls show |
| 194 | // before moving on to the next tab. |
| 195 | useEffect(() => { |
| 196 | if (pinned) return; |
| 197 | let cancelled = false; |
| 198 | let timer: ReturnType<typeof setTimeout> | null = null; |
| 199 | |
| 200 | const schedule = () => { |
| 201 | const dwell = tab === "layer" ? LAYER_CYCLE_MS : CYCLE_MS; |
| 202 | timer = setTimeout(() => { |
| 203 | if (cancelled) return; |
| 204 | setTab((t) => { |
| 205 | const idx = TABS.findIndex((x) => x.id === t); |
| 206 | return TABS[(idx + 1) % TABS.length]!.id; |
| 207 | }); |
| 208 | }, dwell); |
| 209 | }; |
| 210 | |
| 211 | const obs = new IntersectionObserver( |
| 212 | (entries) => { |
| 213 | for (const e of entries) { |
| 214 | if (e.isIntersecting) { |
| 215 | schedule(); |
| 216 | } else if (timer) { |
| 217 | clearTimeout(timer); |
| 218 | timer = null; |
| 219 | } |
| 220 | } |
| 221 | }, |
| 222 | { threshold: 0.3 }, |
| 223 | ); |
| 224 | if (wrapRef.current) obs.observe(wrapRef.current); |
| 225 | |
| 226 | return () => { |
| 227 | cancelled = true; |
| 228 | if (timer) clearTimeout(timer); |
| 229 | obs.disconnect(); |
| 230 | }; |
| 231 | }, [tab, pinned]); |
| 232 | |
| 233 | // Inner oscillation for the Layer tab — swap between ddb and d1 |
| 234 | // implementations on a separate timer. Resets to "ddb" whenever we |
| 235 | // leave the tab so the user always sees the Dynamo example first. |
| 236 | useEffect(() => { |
| 237 | if (tab !== "layer") { |
| 238 | setLayerImpl("ddb"); |
| 239 | return; |
| 240 | } |
| 241 | let cancelled = false; |
| 242 | const timer = setInterval(() => { |
| 243 | if (cancelled) return; |
| 244 | setLayerImpl((i) => (i === "ddb" ? "d1" : "ddb")); |
| 245 | }, LAYER_IMPL_SWAP_MS); |
| 246 | return () => { |
| 247 | cancelled = true; |
| 248 | clearInterval(timer); |
| 249 | }; |
| 250 | }, [tab]); |
| 251 | |
| 252 | const onTab = (next: Tab) => { |
| 253 | setPinned(true); |
| 254 | setTab(next); |
| 255 | }; |
| 256 | |
| 257 | const codeHtml = tab === "layer" ? null : highlightTS(CODE_BY_TAB[tab]); |
| 258 | // Drives the fade-in for the whole panel on tab switch. Intentionally |
| 259 | // does NOT include layerImpl — the Layer tab uses CSS transitions to |
| 260 | // breathe between its two impls without remounting. |
| 261 | const splitKey = tab; |
| 262 | |
| 263 | return ( |
| 264 | <div ref={wrapRef} className="eff-showcase" data-nosnippet=""> |
| 265 | <div className="eff-showcase__chrome"> |
| 266 | <div className="eff-showcase__header"> |
| 267 | <span |
| 268 | className="alc-code-block__dot" |
| 269 | style={{ background: "var(--alc-danger)" }} |
| 270 | /> |
| 271 | <span |
| 272 | className="alc-code-block__dot" |
| 273 | style={{ background: "var(--alc-warn)" }} |
| 274 | /> |
| 275 | <span |
| 276 | className="alc-code-block__dot" |
| 277 | style={{ background: "var(--alc-accent-bright)" }} |
| 278 | /> |
| 279 | <div className="eff-showcase__tabs" role="tablist"> |
| 280 | {TABS.map((t) => ( |
| 281 | <button |
| 282 | key={t.id} |
| 283 | type="button" |
| 284 | role="tab" |
| 285 | aria-selected={tab === t.id} |
| 286 | className="eff-showcase__tab" |
| 287 | data-active={tab === t.id} |
| 288 | onClick={() => onTab(t.id)} |
| 289 | > |
| 290 | {t.label} |
| 291 | </button> |
| 292 | ))} |
| 293 | </div> |
| 294 | </div> |
| 295 | |
| 296 | <div className="eff-showcase__body"> |
| 297 | <div className="eff-showcase__split" key={splitKey}> |
| 298 | <div className="eff-showcase__code"> |
| 299 | {tab === "layer" ? ( |
| 300 | <LayerCodeDiff impl={layerImpl} /> |
| 301 | ) : ( |
| 302 | <pre |
| 303 | className="eff-showcase__pre eff-showcase__fade" |
| 304 | dangerouslySetInnerHTML={{ __html: codeHtml ?? "" }} |
| 305 | /> |
| 306 | )} |
| 307 | </div> |
| 308 | <div className="eff-showcase__artifact"> |
| 309 | <ArtifactPanel tab={tab} layerImpl={layerImpl} /> |
| 310 | </div> |
| 311 | </div> |
| 312 | </div> |
| 313 | </div> |
| 314 | <div className="eff-showcase__hint" aria-hidden> |
| 315 | {pinned ? ( |
| 316 | <span>tap a tab to switch</span> |
| 317 | ) : ( |
| 318 | <span>cycling · tap to pin</span> |
| 319 | )} |
| 320 | </div> |
| 321 | </div> |
| 322 | ); |
| 323 | } |
| 324 | |
| 325 | function ArtifactPanel({ tab, layerImpl }: { tab: Tab; layerImpl: LayerImpl }) { |
| 326 | switch (tab) { |
| 327 | case "iam": |
| 328 | return <IamPanel />; |
| 329 | case "stream": |
| 330 | return <StreamPanel />; |
| 331 | case "do": |
| 332 | return <DurableObjectPanel />; |
| 333 | case "container": |
| 334 | return <ContainerPanel />; |
| 335 | case "workflow": |
| 336 | return <WorkflowPanel />; |
| 337 | case "layer": |
| 338 | return <LayerPanel impl={layerImpl} />; |
| 339 | } |
| 340 | } |
| 341 | |
| 342 | /* ───────────────────────── IAM bindings ───────────────────────── */ |
| 343 | |
| 344 | function IamPanel() { |
| 345 | const PERMISSIONS = [ |
| 346 | { icon: "logos:aws-s3", action: "s3:GetObject", target: "Photos/*" }, |
| 347 | { icon: "logos:aws-dynamodb", action: "dynamodb:PutItem", target: "Jobs" }, |
| 348 | ]; |
| 349 | const ENV_VARS = [ |
| 350 | { name: "PHOTOS_BUCKET", value: "arn:aws:s3:::photos-…" }, |
| 351 | { name: "JOBS_TABLE", value: "arn:aws:dynamodb:…:table/Jobs-…" }, |
| 352 | ]; |
| 353 | return ( |
| 354 | <> |
| 355 | <div className="eff-showcase__group"> |
| 356 | <div className="eff-showcase__group-title">Permissions</div> |
| 357 | {PERMISSIONS.map((p, i) => ( |
| 358 | <div |
| 359 | className="eff-showcase__perm" |
| 360 | key={p.action} |
| 361 | style={{ animationDelay: `${120 + i * 120}ms` }} |
| 362 | > |
| 363 | <Icon |
| 364 | icon={p.icon} |
| 365 | width={20} |
| 366 | height={20} |
| 367 | aria-hidden |
| 368 | className="eff-showcase__perm-icon" |
| 369 | /> |
| 370 | <span className="eff-showcase__perm-action">{p.action}</span> |
| 371 | <span className="eff-showcase__perm-arrow" aria-hidden> |
| 372 | → |
| 373 | </span> |
| 374 | <span className="eff-showcase__perm-target">{p.target}</span> |
| 375 | </div> |
| 376 | ))} |
| 377 | </div> |
| 378 | <div className="eff-showcase__group"> |
| 379 | <div className="eff-showcase__group-title">Environment</div> |
| 380 | <div className="eff-showcase__envs"> |
| 381 | {ENV_VARS.map((e, i) => ( |
| 382 | <div |
| 383 | className="eff-showcase__env" |
| 384 | key={e.name} |
| 385 | style={{ animationDelay: `${360 + i * 100}ms` }} |
| 386 | > |
| 387 | <span className="eff-showcase__env-name">{e.name}</span> |
| 388 | <span className="eff-showcase__env-eq" aria-hidden> |
| 389 | = |
| 390 | </span> |
| 391 | <span className="eff-showcase__env-value">{e.value}</span> |
| 392 | </div> |
| 393 | ))} |
| 394 | </div> |
| 395 | </div> |
| 396 | </> |
| 397 | ); |
| 398 | } |
| 399 | |
| 400 | /* ───────────────────────── Event sources ───────────────────────── */ |
| 401 | |
| 402 | function StreamPanel() { |
| 403 | return ( |
| 404 | <div className="eff-showcase__group"> |
| 405 | <div className="eff-showcase__group-title">Wired event source</div> |
| 406 | <div className="eff-showcase__flow"> |
| 407 | <FlowNode |
| 408 | icon="logos:aws-dynamodb" |
| 409 | label="Jobs" |
| 410 | sub="DynamoDB.Table" |
| 411 | delay={120} |
| 412 | /> |
| 413 | <FlowEdge label="stream" delay={260} /> |
| 414 | <FlowNode |
| 415 | icon="logos:aws-lambda" |
| 416 | label="EventSourceMapping" |
| 417 | sub="created by alchemy" |
| 418 | accent |
| 419 | delay={320} |
| 420 | /> |
| 421 | <FlowEdge label="invoke" delay={440} /> |
| 422 | <FlowNode |
| 423 | icon="mdi:lambda" |
| 424 | label="handler" |
| 425 | sub="your function" |
| 426 | delay={500} |
| 427 | /> |
| 428 | </div> |
| 429 | </div> |
| 430 | ); |
| 431 | } |
| 432 | |
| 433 | /* ───────────────────────── Durable Objects ───────────────────────── */ |
| 434 | |
| 435 | function DurableObjectPanel() { |
| 436 | const FEATURES = [ |
| 437 | { |
| 438 | icon: "logos:cloudflare-icon", |
| 439 | label: "Cloudflare.DurableObject", |
| 440 | sub: "one instance per name · single-threaded", |
| 441 | }, |
| 442 | { |
| 443 | icon: "mdi:database", |
| 444 | label: "DurableObjectState", |
| 445 | sub: "per-instance SQLite, persists across restarts", |
| 446 | }, |
| 447 | { |
| 448 | icon: "mdi:transit-connection-variant", |
| 449 | label: "WebSocket sessions", |
| 450 | sub: "hibernation-safe via serializeAttachment", |
| 451 | }, |
| 452 | ]; |
| 453 | return ( |
| 454 | <div className="eff-showcase__group"> |
| 455 | <div className="eff-showcase__group-title">Generated DO</div> |
| 456 | {FEATURES.map((f, i) => ( |
| 457 | <FeatureRow key={f.label} {...f} delay={120 + i * 120} /> |
| 458 | ))} |
| 459 | </div> |
| 460 | ); |
| 461 | } |
| 462 | |
| 463 | /* ───────────────────────── Containers ───────────────────────── */ |
| 464 | |
| 465 | function ContainerPanel() { |
| 466 | const FEATURES = [ |
| 467 | { |
| 468 | icon: "logos:docker-icon", |
| 469 | label: "Image", |
| 470 | sub: "Dockerfile generated · platform builds", |
| 471 | }, |
| 472 | { |
| 473 | icon: "mdi:server", |
| 474 | label: "Instance", |
| 475 | sub: "dev → 256 MB · prod → standard-1", |
| 476 | }, |
| 477 | { |
| 478 | icon: "mdi:console", |
| 479 | label: "exec(cmd)", |
| 480 | sub: "typed Effect · spawns via ChildProcessSpawner", |
| 481 | }, |
| 482 | ]; |
| 483 | return ( |
| 484 | <div className="eff-showcase__group"> |
| 485 | <div className="eff-showcase__group-title">Generated container</div> |
| 486 | {FEATURES.map((f, i) => ( |
| 487 | <FeatureRow key={f.label} {...f} delay={120 + i * 120} /> |
| 488 | ))} |
| 489 | </div> |
| 490 | ); |
| 491 | } |
| 492 | |
| 493 | /* ───────────────────────── Workflows ───────────────────────── */ |
| 494 | |
| 495 | function WorkflowPanel() { |
| 496 | const STEPS = [ |
| 497 | { label: "store", sub: "task · durable retry" }, |
| 498 | { label: "process", sub: "task · pure transform" }, |
| 499 | { label: "broadcast", sub: "task · DO call" }, |
| 500 | { label: "cooldown", sub: "sleep · 2 seconds" }, |
| 501 | { label: "finalize", sub: "task · DO call" }, |
| 502 | ]; |
| 503 | return ( |
| 504 | <div className="eff-showcase__group"> |
| 505 | <div className="eff-showcase__group-title">Workflow steps</div> |
| 506 | <div className="eff-showcase__steps"> |
| 507 | {STEPS.map((s, i) => ( |
| 508 | <div |
| 509 | className="eff-showcase__step" |
| 510 | key={s.label} |
| 511 | style={{ animationDelay: `${100 + i * 90}ms` }} |
| 512 | > |
| 513 | <span className="eff-showcase__step-num">{i + 1}</span> |
| 514 | <span className="eff-showcase__step-label">{s.label}</span> |
| 515 | <span className="eff-showcase__step-sub">{s.sub}</span> |
| 516 | </div> |
| 517 | ))} |
| 518 | </div> |
| 519 | </div> |
| 520 | ); |
| 521 | } |
| 522 | |
| 523 | /* ───────────────────────── Layers ───────────────────────── */ |
| 524 | |
| 525 | function LayerPanel({ impl }: { impl: LayerImpl }) { |
| 526 | // The "Composed from" group overlays the ddb and d1 row-stacks in the |
| 527 | // same grid cell so only the active variant is visible. Same crossfade |
| 528 | // pattern as the code diff on the left. |
| 529 | return ( |
| 530 | <> |
| 531 | <div className="eff-showcase__group"> |
| 532 | <div className="eff-showcase__group-title">Provides</div> |
| 533 | <FeatureRow |
| 534 | icon="mdi:layers-triple" |
| 535 | label="JobStorage" |
| 536 | sub="Context.Service · putJob · getJob" |
| 537 | delay={120} |
| 538 | /> |
| 539 | </div> |
| 540 | <div className="eff-showcase__group"> |
| 541 | <div className="eff-showcase__group-title">Composed from</div> |
| 542 | <div className="eff-showcase__variant-stack"> |
| 543 | <div |
| 544 | className="eff-showcase__variant" |
| 545 | data-active={impl === "ddb"} |
| 546 | aria-hidden={impl !== "ddb"} |
| 547 | > |
| 548 | <FeatureRow |
| 549 | icon="logos:aws-dynamodb" |
| 550 | label="DynamoDB.Table" |
| 551 | sub='"JobsTable" · partitionKey: "id"' |
| 552 | delay={240} |
| 553 | /> |
| 554 | <FeatureRow |
| 555 | icon="mdi:key" |
| 556 | label="GetItem · PutItem" |
| 557 | sub="bound capabilities · IAM auto-generated" |
| 558 | delay={340} |
| 559 | /> |
| 560 | </div> |
| 561 | <div |
| 562 | className="eff-showcase__variant" |
| 563 | data-active={impl === "d1"} |
| 564 | aria-hidden={impl !== "d1"} |
| 565 | > |
| 566 | <FeatureRow |
| 567 | icon="logos:cloudflare-icon" |
| 568 | label="Cloudflare.D1.Database" |
| 569 | sub='"JobsDB" · serverless SQL' |
| 570 | delay={240} |
| 571 | /> |
| 572 | <FeatureRow |
| 573 | icon="mdi:database-cog" |
| 574 | label="D1Connection" |
| 575 | sub="bound capability · prepare · exec · batch" |
| 576 | delay={340} |
| 577 | /> |
| 578 | </div> |
| 579 | </div> |
| 580 | </div> |
| 581 | </> |
| 582 | ); |
| 583 | } |
| 584 | |
| 585 | /** |
| 586 | * Layer-tab code panel. Common lines render statically; each diff segment is |
| 587 | * an overlay (CSS grid with both variants in the same grid cell) so only the |
| 588 | * active variant is visible while the inactive one fades to opacity 0. The |
| 589 | * slot's height is the max of both variants, so the panel stays stable as |
| 590 | * the active impl swaps. |
| 591 | */ |
| 592 | function LayerCodeDiff({ impl }: { impl: LayerImpl }) { |
| 593 | return ( |
| 594 | <div className="eff-showcase__diff"> |
| 595 | {LAYER_SEGMENTS.map((seg, i) => { |
| 596 | if (seg.kind === "common") { |
| 597 | return ( |
| 598 | <div key={i} className="eff-diff__common"> |
| 599 | {seg.lines.map((line, j) => ( |
| 600 | <CodeLine key={j} text={line} kind="common" /> |
| 601 | ))} |
| 602 | </div> |
| 603 | ); |
| 604 | } |
| 605 | return ( |
| 606 | <div key={i} className="eff-diff__slot"> |
| 607 | <div |
| 608 | className="eff-diff__variant" |
| 609 | data-active={impl === "ddb"} |
| 610 | aria-hidden={impl !== "ddb"} |
| 611 | > |
| 612 | {seg.ddb.map((line, j) => ( |
| 613 | <CodeLine key={j} text={line} kind="ddb" /> |
| 614 | ))} |
| 615 | </div> |
| 616 | <div |
| 617 | className="eff-diff__variant" |
| 618 | data-active={impl === "d1"} |
| 619 | aria-hidden={impl !== "d1"} |
| 620 | > |
| 621 | {seg.d1.map((line, j) => ( |
| 622 | <CodeLine key={j} text={line} kind="d1" /> |
| 623 | ))} |
| 624 | </div> |
| 625 | </div> |
| 626 | ); |
| 627 | })} |
| 628 | </div> |
| 629 | ); |
| 630 | } |
| 631 | |
| 632 | function CodeLine({ |
| 633 | text, |
| 634 | kind, |
| 635 | }: { |
| 636 | text: string; |
| 637 | kind: "common" | "ddb" | "d1"; |
| 638 | }) { |
| 639 | const html = highlightTS(text) || "&nbsp;"; |
| 640 | return ( |
| 641 | <div className={`eff-diff__line eff-diff__line--${kind}`}> |
| 642 | <span className="eff-diff__marker" aria-hidden /> |
| 643 | <span |
| 644 | className="eff-diff__content" |
| 645 | dangerouslySetInnerHTML={{ __html: html }} |
| 646 | /> |
| 647 | </div> |
| 648 | ); |
| 649 | } |
| 650 | |
| 651 | /* ───────────────────────── Reusable building blocks ───────────────────────── */ |
| 652 | |
| 653 | function FeatureRow({ |
| 654 | icon, |
| 655 | label, |
| 656 | sub, |
| 657 | delay = 0, |
| 658 | }: { |
| 659 | icon: string; |
| 660 | label: string; |
| 661 | sub: string; |
| 662 | delay?: number; |
| 663 | }) { |
| 664 | return ( |
| 665 | <div |
| 666 | className="eff-showcase__feature" |
| 667 | style={{ animationDelay: `${delay}ms` }} |
| 668 | > |
| 669 | <Icon |
| 670 | icon={icon} |
| 671 | width={22} |
| 672 | height={22} |
| 673 | aria-hidden |
| 674 | className="eff-showcase__feature-icon" |
| 675 | /> |
| 676 | <div className="eff-showcase__feature-text"> |
| 677 | <div className="eff-showcase__feature-label">{label}</div> |
| 678 | <div className="eff-showcase__feature-sub">{sub}</div> |
| 679 | </div> |
| 680 | </div> |
| 681 | ); |
| 682 | } |
| 683 | |
| 684 | function FlowNode({ |
| 685 | icon, |
| 686 | label, |
| 687 | sub, |
| 688 | accent, |
| 689 | delay = 0, |
| 690 | }: { |
| 691 | icon: string; |
| 692 | label: string; |
| 693 | sub: string; |
| 694 | accent?: boolean; |
| 695 | delay?: number; |
| 696 | }) { |
| 697 | return ( |
| 698 | <div |
| 699 | className={`eff-showcase__node${accent ? " eff-showcase__node--accent" : ""}`} |
| 700 | style={{ animationDelay: `${delay}ms` }} |
| 701 | > |
| 702 | <Icon icon={icon} width={22} height={22} aria-hidden /> |
| 703 | <div className="eff-showcase__node-label">{label}</div> |
| 704 | <div className="eff-showcase__node-sub">{sub}</div> |
| 705 | </div> |
| 706 | ); |
| 707 | } |
| 708 | |
| 709 | function FlowEdge({ label, delay = 0 }: { label: string; delay?: number }) { |
| 710 | return ( |
| 711 | <div |
| 712 | className="eff-showcase__edge" |
| 713 | data-label={label} |
| 714 | style={{ animationDelay: `${delay}ms` }} |
| 715 | aria-hidden |
| 716 | /> |
| 717 | ); |
| 718 | } |