haixunMaster/components/accounts/account-connection-card.tsx

205 lines
7.1 KiB
TypeScript
Raw Normal View History

2026-06-21 12:50:31 +00:00
"use client";
import { useCallback, useEffect, useState } from "react";
import {
CheckCircle2,
Eraser,
Link2,
Loader2,
Unlink,
XCircle,
} from "lucide-react";
import { ChromeSessionSync } from "@/components/connections/chrome-session-sync";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { notify } from "@/lib/notifications/store";
import { cn } from "@/lib/utils";
interface SessionStatus {
synced?: boolean;
valid: boolean;
username?: string | null;
message: string;
}
interface ThreadsStatus {
connected: boolean;
accountName?: string | null;
tokenExpiresAt?: string | null;
appIdConfigured?: boolean;
}
export function AccountConnectionCard() {
const [session, setSession] = useState<SessionStatus | null>(null);
const [threads, setThreads] = useState<ThreadsStatus | null>(null);
const [clearingSession, setClearingSession] = useState(false);
const loadConnections = useCallback(async () => {
try {
const [sessionRes, threadsRes] = await Promise.all([
fetch("/api/session/status"),
fetch("/api/threads/status"),
]);
const [sessionData, threadsData] = await Promise.all([
sessionRes.json().catch(() => null),
threadsRes.json().catch(() => null),
]);
if (sessionData) setSession(sessionData);
if (threadsData) setThreads(threadsData);
} catch {
// 網路異常時保持現狀,不阻塞頁面
}
}, []);
useEffect(() => {
loadConnections();
}, [loadConnections]);
async function handleClearSession() {
setClearingSession(true);
try {
const res = await fetch("/api/session/clear", { method: "POST" });
const data = await res.json().catch(() => ({}));
notify({
type: data.cleared ? "success" : "info",
title: data.cleared ? "已清除瀏覽器 session" : "無需清除",
message: data.message,
});
window.dispatchEvent(new CustomEvent("haixun:accounts-updated"));
loadConnections();
} catch {
notify({ type: "error", title: "清除失敗", message: "網路連線異常,請稍後再試" });
} finally {
setClearingSession(false);
}
}
async function handleBindApi() {
if (!threads?.appIdConfigured) {
notify({
type: "warning",
title: "無法綁定官方 API",
message: "請由管理員在 server 的 .env 設定 THREADS_APP_ID 與 THREADS_APP_SECRET。",
});
return;
}
const res = await fetch("/api/accounts/bind", { method: "POST" });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
notify({ type: "error", title: "無法開始綁定", message: data.error });
return;
}
window.location.href = "/api/threads/oauth/authorize";
}
async function handleDisconnectApi() {
try {
const res = await fetch("/api/threads/disconnect", { method: "POST" });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
notify({ type: "error", title: "中斷失敗", message: data.error });
return;
}
notify({ type: "success", title: "已中斷官方 API 連線" });
window.dispatchEvent(new CustomEvent("haixun:accounts-updated"));
loadConnections();
} catch {
notify({ type: "error", title: "中斷失敗", message: "網路連線異常,請稍後再試" });
}
}
const sessionSynced = session?.synced ?? session?.valid;
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
Threads Chrome API OAuth
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div
className={cn(
"rounded-lg border p-4",
sessionSynced ? "border-success-border bg-success-bg" : "border-warning-border bg-warning-bg"
)}
>
<div className="flex items-start gap-3">
{sessionSynced ? (
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-success" />
) : (
<XCircle className="mt-0.5 h-5 w-5 shrink-0 text-warning" />
)}
<div className="min-w-0 flex-1 space-y-3">
<div>
<p className="text-sm font-semibold">Chrome Session </p>
<p className="mt-0.5 text-xs text-muted-foreground">
{sessionSynced
? `已同步${session?.username ? ` · @${session.username}` : ""}`
: "尚未同步 — 請在 Chrome 登入 threads.com 後按下方按鈕"}
</p>
{session?.message && (
<p className="mt-1 text-[11px] text-muted-foreground">{session.message}</p>
)}
</div>
<ChromeSessionSync onSynced={loadConnections} />
<p className="text-[11px] leading-relaxed text-muted-foreground">
Chrome {" "}
<code className="rounded bg-muted px-1">extension/haixun-threads-sync</code>
</p>
</div>
</div>
</div>
<div className="flex items-center justify-between gap-3 rounded-lg border p-3.5">
<div className="flex items-center gap-3">
{threads?.connected ? (
<CheckCircle2 className="h-5 w-5 shrink-0 text-success" />
) : (
<XCircle className="h-5 w-5 shrink-0 text-warning" />
)}
<div className="min-w-0">
<p className="text-sm font-semibold">Threads API</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{threads?.connected
? `已授權${threads.accountName ? ` · @${threads.accountName}` : ""}`
: "尚未授權選用API 模式才需要)"}
</p>
</div>
</div>
{threads?.connected ? (
<Button size="sm" variant="outline" onClick={handleDisconnectApi}>
<Unlink className="h-4 w-4" />
</Button>
) : (
<Button size="sm" variant="outline" onClick={handleBindApi}>
<Link2 className="h-4 w-4" />
OAuth
</Button>
)}
</div>
<div className="flex items-center justify-between gap-3 border-t border-border pt-3">
<p className="text-xs text-muted-foreground">
session API token
</p>
<Button
size="sm"
variant="ghost"
disabled={clearingSession}
onClick={handleClearSession}
>
{clearingSession ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Eraser className="h-4 w-4" />
)}
session
</Button>
</div>
</CardContent>
</Card>
);
}