Files
gcsim-translater/components/import-team-guide-dialog.tsx
gunshiz b3bc4c3bb4
CI / verify (push) Successful in 59s
CI / build-and-deploy (push) Successful in 1m38s
feat : add import from buzz guide
2026-09-20 00:41:02 +07:00

93 lines
8.6 KiB
TypeScript

"use client";
import { useMemo, useState } from "react";
import { BookOpenIcon, CheckIcon, ChevronsUpDownIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { importGuideTeam, type TeamGuide, type TeamGuideTeam } from "@/lib/team-guides";
import { rarityGradientClass } from "@/lib/catalog-rarity";
import { cn } from "@/lib/utils";
import type { Catalog, Team } from "@/lib/translator/types";
function GuideCharacter({ guide, catalog }: { guide: TeamGuide; catalog: Catalog }) {
const character = catalog.characters.find((item) => item.id === guide.characterId);
return <Avatar size="sm" className={rarityGradientClass(character?.rarity ?? null)}><AvatarImage src={character?.imageUrl || undefined} alt={character?.name ?? guide.name} /><AvatarFallback>{character?.name.slice(0, 1) ?? guide.name.slice(0, 1)}</AvatarFallback></Avatar>;
}
function GuidePicker({ guides, catalog, value, onChange }: { guides: TeamGuide[]; catalog: Catalog; value: string; onChange: (id: string) => void }) {
const [open, setOpen] = useState(false);
const selected = guides.find((guide) => guide.id === value);
return <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger render={<Button variant="outline" className="w-full justify-between normal-case tracking-normal" />}>
{selected && <GuideCharacter guide={selected} catalog={catalog} />}<span className="min-w-0 flex-1 truncate text-left">{selected?.name ?? "Choose guide"}</span><ChevronsUpDownIcon data-icon="inline-end" />
</PopoverTrigger>
<PopoverContent className="w-(--anchor-width) p-0" align="start"><Command><CommandInput placeholder="Search guides..." /><CommandList><CommandEmpty>No matching guide.</CommandEmpty><CommandGroup>{guides.map((guide) => <CommandItem key={guide.id} value={guide.name} data-checked={guide.id === value} onSelect={() => { onChange(guide.id); setOpen(false); }}><CheckIcon className={cn(guide.id === value ? "opacity-100" : "opacity-0")} /><GuideCharacter guide={guide} catalog={catalog} /><span className="min-w-0 flex-1 truncate">{guide.name}</span><Badge variant="secondary">{guide.teams.length}</Badge></CommandItem>)}</CommandGroup></CommandList></Command></PopoverContent>
</Popover>;
}
function TeamCharacters({ team, catalog }: { team: TeamGuideTeam; catalog: Catalog }) {
return <div className="flex shrink-0 gap-1">{team.members.map((member) => {
const character = catalog.characters.find((item) => item.id === member.characterId);
return <Avatar key={`${team.id}-${member.position}`} size="sm" className={rarityGradientClass(character?.rarity ?? null)}><AvatarImage src={character?.imageUrl || undefined} alt={character?.name ?? ""} /><AvatarFallback>{character?.name.slice(0, 1) ?? "?"}</AvatarFallback></Avatar>;
})}</div>;
}
function TeamPicker({ groups, team, catalog, onChange }: { groups: [string, TeamGuideTeam[]][]; team: TeamGuideTeam | undefined; catalog: Catalog; onChange: (id: string) => void }) {
const [open, setOpen] = useState(false);
return <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger render={<Button variant="outline" className="w-full justify-between normal-case tracking-normal" />}>
{team ? <><TeamCharacters team={team} catalog={catalog} /><span className="min-w-0 flex-1 truncate text-left">{team.title}</span></> : <span className="min-w-0 flex-1 text-left text-muted-foreground">Choose team</span>}<ChevronsUpDownIcon data-icon="inline-end" />
</PopoverTrigger>
<PopoverContent className="w-(--anchor-width) p-0" align="start"><Command><CommandInput placeholder="Search teams or characters..." /><CommandList><CommandEmpty>No matching team.</CommandEmpty>{groups.map(([label, teams]) => <CommandGroup key={label} heading={label}>{teams.map((item) => {
const characterNames = item.members.map((member) => catalog.characters.find((character) => character.id === member.characterId)?.name).filter(Boolean).join(" ");
return <CommandItem key={item.id} value={`${item.title} ${characterNames}`} data-checked={item.id === team?.id} onSelect={() => { onChange(item.id); setOpen(false); }}><CheckIcon className={cn(item.id === team?.id ? "opacity-100" : "opacity-0")} /><TeamCharacters team={item} catalog={catalog} /><span className="min-w-0 flex-1 truncate">{item.title}</span></CommandItem>;
})}</CommandGroup>)}</CommandList></Command></PopoverContent>
</Popover>;
}
export function ImportTeamGuideDialog({ catalog, guides, disabled, onImport }: { catalog: Catalog; guides: TeamGuide[]; disabled: boolean; onImport: (team: Team) => void }) {
const [open, setOpen] = useState(false);
const [guideId, setGuideId] = useState(guides[0]?.id ?? "");
const guide = guides.find((item) => item.id === guideId) ?? guides[0];
const [teamId, setTeamId] = useState(guide?.teams[0]?.id ?? "");
const team = guide?.teams.find((item) => item.id === teamId) ?? guide?.teams[0];
const preview = useMemo(() => team ? importGuideTeam(team, catalog) : undefined, [team, catalog]);
const groups = useMemo(() => {
const result = new Map<string, NonNullable<typeof guide>["teams"]>();
for (const item of guide?.teams ?? []) {
const label = item.conditionName?.trim() || "Teams";
result.set(label, [...(result.get(label) ?? []), item]);
}
return [...result.entries()];
}, [guide]);
const chooseGuide = (id: string) => {
const next = guides.find((item) => item.id === id);
setGuideId(id);
setTeamId(next?.teams[0]?.id ?? "");
};
return <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button variant="outline" disabled={disabled || !guides.length} />}><BookOpenIcon data-icon="inline-start" />IMPORT FROM BUZZ GUIDE</DialogTrigger>
<DialogContent className="max-h-[90dvh] overflow-y-auto sm:max-w-2xl">
<DialogHeader><DialogTitle>Import from Buzz Guide</DialogTitle><DialogDescription>Choose a public guide team to import its characters, weapons, artifacts, constellations, refinements, and combo.</DialogDescription></DialogHeader>
<FieldGroup>
<Field><FieldLabel>Guide</FieldLabel><GuidePicker guides={guides} catalog={catalog} value={guide?.id ?? ""} onChange={chooseGuide} /><FieldDescription>{guide?.teams.length ?? 0} teams available.</FieldDescription></Field>
<Field><FieldLabel>Team</FieldLabel><TeamPicker groups={groups} team={team} catalog={catalog} onChange={setTeamId} /></Field>
{preview?.error && <Alert variant="destructive"><AlertTitle>Cannot import this team</AlertTitle><AlertDescription>{preview.error}</AlertDescription></Alert>}
{preview?.team && <>
<Field><FieldLabel>Loadout preview</FieldLabel><div className="grid gap-3 sm:grid-cols-2">{preview.team.builds.map((build) => <div key={build.id} className="flex items-center gap-3 border p-3"><Avatar><AvatarImage src={build.characterImageUrl || undefined} alt="" /><AvatarFallback>{build.characterName.slice(0, 1)}</AvatarFallback></Avatar><div className="min-w-0 flex-1"><p className="truncate font-medium">{build.characterName} <span className="text-muted-foreground">C{build.constellation}</span></p><p className="truncate text-sm text-muted-foreground">{build.weaponName} R{build.refinement}</p><p className="truncate text-sm text-muted-foreground">{build.artifactOneName}{build.artifactTwoName ? ` + ${build.artifactTwoName}` : " · 4pc"}</p></div></div>)}</div></Field>
{team?.combo.trim() && team.combo.trim() !== "-" && <Alert><AlertTitle>{preview.team.rotation?.enabled ? "Combo will be included" : "Combo saved as reference"}</AlertTitle><AlertDescription>{preview.team.rotation?.enabled ? "The guide combo is valid shorthand and will be enabled." : "The combo uses unsupported guide notation, so it will be preserved with rotation disabled."}</AlertDescription></Alert>}
</>}
</FieldGroup>
<DialogFooter><DialogClose render={<Button variant="outline" />}>Cancel</DialogClose><Button disabled={disabled || !preview?.team} onClick={() => { if (!preview?.team || disabled) return; onImport(preview.team); setOpen(false); }}>Import team</Button></DialogFooter>
</DialogContent>
</Dialog>;
}