"use client"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import { Plus, ScanSearch, Settings2 } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { TopicFormDialog } from "@/components/inspiration/topic-form-dialog"; import { EmptyState } from "@/components/layout/empty-state"; import { PageHeader } from "@/components/layout/page-header"; import { notify } from "@/lib/notifications/store"; import { TOPIC_GOAL_LABELS, isPlacementGoal, parseTopicGoal, type TopicGoal, } from "@/lib/types/topic-goal"; import { parseFetchJson } from "@/lib/utils"; interface TopicListItem { id: string; label: string; query: string; topicGoal: string; scanCount: number; latestScan: { id: string; createdAt: string; itemCount: number; } | null; } export default function ScansPage() { const searchParams = useSearchParams(); const [topics, setTopics] = useState([]); const mode: TopicGoal = parseTopicGoal(searchParams.get("mode")); const [search, setSearch] = useState(""); const [createOpen, setCreateOpen] = useState(false); const [loading, setLoading] = useState(true); async function load() { setLoading(true); try { const res = await fetch("/api/topics"); const data = await parseFetchJson<{ topics?: TopicListItem[]; error?: string }>(res); if (!res.ok) { notify({ type: "error", title: "載入主題失敗", message: data.error }); setTopics([]); return; } setTopics(data.topics ?? []); } catch (err) { const message = err instanceof Error ? err.message : "載入失敗"; notify({ type: "error", title: "載入找靈感失敗", message }); setTopics([]); } finally { setLoading(false); } } useEffect(() => { load(); }, []); const modeTopics = useMemo( () => topics.filter((t) => isPlacementGoal(t.topicGoal) ? mode === "placement" : mode === "viral" ), [topics, mode] ); const filteredTopics = useMemo(() => { const q = search.trim().toLowerCase(); if (!q) return modeTopics; return modeTopics.filter( (t) => t.label.toLowerCase().includes(q) || t.query.toLowerCase().includes(q) ); }, [modeTopics, search]); return (
setCreateOpen(true)}> 新增任務 } />
setSearch(e.target.value)} placeholder="搜尋主題名稱或種子關鍵字…" className="max-w-sm" />
{loading ? (
載入中…
) : filteredTopics.length === 0 ? ( setCreateOpen(true)}> 新增任務 ) : ( ) } /> ) : (
{filteredTopics.map((topic, i) => { const goal = isPlacementGoal(topic.topicGoal) ? "placement" : "viral"; const hasScans = topic.scanCount > 0; return (
{topic.label} {topic.query}
{TOPIC_GOAL_LABELS[goal]}
{topic.latestScan ? (

最近海巡 {new Date(topic.latestScan.createdAt).toLocaleString("zh-TW")} {" · "} {topic.latestScan.itemCount} 篇 {" · "} 共 {topic.scanCount} 次海巡

) : (

尚未海巡

)}
{hasScans ? ( ) : ( )}
); })}
)}
); }