feat : quick update
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
ListOrderedIcon,
|
ListOrderedIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
RefreshCcwIcon,
|
RefreshCcwIcon,
|
||||||
|
SearchIcon,
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
SwordIcon,
|
SwordIcon,
|
||||||
TimerIcon,
|
TimerIcon,
|
||||||
@@ -17,7 +18,7 @@ import {
|
|||||||
UserRoundIcon,
|
UserRoundIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useState, type ReactNode } from "react";
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
closestCenter,
|
closestCenter,
|
||||||
DndContext,
|
DndContext,
|
||||||
@@ -134,6 +135,10 @@ import {
|
|||||||
type TeamPreset,
|
type TeamPreset,
|
||||||
type TeamToolValue,
|
type TeamToolValue,
|
||||||
} from "@/lib/guides/team-tools";
|
} from "@/lib/guides/team-tools";
|
||||||
|
import {
|
||||||
|
matchesTeamSearch,
|
||||||
|
type TeamSearchTarget,
|
||||||
|
} from "@/lib/guides/team-search";
|
||||||
import {
|
import {
|
||||||
normalizeTalentPriorities,
|
normalizeTalentPriorities,
|
||||||
reorderTalentPriorities,
|
reorderTalentPriorities,
|
||||||
@@ -2069,6 +2074,35 @@ function TeamEditor({
|
|||||||
const [presetsHydrated, setPresetsHydrated] = useState(false);
|
const [presetsHydrated, setPresetsHydrated] = useState(false);
|
||||||
const [presetsGuideId, setPresetsGuideId] = useState<string | null>(null);
|
const [presetsGuideId, setPresetsGuideId] = useState<string | null>(null);
|
||||||
const [applyFields, setApplyFields] = useState<TeamApplyField[]>(["loadout"]);
|
const [applyFields, setApplyFields] = useState<TeamApplyField[]>(["loadout"]);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [searchTarget, setSearchTarget] = useState<TeamSearchTarget>("all");
|
||||||
|
const filteredTeams = useMemo(() => {
|
||||||
|
const characterNames = new Map(characters.map((item) => [item.key, item.name]));
|
||||||
|
const weaponNames = new Map(weapons.map((item) => [item.key, item.name]));
|
||||||
|
const artifactNames = new Map(artifacts.map((item) => [item.key, item.name]));
|
||||||
|
|
||||||
|
return teams
|
||||||
|
.map((team, teamIndex) => ({ team, teamIndex }))
|
||||||
|
.filter(({ team }) => matchesTeamSearch({
|
||||||
|
team: [team.title],
|
||||||
|
character: team.members.flatMap((member) => {
|
||||||
|
const name = characterNames.get(member.characterKey);
|
||||||
|
return name ? [name] : [];
|
||||||
|
}),
|
||||||
|
weapon: team.members.flatMap((member) => {
|
||||||
|
const name = member.weaponKey
|
||||||
|
? weaponNames.get(member.weaponKey)
|
||||||
|
: undefined;
|
||||||
|
return name ? [name] : [];
|
||||||
|
}),
|
||||||
|
artifact: team.members.flatMap((member) =>
|
||||||
|
[member.artifactKey, member.secondArtifactKey].flatMap((key) => {
|
||||||
|
const name = key ? artifactNames.get(key) : undefined;
|
||||||
|
return name ? [name] : [];
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}, searchQuery, searchTarget));
|
||||||
|
}, [artifacts, characters, searchQuery, searchTarget, teams, weapons]);
|
||||||
const value = {
|
const value = {
|
||||||
teams: teams.map((team) => ({
|
teams: teams.map((team) => ({
|
||||||
title: team.title,
|
title: team.title,
|
||||||
@@ -2213,13 +2247,51 @@ function TeamEditor({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<FieldGroup className="mt-4">
|
||||||
|
<Field>
|
||||||
|
<FieldLabel htmlFor="team-search">ค้นหา Team</FieldLabel>
|
||||||
|
<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
|
||||||
|
id="team-search"
|
||||||
|
className="pl-9"
|
||||||
|
placeholder="ค้นหาชื่อทีม ตัวละคร อาวุธ หรืออาร์ติแฟกต์"
|
||||||
|
value={searchQuery}
|
||||||
|
onValueChange={setSearchQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel>ค้นหาใน</FieldLabel>
|
||||||
|
<ToggleGroup
|
||||||
|
aria-label="เลือกประเภทข้อมูลที่ต้องการค้นหา"
|
||||||
|
className="max-w-full flex-wrap justify-start"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
value={[searchTarget]}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const target = value[0] as TeamSearchTarget | undefined;
|
||||||
|
if (target) setSearchTarget(target);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value="all">ทั้งหมด</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="team">ชื่อทีม</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="character">ตัวละคร</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="weapon">อาวุธ</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="artifact">อาร์ติแฟกต์</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={teamSensors}
|
sensors={teamSensors}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
onDragEnd={handleTeamDragEnd}
|
onDragEnd={handleTeamDragEnd}
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={teams.map((team) => team.clientId)}
|
items={filteredTeams.map(({ team }) => team.clientId)}
|
||||||
strategy={verticalListSortingStrategy}
|
strategy={verticalListSortingStrategy}
|
||||||
>
|
>
|
||||||
<Accordion
|
<Accordion
|
||||||
@@ -2227,7 +2299,7 @@ function TeamEditor({
|
|||||||
defaultValue={teams.length ? [teams[0].clientId] : []}
|
defaultValue={teams.length ? [teams[0].clientId] : []}
|
||||||
className="gap-3"
|
className="gap-3"
|
||||||
>
|
>
|
||||||
{teams.map((team, teamIndex) => {
|
{filteredTeams.map(({ team, teamIndex }) => {
|
||||||
const total = team.members.reduce(
|
const total = team.members.reduce(
|
||||||
(sum, member) => sum + (Number(member.damage) || 0),
|
(sum, member) => sum + (Number(member.damage) || 0),
|
||||||
0,
|
0,
|
||||||
@@ -2764,6 +2836,11 @@ function TeamEditor({
|
|||||||
</SortableTeamItem>
|
</SortableTeamItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{filteredTeams.length === 0 ? (
|
||||||
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||||
|
ไม่พบ Team ที่ตรงกับคำค้นหา
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
|||||||
@@ -10,13 +10,152 @@ import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle }
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Kbd } from "@/components/ui/kbd";
|
import { Kbd } from "@/components/ui/kbd";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import { elementBackgroundColor } from "@/lib/catalog/element";
|
import { elementBackgroundColor } from "@/lib/catalog/element";
|
||||||
import { rarityGradientClass } from "@/lib/catalog/rarity";
|
import { rarityGradientClass } from "@/lib/catalog/rarity";
|
||||||
import { isCustomTravelerKey } from "@/lib/catalog/travelers";
|
import { isCustomTravelerKey } from "@/lib/catalog/travelers";
|
||||||
import type { PublicGuide } from "@/lib/guides/queries";
|
import type { PublicGuide } from "@/lib/guides/queries";
|
||||||
|
import {
|
||||||
|
matchesTeamSearch,
|
||||||
|
type TeamSearchTarget,
|
||||||
|
} from "@/lib/guides/team-search";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type Team = PublicGuide["teams"][number];
|
type Team = PublicGuide["teams"][number];
|
||||||
|
type TeamArtifact = NonNullable<Team["members"][number]["artifact"]>;
|
||||||
|
|
||||||
|
function ArtifactHalf({
|
||||||
|
artifact,
|
||||||
|
side,
|
||||||
|
eager,
|
||||||
|
linked,
|
||||||
|
sizes,
|
||||||
|
}: {
|
||||||
|
artifact: TeamArtifact;
|
||||||
|
side: "left" | "right";
|
||||||
|
eager: boolean;
|
||||||
|
linked: boolean;
|
||||||
|
sizes: string;
|
||||||
|
}) {
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
<span className="relative block size-full">
|
||||||
|
<Image
|
||||||
|
src={artifact.imageUrl}
|
||||||
|
alt={linked ? artifact.name : ""}
|
||||||
|
fill
|
||||||
|
loading={eager ? "eager" : undefined}
|
||||||
|
sizes={sizes}
|
||||||
|
className="object-contain p-1"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{linked ? (
|
||||||
|
<Kbd
|
||||||
|
className={cn(
|
||||||
|
"absolute h-3 min-w-3 bg-transparent p-0 text-white",
|
||||||
|
side === "left" ? "bottom-0 left-0" : "top-0 right-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ArrowUpRightIcon aria-hidden="true" />
|
||||||
|
</Kbd>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
const className = cn(
|
||||||
|
"absolute inset-0 overflow-hidden",
|
||||||
|
side === "left"
|
||||||
|
? "[clip-path:polygon(0_0,0_100%,100%_100%)]"
|
||||||
|
: "[clip-path:polygon(0_0,100%_0,100%_100%)]",
|
||||||
|
rarityGradientClass(artifact.rarity),
|
||||||
|
);
|
||||||
|
|
||||||
|
return linked ? (
|
||||||
|
<Link
|
||||||
|
href={`https://lunaris.moe/artifact/${encodeURIComponent(artifact.key)}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
aria-label={`เปิดข้อมูลอาร์ติแฟกต์ ${artifact.name} บน Lunaris`}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className={className}>{content}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TeamArtifactIcon({
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
eager = false,
|
||||||
|
linked = false,
|
||||||
|
className,
|
||||||
|
sizes,
|
||||||
|
}: {
|
||||||
|
first: TeamArtifact | null;
|
||||||
|
second: TeamArtifact | null;
|
||||||
|
eager?: boolean;
|
||||||
|
linked?: boolean;
|
||||||
|
className?: string;
|
||||||
|
sizes: string;
|
||||||
|
}) {
|
||||||
|
const primary = first ?? second;
|
||||||
|
const secondary = first ? second : null;
|
||||||
|
|
||||||
|
if (!primary) {
|
||||||
|
return <div className={cn("aspect-square rounded-md border bg-muted", className)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secondary) {
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
<Image
|
||||||
|
src={primary.imageUrl}
|
||||||
|
alt={linked ? primary.name : ""}
|
||||||
|
fill
|
||||||
|
loading={eager ? "eager" : undefined}
|
||||||
|
sizes={sizes}
|
||||||
|
className="object-contain p-1"
|
||||||
|
/>
|
||||||
|
{linked ? (
|
||||||
|
<Kbd className="absolute top-0 right-0 h-3 min-w-3 bg-transparent p-0 text-white">
|
||||||
|
<ArrowUpRightIcon aria-hidden="true" />
|
||||||
|
</Kbd>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
const artifactClassName = cn(
|
||||||
|
"relative aspect-square scale-110 overflow-hidden rounded-md bg-muted",
|
||||||
|
rarityGradientClass(primary.rarity),
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
|
||||||
|
return linked ? (
|
||||||
|
<Link
|
||||||
|
href={`https://lunaris.moe/artifact/${encodeURIComponent(primary.key)}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
aria-label={`เปิดข้อมูลอาร์ติแฟกต์ ${primary.name} บน Lunaris`}
|
||||||
|
className={artifactClassName}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div className={artifactClassName}>{content}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative aspect-square scale-110 overflow-hidden rounded-md", className)}>
|
||||||
|
<ArtifactHalf artifact={primary} side="left" eager={eager} linked={linked} sizes={sizes} />
|
||||||
|
<ArtifactHalf artifact={secondary} side="right" eager={eager} linked={linked} sizes={sizes} />
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute top-1/2 left-1/2 h-px w-[142%] -translate-x-1/2 -translate-y-1/2 rotate-45 bg-white/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TeamExportImage({ team }: { team: Team }) {
|
function TeamExportImage({ team }: { team: Team }) {
|
||||||
const total = team.members.reduce((sum, member) => sum + Number(member.damage), 0);
|
const total = team.members.reduce((sum, member) => sum + Number(member.damage), 0);
|
||||||
@@ -38,8 +177,14 @@ function TeamExportImage({ team }: { team: Team }) {
|
|||||||
<span className="absolute top-1.5 left-1.5 text-xl font-bold">C{member.constellation}</span>
|
<span className="absolute top-1.5 left-1.5 text-xl font-bold">C{member.constellation}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-48 shrink-0 grid-cols-2 gap-3">
|
<div className="grid w-48 shrink-0 grid-cols-2 gap-3">
|
||||||
{member.weapon ? <div className={cn("relative col-span-2 aspect-square min-w-0 scale-110 overflow-hidden rounded-2xl", rarityGradientClass(member.weapon.rarity))}><Image src={member.weapon.imageUrl} alt="" fill loading="eager" sizes="96px" className="object-contain p-1" /><span className="absolute top-1 left-1 text-base font-bold">R{member.refinement}</span></div> : null}
|
{member.weapon ? <div className={cn("relative aspect-square min-w-0 scale-110 overflow-hidden rounded-2xl", rarityGradientClass(member.weapon.rarity))}><Image src={member.weapon.imageUrl} alt="" fill loading="eager" sizes="96px" className="object-contain p-1" /><span className="absolute top-1 left-1 text-base font-bold">R{member.refinement}</span></div> : null}
|
||||||
{[member.artifact, member.secondArtifact].map((item, index) => item ? <div key={`${item.key}-${index}`} className={cn("relative aspect-square min-w-0 scale-110 overflow-hidden rounded-2xl", rarityGradientClass(item.rarity))}><Image src={item.imageUrl} alt="" fill loading="eager" sizes="96px" className="object-contain p-1" /></div> : null)}
|
<TeamArtifactIcon
|
||||||
|
first={member.artifact}
|
||||||
|
second={member.secondArtifact}
|
||||||
|
eager
|
||||||
|
sizes="96px"
|
||||||
|
className="min-w-0 rounded-2xl"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-3.5">
|
<div className="flex min-w-0 flex-1 flex-col gap-3.5">
|
||||||
<div className="flex items-center justify-between gap-4"><span className="truncate text-[36px] font-semibold">{member.character.name}</span><span className="text-[36px]">{damage.toLocaleString()}</span></div>
|
<div className="flex items-center justify-between gap-4"><span className="truncate text-[36px] font-semibold">{member.character.name}</span><span className="text-[36px]">{damage.toLocaleString()}</span></div>
|
||||||
@@ -167,64 +312,14 @@ function TeamCard({
|
|||||||
) : (
|
) : (
|
||||||
<div className="col-span-2 aspect-square rounded-md border bg-muted" />
|
<div className="col-span-2 aspect-square rounded-md border bg-muted" />
|
||||||
)}
|
)}
|
||||||
<div
|
<TeamArtifactIcon
|
||||||
className={cn(
|
first={member.artifact}
|
||||||
"col-span-2 grid",
|
second={member.secondArtifact}
|
||||||
member.secondArtifact && "w-[150%] grid-cols-3 gap-2",
|
eager={exporting}
|
||||||
)}
|
linked={!exporting}
|
||||||
>
|
sizes="64px"
|
||||||
{member.artifact ? (
|
className="col-span-2"
|
||||||
<Link
|
|
||||||
href={`https://lunaris.moe/artifact/${encodeURIComponent(member.artifact.key)}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
tabIndex={exporting ? -1 : undefined}
|
|
||||||
aria-label={`เปิดข้อมูลอาร์ติแฟกต์ ${member.artifact.name} บน Lunaris`}
|
|
||||||
className={cn(
|
|
||||||
"relative col-span-2 aspect-square scale-110 overflow-hidden rounded-md bg-muted",
|
|
||||||
member.secondArtifact && "col-start-1 row-start-1",
|
|
||||||
rarityGradientClass(member.artifact.rarity),
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={member.artifact.imageUrl}
|
|
||||||
alt={member.artifact.name}
|
|
||||||
fill
|
|
||||||
loading={exporting ? "eager" : undefined}
|
|
||||||
className="object-contain p-1"
|
|
||||||
/>
|
/>
|
||||||
<Kbd className="absolute top-0 right-0 h-3 min-w-3 bg-transparent p-0 text-white">
|
|
||||||
<ArrowUpRightIcon aria-hidden="true" />
|
|
||||||
</Kbd>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<div className="col-span-2 aspect-square rounded-md border bg-muted" />
|
|
||||||
)}
|
|
||||||
{member.secondArtifact ? (
|
|
||||||
<Link
|
|
||||||
href={`https://lunaris.moe/artifact/${encodeURIComponent(member.secondArtifact.key)}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
tabIndex={exporting ? -1 : undefined}
|
|
||||||
aria-label={`เปิดข้อมูลอาร์ติแฟกต์ ${member.secondArtifact.name} บน Lunaris`}
|
|
||||||
className={cn(
|
|
||||||
"relative col-span-2 col-start-2 row-start-1 aspect-square scale-110 overflow-hidden rounded-md bg-muted",
|
|
||||||
rarityGradientClass(member.secondArtifact.rarity),
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={member.secondArtifact.imageUrl}
|
|
||||||
alt={member.secondArtifact.name}
|
|
||||||
fill
|
|
||||||
loading={exporting ? "eager" : undefined}
|
|
||||||
className="object-contain p-1"
|
|
||||||
/>
|
|
||||||
<Kbd className="absolute top-0 right-0 h-3 min-w-3 bg-transparent p-0 text-white">
|
|
||||||
<ArrowUpRightIcon aria-hidden="true" />
|
|
||||||
</Kbd>
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 truncate text-sm font-medium">
|
<p className="mt-2 truncate text-sm font-medium">
|
||||||
@@ -271,22 +366,15 @@ function TeamCard({
|
|||||||
|
|
||||||
export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const normalizedQuery = query.trim().toLocaleLowerCase();
|
const [searchTarget, setSearchTarget] = useState<TeamSearchTarget>("all");
|
||||||
const filteredTeams = useMemo(
|
const filteredTeams = useMemo(
|
||||||
() => teams.filter((team) => {
|
() => teams.filter((team) => matchesTeamSearch({
|
||||||
if (!normalizedQuery) return true;
|
team: [team.title],
|
||||||
const searchableNames = [
|
character: team.members.map((member) => member.character.name),
|
||||||
team.title,
|
weapon: team.members.flatMap((member) => member.weapon ? [member.weapon.name] : []),
|
||||||
...team.members.flatMap((member) => [
|
artifact: team.members.flatMap((member) => [member.artifact, member.secondArtifact].flatMap((artifact) => artifact ? [artifact.name] : [])),
|
||||||
member.character.name,
|
}, query, searchTarget)),
|
||||||
member.weapon?.name,
|
[query, searchTarget, teams],
|
||||||
member.artifact?.name,
|
|
||||||
member.secondArtifact?.name,
|
|
||||||
]),
|
|
||||||
].filter((name): name is string => Boolean(name));
|
|
||||||
return searchableNames.some((name) => name.toLocaleLowerCase().includes(normalizedQuery));
|
|
||||||
}),
|
|
||||||
[normalizedQuery, teams],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -301,6 +389,23 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
|||||||
onValueChange={setQuery}
|
onValueChange={setQuery}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<ToggleGroup
|
||||||
|
aria-label="เลือกประเภทข้อมูลที่ต้องการค้นหา"
|
||||||
|
className="max-w-full flex-wrap justify-start"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
value={[searchTarget]}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const target = value[0] as TeamSearchTarget | undefined;
|
||||||
|
if (target) setSearchTarget(target);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value="all">ทั้งหมด</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="team">ชื่อทีม</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="character">ตัวละคร</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="weapon">อาวุธ</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="artifact">อาร์ติแฟกต์</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
{filteredTeams.length ? (
|
{filteredTeams.length ? (
|
||||||
<div className="grid gap-5 xl:grid-cols-2">
|
<div className="grid gap-5 xl:grid-cols-2">
|
||||||
{filteredTeams.map((team) => (
|
{filteredTeams.map((team) => (
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { matchesTeamSearch } from "./team-search";
|
||||||
|
|
||||||
|
const team = {
|
||||||
|
team: ["Vaporize Team"],
|
||||||
|
character: ["Mualani"],
|
||||||
|
weapon: ["Surf's Up"],
|
||||||
|
artifact: ["Obsidian Codex", "Scroll of the Hero"],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("team search", () => {
|
||||||
|
it("searches every category when the target is all", () => {
|
||||||
|
expect(matchesTeamSearch(team, "surf", "all")).toBe(true);
|
||||||
|
expect(matchesTeamSearch(team, "mualani", "all")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only searches the selected target", () => {
|
||||||
|
expect(matchesTeamSearch(team, "vaporize", "team")).toBe(true);
|
||||||
|
expect(matchesTeamSearch(team, "obsidian", "team")).toBe(false);
|
||||||
|
expect(matchesTeamSearch(team, "obsidian", "artifact")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores case and surrounding whitespace", () => {
|
||||||
|
expect(matchesTeamSearch(team, " HERO ", "artifact")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches every team when the query is empty", () => {
|
||||||
|
expect(matchesTeamSearch(team, " ", "weapon")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
export type TeamSearchTarget =
|
||||||
|
| "all"
|
||||||
|
| "team"
|
||||||
|
| "character"
|
||||||
|
| "weapon"
|
||||||
|
| "artifact";
|
||||||
|
|
||||||
|
export interface TeamSearchValues {
|
||||||
|
team: string[];
|
||||||
|
character: string[];
|
||||||
|
weapon: string[];
|
||||||
|
artifact: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchesTeamSearch(
|
||||||
|
values: TeamSearchValues,
|
||||||
|
query: string,
|
||||||
|
target: TeamSearchTarget,
|
||||||
|
): boolean {
|
||||||
|
const normalizedQuery = query.trim().toLocaleLowerCase();
|
||||||
|
if (!normalizedQuery) return true;
|
||||||
|
|
||||||
|
const candidates = target === "all"
|
||||||
|
? Object.values(values).flat()
|
||||||
|
: values[target];
|
||||||
|
|
||||||
|
return candidates.some((value) =>
|
||||||
|
value.toLocaleLowerCase().includes(normalizedQuery),
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user