93 lines
8.6 KiB
TypeScript
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>;
|
|
}
|