114 lines
5.1 KiB
TypeScript
114 lines
5.1 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
import Link from "next/link";
|
||
import { CategoryTabs } from "@/components/sections";
|
||
import { AppResource } from "@/lib/types";
|
||
|
||
export function AppsExplorer({ apps }: { apps: AppResource[] }) {
|
||
const [query, setQuery] = useState("");
|
||
const [category, setCategory] = useState("all");
|
||
const [sort, setSort] = useState("latest");
|
||
const [count, setCount] = useState(6);
|
||
|
||
const filtered = useMemo(() => {
|
||
let list = [...apps].filter((a) =>
|
||
[a.name, a.subtitle, ...a.tags].join(" ").toLowerCase().includes(query.toLowerCase())
|
||
);
|
||
if (category !== "all") list = list.filter((a) => a.category === category);
|
||
list.sort((a, b) => {
|
||
if (sort === "hot") return b.downloads - a.downloads;
|
||
if (sort === "update") return b.updatedAt.localeCompare(a.updatedAt);
|
||
return b.uploadedAt.localeCompare(a.uploadedAt);
|
||
});
|
||
return list;
|
||
}, [apps, query, category, sort]);
|
||
|
||
return (
|
||
<section className="space-y-4 md:space-y-5">
|
||
<div className="panel space-y-4">
|
||
<div className="grid gap-3 md:grid-cols-12 md:items-center">
|
||
<div className="md:col-span-8">
|
||
<input
|
||
className="input-fancy"
|
||
placeholder="搜索资源、标签或名称"
|
||
value={query}
|
||
onChange={(e) => setQuery(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="flex items-center justify-between rounded-xl border border-white/10 px-3 py-2 text-xs text-slate-300 md:col-span-4 md:text-sm">
|
||
<span>已找到资源</span>
|
||
<strong className="text-cyan-200">{filtered.length}</strong>
|
||
</div>
|
||
</div>
|
||
<CategoryTabs active={category} onChange={setCategory} />
|
||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||
<span className="text-slate-300">排序方式:</span>
|
||
{[
|
||
{ id: "latest", label: "最新上架" },
|
||
{ id: "hot", label: "热度优先" },
|
||
{ id: "update", label: "最近更新" },
|
||
].map((s) => (
|
||
<button key={s.id} className={`pill ${sort === s.id ? "pill-active" : ""}`} onClick={() => setSort(s.id)}>
|
||
{s.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{filtered.length > 0 ? (
|
||
<div className="space-y-3">
|
||
{filtered.slice(0, count).map((app) => (
|
||
<div key={app.id} className="panel-strong flex min-w-0 flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||
<div className="flex min-w-0 flex-1 items-start gap-3">
|
||
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-xl border border-white/10 bg-slate-900/50 text-lg">{app.icon}</span>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<Link href={`/apps/${app.slug}`} className="truncate text-sm font-semibold transition hover:text-cyan-300 md:text-base">
|
||
{app.name}
|
||
</Link>
|
||
<span className="rounded-md border border-cyan-300/35 bg-cyan-400/10 px-1.5 py-0.5 text-[10px] text-cyan-100">{app.version}</span>
|
||
</div>
|
||
<p className="mt-1 truncate text-xs text-slate-400 md:text-sm">{app.subtitle}</p>
|
||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||
{app.tags.slice(0, 3).map((tag) => (
|
||
<span key={tag} className="pill py-1">{tag}</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-2 text-xs text-slate-400 md:w-[320px] md:text-sm">
|
||
<div className="rounded-lg border border-white/10 px-2 py-2 text-center">
|
||
<p className="text-[10px] uppercase tracking-wide text-slate-500">更新</p>
|
||
<p className="mt-1 text-slate-200">{app.updatedAt}</p>
|
||
</div>
|
||
<div className="rounded-lg border border-white/10 px-2 py-2 text-center">
|
||
<p className="text-[10px] uppercase tracking-wide text-slate-500">体积</p>
|
||
<p className="mt-1 text-slate-200">{app.size}</p>
|
||
</div>
|
||
<div className="rounded-lg border border-white/10 px-2 py-2 text-center">
|
||
<p className="text-[10px] uppercase tracking-wide text-slate-500">下载</p>
|
||
<p className="mt-1 text-slate-200">{app.downloads}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="panel text-center">
|
||
<p className="text-sm text-slate-300">没有找到匹配资源,换个关键词试试。</p>
|
||
<button onClick={() => setQuery("")} className="mt-3 cta-sub">清空搜索</button>
|
||
</div>
|
||
)}
|
||
|
||
{count < filtered.length && (
|
||
<div className="pt-1">
|
||
<button onClick={() => setCount((c) => c + 6)} className="mx-auto block cta-sub">
|
||
加载更多资源
|
||
</button>
|
||
</div>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|