Highly ambitious ATProtocol AppView service and sdks

update landing page with hero and features showcase, update waitlist avatars, update unauthenticated state on lexicons page to include banner

+90 -1
deno.lock
··· 2 2 "version": "5", 3 3 "specifiers": { 4 4 "jsr:@shikijs/shiki@*": "3.7.0", 5 + "jsr:@std/cli@^1.0.21": "1.0.22", 5 6 "jsr:@std/cli@^1.0.22": "1.0.22", 7 + "jsr:@std/encoding@^1.0.10": "1.0.10", 6 8 "jsr:@std/fmt@^1.0.2": "1.0.8", 9 + "jsr:@std/fmt@^1.0.8": "1.0.8", 10 + "jsr:@std/fs@^1.0.19": "1.0.19", 7 11 "jsr:@std/fs@^1.0.4": "1.0.19", 12 + "jsr:@std/html@^1.0.4": "1.0.4", 8 13 "jsr:@std/http@^1.0.20": "1.0.20", 9 14 "jsr:@std/internal@^1.0.10": "1.0.10", 10 15 "jsr:@std/internal@^1.0.9": "1.0.10", 16 + "jsr:@std/media-types@^1.1.0": "1.1.0", 17 + "jsr:@std/net@^1.0.4": "1.0.5", 11 18 "jsr:@std/path@^1.0.6": "1.1.2", 12 19 "jsr:@std/path@^1.1.1": "1.1.2", 20 + "jsr:@std/streams@^1.0.10": "1.0.11", 13 21 "npm:@shikijs/core@^3.7.0": "3.13.0", 14 22 "npm:@shikijs/engine-oniguruma@^3.7.0": "3.13.0", 23 + "npm:@shikijs/types@^3.7.0": "3.13.0", 24 + "npm:@takumi-rs/core@~0.29.8": "0.29.8", 25 + "npm:@takumi-rs/helpers@~0.29.8": "0.29.8", 15 26 "npm:@types/node@*": "22.15.15", 16 27 "npm:clsx@^2.1.1": "2.1.1", 17 28 "npm:lucide-preact@0.544": "0.544.0_preact@10.27.1", ··· 35 46 "@std/cli@1.0.22": { 36 47 "integrity": "50d1e4f87887cb8a8afa29b88505ab5081188f5cad3985460c3b471fa49ff21a" 37 48 }, 49 + "@std/encoding@1.0.10": { 50 + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" 51 + }, 38 52 "@std/fmt@1.0.8": { 39 53 "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" 40 54 }, ··· 45 59 "jsr:@std/path@^1.1.1" 46 60 ] 47 61 }, 62 + "@std/html@1.0.4": { 63 + "integrity": "eff3497c08164e6ada49b7f81a28b5108087033823153d065e3f89467dd3d50e" 64 + }, 48 65 "@std/http@1.0.20": { 49 - "integrity": "b5cc33fc001bccce65ed4c51815668c9891c69ccd908295997e983d8f56070a1" 66 + "integrity": "b5cc33fc001bccce65ed4c51815668c9891c69ccd908295997e983d8f56070a1", 67 + "dependencies": [ 68 + "jsr:@std/cli@^1.0.21", 69 + "jsr:@std/encoding", 70 + "jsr:@std/fmt@^1.0.8", 71 + "jsr:@std/fs@^1.0.19", 72 + "jsr:@std/html", 73 + "jsr:@std/media-types", 74 + "jsr:@std/net", 75 + "jsr:@std/path@^1.1.1", 76 + "jsr:@std/streams" 77 + ] 50 78 }, 51 79 "@std/internal@1.0.10": { 52 80 "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" 53 81 }, 82 + "@std/media-types@1.1.0": { 83 + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" 84 + }, 85 + "@std/net@1.0.5": { 86 + "integrity": "b759d8c5e17d997e164af6379d57764668c6714f30109685eec0fd5e194d501a" 87 + }, 54 88 "@std/path@1.1.2": { 55 89 "integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038", 56 90 "dependencies": [ 57 91 "jsr:@std/internal@^1.0.10" 58 92 ] 93 + }, 94 + "@std/streams@1.0.11": { 95 + "integrity": "db583d27e28d133f389f1eec318cffdf4998305e5134c1d4b1c56b361cee6018" 59 96 } 60 97 }, 61 98 "npm": { ··· 130 167 }, 131 168 "@shikijs/vscode-textmate@10.0.2": { 132 169 "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==" 170 + }, 171 + "@takumi-rs/core-darwin-arm64@0.29.8": { 172 + "integrity": "sha512-hvDjiKkxClqzV0j03xaSYJ3q7a76zJ4d0SCyI8x4qhZ/5IJzXTKoYMcnr8D3dO9uMrw20CRvwawLh6AD85Auig==", 173 + "os": ["darwin"], 174 + "cpu": ["arm64"] 175 + }, 176 + "@takumi-rs/core-linux-arm64-gnu@0.29.8": { 177 + "integrity": "sha512-uRPnK6etEK7pCd3mUZvGjKfpXfggJjNFLkCVytJFFUcPDFmWQz8XZUhoVe12S2hP5W1jwfatdTAxHwxCMUFytg==", 178 + "os": ["linux"], 179 + "cpu": ["arm64"] 180 + }, 181 + "@takumi-rs/core-linux-arm64-musl@0.29.8": { 182 + "integrity": "sha512-6h+ZahJ33gfxM0zHlyXaoMc6nPEP/r0EOlU69UN2LO85SEG9xbY/BiETPkW+5uNv/99hPmS8KHVt1M5KPaYjNQ==", 183 + "os": ["linux"], 184 + "cpu": ["arm64"] 185 + }, 186 + "@takumi-rs/core-linux-x64-gnu@0.29.8": { 187 + "integrity": "sha512-Z8vMKjuP8DIcb6+XGzcxdeeyTD17RZ+HY3e46zBIySe2D3pd7wNGyDeXMSjn1yCjgOv0FFbec8PojNSlWQI2kw==", 188 + "os": ["linux"], 189 + "cpu": ["x64"] 190 + }, 191 + "@takumi-rs/core-linux-x64-musl@0.29.8": { 192 + "integrity": "sha512-kd13wXY0YMr+kSxENZZB+1EK1uJbfqccEG2+Osc0Jvm+6d2/urksFPAOqQs/SIXVbd4O4yEhxTe5TSQ8IDdKrw==", 193 + "os": ["linux"], 194 + "cpu": ["x64"] 195 + }, 196 + "@takumi-rs/core-win32-arm64-msvc@0.29.8": { 197 + "integrity": "sha512-mALGz9A8VLX25AtaDv/bAxbGYGiHKWFN2tCCCkUxX5njs1Hwn7znFRVoouwlD93A2KRVpTXQSE74bmRiCD4VuA==", 198 + "os": ["win32"], 199 + "cpu": ["arm64"] 200 + }, 201 + "@takumi-rs/core-win32-x64-msvc@0.29.8": { 202 + "integrity": "sha512-t236EXR/DsBWbWpSnZYoR7xgWPU4xZtrCyzbtxiDQhIrLGPzgZ3ZOgkEvhf7ipXGZUv3SGqR1VBzBSHwTPfrTw==", 203 + "os": ["win32"], 204 + "cpu": ["x64"] 205 + }, 206 + "@takumi-rs/core@0.29.8": { 207 + "integrity": "sha512-kCzDirdGu0namxZPn9ul6B0Lt5a4BI4EuRsV2Zj2dTzfFRTGIl/Cwi8by+lnxuuhZ7s++GpB6z7M3kPBEugOww==", 208 + "optionalDependencies": [ 209 + "@takumi-rs/core-darwin-arm64", 210 + "@takumi-rs/core-linux-arm64-gnu", 211 + "@takumi-rs/core-linux-arm64-musl", 212 + "@takumi-rs/core-linux-x64-gnu", 213 + "@takumi-rs/core-linux-x64-musl", 214 + "@takumi-rs/core-win32-arm64-msvc", 215 + "@takumi-rs/core-win32-x64-msvc" 216 + ] 217 + }, 218 + "@takumi-rs/helpers@0.29.8": { 219 + "integrity": "sha512-a9jfiqcjaUVkTaMN9IKtCJtJzWZdv9LXSFWtaOM8EwTruyjk7sSJVjlYKM26a1XRPPZaWZGBN+4EQT4EgCAsUA==" 133 220 }, 134 221 "@ts-morph/common@0.27.0": { 135 222 "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", ··· 563 650 "jsr:@slices/client@~0.1.0-alpha.3", 564 651 "jsr:@std/assert@^1.0.14", 565 652 "jsr:@std/http@^1.0.20", 653 + "npm:@takumi-rs/core@~0.29.8", 654 + "npm:@takumi-rs/helpers@~0.29.8", 566 655 "npm:clsx@^2.1.1", 567 656 "npm:lucide-preact@0.544", 568 657 "npm:preact-render-to-string@^6.5.13",
+4 -2
frontend/deno.json
··· 4 4 "dev": "deno run -A --env-file=.env --watch src/main.ts" 5 5 }, 6 6 "compilerOptions": { 7 - "jsx": "precompile", 7 + "jsx": "react-jsx", 8 8 "jsxImportSource": "preact" 9 9 }, 10 10 "imports": { ··· 19 19 "@std/http": "jsr:@std/http@^1.0.20", 20 20 "clsx": "npm:clsx@^2.1.1", 21 21 "tailwind-merge": "npm:tailwind-merge@^2.5.5", 22 - "lucide-preact": "npm:lucide-preact@^0.544.0" 22 + "lucide-preact": "npm:lucide-preact@^0.544.0", 23 + "@takumi-rs/helpers": "npm:@takumi-rs/helpers@^0.29.8", 24 + "@takumi-rs/core": "npm:@takumi-rs/core@^0.29.8" 23 25 } 24 26 }
frontend/src/assets/grain-dashboard-dark-web.png

