mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
The commits section read numbers that were typed into scripts/sync-oss.ts (5,420 repositories, 214 orgs, 784 pushes a day, 245,417 commits, each contributor's count), a day's count scaled by a made-up factor, and a star count that began at 1,420. The forge's database holds 1,150 repositories and 46 orgs; none of that was read. Its panels were tinted rgba over a stylesheet, DualStarButton showed its guess before GitHub answered, and BulkStarModal said "successfully starred" over a forge PUT. scripts/sync-oss.ts (was .mjs) now counts GitHub's public activity for the orgs we publish: commits a day, the year, the week, active repositories and the newest commits, written at each build with the moment it was read. A figure the snapshot does not hold is not drawn. The grid, bars and repository card are @hanzo/ui/marketing (8.29.19); the showcase stylesheet and DayActivityModal are deleted. The org and repo pages lose their typed "velocity" banners. Design lint is zero in components/oss, home/OpenSource and ForgeBetaBanner. e2e/gates/activity.spec.ts holds each figure to the snapshot, the cards to one fill and edge in both schemes, a card's name, line and foot to their own rows, and the phone to no sideways scroll. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
234 lines
11 KiB
TypeScript
234 lines
11 KiB
TypeScript
'use client'
|
||
|
||
import { useMemo, useRef, useState } from 'react'
|
||
import Link from 'next/link'
|
||
import { ArrowRight, ArrowUpRight, Search } from 'lucide-react'
|
||
import { useI18n } from '@hanzo/i18n/react'
|
||
import { Input, Text, View, XStack, YStack } from '@hanzo/ui'
|
||
import { Action, Repo } from '@hanzo/ui/marketing'
|
||
import stats from '@/lib/data/oss-stats.json'
|
||
import { Caption, Card, CardBody, CardTitle, Mono } from '@/components/home/plate'
|
||
import { catalog, type Organization } from './showcase-catalog'
|
||
import Activity from './Activity'
|
||
import DualStarButton from './DualStarButton'
|
||
|
||
/**
|
||
* The open-source directory: what we ship, counted (./Activity), the repositories
|
||
* to start from, and every organization, searchable by name and filtered by kind.
|
||
* Drawn from the landing's own parts, so a card here is the card everywhere.
|
||
*/
|
||
|
||
type Scope = 'hanzo' | 'related'
|
||
|
||
/** A card that is a link: one hairline, one radius, one inset, the page's own fill. */
|
||
const FRAME = { rounded: 'var(--radius-xl)', borderWidth: 1, borderColor: '$edge', bg: '$panel', p: 20, gap: 8, minW: 0 } as const
|
||
|
||
export interface HanzoOpenSourceShowcaseProps {
|
||
/** When true, renders only the activity panel (for embedding at the top of an existing repos section). */
|
||
activityOnly?: boolean
|
||
/** Start in the related ecosystem rather than the Hanzo-branded organizations. */
|
||
initialScope?: Scope
|
||
}
|
||
|
||
export function HanzoOpenSourceShowcase({ activityOnly = false, initialScope = 'hanzo' }: HanzoOpenSourceShowcaseProps) {
|
||
const { t } = useI18n()
|
||
const [scope, setScope] = useState<Scope>(initialScope)
|
||
const [category, setCategory] = useState('All')
|
||
const [query, setQuery] = useState('')
|
||
const [expanded, setExpanded] = useState(false)
|
||
const inputRef = useRef<HTMLInputElement>(null)
|
||
|
||
const organizations = scope === 'hanzo' ? catalog.primary : catalog.related
|
||
const categories = useMemo(() => ['All', ...Array.from(new Set(organizations.map((o) => o.category)))], [organizations])
|
||
const filtered = useMemo(
|
||
() => organizations.filter((o) => (category === 'All' || o.category === category) && `${o.slug} ${o.title} ${o.description}`.toLowerCase().includes(query.toLowerCase())),
|
||
[organizations, category, query],
|
||
)
|
||
const visible = expanded || category !== 'All' || query ? filtered : filtered.slice(0, 9)
|
||
|
||
const choose = (value: Scope) => {
|
||
setScope(value)
|
||
setCategory('All')
|
||
setQuery('')
|
||
setExpanded(false)
|
||
}
|
||
|
||
if (activityOnly) return <Activity />
|
||
|
||
return (
|
||
<YStack gap={48} width="100%" data-slot="oss">
|
||
<Activity />
|
||
|
||
<YStack gap={20} render="section" aria-labelledby="oss-repos">
|
||
<XStack justify="space-between" items="flex-end" flexWrap="wrap" gap={12}>
|
||
<YStack gap={6} maxW={640}>
|
||
<Text render="h2" id="oss-repos" fontSize="$7" lineHeight="$7" fontWeight="600" color="var(--foreground)">
|
||
{t('Start with the foundations.')}
|
||
</Text>
|
||
<Caption>{t('Selected open projects with direct paths to their source code.')}</Caption>
|
||
</YStack>
|
||
<Action href="https://forge.hanzo.ai" target="_blank" rel="noopener noreferrer">
|
||
{t('All repositories on the forge')}
|
||
<ArrowUpRight size={14} aria-hidden data-i18n-mirror="" />
|
||
</Action>
|
||
</XStack>
|
||
<View display="grid" gridTemplateColumns="minmax(0, 1fr)" gap={12} $sm={{ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' }} $lg={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}>
|
||
{catalog.repos.map((repo) => (
|
||
<Repo
|
||
key={`${repo.org}/${repo.name}`}
|
||
name={`${repo.org}/${repo.name}`}
|
||
description={repo.desc}
|
||
href={`/oss/${repo.org}/${repo.name}`}
|
||
tag={repo.tag}
|
||
actions={<DualStarButton owner={repo.org} repo={repo.name} size="sm" />}
|
||
/>
|
||
))}
|
||
</View>
|
||
</YStack>
|
||
|
||
<YStack gap={20} render="section" id="hanzo-oss-directory" aria-labelledby="oss-orgs">
|
||
<XStack justify="space-between" items="flex-end" flexWrap="wrap" gap={12}>
|
||
<YStack gap={6} maxW={640}>
|
||
<Text render="h2" id="oss-orgs" fontSize="$7" lineHeight="$7" fontWeight="600" color="var(--foreground)">
|
||
{t('One ecosystem. Many layers.')}
|
||
</Text>
|
||
<Caption>{t('{n} Hanzo-branded organizations, plus related ecosystem projects in one directory.', { n: catalog.primary.length })}</Caption>
|
||
</YStack>
|
||
<Mono color="var(--muted-foreground)">{t('{n} organizations', { n: filtered.length })}</Mono>
|
||
</XStack>
|
||
|
||
<XStack justify="space-between" items="center" flexWrap="wrap" gap={12}>
|
||
<XStack role="group" aria-label={t('Organization scope')} gap={2} p={3} rounded={9} borderWidth={1} borderColor="var(--border)" $platform-web={{ borderRadius: '9px' }}>
|
||
{(['hanzo', 'related'] as const).map((s) => (
|
||
<XStack
|
||
key={s}
|
||
render={<button type="button" aria-pressed={scope === s} onClick={() => choose(s)} />}
|
||
px={12}
|
||
py={6}
|
||
rounded={6}
|
||
gap={8}
|
||
items="center"
|
||
bg={scope === s ? 'var(--foreground)' : 'transparent'}
|
||
cursor="pointer"
|
||
$platform-web={{ borderRadius: '6px', border: 'none' }}
|
||
>
|
||
<Text fontSize="$2" fontWeight="500" color={scope === s ? 'var(--background)' : 'var(--muted-foreground)'}>
|
||
{s === 'hanzo' ? t('Hanzo organizations') : t('Related ecosystem')}
|
||
</Text>
|
||
<Mono color={scope === s ? 'var(--background)' : 'var(--muted-foreground)'}>{s === 'hanzo' ? catalog.primary.length : catalog.related.length}</Mono>
|
||
</XStack>
|
||
))}
|
||
</XStack>
|
||
<XStack items="center" gap={8} px={10} height={36} rounded={8} borderWidth={1} borderColor="var(--border)" minW={235} $platform-web={{ borderRadius: '8px' }}>
|
||
<Search size={14} color="var(--muted-foreground)" aria-hidden />
|
||
<Input
|
||
ref={inputRef}
|
||
type="search"
|
||
value={query}
|
||
onChange={(e) => setQuery(e.target.value)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Escape') setQuery('')
|
||
}}
|
||
placeholder={t('Search organizations')}
|
||
aria-label={t('Search organizations')}
|
||
flex={1}
|
||
minW={0}
|
||
borderWidth={0}
|
||
bg="transparent"
|
||
/>
|
||
</XStack>
|
||
</XStack>
|
||
|
||
<XStack gap={8} flexWrap="wrap" role="group" aria-label={t('Filter organizations by category')}>
|
||
{categories.map((c) => (
|
||
<XStack
|
||
key={c}
|
||
render={
|
||
<button
|
||
type="button"
|
||
aria-pressed={category === c}
|
||
onClick={() => {
|
||
setCategory(c)
|
||
setExpanded(false)
|
||
}}
|
||
/>
|
||
}
|
||
px={12}
|
||
py={5}
|
||
rounded={9999}
|
||
borderWidth={1}
|
||
borderColor={category === c ? 'var(--foreground)' : 'var(--border)'}
|
||
bg={category === c ? 'var(--secondary)' : 'transparent'}
|
||
cursor="pointer"
|
||
$platform-web={{ borderRadius: '9999px' }}
|
||
>
|
||
<Text fontSize="$2" color={category === c ? 'var(--foreground)' : 'var(--muted-foreground)'}>
|
||
{c === 'All' ? `${t('All')} (${organizations.length})` : c}
|
||
</Text>
|
||
</XStack>
|
||
))}
|
||
</XStack>
|
||
|
||
<View display="grid" gridTemplateColumns="minmax(0, 1fr)" gap={12} $sm={{ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' }} $lg={{ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))' }} aria-live="polite">
|
||
{visible.length ? (
|
||
visible.map((org: Organization) => (
|
||
<YStack key={org.slug} render={<Link href={`/oss/${org.slug}`} />} {...FRAME} hoverStyle={{ borderColor: 'var(--border-strong)' }} $platform-web={{ textDecoration: 'none' }}>
|
||
<XStack justify="space-between" items="center" gap={8}>
|
||
<CardTitle render="div">{org.title}</CardTitle>
|
||
<ArrowRight size={14} color="var(--muted-foreground)" aria-hidden data-i18n-mirror="" />
|
||
</XStack>
|
||
<Mono color="var(--muted-foreground)">/oss/{org.slug}</Mono>
|
||
<CardBody>
|
||
{org.description}
|
||
{org.visibility === 'mixed' ? ` ${t('Private work excluded.')}` : ''}
|
||
</CardBody>
|
||
</YStack>
|
||
))
|
||
) : (
|
||
<YStack gridColumn="1 / -1" p={32} items="center" rounded="var(--radius-xl)" borderWidth={1} borderColor="var(--border)" $platform-web={{ borderStyle: 'dashed' }}>
|
||
<CardBody>{t('No organizations match this filter. Try a broader search.')}</CardBody>
|
||
</YStack>
|
||
)}
|
||
</View>
|
||
|
||
<XStack justify="space-between" items="center" flexWrap="wrap" gap={12}>
|
||
{filtered.length > 9 && category === 'All' && !query ? (
|
||
<Action render="button" type="button" aria-expanded={expanded} onClick={() => setExpanded(!expanded)}>
|
||
{expanded ? t('Show fewer organizations') : t('View all organizations')}
|
||
</Action>
|
||
) : (
|
||
<View />
|
||
)}
|
||
<Caption>{t('Showing {shown} of {total} matched organizations', { shown: visible.length, total: filtered.length })}</Caption>
|
||
</XStack>
|
||
</YStack>
|
||
|
||
<Card
|
||
title={t('Counted from GitHub')}
|
||
body={t('{repos} public repositories across {orgs} organizations, with the source on git.hanzo.ai and mirrored on GitHub.', { repos: stats.repos.toLocaleString('en-US'), orgs: stats.orgs })}
|
||
/>
|
||
|
||
<XStack justify="space-between" items="center" flexWrap="wrap" gap={20} pt={24} borderTopWidth={1} borderColor="var(--border)">
|
||
<YStack gap={6} maxW={560}>
|
||
<Text render="h2" fontSize="$8" lineHeight="$8" fontWeight="600" color="var(--foreground)">
|
||
{t('See what we’re building.')}
|
||
</Text>
|
||
<Caption>{t('Read the source. Explore the stack. Build something better.')}</Caption>
|
||
</YStack>
|
||
<XStack gap={10} flexWrap="wrap">
|
||
<Action href="https://forge.hanzo.ai" fill target="_blank" rel="noopener noreferrer">
|
||
{t('Explore the forge')}
|
||
<ArrowUpRight size={14} aria-hidden data-i18n-mirror="" />
|
||
</Action>
|
||
<Action href="https://github.com/hanzoai" target="_blank" rel="noopener noreferrer">
|
||
{t('Explore GitHub')}
|
||
<ArrowUpRight size={14} aria-hidden data-i18n-mirror="" />
|
||
</Action>
|
||
</XStack>
|
||
</XStack>
|
||
</YStack>
|
||
)
|
||
}
|
||
|
||
export default HanzoOpenSourceShowcase
|