From cf21a2fde3a2aaf6c69542525eedfd0c3b23825e Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 25 Mar 2026 15:36:02 +0100 Subject: [PATCH 01/29] feat: add ecosystem page and refine shared UI components Introduces the ecosystem directory at /ecosystem with a filterable app grid, search with clear functionality, and a submission flow that integrates with GitHub issue templates for structured app submissions. Shared UI components were extracted and improved for consistency across the site. FilterPills replaces inline filter buttons on both blog and ecosystem pages. Badge gains a "tag" variant for neutral card metadata. PageHero's grid background was rebuilt using CSS Grid instead of background-size to eliminate sub-pixel rounding drift between pages. Secondary interactive elements now follow a unified two-tier hover system (white/50 rest, white/80 hover) with consistent cursor and border behavior. Accessibility improvements include aria-pressed on filter pills and aria-label on the search input. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ISSUE_TEMPLATE/ecosystem-submission.yml | 70 + .github/workflows/check-ecosystem-urls.yml | 24 + .gitignore | 4 + app/blog/page.tsx | 5 +- app/ecosystem/page.tsx | 206 + app/ecosystem/submit/page.tsx | 144 + components/blog/BlogCategoryFilter.tsx | 35 +- components/layout/Footer.tsx | 3 +- components/ui/Badge.tsx | 10 +- components/ui/Button.tsx | 22 +- components/ui/FilterPills.tsx | 58 + components/ui/PageHero.tsx | 55 + data/ecosystem.json | 34 + lib/constants.ts | 3 +- lib/ecosystem-data.ts | 21 + next.config.ts | 5 - package.json | 1 + pnpm-lock.yaml | 4469 +++++++++++++++++ public/ecosystem/daydream.svg | 95 + public/ecosystem/embody.svg | 15 + public/ecosystem/frameworks.svg | 56 + public/ecosystem/no-results.png | Bin 0 -> 3611412 bytes public/ecosystem/stream-place.png | Bin 0 -> 10338 bytes scripts/check-ecosystem-urls.mjs | 78 + 24 files changed, 5375 insertions(+), 38 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/ecosystem-submission.yml create mode 100644 .github/workflows/check-ecosystem-urls.yml create mode 100644 app/ecosystem/page.tsx create mode 100644 app/ecosystem/submit/page.tsx create mode 100644 components/ui/FilterPills.tsx create mode 100644 components/ui/PageHero.tsx create mode 100644 data/ecosystem.json create mode 100644 lib/ecosystem-data.ts create mode 100644 pnpm-lock.yaml create mode 100644 public/ecosystem/daydream.svg create mode 100644 public/ecosystem/embody.svg create mode 100644 public/ecosystem/frameworks.svg create mode 100644 public/ecosystem/no-results.png create mode 100644 public/ecosystem/stream-place.png create mode 100644 scripts/check-ecosystem-urls.mjs 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/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/ecosystem/page.tsx b/app/ecosystem/page.tsx new file mode 100644 index 0000000..e105f8c --- /dev/null +++ b/app/ecosystem/page.tsx @@ -0,0 +1,206 @@ +"use client"; + +import { useState, useMemo, useEffect } from "react"; +import { Search, Plus, ArrowUpRight } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +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 FilterPills from "@/components/ui/FilterPills"; +import Badge from "@/components/ui/Badge"; +import Button from "@/components/ui/Button"; + +const BATCH_SIZE = 12; + +export default function EcosystemPage() { + const [activeCategory, setActiveCategory] = useState("All"); + const [search, setSearch] = useState(""); + const [visible, setVisible] = useState(BATCH_SIZE); + + useEffect(() => { + setVisible(BATCH_SIZE); + }, [activeCategory, search]); + + const filtered = useMemo(() => { + return ECOSYSTEM_APPS.filter((app) => { + const matchesCategory = + activeCategory === "All" || app.categories.includes(activeCategory); + const matchesSearch = + !search || + app.name.toLowerCase().includes(search.toLowerCase()) || + app.description.toLowerCase().includes(search.toLowerCase()); + return matchesCategory && matchesSearch; + }); + }, [activeCategory, search]); + + const shown = filtered.slice(0, visible); + const hasMore = visible < filtered.length; + + return ( + +
+ +
+ + +
+ + {/* Filter bar */} +
+ + +
+ + 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) => ( + +
+
+ {app.logo ? ( + {`${app.name} + ) : ( + + {app.name.charAt(0)} + + )} +
+ +
+

+ {app.name} +

+

+ {app.hostname} +

+

+ {app.description} +

+
+ {app.categories.map((cat) => ( + + {cat} + + ))} +
+
+ ))} +
+ ) : ( +
+

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

+

+ Try searching for another term. + +

+ +
+ )} + + {hasMore && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/app/ecosystem/submit/page.tsx b/app/ecosystem/submit/page.tsx new file mode 100644 index 0000000..5dab963 --- /dev/null +++ b/app/ecosystem/submit/page.tsx @@ -0,0 +1,144 @@ +"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 FilterPills from "@/components/ui/FilterPills"; + +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" + /> +
+ +
+ +