Highly ambitious ATProtocol AppView service and sdks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+719 -53
+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>