File size: 3,627 Bytes
268c5a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useState, useCallback, useRef } from "preact/hooks";
import { useT } from "../../../shared/i18n/context";

interface ImportResult {
  success: boolean;
  added: number;
  updated: number;
  failed: number;
  errors: string[];
}

interface AccountImportExportProps {
  onExport: (selectedIds?: string[]) => Promise<void>;
  onImport: (file: File) => Promise<ImportResult>;
  selectedIds: Set<string>;
}

export function AccountImportExport({ onExport, onImport, selectedIds }: AccountImportExportProps) {
  const t = useT();
  const fileRef = useRef<HTMLInputElement>(null);
  const [importing, setImporting] = useState(false);
  const [result, setResult] = useState<string | null>(null);

  const handleExport = useCallback(async () => {
    try {
      const ids = selectedIds.size > 0 ? [...selectedIds] : undefined;
      await onExport(ids);
    } catch (err) {
      console.error("[AccountExport] failed:", err);
    }
  }, [onExport, selectedIds]);

  const handleFileChange = useCallback(async () => {
    const files = fileRef.current?.files;
    if (!files || files.length === 0) return;

    setImporting(true);
    setResult(null);
    try {
      let totalAdded = 0, totalUpdated = 0, totalFailed = 0;
      for (const file of files) {
        const res = await onImport(file);
        totalAdded += res.added;
        totalUpdated += res.updated;
        totalFailed += res.failed;
      }
      const msg = t("accountImportResult")
        .replace("{added}", String(totalAdded))
        .replace("{updated}", String(totalUpdated))
        .replace("{failed}", String(totalFailed));
      setResult(msg);
    } catch {
      setResult(t("accountImportError"));
    } finally {
      setImporting(false);
      if (fileRef.current) fileRef.current.value = "";
    }
  }, [onImport, t]);

  const triggerFileSelect = useCallback(() => {
    fileRef.current?.click();
  }, []);

  const exportTitle = selectedIds.size > 0
    ? `${t("exportBtn")} (${selectedIds.size})`
    : t("exportBtn");

  return (
    <>
      <input
        ref={fileRef}
        type="file"
        accept=".json"
        multiple
        onChange={handleFileChange}
        class="hidden"
      />
      <button
        onClick={triggerFileSelect}
        disabled={importing}
        title={t("importBtn")}
        class="p-1.5 text-slate-400 dark:text-text-dim hover:text-primary transition-colors rounded-md hover:bg-primary/10 disabled:opacity-40"
      >
        <svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
          <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" />
        </svg>
      </button>
      <button
        onClick={handleExport}
        title={exportTitle}
        class="p-1.5 text-slate-400 dark:text-text-dim hover:text-primary transition-colors rounded-md hover:bg-primary/10"
      >
        <svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
          <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12M12 16.5V3" />
        </svg>
      </button>
      {selectedIds.size > 0 && (
        <span class="text-[0.7rem] text-primary font-medium hidden sm:inline">
          {selectedIds.size}
        </span>
      )}
      {result && (
        <span class="text-[0.75rem] text-slate-500 dark:text-text-dim hidden sm:inline">
          {result}
        </span>
      )}
    </>
  );
}