diff --git a/app/[character]/page.tsx b/app/[character]/page.tsx index 3421ed7..26b02ea 100644 --- a/app/[character]/page.tsx +++ b/app/[character]/page.tsx @@ -7,13 +7,23 @@ import { getPublicGuide } from "@/lib/guides/queries"; type Params = Promise<{ character: string }>; -export async function generateMetadata({ params }: { params: Params }): Promise { +export async function generateMetadata({ + params, +}: { + params: Params; +}): Promise { await connection(); const guide = await getPublicGuide((await params).character); - return guide ? { title: `${guide.name} Build Guide`, description: guide.overview } : { title: "ไม่พบ Guide" }; + return guide + ? { title: `${guide.name} Build Guide`, description: guide.overview } + : { title: "ไม่พบ Guide" }; } -export default async function GuideOverviewPage({ params }: { params: Params }) { +export default async function GuideOverviewPage({ + params, +}: { + params: Params; +}) { await connection(); const guide = await getPublicGuide((await params).character); if (!guide) notFound(); diff --git a/app/layout.tsx b/app/layout.tsx index 7798ce2..3d0786f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -23,7 +23,7 @@ export const metadata: Metadata = { default: "Buzz Guide", template: "%s", }, - description: "คู่มือตัวละคร บิลด์ ทีม และโรเทชันที่อ่านง่ายในทุกหน้าจอ", + description: "ไกด์ตัวละครจากช่องเกนชินไม่ใช่เกมมือถือ", }; export default function RootLayout({ children }: LayoutProps<"/">) { @@ -39,7 +39,7 @@ export default function RootLayout({ children }: LayoutProps<"/">) { {children} - + diff --git a/app/page.tsx b/app/page.tsx index 469cc9e..5489a45 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,5 +8,24 @@ import { listGuideCards } from "@/lib/guides/queries"; export default async function HomePage() { await connection(); const guides = await listGuideCards(); - return

Genshin Impact Build Guides

เลือกตัวละคร แล้วจัด Build และ Team ได้เลย

คู่มือแบบภาพ-first สำหรับ Weapons, Artifacts, Constellations และ Team Calc

; + return ( +
+ + +
+
+

+ Genshin Impact Build Guides +

+

+ เลือกตัวละครแล้วส่องดูได้เลย +

+

+ ไกด์ตัวละคร Weapons, Artifacts, Constellations และ Team Calc +

+
+ +
+
+ ); } diff --git a/app/register/page.tsx b/app/register/page.tsx index 8f943f9..14e7261 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -8,7 +8,7 @@ import { getAdminSession } from "@/lib/auth/server"; export default async function RegisterPage() { await connection(); if (await getAdminSession()) redirect("/admin"); - + if (await !getAdminSession()) redirect("/"); return ( null //
// diff --git a/components/admin/login-card.tsx b/components/admin/login-card.tsx index 4c2f8a7..b7e369b 100644 --- a/components/admin/login-card.tsx +++ b/components/admin/login-card.tsx @@ -115,12 +115,12 @@ export function LoginCard() { - - สร้างบัญชีผู้ดูแลชั่วคราว - +
+ ยังไม่มีบัญชี? {" "} + + สร้างบัญชีเลย + +
); diff --git a/components/public/site-header.tsx b/components/public/site-header.tsx index 9d58dc5..3c6c2ab 100644 --- a/components/public/site-header.tsx +++ b/components/public/site-header.tsx @@ -1,8 +1,5 @@ import Image from "next/image"; import Link from "next/link"; -import { SettingsIcon } from "lucide-react"; - -import { Button } from "@/components/ui/button"; export function SiteHeader() { return ( @@ -15,10 +12,6 @@ export function SiteHeader() { ไกด์ช่องเกนชินไม่ใช่เกมมือถือ -
); diff --git a/components/public/structured-directory.tsx b/components/public/structured-directory.tsx index 9c65f0d..a423a9c 100644 --- a/components/public/structured-directory.tsx +++ b/components/public/structured-directory.tsx @@ -6,15 +6,88 @@ import { SearchIcon } from "lucide-react"; import { useState } from "react"; import { Badge } from "@/components/ui/badge"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty"; -import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyTitle, +} from "@/components/ui/empty"; +import { + InputGroup, + InputGroupAddon, + InputGroupInput, +} from "@/components/ui/input-group"; -type GuideCard = { id: string; slug: string; name: string; overview: string; coverUrl: string; version: number }; +type GuideCard = { + id: string; + slug: string; + name: string; + overview: string; + coverUrl: string; + version: number; +}; export function StructuredDirectory({ guides }: { guides: GuideCard[] }) { const [query, setQuery] = useState(""); const normalized = query.trim().toLowerCase(); - const filtered = guides.filter((guide) => `${guide.name} ${guide.overview}`.toLowerCase().includes(normalized)); - return
setQuery(event.target.value)} placeholder="ค้นหา Guide…" aria-label="ค้นหา Guide" />{filtered.length ?
{filtered.map((guide) =>
{`Guide
{guide.name}{guide.overview}Build Guide
)}
: ไม่พบ Guideลองค้นหาด้วยชื่อตัวละครอื่น}
; + const filtered = guides.filter((guide) => + `${guide.name} ${guide.overview}`.toLowerCase().includes(normalized), + ); + return ( +
+ + + + + setQuery(event.target.value)} + placeholder="ค้นหา Guide…" + aria-label="ค้นหา Guide" + /> + + {filtered.length ? ( +
+ {filtered.map((guide) => ( + + +
+ {`Guide +
+ + {guide.name} + + {guide.overview} + + +
+ + ))} +
+ ) : ( + + + ไม่พบ Guide + ลองค้นหาด้วยชื่อตัวละครอื่น + + + )} +
+ ); } diff --git a/components/public/structured-guide.tsx b/components/public/structured-guide.tsx index 1a732e0..c7b0ec3 100644 --- a/components/public/structured-guide.tsx +++ b/components/public/structured-guide.tsx @@ -4,7 +4,14 @@ import { ChevronLeftIcon, DownloadIcon } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { buttonVariants } from "@/components/ui/button"; -import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Kbd } from "@/components/ui/kbd"; import { Separator } from "@/components/ui/separator"; import { SiteHeader } from "@/components/public/site-header"; @@ -15,51 +22,239 @@ import { cn } from "@/lib/utils"; function hasSection(guide: PublicGuide, slug: string): boolean { if (slug === "weapon") return guide.weapons.length > 0; - if (slug === "artifact") return guide.artifactGroups.some((group) => group.rows.length > 0) || Boolean(guide.artifactProfile && (guide.artifactProfile.sands.length || guide.artifactProfile.goblet.length || guide.artifactProfile.circlet.length)); + if (slug === "artifact") + return ( + guide.artifactGroups.some((group) => group.rows.length > 0) || + Boolean( + guide.artifactProfile && + (guide.artifactProfile.sands.length || + guide.artifactProfile.goblet.length || + guide.artifactProfile.circlet.length), + ) + ); if (slug === "constellations") return guide.constellations.length > 0; if (slug === "team") return guide.teams.length > 0; - return guide.extras.some((extra) => extra.slug === slug && extra.rows.length > 0); + return guide.extras.some( + (extra) => extra.slug === slug && extra.rows.length > 0, + ); } export function publicSections(guide: PublicGuide) { return guide.sections.filter((section) => hasSection(guide, section.slug)); } -export function StructuredGuideShell({ guide, section }: { guide: PublicGuide; section?: string }) { +export function StructuredGuideShell({ + guide, + section, +}: { + guide: PublicGuide; + section?: string; +}) { const sections = publicSections(guide); - return
{guide.name}

{guide.name}

Build Guide

{guide.character.element ? {guide.character.elementIconUrl ? : null}{guide.character.element} : null}
{section ? : }
; + return ( +
+ + +
+
+ + + +
+ {guide.name} +
+
+

{guide.name}

+
+ {guide.character.element ? ( + + {guide.character.elementIconUrl ? ( + + ) : null} + {guide.character.element} + + ) : null} +
+ +
+
+ {section ? ( + + ) : ( + + )} +
+
+ ); } -function GuideOverview({ guide, sections }: { guide: PublicGuide; sections: ReturnType }) { - return
{`Guide

Build Overview

{guide.name}

{guide.overview}

{sections.length ?
{sections.map((item) => {item.title}เปิดดูคำแนะนำฉบับเต็ม)}
: กำลังจัดทำเนื้อหากลับมาตรวจสอบอีกครั้งหลังผู้ดูแลเพิ่มข้อมูล}
; +function GuideOverview({ + guide, + sections, +}: { + guide: PublicGuide; + sections: ReturnType; +}) { + return ( +
+
+ {`Guide +
+
+

Build Overview

+

{guide.name}

+

+ {guide.overview} +

+
+ {sections.length ? ( +
+ {sections.map((item) => ( + + + + {item.title} + เปิดดูคำแนะนำฉบับเต็ม + + + + ))} +
+ ) : ( + + + กำลังจัดทำเนื้อหา + + กลับมาตรวจสอบอีกครั้งหลังผู้ดูแลเพิ่มข้อมูล + + + + )} +
+ ); } -function GuideSection({ guide, section }: { guide: PublicGuide; section: string }) { +function GuideSection({ + guide, + section, +}: { + guide: PublicGuide; + section: string; +}) { if (section === "weapon") return ; if (section === "artifact") return ; - if (section === "constellations") return ; + if (section === "constellations") + return ; if (section === "team") return ; const extra = guide.extras.find((item) => item.slug === section); return extra ? : null; } -function metric(value: string | null) { return value === null ? null : `${Number(value).toLocaleString(undefined, { maximumFractionDigits: 2 })}%`; } +function metric(value: string | null) { + return value === null + ? null + : `${Number(value).toLocaleString(undefined, { maximumFractionDigits: 2 })}%`; +} function WeaponSection({ guide }: { guide: PublicGuide }) { return (
-

อาวุธที่แนะนำ

Weapons

+
+

อาวุธที่แนะนำ

+

Weapons

+
{guide.weapons.map((row, index) => ( -
-
{row.weapon.name}
-

{index + 1}. {row.weapon.name} R{row.refinement}

+
+
+ {row.weapon.name} +
+

+ {index + 1}. {row.weapon.name} R{row.refinement} +

- {row.overallPercent ?

{metric(row.overallPercent)}

ดาเมททั้งทีม

: null} - {row.personalPercent ?

{metric(row.personalPercent)}

ดาเมจส่วนตัว

: null} + {row.overallPercent ? ( +
+

+ {metric(row.overallPercent)} +

+

ดาเมททั้งทีม

+
+ ) : null} + {row.personalPercent ? ( +
+

+ {metric(row.personalPercent)} +

+

ดาเมจส่วนตัว

+
+ ) : null}
@@ -69,12 +264,346 @@ function WeaponSection({ guide }: { guide: PublicGuide }) { ); } -function ArtifactSection({ guide }: { guide: PublicGuide }) { const profile = guide.artifactProfile; return

อาร์ติแฟกต์และสเตตัสที่แนะนำ

Artifacts & Stats

{guide.artifactGroups.map((group) => {group.name}{group.rows.map((row, index) =>
{row.first.name}
{row.second ?
{row.second.name}
: null}

{index + 1}. {row.first.name}{row.second ? ` + ${row.second.name}` : ""}

{metric(row.percent)}

)}
)}
{profile ? Recommended Stats{profile.targets.length ?

Target Stats

{profile.targets.map((target) =>
{target.stat}{target.value}
)}
: null}{profile.talents.length ?

Talent Priority

{[...profile.talents].sort((a,b) => a.priority-b.priority).map((talent) => {talent.priority}. {talent.type}{talent.level ? ` ${talent.level}` : ""})}
: null}{profile.note ?

{profile.note}

: null}
: null}
; } +function ArtifactSection({ guide }: { guide: PublicGuide }) { + const profile = guide.artifactProfile; + return ( +
+
+

+ อาร์ติแฟกต์และสเตตัสที่แนะนำ +

+

+ Artifacts & Stats +

+
+
+ {" "} +
+ {guide.artifactGroups.map((group) => ( + + + {group.name} + + + {group.rows.map((row, index) => ( +
+
+
+ {row.first.name} +
+ {row.second ? ( +
+ {row.second.name} +
+ ) : null} +
+
+

+ {index + 1}. {row.first.name} + {row.second ? ` + ${row.second.name}` : ""} +

+
+

+ {metric(row.percent)} +

+
+ ))} +
+
+ ))} +
+ {profile ? ( + + + สแตทที่แนะนำ + + + + + + + + {profile.targets.length ? ( +
+

Target Stats

+ {profile.targets.map((target) => ( +
+ {target.stat} + {target.value} +
+ ))} +
+ ) : null} + {profile.talents.length ? ( +
+

ลำดับการอัพพรสรววค์

+
+ {[...profile.talents] + .sort((a, b) => a.priority - b.priority) + .map((talent) => ( + + {talent.priority}. {talent.type} + {talent.level ? ` ${talent.level}` : ""} + + ))} +
+
+ ) : null} + {profile.note ? ( +
+

โน็ต

+

+ {profile.note} +

+
+ ) : null} +
+
+ ) : null} +
+
+ ); +} -function StatLine({ label, values }: { label: string; values: string[] }) { return

{label}

{values.map((value) => {value})}
; } +function StatLine({ label, values }: { label: string; values: string[] }) { + return ( +
+

{label}

+
+ {values.map((value) => ( + + {value} + + ))} +
+
+ ); +} -function ConstellationSection({ guide }: { guide: PublicGuide }) { return

กลุ่มดาว

Constellations

{guide.constellations.map((row) => {metric(row.cumulativePercent)}C{row.level} · {row.name}{row.iconUrl ? : null}

{row.detail}

)}
; } +function ConstellationSection({ guide }: { guide: PublicGuide }) { + return ( +
+
+

กลุ่มดาว

+

Constellations

+
+
+ {guide.constellations.map((row) => ( + + + + {metric(row.cumulativePercent)} + + + C{row.level} · {row.name} + + {row.iconUrl ? ( + + + + ) : null} + + +

{row.detail}

+
+
+ ))} +
+
+ ); +} -function TeamSection({ guide }: { guide: PublicGuide }) { return

คำนวณความเสียหาย

Team Calc

{guide.teams.map((team) => { const total = team.members.reduce((sum, member) => sum + Number(member.damage), 0); const seconds = Number(team.rotationSeconds); return {team.title}{seconds}s rotationExport image
{team.members.map((member) =>
{member.character.name}
{member.weapon ?
{member.weapon.name}R{member.refinement}
:
}{member.artifact ?
{member.artifact.name}
:
}

C{member.constellation} {member.character.name}

{Number(member.damage).toLocaleString(undefined, { maximumFractionDigits: 2 })}

{total ? (Number(member.damage) / total * 100).toFixed(2) : "0.00"}%

)}

Total damage

{total.toLocaleString()}

Total DPS

{seconds > 0 ? (total / seconds).toLocaleString(undefined, { maximumFractionDigits: 0 }) : "0"}

{team.combo}

; })}
; } +function TeamSection({ guide }: { guide: PublicGuide }) { + return ( +
+
+

คำนวณความเสียหาย

+

Team Calc

+
+
+ {guide.teams.map((team) => { + const total = team.members.reduce( + (sum, member) => sum + Number(member.damage), + 0, + ); + const seconds = Number(team.rotationSeconds); + return ( + + + {team.title} + {seconds}s rotation + + + + Export image + + + + +
+ {team.members.map((member) => ( +
+
+
+ {member.character.name} +
+
+ {member.weapon ? ( +
+ {member.weapon.name} + + R{member.refinement} + +
+ ) : ( +
+ )} + {member.artifact ? ( +
+ {member.artifact.name} +
+ ) : ( +
+ )} +
+
+

+ C{member.constellation} {member.character.name} +

+

+ {Number(member.damage).toLocaleString(undefined, { + maximumFractionDigits: 2, + })} +

+

+ {total + ? ((Number(member.damage) / total) * 100).toFixed(2) + : "0.00"} + % +

+
+ ))} +
+ +
+
+

+ Total damage +

+

{total.toLocaleString()}

+
+
+

Total DPS

+

+ {seconds > 0 + ? (total / seconds).toLocaleString(undefined, { + maximumFractionDigits: 0, + }) + : "0"} +

+
+
+

+ {team.combo} +

+ + + ); + })} +
+
+ ); +} -function ExtraSection({ extra }: { extra: PublicGuide["extras"][number] }) { return

Extra Guide

{extra.title}

{extra.rows.map((row) => {row.imageUrl ?
{row.heading}
: null}{row.heading}{row.body}{row.percent ? {metric(row.percent)} : null}
)}
; } +function ExtraSection({ extra }: { extra: PublicGuide["extras"][number] }) { + return ( +
+
+

Extra Guide

+

{extra.title}

+
+
+ {extra.rows.map((row) => ( + + {row.imageUrl ? ( +
+ {row.heading} +
+ ) : null} + + {row.heading} + {row.body} + {row.percent ? {metric(row.percent)} : null} + +
+ ))} +
+
+ ); +} diff --git a/lib/catalog/assets/traveler_anemo.webp b/lib/catalog/assets/traveler_anemo.webp new file mode 100644 index 0000000..7e46d9a Binary files /dev/null and b/lib/catalog/assets/traveler_anemo.webp differ diff --git a/lib/catalog/assets/traveler_cyro.webp b/lib/catalog/assets/traveler_cyro.webp new file mode 100644 index 0000000..1c84136 Binary files /dev/null and b/lib/catalog/assets/traveler_cyro.webp differ diff --git a/lib/catalog/assets/traveler_dendro.webp b/lib/catalog/assets/traveler_dendro.webp new file mode 100644 index 0000000..e8c7b8b Binary files /dev/null and b/lib/catalog/assets/traveler_dendro.webp differ diff --git a/lib/catalog/assets/traveler_electro.webp b/lib/catalog/assets/traveler_electro.webp new file mode 100644 index 0000000..e47f1fb Binary files /dev/null and b/lib/catalog/assets/traveler_electro.webp differ diff --git a/lib/catalog/assets/traveler_geo.webp b/lib/catalog/assets/traveler_geo.webp new file mode 100644 index 0000000..4235788 Binary files /dev/null and b/lib/catalog/assets/traveler_geo.webp differ diff --git a/lib/catalog/assets/traveler_hydro.webp b/lib/catalog/assets/traveler_hydro.webp new file mode 100644 index 0000000..c42f005 Binary files /dev/null and b/lib/catalog/assets/traveler_hydro.webp differ diff --git a/lib/catalog/assets/traveler_pyro.webp b/lib/catalog/assets/traveler_pyro.webp new file mode 100644 index 0000000..44853e9 Binary files /dev/null and b/lib/catalog/assets/traveler_pyro.webp differ