"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { FavoriteButton } from "@/components/common/FavoriteButton"; import { HeroBanner } from "@/components/common/HeroBanner"; import StatsSummary from "@/components/common/StatsSummary"; import { PRICING_LABEL, PRICING_COLOR } from "@/lib/constants"; import Image from "next/image"; interface Tool { id: number; name: string; slug: string; description: string; logoUrl: string | null; websiteUrl: string; pricingModel: string; viewCount: number; } interface TrendingTool { id: number; name: string; slug: string; description: string; logoUrl: string | null; websiteUrl: string; pricingModel: string; viewCount: number; category: { name: string; slug: string } | null; } interface FlatCategory { id: number; name: string; slug: string; icon: string | null; parentId: number | null; toolCount: number; tools: Tool[]; } interface ParentCategory { id: number; name: string; slug: string; icon: string | null; } interface HomePageClientProps { flatCategories: FlatCategory[]; parentCategories: ParentCategory[]; totalTools: number; totalSkills: number; trendingTools: TrendingTool[]; } export default function HomePageClient({ flatCategories: categories, parentCategories, totalTools, totalSkills, trendingTools, }: HomePageClientProps) { const router = useRouter(); const [activeSection, setActiveSection] = useState(""); const [search, setSearch] = useState(""); const [mobileNavOpen, setMobileNavOpen] = useState(false); const [activeTabs, setActiveTabs] = useState>({}); const sectionRefs = useRef>({}); useEffect(() => { if (categories.length === 0) return; const observer = new IntersectionObserver( (entries) => { const visible = entries .filter((e) => e.isIntersecting) .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top); if (visible.length > 0) setActiveSection(visible[0].target.id); }, { rootMargin: "-80px 0px -60% 0px", threshold: 0 } ); Object.values(sectionRefs.current).forEach((el) => { if (el) observer.observe(el); }); return () => observer.disconnect(); }, [categories]); const scrollToSection = useCallback((id: string) => { const el = sectionRefs.current[id]; if (el) { const top = el.getBoundingClientRect().top + window.scrollY - 80; window.scrollTo({ top, behavior: "smooth" }); } setMobileNavOpen(false); }, []); const handleSearch = () => { if (search.trim()) { router.push(`/tools?search=${encodeURIComponent(search.trim())}`); } }; const getActiveTab = (parentId: number, children: FlatCategory[]) => { if (activeTabs[parentId]) return activeTabs[parentId]; return "all"; }; const setActiveTab = (parentId: number, slug: string) => { setActiveTabs((prev) => ({ ...prev, [parentId]: slug })); }; const groupedCategories = parentCategories.map((parent) => { const ownCat = categories.find((c) => c.id === parent.id); const children = categories.filter((c) => c.parentId === parent.id); return { parent, ownCat, children }; }); const isEmpty = categories.length === 0 && parentCategories.length === 0; if (isEmpty) { return (
{Array.from({ length: 4 }).map((_, i) => (
{Array.from({ length: 4 }).map((_, j) => (
))}
))}
); } const renderSidebar = () => ( ); return (
setSearch(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} placeholder="搜索AI工具..." className="input-field !pl-10 !h-10 !rounded-xl" />
{mobileNavOpen && (
{renderSidebar()}
)} {trendingTools.length > 0 && (
近期热门
最近加入 · 最受关注
{trendingTools.map((tool) => (
{tool.logoUrl && ( { const target = e.target as HTMLImageElement; target.style.display = "none"; const fallback = target.nextElementSibling as HTMLElement | null; if (fallback) fallback.classList.remove("hidden"); }} /> )}
{tool.name.slice(0, 2)}

{tool.name}

{tool.category && ( {tool.category.name} )}

{tool.description}

{PRICING_LABEL[tool.pricingModel] || tool.pricingModel} {tool.viewCount} 次浏览
))}
)} {groupedCategories.map(({ parent, ownCat, children }) => { const allCatsInGroup = [ownCat, ...children].filter(Boolean) as FlatCategory[]; if (allCatsInGroup.length === 0) return null; const hasChildren = children.length > 0; const activeTab = getActiveTab(parent.id, children); const totalCount = allCatsInGroup.reduce((s, c) => s + c.toolCount, 0); const allTools = allCatsInGroup.flatMap((c) => c.tools); const displayTools = activeTab === "all" ? allTools : children.find((c) => c.slug === activeTab)?.tools || ownCat?.tools || []; const maxDisplay = 8; const visibleTools = displayTools.slice(0, maxDisplay); const hasMore = displayTools.length > maxDisplay; return (
{ sectionRefs.current[`cat-${parent.slug}`] = el; }} className="mb-8" >

{parent.name}

{totalCount}
{hasChildren && (
{children.map((child) => ( ))}
)} {visibleTools.length > 0 ? (
{visibleTools.map((tool) => (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); } }} onClick={(e) => e.stopPropagation()}>
{tool.logoUrl ? (
{tool.name} { const target = e.target as HTMLImageElement; target.style.display = 'none'; const parent = target.parentElement; if (parent) { const fallback = parent.nextElementSibling as HTMLElement; if (fallback) fallback.classList.remove('hidden'); } }} />
) : null}
{tool.name.charAt(0)}

{tool.name}

{PRICING_LABEL[tool.pricingModel] || tool.pricingModel}

{tool.description || "暂无简介"}

))}
) : (
敬请期待
)} {hasMore && (
c.slug === activeTab)?.slug || parent.slug}`} className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors" > 查看全部 {displayTools.length} 个 →
)}
); })}
); }