Files
buzz-sheet/components/public/team-search.tsx
T
gunshiz e3692fe75b
CI / Verify (push) Successful in 59s
CI / Build immutable images and deploy (push) Failing after 1m4s
feat : better ui
2026-08-31 00:48:33 +07:00

185 lines
8.2 KiB
TypeScript

"use client";
import Image from "next/image";
import { DownloadIcon, SearchIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { buttonVariants } from "@/components/ui/button";
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Kbd } from "@/components/ui/kbd";
import { Separator } from "@/components/ui/separator";
import { isCustomTravelerKey } from "@/lib/catalog/travelers";
import { rarityGradientClass } from "@/lib/catalog/rarity";
import type { PublicGuide } from "@/lib/guides/queries";
import { cn } from "@/lib/utils";
export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
const [query, setQuery] = useState("");
const normalizedQuery = query.trim().toLocaleLowerCase();
const filteredTeams = useMemo(
() => teams.filter((team) => {
if (!normalizedQuery) return true;
const searchableNames = [
team.title,
...team.members.flatMap((member) => [
member.character.name,
member.weapon?.name,
member.artifact?.name,
]),
].filter((name): name is string => Boolean(name));
return searchableNames.some((name) => name.toLocaleLowerCase().includes(normalizedQuery));
}),
[normalizedQuery, teams],
);
return (
<div className="flex flex-col gap-4">
<div className="relative">
<SearchIcon aria-hidden="true" className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
aria-label="ค้นหาทีม ตัวละคร อาวุธ หรืออาร์ติแฟกต์"
className="pl-9"
placeholder="ค้นหาทีม ตัวละคร อาวุธ หรืออาร์ติแฟกต์"
value={query}
onValueChange={setQuery}
/>
</div>
{filteredTeams.length ? (
<div className="grid gap-5 xl:grid-cols-2">
{filteredTeams.map((team) => {
const total = team.members.reduce(
(sum, member) => sum + Number(member.damage),
0,
);
const seconds = Number(team.rotationSeconds);
return (
<Card key={team.id}>
<CardHeader>
<CardTitle>{team.title}</CardTitle>
<CardDescription>ระยะเวลาโรเทชั่น {seconds} วิ</CardDescription>
<CardAction>
<a
href={`/api/teams/${team.id}/image`}
download
className={buttonVariants({
variant: "outline",
size: "sm",
})}
>
<DownloadIcon data-icon="inline-start" />
ส่งออกภาพ
</a>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="grid grid-cols-4 gap-1 sm:gap-3">
{team.members.map((member) => (
<div key={member.position} className="min-w-0 text-center">
<div className="grid grid-cols-3 gap-2">
<div className={cn("relative col-span-2 row-span-2 min-h-0 overflow-hidden rounded-lg bg-muted", rarityGradientClass(isCustomTravelerKey(member.character.key) ? 5 : member.character.rarity))}>
<Image
src={member.character.imageUrl}
alt={member.character.name}
fill
className="object-contain"
/>
<Kbd className="absolute top-0 left-0 bg-transparent text-white">
C{member.constellation}
</Kbd>
</div>
<div className="col-span-1 row-span-2 flex min-w-0 flex-col gap-2">
{member.weapon ? (
<div
className={cn(
"relative aspect-square overflow-hidden rounded-md bg-muted",
rarityGradientClass(member.weapon.rarity),
)}
>
<Image
src={member.weapon.imageUrl}
alt={member.weapon.name}
fill
className="object-contain p-1"
/>
<Kbd className="absolute top-0 left-0 bg-transparent text-white">
R{member.refinement}
</Kbd>
</div>
) : (
<div className="aspect-square rounded-md border bg-muted" />
)}
{member.artifact ? (
<div
className={cn(
"relative aspect-square overflow-hidden rounded-md bg-muted",
rarityGradientClass(member.artifact.rarity),
)}
>
<Image
src={member.artifact.imageUrl}
alt={member.artifact.name}
fill
className="object-contain p-1"
/>
</div>
) : (
<div className="aspect-square rounded-md border bg-muted" />
)}
</div>
</div>
<p className="mt-2 truncate text-sm font-medium">
{member.character.name}
</p>
<p className="text-sm font-semibold">
{Number(member.damage).toLocaleString(undefined, {
maximumFractionDigits: 2,
})}
</p>
<p className="text-xs text-muted-foreground">
{total
? ((Number(member.damage) / total) * 100).toFixed(2)
: "0.00"}
%
</p>
</div>
))}
</div>
<Separator />
<div className="flex items-end justify-between">
<div>
<p className="text-sm text-muted-foreground">
ดาเมจรวม
</p>
<p className="font-semibold">{total.toLocaleString()}</p>
</div>
<div className="text-right">
<p className="text-sm text-muted-foreground">DPS</p>
<p className="text-3xl font-semibold text-primary">
{seconds > 0
? (total / seconds).toLocaleString(undefined, {
maximumFractionDigits: 0,
})
: "0"}
</p>
</div>
</div>
<p className="rounded-lg bg-muted p-3 text-sm leading-6">
{team.combo}
</p>
</CardContent>
</Card>
);
})}
</div>
) : (
<Card>
<CardContent className="py-8 text-center text-sm text-muted-foreground">
ไม่พบทีมที่ตรงกับคำค้นหา
</CardContent>
</Card>
)}
</div>
);
}