icebear0828
refactor: migrate electron from branch to npm workspace
178e38e
raw
history blame
6.98 kB
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>
)}
</>
);
}