feat : skeleton load for team import

This commit is contained in:
2026-09-27 15:05:29 +07:00 Unverified
parent 480bfeb9a5
commit 33ee40a0a8
+123 -37
View File
@@ -125,7 +125,6 @@ import {
import { Textarea } from "@/components/ui/textarea";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Toggle } from "@/components/ui/toggle";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import {
@@ -214,6 +213,13 @@ type CatalogItem = {
};
const catalogItemsEqual = (item: CatalogItem, value: CatalogItem) =>
item.key === value.key;
const teamSearchTargetLabels: Record<TeamSearchTarget, string> = {
all: "ทั้งหมด",
team: "ชื่อทีม",
character: "ตัวละคร",
weapon: "อาวุธ",
artifact: "อาร์ติแฟกต์",
};
type ExtraCatalogItem = {
kind: "character" | "weapon" | "artifact";
key: string;
@@ -2835,7 +2841,7 @@ function TeamEditor({
const [importGuides, setImportGuides] = useState<Array<{ id: string; name: string; characterKey: string; isPublic: boolean; coverUrl: string }> | null>(null);
const [activeImportGuide, setActiveImportGuide] = useState<TeamImportGuide | null>(null);
const [selectedImports, setSelectedImports] = useState<Record<string, Omit<TeamToolValue, "clientId">>>({});
const [guideImportLoading, setGuideImportLoading] = useState(false);
const [guideImportLoading, setGuideImportLoading] = useState<"guides" | "teams" | null>(null);
const [guideImportError, setGuideImportError] = useState<string | null>(null);
const [guideImportCondition, setGuideImportCondition] = useState("0");
const [guideTeamQuery, setGuideTeamQuery] = useState("");
@@ -3207,17 +3213,17 @@ function TeamEditor({
setSelectedImports({});
setGuideListQuery("");
setGuideImportError(null);
setGuideImportLoading(true);
setGuideImportLoading("guides");
const result = await listTeamImportGuides(guide.id);
setGuideImportLoading(false);
setGuideImportLoading(null);
if (result.status === "success") setImportGuides(result.guides);
else setGuideImportError(result.message);
};
const enterImportGuide = async (guideId: string) => {
setGuideImportError(null);
setGuideImportLoading(true);
setGuideImportLoading("teams");
const result = await loadTeamImportGuide(guideId);
setGuideImportLoading(false);
setGuideImportLoading(null);
if (result.status === "success") {
setGuideImportCondition("0");
setGuideTeamQuery("");
@@ -4027,9 +4033,74 @@ function TeamEditor({
</Alert>
) : null}
{guideImportLoading ? (
<div className="flex h-48 items-center justify-center">
<Spinner />
</div>
guideImportLoading === "guides" ? (
<div
className="flex h-full min-h-0 flex-col gap-4"
aria-label="กำลังโหลดรายการ Guide"
aria-busy="true"
>
<Skeleton className="h-8 w-full" />
<div className="min-h-0 overflow-y-auto pr-1">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }, (_, index) => (
<Card key={index} className="overflow-hidden py-0">
<Skeleton className="aspect-4/3 rounded-b-none" />
<CardHeader className="pt-4">
<CardTitle><Skeleton className="h-5 w-2/3" /></CardTitle>
<CardDescription><Skeleton className="h-4 w-1/2" /></CardDescription>
<CardAction><Skeleton className="h-5 w-16" /></CardAction>
</CardHeader>
<CardFooter className="pb-4">
<Skeleton className="h-7 w-20" />
</CardFooter>
</Card>
))}
</div>
</div>
</div>
) : (
<div
className="flex h-full min-h-0 flex-col gap-3"
aria-label="กำลังโหลดรายการ Team"
aria-busy="true"
>
<Skeleton className="h-8 w-32" />
<div className="flex min-w-0 items-center gap-2">
<Skeleton className="h-8 min-w-0 flex-1" />
<Skeleton className="h-8 w-32 shrink-0" />
</div>
<div className="min-h-0 overflow-y-auto pr-1">
<div className="grid gap-4 lg:grid-cols-2">
{Array.from({ length: 4 }, (_, index) => (
<Card key={index}>
<CardHeader>
<CardTitle><Skeleton className="h-5 w-1/3" /></CardTitle>
<CardDescription><Skeleton className="h-4 w-24" /></CardDescription>
<CardAction><Skeleton className="h-5 w-12" /></CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="grid grid-cols-4 gap-2">
{Array.from({ length: 4 }, (_, memberIndex) => (
<div key={memberIndex} className="flex min-w-0 flex-col items-center gap-2">
<Skeleton className="size-16 rounded-lg" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
</div>
))}
</div>
<Separator />
<div className="flex items-end justify-between gap-4">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-12 w-28" />
</div>
<Skeleton className="h-12 w-full" />
</CardContent>
</Card>
))}
</div>
</div>
</div>
)
) : activeImportGuide ? (
<div className="flex h-full min-h-0 flex-col gap-3">
<Button type="button" variant="ghost" className="self-start" onClick={() => setActiveImportGuide(null)}>
@@ -4052,30 +4123,37 @@ function TeamEditor({
</ToggleGroup>
</ScrollableNav>
) : null}
<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={guideTeamQuery}
onValueChange={setGuideTeamQuery}
/>
<div className="flex min-w-0 items-center gap-2">
<div className="relative min-w-0 flex-1">
<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={guideTeamQuery}
onValueChange={setGuideTeamQuery}
/>
</div>
<Select value={guideTeamSearchTarget} onValueChange={(value) => {
if (value) setGuideTeamSearchTarget(value as TeamSearchTarget);
}}>
<SelectTrigger
aria-label="เลือกประเภทข้อมูลที่ต้องการค้นหา"
className="w-32 shrink-0"
>
<SelectValue>{teamSearchTargetLabels[guideTeamSearchTarget]}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="all">ทั้งหมด</SelectItem>
<SelectItem value="team">ชื่อทีม</SelectItem>
<SelectItem value="character">ตัวละคร</SelectItem>
<SelectItem value="weapon">อาวุธ</SelectItem>
<SelectItem value="artifact">อาร์ติแฟกต์</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<ToggleGroup
aria-label="เลือกประเภทข้อมูลที่ต้องการค้นหา"
className="max-w-full flex-wrap justify-start"
variant="outline"
size="sm"
value={[guideTeamSearchTarget]}
onValueChange={(values) => { if (values[0]) setGuideTeamSearchTarget(values[0] as TeamSearchTarget); }}
>
<ToggleGroupItem value="all">ทั้งหมด</ToggleGroupItem>
<ToggleGroupItem value="team">ชื่อทีม</ToggleGroupItem>
<ToggleGroupItem value="character">ตัวละคร</ToggleGroupItem>
<ToggleGroupItem value="weapon">อาวุธ</ToggleGroupItem>
<ToggleGroupItem value="artifact">อาร์ติแฟกต์</ToggleGroupItem>
</ToggleGroup>
<div className="min-h-0 overflow-y-auto pr-1">
{filteredImportTeams.length ? (
@@ -4134,11 +4212,19 @@ function TeamEditor({
{character ? <Image src={character.imageUrl} alt={character.name} fill sizes="64px" className="object-contain" /> : null}
</div>
<div className="flex flex-col gap-1">
{[weapon, artifact, secondArtifact].filter((item): item is CatalogItem => Boolean(item)).map((item) => (
<div key={item.key} className={cn("relative size-7 overflow-hidden rounded-md", rarityGradientClass(item.rarity))}>
<Image src={item.imageUrl} alt={item.name} fill sizes="28px" className="object-contain p-0.5" />
{weapon ? (
<div className={cn("relative size-7 overflow-hidden rounded-md", rarityGradientClass(weapon.rarity))}>
<Image src={weapon.imageUrl} alt={weapon.name} fill sizes="28px" className="object-contain p-0.5" />
</div>
))}
) : null}
{artifact || secondArtifact ? (
<TeamArtifactIcon
first={artifact ? { ...artifact, rarity: artifact.rarity ?? null } : null}
second={secondArtifact ? { ...secondArtifact, rarity: secondArtifact.rarity ?? null } : null}
sizes="28px"
className="size-7 shrink-0 scale-100"
/>
) : null}
</div>
</div>
<div className="mt-2 flex items-center justify-center gap-1">
@@ -4211,7 +4297,7 @@ function TeamEditor({
<CardDescription>{sourceGuide.characterKey}</CardDescription>
<CardAction>
<Badge variant={sourceGuide.isPublic ? "default" : "secondary"}>
{sourceGuide.isPublic ? "Published" : "Draft"}
{sourceGuide.isPublic ? "Published" : "Private"}
</Badge>
</CardAction>
</CardHeader>