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

205 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}