Spaces:
Sleeping
Sleeping
| import { useState } from 'react' | |
| import { FileCode, Download, Upload, Copy, Check, AlertTriangle } from 'lucide-react' | |
| import clsx from 'clsx' | |
| import { useI18n } from '../i18n' | |
| import { getBatchImportTemplates } from '../utils/batchImportTemplates' | |
| export default function BatchImport({ onRefresh, onMessage, authFetch }) { | |
| const { t } = useI18n() | |
| const [jsonInput, setJsonInput] = useState('') | |
| const [loading, setLoading] = useState(false) | |
| const [result, setResult] = useState(null) | |
| const [copied, setCopied] = useState(false) | |
| const apiFetch = authFetch || fetch | |
| const templates = getBatchImportTemplates(t) | |
| const handleImport = async () => { | |
| if (!jsonInput.trim()) { | |
| onMessage('error', t('batchImport.enterJson')) | |
| return | |
| } | |
| let config | |
| try { | |
| config = JSON.parse(jsonInput) | |
| } catch (e) { | |
| onMessage('error', t('messages.invalidJson')) | |
| return | |
| } | |
| setLoading(true) | |
| setResult(null) | |
| try { | |
| const res = await apiFetch('/admin/import', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(config), | |
| }) | |
| const data = await res.json() | |
| if (res.ok) { | |
| setResult(data) | |
| onMessage('success', t('batchImport.importSuccess', { keys: data.imported_keys, accounts: data.imported_accounts })) | |
| onRefresh() | |
| } else { | |
| onMessage('error', data.detail || t('messages.importFailed')) | |
| } | |
| } catch (e) { | |
| onMessage('error', t('messages.networkError')) | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| const loadTemplate = (key) => { | |
| const tpl = templates[key] | |
| if (tpl) { | |
| setJsonInput(JSON.stringify(tpl.config, null, 2)) | |
| onMessage('info', t('batchImport.templateLoaded', { name: tpl.name })) | |
| } | |
| } | |
| const handleExport = async () => { | |
| try { | |
| const res = await apiFetch('/admin/export') | |
| if (res.ok) { | |
| const data = await res.json() | |
| setJsonInput(JSON.stringify(JSON.parse(data.json), null, 2)) | |
| onMessage('success', t('batchImport.currentConfigLoaded')) | |
| } | |
| } catch (e) { | |
| onMessage('error', t('batchImport.fetchConfigFailed')) | |
| } | |
| } | |
| const copyBase64 = async () => { | |
| try { | |
| const res = await apiFetch('/admin/export') | |
| if (res.ok) { | |
| const data = await res.json() | |
| await navigator.clipboard.writeText(data.base64) | |
| setCopied(true) | |
| setTimeout(() => setCopied(false), 2000) | |
| onMessage('success', t('batchImport.copySuccess')) | |
| } | |
| } catch (e) { | |
| onMessage('error', t('messages.copyFailed')) | |
| } | |
| } | |
| return ( | |
| <div className="flex flex-col lg:grid lg:grid-cols-3 gap-6 lg:h-[calc(100vh-140px)]"> | |
| {/* Templates Panel */} | |
| <div className="md:col-span-1 space-y-4"> | |
| <div className="bg-card border border-border rounded-xl p-5 shadow-sm"> | |
| <h3 className="font-semibold flex items-center gap-2 mb-4"> | |
| <FileCode className="w-4 h-4 text-primary" /> | |
| {t('batchImport.quickTemplates')} | |
| </h3> | |
| <div className="space-y-3"> | |
| {Object.entries(templates).map(([key, tpl]) => ( | |
| <button | |
| key={key} | |
| onClick={() => loadTemplate(key)} | |
| className="w-full text-left p-3 rounded-lg border border-border bg-secondary/20 hover:bg-secondary/50 hover:border-primary/50 transition-all custom-focus group" | |
| > | |
| <div className="font-medium text-sm group-hover:text-primary transition-colors">{tpl.name}</div> | |
| <div className="text-xs text-muted-foreground mt-0.5">{tpl.desc}</div> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="bg-linear-to-br from-primary/10 to-transparent border border-primary/20 rounded-xl p-5 shadow-sm"> | |
| <h3 className="font-semibold flex items-center gap-2 mb-2 text-primary"> | |
| <Download className="w-4 h-4" /> | |
| {t('batchImport.dataExport')} | |
| </h3> | |
| <p className="text-sm text-muted-foreground mb-4"> | |
| {t('batchImport.dataExportDesc')} | |
| </p> | |
| <button | |
| onClick={copyBase64} | |
| className="w-full flex items-center justify-center gap-2 py-2.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-all font-medium text-sm shadow-sm" | |
| > | |
| {copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />} | |
| {copied ? t('batchImport.copied') : t('batchImport.copyBase64')} | |
| </button> | |
| <p className="text-[10px] text-muted-foreground mt-2 text-center"> | |
| {t('batchImport.variableName')}: <code className="bg-background px-1 py-0.5 rounded border border-border">DS2API_CONFIG_JSON</code> | |
| </p> | |
| </div> | |
| </div> | |
| {/* Editor Panel */} | |
| <div className="lg:col-span-2 flex flex-col bg-card border border-border rounded-xl shadow-sm overflow-hidden min-h-[400px] lg:h-full"> | |
| <div className="p-4 border-b border-border flex items-center justify-between bg-muted/20"> | |
| <h3 className="font-semibold flex items-center gap-2"> | |
| <Upload className="w-4 h-4 text-primary" /> | |
| {t('batchImport.jsonEditor')} | |
| </h3> | |
| <div className="flex gap-2"> | |
| <button onClick={handleExport} className="px-3 py-1.5 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border"> | |
| {t('batchImport.loadCurrentConfig')} | |
| </button> | |
| <button onClick={handleImport} disabled={loading} className="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-xs font-medium disabled:opacity-50"> | |
| {loading ? t('batchImport.importing') : t('batchImport.applyConfig')} | |
| </button> | |
| </div> | |
| </div> | |
| <div className="flex-1 relative min-h-[400px]"> | |
| <textarea | |
| className="absolute inset-0 w-full h-full p-4 font-mono text-sm bg-[#09090b] text-foreground resize-none focus:outline-none custom-scrollbar" | |
| value={jsonInput} | |
| onChange={e => setJsonInput(e.target.value)} | |
| placeholder={'{\n "keys": ["your-api-key"],\n "accounts": [\n {"email": "...", "password": "...", "token": ""}\n ]\n}'} | |
| spellCheck={false} | |
| /> | |
| </div> | |
| {result && ( | |
| <div className={clsx( | |
| "p-4 border-t", | |
| result.imported_keys || result.imported_accounts ? "bg-emerald-500/10 border-emerald-500/20" : "bg-destructive/10 border-destructive/20" | |
| )}> | |
| <div className="flex items-start gap-3"> | |
| {result.imported_keys || result.imported_accounts ? ( | |
| <Check className="w-5 h-5 text-emerald-500 mt-0.5" /> | |
| ) : ( | |
| <AlertTriangle className="w-5 h-5 text-destructive mt-0.5" /> | |
| )} | |
| <div> | |
| <h4 className={clsx("font-medium", result.imported_keys || result.imported_accounts ? "text-emerald-500" : "text-destructive")}> | |
| {t('batchImport.importComplete')} | |
| </h4> | |
| <p className="text-sm opacity-80 mt-1"> | |
| {t('batchImport.importSummary', { keys: result.imported_keys, accounts: result.imported_accounts })} | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } | |