diff --git a/.github/ISSUE_TEMPLATE/ecosystem-submission.yml b/.github/ISSUE_TEMPLATE/ecosystem-submission.yml new file mode 100644 index 0000000..64b08b7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ecosystem-submission.yml @@ -0,0 +1,70 @@ +name: Ecosystem Submission +description: Submit your app to the Livepeer ecosystem directory +title: "Add " +labels: ["ecosystem"] +body: + - type: markdown + attributes: + value: | + ## Ecosystem App Submission + Thanks for building on Livepeer! Please fill in the details below so we can review and add your app to the ecosystem directory. + + - type: input + id: app-name + attributes: + label: App Name + placeholder: "e.g., My Livepeer App" + validations: + required: true + + - type: input + id: website + attributes: + label: Website URL + placeholder: "https://myapp.com" + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: A short description of what your app does and how it uses Livepeer. + placeholder: "Our app uses Livepeer for..." + validations: + required: true + + - type: textarea + id: categories + attributes: + label: Categories + description: "Select all that apply: AI Video, Streaming, Developer Tools" + placeholder: "AI Video, Streaming" + validations: + required: true + + - type: input + id: contact + attributes: + label: Contact Email + description: We'll only use this to follow up on your submission. + placeholder: "you@example.com" + validations: + required: true + + - type: textarea + id: logo + attributes: + label: Logo + description: Please drag and drop your app logo here (SVG or PNG, square, min 128x128px). + placeholder: "Drag and drop your logo image here..." + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Anything else? + description: Optional — anything we should know about your app or a category you think is missing. + validations: + required: false diff --git a/.github/workflows/check-ecosystem-urls.yml b/.github/workflows/check-ecosystem-urls.yml new file mode 100644 index 0000000..20a1d13 --- /dev/null +++ b/.github/workflows/check-ecosystem-urls.yml @@ -0,0 +1,24 @@ +name: Check Ecosystem URLs + +on: + # Run on PRs that touch ecosystem data + pull_request: + paths: + - "data/ecosystem.json" + - "public/ecosystem/**" + + # Weekly check — Mondays at 9am UTC + schedule: + - cron: "0 9 * * 1" + + # Manual trigger + workflow_dispatch: + +jobs: + check-urls: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check ecosystem URLs + run: node scripts/check-ecosystem-urls.mjs diff --git a/.gitignore b/.gitignore index 07b773a..0cf75ca 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ brand_guidelines/ img/ *.pdf .claude/ +.codex +.vscode/ +*.code-workspace +.playwright-mcp/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8f1866a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/CLAUDE.md b/CLAUDE.md index a465d11..ddaf2a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,6 +3,7 @@ **Stack:** Next.js 15, React 19, TypeScript, Tailwind CSS v4, Framer Motion 11. Package manager: npm. No test framework. **Environment variables** (set in Vercel / `.env.local`): + - `MAILCHIMP_API_KEY` — Mailchimp API key (early access signups) - `MAILCHIMP_AUDIENCE_ID` — Mailchimp audience/list ID - `MAILCHIMP_TAG` — Mailchimp tag applied to new subscribers (default: "v2 Website Signups") @@ -29,7 +30,7 @@ ### `components/` -- **`home/`** — self-contained homepage sections. Render order in `app/page.tsx`: Hero, Capabilities, WhatIsLivepeer, WhyLivepeer, BuiltOnLivepeer, NetworkStats, DeveloperCTA, CommunityCTA. (`NetworkParticipants.tsx` exists but is not currently rendered.) +- **`home/`** — self-contained homepage sections. Render order in `app/page.tsx`: Hero, BuiltOnLivepeer, CommunityCTA. (`NetworkParticipants.tsx` exists but is not currently rendered.) - **`layout/`** — Header, Footer. Header has headroom behavior on `/primer` (hides on scroll down, reveals on scroll up). - **`ui/`** — shared primitives (Button, Card, Container, SectionHeader, Badge, ImageMask, GlowOverlay, EarlyAccessCTA, etc.). Reuse these; don't create new wrappers for the same purpose. Also contains canvas components: `GenerativeCanvas.tsx` (GLSL shader), `LiveNetwork.tsx` (Canvas 2D particle trails), `AiVideoHero.tsx` (Sobel edge detection on video texture). All canvas components follow `useEffect` + `useRef` + `requestAnimationFrame` + cleanup. - **`blog/`** — blog listing page (`BlogListingClient`, `BlogCategoryFilter`, `BlogPostCard`) and post detail (`BlogPostHeader`, `BlogPostContent`). @@ -80,6 +81,7 @@ The site's signature visual is a layered grid system that combines B&W video/imagery, geometric shapes, animated particle trails, and liquid glass effects. This creates an "outer space control room" aesthetic — technical, cinematic, and distinctly Livepeer. **Layer stack (bottom to top):** + 1. **Media layer** — B&W video or image with green tint, darkened (`ImageMask`) 2. **Tile grid** — 9-column square grid with 1px white borders, overlaid on the media 3. **Geometric shapes** — circles, crosshairs, and a starburst node positioned at grid intersections @@ -101,6 +103,7 @@ The site's signature visual is a layered grid system that combines B&W video/ima Full spec in `brand-tokens.md` — colors, typography, logo rules, greyscale ramp, gradients, graphic elements. **Theme tokens** (`globals.css` `@theme`): + - Colors: `green`, `green-light`, `green-dark`, `green-bright`, `green-subtle`, `blue`, `blue-light`, `blue-bright`, `blue-dark`, `blue-subtle`, `dark`, `dark-lighter`, `dark-card`, `dark-border` - Fonts: `--font-sans` (Favorit Pro — Light/Book/Regular/Medium/Bold), `--font-mono` (Favorit Mono — Regular/Medium/Bold) @@ -114,70 +117,111 @@ Dark theme only — except `/primer`, which uses a light theme override with scr ## Messaging -### Positioning - -- **Canonical headline:** "Open infrastructure for real-time AI video" -- **Tagline (footer):** "The world's open video infrastructure." -- Lead with AI video, not transcoding. Transcoding is a capability, not the identity. -- The network IS the product. Daydream, Livepeer Studio, Streamplace, Embody are built ON it — they are ecosystem proof, not Livepeer products. -- **Network vs. Gateway:** The Livepeer Network is the open protocol (orchestrators, delegators, staking, distributed GPU compute). The Livepeer Gateway is the developer-facing product layer on top — API keys, request routing, billing, SLA enforcement. "Build on Livepeer" = use the Gateway. "Run the network" = be an orchestrator/delegator. -- **Differentiation:** real-time, frame-by-frame AI inference on live video — not batch processing, not generic GPU compute. Centralized GPU clouds are optimized for batch/static and structurally disadvantaged here. -- **Three demand drivers:** AI-generated worlds/games, real-time video analysis, AI-mediated avatars/agents. +### Positioning (v2 Thesis — March 2026) + +- **Thesis statement:** "Livepeer is the open network for GPU-powered video." +- **Canonical headline:** "The open network for GPU-powered video" +- **Tagline (footer):** "The open network for GPU-powered video." +- Lead with solutions (builds on the network), not raw protocol capabilities. +- The network provides GPU infrastructure. Solutions provide the product experience. +- **Three competitive variables (cost / capability / community):** + - **Cost:** 60–85% cheaper than centralized alternatives (AWS, RunPod, Fal). [FLAG: pending validation with Rick — do not scale to "10x" without confirmed data] + - **Capability:** Specialized for real-time, streaming video inference. Nine years of video processing optimization. BYOC flexibility for custom models and pipelines. No other network is built specifically for this. + - **Community:** The network is operated by independent orchestrators, expanded by builders building on builders, and open to permissionless participation. This is a structural property of the infrastructure — not a Discord server or governance forum. A centralized provider structurally cannot replicate this. +- Not a trilemma — cost, capability, and community interact. +- **External competitive frame:** Livepeer solutions (powered by community-operated GPU infrastructure) vs. centralized alternatives (AWS, RunPod, Fal, Replicate) for GPU-powered video. Name competitors explicitly — the target audience is comparison-shopping. +- **Internal only (not for website copy):** DePIN comparisons (Theta, Render, Akash). These are relevant for internal strategy but not for developer-facing positioning. +- **Narrative routing principle:** Every piece of external communication should route audiences to solutions on the network. The question the website answers: "What's being built on Livepeer, and how can I use it?" +- **Solutions on the network:** Daydream (real-time generative video), Frameworks (sovereign live streaming), Streamplace (decentralized social video), Embody (AI avatars). These are the ecosystem, not Livepeer products. +- **CTA pattern:** All CTAs point to Discord ("Build with Livepeer"). No email capture. ### Voice - Confident, technical but accessible, no-nonsense. -- Lead with what the network does and why it matters. Use concrete numbers (10x cost reduction, <1s latency, 100K+ GPUs). +- Lead with what builders can do on the network and why it matters. Use concrete numbers where validated. +- Name competitors directly — credibility comes from honest comparison, not avoidance. +- Be honest about constraints. The network has ~100 AI-capable GPUs, estimated 90–95% uptime, no formal SLAs. Don't oversell what doesn't exist yet. - Explain technical concepts simply on first reference. -- **Avoid:** "revolutionary," "game-changing," empty superlatives, hype language, "web3 bro" tone, lorem ipsum. +- **Avoid:** "revolutionary," "game-changing," empty superlatives, hype language, "web3 bro" tone, "decentralized" as a selling point, "the platform," lorem ipsum. ### Terminology -| Use | Don't use | -|-----|-----------| -| "the network" | "the platform," "the service" | -| "open infrastructure" | "decentralized infrastructure" (too web3-coded) | -| "GPU providers" (dev-facing) | "nodes" (too generic) | -| "orchestrators" (protocol/network context) | "miners," "validators" (wrong mental model) | -| "gateway" | "API endpoint" (gateway is the product concept) | -| "delegators" | "stakers" (delegators is the protocol term) | -| "frames," "streams" | "requests," "calls" (video-native language) | -| "inference" | "processing" (when referring to AI specifically) | -| "Livepeer Gateway" | "NaaP" (internal-only project name), "Livepeer platform" (too generic) | +| Use | Don't use | +| ------------------------------------------ | ----------------------------------------------------- | +| "the network" | "the platform," "the service" | +| "open network" | "decentralized infrastructure" (too web3-coded) | +| "solutions" | "builds" (internal only), "gateways," "tools," "DGEs" | +| "GPU providers" (dev-facing) | "nodes" (too generic) | +| "orchestrators" (protocol/network context) | "miners," "validators" (wrong mental model) | +| "inference" | "processing" (when referring to AI specifically) | +| "GPU-powered video" | "real-time AI video" (previous headline, now retired) | + +### What the v2 thesis kills (do not use this framing) + +- ~~"The network is the product."~~ → The network is infrastructure. Solutions are the products. +- ~~"Livepeer is a generalized GPU compute network."~~ → It's specialized. GPU-powered video. +- ~~"We need to attract developers to the raw protocol."~~ → Route audiences to solutions. +- ~~"Enterprise is the near-term market."~~ → The edges are. +- ~~"Centralized vs. decentralized."~~ → The tension is centralized-proprietary vs. community-operated/open. +- ~~"10x cost reduction."~~ → 60–85% cheaper. [FLAG: pending Rick validation] +- ~~"100K+ GPUs."~~ → ~100 AI-capable GPUs currently. Don't inflate supply numbers. ## Strategic Context -This website is a redesign of livepeer.org to reflect Livepeer's repositioning from decentralized video transcoding to **infrastructure purpose-built for real-time AI video**, guided by the **Cascade vision**. As of late 2025, 70%+ of network fees come from AI inference (not transcoding), with ~3x YoY fee growth — AI is already the primary economic driver. Core strengths: low-latency video pipelines, distributed GPU operator network, open stake-coordinated execution, permissionless protocol on public blockchain. +This website reflects Livepeer's v2 positioning as a specialized GPU network for real-time video inference. The thesis organizes everything around a three-layer stack: supply (GPUs contributed by orchestrators), protocol (routing that matches workloads to GPUs), and demand (builds that package network capabilities for their audiences). -### Livepeer Gateway (internally "NaaP") +### The Stack -"NaaP" is an **internal project name** for the canonical open-source gateway and developer platform. **Do not surface "NaaP" as user-facing terminology on the website.** The user-facing name is **Livepeer Gateway**. +``` +┌─────────────────────────────────────────────┐ +│ DEMAND (top) │ +│ Builds. Products and businesses on the │ +│ protocol that serve their own audiences. │ +├─────────────────────────────────────────────┤ +│ PROTOCOL (middle) │ +│ Routing. Orchestrator discovery, job │ +│ routing, staking, delegation, payments. │ +├─────────────────────────────────────────────┤ +│ SUPPLY (bottom) │ +│ GPUs. Physical compute contributed by │ +│ independent orchestrators. │ +└─────────────────────────────────────────────┘ +``` -The 2026 focus is preparing the network for scalable demand. This platform is the developer-facing product layer — a "single pane of glass" for core network services. Stewarded by the Livepeer Foundation. +### Current Builds (the ecosystem) -**Product principles:** Observable (transparent network data, LLM/agent-readable), Performant (production-grade real-time execution), User-Centric (developers who drive demand). +- **Scope / Daydream** — Creative AI tools for real-time video. ~30-person creative technologist cohort. Demand R&D partner, not primary demand driver. +- **Frameworks (Marco)** — Live streaming infrastructure. Bare-metal video pipelines on the network. +- **Embody (George)** — Embodied AI avatars. ~70% dev time on integration infrastructure. +- **Streamplace (Eli)** — Open-source video infrastructure for decentralized social (AT Protocol). -**Priority personas:** -1. **App Developers** — build demand-generating apps. "Get an API key and go." -2. **Service Providers** — build plugins for app devs: billing, analytics, CDN. -3. **Orchestrators** — provide GPU compute by meeting SLAs. +### Target Audience: The Edges -### Daydream +The near-term audience is the edges — builders drawn to frontier, specialized infrastructure for emerging use cases. They tolerate constraints (reliability, polish) because cost, capability, or community alignment makes their work possible. This is not the enterprise market. -Flagship product and design partner. Translates the real-time AI video thesis into developer workflows and generates early production demand. -- **Scope**: open-source local tool for designing real-time AI video workflows (integrates with TouchDesigner, Unity, Unreal) -- **Daydream API**: remote inference on Livepeer GPU backends -- **Community Hub**: workflow discovery and collaboration +Three segments: creative technologists (enter through Scope), early-stage AI video builders (enter through whichever build fits), orchestrator-entrepreneurs (enter through the network itself). -### 2026 Roadmap Phases +### 2026 Foundation Priorities -1. **Now:** Drive network performance — observability, reliability, security; expand dev tooling; remove barriers to production use -2. **Next:** Accelerate network GTM — convert readiness into demand; design partners, onboarding, ecosystem support -3. **Beyond:** Scale enterprise adoption — higher demand, GPU supply expansion, enterprise-grade ops +1. Route audiences to builds — the ecosystem page is the website's primary routing surface +2. Validate edges audience motivations through builder discovery and audience research +3. Ship case studies proving cost, capability, and community claims before they become headlines +4. Grow the builder ecosystem — the network's demand story is the builds' story ### Website Design Direction -- **AI-first positioning** — lead with real-time AI video, not legacy transcoding +- **Specialized, not generic** — lead with video inference specialization, not "open infrastructure" - **Brand source of truth:** Holographic Agency brand guidelines (see `brand-tokens.md`) - **Design references:** Vercel, Linear, Stripe, Raycast — clean, developer-focused, premium feel -- **Target audience:** Developers building real-time AI/video applications +- **Primary audience:** Builders evaluating GPU infrastructure for real-time video AI applications +- **Ecosystem page is the primary routing surface** — every visitor should be able to answer "what's being built here and how do I use it?" +- **Honest about constraints.** Don't mock up product UI that doesn't exist. The CTA is "Build with Livepeer" (Discord) — match the copy to that reality. + +### Open Flags + +These claims require validation before shipping as headlines. Use [FLAG] markers in copy: + +- 60–85% cost claim — needs Rick validation against current network pricing +- "Agents can discover and pay directly" — pending Rick's architecture confirmation +- Framework case study — blocked on Marco discovery conversation +- Specific GPU/node counts — verify current numbers before publishing diff --git a/README.md b/README.md index 78fc862..f861bc1 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ Open [http://localhost:3000](http://localhost:3000) in your browser. ## Scripts -| Command | Description | -| --------------- | ------------------------------------ | -| `npm run dev` | Start development server | -| `npm run build` | Create production build | -| `npm run start` | Serve production build | -| `npm run lint` | Run ESLint | +| Command | Description | +| --------------- | ------------------------ | +| `npm run dev` | Start development server | +| `npm run build` | Create production build | +| `npm run start` | Serve production build | +| `npm run lint` | Run ESLint | ## Project Structure diff --git a/app/api/early-access/route.ts b/app/api/early-access/route.ts index 57b9a07..9a0abda 100644 --- a/app/api/early-access/route.ts +++ b/app/api/early-access/route.ts @@ -12,10 +12,7 @@ export async function POST(request: Request) { const { email } = await request.json(); if (!email || typeof email !== "string") { - return NextResponse.json( - { error: "Email is required" }, - { status: 400 } - ); + return NextResponse.json({ error: "Email is required" }, { status: 400 }); } // Add member to audience (or update if already exists) @@ -50,9 +47,6 @@ export async function POST(request: Request) { return NextResponse.json({ success: true }); } catch (error) { console.error("Early access signup failed:", error); - return NextResponse.json( - { error: "Failed to subscribe" }, - { status: 500 } - ); + return NextResponse.json({ error: "Failed to subscribe" }, { status: 500 }); } } diff --git a/app/blog/layout.tsx b/app/blog/layout.tsx index 8e3ee9f..218cb31 100644 --- a/app/blog/layout.tsx +++ b/app/blog/layout.tsx @@ -17,6 +17,10 @@ export const metadata: Metadata = { }, }; -export default function BlogLayout({ children }: { children: React.ReactNode }) { +export default function BlogLayout({ + children, +}: { + children: React.ReactNode; +}) { return children; } diff --git a/app/blog/opengraph-image.tsx b/app/blog/opengraph-image.tsx index 56ba8ab..e8ecdad 100644 --- a/app/blog/opengraph-image.tsx +++ b/app/blog/opengraph-image.tsx @@ -12,126 +12,152 @@ export default async function OGImage() { ); return new ImageResponse( - ( +
+ {/* Centered radial glow */}
- {/* Centered radial glow */} -
+ /> - {/* Secondary glow — bottom right for depth */} -
+ {/* Secondary glow — bottom right for depth */} +
- {/* Subtle grid overlay */} -
+ {/* Subtle grid overlay */} +
- {/* Lockup (symbol + LIVEPEER text) — 560px wide */} - - - - - - - - - - - - - - - - + {/* Lockup (symbol + LIVEPEER text) — 560px wide */} + + + + + + + + + + + + + + + + - {/* Tagline */} -
- The latest in Livepeer -
+ {/* Tagline */} +
+ The latest in Livepeer +
- {/* Accent line */} -
+ {/* Accent line */} +
- {/* Bottom domain */} -
- livepeer.org -
+ {/* Bottom domain */} +
+ livepeer.org
- ), +
, { ...size, fonts: [ diff --git a/app/blog/page.tsx b/app/blog/page.tsx index ec9e0dc..6f5876d 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -1,6 +1,7 @@ import Container from "@/components/ui/Container"; import SectionHeader from "@/components/ui/SectionHeader"; import BlogListingClient from "@/components/blog/BlogListingClient"; +import PageHero from "@/components/ui/PageHero"; import { getAllPosts, getCategories } from "@/lib/blog"; export default function BlogPage() { @@ -8,7 +9,7 @@ export default function BlogPage() { const categories = getCategories(); return ( -
+
- + ); } diff --git a/app/brand/page.tsx b/app/brand/page.tsx index b23cfee..89848ca 100644 --- a/app/brand/page.tsx +++ b/app/brand/page.tsx @@ -1,7 +1,12 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import { motion, animate, useMotionValue, useMotionValueEvent } from "framer-motion"; +import { + motion, + animate, + useMotionValue, + useMotionValueEvent, +} from "framer-motion"; import Container from "@/components/ui/Container"; import ImageMask from "@/components/ui/ImageMask"; import { @@ -324,9 +329,7 @@ function ColorSwatch({ {name}

- {token && ( -

{token}

- )} + {token &&

{token}

}
); } @@ -586,8 +589,8 @@ export default function BrandPage() { Brand Guidelines

- Resources for presenting the Livepeer brand consistently - and professionally. + Resources for presenting the Livepeer brand consistently and + professionally.

@@ -600,7 +603,6 @@ export default function BrandPage() { background: "linear-gradient(to bottom, transparent, #121212)", }} /> - {/* Section 2: Logo */} @@ -641,7 +643,6 @@ export default function BrandPage() {
  • - Clear Space - {" "}— Minimum clear space equals the width of the symbol on all sides. + + Clear Space + {" "} + — Minimum clear space equals the width of the symbol on all + sides.
  • - Placement - {" "}— Primary: top-left or bottom-left. Secondary: top-right, bottom-right, or center. + + Placement + {" "} + — Primary: top-left or bottom-left. Secondary: top-right, + bottom-right, or center.
  • - Avatars - {" "}— Green gradient bg + white symbol, black bg + white symbol, or white bg + black symbol. + + Avatars + {" "} + — Green gradient bg + white symbol, black bg + white symbol, + or white bg + black symbol.
- @@ -905,10 +909,7 @@ export default function BrandPage() {
{greyscale.map((c) => (
-
+

{c.hex} @@ -1135,8 +1136,8 @@ export default function BrandPage() {

- The Livepeer network is open infrastructure for real-time - AI video. Build applications powered by decentralized + The Livepeer network is open infrastructure for real-time AI + video. Build applications powered by decentralized transcoding, streaming, and AI processing.

@@ -1150,8 +1151,8 @@ export default function BrandPage() {

- Supporting text, descriptions, and secondary content use - the smaller body size with reduced opacity. + Supporting text, descriptions, and secondary content use the + smaller body size with reduced opacity.

@@ -1183,9 +1184,7 @@ export default function BrandPage() {

93%

-

- Favorit Pro Bold -

+

Favorit Pro Bold

@@ -1194,9 +1193,7 @@ export default function BrandPage() {

120%

-

- Favorit Mono Bold -

+

Favorit Mono Bold

@@ -1205,9 +1202,7 @@ export default function BrandPage() {

100%

-

- Favorit Pro Medium -

+

Favorit Pro Medium

@@ -1232,12 +1227,19 @@ export default function BrandPage() { Holographik Grid System

- The site's signature visual combines B&W video/imagery, a 9-column square tile grid, geometric shapes, animated particle trails, and liquid glass effects into a layered “control room” aesthetic. + The site's signature visual combines B&W video/imagery, a + 9-column square tile grid, geometric shapes, animated particle + trails, and liquid glass effects into a layered “control + room” aesthetic.

{/* Layer stack — visual demo */} - +

Layer Stack (bottom to top)

@@ -1277,7 +1279,7 @@ export default function BrandPage() { />

- 1{" "}Media + 1 Media

@@ -1297,7 +1299,7 @@ export default function BrandPage() { />

- 2{" "}Tile Grid + 2 Tile Grid

@@ -1359,7 +1361,7 @@ export default function BrandPage() { ))}

- 3{" "}Shapes + 3 Shapes

@@ -1465,7 +1467,7 @@ export default function BrandPage() { />

- ={" "}Combined + = Combined

@@ -1560,7 +1562,6 @@ export default function BrandPage() { - ); } diff --git a/app/ecosystem/page.tsx b/app/ecosystem/page.tsx new file mode 100644 index 0000000..f334979 --- /dev/null +++ b/app/ecosystem/page.tsx @@ -0,0 +1,351 @@ +"use client"; + +import { Suspense, useState, useMemo, useEffect, useRef, useCallback } from "react"; +import { Search, Plus, ArrowUpRight } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useSearchParams, useRouter, usePathname } from "next/navigation"; +import { ECOSYSTEM_APPS, ECOSYSTEM_CATEGORIES } from "@/lib/ecosystem-data"; +import PageHero from "@/components/ui/PageHero"; +import Container from "@/components/ui/Container"; +import SectionHeader from "@/components/ui/SectionHeader"; +import Badge from "@/components/ui/Badge"; +import Button from "@/components/ui/Button"; +import FilterPill from "@/components/ui/FilterPill"; + +const BATCH_SIZE = 12; + +/* Collect tags grouped by category */ +const TAGS_BY_CATEGORY: Record = {}; +for (const app of ECOSYSTEM_APPS) { + for (const cat of app.categories) { + if (!TAGS_BY_CATEGORY[cat]) TAGS_BY_CATEGORY[cat] = []; + for (const tag of app.tags ?? []) { + if (!TAGS_BY_CATEGORY[cat].includes(tag)) { + TAGS_BY_CATEGORY[cat].push(tag); + } + } + } +} + +/* Categories that have tags get a dropdown chevron */ +const CATEGORIES_WITH_TAGS = ECOSYSTEM_CATEGORIES.filter( + (cat) => cat !== "All" && TAGS_BY_CATEGORY[cat]?.length > 0, +); + +function EcosystemPageInner() { + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + + const [activeCategories, setActiveCategories] = useState(() => { + const param = searchParams.get("categories"); + return param ? param.split(",").map(decodeURIComponent) : []; + }); + const [activeTags, setActiveTags] = useState(() => { + const param = searchParams.get("tags"); + return param ? param.split(",").map(decodeURIComponent) : []; + }); + const [search, setSearch] = useState(() => searchParams.get("q") ?? ""); + const [visible, setVisible] = useState(BATCH_SIZE); + const [buttonBatch, setButtonBatch] = useState(0); + const sentinelRef = useRef(null); + + const isAllActive = activeCategories.length === 0; + const loadMore = useCallback(() => setVisible((v) => v + BATCH_SIZE), []); + const handleButtonLoad = useCallback(() => { + setButtonBatch(visible); + loadMore(); + }, [visible, loadMore]); + + /* Sync filter state → URL query params */ + useEffect(() => { + const params = new URLSearchParams(); + if (activeCategories.length > 0) + params.set("categories", activeCategories.join(",")); + if (activeTags.length > 0) params.set("tags", activeTags.join(",")); + if (search) params.set("q", search); + + const qs = params.toString(); + const url = qs ? `${pathname}?${qs}` : pathname; + router.replace(url, { scroll: false }); + }, [activeCategories, activeTags, search, pathname, router]); + + useEffect(() => { + setVisible(BATCH_SIZE); + }, [activeCategories, activeTags, search]); + + const handleCategoryToggle = (cat: string) => { + if (cat === "All") { + setActiveCategories([]); + setActiveTags([]); + return; + } + setActiveCategories((prev) => { + const next = prev.includes(cat) + ? prev.filter((c) => c !== cat) + : [...prev, cat]; + if (next.length === 0) setActiveTags([]); + return next; + }); + }; + + const handleTagToggle = (tag: string) => { + setActiveTags((prev) => + prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag], + ); + }; + + const filtered = useMemo(() => { + return ECOSYSTEM_APPS.filter((app) => { + const matchesCategory = + isAllActive || activeCategories.some((c) => app.categories.includes(c)); + const matchesTags = + activeTags.length === 0 || + activeTags.some((t) => app.tags?.includes(t)); + const matchesSearch = + !search || + app.name.toLowerCase().includes(search.toLowerCase()) || + app.description.toLowerCase().includes(search.toLowerCase()); + return matchesCategory && matchesTags && matchesSearch; + }); + }, [activeCategories, activeTags, search, isAllActive]); + + const shown = filtered.slice(0, visible); + const hasMore = visible < filtered.length; + + // Infinite scroll with IntersectionObserver on mobile, "View more" button on desktop. + useEffect(() => { + const el = sentinelRef.current; + if (!el || !hasMore) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) loadMore(); + }, + { rootMargin: "200px" }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, [hasMore, loadMore]); + + return ( + +
+ + + + Submit App + + } + /> + + {/* Filter bar */} +
+
+ handleCategoryToggle("All")} + /> + {CATEGORIES_WITH_TAGS.map((cat) => ( + handleCategoryToggle(cat)} + dropdown={ + TAGS_BY_CATEGORY[cat]?.length > 0 + ? { + items: TAGS_BY_CATEGORY[cat], + activeItems: activeTags, + onItemToggle: handleTagToggle, + } + : undefined + } + /> + ))} +
+ +
+ + setSearch(e.target.value)} + className="w-full rounded-md border border-white/[0.12] bg-white/[0.03] backdrop-blur-sm py-1.5 pl-9 pr-8 text-sm text-white/60 placeholder:text-white/30 transition-colors duration-200 focus:bg-white/[0.05] focus:border-white/20 focus:outline-none sm:w-56 select-none" + /> + + {search && ( + setSearch("")} + className="absolute right-2.5 top-1/2 -translate-y-1/2 cursor-pointer text-white/50 transition-colors hover:text-white/80" + aria-label="Clear search" + > + + + + + )} + +
+
+ + {/* App grid */} + {shown.length > 0 ? ( +
+ {shown.map((app, index) => { + const inButtonBatch = + buttonBatch >= 0 && index >= buttonBatch && index < buttonBatch + BATCH_SIZE; + return ( + +
+
+ {app.logo ? ( + {`${app.name} + ) : ( + + {app.name.charAt(0)} + + )} +
+ +
+

+ {app.name} +

+

+ {app.hostname} +

+

+ {app.description} +

+
+ {app.categories.map((cat) => ( + + {cat} + + ))} + {app.tags?.map((tag) => ( + + {tag} + + ))} +
+
+ ); + })} +
+ ) : ( +
+

+ No results found{search ? ` for \u201c${search}\u201d` : ""} +

+

+ Try searching for another term. + +

+ +
+ )} + + {hasMore && ( +
+ {/* Infinite scroll on mobile, "View more" button on desktop */} + + )} + +
+ + ); +} + +export default function EcosystemPage() { + return ( + + + + ); +} diff --git a/app/ecosystem/submit/page.tsx b/app/ecosystem/submit/page.tsx new file mode 100644 index 0000000..fa30df7 --- /dev/null +++ b/app/ecosystem/submit/page.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { ECOSYSTEM_CATEGORIES } from "@/lib/ecosystem-data"; +import PageHero from "@/components/ui/PageHero"; +import Container from "@/components/ui/Container"; +import Button from "@/components/ui/Button"; +import FilterPill from "@/components/ui/FilterPill"; + +const CATEGORIES = ECOSYSTEM_CATEGORIES.filter((c) => c !== "All"); + +export default function SubmitAppPage() { + const [name, setName] = useState(""); + const [url, setUrl] = useState(""); + const [description, setDescription] = useState(""); + const [categories, setCategories] = useState([]); + const [email, setEmail] = useState(""); + + const handleSubmit = (e: React.SyntheticEvent) => { + e.preventDefault(); + + const issueUrl = new URL("https://github.com/livepeer/naap/issues/new"); + issueUrl.searchParams.set("template", "ecosystem-submission.yml"); + issueUrl.searchParams.set("title", `Add ${name}`); + issueUrl.searchParams.set("app-name", name); + issueUrl.searchParams.set("website", url); + issueUrl.searchParams.set("description", description); + issueUrl.searchParams.set("categories", categories.join(", ")); + issueUrl.searchParams.set("contact", email); + + window.open(issueUrl.toString(), "_blank"); + }; + + const isValid = name && url && description && categories.length > 0 && email; + + return ( + + +
+ + Ecosystem + + + Submit +
+

+ Submit your app +

+

+ Built something on Livepeer? Submit your app to be featured in the + ecosystem directory. This will open a GitHub issue for review. +

+ +
+
+ + setName(e.target.value)} + placeholder="My Livepeer App" + className="w-full rounded-lg border border-dark-border bg-dark-card px-4 py-3 text-sm text-white placeholder:text-white/20 focus:border-white/20 focus:outline-none" + /> +
+ +
+ + setUrl(e.target.value)} + placeholder="https://myapp.com" + className="w-full rounded-lg border border-dark-border bg-dark-card px-4 py-3 text-sm text-white placeholder:text-white/20 focus:border-white/20 focus:outline-none" + /> +
+ +
+ +