This is a binary file and will not be displayed.

frontend/src/assets/grain-dashboard-dark.png

This is a binary file and will not be displayed.

frontend/src/assets/grain-dashboard-web.png

This is a binary file and will not be displayed.

frontend/src/assets/grain-dashboard.png

This is a binary file and will not be displayed.

frontend/src/assets/grain-lexicon-dark-web.png

This is a binary file and will not be displayed.

frontend/src/assets/grain-lexicon-dark.png

This is a binary file and will not be displayed.

frontend/src/assets/grain-lexicon-web.png

This is a binary file and will not be displayed.

frontend/src/assets/grain-lexicon.png

This is a binary file and will not be displayed.

frontend/src/assets/grain-oauth-dark-web.png

This is a binary file and will not be displayed.

frontend/src/assets/grain-oauth-dark.png

This is a binary file and will not be displayed.

frontend/src/assets/grain-oauth-web.png

This is a binary file and will not be displayed.

frontend/src/assets/grain-oauth.png

This is a binary file and will not be displayed.

+58 -6
frontend/src/features/landing/handlers.tsx
··· 2 2 import { renderHTML } from "../../utils/render.tsx"; 3 3 import { withAuth } from "../../routes/middleware.ts"; 4 4 import { LandingPage } from "./templates/LandingPage.tsx"; 5 + import { LandingOGImage } from "./templates/fragments/LandingOGImage.tsx"; 5 6 import { publicClient } from "../../config.ts"; 6 7 import { getTimeline } from "../../lib/api.ts"; 8 + import { fromJsx } from "@takumi-rs/helpers/jsx"; 9 + import { Renderer } from "@takumi-rs/core"; 7 10 8 11 async function handleLandingPage(req: Request): Promise<Response> { 9 12 const context = await withAuth(req); 13 + const url = new URL(req.url); 10 14 11 15 // Fetch timeline slices 12 16 const slices = await getTimeline(publicClient, 20); 13 17 18 + // Build OG image URL 19 + const ogImageUrl = `${url.origin}/og-image`; 20 + 14 21 return renderHTML( 15 - <LandingPage 16 - currentUser={context.currentUser} 17 - slices={slices} 18 - />, 22 + await LandingPage({ 23 + currentUser: context.currentUser, 24 + slices: slices, 25 + ogImage: ogImageUrl, 26 + }), 19 27 { 20 - title: "Slice - AT Protocol Data Management Platform", 28 + title: "Slice - Build AT Protocol AppViews in minutes, not months", 21 29 description: 22 - "Build, manage, and integrate with AT Protocol data effortlessly. Create custom lexicons, sync records, and generate TypeScript clients.", 30 + "The complete backend platform for AT Protocol developers. Deploy schemas, query indexed data, authenticate users. Everything you need to ship your AppView.", 23 31 }, 24 32 ); 25 33 } 26 34 35 + async function handleLandingOGImage(req: Request): Promise<Response> { 36 + try { 37 + // Set up fonts for Takumi 38 + const fontBuffer = await Deno.readFile("./src/fonts/InterVariable.ttf"); 39 + 40 + const fonts = [ 41 + { 42 + name: "Inter", 43 + data: fontBuffer, 44 + style: "normal" as const, 45 + weight: 400, 46 + }, 47 + ]; 48 + 49 + const renderer = new Renderer({ 50 + //@ts-ignore Takumi types are wrong for some reason 51 + fonts, 52 + }); 53 + 54 + // Generate landing page OG image using Takumi 55 + const node = await fromJsx(<LandingOGImage />); 56 + 57 + // Render to PNG using Takumi 58 + const pngBuffer = await renderer.renderAsync(node, { 59 + width: 1200, 60 + height: 630, 61 + format: "png", 62 + }); 63 + 64 + return new Response(pngBuffer, { 65 + headers: { 66 + "Content-Type": "image/png", 67 + "Cache-Control": "public, max-age=3600", 68 + }, 69 + }); 70 + } catch (error) { 71 + return new Response(`Error generating image: ${error.message}`, { status: 500 }); 72 + } 73 + } 74 + 27 75 export const landingRoutes: Route[] = [ 28 76 { 29 77 pattern: new URLPattern({ pathname: "/" }), 30 78 handler: handleLandingPage, 79 + }, 80 + { 81 + pattern: new URLPattern({ pathname: "/og-image" }), 82 + handler: handleLandingOGImage, 31 83 }, 32 84 ];
+302 -3
frontend/src/features/landing/templates/LandingPage.tsx
··· 1 1 import { Layout } from "../../../shared/fragments/Layout.tsx"; 2 2 import { PageHeader } from "../../../shared/fragments/PageHeader.tsx"; 3 3 import { SliceCard } from "../../../shared/fragments/SliceCard.tsx"; 4 + import { Button } from "../../../shared/fragments/Button.tsx"; 5 + import { Text } from "../../../shared/fragments/Text.tsx"; 6 + import { codeToHtml } from "jsr:@shikijs/shiki"; 4 7 import type { AuthenticatedUser } from "../../../routes/middleware.ts"; 5 8 import type { NetworkSlicesSliceDefsSliceView } from "../../../client.ts"; 9 + import { BarChart3, RotateCcw, Users, Search } from "lucide-preact"; 6 10 7 11 interface LandingPageProps { 8 12 currentUser?: AuthenticatedUser; 9 13 slices?: NetworkSlicesSliceDefsSliceView[]; 14 + ogImage?: string; 10 15 } 11 16 12 - export function LandingPage({ 17 + export async function LandingPage({ 13 18 currentUser, 14 19 slices = [], 20 + ogImage, 15 21 }: LandingPageProps = {}) { 22 + // Generate code example for Type-Safe APIs feature 23 + const codeExample = `// Generated from your lexicon 24 + import { client } from './generated-client'; 25 + 26 + // Type-safe queries with IntelliSense 27 + const resp = await client.com.recordcollector.album.getRecords({ 28 + where: { 29 + artist: { eq: "Nirvana" }, 30 + genre: { contains: "grunge" }, 31 + condition: { in: ["Mint", "Near Mint"] } 32 + }, 33 + sortBy: [{ field: "releaseDate", direction: "desc" }], 34 + limit: 50 35 + }); 36 + 37 + // Fully typed response 38 + resp.records.forEach(album => { 39 + console.log(album.value.title); // ✅ TypeScript knows this exists 40 + console.log(album.value.artist); // ✅ Autocomplete works 41 + console.log(album.value.invalidField); // ❌ Type error! 42 + });`; 43 + 44 + const highlightedCode = await codeToHtml(codeExample, { 45 + lang: "typescript", 46 + themes: { 47 + light: "github-light", 48 + dark: "github-dark", 49 + }, 50 + }); 16 51 return ( 17 52 <Layout 18 - title="Slice - AT Protocol Data Management Platform" 19 - description="Build, manage, and integrate with AT Protocol data effortlessly. Create custom lexicons, sync records, and generate TypeScript clients." 53 + title="Slice - Build AT Protocol AppViews in minutes, not months" 54 + description="The complete backend platform for AT Protocol developers. Deploy schemas, query indexed data, authenticate users. Everything you need to ship your AppView." 55 + ogImage={ogImage} 20 56 currentUser={currentUser} 21 57 > 22 58 <div className="px-4 py-8"> 59 + {/* Hero Section */} 60 + <div className="text-center mb-16"> 61 + <Text as="h1" size="3xl" className="text-4xl md:text-6xl font-bold text-zinc-900 dark:text-white mb-6"> 62 + Build AT Protocol AppViews<a href="https://bsky.app/profile/pfrazee.com/post/3lyucxfxq622w" className="text-blue-600 dark:text-blue-400 text-4xl align-super no-underline hover:underline" target="_blank" rel="noopener noreferrer">*</a><br /> 63 + <span className="text-blue-600 dark:text-blue-400">in minutes, not months.</span> 64 + </Text> 65 + <Text as="p" size="xl" variant="secondary" className="mb-8 max-w-2xl mx-auto"> 66 + The complete backend platform for{" "} 67 + <a href="https://atproto.com" className="text-blue-600 dark:text-blue-400 underline">AT Protocol</a> developers. 68 + Deploy schemas, query indexed data, authenticate users. 69 + </Text> 70 + <Text as="p" size="lg" variant="muted" className="mb-8 max-w-3xl mx-auto"> 71 + Skip the infrastructure. Focus on your app logic. Everything you need 72 + to ship production AppViews with type-safe APIs and automatic indexing. 73 + </Text> 74 + <Button variant="blue" size="lg" href="/waitlist"> 75 + Ship your AppView → 76 + </Button> 77 + </div> 78 + 79 + {/* Features Section */} 80 + <div className="mb-16"> 81 + <div className="max-w-6xl mx-auto"> 82 + 83 + {/* Feature 1: Auto-Indexing Engine */} 84 + <div className="grid md:grid-cols-2 gap-12 items-center mb-20"> 85 + <div> 86 + <Text as="h2" size="3xl" className="font-bold text-zinc-900 dark:text-white mb-4"> 87 + Auto-Indexing Engine 88 + </Text> 89 + <Text as="p" size="xl" className="text-blue-600 dark:text-blue-400 mb-6"> 90 + Real-time data sync from the AT Protocol network. 91 + </Text> 92 + <Text as="p" size="lg" variant="muted" className="leading-relaxed"> 93 + Automatically discover and index records matching your lexicons. Connected to the firehose 94 + to keep your data fresh. 95 + </Text> 96 + </div> 97 + <div className="bg-zinc-100 dark:bg-zinc-800 rounded-lg overflow-hidden border border-zinc-200 dark:border-zinc-700"> 98 + <img 99 + src="/static/grain-dashboard-web.png?v=2" 100 + alt="Grain dashboard showing auto-indexing features with 4,400 records, 8 collections, and 254 actors" 101 + className="w-full h-auto block dark:hidden" 102 + /> 103 + <img 104 + src="/static/grain-dashboard-dark-web.png?v=2" 105 + alt="Grain dashboard showing auto-indexing features with 4,400 records, 8 collections, and 254 actors" 106 + className="w-full h-auto hidden dark:block" 107 + /> 108 + </div> 109 + </div> 110 + 111 + {/* Feature 2: Type-Safe Collection APIs */} 112 + <div className="grid md:grid-cols-2 gap-12 items-center mb-20"> 113 + <div className="bg-zinc-50 dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-700 md:order-first overflow-hidden"> 114 + <div 115 + className="text-sm overflow-x-auto [&_pre]:p-4 [&_pre]:m-0 [&_pre]:min-w-full [&_pre]:w-max" 116 + dangerouslySetInnerHTML={{ __html: highlightedCode }} 117 + /> 118 + </div> 119 + <div> 120 + <Text as="h2" size="3xl" className="font-bold text-zinc-900 dark:text-white mb-4"> 121 + Type-Safe APIs 122 + </Text> 123 + <Text as="p" size="xl" className="text-blue-600 dark:text-blue-400 mb-6"> 124 + Generated clients with collection methods. 125 + </Text> 126 + <Text as="p" size="lg" variant="muted" className="leading-relaxed"> 127 + From lexicon to production code in seconds. Get fully-typed getRecords(), createRecord(), 128 + updateRecord(), and deleteRecord() methods with filtering, sorting, and pagination. 129 + Complete with project templates for popular frameworks and CLI tools to scaffold your AppView. 130 + Complex infrastructure made simple. 131 + </Text> 132 + </div> 133 + </div> 134 + 135 + {/* Feature 3: Lexicon Management */} 136 + <div className="grid md:grid-cols-2 gap-12 items-center mb-20"> 137 + <div> 138 + <Text as="h2" size="3xl" className="font-bold text-zinc-900 dark:text-white mb-4"> 139 + Schema Management 140 + </Text> 141 + <Text as="p" size="xl" className="text-blue-600 dark:text-blue-400 mb-6"> 142 + Define once, query everywhere. 143 + </Text> 144 + <Text as="p" size="lg" variant="muted" className="leading-relaxed"> 145 + Configure lexicons that instantly become queryable collections. Built-in 146 + validation ensures your schemas work correctly from day one. Deploy and 147 + start querying immediately. 148 + </Text> 149 + </div> 150 + <div className="bg-zinc-100 dark:bg-zinc-800 rounded-lg overflow-hidden border border-zinc-200 dark:border-zinc-700"> 151 + <img 152 + src="/static/grain-lexicon-web.png?v=2" 153 + alt="Grain lexicon editor showing schema definitions and validation" 154 + className="w-full h-auto block dark:hidden" 155 + /> 156 + <img 157 + src="/static/grain-lexicon-dark-web.png?v=2" 158 + alt="Grain lexicon editor showing schema definitions and validation" 159 + className="w-full h-auto hidden dark:block" 160 + /> 161 + </div> 162 + </div> 163 + 164 + {/* Feature 4: OAuth Integration */} 165 + <div className="grid md:grid-cols-2 gap-12 items-center mb-20"> 166 + <div className="bg-zinc-100 dark:bg-zinc-800 rounded-lg overflow-hidden border border-zinc-200 dark:border-zinc-700 md:order-first"> 167 + <img 168 + src="/static/grain-oauth-web.png?v=2" 169 + alt="Grain OAuth client configuration showing token management and authentication flows" 170 + className="w-full h-auto block dark:hidden" 171 + /> 172 + <img 173 + src="/static/grain-oauth-dark-web.png?v=2" 174 + alt="Grain OAuth client configuration showing token management and authentication flows" 175 + className="w-full h-auto hidden dark:block" 176 + /> 177 + </div> 178 + <div> 179 + <Text as="h2" size="3xl" className="font-bold text-zinc-900 dark:text-white mb-4"> 180 + User Authentication 181 + </Text> 182 + <Text as="p" size="xl" className="text-blue-600 dark:text-blue-400 mb-6"> 183 + OAuth flows that just work. 184 + </Text> 185 + <Text as="p" size="lg" variant="muted" className="leading-relaxed"> 186 + Production-ready authentication with OAuth 2.0 PKCE for web apps and 187 + Device Code Auth for CLI tools. Automatic token management, refresh handling, 188 + and secure session storage. Focus on features, not auth infrastructure. 189 + </Text> 190 + </div> 191 + </div> 192 + 193 + </div> 194 + </div> 195 + 196 + {/* Production-Ready Operations */} 197 + <div className="mb-16"> 198 + <div className="text-center mb-12"> 199 + <Text as="h2" size="2xl" className="font-bold text-zinc-900 dark:text-white mb-4"> 200 + Production-Ready Operations 201 + </Text> 202 + <Text as="p" size="lg" variant="secondary" className="max-w-2xl mx-auto"> 203 + Everything you need to monitor, manage, and scale your AppView in production. 204 + </Text> 205 + </div> 206 + 207 + <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-6xl mx-auto"> 208 + {/* Jetstream Logs */} 209 + <div className="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700"> 210 + <div className="w-8 h-8 text-blue-600 dark:text-blue-400 mb-3"> 211 + <BarChart3 size={32} /> 212 + </div> 213 + <Text as="h3" size="lg" className="font-semibold text-zinc-900 dark:text-white mb-2"> 214 + Jetstream Logs 215 + </Text> 216 + <Text variant="muted" size="sm"> 217 + Real-time connection monitoring and detailed event logs for debugging and observability. 218 + </Text> 219 + </div> 220 + 221 + {/* Data Backfill */} 222 + <div className="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700"> 223 + <div className="w-8 h-8 text-green-600 dark:text-green-400 mb-3"> 224 + <RotateCcw size={32} /> 225 + </div> 226 + <Text as="h3" size="lg" className="font-semibold text-zinc-900 dark:text-white mb-2"> 227 + Data Backfill 228 + </Text> 229 + <Text variant="muted" size="sm"> 230 + Historical data synchronization and migration tools to populate your AppView from scratch. 231 + </Text> 232 + </div> 233 + 234 + {/* Waitlist Management */} 235 + <div className="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700"> 236 + <div className="w-8 h-8 text-orange-600 dark:text-orange-400 mb-3"> 237 + <Users size={32} /> 238 + </div> 239 + <Text as="h3" size="lg" className="font-semibold text-zinc-900 dark:text-white mb-2"> 240 + Waitlist Management 241 + </Text> 242 + <Text variant="muted" size="sm"> 243 + User onboarding and access control workflows for managing early access and beta testing. 244 + </Text> 245 + </div> 246 + 247 + {/* Record Explorer */} 248 + <div className="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700"> 249 + <div className="w-8 h-8 text-purple-600 dark:text-purple-400 mb-3"> 250 + <Search size={32} /> 251 + </div> 252 + <Text as="h3" size="lg" className="font-semibold text-zinc-900 dark:text-white mb-2"> 253 + Record Explorer 254 + </Text> 255 + <Text variant="muted" size="sm"> 256 + Visual query interface with advanced filtering, search, and data exploration capabilities. 257 + </Text> 258 + </div> 259 + 260 + </div> 261 + </div> 262 + 263 + {/* Social Platform Section */} 264 + <div className="mb-16"> 265 + <div className="text-center mb-12"> 266 + <Text as="h2" size="2xl" className="font-bold text-zinc-900 dark:text-white mb-4"> 267 + Built for Collaboration 268 + </Text> 269 + <Text as="p" size="lg" variant="secondary" className="max-w-2xl mx-auto"> 270 + Share lexicons, discover AppViews, and learn from the community timeline. 271 + </Text> 272 + </div> 273 + 274 + <div className="grid md:grid-cols-3 gap-8 max-w-4xl mx-auto"> 275 + {/* Community Timeline */} 276 + <div className="text-center"> 277 + <div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center mx-auto mb-4"> 278 + <div className="w-6 h-6 text-blue-600 dark:text-blue-400"> 279 + <BarChart3 size={24} /> 280 + </div> 281 + </div> 282 + <Text as="h3" size="lg" className="font-semibold text-zinc-900 dark:text-white mb-2"> 283 + Community Timeline 284 + </Text> 285 + <Text variant="muted" size="sm"> 286 + See what other developers are building. Get inspired by new lexicons and AppView implementations. 287 + </Text> 288 + </div> 289 + 290 + {/* Lexicon Sharing */} 291 + <div className="text-center"> 292 + <div className="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-lg flex items-center justify-center mx-auto mb-4"> 293 + <div className="w-6 h-6 text-green-600 dark:text-green-400"> 294 + <Users size={24} /> 295 + </div> 296 + </div> 297 + <Text as="h3" size="lg" className="font-semibold text-zinc-900 dark:text-white mb-2"> 298 + Lexicon Discovery 299 + </Text> 300 + <Text variant="muted" size="sm"> 301 + Browse and fork community lexicons. Build on proven schemas instead of starting from scratch. 302 + </Text> 303 + </div> 304 + 305 + {/* Knowledge Sharing */} 306 + <div className="text-center"> 307 + <div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center mx-auto mb-4"> 308 + <div className="w-6 h-6 text-purple-600 dark:text-purple-400"> 309 + <Search size={24} /> 310 + </div> 311 + </div> 312 + <Text as="h3" size="lg" className="font-semibold text-zinc-900 dark:text-white mb-2"> 313 + Learn & Share 314 + </Text> 315 + <Text variant="muted" size="sm"> 316 + Documentation, tutorials, and best practices shared by the community. Level up your AT Protocol skills. 317 + </Text> 318 + </div> 319 + </div> 320 + </div> 321 + 23 322 <PageHeader title="Timeline" /> 24 323 25 324 {slices.length > 0 ? (
+150
frontend/src/features/landing/templates/fragments/LandingOGImage.tsx
··· 1 + // @ts-nocheck 2 + interface LandingOGImageProps { 3 + title?: string; 4 + subtitle?: string; 5 + } 6 + 7 + export const LandingOGImage = ({ 8 + title = "Build AT Protocol AppViews", 9 + subtitle = "in minutes, not months.", 10 + }: LandingOGImageProps) => ( 11 + <div 12 + style={{ 13 + width: "100%", 14 + height: "100%", 15 + backgroundColor: "#18181b", // zinc-900 (dark mode background) 16 + display: "flex", 17 + flexDirection: "column", 18 + padding: "80px", 19 + fontFamily: "Inter", 20 + color: "#ffffff", 21 + justifyContent: "center", 22 + }} 23 + > 24 + {/* Slices logo */} 25 + <div 26 + style={{ 27 + display: "flex", 28 + alignItems: "center", 29 + gap: "16px", 30 + marginBottom: "48px", 31 + }} 32 + > 33 + <svg 34 + viewBox="0 0 60 60" 35 + style={{ width: "48px", height: "48px" }} 36 + xmlns="http://www.w3.org/2000/svg" 37 + > 38 + <defs> 39 + <linearGradient id="board1" x1="0%" y1="0%" x2="100%" y2="100%"> 40 + <stop offset="0%" style={{ stopColor: "#FF6347", stopOpacity: 1 }} /> 41 + <stop offset="100%" style={{ stopColor: "#FF4500", stopOpacity: 1 }} /> 42 + </linearGradient> 43 + <linearGradient id="board2" x1="0%" y1="0%" x2="100%" y2="100%"> 44 + <stop offset="0%" style={{ stopColor: "#00CED1", stopOpacity: 1 }} /> 45 + <stop offset="100%" style={{ stopColor: "#4682B4", stopOpacity: 1 }} /> 46 + </linearGradient> 47 + </defs> 48 + <g transform="translate(30, 30)"> 49 + <ellipse cx="0" cy="-12" rx="25" ry="8" fill="url(#board1)" /> 50 + <ellipse cx="0" cy="0" rx="28" ry="8" fill="url(#board2)" /> 51 + <ellipse cx="0" cy="12" rx="22" ry="8" fill="#32CD32" /> 52 + </g> 53 + </svg> 54 + <div 55 + style={{ 56 + color: "#60a5fa", // blue-400 (dark mode blue) 57 + fontSize: "28px", 58 + fontWeight: "bold", 59 + letterSpacing: "2px", 60 + }} 61 + > 62 + Slices 63 + </div> 64 + </div> 65 + 66 + {/* Main headline */} 67 + <div 68 + style={{ 69 + fontSize: "64px", 70 + fontWeight: "bold", 71 + color: "#ffffff", 72 + lineHeight: 1.1, 73 + marginBottom: "24px", 74 + }} 75 + > 76 + {title} 77 + </div> 78 + 79 + {/* Subtitle */} 80 + <div 81 + style={{ 82 + fontSize: "48px", 83 + fontWeight: "bold", 84 + color: "#60a5fa", // blue-400 (dark mode blue) 85 + marginBottom: "64px", 86 + }} 87 + > 88 + {subtitle} 89 + </div> 90 + 91 + {/* Feature highlights */} 92 + <div 93 + style={{ 94 + display: "flex", 95 + flexDirection: "row", 96 + gap: "48px", 97 + fontSize: "18px", 98 + color: "#a1a1aa", // zinc-400 99 + }} 100 + > 101 + <div style={{ display: "flex", alignItems: "center", gap: "12px" }}> 102 + <div 103 + style={{ 104 + width: "8px", 105 + height: "8px", 106 + backgroundColor: "#10b981", // emerald-500 107 + borderRadius: "50%", 108 + }} 109 + /> 110 + <span>Auto-Indexing</span> 111 + </div> 112 + <div style={{ display: "flex", alignItems: "center", gap: "12px" }}> 113 + <div 114 + style={{ 115 + width: "8px", 116 + height: "8px", 117 + backgroundColor: "#f59e0b", // amber-500 118 + borderRadius: "50%", 119 + }} 120 + /> 121 + <span>Type-Safe APIs</span> 122 + </div> 123 + <div style={{ display: "flex", alignItems: "center", gap: "12px" }}> 124 + <div 125 + style={{ 126 + width: "8px", 127 + height: "8px", 128 + backgroundColor: "#8b5cf6", // violet-500 129 + borderRadius: "50%", 130 + }} 131 + /> 132 + <span>OAuth Integration</span> 133 + </div> 134 + </div> 135 + 136 + {/* URL */} 137 + <div 138 + style={{ 139 + position: "absolute", 140 + bottom: "40px", 141 + right: "80px", 142 + fontSize: "16px", 143 + color: "#71717a", // zinc-500 144 + fontFamily: "Inter", 145 + }} 146 + > 147 + @slices.network 148 + </div> 149 + </div> 150 + );
+21 -1
frontend/src/features/slices/lexicon/templates/SliceLexiconPage.tsx
··· 2 2 import { EmptyState } from "../../../../shared/fragments/EmptyState.tsx"; 3 3 import { Button } from "../../../../shared/fragments/Button.tsx"; 4 4 import { Card } from "../../../../shared/fragments/Card.tsx"; 5 + import { Text } from "../../../../shared/fragments/Text.tsx"; 5 6 import { LexiconsList } from "./fragments/LexiconsList.tsx"; 6 - import { FileCode } from "lucide-preact"; 7 + import { FileCode, Info } from "lucide-preact"; 7 8 import type { AuthenticatedUser } from "../../../../routes/middleware.ts"; 8 9 import type { 9 10 NetworkSlicesSliceDefsSliceView, ··· 26 27 currentUser, 27 28 hasSliceAccess, 28 29 }: SliceLexiconPageProps) { 30 + const banner = !hasSliceAccess ? ( 31 + <div className="mb-6 bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700"> 32 + <div className="w-8 h-8 text-blue-600 dark:text-blue-400 mb-3"> 33 + <Info size={32} /> 34 + </div> 35 + <Text as="h3" size="lg" className="font-semibold text-zinc-900 dark:text-white mb-2"> 36 + Full Slice Profiles Coming Soon 37 + </Text> 38 + <Text as="p" variant="muted" size="sm" className="mb-4"> 39 + Want to see metrics, sync status, and API endpoints? Get early access when they launch. 40 + </Text> 41 + <Button variant="blue" size="sm" href="/waitlist"> 42 + Join waitlist 43 + </Button> 44 + </div> 45 + ) : null; 46 + 29 47 return ( 30 48 <SlicePage 31 49 slice={slice} ··· 34 52 currentUser={currentUser} 35 53 hasSliceAccess={hasSliceAccess} 36 54 title={`${slice.name} - Lexicons`} 55 + banner={banner} 37 56 > 38 57 <div> 39 58 {hasSliceAccess && ( ··· 73 92 )} 74 93 </Card.Content> 75 94 </Card> 95 + 76 96 </div> 77 97 78 98 <div id="modal-container"></div>
+4
frontend/src/features/slices/shared/fragments/SlicePage.tsx
··· 13 13 hasSliceAccess?: boolean; 14 14 title?: string; 15 15 headerActions?: preact.ComponentChildren; 16 + banner?: preact.ComponentChildren; 16 17 children: preact.ComponentChildren; 17 18 } 18 19 ··· 24 25 hasSliceAccess, 25 26 title, 26 27 headerActions, 28 + banner, 27 29 children, 28 30 }: SlicePageProps) { 29 31 const pageTitle = title || slice.name; ··· 46 48 <PageHeader title={slice.name}> 47 49 {headerActions} 48 50 </PageHeader> 51 + 52 + {banner} 49 53 50 54 {currentTab && ( 51 55 <SliceTabs
+2 -2
frontend/src/features/waitlist/handlers.tsx
··· 19 19 if (SLICE_URI) { 20 20 try { 21 21 recentRequests = await getHydratedWaitlistRequests(publicClient, SLICE_URI); 22 - // Limit to most recent 50 and reverse to show newest first 23 - recentRequests = recentRequests.slice(0, 50); 22 + // Limit to most recent 10 and reverse to show newest first 23 + recentRequests = recentRequests.slice(0, 10); 24 24 } catch (error) { 25 25 console.error("Failed to fetch recent waitlist requests:", error); 26 26 // Continue without recent requests if fetch fails
+2 -19
frontend/src/features/waitlist/templates/fragments/WaitlistForm.tsx
··· 3 3 import { Card } from "../../../../shared/fragments/Card.tsx"; 4 4 import { Text } from "../../../../shared/fragments/Text.tsx"; 5 5 import { FlashMessage } from "../../../../shared/fragments/FlashMessage.tsx"; 6 - import { ActorAvatar } from "../../../../shared/fragments/ActorAvatar.tsx"; 6 + import { AvatarStack } from "../../../../shared/fragments/AvatarStack.tsx"; 7 7 import type { NetworkSlicesWaitlistDefsRequestView } from "../../../../client.ts"; 8 8 9 9 interface WaitlistFormProps { ··· 38 38 <Text as="p" size="sm" variant="muted" className="mb-3"> 39 39 Join {recentRequests.length} others who are waiting 40 40 </Text> 41 - <div className="flex flex-wrap justify-center gap-1"> 42 - {recentRequests.slice(0, 20).map((request, index) => ( 43 - <div key={index} className="relative"> 44 - <ActorAvatar 45 - profile={request.profile || { handle: "user" }} 46 - size={24} 47 - className="border border-white dark:border-zinc-800" 48 - /> 49 - </div> 50 - ))} 51 - {recentRequests.length > 20 && ( 52 - <div className="w-6 h-6 bg-zinc-100 dark:bg-zinc-800 border border-white dark:border-zinc-800 rounded-full flex items-center justify-center"> 53 - <Text size="xs" variant="muted"> 54 - +{recentRequests.length - 20} 55 - </Text> 56 - </div> 57 - )} 58 - </div> 41 + <AvatarStack requests={recentRequests} maxDisplay={20} size={24} /> 59 42 </div> 60 43 )} 61 44
+2 -19
frontend/src/features/waitlist/templates/fragments/WaitlistSuccess.tsx
··· 2 2 import { Card } from "../../../../shared/fragments/Card.tsx"; 3 3 import { Text } from "../../../../shared/fragments/Text.tsx"; 4 4 import { Link } from "../../../../shared/fragments/Link.tsx"; 5 - import { ActorAvatar } from "../../../../shared/fragments/ActorAvatar.tsx"; 5 + import { AvatarStack } from "../../../../shared/fragments/AvatarStack.tsx"; 6 6 import { Check } from "lucide-preact"; 7 7 import type { NetworkSlicesWaitlistDefsRequestView } from "../../../../client.ts"; 8 8 ··· 47 47 <Text as="p" size="sm" variant="muted" className="mb-3"> 48 48 You've joined {recentRequests.length} others 49 49 </Text> 50 - <div className="flex flex-wrap justify-center gap-1"> 51 - {recentRequests.slice(0, 30).map((request, index) => ( 52 - <div key={index} className="relative"> 53 - <ActorAvatar 54 - profile={request.profile || { handle: "user" }} 55 - size={32} 56 - className="border border-white dark:border-zinc-800" 57 - /> 58 - </div> 59 - ))} 60 - {recentRequests.length > 30 && ( 61 - <div className="w-8 h-8 bg-zinc-100 dark:bg-zinc-800 border border-white dark:border-zinc-800 rounded-full flex items-center justify-center"> 62 - <Text size="xs" variant="muted"> 63 - +{recentRequests.length - 30} 64 - </Text> 65 - </div> 66 - )} 67 - </div> 50 + <AvatarStack requests={recentRequests} maxDisplay={30} size={32} /> 68 51 </div> 69 52 )} 70 53
frontend/src/fonts/InterVariable.ttf

This is a binary file and will not be displayed.

+4
frontend/src/routes/mod.ts
··· 18 18 } from "../features/slices/mod.ts"; 19 19 import { settingsRoutes } from "../features/settings/handlers.tsx"; 20 20 import { docsRoutes } from "../features/docs/handlers.tsx"; 21 + import { staticRoutes } from "./static.ts"; 21 22 22 23 export const allRoutes: Route[] = [ 24 + // Static file serving (must come first to avoid conflicts) 25 + ...staticRoutes, 26 + 23 27 // Landing page (public, no auth required) 24 28 ...landingRoutes, 25 29
+22
frontend/src/routes/static.ts
··· 1 + import type { Route } from "@std/http/unstable-route"; 2 + import { serveDir } from "@std/http/file-server"; 3 + 4 + async function handleStatic(req: Request): Promise<Response> { 5 + const { pathname } = new URL(req.url); 6 + 7 + if (pathname.startsWith("/static/")) { 8 + return serveDir(req, { 9 + fsRoot: "./src/assets", 10 + urlRoot: "static", 11 + }); 12 + } 13 + 14 + return new Response("Not Found", { status: 404 }); 15 + } 16 + 17 + export const staticRoutes: Route[] = [ 18 + { 19 + pattern: new URLPattern({ pathname: "/static/*" }), 20 + handler: handleStatic, 21 + }, 22 + ];
+54
frontend/src/shared/fragments/AvatarStack.tsx
··· 1 + import { ActorAvatar } from "./ActorAvatar.tsx"; 2 + import { Text } from "./Text.tsx"; 3 + import { cn } from "../../utils/cn.ts"; 4 + import type { NetworkSlicesWaitlistDefsRequestView } from "../../client.ts"; 5 + 6 + interface AvatarStackProps { 7 + requests: NetworkSlicesWaitlistDefsRequestView[]; 8 + maxDisplay?: number; 9 + size?: number; 10 + className?: string; 11 + } 12 + 13 + export function AvatarStack({ 14 + requests, 15 + maxDisplay = 20, 16 + size = 24, 17 + className, 18 + }: AvatarStackProps) { 19 + const displayedRequests = requests.slice(0, maxDisplay); 20 + const remainingCount = requests.length - maxDisplay; 21 + const overlapClass = size === 32 ? "-ml-2.5" : "-ml-2"; 22 + 23 + return ( 24 + <div className={cn("flex justify-center", className)}> 25 + {displayedRequests.map((request, index) => ( 26 + <div 27 + key={index} 28 + className={cn( 29 + "relative transition-transform duration-200 hover:scale-110 hover:z-10", 30 + index > 0 && overlapClass 31 + )} 32 + > 33 + <ActorAvatar 34 + profile={request.profile || { handle: "user" }} 35 + size={size} 36 + className="border-2 border-white dark:border-zinc-800 hover:border-blue-500 dark:hover:border-blue-400 transition-colors duration-200" 37 + /> 38 + </div> 39 + ))} 40 + {remainingCount > 0 && ( 41 + <div 42 + className={cn( 43 + "bg-zinc-100 dark:bg-zinc-800 border-2 border-white dark:border-zinc-800 rounded-full flex items-center justify-center transition-transform duration-200 hover:scale-110 hover:z-10", 44 + size === 32 ? "w-8 h-8 -ml-2.5" : "w-6 h-6 -ml-2" 45 + )} 46 + > 47 + <Text size="xs" variant="muted"> 48 + +{remainingCount} 49 + </Text> 50 + </div> 51 + )} 52 + </div> 53 + ); 54 + }
+4
frontend/src/shared/fragments/Layout.tsx
··· 7 7 interface LayoutProps { 8 8 title?: string; 9 9 description?: string; 10 + ogImage?: string; 10 11 children: JSX.Element | JSX.Element[]; 11 12 currentUser?: AuthenticatedUser; 12 13 showNavigation?: boolean; ··· 18 19 export function Layout({ 19 20 title = "Slices", 20 21 description = "AT Protocol data management platform", 22 + ogImage, 21 23 children, 22 24 currentUser, 23 25 showNavigation = true, ··· 43 45 <meta property="og:type" content="website" /> 44 46 <meta property="og:title" content={title} /> 45 47 <meta property="og:description" content={description} /> 48 + {ogImage && <meta property="og:image" content={ogImage} />} 46 49 47 50 {/* Twitter */} 48 51 <meta property="twitter:card" content="summary_large_image" /> 49 52 <meta property="twitter:title" content={title} /> 50 53 <meta property="twitter:description" content={description} /> 54 + {ogImage && <meta property="twitter:image" content={ogImage} />} 51 55 <script src="https://unpkg.com/htmx.org@1.9.10"></script> 52 56 <script src="https://unpkg.com/hyperscript.org@0.9.12"></script> 53 57 <script src="https://cdn.tailwindcss.com/3.4.1"></script>