feat: update
CI / Verify (push) Successful in 55s
CI / Build immutable images and deploy (push) Successful in 1m13s

This commit is contained in:
2026-09-01 16:31:05 +07:00 Unverified
parent 8fa651684f
commit 1d19961d8b
11 changed files with 113 additions and 91 deletions
+1 -1
View File
@@ -25,5 +25,5 @@ export function GuideOverviewForm({ guide }: { guide: { id: string; version: num
initialVersion: guide.version,
persist: (next, expectedVersion) => saveGuideOverview(guide.id, { ...next, expectedVersion }),
});
return <Card><CardHeader><CardTitle>Overview</CardTitle><CardDescription>ภาพและข้อความนี้แสดงบนหน้าแรกและส่วนแนะนำของ Guide</CardDescription></CardHeader><CardContent><FieldGroup><Field><FieldLabel>Guide Image</FieldLabel><div className="relative aspect-square max-w-md overflow-hidden rounded-lg border"><Image src={cover.url} alt={`Guide ${guide.name}`} fill className="object-cover" /></div><MediaUploader onReady={(asset) => setCover(asset)} /></Field><Field><FieldLabel htmlFor="guide-overview">Overview</FieldLabel><Textarea id="guide-overview" value={overview} maxLength={500} onChange={(event) => setOverview(event.target.value)} /><FieldDescription>{overview.length}/500 ตัวอักษร</FieldDescription></Field></FieldGroup></CardContent><CardFooter className="justify-end"><AutosaveStatus state={autosave} issues={incompleteIssues} /></CardFooter></Card>;
return <Card><CardHeader><CardTitle>Overview</CardTitle><CardDescription>ภาพและข้อความนี้แสดงบนหน้าแรกและส่วนแนะนำของ Guide</CardDescription></CardHeader><CardContent><FieldGroup><Field><FieldLabel>Guide Image</FieldLabel><div className="relative mx-auto aspect-square w-full max-w-md overflow-hidden rounded-lg border"><Image src={cover.url} alt={`Guide ${guide.name}`} fill className="object-cover" /></div><MediaUploader onReady={(asset) => setCover(asset)} /></Field><Field><FieldLabel htmlFor="guide-overview">Overview</FieldLabel><Textarea id="guide-overview" value={overview} maxLength={500} onChange={(event) => setOverview(event.target.value)} /><FieldDescription>{overview.length}/500 ตัวอักษร</FieldDescription></Field></FieldGroup></CardContent><CardFooter className="justify-end"><AutosaveStatus state={autosave} issues={incompleteIssues} /></CardFooter></Card>;
}
+10 -11
View File
@@ -28,6 +28,7 @@ import {
} from "@dnd-kit/core";
import {
arrayMove,
horizontalListSortingStrategy,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
@@ -212,12 +213,10 @@ function TalentLevelPicker({ value, onChange }: { value?: string; onChange: (val
function SortableTalentCard({
talent,
index,
catalogTalent,
onLevelChange,
}: {
talent: TalentPriority;
index: number;
catalogTalent?: CatalogTalent;
onLevelChange: (level?: string) => void;
}) {
@@ -231,7 +230,7 @@ function SortableTalentCard({
<Card size="sm" className="h-full">
<CardHeader className="flex items-center gap-2">
{catalogTalent?.iconUrl ? <Image src={catalogTalent.iconUrl} alt="" width={40} height={40} className="size-10 shrink-0 rounded-lg bg-muted" /> : null}
<CardTitle className="min-w-0 flex-1 truncate">{index + 1}. {catalogTalent?.name ?? talent.type}</CardTitle>
<CardTitle className="min-w-0 flex-1 truncate">{talent.type}. {catalogTalent?.name ?? talent.type}</CardTitle>
<CardAction>
<Button type="button" variant="ghost" size="icon-sm" className="touch-none" {...attributes} {...listeners}>
<GripVerticalIcon />
@@ -460,13 +459,13 @@ function ArtifactEditor({ guide, artifacts, initial, catalogTalents }: EditorBas
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle>Recommended Stats</CardTitle><CardDescription>เลือกค่าจากรายการแทนปุ่ม และเลือก Main Stat ได้สูงสุด 2 ค่าต่อชิ้น</CardDescription></CardHeader>
<CardHeader><CardTitle>Recommended Stats</CardTitle><CardDescription>เลือกค่าจากรายการแทนปุ่ม และเลือก Main Stat ได้สูงสุด 3 ค่าต่อชิ้น</CardDescription></CardHeader>
<CardContent>
<FieldGroup>
<div className="grid gap-4 sm:grid-cols-3">
<StatSelect label="Sands / นาฬิกา" options={mainStatOptions.sands} value={sands} max={2} onChange={setSands} />
<StatSelect label="Goblet / ถ้วย" options={mainStatOptions.goblet} value={goblet} max={2} onChange={setGoblet} />
<StatSelect label="Circlet / หมวก" options={mainStatOptions.circlet} value={circlet} max={2} onChange={setCirclet} />
<StatSelect label="Sands / นาฬิกา" options={mainStatOptions.sands} value={sands} max={3} onChange={setSands} />
<StatSelect label="Goblet / ถ้วย" options={mainStatOptions.goblet} value={goblet} max={3} onChange={setGoblet} />
<StatSelect label="Circlet / หมวก" options={mainStatOptions.circlet} value={circlet} max={3} onChange={setCirclet} />
</div>
<StatSelect label="Substats" options={substatOptions} value={substats} max={substatOptions.length} onChange={setSubstats} />
<FieldSet>
@@ -486,12 +485,12 @@ function ArtifactEditor({ guide, artifacts, initial, catalogTalents }: EditorBas
</div>
))}
</div>
<FieldDescription>กรอกเฉพาะค่าเป็นตัวเลข เปอร์เซ็นต์ หรือข้อความ เช่น -%</FieldDescription>
<FieldDescription>กรอกเฉพาะค่าเป็นตัวเลข เปอร์เซ็นต์ หรือข้อความ เช่น -% หากไม่กรอกค่า ระบบจะไม่แสดงส่วนค่าสถานะเป้าหมาย</FieldDescription>
</FieldSet>
<FieldSet>
<FieldLegend>Talent Priority</FieldLegend>
<DndContext sensors={talentSensors} collisionDetection={closestCenter} onDragEnd={handleTalentDragEnd}>
<SortableContext items={orderedTalents.map((talent) => talent.type)} strategy={verticalListSortingStrategy}>
<SortableContext items={orderedTalents.map((talent) => talent.type)} strategy={horizontalListSortingStrategy}>
<div className="flex items-start gap-3 overflow-x-auto pb-1">
{orderedTalents.map((talent, index) => {
const catalogTalent = catalogTalents.find((item) => item.type === talent.type);
@@ -499,7 +498,6 @@ function ArtifactEditor({ guide, artifacts, initial, catalogTalents }: EditorBas
<div key={talent.type} className="flex shrink-0 items-center gap-3">
<SortableTalentCard
talent={talent}
index={index}
catalogTalent={catalogTalent}
onLevelChange={(level) => setTalents(orderedTalents.map((item) => item.type === talent.type ? { ...item, level } : item))}
/>
@@ -517,6 +515,7 @@ function ArtifactEditor({ guide, artifacts, initial, catalogTalents }: EditorBas
>
<ToggleGroupItem className={SELECTED_TOGGLE_CLASS} value="gt" aria-label="มากกว่า">&gt;</ToggleGroupItem>
<ToggleGroupItem className={SELECTED_TOGGLE_CLASS} value="gte" aria-label="มากกว่าหรือเท่ากับ">≥</ToggleGroupItem>
<ToggleGroupItem className={SELECTED_TOGGLE_CLASS} value="eq" aria-label="เท่ากับ">=</ToggleGroupItem>
</ToggleGroup>
) : null}
</div>
@@ -632,7 +631,7 @@ function TeamEditor({ guide, characters, weapons, artifacts, initial }: EditorBa
<CardHeader>
<CardTitle>ตัวละคร {memberIndex + 1}</CardTitle>
<CardDescription>{total ? `${((Number(member.damage) || 0) / total * 100).toFixed(2)}% ของ Team` : "0% ของ Team"}</CardDescription>
{character ? <CardAction><Avatar size="lg"><AvatarImage src={character.imageUrl} alt={character.name} /><AvatarFallback>{character.name.slice(0, 2).toUpperCase()}</AvatarFallback></Avatar></CardAction> : null}
{character ? <CardAction><Avatar><AvatarImage src={character.imageUrl} alt={character.name} /><AvatarFallback>{character.name.slice(0, 2).toUpperCase()}</AvatarFallback></Avatar></CardAction> : null}
</CardHeader>
<CardContent>
<FieldGroup>
+52 -40
View File
@@ -21,7 +21,7 @@ import { rarityGradientClass, rarityTextClass } from "@/lib/catalog/rarity";
import type { PublicGuide } from "@/lib/guides/queries";
import { artifactTierCount } from "@/lib/guides/artifacts";
import { navigationIconPath } from "@/lib/navigation";
import { normalizeTargetStat, statPriorityColorClass, targetStatIconName } from "@/lib/guides/stats";
import { normalizeTargetStat, recommendedStatIconName, statPriorityColorClass, targetStatIconName, targetStatOptions } from "@/lib/guides/stats";
import { normalizeTalentPriorities, talentRelationSymbol, talentTypeLabel } from "@/lib/guides/talents";
import { cn } from "@/lib/utils";
@@ -142,6 +142,13 @@ 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>
<div className="relative mx-auto aspect-square w-full max-w-3xl overflow-hidden rounded-xl border bg-muted">
<Image
src={guide.coverUrl}
@@ -151,13 +158,6 @@ function GuideOverview({
className="object-cover"
/>
</div>
<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-semibold">{guide.name}</h1>
<p className="text-lg leading-8 text-muted-foreground">
{guide.overview}
</p>
</div>
{sections.length ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{sections.map((item) => (
@@ -238,11 +238,12 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
rarityGradientClass(row.weapon.rarity),
)}
>
<div className="relative size-20 shrink-0">
<div className="relative size-24 shrink-0 md:size-28">
<Image
src={row.weapon.imageUrl}
alt={row.weapon.name}
fill
sizes="(min-width: 768px) 7rem, 6rem"
className="object-contain"
/>
</div>
@@ -312,6 +313,30 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
{" "}
<div className="flex flex-col gap-5">
{profile ? (
<Card className="overflow-hidden pt-0">
<CardHeader className="bg-primary py-4 text-center text-primary-foreground">
<CardTitle className="text-2xl">ออฟหลักที่แนะนำ</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-3 p-0">
<ArtifactStat iconSrc="/icon/Icon_Sands_of_Eon.webp" label="นาฬิกา" values={profile.sands} />
<ArtifactStat iconSrc="/icon/Icon_Goblet_of_Eonothem.webp" label="ถ้วย" values={profile.goblet} />
<ArtifactStat iconSrc="/icon/Icon_Circlet_of_Logos.webp" label="หัว" values={profile.circlet} />
</CardContent>
</Card>
) : null}
{profile?.substats.length ? (
<Card>
<CardContent className="flex flex-col items-center gap-2 py-5 text-center text-sm">
<strong>ออฟรอง</strong>
<div className="flex flex-col items-center gap-1">
{profile.substats.map((stat, index) => (
<ArtifactStatValue key={`${stat}-${index}`} value={stat} index={index} />
))}
</div>
</CardContent>
</Card>
) : null}
{guide.artifactGroups.map((group) => (
<Card key={group.id}>
<CardHeader>
@@ -323,7 +348,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
<div className="flex -space-x-3">
<div
className={cn(
"relative size-14 overflow-hidden rounded-lg border",
"relative size-16 overflow-hidden rounded-lg border sm:size-20",
rarityGradientClass(row.first.rarity),
)}
>
@@ -331,13 +356,14 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
src={row.first.imageUrl}
alt={row.first.name}
fill
sizes="(min-width: 640px) 5rem, 4rem"
className="object-contain"
/>
</div>
{row.second ? (
<div
className={cn(
"relative size-14 overflow-hidden rounded-lg border",
"relative size-16 overflow-hidden rounded-lg border sm:size-20",
rarityGradientClass(row.second.rarity),
)}
>
@@ -345,6 +371,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
src={row.second.imageUrl}
alt={row.second.name}
fill
sizes="(min-width: 640px) 5rem, 4rem"
className="object-contain"
/>
</div>
@@ -369,30 +396,6 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
</div>
{profile ? (
<div className="flex flex-col gap-5">
<Card className="overflow-hidden pt-0">
<CardHeader className="bg-primary py-4 text-center text-primary-foreground">
<CardTitle className="text-2xl">ออฟหลักที่แนะนำ</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-3 p-0">
<ArtifactStat iconSrc="/icon/Icon_Sands_of_Eon.webp" label="นาฬิกา" values={profile.sands} />
<ArtifactStat iconSrc="/icon/Icon_Goblet_of_Eonothem.webp" label="ถ้วย" values={profile.goblet} />
<ArtifactStat iconSrc="/icon/Icon_Circlet_of_Logos.webp" label="หัว" values={profile.circlet} />
</CardContent>
</Card>
{profile.substats.length ? (
<Card>
<CardContent className="flex flex-col items-center gap-2 py-5 text-center text-sm">
<strong>ลำดับค่าสถานะ:</strong>
<div className="flex flex-col items-center gap-1">
{profile.substats.map((stat, index) => (
<span key={`${stat}-${index}`} className={statPriorityColorClass(index)}>
{statLabel(stat)}
</span>
))}
</div>
</CardContent>
</Card>
) : null}
<Card>
<CardHeader><CardTitle>รายละเอียดเพิ่มเติม</CardTitle></CardHeader>
<CardContent className="flex flex-col gap-5">
@@ -409,7 +412,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
)}
>
<span className="flex min-w-0 items-center gap-2">
{targetStatIconName(target.stat) ? <Image src={`/icon/stats/${targetStatIconName(target.stat)}.webp`} alt="" width={28} height={28} className="size-7 shrink-0 object-contain brightness-0 invert" /> : null}
{targetStatIconName(target.stat) ? <Image src={`/icon/stats/${targetStatIconName(target.stat)}.webp`} alt="" width={14} height={14} className="size-3.5 shrink-0 object-contain brightness-0 invert" /> : null}
<span>{statLabel(normalizeTargetStat(target.stat) ?? target.stat)}</span>
</span>
<strong className="shrink-0">{target.value}</strong>
@@ -437,7 +440,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
<p className="text-[11px] text-muted-foreground sm:text-xs">{talentTypeLabel(talent.type)}</p>
</div>
{index < talents.length - 1 ? (
<span className="mt-4 text-xl font-semibold sm:mt-6 sm:text-3xl" aria-label={talent.relationToNext === "gte" ? "มากกว่าหรือเท่ากับ" : "มากกว่า"}>
<span className="mt-4 text-xl font-semibold sm:mt-6 sm:text-3xl" aria-label={talent.relationToNext === "gte" ? "มากกว่าหรือเท่ากับ" : talent.relationToNext === "eq" ? "เท่ากับ" : "มากกว่า"}>
{talentRelationSymbol(talent.relationToNext)}
</span>
) : null}
@@ -499,9 +502,7 @@ function ArtifactStat({ iconSrc, label, values }: { iconSrc: string; label: stri
<div className="flex max-w-full flex-col items-center gap-1 text-xs leading-5 sm:text-sm sm:leading-6">
{values.length
? values.map((value, index) => (
<span key={`${value}-${index}`} className={cn("max-w-full break-words text-center", statPriorityColorClass(index))}>
{statLabel(value)}
</span>
<ArtifactStatValue key={`${value}-${index}`} value={value} index={index} />
))
: <span>—</span>}
</div>
@@ -509,6 +510,17 @@ function ArtifactStat({ iconSrc, label, values }: { iconSrc: string; label: stri
);
}
function ArtifactStatValue({ value, index }: { value: string; index: number }) {
const iconName = recommendedStatIconName(value);
const hasIcon = iconName && targetStatOptions.some((stat) => targetStatIconName(stat) === iconName);
return (
<span className={cn("flex max-w-full items-center justify-center gap-1 text-center", statPriorityColorClass(index))}>
{hasIcon ? <Image src={`/icon/stats/${iconName}.webp`} alt="" width={20} height={20} className="size-5 shrink-0 object-contain brightness-0 invert" /> : null}
<span className="break-words">{statLabel(value)}</span>
</span>
);
}
function ConstellationSection({ guide }: { guide: PublicGuide }) {
const note = guide.sections.find((section) => section.slug === "constellations")?.note;
return (