"use client";
import Image from "next/image";
import Link from "next/link";
import { ArrowUpRightIcon, SearchIcon, XIcon } from "lucide-react";
import { useEffect, useMemo, useState, type ReactNode } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { GuideImageExport } from "@/components/public/guide-image-export";
import { ExportText } from "@/components/public/export-language";
import { ScrollableNav } from "@/components/public/scrollable-nav";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
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 { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { elementBackgroundColor } from "@/lib/catalog/element";
import { rarityGradientClass } from "@/lib/catalog/rarity";
import { isCustomTravelerKey } from "@/lib/catalog/travelers";
import type { PublicTeamCardData, PublicTeamConditionGroup } from "@/lib/guides/public-filters";
import {
searchTeams,
sortTeamsByDps,
type DpsOrder,
type TeamSearchTarget,
} from "@/lib/guides/team-search";
import { cn } from "@/lib/utils";
type Team = PublicTeamCardData;
type TeamArtifact = {
key: string;
name: string;
imageUrl: string;
rarity: number | null;
};
function ArtifactHalf({
artifact,
side,
eager,
linked,
sizes,
}: {
artifact: TeamArtifact;
side: "left" | "right";
eager: boolean;
linked: boolean;
sizes: string;
}) {
const content = (
);
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 ? (
{content}
) : (
{content}
);
}
export 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
;
}
if (!secondary) {
const content = (
<>
{linked ? (
) : null}
>
);
const artifactClassName = cn(
"relative aspect-square scale-110 overflow-hidden rounded-md bg-muted",
rarityGradientClass(primary.rarity),
className,
);
return linked ? (
{content}
) : (
{content}
);
}
return (
);
}
function TeamExportImage({ team }: { team: Team }) {
const total = team.members.reduce((sum, member) => sum + Number(member.damage), 0);
const seconds = Number(team.rotationSeconds);
return (
{team.members.map((member) => {
const damage = Number(member.damage);
const percent = total > 0 ? damage / total * 100 : 0;
return (
{member.character.name}
C{member.constellation}R{member.refinement}
{damage.toLocaleString("en-US")}
);
})}
DPS: {seconds > 0 ? (total / seconds).toLocaleString("en-US", { maximumFractionDigits: 0 }) : "0"}
By YT เกนชินไม่ใช่เกมมือถือ
);
}
function TeamCard({
team,
action,
exporting = false,
}: {
team: Team;
action?: ReactNode;
exporting?: boolean;
}) {
const total = team.members.reduce(
(sum, member) => sum + Number(member.damage),
0,
);
const seconds = Number(team.rotationSeconds);
return (
{team.title}
โรเทชั่น {seconds} วิ
{action ? {action} : null}
{team.members.map((member) => (
{member.weapon ? (
) : (
)}
C{member.constellation}
R{member.refinement}
{member.character.name}
{Number(member.damage).toLocaleString(undefined, {
maximumFractionDigits: 2,
})}
{total
? ((Number(member.damage) / total) * 100).toFixed(2)
: "0.00"}
%
))}
ดาเมจรวม
{total.toLocaleString()}
DPS
{seconds > 0
? (total / seconds).toLocaleString(undefined, {
maximumFractionDigits: 0,
})
: "0"}
{team.combo}
);
}
export function TeamSearch({
groups,
serverFilters,
}: {
groups: PublicTeamConditionGroup[];
serverFilters?: {
conditionIndex: number;
query: string;
field: TeamSearchTarget;
sort: DpsOrder;
};
}) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const searchString = searchParams.toString();
const [activeCondition, setActiveCondition] = useState("0");
const activeConditionIndex = serverFilters?.conditionIndex ?? Number(activeCondition);
const activeGroup = groups[activeConditionIndex] ?? groups[0];
const teams = useMemo(() => activeGroup?.teams ?? [], [activeGroup]);
const [query, setQuery] = useState(serverFilters?.query ?? "");
const [searchTarget, setSearchTarget] = useState("all");
const [dpsOrder, setDpsOrder] = useState(null);
const currentQuery = serverFilters?.query ?? "";
const currentField = serverFilters?.field ?? searchTarget;
const currentSort = serverFilters?.sort ?? dpsOrder;
useEffect(() => {
if (!serverFilters) return;
const nextQuery = query.trim();
if (nextQuery === currentQuery) return;
const timeout = window.setTimeout(() => {
const nextParams = new URLSearchParams(searchString);
if (nextQuery) nextParams.set("q", nextQuery);
else nextParams.delete("q");
const queryString = nextParams.toString();
router.replace(`${pathname}${queryString ? `?${queryString}` : ""}`, { scroll: false });
}, 300);
return () => window.clearTimeout(timeout);
}, [currentQuery, pathname, query, router, searchString, serverFilters]);
const localSearch = useMemo(
() => serverFilters ? null : searchTeams(teams, (team) => ({
team: [team.title],
character: team.members.flatMap((member) => [member.character.name, ...(member.character.aliases ?? [])]),
weapon: team.members.flatMap((member) => member.weapon ? [member.weapon.name, ...(member.weapon.aliases ?? [])] : []),
artifact: team.members.flatMap((member) => [member.artifact, member.secondArtifact].flatMap((artifact) => artifact ? [artifact.name, ...(artifact.aliases ?? [])] : [])),
}), query, currentField),
[currentField, query, serverFilters, teams],
);
const filteredTeams = serverFilters
? teams
: sortTeamsByDps(localSearch?.items ?? [], currentSort);
function updateUrl(name: string, value: string | null) {
const nextParams = new URLSearchParams(searchString);
const nextQuery = query.trim();
if (nextQuery) nextParams.set("q", nextQuery);
else nextParams.delete("q");
if (value === null) nextParams.delete(name);
else nextParams.set(name, value);
const queryString = nextParams.toString();
router.push(`${pathname}${queryString ? `?${queryString}` : ""}`, { scroll: false });
}
function selectCondition(index: number) {
if (!serverFilters) {
setActiveCondition(String(index));
return;
}
updateUrl("condition", index === 0 ? null : String(index + 1));
}
function selectField(field: TeamSearchTarget) {
if (!serverFilters) {
setSearchTarget(field);
return;
}
updateUrl("field", field === "all" ? null : field);
}
function selectSort(order: Exclude) {
if (!serverFilters) {
setDpsOrder(order);
return;
}
updateUrl("sort", order);
}
function clearSort() {
if (!serverFilters) {
setDpsOrder(null);
return;
}
updateUrl("sort", null);
}
return (
{groups.length > 1 ? (
{
if (values[0]) selectCondition(Number(values[0]));
}}>
{groups.map((group, index) => {group.name || `เงื่อนไข ${index + 1}`})}
) : null}
{activeGroup?.note ?
{activeGroup.note}
: null}
{
const field = value[0] as TeamSearchTarget | undefined;
if (field) selectField(field);
}}
>
ทั้งหมด
ชื่อทีม
ตัวละคร
อาวุธ
อาร์ติแฟกต์
{
const order = value[0] as Exclude | undefined;
if (order) selectSort(order);
}}
>
Best DPS
Worst DPS
{filteredTeams.length ? (
{filteredTeams.map((team) => (
}
/>
))}
) : (
ไม่พบทีมที่ตรงกับคำค้นหา
)}
);
}