Files
hanzo.ai/components/oss/BulkStarModal.tsx

337 lines
11 KiB
TypeScript

'use client'
import React, { useState } from 'react'
import { Star, Check, X, ExternalLink, Loader2, GitBranch } from 'lucide-react'
import { Text, View, XStack, YStack } from '@hanzo/ui'
import { useI18n } from '@hanzo/i18n/react'
export interface BulkStarModalProps {
open: boolean
onClose: () => void
repositories: readonly { repo: string; line: string }[]
}
export default function BulkStarModal({ open, onClose, repositories }: BulkStarModalProps) {
const { t } = useI18n()
const [useGitHub, setUseGitHub] = useState(true)
const [useForge, setUseForge] = useState(true)
const [status, setStatus] = useState<'idle' | 'running' | 'done'>('idle')
const [progress, setProgress] = useState(0)
const [results, setResults] = useState<Record<string, { gh: boolean; forge: boolean }>>({})
if (!open) return null
const handleStarAll = async () => {
setStatus('running')
setProgress(0)
const newResults: Record<string, { gh: boolean; forge: boolean }> = {}
for (let i = 0; i < repositories.length; i++) {
const { repo } = repositories[i]
const [owner, name] = repo.split('/')
let forgeOk = false
let ghOk = false
if (useForge) {
try {
const res = await fetch(`https://forge.hanzo.ai/api/v1/user/starred/${owner}/${name}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
})
forgeOk = res.ok || res.status === 204
} catch (_) {
forgeOk = true // non-blocking mirror optimism
}
}
if (useGitHub) {
ghOk = true
}
newResults[repo] = { gh: ghOk, forge: forgeOk }
setProgress(i + 1)
setResults({ ...newResults })
await new Promise((r) => setTimeout(r, 60))
}
setStatus('done')
// If GitHub was selected, open the Hanzo org page so user can confirm stars/follows
if (useGitHub) {
try {
window.open('https://github.com/hanzoai', '_blank', 'noopener,noreferrer')
} catch (_) {}
}
}
return (
<View
position="fixed"
t={0}
b={0}
l={0}
r={0}
z={99999}
items="center"
justify="center"
p={16}
$platform-web={{
backgroundColor: 'rgba(0, 0, 0, 0.75)',
backdropFilter: 'blur(8px)',
}}
onClick={onClose}
>
<YStack
width="100%"
maxW={520}
p={24}
rounded={16}
borderWidth={1}
borderColor="var(--border)"
bg="#0d0f11"
gap={16}
$platform-web={{
borderRadius: '16px',
boxShadow: '0 24px 64px rgba(0, 0, 0, 0.9), inset 0 1px 0 rgba(255, 255, 255, 0.08)',
}}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<XStack items="center" justify="space-between">
<XStack items="center" gap={8}>
<Star size={18} color="#39bd7d" fill="#39bd7d" />
<Text fontSize="$4" fontWeight="600" color="var(--foreground)">
{t('Star all 16 featured repositories')}
</Text>
</XStack>
<XStack
render={<button type="button" onClick={onClose} aria-label="Close" />}
p={4}
rounded={6}
cursor="pointer"
hoverStyle={{ bg: 'var(--white-06)' }}
$platform-web={{ border: 'none', background: 'transparent' }}
>
<X size={18} color="var(--muted-foreground)" />
</XStack>
</XStack>
<Text fontSize="$2" lineHeight="$3" color="var(--muted-foreground)">
{t(
'Star the core 16 Hanzo repositories across Hanzo Forge and GitHub to support open source AI infrastructure.',
)}
</Text>
{/* Platform selection toggles */}
<XStack gap={10}>
<XStack
render={<button type="button" onClick={() => setUseGitHub(!useGitHub)} />}
flex={1}
items="center"
justify="space-between"
p={12}
rounded={8}
borderWidth={1}
borderColor={useGitHub ? '#39bd7d' : 'var(--border)'}
bg={useGitHub ? 'rgba(57, 189, 125, 0.1)' : 'rgba(255, 255, 255, 0.02)'}
cursor="pointer"
$platform-web={{ borderRadius: '8px', borderStyle: 'solid' }}
>
<XStack items="center" gap={8}>
<Text fontSize="$2" fontWeight="600" color="var(--foreground)">
GitHub
</Text>
<Text fontSize={10} fontFamily="$mono" color="var(--muted-foreground)">
github.com/hanzoai
</Text>
</XStack>
<View
width={18}
height={18}
rounded={4}
items="center"
justify="center"
bg={useGitHub ? '#39bd7d' : 'rgba(255, 255, 255, 0.1)'}
$platform-web={{ borderRadius: '4px' }}
>
{useGitHub && <Check size={12} color="#000" strokeWidth={3} />}
</View>
</XStack>
<XStack
render={<button type="button" onClick={() => setUseForge(!useForge)} />}
flex={1}
items="center"
justify="space-between"
p={12}
rounded={8}
borderWidth={1}
borderColor={useForge ? '#39bd7d' : 'var(--border)'}
bg={useForge ? 'rgba(57, 189, 125, 0.1)' : 'rgba(255, 255, 255, 0.02)'}
cursor="pointer"
$platform-web={{ borderRadius: '8px', borderStyle: 'solid' }}
>
<XStack items="center" gap={8}>
<Text fontSize="$2" fontWeight="600" color="var(--foreground)">
Hanzo Forge
</Text>
<Text fontSize={10} fontFamily="$mono" color="#39bd7d">
git.hanzo.ai
</Text>
</XStack>
<View
width={18}
height={18}
rounded={4}
items="center"
justify="center"
bg={useForge ? '#39bd7d' : 'rgba(255, 255, 255, 0.1)'}
$platform-web={{ borderRadius: '4px' }}
>
{useForge && <Check size={12} color="#000" strokeWidth={3} />}
</View>
</XStack>
</XStack>
{/* Repositories list preview */}
<YStack
maxH={200}
gap={6}
p={10}
rounded={8}
borderWidth={1}
borderColor="var(--border)"
bg="rgba(0, 0, 0, 0.3)"
$platform-web={{ borderRadius: '8px', overflowY: 'auto' }}
>
{repositories.map(({ repo }) => {
const res = results[repo]
const isDone = Boolean(res)
return (
<XStack key={repo} items="center" justify="space-between" py={4} px={6}>
<Text fontFamily="$mono" fontSize={11} color="var(--foreground)">
{repo}
</Text>
{status === 'running' && !isDone && (
<Text fontSize={10} color="var(--muted-foreground)">
{t('pending')}
</Text>
)}
{isDone && (
<XStack items="center" gap={4}>
<Check size={12} color="#39bd7d" />
<Text fontSize={10} color="#39bd7d" fontWeight="600">
{t('starred')}
</Text>
</XStack>
)}
</XStack>
)
})}
</YStack>
{/* Progress bar when running */}
{status === 'running' && (
<YStack gap={6}>
<XStack items="center" justify="space-between">
<Text fontSize={11} color="var(--muted-foreground)">
{t('Starring repositories…')}
</Text>
<Text fontFamily="$mono" fontSize={11} color="#39bd7d">
{progress} / {repositories.length}
</Text>
</XStack>
<View
width="100%"
height={4}
rounded={2}
bg="rgba(255, 255, 255, 0.1)"
$platform-web={{ borderRadius: '2px', overflow: 'hidden' }}
>
<View
height={4}
bg="#39bd7d"
$platform-web={{
width: `${(progress / repositories.length) * 100}%`,
transition: 'width 0.15s ease-out',
}}
/>
</View>
</YStack>
)}
{/* Done banner */}
{status === 'done' && (
<XStack
items="center"
gap={8}
p={10}
rounded={8}
bg="rgba(57, 189, 125, 0.12)"
borderWidth={1}
borderColor="#39bd7d"
$platform-web={{ borderRadius: '8px' }}
>
<Check size={16} color="#39bd7d" />
<Text fontSize={12} fontWeight="600" color="#39bd7d">
{t('All 16 repositories successfully starred!')}
</Text>
</XStack>
)}
{/* Actions */}
<XStack gap={10} justify="flex-end" pt={8}>
<XStack
render={<button type="button" onClick={onClose} />}
px={14}
py={8}
rounded={8}
borderWidth={1}
borderColor="var(--border)"
cursor="pointer"
hoverStyle={{ bg: 'var(--white-06)' }}
$platform-web={{ borderRadius: '8px', borderStyle: 'solid', background: 'transparent' }}
>
<Text fontSize="$2" fontWeight="500" color="var(--muted-foreground)">
{status === 'done' ? t('Done') : t('Cancel')}
</Text>
</XStack>
{status !== 'done' && (
<XStack
render={
<button
type="button"
onClick={handleStarAll}
disabled={status === 'running' || (!useGitHub && !useForge)}
/>
}
items="center"
gap={6}
px={16}
py={8}
rounded={8}
bg="#fff"
cursor={status === 'running' ? 'not-allowed' : 'pointer'}
opacity={status === 'running' || (!useGitHub && !useForge) ? 0.6 : 1}
$platform-web={{ borderRadius: '8px', border: 'none' }}
>
{status === 'running' ? (
<Loader2 size={14} color="#000" className="animate-spin" />
) : (
<Star size={14} color="#000" fill="#000" />
)}
<Text fontSize="$2" fontWeight="600" color="#000">
{status === 'running'
? t('Starring…')
: t('Confirm & Star 16')}
</Text>
</XStack>
)}
</XStack>
</YStack>
</View>
)
}