Files
gitlab-instance-0a899031_di…/app/components/ResourceNavigation.tsx
2026-03-12 02:23:10 -05:00

276 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { useState, useMemo, useEffect } from "react";
import { useTranslation } from "@/i18n/navigation";
import type { ResourceData } from "@/app/lib/theme-data";
const PER_PAGE_MOBILE = 4;
const PER_PAGE_PC = 8;
const BREAKPOINT = 768;
function useIsPC() {
const [isPC, setIsPC] = useState(false);
useEffect(() => {
const mq = window.matchMedia(`(min-width: ${BREAKPOINT}px)`);
setIsPC(mq.matches);
const fn = () => setIsPC(mq.matches);
mq.addEventListener("change", fn);
return () => mq.removeEventListener("change", fn);
}, []);
return isPC;
}
/** 每页条数H5=4PC=8总数不足则全显 */
function getPerPage(total: number, isPC: boolean): number {
const cap = isPC ? PER_PAGE_PC : PER_PAGE_MOBILE;
return total <= cap ? total : cap;
}
type ResourceNavigationProps = {
data: ResourceData;
};
export default function ResourceNavigation({ data }: ResourceNavigationProps) {
const { t } = useTranslation("community");
const isPC = useIsPC();
const firstCategoryId = data.pan.categories[0]?.id ?? "";
const [activePanCategory, setActivePanCategory] = useState(firstCategoryId);
const [booksPage, setBooksPage] = useState(0);
const [productsPage, setProductsPage] = useState(0);
const [toast, setToast] = useState(false);
const showUpdating = () => {
setToast(true);
setTimeout(() => setToast(false), 2000);
};
const activeCategory = data.pan.categories.find((c) => c.id === activePanCategory);
const booksPerPage = getPerPage(data.books.length, isPC);
const productsPerPage = getPerPage(data.products.length, isPC);
const booksTotalPages = Math.ceil(data.books.length / booksPerPage) || 1;
const productsTotalPages = Math.ceil(data.products.length / productsPerPage) || 1;
useEffect(() => {
setBooksPage((p) => Math.max(0, Math.min(p, booksTotalPages - 1)));
}, [booksTotalPages]);
useEffect(() => {
setProductsPage((p) => Math.max(0, Math.min(p, productsTotalPages - 1)));
}, [productsTotalPages]);
const paginatedBooks = useMemo(() => {
const start = booksPage * booksPerPage;
return data.books.slice(start, start + booksPerPage);
}, [data.books, booksPage, booksPerPage]);
const paginatedProducts = useMemo(() => {
const start = productsPage * productsPerPage;
return data.products.slice(start, start + productsPerPage);
}, [data.products, productsPage, productsPerPage]);
return (
<div className="mt-12 space-y-16">
{/* 图书 - 封面+书名,数量少则大封面,多则分页 */}
<div>
<h3 className="mb-4 flex items-center gap-2 text-xl font-bold text-slate-900 dark:text-slate-100">
<span>📚</span>
{t("resourceSections.books")}
{data.books.length > 0 && (
<span className="text-sm font-normal text-slate-500 dark:text-slate-400">
{t("resourceSections.totalBooks").replace("{count}", String(data.books.length))}
</span>
)}
</h3>
<div
className="grid grid-cols-2 gap-3 gap-y-4 sm:gap-4 md:grid-cols-8"
>
{paginatedBooks.map((book, i) => (
<button
key={`book-${booksPage}-${i}`}
type="button"
onClick={showUpdating}
className="group flex cursor-not-allowed flex-col text-left"
>
<div
className="overflow-hidden rounded-lg border border-slate-200 bg-slate-100 shadow-sm transition-all group-hover:shadow-md dark:border-slate-700 dark:bg-slate-800"
style={{ aspectRatio: "2/3" }}
>
<img
src={book.cover}
alt={book.title}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
</div>
<p className="mt-2 line-clamp-2 text-center text-xs font-medium text-slate-700 dark:text-slate-300 sm:text-sm">
{book.title}
</p>
</button>
))}
</div>
{booksTotalPages > 1 && (
<div className="mt-6 flex items-center justify-center gap-2">
<button
type="button"
onClick={() => setBooksPage((p) => Math.max(0, p - 1))}
disabled={booksPage === 0}
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
aria-label="上一页"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<span className="px-4 text-sm text-slate-600 dark:text-slate-400">
{booksPage + 1} / {booksTotalPages}
</span>
<button
type="button"
onClick={() => setBooksPage((p) => Math.min(booksTotalPages - 1, p + 1))}
disabled={booksPage === booksTotalPages - 1}
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
aria-label="下一页"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
)}
</div>
{/* 商业数码 - 商品图+跳转电商 */}
<div>
<h3 className="mb-4 flex items-center gap-2 text-xl font-bold text-slate-900 dark:text-slate-100">
<span>🛒</span>
{t("resourceSections.products")}
{data.products.length > 0 && (
<span className="text-sm font-normal text-slate-500 dark:text-slate-400">
{t("resourceSections.totalProducts").replace("{count}", String(data.products.length))}
</span>
)}
</h3>
<div
className="grid grid-cols-2 gap-3 gap-y-4 sm:gap-4 md:grid-cols-8"
>
{paginatedProducts.map((product, i) => (
<button
key={`product-${productsPage}-${i}`}
type="button"
onClick={showUpdating}
className="group flex cursor-not-allowed flex-col text-left"
>
<div
className="overflow-hidden rounded-xl border border-slate-200 bg-white p-3 shadow-sm transition-all group-hover:shadow-md dark:border-slate-700 dark:bg-slate-900"
style={{ aspectRatio: "1/1" }}
>
<img
src={product.image}
alt={product.name}
className="h-full w-full object-contain transition-transform duration-300 group-hover:scale-105"
/>
</div>
<p className="mt-2 line-clamp-2 text-center text-xs font-medium text-slate-700 dark:text-slate-300 sm:text-sm">
{product.name}
</p>
</button>
))}
</div>
{productsTotalPages > 1 && (
<div className="mt-6 flex items-center justify-center gap-2">
<button
type="button"
onClick={() => setProductsPage((p) => Math.max(0, p - 1))}
disabled={productsPage === 0}
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
aria-label="上一页"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<span className="px-4 text-sm text-slate-600 dark:text-slate-400">
{productsPage + 1} / {productsTotalPages}
</span>
<button
type="button"
onClick={() => setProductsPage((p) => Math.min(productsTotalPages - 1, p + 1))}
disabled={productsPage === productsTotalPages - 1}
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
aria-label="下一页"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
)}
</div>
{/* 网盘 - 分类 Tab */}
<div>
<h3 className="mb-4 flex items-center gap-2 text-xl font-bold text-slate-900 dark:text-slate-100">
<span></span>
{t("resourceSections.pan")}
</h3>
<div className="rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
<div className="flex flex-wrap gap-1 border-b border-slate-200 p-2 dark:border-slate-700">
{data.pan.categories.map((cat) => (
<button
key={cat.id}
type="button"
onClick={() => setActivePanCategory(cat.id)}
className={`rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
activePanCategory === cat.id
? "bg-sky-100 text-sky-700 dark:bg-sky-900/50 dark:text-sky-300"
: "text-slate-600 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800"
}`}
>
<span className="mr-1.5">{cat.icon}</span>
{cat.title}
</button>
))}
</div>
<div className="p-4">
{activeCategory && (
<div
className="grid gap-3"
style={{
gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 260px), 1fr))",
}}
>
{activeCategory.items.map((item, i) => (
<a
key={i}
href={item.href}
target="_blank"
rel="noopener noreferrer sponsored"
className="flex items-center gap-3 rounded-xl border border-slate-100 p-3 transition-colors hover:border-sky-200 hover:bg-sky-50/50 dark:border-slate-700 dark:hover:border-sky-800 dark:hover:bg-sky-900/20"
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-slate-100 text-lg dark:bg-slate-800">
{activeCategory.icon}
</div>
<div className="min-w-0 flex-1">
<div className="font-medium text-slate-900 dark:text-slate-100">{item.name}</div>
<div className="text-xs text-slate-500 dark:text-slate-400">{item.desc}</div>
</div>
<span className="shrink-0 text-sm font-medium text-sky-600"></span>
</a>
))}
</div>
)}
</div>
</div>
</div>
{toast && (
<div
className="fixed bottom-8 left-1/2 z-50 -translate-x-1/2 rounded-lg bg-slate-800 px-6 py-3 text-sm font-medium text-white shadow-lg dark:bg-slate-700"
role="alert"
>
{t("updating")}
</div>
)}
</div>
);
}