Spaces:
Paused
Paused
| import { useState, useCallback, useRef } from "preact/hooks"; | |
| import { useT } from "@shared/i18n/context"; | |
| import type { ImportDiff } from "@shared/hooks/use-proxy-assignments"; | |
| interface ImportExportProps { | |
| onExport: () => Promise<Array<{ email: string; proxyId: string }>>; | |
| onImportPreview: (data: Array<{ email: string; proxyId: string }>) => Promise<ImportDiff | null>; | |
| onApplyImport: (assignments: Array<{ accountId: string; proxyId: string }>) => Promise<void>; | |
| } | |
| export function ImportExport({ onExport, onImportPreview, onApplyImport }: ImportExportProps) { | |
| const t = useT(); | |
| const [showImport, setShowImport] = useState(false); | |
| const [diff, setDiff] = useState<ImportDiff | null>(null); | |
| const [importing, setImporting] = useState(false); | |
| const fileRef = useRef<HTMLInputElement>(null); | |
| const handleExport = useCallback(async () => { | |
| const data = await onExport(); | |
| const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = "proxy-assignments.json"; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| }, [onExport]); | |
| const handleFileSelect = useCallback(async () => { | |
| const file = fileRef.current?.files?.[0]; | |
| if (!file) return; | |
| try { | |
| const text = await file.text(); | |
| const parsed = JSON.parse(text) as Array<{ email: string; proxyId: string }>; | |
| if (!Array.isArray(parsed)) { | |
| alert("Invalid format: expected array of { email, proxyId }"); | |
| return; | |
| } | |
| setImporting(true); | |
| const result = await onImportPreview(parsed); | |
| setDiff(result); | |
| setImporting(false); | |
| } catch { | |
| alert("Failed to parse JSON file"); | |
| } | |
| }, [onImportPreview]); | |
| const handleApply = useCallback(async () => { | |
| if (!diff || diff.changes.length === 0) return; | |
| setImporting(true); | |
| await onApplyImport( | |
| diff.changes.map((c) => ({ accountId: c.accountId, proxyId: c.to })), | |
| ); | |
| setImporting(false); | |
| setDiff(null); | |
| setShowImport(false); | |
| }, [diff, onApplyImport]); | |
| const handleClose = useCallback(() => { | |
| setShowImport(false); | |
| setDiff(null); | |
| if (fileRef.current) fileRef.current.value = ""; | |
| }, []); | |
| return ( | |
| <> | |
| {/* Buttons */} | |
| <div class="flex items-center gap-2"> | |
| <button | |
| onClick={() => setShowImport(true)} | |
| class="px-3 py-1.5 text-xs font-medium rounded-lg border border-gray-200 dark:border-border-dark hover:bg-slate-50 dark:hover:bg-border-dark transition-colors" | |
| > | |
| {t("importBtn")} | |
| </button> | |
| <button | |
| onClick={handleExport} | |
| class="px-3 py-1.5 text-xs font-medium rounded-lg border border-gray-200 dark:border-border-dark hover:bg-slate-50 dark:hover:bg-border-dark transition-colors" | |
| > | |
| {t("exportBtn")} | |
| </button> | |
| </div> | |
| {/* Import Modal */} | |
| {showImport && ( | |
| <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={handleClose}> | |
| <div | |
| class="bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl p-6 w-full max-w-lg mx-4 shadow-xl" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| <h3 class="text-lg font-semibold mb-4">{t("importPreview")}</h3> | |
| {!diff ? ( | |
| <div class="space-y-4"> | |
| <p class="text-sm text-slate-500 dark:text-text-dim">{t("importFile")}</p> | |
| <input | |
| ref={fileRef} | |
| type="file" | |
| accept=".json" | |
| onChange={handleFileSelect} | |
| class="block w-full text-sm text-slate-500 dark:text-text-dim file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-primary/10 file:text-primary hover:file:bg-primary/20 cursor-pointer" | |
| /> | |
| {importing && ( | |
| <p class="text-sm text-slate-500 dark:text-text-dim animate-pulse">Loading...</p> | |
| )} | |
| </div> | |
| ) : ( | |
| <div class="space-y-4"> | |
| {diff.changes.length === 0 ? ( | |
| <p class="text-sm text-slate-500 dark:text-text-dim">{t("noChanges")}</p> | |
| ) : ( | |
| <> | |
| <p class="text-sm font-medium"> | |
| {diff.changes.length} {t("changesCount")} | |
| {diff.unchanged > 0 && ( | |
| <span class="text-slate-400 dark:text-text-dim ml-2"> | |
| ({diff.unchanged} unchanged) | |
| </span> | |
| )} | |
| </p> | |
| <div class="max-h-60 overflow-y-auto border border-gray-100 dark:border-border-dark rounded-lg"> | |
| {diff.changes.map((c) => ( | |
| <div | |
| key={c.accountId} | |
| class="flex items-center justify-between px-3 py-2 text-xs border-b border-gray-50 dark:border-border-dark/50" | |
| > | |
| <span class="font-medium truncate flex-1 text-slate-700 dark:text-text-main"> | |
| {c.email} | |
| </span> | |
| <span class="flex items-center gap-1.5 shrink-0 ml-2"> | |
| <span class="text-slate-400 dark:text-text-dim">{c.from}</span> | |
| <svg class="size-3 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /> | |
| </svg> | |
| <span class="text-primary font-medium">{c.to}</span> | |
| </span> | |
| </div> | |
| ))} | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| )} | |
| {/* Actions */} | |
| <div class="flex items-center justify-end gap-2 mt-4"> | |
| <button | |
| onClick={handleClose} | |
| class="px-4 py-2 text-sm rounded-lg border border-gray-200 dark:border-border-dark hover:bg-slate-50 dark:hover:bg-border-dark transition-colors" | |
| > | |
| {t("cancelBtn")} | |
| </button> | |
| {diff && diff.changes.length > 0 && ( | |
| <button | |
| onClick={handleApply} | |
| disabled={importing} | |
| class="px-4 py-2 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary-hover transition-colors disabled:opacity-50" | |
| > | |
| {importing ? "..." : t("confirmApply")} | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| ); | |
| } | |