Files

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>
)
}