mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
113 lines
4.7 KiB
TypeScript
113 lines
4.7 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo, useState } from 'react'
|
|
import { Brain, Briefcase, Check, Code2, Database, Search, Shield, Sparkles, Zap } from 'lucide-react'
|
|
import { Input, Text, View, XStack, YStack, toast } from '@hanzo/ui'
|
|
import { Action, Chip, Eyebrow } from '@hanzo/ui/marketing'
|
|
import { phrase } from '@hanzo/i18n'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
import { track } from '@hanzo/app/lib/tags'
|
|
import { BETAS, GROUPS, opens, type Group } from '@hanzo/app/lib/betas'
|
|
import { Quiet } from './card'
|
|
|
|
const ICONS: Record<Group, typeof Briefcase> = {
|
|
Business: Briefcase,
|
|
Data: Database,
|
|
Agents: Zap,
|
|
'Build & run': Code2,
|
|
Security: Shield,
|
|
Creative: Sparkles,
|
|
Models: Brain,
|
|
}
|
|
|
|
const HEADINGS: Record<Group, string> = {
|
|
Business: phrase('Business'),
|
|
Data: phrase('Data'),
|
|
Agents: phrase('Agents'),
|
|
'Build & run': phrase('Build & run'),
|
|
Security: phrase('Security'),
|
|
Creative: phrase('Creative'),
|
|
Models: phrase('Models'),
|
|
}
|
|
|
|
/**
|
|
* Step 3: every app and model in beta, one row each, and one control on the
|
|
* row. Whether a row is open is the cloud's answer and `opens` is the one
|
|
* predicate that reads it (staff are open everywhere); a closed one is asked
|
|
* for here, and says so at once. The cloud has one switch a member holds, for
|
|
* every public beta together, and it is the first thing on the page.
|
|
*/
|
|
export function Betas({ flags, staff, asked, request }: { flags: Record<string, boolean>; staff: boolean; asked: string[]; request: (ids: string[]) => Promise<void> }) {
|
|
const { t } = useI18n()
|
|
const [find, setFind] = useState('')
|
|
const sections = useMemo(() => {
|
|
const q = find.trim().toLowerCase()
|
|
const hit = (b: (typeof BETAS)[number]) => !q || `${b.name} ${b.id} ${b.line}`.toLowerCase().includes(q)
|
|
return GROUPS.map((g) => ({ g, items: BETAS.filter((b) => b.group === g && hit(b)) })).filter((s) => s.items.length)
|
|
}, [find])
|
|
|
|
const ask = (id: string, name: string) => {
|
|
track('beta_enrolled_toggled', { beta_id: id, enrolled: true })
|
|
void request([id])
|
|
toast.success(t('Requested {name}.', { name }))
|
|
}
|
|
|
|
return (
|
|
<YStack gap={20} data-slot="ecosystem-betas">
|
|
<XStack items="center" gap={10} px={14} rounded={14} borderWidth={1} borderColor="var(--border)">
|
|
<Search size={16} color="var(--muted-foreground)" aria-hidden />
|
|
<Input height={44} flex={1} px={0} borderWidth={0} bg="transparent" placeholder={t('Search betas')} aria-label={t('Search betas')} value={find} onChange={(e) => setFind(e.target.value)} />
|
|
</XStack>
|
|
|
|
{sections.length ? (
|
|
sections.map(({ g, items }) => (
|
|
<YStack key={g} gap={4}>
|
|
<Eyebrow>{t(HEADINGS[g])}</Eyebrow>
|
|
{items.map((b, i) => {
|
|
const Icon = ICONS[b.group] ?? Sparkles
|
|
const on = opens(b.id, flags, staff)
|
|
const sent = asked.includes(b.id)
|
|
return (
|
|
<XStack key={b.id} data-beta={b.id} items="center" gap={14} py={14} borderTopWidth={i ? 1 : 0} borderColor="var(--border)">
|
|
<View width={36} height={36} shrink={0} rounded={10} bg="var(--secondary)" items="center" justify="center">
|
|
<Icon size={18} color="var(--foreground)" aria-hidden />
|
|
</View>
|
|
<YStack flex={1} minW={0}>
|
|
<Text fontSize="$4" fontWeight="600" color="var(--foreground)">
|
|
{b.name}
|
|
</Text>
|
|
<Text fontSize="$2" color="var(--muted-foreground)" overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
|
|
{t(b.line)}
|
|
</Text>
|
|
</YStack>
|
|
{on ? (
|
|
<XStack items="center" gap={12} shrink={0}>
|
|
{b.href ? <Quiet href={b.href} target="_blank" rel="noopener noreferrer">{t('Open')}</Quiet> : null}
|
|
<Chip px={12} py={6} aria-label={t('{name} is on', { name: b.name })}>
|
|
<Check size={13} aria-hidden />
|
|
{t('On')}
|
|
</Chip>
|
|
</XStack>
|
|
) : sent ? (
|
|
<Chip px={12} py={6} shrink={0}>
|
|
{t('Requested')}
|
|
</Chip>
|
|
) : (
|
|
<Action render="button" type="button" fill shrink={0} aria-label={t('Get access to {name}', { name: b.name })} onClick={() => ask(b.id, b.name)}>
|
|
{t('Get access')}
|
|
</Action>
|
|
)}
|
|
</XStack>
|
|
)
|
|
})}
|
|
</YStack>
|
|
))
|
|
) : (
|
|
<Text fontSize="$3" color="var(--muted-foreground)">
|
|
{t('No beta matches.')}
|
|
</Text>
|
|
)}
|
|
</YStack>
|
|
)
|
|
}
|