feat : improve UX/UI
This commit is contained in:
+2
-2
@@ -20,9 +20,9 @@ export default function HomePage() {
|
||||
<GoogleAdsense />
|
||||
<LiveRefresh endpoint="/api/events/directory" />
|
||||
<SiteHeader />
|
||||
<main className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-4 py-10 sm:px-6 sm:py-14">
|
||||
<main className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-4 pt-4 pb-10 sm:px-6 sm:pt-6 sm:pb-14">
|
||||
<header className="max-w-3xl">
|
||||
<h1 className="mt-2 font-heading text-4xl font-semibold">
|
||||
<h1 className="font-heading text-4xl font-semibold">
|
||||
เลือกตัวละครแล้วส่องดูได้เลย
|
||||
</h1>
|
||||
<p className="mt-3 text-lg leading-8 text-muted-foreground">
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
CopyIcon,
|
||||
GripVerticalIcon,
|
||||
HeartPulseIcon,
|
||||
MousePointerClickIcon,
|
||||
PlusIcon,
|
||||
RefreshCcwIcon,
|
||||
SearchIcon,
|
||||
@@ -130,6 +131,7 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -918,17 +920,17 @@ function SortableConditionTab({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||||
className={cn("flex shrink-0 items-center rounded-md border", isDragging && "z-10 opacity-60")}
|
||||
className={cn("flex shrink-0 items-center rounded-md", isDragging && "z-10 opacity-60")}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
<TabsTrigger
|
||||
value={item.clientId}
|
||||
className="border-0 aria-pressed:bg-primary aria-pressed:text-primary-foreground"
|
||||
onClick={onSelect}
|
||||
onDoubleClick={onRename}
|
||||
>
|
||||
<MousePointerClickIcon aria-hidden="true" data-icon="inline-start" />
|
||||
{label}
|
||||
{index === 0 ? <Badge variant={active ? "secondary" : "outline"}>Default</Badge> : null}
|
||||
</ToggleGroupItem>
|
||||
</TabsTrigger>
|
||||
<SimpleTooltip text={`ลากเพื่อจัดลำดับ ${label}`}>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -980,15 +982,11 @@ function SortableConditionSwitcher({
|
||||
}}
|
||||
>
|
||||
<SortableContext items={items.map((item) => item.clientId)} strategy={rectSortingStrategy}>
|
||||
<ToggleGroup
|
||||
aria-label={ariaLabel}
|
||||
variant="outline"
|
||||
value={activeId ? [activeId] : []}
|
||||
onValueChange={(values) => {
|
||||
const index = items.findIndex((item) => item.clientId === values[0]);
|
||||
<Tabs value={activeId} onValueChange={(value) => {
|
||||
const index = items.findIndex((item) => item.clientId === value);
|
||||
if (index !== -1) onActiveIndexChange(index);
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<TabsList aria-label={ariaLabel} className="shrink-0 gap-1">
|
||||
{items.map((item, index) => (
|
||||
<SortableConditionTab
|
||||
key={item.clientId}
|
||||
@@ -999,7 +997,8 @@ function SortableConditionSwitcher({
|
||||
onRename={() => onRename(index)}
|
||||
/>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</ScrollableNav>
|
||||
@@ -3812,18 +3811,16 @@ function TeamEditor({
|
||||
</Button>
|
||||
{activeImportGuide.groups.length > 1 ? (
|
||||
<ScrollableNav ariaLabel="เลือกเงื่อนไขทีม" className="w-full min-w-0">
|
||||
<ToggleGroup
|
||||
aria-label="เลือกเงื่อนไขทีม"
|
||||
variant="outline"
|
||||
value={[guideImportCondition]}
|
||||
onValueChange={(values) => { if (values[0]) setGuideImportCondition(values[0]); }}
|
||||
>
|
||||
<Tabs value={guideImportCondition} onValueChange={setGuideImportCondition}>
|
||||
<TabsList aria-label="เลือกเงื่อนไขทีม">
|
||||
{activeImportGuide.groups.map((group, index) => (
|
||||
<ToggleGroupItem key={group.id} value={String(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground">
|
||||
<TabsTrigger key={group.id} value={String(index)}>
|
||||
<MousePointerClickIcon aria-hidden="true" data-icon="inline-start" />
|
||||
{group.name || `Condition ${index + 1}`}
|
||||
</ToggleGroupItem>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</ScrollableNav>
|
||||
) : null}
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
|
||||
@@ -49,14 +49,6 @@ import {
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/components/ui/input-group";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useAdminPresence } from "@/hooks/use-presence";
|
||||
import type { CatalogSyncJob } from "@/lib/catalog/sync";
|
||||
@@ -86,7 +78,6 @@ export function StructuredDashboard({
|
||||
const router = useRouter();
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [query, setQuery] = useState("");
|
||||
const [sort, setSort] = useState("default");
|
||||
const [syncJob, setSyncJob] = useState<CatalogSyncJob | null>(null);
|
||||
const presence = useAdminPresence(null);
|
||||
const catalogReady = Boolean(catalogVersion);
|
||||
@@ -95,10 +86,7 @@ export function StructuredDashboard({
|
||||
const search = searchItems(guides, query,
|
||||
(guide) => `${guide.name} ${guide.characterKey} ${guide.overview}`,
|
||||
(guide) => [guide.name]);
|
||||
const filteredGuides = sort === "default" ? search.items : [...search.items].sort((left, right) => {
|
||||
const order = left.name.localeCompare(right.name, "th", { numeric: true });
|
||||
return sort === "name-asc" ? order : -order;
|
||||
});
|
||||
const filteredGuides = search.items;
|
||||
|
||||
useEffect(() => {
|
||||
if (!syncJobId || !syncJobStatus || ["completed", "unchanged", "cancelled", "error"].includes(syncJobStatus)) return;
|
||||
@@ -198,8 +186,7 @@ export function StructuredDashboard({
|
||||
</Card>
|
||||
) : null}
|
||||
{guides.length ? (
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<InputGroup className="h-11 max-w-2xl flex-1">
|
||||
<InputGroup className="h-11 max-w-2xl">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
@@ -210,19 +197,6 @@ export function StructuredDashboard({
|
||||
aria-label="ค้นหาตัวละคร"
|
||||
/>
|
||||
</InputGroup>
|
||||
<Select value={sort} onValueChange={(value) => value && setSort(value)}>
|
||||
<SelectTrigger aria-label="เรียง Guide" className="h-11 w-full sm:w-44">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="default">เรียงตามเดิม</SelectItem>
|
||||
<SelectItem value="name-asc">ชื่อ ก-ฮ</SelectItem>
|
||||
<SelectItem value="name-desc">ชื่อ ฮ-ก</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{filteredGuides.length ? (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { DownloadIcon } from "lucide-react";
|
||||
import { DownloadIcon, MousePointerClickIcon } from "lucide-react";
|
||||
|
||||
import { GuideImageExport } from "@/components/public/guide-image-export";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
const sectionOptions = [
|
||||
["tierlist", "อันดับอาร์ติแฟกต์"],
|
||||
@@ -106,53 +106,41 @@ export function ArtifactImageExport({
|
||||
{sections.has("best-stats") && recommendedConditionLabels.length > 1 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">เงื่อนไขค่าสถานะแนะนำ</p>
|
||||
<ToggleGroup
|
||||
variant="outline"
|
||||
value={[recommendedCondition]}
|
||||
onValueChange={(values) => {
|
||||
if (values[0]) setRecommendedCondition(values[0]);
|
||||
}}
|
||||
>
|
||||
<Tabs value={recommendedCondition} onValueChange={setRecommendedCondition}>
|
||||
<TabsList aria-label="เงื่อนไขค่าสถานะแนะนำ">
|
||||
{recommendedConditionLabels.map((label, index) => (
|
||||
<ToggleGroupItem
|
||||
key={index}
|
||||
value={String(index)}
|
||||
className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"
|
||||
>
|
||||
<TabsTrigger key={index} value={String(index)}>
|
||||
<MousePointerClickIcon aria-hidden="true" data-icon="inline-start" />
|
||||
{label}
|
||||
</ToggleGroupItem>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
) : null}
|
||||
{sections.has("tierlist") && tierConditionLabels.length > 1 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">เงื่อนไขอันดับอาร์ติแฟกต์</p>
|
||||
<ToggleGroup variant="outline" value={[tierCondition]} onValueChange={(values) => { if (values[0]) setTierCondition(values[0]); }}>
|
||||
{tierConditionLabels.map((label, index) => <ToggleGroupItem key={index} value={String(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground">{label}</ToggleGroupItem>)}
|
||||
</ToggleGroup>
|
||||
<Tabs value={tierCondition} onValueChange={setTierCondition}>
|
||||
<TabsList aria-label="เงื่อนไขอันดับอาร์ติแฟกต์">
|
||||
{tierConditionLabels.map((label, index) => <TabsTrigger key={index} value={String(index)}><MousePointerClickIcon aria-hidden="true" data-icon="inline-start" />{label}</TabsTrigger>)}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
) : null}
|
||||
{sections.has("target-stats") && targetConditionLabels.length > 1 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">เงื่อนไขค่าสถานะเป้าหมาย</p>
|
||||
<ToggleGroup
|
||||
variant="outline"
|
||||
value={[targetCondition]}
|
||||
onValueChange={(values) => {
|
||||
if (values[0]) setTargetCondition(values[0]);
|
||||
}}
|
||||
>
|
||||
<Tabs value={targetCondition} onValueChange={setTargetCondition}>
|
||||
<TabsList aria-label="เงื่อนไขค่าสถานะเป้าหมาย">
|
||||
{targetConditionLabels.map((label, index) => (
|
||||
<ToggleGroupItem
|
||||
key={index}
|
||||
value={String(index)}
|
||||
className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"
|
||||
>
|
||||
<TabsTrigger key={index} value={String(index)}>
|
||||
<MousePointerClickIcon aria-hidden="true" data-icon="inline-start" />
|
||||
{label}
|
||||
</ToggleGroupItem>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { MousePointerClickIcon } from "lucide-react";
|
||||
import { Children, type ReactNode, useState } from "react";
|
||||
|
||||
import { ScrollableNav } from "@/components/public/scrollable-nav";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
export function ConditionSwitcher({
|
||||
children,
|
||||
@@ -20,23 +21,21 @@ export function ConditionSwitcher({
|
||||
if (items.length <= 1) return items[0] ?? null;
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-4">
|
||||
<Tabs className="min-w-0 gap-4" value={active} onValueChange={setActive}>
|
||||
<ScrollableNav ariaLabel={label} className="w-full min-w-0">
|
||||
<ToggleGroup aria-label={label} variant="outline" value={[active]} onValueChange={(values) => {
|
||||
if (values[0]) setActive(values[0]);
|
||||
}}>
|
||||
<TabsList aria-label={label} className="mx-auto shrink-0">
|
||||
{items.map((_, index) => (
|
||||
<ToggleGroupItem
|
||||
<TabsTrigger
|
||||
key={index}
|
||||
value={String(index)}
|
||||
className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"
|
||||
>
|
||||
<MousePointerClickIcon className="text-primary group-aria-pressed/tabs:text-primary-foreground" aria-hidden="true" data-icon="inline-start" />
|
||||
{labels?.[index] || `Condition ${index + 1}`}
|
||||
</ToggleGroupItem>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</TabsList>
|
||||
</ScrollableNav>
|
||||
{items[Number(active)]}
|
||||
</div>
|
||||
{items.map((item, index) => <TabsContent key={index} value={String(index)} className="text-base">{item}</TabsContent>)}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import { searchItems } from "@/lib/search";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { SearchIcon, UsersIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { SearchIcon, StarIcon, UsersIcon } from "lucide-react";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
|
||||
import { ElementIcon } from "@/components/public/element-icon";
|
||||
|
||||
import {
|
||||
Card,
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
InputGroupInput,
|
||||
} from "@/components/ui/input-group";
|
||||
import { useVisitorPresence } from "@/hooks/use-presence";
|
||||
import { weaponTypeIconPath } from "@/lib/overview-image";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -36,6 +39,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
|
||||
type GuideCard = {
|
||||
id: string;
|
||||
@@ -46,6 +50,7 @@ type GuideCard = {
|
||||
version: number;
|
||||
aliases: string[];
|
||||
element: string | null;
|
||||
elementIconUrl: string | null;
|
||||
weaponType: string | null;
|
||||
rarity: number | null;
|
||||
updatedAt: Date;
|
||||
@@ -70,6 +75,13 @@ const weaponTypeLabels: Record<string, string> = {
|
||||
WEAPON_CATALYST: "สื่อเวท",
|
||||
};
|
||||
|
||||
const sortOptions = [
|
||||
{ value: "manual", label: "แนะนำ" },
|
||||
{ value: "recent", label: "อัปเดตล่าสุด" },
|
||||
{ value: "name-asc", label: "A - Z" },
|
||||
{ value: "name-desc", label: "Z - A" },
|
||||
] as const;
|
||||
|
||||
export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
|
||||
const searchParams = useSearchParams();
|
||||
const query = searchParams.get("q") ?? "";
|
||||
@@ -77,17 +89,16 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
|
||||
const elements = useMemo(() => [...new Set(guides.flatMap((guide) => guide.element ? [guide.element] : []))].sort(), [guides]);
|
||||
const weapons = useMemo(() => [...new Set(guides.flatMap((guide) => guide.weaponType ? [guide.weaponType] : []))].sort(), [guides]);
|
||||
const rarities = useMemo(() => [...new Set(guides.flatMap((guide) => guide.rarity ? [guide.rarity] : []))].sort((a, b) => b - a), [guides]);
|
||||
const elementParam = searchParams.get("element");
|
||||
const weaponParam = searchParams.get("weapon");
|
||||
const rarityParam = searchParams.get("rarity");
|
||||
const element = elementParam && elements.includes(elementParam) ? elementParam : "all";
|
||||
const weapon = weaponParam && weapons.includes(weaponParam) ? weaponParam : "all";
|
||||
const rarity = rarityParam && rarities.includes(Number(rarityParam)) ? rarityParam : "all";
|
||||
const sort = searchParams.get("sort") === "recent" ? "recent" : "manual";
|
||||
const elementIconUrls = useMemo(() => Object.fromEntries(guides.flatMap((guide) => guide.element && guide.elementIconUrl ? [[guide.element, guide.elementIconUrl]] : [])), [guides]);
|
||||
const selectedElements = [...new Set((searchParams.get("element") ?? "").split(",").filter((value) => elements.includes(value)))];
|
||||
const selectedWeapons = [...new Set((searchParams.get("weapon") ?? "").split(",").filter((value) => weapons.includes(value)))];
|
||||
const selectedRarities = [...new Set((searchParams.get("rarity") ?? "").split(",").filter((value) => rarities.includes(Number(value))))];
|
||||
const sortParam = searchParams.get("sort");
|
||||
const sort = sortParam === "recent" || sortParam === "name-asc" || sortParam === "name-desc" ? sortParam : "manual";
|
||||
const matchingGuides = guides.filter((guide) =>
|
||||
(element === "all" || guide.element === element) &&
|
||||
(weapon === "all" || guide.weaponType === weapon) &&
|
||||
(rarity === "all" || guide.rarity === Number(rarity)),
|
||||
(!selectedElements.length || (guide.element !== null && selectedElements.includes(guide.element))) &&
|
||||
(!selectedWeapons.length || (guide.weaponType !== null && selectedWeapons.includes(guide.weaponType))) &&
|
||||
(!selectedRarities.length || (guide.rarity !== null && selectedRarities.includes(String(guide.rarity)))),
|
||||
);
|
||||
const search = searchItems(matchingGuides, query,
|
||||
(guide) => `${guide.name} ${guide.overview} ${guide.aliases.join(" ")}`,
|
||||
@@ -95,6 +106,11 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
|
||||
const filtered = sort === "recent"
|
||||
? [...search.items].sort((left, right) =>
|
||||
new Date(right.publishedAt ?? right.updatedAt).getTime() - new Date(left.publishedAt ?? left.updatedAt).getTime())
|
||||
: sort === "name-asc" || sort === "name-desc"
|
||||
? [...search.items].sort((left, right) => {
|
||||
const order = left.name.localeCompare(right.name, "th", { numeric: true });
|
||||
return sort === "name-asc" ? order : -order;
|
||||
})
|
||||
: search.items;
|
||||
|
||||
function setParam(name: string, value: string) {
|
||||
@@ -106,8 +122,9 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<InputGroup className="h-11 min-w-64 flex-1">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<InputGroup className="h-9 w-48 shrink-0">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
@@ -118,11 +135,22 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
|
||||
aria-label="ค้นหาไกด์"
|
||||
/>
|
||||
</InputGroup>
|
||||
<div className="flex flex-wrap gap-2" aria-label="ตัวกรองไกด์">
|
||||
<GuideFilter label="ธาตุ" value={element} options={elements} optionLabels={elementLabels} onChange={(value) => setParam("element", value)} />
|
||||
<GuideFilter label="อาวุธ" value={weapon} options={weapons} optionLabels={weaponTypeLabels} onChange={(value) => setParam("weapon", value)} />
|
||||
<GuideFilter label="ดาว" value={rarity} options={rarities.map(String)} onChange={(value) => setParam("rarity", value)} />
|
||||
<GuideFilter label="เรียงตาม" value={sort} options={["manual", "recent"]} optionLabels={{ manual: "แนะนำ", recent: "อัปเดตล่าสุด" }} includeAll={false} onChange={(value) => setParam("sort", value)} />
|
||||
<Select value={sort} onValueChange={(next) => next && setParam("sort", next)}>
|
||||
<SelectTrigger aria-label="เรียงตาม"><SelectValue>{sortOptions.find((option) => option.value === sort)?.label}</SelectValue></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{sortOptions.map((option) => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2 overflow-x-auto pb-1" aria-label="ตัวกรองไกด์">
|
||||
<GuideToggleFilter label="ธาตุ" value={selectedElements} options={elements} optionLabels={elementLabels} optionIcon={(option) => elementIconUrls[option] ? <ElementIcon element={option} url={elementIconUrls[option]} /> : null} onChange={(values) => setParam("element", values.join(","))} />
|
||||
<GuideToggleFilter label="อาวุธ" value={selectedWeapons} options={weapons} optionLabels={weaponTypeLabels} optionIcon={(option) => {
|
||||
const src = weaponTypeIconPath(option);
|
||||
return src ? <Image src={src} alt="" width={16} height={16} className="size-4 object-contain" /> : null;
|
||||
}} onChange={(values) => setParam("weapon", values.join(","))} />
|
||||
<GuideToggleFilter label="ดาว" value={selectedRarities} options={rarities.map(String)} optionIcon={() => <StarIcon className="size-4 fill-current text-yellow-500" />} optionText={(option) => option} onChange={(values) => setParam("rarity", values.join(","))} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -176,34 +204,32 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
function GuideFilter({
|
||||
function GuideToggleFilter({
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
optionLabels = {},
|
||||
includeAll = true,
|
||||
optionIcon,
|
||||
optionText,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
value: string[];
|
||||
options: string[];
|
||||
optionLabels?: Record<string, string>;
|
||||
includeAll?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
optionIcon?: (value: string) => ReactNode;
|
||||
optionText?: (value: string) => string;
|
||||
onChange: (value: string[]) => void;
|
||||
}) {
|
||||
return (
|
||||
<Select value={value} onValueChange={(next) => next && onChange(next)}>
|
||||
<SelectTrigger aria-label={label}>
|
||||
<SelectValue>{optionLabels[value] ?? (value === "all" ? label : value)}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{includeAll ? <SelectItem value="all">{label}: ทั้งหมด</SelectItem> : null}
|
||||
<div className="shrink-0 border-l pl-2">
|
||||
<ToggleGroup aria-label={label} variant="outline" size="sm" value={value} onValueChange={onChange}>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option} value={option}>{optionLabels[option] ?? option}</SelectItem>
|
||||
<ToggleGroupItem key={option} value={option} aria-label={`${label}: ${optionLabels[option] ?? option}`} title={`${label}: ${optionLabels[option] ?? option}`} className="aria-pressed:border-primary aria-pressed:bg-transparent aria-pressed:text-primary">
|
||||
{optionIcon?.(option)}{optionText?.(option)}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,7 +209,6 @@ function GuideOverview({
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="mx-auto flex max-w-3xl flex-col gap-3 text-center">
|
||||
<p className="text-sm font-medium text-primary">ภาพรวมแนวทาง</p>
|
||||
<h1 className="font-heading text-4xl font-bold">{guide.name}</h1>
|
||||
<p className="text-lg font-semibold">{guide.overview}</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ArrowUpRightIcon, SearchIcon, XIcon } from "lucide-react";
|
||||
import { ArrowUpRightIcon, MousePointerClickIcon, SearchIcon, XIcon } from "lucide-react";
|
||||
import { useMemo, useState, type ReactNode } from "react";
|
||||
|
||||
import { GuideImageExport } from "@/components/public/guide-image-export";
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { elementBackgroundColor } from "@/lib/catalog/element";
|
||||
import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
|
||||
import { isCustomTravelerKey } from "@/lib/catalog/travelers";
|
||||
@@ -448,11 +449,11 @@ export function TeamSearch({ groups }: { groups: PublicGuide["teamGroups"] }) {
|
||||
|
||||
{groups.length > 1 ? (
|
||||
<ScrollableNav ariaLabel="เลือกเงื่อนไขทีม" className="w-full min-w-0">
|
||||
<ToggleGroup aria-label="เลือกเงื่อนไขทีม" variant="outline" value={[activeCondition]} onValueChange={(values) => {
|
||||
if (values[0]) setActiveCondition(values[0]);
|
||||
}}>
|
||||
{groups.map((group, index) => <ToggleGroupItem key={group.id} value={String(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground">{group.name || `เงื่อนไข ${index + 1}`}</ToggleGroupItem>)}
|
||||
</ToggleGroup>
|
||||
<Tabs className="mx-auto" value={activeCondition} onValueChange={setActiveCondition}>
|
||||
<TabsList aria-label="เลือกเงื่อนไขทีม">
|
||||
{groups.map((group, index) => <TabsTrigger key={group.id} value={String(index)}><MousePointerClickIcon aria-hidden="true" data-icon="inline-start" />{group.name || `เงื่อนไข ${index + 1}`}</TabsTrigger>)}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</ScrollableNav>
|
||||
) : null}
|
||||
{activeGroup?.note ? <p className="whitespace-pre-wrap text-muted-foreground">{activeGroup.note}</p> : null}
|
||||
|
||||
@@ -265,6 +265,7 @@ export async function listPublicGuideCards() {
|
||||
updatedAt: guides.updatedAt,
|
||||
publishedAt: guides.publishedAt,
|
||||
element: catalogCharacters.element,
|
||||
elementIconKey: catalogCharacters.elementIconKey,
|
||||
weaponType: catalogCharacters.weaponType,
|
||||
rarity: catalogCharacters.rarity,
|
||||
coverObjectKey: media.objectKey,
|
||||
@@ -279,10 +280,11 @@ export async function listPublicGuideCards() {
|
||||
alias: glossaryAliases.alias,
|
||||
}).from(glossaryAliases)]);
|
||||
const grouped = aliasesByItem(aliases);
|
||||
return rows.map(({ coverObjectKey, ...guide }) => ({
|
||||
return rows.map(({ coverObjectKey, elementIconKey, ...guide }) => ({
|
||||
...guide,
|
||||
aliases: grouped.character.get(guide.characterKey) ?? [],
|
||||
coverUrl: publicMediaUrl(coverObjectKey),
|
||||
elementIconUrl: elementIconKey ? publicMediaUrl(elementIconKey) : null,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user