mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
337 lines
11 KiB
TypeScript
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>
|
|
)
|
|
}
|