feat : better ui
CI / Verify (push) Successful in 59s
CI / Build immutable images and deploy (push) Failing after 1m4s

This commit is contained in:
2026-08-31 00:48:33 +07:00 Unverified
parent fd841057c1
commit e3692fe75b
19 changed files with 465 additions and 114 deletions
+4 -4
View File
@@ -49,8 +49,8 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
<InputGroupInput
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="ค้นหา Guide…"
aria-label="ค้นหา Guide"
placeholder="ค้นหาไกด์…"
aria-label="ค้นหาไกด์"
/>
</InputGroup>
{filtered.length ? (
@@ -65,7 +65,7 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
<div className="relative aspect-square bg-muted">
<Image
src={guide.coverUrl}
alt={`Guide ${guide.name}`}
alt={`ไกด์ ${guide.name}`}
fill
className="object-cover"
/>
@@ -83,7 +83,7 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
) : (
<Empty className="border">
<EmptyHeader>
<EmptyTitle>ไม่พบ Guide</EmptyTitle>
<EmptyTitle>ไม่พบไกด์</EmptyTitle>
<EmptyDescription>ลองค้นหาด้วยชื่อตัวละครอื่น</EmptyDescription>
</EmptyHeader>
</Empty>
+50 -20
View File
@@ -48,6 +48,14 @@ export function publicSections(guide: PublicGuide) {
return guide.sections.filter((section) => hasSection(guide, section.slug));
}
function publicSectionTitle(slug: string, fallback: string): string {
if (slug === "weapon") return "อาวุธ";
if (slug === "artifact") return "อาร์ติแฟกต์และค่าสถานะ";
if (slug === "constellations") return "กลุ่มดาว";
if (slug === "team") return "คำนวณทีม";
return fallback;
}
export function StructuredGuideShell({
guide,
section,
@@ -135,14 +143,14 @@ function GuideOverview({
<div className="relative mx-auto aspect-square w-full max-w-3xl overflow-hidden rounded-xl border bg-muted">
<Image
src={guide.coverUrl}
alt={`Guide ${guide.name}`}
alt={`ไกด์ ${guide.name}`}
fill
priority
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">Build Overview</p>
<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}
@@ -155,7 +163,7 @@ function GuideOverview({
<Card className="h-full">
<CardHeader>
{navigationIconPath(item.slug) ? <Image src={navigationIconPath(item.slug)!} alt="" width={40} height={40} className="size-10 object-contain" /> : null}
<CardTitle>{item.title}</CardTitle>
<CardTitle>{publicSectionTitle(item.slug, item.title)}</CardTitle>
<CardDescription>เปิดดูคำแนะนำฉบับเต็ม</CardDescription>
<CardAction>
<ArrowUpRightIcon aria-hidden="true" className="size-5 text-primary" />
@@ -206,14 +214,14 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
<section className="flex flex-col gap-6">
<header className="flex flex-col items-center gap-3 text-center">
<p className="text-sm font-medium text-primary">อาวุธที่แนะนำ</p>
<h1 className="whitespace-nowrap font-heading text-4xl font-semibold">Weapons</h1>
<h1 className="whitespace-nowrap font-heading text-4xl font-semibold">อาวุธ</h1>
<a
href={`/api/guides/${guide.id}/weapons/image`}
download
className={buttonVariants({ variant: "outline", size: "sm" })}
>
<DownloadIcon data-icon="inline-start" />
Export image
ส่งออกภาพ
</a>
</header>
<div className="grid gap-3">
@@ -286,7 +294,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
อาร์ติแฟกต์และสเตตัสที่แนะนำ
</p>
<h1 className="font-heading text-4xl font-semibold">
Artifacts & Stats
อาร์ติแฟกต์และค่าสถานะ
</h1>
<a
href={`/api/guides/${guide.id}/artifacts/image`}
@@ -294,7 +302,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
className={buttonVariants({ variant: "outline", size: "sm" })}
>
<DownloadIcon data-icon="inline-start" />
Export image
ส่งออกภาพ
</a>
</header>
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
@@ -303,7 +311,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
{guide.artifactGroups.map((group) => (
<Card key={group.id}>
<CardHeader>
<CardTitle>{group.name}</CardTitle>
<CardTitle>{group.name === "Late Game" ? "เลทเกม" : group.name === "Early Game" ? "ช่วงต้นเกม" : group.name}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
{group.rows.map((row, index) => (
@@ -359,18 +367,18 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
<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">Artifact Stats</CardTitle>
<CardTitle className="text-2xl">ออฟหลักที่แนะนำ</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-3 p-0">
<ArtifactStat iconSrc="/icon/Icon_Sands_of_Eon.webp" label="Sands" values={profile.sands} />
<ArtifactStat iconSrc="/icon/Icon_Goblet_of_Eonothem.webp" label="Goblet" values={profile.goblet} />
<ArtifactStat iconSrc="/icon/Icon_Circlet_of_Logos.webp" label="Circlet" values={profile.circlet} />
<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>Stat Priority:</strong>
<strong>ลำดับค่าสถานะ:</strong>
<div className="flex flex-col items-center gap-1">
{profile.substats.map((stat, index) => (
<span key={`${stat}-${index}`} className={statPriorityColorClass(index)}>
@@ -386,7 +394,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
<CardContent className="flex flex-col gap-5">
{profile.targets.length ? (
<div>
<p className="mb-2 text-sm font-medium">Target Stats</p>
<p className="mb-2 text-sm font-medium">ค่าสถานะเป้าหมาย</p>
<div className="flex flex-col gap-1">
{profile.targets.map((target, index) => (
<div
@@ -398,7 +406,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}
<span>{normalizeTargetStat(target.stat) ?? target.stat}</span>
<span>{statLabel(normalizeTargetStat(target.stat) ?? target.stat)}</span>
</span>
<strong className="shrink-0">{target.value}</strong>
</div>
@@ -420,7 +428,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
<div className="relative size-14 overflow-hidden rounded-xl border bg-muted sm:size-20">
{catalogTalent?.iconUrl ? <Image src={catalogTalent.iconUrl} alt={talentName} fill className="object-contain" /> : <span className="grid size-full place-items-center font-semibold">{talent.type}</span>}
</div>
<Badge variant="secondary">{talent.level ? `Lv.${talent.level}+` : "—"}</Badge>
<Badge variant="secondary">{talent.level ? `เลเวล ${talent.level}+` : "—"}</Badge>
<p className="text-xs font-medium sm:text-sm">{talentName}</p>
<p className="text-[11px] text-muted-foreground sm:text-xs">{talentTypeLabel(talent.type)}</p>
</div>
@@ -453,7 +461,29 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
}
function statLabel(value: string): string {
return value === "EM" ? "Elemental Mastery" : value === "ER" ? "Energy Recharge" : value;
const labels: Record<string, string> = {
HP: "พลังชีวิต",
ATK: "พลังโจมตี",
DEF: "พลังป้องกัน",
EM: "ความชำนาญธาตุ",
ER: "ฟื้นฟูพลังงาน",
"CRIT RATE": "อัตราคริ",
"CRIT DAMAGE": "ความแรงคริ",
"ATK%": "พลังโจมตี%",
"HP%": "พลังชีวิต%",
"DEF%": "พลังป้องกัน%",
"CRIT Rate%/CRIT DMG%": "อัตราคริ/ความแรงคริ",
"Pyro DMG%": "ความเสียหายธาตุไฟ%",
"Hydro DMG%": "ความเสียหายธาตุน้ำ%",
"Cryo DMG%": "ความเสียหายธาตุน้ำแข็ง%",
"Electro DMG%": "ความเสียหายธาตุไฟฟ้า%",
"Anemo DMG%": "ความเสียหายธาตุลม%",
"Geo DMG%": "ความเสียหายธาตุดิน%",
"Dendro DMG%": "ความเสียหายธาตุไม้%",
"Physical DMG%": "ความเสียหายกายภาพ%",
"Healing Bonus%": "โบนัสการรักษา%",
};
return labels[value] ?? value;
}
function ArtifactStat({ iconSrc, label, values }: { iconSrc: string; label: string; values: string[] }) {
@@ -480,7 +510,7 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
<section className="flex flex-col gap-6">
<header className="text-center">
<p className="text-sm font-medium text-primary">กลุ่มดาว</p>
<h1 className="font-heading text-4xl font-semibold">Constellations</h1>
<h1 className="font-heading text-4xl font-semibold">กลุ่มดาว</h1>
</header>
<div className="grid gap-4 md:grid-cols-2">
{guide.constellations.map((row) => (
@@ -519,7 +549,7 @@ function TeamSection({ guide }: { guide: PublicGuide }) {
<section className="flex flex-col gap-6">
<header className="text-center">
<p className="text-sm font-medium text-primary">คำนวณความเสียหาย</p>
<h1 className="font-heading text-4xl font-semibold">Team Calc</h1>
<h1 className="font-heading text-4xl font-semibold">คำนวณทีม</h1>
</header>
<TeamSearch teams={guide.teams} />
</section>
@@ -530,7 +560,7 @@ function ExtraSection({ extra }: { extra: PublicGuide["extras"][number] }) {
return (
<section className="flex flex-col gap-6">
<header className="text-center">
<p className="text-sm font-medium text-primary">Extra Guide</p>
<p className="text-sm font-medium text-primary">คู่มือเสริม</p>
<h1 className="font-heading text-4xl font-semibold">{extra.title}</h1>
</header>
<div className="grid gap-5 md:grid-cols-2">
+12 -9
View File
@@ -38,9 +38,9 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
<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="Search teams, characters, weapons, or artifacts"
aria-label="ค้นหาทีม ตัวละคร อาวุธ หรืออาร์ติแฟกต์"
className="pl-9"
placeholder="Search teams, characters, weapons, or artifacts"
placeholder="ค้นหาทีม ตัวละคร อาวุธ หรืออาร์ติแฟกต์"
value={query}
onValueChange={setQuery}
/>
@@ -57,7 +57,7 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
<Card key={team.id}>
<CardHeader>
<CardTitle>{team.title}</CardTitle>
<CardDescription>{seconds}s rotation</CardDescription>
<CardDescription>ระยะเวลาโรเทชั่น {seconds} วิ</CardDescription>
<CardAction>
<a
href={`/api/teams/${team.id}/image`}
@@ -68,7 +68,7 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
})}
>
<DownloadIcon data-icon="inline-start" />
Export image
ส่งออกภาพ
</a>
</CardAction>
</CardHeader>
@@ -84,6 +84,9 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
fill
className="object-contain"
/>
<Kbd className="absolute top-0 left-0 bg-transparent text-white">
C{member.constellation}
</Kbd>
</div>
<div className="col-span-1 row-span-2 flex min-w-0 flex-col gap-2">
{member.weapon ? (
@@ -99,7 +102,7 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
fill
className="object-contain p-1"
/>
<Kbd className="absolute top-0 left-0 bg-transparent text-black">
<Kbd className="absolute top-0 left-0 bg-transparent text-white">
R{member.refinement}
</Kbd>
</div>
@@ -126,7 +129,7 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
</div>
</div>
<p className="mt-2 truncate text-sm font-medium">
C{member.constellation} {member.character.name}
{member.character.name}
</p>
<p className="text-sm font-semibold">
{Number(member.damage).toLocaleString(undefined, {
@@ -146,12 +149,12 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
<div className="flex items-end justify-between">
<div>
<p className="text-sm text-muted-foreground">
Total damage
ดาเมจรวม
</p>
<p className="font-semibold">{total.toLocaleString()}</p>
</div>
<div className="text-right">
<p className="text-sm text-muted-foreground">Total DPS</p>
<p className="text-sm text-muted-foreground">DPS</p>
<p className="text-3xl font-semibold text-primary">
{seconds > 0
? (total / seconds).toLocaleString(undefined, {
@@ -172,7 +175,7 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
) : (
<Card>
<CardContent className="py-8 text-center text-sm text-muted-foreground">
No teams match your search.
ไม่พบทีมที่ตรงกับคำค้นหา
</CardContent>
</Card>
)}