feat : i have no idea what to commit
This commit is contained in:
@@ -1,99 +1,59 @@
|
||||
import { connection } from "next/server";
|
||||
import { getCachedPublicGuide } from "@/lib/cache/public";
|
||||
import { publicSections } from "@/components/public/structured-guide";
|
||||
import {
|
||||
generateOgImage,
|
||||
ogImageContentType,
|
||||
ogImageSize,
|
||||
} from "@/lib/og-image";
|
||||
|
||||
export const alt = "Buzz Guide section";
|
||||
export const alt = "Buzz Guide character guide";
|
||||
export const size = ogImageSize;
|
||||
export const contentType = ogImageContentType;
|
||||
|
||||
function sectionTitle(slug: string, fallback: string): string {
|
||||
if (slug === "weapon") return "อาวุธแนะนำ";
|
||||
if (slug === "artifact") return "อาร์ติแฟกต์และค่าสถานะ";
|
||||
if (slug === "constellations") return "กลุ่มดาว";
|
||||
if (slug === "team") return "ทีมแนะนำ";
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function sectionContent(
|
||||
guide: Awaited<ReturnType<typeof getCachedPublicGuide>>,
|
||||
slug: string,
|
||||
) {
|
||||
if (!guide) return { description: "คู่มือเกนชินอิมแพกต์", highlights: [] };
|
||||
|
||||
if (slug === "weapon") {
|
||||
return {
|
||||
description: "อาวุธที่เหมาะกับตัวละคร พร้อมอันดับและเปอร์เซ็นต์การใช้งาน",
|
||||
highlights: guide.weapons.slice(0, 3).map((item, index) => {
|
||||
const percent = item.overallPercent ? ` · ${item.overallPercent}%` : "";
|
||||
return `${index + 1}. ${item.weapon.name}${percent}`;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (slug === "artifact") {
|
||||
const artifactHighlights = guide.artifactGroups.flatMap((group) =>
|
||||
group.rows.slice(0, 2).map((row) =>
|
||||
`${group.name}: ${row.first.name}${row.second ? ` + ${row.second.name}` : ""}`,
|
||||
),
|
||||
);
|
||||
const targetHighlights = guide.artifactProfile?.targets
|
||||
.slice(0, 2)
|
||||
.map((target) => `${target.stat}: ${target.value}`) ?? [];
|
||||
return {
|
||||
description: "เซ็ตอาร์ติแฟกต์ ค่าสถานะหลัก และเป้าหมายที่ควรจัด",
|
||||
highlights: [...artifactHighlights, ...targetHighlights],
|
||||
};
|
||||
}
|
||||
|
||||
if (slug === "team") {
|
||||
return {
|
||||
description: "ทีมแนะนำพร้อมสมาชิกและเวลาหมุนคอมโบ",
|
||||
highlights: guide.teams
|
||||
.slice(0, 3)
|
||||
.map((team) => `${team.title} · ${team.members.length} ตัวละคร`),
|
||||
};
|
||||
}
|
||||
|
||||
if (slug === "constellations") {
|
||||
return {
|
||||
description: "รายละเอียดกลุ่มดาวและพลังที่เพิ่มขึ้นในแต่ละระดับ",
|
||||
highlights: guide.constellations
|
||||
.slice(0, 3)
|
||||
.map((constellation) => `C${constellation.level} ${constellation.name}`),
|
||||
};
|
||||
}
|
||||
|
||||
const extra = guide.extras.find((item) => item.slug === slug);
|
||||
return {
|
||||
description: extra?.title ?? "รายละเอียดเพิ่มเติมของคู่มือ",
|
||||
highlights: extra?.rows.slice(0, 3).map((row) => row.heading || row.body) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Image({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ character: string; page: string }>;
|
||||
params: Promise<{ character: string }>;
|
||||
}) {
|
||||
await connection();
|
||||
const { character, page } = await params;
|
||||
const guide = await getCachedPublicGuide(character);
|
||||
const section = guide
|
||||
? publicSections(guide).find((item) => item.slug === page)
|
||||
: null;
|
||||
if (!guide || !section) return new Response("Not found", { status: 404 });
|
||||
|
||||
const content = sectionContent(guide, page);
|
||||
const guide = await getCachedPublicGuide((await params).character);
|
||||
if (!guide) return new Response("Not found", { status: 404 });
|
||||
const recommendedWeapon = guide.weapons[0];
|
||||
const recommendedArtifact = guide.artifactGroups.find(
|
||||
(group) => group.rows.length > 0,
|
||||
)?.rows[0];
|
||||
return generateOgImage({
|
||||
title: guide.name,
|
||||
sub: `${sectionTitle(page, section.title)} · ${content.highlights.join(" · ") || content.description}`,
|
||||
sub: guide.overview,
|
||||
characterCard: true,
|
||||
backgroundUrl: guide.character.nameCardImageUrl,
|
||||
characterUrl: guide.character.splashImageUrl ?? guide.character.imageUrl,
|
||||
recommendedWeapon: recommendedWeapon
|
||||
? {
|
||||
name: recommendedWeapon.weapon.name,
|
||||
refinement: recommendedWeapon.refinement,
|
||||
rarity: recommendedWeapon.weapon.rarity,
|
||||
imageUrl: recommendedWeapon.weapon.imageUrl,
|
||||
overallPercent: recommendedWeapon.overallPercent,
|
||||
personalPercent: recommendedWeapon.personalPercent,
|
||||
}
|
||||
: null,
|
||||
recommendedArtifact: recommendedArtifact
|
||||
? {
|
||||
first: {
|
||||
name: recommendedArtifact.first.name,
|
||||
rarity: recommendedArtifact.first.rarity,
|
||||
imageUrl: recommendedArtifact.first.imageUrl,
|
||||
},
|
||||
second: recommendedArtifact.second
|
||||
? {
|
||||
name: recommendedArtifact.second.name,
|
||||
rarity: recommendedArtifact.second.rarity,
|
||||
imageUrl: recommendedArtifact.second.imageUrl,
|
||||
}
|
||||
: null,
|
||||
percent: recommendedArtifact.percent,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ type Params = Promise<{ character: string; page: string }>;
|
||||
|
||||
export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {
|
||||
await connection();
|
||||
const { character, page } = await params;
|
||||
const { character } = await params;
|
||||
const guide = await getCachedPublicGuide(character);
|
||||
return guide ? { title: `${page} - ${guide.name} - คู่มือ`, description: guide.overview } : { title: "ไม่พบไกด์" };
|
||||
return guide ? { title: `${guide.name}`, description: guide.overview } : { title: "ไม่พบไกด์" };
|
||||
}
|
||||
|
||||
export default async function GuideSectionPage({ params }: { params: Params }) {
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function generateMetadata({
|
||||
await connection();
|
||||
const guide = await getCachedPublicGuide((await params).character);
|
||||
return guide
|
||||
? { title: `${guide.name} - คู่มือ`, description: guide.overview }
|
||||
? { title: `${guide.name}`, description: guide.overview }
|
||||
: { title: "ไม่พบไกด์" };
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -29,8 +29,7 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
const deploymentId =
|
||||
process.env.NEXT_DEPLOYMENT_ID?.trim() || "development";
|
||||
const deploymentId = process.env.NEXT_DEPLOYMENT_ID?.trim() || "development";
|
||||
|
||||
return (
|
||||
<html
|
||||
@@ -42,7 +41,11 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
<PageProgress />
|
||||
{children}
|
||||
<DeploymentUpdateNotifier deploymentId={deploymentId} />
|
||||
<Toaster richColors closeButton position="bottom-right" />
|
||||
<Toaster
|
||||
richColors
|
||||
closeButton
|
||||
position="bottom-right"
|
||||
toastOptions={{ className: anuphan.className }}/>
|
||||
</TooltipProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,13 +4,13 @@ import {
|
||||
ogImageSize,
|
||||
} from "@/lib/og-image";
|
||||
|
||||
export const alt = "Buzz Guide - คู่มือเกนชินสาธารณะที่เตรียมไว้ให้คุณ";
|
||||
export const alt = "Buzz Guide ";
|
||||
export const size = ogImageSize;
|
||||
export const contentType = ogImageContentType;
|
||||
|
||||
export default function Image() {
|
||||
return generateOgImage({
|
||||
title: "ไกด์ตัวละคร",
|
||||
sub: "คู่มือเกนชินสาธารณะที่เตรียมไว้ให้คุณ",
|
||||
sub: "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,9 +14,6 @@ export default async function HomePage() {
|
||||
<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">
|
||||
<header className="max-w-3xl">
|
||||
<p className="text-sm font-medium text-primary">
|
||||
คู่มือการจัดตัวละครเกนชินอิมแพกต์
|
||||
</p>
|
||||
<h1 className="mt-2 font-heading text-4xl font-semibold">
|
||||
เลือกตัวละครแล้วส่องดูได้เลย
|
||||
</h1>
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function SiteHeader({ editHref = "/admin" }: { editHref?: string }
|
||||
className="mr-auto flex min-h-11 items-center gap-3 rounded-lg font-heading font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<Image src="/icon.png" alt="" width={36} height={36} className="size-9 object-contain" />
|
||||
<span className="hidden sm:inline">ไกด์ช่องเกนชินไม่ใช่เกมมือถือ</span>
|
||||
<span className="hidden sm:inline">ไกด์เกนชินไม่ใช่เกมมือถือ</span>
|
||||
</Link>
|
||||
<nav aria-label="Public navigation" className="flex items-center gap-1">
|
||||
{isAdmin ? <Button variant="ghost" size="sm" nativeButton={false} render={<Link href={editHref} />}>ไปหน้าแก้ไข</Button> : null}
|
||||
|
||||
@@ -21,8 +21,18 @@ 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, recommendedStatIconName, statPriorityColorClass, targetStatIconName, targetStatOptions } from "@/lib/guides/stats";
|
||||
import { normalizeTalentPriorities, talentRelationSymbol, talentTypeLabel } from "@/lib/guides/talents";
|
||||
import {
|
||||
normalizeTargetStat,
|
||||
recommendedStatIconName,
|
||||
statPriorityColorClass,
|
||||
targetStatIconName,
|
||||
targetStatOptions,
|
||||
} from "@/lib/guides/stats";
|
||||
import {
|
||||
normalizeTalentPriorities,
|
||||
talentRelationSymbol,
|
||||
talentTypeLabel,
|
||||
} from "@/lib/guides/talents";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function hasSection(guide: PublicGuide, slug: string): boolean {
|
||||
@@ -64,7 +74,9 @@ export function StructuredGuideShell({
|
||||
section?: string;
|
||||
}) {
|
||||
const sections = publicSections(guide);
|
||||
const currentPageTitle = section ? sections.find((item) => item.slug === section)?.title ?? section : "Overview";
|
||||
const currentPageTitle = section
|
||||
? (sections.find((item) => item.slug === section)?.title ?? section)
|
||||
: "Overview";
|
||||
return (
|
||||
<div className="min-h-svh bg-background">
|
||||
<LiveRefresh endpoint={`/api/events/page/${guide.id}`} />
|
||||
@@ -78,7 +90,12 @@ export function StructuredGuideShell({
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</Link>
|
||||
<div className={cn("relative size-11 overflow-hidden rounded-lg border", rarityGradientClass(guide.character.rarity))}>
|
||||
<div
|
||||
className={cn(
|
||||
"relative size-11 overflow-hidden rounded-lg border",
|
||||
rarityGradientClass(guide.character.rarity),
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={guide.character.imageUrl}
|
||||
alt={guide.name}
|
||||
@@ -87,8 +104,12 @@ export function StructuredGuideShell({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col items-start gap-1">
|
||||
<p className="truncate text-sm text-muted-foreground">{guide.name}</p>
|
||||
<p className="truncate font-heading text-2xl font-semibold">{currentPageTitle}</p>
|
||||
<p className="truncate text-sm text-muted-foreground">
|
||||
{guide.name}
|
||||
</p>
|
||||
<p className="truncate font-heading text-2xl font-semibold">
|
||||
{currentPageTitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollableNav
|
||||
@@ -99,24 +120,42 @@ export function StructuredGuideShell({
|
||||
href={`/${guide.slug}`}
|
||||
className={cn(
|
||||
"flex min-h-8 shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
!section ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
!section
|
||||
? "bg-muted text-foreground"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
prefetch
|
||||
>
|
||||
<Image src={navigationIconPath("overview")!} alt="" width={20} height={20} className="size-5 object-contain" />
|
||||
<Image
|
||||
src={navigationIconPath("overview")!}
|
||||
alt=""
|
||||
width={20}
|
||||
height={20}
|
||||
className="size-5 object-contain"
|
||||
/>
|
||||
Overview
|
||||
</Link>
|
||||
{sections.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={`/${guide.slug}/${item.slug}`}
|
||||
className={cn(
|
||||
key={item.id}
|
||||
href={`/${guide.slug}/${item.slug}`}
|
||||
className={cn(
|
||||
"flex min-h-8 shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
section === item.slug ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
prefetch
|
||||
section === item.slug
|
||||
? "bg-muted text-foreground"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
prefetch
|
||||
>
|
||||
{navigationIconPath(item.slug) ? <Image src={navigationIconPath(item.slug)!} alt="" width={20} height={20} className="size-5 object-contain" /> : null}
|
||||
{navigationIconPath(item.slug) ? (
|
||||
<Image
|
||||
src={navigationIconPath(item.slug)!}
|
||||
alt=""
|
||||
width={20}
|
||||
height={20}
|
||||
className="size-5 object-contain"
|
||||
/>
|
||||
) : null}
|
||||
{item.title}
|
||||
</Link>
|
||||
))}
|
||||
@@ -145,9 +184,7 @@ function GuideOverview({
|
||||
<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>
|
||||
<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
|
||||
@@ -164,11 +201,24 @@ function GuideOverview({
|
||||
<Link key={item.id} href={`/${guide.slug}/${item.slug}`} prefetch>
|
||||
<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>{publicSectionTitle(item.slug, item.title)}</CardTitle>
|
||||
{navigationIconPath(item.slug) ? (
|
||||
<Image
|
||||
src={navigationIconPath(item.slug)!}
|
||||
alt=""
|
||||
width={40}
|
||||
height={40}
|
||||
className="size-10 object-contain"
|
||||
/>
|
||||
) : null}
|
||||
<CardTitle>
|
||||
{publicSectionTitle(item.slug, item.title)}
|
||||
</CardTitle>
|
||||
<CardDescription>เปิดดูคำแนะนำฉบับเต็ม</CardDescription>
|
||||
<CardAction>
|
||||
<ArrowUpRightIcon aria-hidden="true" className="size-5 text-primary" />
|
||||
<ArrowUpRightIcon
|
||||
aria-hidden="true"
|
||||
className="size-5 text-primary"
|
||||
/>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
@@ -212,13 +262,21 @@ function metric(value: string | null) {
|
||||
}
|
||||
|
||||
function WeaponSection({ guide }: { guide: PublicGuide }) {
|
||||
const note = guide.sections.find((section) => section.slug === "weapon")?.note;
|
||||
const note = guide.sections.find(
|
||||
(section) => section.slug === "weapon",
|
||||
)?.note;
|
||||
return (
|
||||
<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">อาวุธ</h1>
|
||||
{note ? <p className="max-w-3xl whitespace-pre-wrap text-muted-foreground">{note}</p> : null}
|
||||
<h1 className="whitespace-nowrap font-heading text-4xl font-semibold">
|
||||
อาวุธ
|
||||
</h1>
|
||||
{note ? (
|
||||
<p className="max-w-3xl whitespace-pre-wrap text-muted-foreground">
|
||||
{note}
|
||||
</p>
|
||||
) : null}
|
||||
<a
|
||||
href={`/api/guides/${guide.id}/weapons/image`}
|
||||
download
|
||||
@@ -238,12 +296,12 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
|
||||
rarityGradientClass(row.weapon.rarity),
|
||||
)}
|
||||
>
|
||||
<div className="relative size-24 shrink-0 md:size-28">
|
||||
<div className="relative size-20 shrink-0">
|
||||
<Image
|
||||
src={row.weapon.imageUrl}
|
||||
alt={row.weapon.name}
|
||||
fill
|
||||
sizes="(min-width: 768px) 7rem, 6rem"
|
||||
sizes="(min-width: 768px) 5rem, 4rem"
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
@@ -251,7 +309,9 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
|
||||
<p className="truncate text-base font-semibold sm:text-lg">
|
||||
{index + 1}. {row.weapon.name} R{row.refinement}
|
||||
</p>
|
||||
{row.note ? <p className="whitespace-pre-wrap text-sm">{row.note}</p> : null}
|
||||
{row.note ? (
|
||||
<p className="whitespace-pre-wrap text-sm">{row.note}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-around gap-4 bg-white px-4 py-5 text-black">
|
||||
@@ -263,7 +323,9 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
|
||||
rarityTextClass(row.weapon.rarity),
|
||||
)}
|
||||
>
|
||||
{Number(row.overallPercent) === 0 ? "-%" : metric(row.overallPercent)}
|
||||
{Number(row.overallPercent) === 0
|
||||
? "-%"
|
||||
: metric(row.overallPercent)}
|
||||
</p>
|
||||
<p className="mt-1 text-sm sm:text-base">ดาเมททั้งทีม</p>
|
||||
</div>
|
||||
@@ -276,7 +338,9 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
|
||||
rarityTextClass(row.weapon.rarity),
|
||||
)}
|
||||
>
|
||||
{Number(row.personalPercent) === 0 ? "-%" : metric(row.personalPercent)}
|
||||
{Number(row.personalPercent) === 0
|
||||
? "-%"
|
||||
: metric(row.personalPercent)}
|
||||
</p>
|
||||
<p className="mt-1 text-sm sm:text-base">ดาเมจส่วนตัว</p>
|
||||
</div>
|
||||
@@ -296,10 +360,10 @@ function ArtifactSection({ 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="font-heading text-4xl font-semibold">
|
||||
อาร์ติแฟกต์และค่าสถานะ
|
||||
Artifact & Stats
|
||||
</h1>
|
||||
<a
|
||||
href={`/api/guides/${guide.id}/artifacts/image`}
|
||||
@@ -316,150 +380,231 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
|
||||
{profile ? (
|
||||
<Card className="overflow-hidden pt-0">
|
||||
<CardHeader className="bg-primary py-4 text-center text-primary-foreground">
|
||||
<CardTitle className="text-2xl">ออฟหลักที่แนะนำ</CardTitle>
|
||||
<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} />
|
||||
<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>
|
||||
<strong>ออฟรองที่ดีที่สุด</strong>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
{profile.substats.map((stat, index) => (
|
||||
<ArtifactStatValue key={`${stat}-${index}`} value={stat} index={index} />
|
||||
<ArtifactStatValue
|
||||
key={`${stat}-${index}`}
|
||||
value={stat}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
<div className="order-first flex flex-col gap-5">
|
||||
{guide.artifactGroups.map((group) => (
|
||||
<Card key={group.id}>
|
||||
<CardHeader>
|
||||
<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) => (
|
||||
<div key={row.id} className={cn("flex items-center gap-3 rounded-md px-3 py-2", index % 2 === 0 ? "bg-muted/60" : "bg-muted/20")}>
|
||||
<div className="flex -space-x-3">
|
||||
<div
|
||||
className={cn(
|
||||
"relative size-16 overflow-hidden rounded-lg border sm:size-20",
|
||||
rarityGradientClass(row.first.rarity),
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={row.first.imageUrl}
|
||||
alt={row.first.name}
|
||||
fill
|
||||
sizes="(min-width: 640px) 5rem, 4rem"
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
{row.second ? (
|
||||
{guide.artifactGroups.map((group) => (
|
||||
<Card key={group.id}>
|
||||
<CardHeader>
|
||||
<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) => (
|
||||
<div
|
||||
key={row.id}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-md px-3 py-2",
|
||||
index % 2 === 0 ? "bg-muted/60" : "bg-muted/20",
|
||||
)}
|
||||
>
|
||||
<div className="flex -space-x-3">
|
||||
<div
|
||||
className={cn(
|
||||
"relative size-16 overflow-hidden rounded-lg border sm:size-20",
|
||||
rarityGradientClass(row.second.rarity),
|
||||
"relative size-14 overflow-hidden rounded-lg border sm:size-16",
|
||||
rarityGradientClass(row.first.rarity),
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={row.second.imageUrl}
|
||||
alt={row.second.name}
|
||||
src={row.first.imageUrl}
|
||||
alt={row.first.name}
|
||||
fill
|
||||
sizes="(min-width: 640px) 5rem, 4rem"
|
||||
sizes="(min-width: 640px) 4rem, 3.5rem"
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium">
|
||||
{artifactTierCount(row.second?.name ?? null)}{" "}
|
||||
{row.first.name}
|
||||
{row.second ? ` + ${row.second.name}` : ""}
|
||||
{row.second ? (
|
||||
<div
|
||||
className={cn(
|
||||
"relative size-14 overflow-hidden rounded-lg border sm:size-16",
|
||||
rarityGradientClass(row.second.rarity),
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={row.second.imageUrl}
|
||||
alt={row.second.name}
|
||||
fill
|
||||
sizes="(min-width: 640px) 4rem, 3.5rem"
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium">
|
||||
{artifactTierCount(row.second?.name ?? null)}{" "}
|
||||
{row.first.name}
|
||||
{row.second ? ` + ${row.second.name}` : ""}
|
||||
</p>
|
||||
{row.note ? (
|
||||
<p className="whitespace-pre-wrap text-sm text-muted-foreground">
|
||||
{row.note}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-2xl font-semibold text-primary">
|
||||
{Number(row.percent) === 0 ? "-%" : metric(row.percent)}
|
||||
</p>
|
||||
{row.note ? <p className="whitespace-pre-wrap text-sm text-muted-foreground">{row.note}</p> : null}
|
||||
</div>
|
||||
<p className="text-2xl font-semibold text-primary">
|
||||
{Number(row.percent) === 0 ? "-%" : metric(row.percent)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{profile ? (
|
||||
<div className="flex flex-col gap-5">
|
||||
<Card>
|
||||
<CardHeader><CardTitle>รายละเอียดเพิ่มเติม</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-5">
|
||||
{profile.targets.length ? (
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium">ค่าสถานะเป้าหมาย</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
{profile.targets.map((target, index) => (
|
||||
<div
|
||||
key={`${target.stat}-${target.value}`}
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-4 rounded-md px-3 py-2 text-sm",
|
||||
index % 2 === 0 ? "bg-muted/60" : "bg-muted/20",
|
||||
)}
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
{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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{profile.talents.length ? (
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium">ลำดับการอัพพรสวรรค์</p>
|
||||
<div className="flex items-start justify-center gap-1 sm:gap-3">
|
||||
{normalizeTalentPriorities(profile.talents)
|
||||
.map((talent, index, talents) => {
|
||||
const catalogTalent = guide.character.talents.find((item) => item.type === talent.type);
|
||||
const talentName = catalogTalent?.name ?? talent.type;
|
||||
return (
|
||||
<div key={talent.type} className="flex min-w-0 items-start gap-1 sm:gap-3">
|
||||
<div className="flex w-16 flex-col items-center gap-2 text-center sm:w-28">
|
||||
<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 ? `เลเวล ${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>
|
||||
{index < talents.length - 1 ? (
|
||||
<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>
|
||||
{profile.targets.length ? (
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium">ค่าสถานะต่ำๆที่ควรจะปั้นไว้</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
{profile.targets.map((target, index) => (
|
||||
<div
|
||||
key={`${target.stat}-${target.value}`}
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-4 rounded-md px-3 py-2 text-sm",
|
||||
index % 2 === 0 ? "bg-muted/60" : "bg-muted/20",
|
||||
)}
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
{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}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<span>
|
||||
{statLabel(
|
||||
normalizeTargetStat(target.stat) ?? target.stat,
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<strong className="shrink-0">{target.value}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{profile.note ? (
|
||||
<div className="pt-3">
|
||||
<p className="mb-2 text-sm font-medium">โน็ต</p>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{profile.note}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
) : null}
|
||||
{profile.talents.length ? (
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium">
|
||||
ลำดับการอัพพรสวรรค์
|
||||
</p>
|
||||
<div className="flex items-start justify-center gap-1 sm:gap-3">
|
||||
{normalizeTalentPriorities(profile.talents).map(
|
||||
(talent, index, talents) => {
|
||||
const catalogTalent = guide.character.talents.find(
|
||||
(item) => item.type === talent.type,
|
||||
);
|
||||
const talentName = catalogTalent?.name ?? talent.type;
|
||||
return (
|
||||
<div
|
||||
key={talent.type}
|
||||
className="flex min-w-0 items-start gap-1 sm:gap-3"
|
||||
>
|
||||
<div className="flex w-16 flex-col items-center gap-2 text-center sm:w-28">
|
||||
<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
|
||||
? `เลเวล ${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>
|
||||
{index < talents.length - 1 ? (
|
||||
<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}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{profile.note ? (
|
||||
<div className="pt-3">
|
||||
<p className="mb-2 text-sm font-medium">โน็ต</p>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{profile.note}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -495,18 +640,38 @@ function statLabel(value: string): string {
|
||||
return labels[value] ?? value;
|
||||
}
|
||||
|
||||
function ArtifactStat({ iconSrc, label, values }: { iconSrc: string; label: string; values: string[] }) {
|
||||
function ArtifactStat({
|
||||
iconSrc,
|
||||
label,
|
||||
values,
|
||||
}: {
|
||||
iconSrc: string;
|
||||
label: string;
|
||||
values: string[];
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col items-center gap-3 border-r p-3 text-center last:border-r-0 sm:p-5">
|
||||
<Image src={iconSrc} alt="" width={48} height={48} className="size-12 object-contain" />
|
||||
<Image
|
||||
src={iconSrc}
|
||||
alt=""
|
||||
width={48}
|
||||
height={48}
|
||||
className="size-12 object-contain"
|
||||
/>
|
||||
<p className="font-semibold">{label}</p>
|
||||
<Separator />
|
||||
<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) => (
|
||||
<ArtifactStatValue key={`${value}-${index}`} value={value} index={index} />
|
||||
))
|
||||
: <span>-</span>}
|
||||
{values.length ? (
|
||||
values.map((value, index) => (
|
||||
<ArtifactStatValue
|
||||
key={`${value}-${index}`}
|
||||
value={value}
|
||||
index={index}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<span>-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -514,23 +679,44 @@ 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);
|
||||
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={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;
|
||||
const note = guide.sections.find(
|
||||
(section) => section.slug === "constellations",
|
||||
)?.note;
|
||||
return (
|
||||
<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="font-heading text-4xl font-semibold">กลุ่มดาว</h1>
|
||||
{note ? <p className="max-w-3xl whitespace-pre-wrap text-muted-foreground">{note}</p> : null}
|
||||
{note ? (
|
||||
<p className="max-w-3xl whitespace-pre-wrap text-muted-foreground">
|
||||
{note}
|
||||
</p>
|
||||
) : null}
|
||||
</header>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{guide.constellations.map((row) => (
|
||||
@@ -546,7 +732,12 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
|
||||
/>
|
||||
) : null}
|
||||
<div className="min-w-0">
|
||||
<CardTitle className={cn("text-3xl text-white", row.isGreat && "text-warning")}>
|
||||
<CardTitle
|
||||
className={cn(
|
||||
"text-3xl text-white",
|
||||
row.isGreat && "text-warning",
|
||||
)}
|
||||
>
|
||||
{metric(row.cumulativePercent)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-base font-medium text-white">
|
||||
@@ -587,7 +778,12 @@ function ExtraSection({ extra }: { extra: PublicGuide["extras"][number] }) {
|
||||
{extra.rows.map((row) => (
|
||||
<Card key={row.id} className="overflow-hidden py-0">
|
||||
{row.imageUrl ? (
|
||||
<div className={cn("relative bg-muted", row.ratio === "1:1" ? "aspect-square" : "aspect-video")}>
|
||||
<div
|
||||
className={cn(
|
||||
"relative bg-muted",
|
||||
row.ratio === "1:1" ? "aspect-square" : "aspect-video",
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={row.imageUrl}
|
||||
alt={row.heading}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { DownloadIcon, SearchIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
@@ -58,7 +59,7 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
||||
<Card key={team.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{team.title}</CardTitle>
|
||||
<CardDescription>ระยะเวลาโรเทชั่น {seconds} วิ</CardDescription>
|
||||
<CardDescription>โรเทชั่น {seconds} วิ</CardDescription>
|
||||
<CardAction>
|
||||
<a
|
||||
href={`/api/teams/${team.id}/image`}
|
||||
@@ -84,7 +85,7 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
||||
<div className="flex flex-col gap-2 sm:grid sm:grid-cols-3">
|
||||
<div
|
||||
className={cn(
|
||||
"relative aspect-square min-h-0 overflow-hidden rounded-lg bg-muted sm:col-span-2 sm:row-span-2 sm:aspect-auto",
|
||||
"relative aspect-square min-h-0 overflow-hidden rounded-lg bg-muted sm:col-span-2 sm:row-span-2",
|
||||
rarityGradientClass(isCustomTravelerKey(member.character.key) ? 5 : member.character.rarity),
|
||||
)}
|
||||
>
|
||||
@@ -106,7 +107,7 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
||||
rel="noreferrer"
|
||||
aria-label={`เปิดข้อมูลอาวุธ ${member.weapon.name} บน Lunaris`}
|
||||
className={cn(
|
||||
"relative aspect-square overflow-hidden rounded-md bg-muted outline-none transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-ring",
|
||||
"relative aspect-square overflow-hidden rounded-md bg-muted outline-none",
|
||||
rarityGradientClass(member.weapon.rarity),
|
||||
)}
|
||||
>
|
||||
@@ -124,13 +125,13 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
||||
<div className="aspect-square rounded-md border bg-muted" />
|
||||
)}
|
||||
{member.artifact ? (
|
||||
<a
|
||||
<Link
|
||||
href={`https://lunaris.moe/artifact/${encodeURIComponent(member.artifact.key)}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={`เปิดข้อมูลอาร์ติแฟกต์ ${member.artifact.name} บน Lunaris`}
|
||||
className={cn(
|
||||
"relative aspect-square overflow-hidden rounded-md bg-muted outline-none transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-ring",
|
||||
"relative aspect-square overflow-hidden rounded-md bg-muted",
|
||||
rarityGradientClass(member.artifact.rarity),
|
||||
)}
|
||||
>
|
||||
@@ -140,7 +141,7 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
||||
fill
|
||||
className="object-contain p-1"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="aspect-square rounded-md border bg-muted" />
|
||||
)}
|
||||
|
||||
+4
-3
@@ -186,7 +186,7 @@ export async function generateOgImage({
|
||||
>
|
||||
{characterCard ? (
|
||||
<div style={{ display: "flex", color: "#d8f4ee", fontSize: 28 }}>
|
||||
เกนชินไม่ใช่เกมมือถือไกด์
|
||||
ไกด์เกนชินไม่ใช่เกมมือถือ
|
||||
</div>
|
||||
) : null}
|
||||
<div style={{ display: "flex", fontFamily: characterCard ? "Genshin" : "Kanit", fontSize: 64, marginTop: characterCard ? "8px" : 0 }}>
|
||||
@@ -248,18 +248,19 @@ export async function generateOgImage({
|
||||
alignItems: "center",
|
||||
justifyContent: "space-around",
|
||||
color: "#17171c",
|
||||
textShadow: "none",
|
||||
padding: "6px",
|
||||
}}
|
||||
>
|
||||
{recommendedWeapon.overallPercent ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||
<div style={{ display: "flex", fontSize: 16 }}>{metric(recommendedWeapon.overallPercent)}</div>
|
||||
<div style={{ display: "flex", fontSize: 22, fontWeight: 700 }}>{metric(recommendedWeapon.overallPercent)}</div>
|
||||
<div style={{ display: "flex", fontSize: 8 }}>ทั้งทีม</div>
|
||||
</div>
|
||||
) : null}
|
||||
{recommendedWeapon.personalPercent ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||
<div style={{ display: "flex", fontSize: 16 }}>{metric(recommendedWeapon.personalPercent)}</div>
|
||||
<div style={{ display: "flex", fontSize: 22, fontWeight: 700 }}>{metric(recommendedWeapon.personalPercent)}</div>
|
||||
<div style={{ display: "flex", fontSize: 8 }}>ส่วนตัว</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user