feat : skeleton load for team import
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user