feat : update
This commit is contained in:
@@ -7,6 +7,7 @@ import type {
|
||||
CatalogAscensionGroup,
|
||||
CatalogBaseStat,
|
||||
} from "@/lib/catalog/lunaris";
|
||||
import { accentGradient } from "@/lib/overview-image";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type PosterMaterial = {
|
||||
@@ -37,6 +38,14 @@ function statLabel(key: string): string {
|
||||
return statLabels[key.toLowerCase()] ?? key;
|
||||
}
|
||||
|
||||
function statIconName(key: string): string | null {
|
||||
const normalized = key.toLowerCase().replace(/[^a-z]/gu, "");
|
||||
if (["hp", "atk", "def", "em", "er"].includes(normalized)) return normalized;
|
||||
if (["critrate", "crit", "criticalrate"].includes(normalized)) return "critrate";
|
||||
if (["critdmg", "critdamage", "criticaldamage"].includes(normalized)) return "critdmg";
|
||||
return null;
|
||||
}
|
||||
|
||||
function statValue(key: string, value: number | string): string {
|
||||
const formatted = typeof value === "number"
|
||||
? value.toLocaleString("en-US", { maximumFractionDigits: 2 })
|
||||
@@ -67,11 +76,10 @@ export function MaterialPoster({
|
||||
onArtworkMove?: (offset: { x: number; y: number }) => void;
|
||||
className?: string;
|
||||
artworkScale: number;
|
||||
bleeding?: Array<{ id: string; imageUrl: string | null; amount: number; scale: number; blendMode: PosterBlendMode }>;
|
||||
bleeding?: Array<{ id: string; imageUrl: string | null; amount: number; blendMode: PosterBlendMode }>;
|
||||
}) {
|
||||
const dragStart = useRef<{ pointerId: number; x: number; y: number; offsetX: number; offsetY: number } | null>(null);
|
||||
const preferredStats = ["hp", "atk", "def", "em"];
|
||||
const stats = preferredStats.map((key) => character.baseStats.find((stat) => stat.key.toLowerCase() === key));
|
||||
const stats = character.baseStats.slice(0, 4);
|
||||
const materials = [...character.ascension, ...character.leveling].flatMap((group) => group.items);
|
||||
const background = `linear-gradient(180deg, ${topColors[0]} 0%, ${topColors[1]} 22%, rgba(0,0,0,0) 42%, rgba(0,0,0,0) 62%, ${bottomColors[0]} 88%, ${bottomColors[1]} 100%)`;
|
||||
|
||||
@@ -143,49 +151,67 @@ export function MaterialPoster({
|
||||
className="object-contain object-center"
|
||||
priority={!interactive}
|
||||
/>
|
||||
{[...bleeding].reverse().map((layer) => (
|
||||
<Image
|
||||
key={layer.id}
|
||||
src={characterArtUrl}
|
||||
alt=""
|
||||
fill
|
||||
sizes="(min-width: 1024px) 800px, 100vw"
|
||||
className="object-contain object-center"
|
||||
style={{ opacity: Math.max(0, Math.min(100, layer.amount)) / 100, mixBlendMode: layer.blendMode }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
{bleeding.map((layer) => layer.imageUrl ? (
|
||||
<Image
|
||||
key={layer.id}
|
||||
src={layer.imageUrl}
|
||||
alt=""
|
||||
fill
|
||||
sizes="(min-width: 1024px) 800px, 100vw"
|
||||
className="object-contain object-center"
|
||||
style={{ opacity: Math.max(0, Math.min(100, layer.amount)) / 100, mixBlendMode: layer.blendMode, transform: `scale(${Math.max(25, Math.min(300, layer.scale)) / 100})` }}
|
||||
/>
|
||||
) : null)}
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-black/10 via-transparent to-black/80" />
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 top-0 h-[1.35cqw]"
|
||||
style={{ backgroundImage: accentGradient(...topColors) }}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-[1.35cqw]"
|
||||
style={{ backgroundImage: accentGradient(...bottomColors) }}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-x-[4%] top-[5%]" style={{ fontFamily: "Kanit Poster", fontWeight: 800, fontStyle: "italic", textShadow: "-6px 14px 0 rgba(0,0,0,0.59)" }}>
|
||||
<h2 className="text-[7cqw] leading-none">BASE STATS</h2>
|
||||
<div className="mt-[4%] flex w-[53%] flex-col gap-[2.5%]">
|
||||
<div className="absolute inset-x-[4%] top-[5%]" style={{ fontFamily: "Kanit Poster", fontWeight: 800, fontStyle: "italic" }}>
|
||||
<h2 className="text-[7cqw] leading-none" style={{ textShadow: "-6px 14px 0 rgba(0,0,0,0.59)" }}>BASE STATS</h2>
|
||||
<div className="mt-[4%] flex w-[53%] flex-col gap-[1.2cqw]">
|
||||
{Array.from({ length: 4 }, (_, index) => {
|
||||
const stat = stats[index];
|
||||
const iconName = stat ? statIconName(stat.key) : null;
|
||||
return (
|
||||
<div key={stat?.key ?? index} className="flex h-[7cqw] items-center justify-between rounded-[1.5cqw] bg-black/95 px-[4%] text-[3.2cqw] leading-none">
|
||||
<span>{stat ? statLabel(stat.key) : "—"}</span>
|
||||
<span>{stat ? statValue(stat.key, stat.value) : "—"}</span>
|
||||
<span className="flex min-w-0 items-center gap-[1cqw] whitespace-nowrap">
|
||||
{iconName ? (
|
||||
<Image
|
||||
src={`/icon/stats/${iconName}.webp`}
|
||||
alt=""
|
||||
width={64}
|
||||
height={64}
|
||||
className="size-[3.5cqw] shrink-0 object-contain brightness-0 invert"
|
||||
/>
|
||||
) : null}
|
||||
<span>{stat ? statLabel(stat.key) : "—"}</span>
|
||||
</span>
|
||||
<span className="whitespace-nowrap text-[2.7cqw]">{stat ? statValue(stat.key, stat.value) : "—"}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-[4%] top-[51%]" style={{ fontFamily: "Kanit Poster", fontWeight: 800, fontStyle: "italic", textShadow: "-6px 14px 0 rgba(0,0,0,0.59)" }}>
|
||||
<h2 className="text-[7cqw] leading-none">MATERIALS</h2>
|
||||
<div className="absolute inset-x-[4%] top-[49%]" style={{ fontFamily: "Kanit Poster", fontWeight: 800, fontStyle: "italic" }}>
|
||||
<h2 className="text-[7cqw] leading-none" style={{ textShadow: "-6px 14px 0 rgba(0,0,0,0.59)" }}>MATERIALS</h2>
|
||||
<p className="mt-[1%] text-[4.5cqw] leading-none">LV.90</p>
|
||||
<div className="mt-[5%] grid grid-cols-8 gap-[1.2%]">
|
||||
{materials.map((material, index) => (
|
||||
<div key={`${material.key}-${index}`} className="aspect-[0.65] overflow-hidden rounded-[1.3cqw] bg-black/95 ring-1 ring-white/10">
|
||||
<div key={`${material.key}-${index}`} className="aspect-[0.72] overflow-hidden rounded-[1.3cqw] bg-black/95 ring-1 ring-white/10">
|
||||
<div className="flex h-[68%] items-center justify-center rounded-br-[1.3cqw] bg-[#c1c1c1] p-[8%]">
|
||||
{material.imageUrl ? <Image src={material.imageUrl} alt="" width={96} height={96} className="size-full object-contain" /> : null}
|
||||
</div>
|
||||
<div className="flex h-[32%] items-center justify-center px-[4%] text-center text-[2.2cqw] leading-none">
|
||||
<span>×{material.amount.toLocaleString("en-US")}</span>
|
||||
<span className="whitespace-nowrap text-[1.8cqw]">×{material.amount.toLocaleString("en-US")}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -209,8 +209,8 @@ function GuideOverview({
|
||||
<h1 className="font-heading text-4xl font-bold">{guide.name}</h1>
|
||||
<p className="text-lg font-semibold">{guide.overview}</p>
|
||||
</div>
|
||||
<div className="grid items-start gap-4 lg:grid-cols-2">
|
||||
<div className="relative aspect-square w-full overflow-hidden rounded-xl border bg-muted">
|
||||
<div className="grid items-start gap-6 lg:grid-cols-2">
|
||||
<div className="order-1 relative aspect-square w-full overflow-hidden rounded-xl border bg-muted lg:col-start-1 lg:row-start-1">
|
||||
<Image
|
||||
src={guide.coverUrl}
|
||||
alt={`ไกด์ ${guide.name}`}
|
||||
@@ -219,59 +219,13 @@ function GuideOverview({
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
{sections.length ? (
|
||||
<nav
|
||||
aria-label={`เมนูคู่มือ ${guide.name}`}
|
||||
className="flex flex-col gap-3"
|
||||
>
|
||||
{sections.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={`/${guide.slug}/${item.slug}`}
|
||||
className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
prefetch
|
||||
>
|
||||
<Card size="sm" className="transition-colors hover:bg-muted/50">
|
||||
<CardHeader>
|
||||
{navigationIconPath(item.slug) ? (
|
||||
<Image
|
||||
src={navigationIconPath(item.slug)!}
|
||||
alt=""
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain"
|
||||
/>
|
||||
) : null}
|
||||
<CardTitle>
|
||||
{publicSectionTitle(item.slug, item.title)}
|
||||
</CardTitle>
|
||||
<CardDescription>เปิดดูคำแนะนำฉบับเต็ม</CardDescription>
|
||||
<CardAction>
|
||||
<ArrowUpRightIcon
|
||||
aria-hidden="true"
|
||||
className="size-5 text-primary"
|
||||
/>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>กำลังจัดทำเนื้อหา</CardTitle>
|
||||
<CardDescription>
|
||||
กลับมาตรวจสอบอีกครั้งหลังผู้ดูแลเพิ่มข้อมูล
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
{guide.posterArtUrl || youtubeVideo ? (
|
||||
<div className="grid items-start gap-6 lg:grid-cols-[minmax(0,1.25fr)_minmax(20rem,0.75fr)]">
|
||||
<div className="order-3 lg:col-start-2 lg:row-start-1">
|
||||
<GuideNavigation guide={guide} sections={sections} />
|
||||
</div>
|
||||
{guide.posterArtUrl || youtubeVideo ? (
|
||||
<>
|
||||
{guide.posterArtUrl ? (
|
||||
<section className="order-1 lg:order-2">
|
||||
<section className="order-2 flex flex-col items-center gap-3 lg:col-start-1 lg:row-start-2">
|
||||
<MaterialPoster
|
||||
character={guide.character}
|
||||
characterArtUrl={guide.posterArtUrl}
|
||||
@@ -282,10 +236,28 @@ function GuideOverview({
|
||||
bottomColors={guide.posterBottomColors}
|
||||
bleeding={guide.posterBleeding}
|
||||
/>
|
||||
<GuideImageExport
|
||||
width={1600}
|
||||
height={2000}
|
||||
format="png"
|
||||
fileName={`${guide.name}-materials.png`}
|
||||
captureClassName="p-0"
|
||||
>
|
||||
<MaterialPoster
|
||||
character={guide.character}
|
||||
characterArtUrl={guide.posterArtUrl}
|
||||
artworkOffsetX={guide.posterArtOffsetX}
|
||||
artworkOffsetY={guide.posterArtOffsetY}
|
||||
artworkScale={guide.posterArtScale}
|
||||
topColors={guide.posterTopColors}
|
||||
bottomColors={guide.posterBottomColors}
|
||||
bleeding={guide.posterBleeding}
|
||||
/>
|
||||
</GuideImageExport>
|
||||
</section>
|
||||
) : null}
|
||||
{youtubeVideo ? (
|
||||
<section className="order-2 flex flex-col items-center gap-3 lg:order-1">
|
||||
<section className="order-4 flex flex-col items-center gap-3 lg:col-start-2 lg:row-start-2">
|
||||
<div className="aspect-video w-full overflow-hidden rounded-xl border bg-muted">
|
||||
<iframe
|
||||
src={youtubeVideo.embedUrl}
|
||||
@@ -313,12 +285,60 @@ function GuideOverview({
|
||||
</Button>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GuideNavigation({
|
||||
guide,
|
||||
sections,
|
||||
}: {
|
||||
guide: PublicGuide;
|
||||
sections: ReturnType<typeof publicSections>;
|
||||
}) {
|
||||
return sections.length ? (
|
||||
<nav aria-label={`เมนูคู่มือ ${guide.name}`} className="flex flex-col gap-3">
|
||||
{sections.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={`/${guide.slug}/${item.slug}`}
|
||||
className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
prefetch
|
||||
>
|
||||
<Card size="sm" className="transition-colors hover:bg-muted/50">
|
||||
<CardHeader>
|
||||
{navigationIconPath(item.slug) ? (
|
||||
<Image
|
||||
src={navigationIconPath(item.slug)!}
|
||||
alt=""
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain"
|
||||
/>
|
||||
) : null}
|
||||
<CardTitle>{publicSectionTitle(item.slug, item.title)}</CardTitle>
|
||||
<CardDescription>เปิดดูคำแนะนำฉบับเต็ม</CardDescription>
|
||||
<CardAction>
|
||||
<ArrowUpRightIcon aria-hidden="true" className="size-5 text-primary" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>กำลังจัดทำเนื้อหา</CardTitle>
|
||||
<CardDescription>กลับมาตรวจสอบอีกครั้งหลังผู้ดูแลเพิ่มข้อมูล</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function GuideSection({
|
||||
guide,
|
||||
section,
|
||||
|
||||
Reference in New Issue
Block a user