File size: 7,707 Bytes
4f2665c
4a940a5
b7d4394
268c5a4
4f2665c
b7d4394
 
 
 
 
e6a1587
 
 
4ebb914
 
268c5a4
 
b7d4394
 
268c5a4
b7d4394
e6a1587
268c5a4
4f2665c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268c5a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e6a1587
 
 
 
b7d4394
 
 
e6a1587
b7d4394
 
 
 
e6a1587
 
 
 
 
 
7516302
 
 
 
 
 
df56b50
 
 
 
 
 
268c5a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e6a1587
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b7d4394
4f2665c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b7d4394
 
 
 
 
 
 
 
 
 
 
268c5a4
b7d4394
 
 
 
 
 
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
import { useState, useCallback, useEffect } from "preact/hooks";
import { useI18n, useT } from "../../../shared/i18n/context";
import { AccountCard } from "./AccountCard";
import { AccountImportExport } from "./AccountImportExport";
import type { Account, ProxyEntry, QuotaWarning } from "../../../shared/types";

interface AccountListProps {
  accounts: Account[];
  loading: boolean;
  onDelete: (id: string) => Promise<string | null>;
  onRefresh: () => void;
  refreshing: boolean;
  lastUpdated: Date | null;
  proxies?: ProxyEntry[];
  onProxyChange?: (accountId: string, proxyId: string) => void;
  onExport?: (selectedIds?: string[]) => Promise<void>;
  onImport?: (file: File) => Promise<{ success: boolean; added: number; updated: number; failed: number; errors: string[] }>;
}

export function AccountList({ accounts, loading, onDelete, onRefresh, refreshing, lastUpdated, proxies, onProxyChange, onExport, onImport }: AccountListProps) {
  const t = useT();
  const { lang } = useI18n();
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
  const [warnings, setWarnings] = useState<QuotaWarning[]>([]);

  // Poll quota warnings
  useEffect(() => {
    const fetchWarnings = async () => {
      try {
        const resp = await fetch("/auth/quota/warnings");
        const data = await resp.json();
        setWarnings(data.warnings || []);
      } catch { /* ignore */ }
    };
    fetchWarnings();
    const timer = setInterval(fetchWarnings, 30_000);
    return () => clearInterval(timer);
  }, []);

  const toggleSelect = useCallback((id: string) => {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }, []);

  const toggleSelectAll = useCallback(() => {
    setSelectedIds((prev) => {
      if (prev.size === accounts.length) return new Set();
      return new Set(accounts.map((a) => a.id));
    });
  }, [accounts]);

  const updatedAtText = lastUpdated
    ? lastUpdated.toLocaleTimeString(lang === "zh" ? "zh-CN" : "en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })
    : null;

  return (
    <section class="flex flex-col gap-4">
      <div class="flex items-center justify-between">
        <div class="flex flex-col gap-1">
          <h2 class="text-[0.95rem] font-bold tracking-tight">{t("connectedAccounts")}</h2>
          <p class="text-slate-500 dark:text-text-dim text-[0.8rem]">{t("connectedAccountsDesc")}</p>
        </div>
        <div class="flex items-center gap-2 shrink-0">
          {updatedAtText && (
            <span class="text-[0.75rem] text-slate-400 dark:text-text-dim hidden sm:inline">
              {t("updatedAt")} {updatedAtText}
            </span>
          )}
          <a
            href="#/account-management"
            class="text-[0.75rem] text-primary hover:text-primary/80 font-medium transition-colors"
          >
            {t("manageAccounts")} &rarr;
          </a>
          <a
            href="#/usage-stats"
            class="text-[0.75rem] text-primary hover:text-primary/80 font-medium transition-colors"
          >
            {t("usageStats")} &rarr;
          </a>
          {onExport && onImport && (
            <AccountImportExport onExport={onExport} onImport={onImport} selectedIds={selectedIds} />
          )}
          {accounts.length > 0 && (
            <button
              onClick={toggleSelectAll}
              title={selectedIds.size === accounts.length ? t("deselectAll") : t("selectAll")}
              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">
                {selectedIds.size === accounts.length ? (
                  <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
                ) : (
                  <path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
                )}
              </svg>
            </button>
          )}
          <button
            onClick={onRefresh}
            disabled={refreshing}
            title={t("refresh")}
            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 disabled:cursor-not-allowed"
          >
            <svg
              class={`size-[18px] ${refreshing ? "animate-spin" : ""}`}
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="1.5"
            >
              <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
            </svg>
          </button>
        </div>
      </div>
      {/* Quota warning banners */}
      {warnings.filter((w) => w.level === "critical").length > 0 && (
        <div class="px-4 py-2.5 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800/30 text-red-700 dark:text-red-400 text-sm flex items-center gap-2">
          <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
          </svg>
          <span>
            {t("quotaCriticalWarning").replace("{count}", String(warnings.filter((w) => w.level === "critical").length))}
          </span>
        </div>
      )}
      {warnings.filter((w) => w.level === "warning").length > 0 && warnings.filter((w) => w.level === "critical").length === 0 && (
        <div class="px-4 py-2.5 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/30 text-amber-700 dark:text-amber-400 text-sm flex items-center gap-2">
          <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
          </svg>
          <span>
            {t("quotaWarning").replace("{count}", String(warnings.filter((w) => w.level === "warning").length))}
          </span>
        </div>
      )}
      <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
        {loading ? (
          <div class="md:col-span-2 text-center py-8 text-slate-400 dark:text-text-dim text-sm bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl transition-colors">
            {t("loadingAccounts")}
          </div>
        ) : accounts.length === 0 ? (
          <div class="md:col-span-2 text-center py-8 text-slate-400 dark:text-text-dim text-sm bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl transition-colors">
            {t("noAccounts")}
          </div>
        ) : (
          accounts.map((acct, i) => (
            <AccountCard key={acct.id} account={acct} index={i} onDelete={onDelete} proxies={proxies} onProxyChange={onProxyChange} selected={selectedIds.has(acct.id)} onToggleSelect={toggleSelect} />
          ))
        )}
      </div>
    </section>
  );
}