File size: 8,973 Bytes
8d3471e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | 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>
)
}
|