feat(overview) : add YouTube video section
CI / Verify (push) Successful in 1m4s
CI / Build immutable images and deploy (push) Successful in 1m54s

This commit is contained in:
2026-09-03 12:06:36 +07:00 Unverified
parent dd672e5331
commit cfc6f0c146
11 changed files with 3585 additions and 100 deletions
+133 -91
View File
@@ -3,6 +3,7 @@ import Link from "next/link";
import { ArrowUpRightIcon, ChevronLeftIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
@@ -33,6 +34,7 @@ import {
talentRelationSymbol,
talentTypeLabel,
} from "@/lib/guides/talents";
import { parseYouTubeUrl } from "@/lib/guides/youtube";
import { cn } from "@/lib/utils";
function hasSection(guide: PublicGuide, slug: string): boolean {
@@ -112,54 +114,51 @@ export function StructuredGuideShell({
</p>
</div>
</div>
<ScrollableNav
ariaLabel={`เมนูคู่มือ ${guide.name}`}
className="mx-auto min-h-11 w-full max-w-7xl rounded-xl border bg-card p-2"
>
<Link
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",
)}
prefetch
{section ? (
<ScrollableNav
ariaLabel={`เมนูคู่มือ ${guide.name}`}
className="mx-auto min-h-11 w-full max-w-7xl rounded-xl border bg-card p-2"
>
<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(
"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",
)}
href={`/${guide.slug}`}
className="flex min-h-8 shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 text-sm text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
prefetch
>
{navigationIconPath(item.slug) ? (
<Image
src={navigationIconPath(item.slug)!}
alt=""
width={20}
height={20}
className="size-5 object-contain"
/>
) : null}
{item.title}
<Image
src={navigationIconPath("overview")!}
alt=""
width={20}
height={20}
className="size-5 object-contain"
/>
Overview
</Link>
))}
</ScrollableNav>
{sections.map((item) => (
<Link
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
>
{navigationIconPath(item.slug) ? (
<Image
src={navigationIconPath(item.slug)!}
alt=""
width={20}
height={20}
className="size-5 object-contain"
/>
) : null}
{item.title}
</Link>
))}
</ScrollableNav>
) : null}
</div>
<main className="mx-auto w-full max-w-7xl p-4 sm:p-6 lg:p-10">
{section ? (
@@ -179,6 +178,10 @@ function GuideOverview({
guide: PublicGuide;
sections: ReturnType<typeof publicSections>;
}) {
const youtubeVideo = guide.youtubeUrl
? parseYouTubeUrl(guide.youtubeUrl)
: null;
return (
<div className="flex flex-col gap-8">
<div className="mx-auto flex max-w-3xl flex-col gap-3 text-center">
@@ -186,55 +189,94 @@ 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="relative mx-auto aspect-square w-full max-w-3xl overflow-hidden rounded-xl border bg-muted">
<Image
src={guide.coverUrl}
alt={`ไกด์ ${guide.name}`}
fill
priority
className="object-cover"
/>
</div>
{sections.length ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{sections.map((item) => (
<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>
<CardDescription>เปิดดูคำแนะนำฉบับเต็ม</CardDescription>
<CardAction>
<ArrowUpRightIcon
aria-hidden="true"
className="size-5 text-primary"
/>
</CardAction>
</CardHeader>
</Card>
</Link>
))}
<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">
<Image
src={guide.coverUrl}
alt={`ไกด์ ${guide.name}`}
fill
priority
className="object-cover"
/>
</div>
) : (
<Card>
<CardHeader>
<CardTitle>กำลังจัดทำเนื้อหา</CardTitle>
<CardDescription>
กลับมาตรวจสอบอีกครั้งหลังผู้ดูแลเพิ่มข้อมูล
</CardDescription>
</CardHeader>
</Card>
)}
{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>
{youtubeVideo ? (
<section className="flex flex-col items-center gap-3">
<div className="aspect-video w-full overflow-hidden rounded-xl border bg-muted">
<iframe
src={youtubeVideo.embedUrl}
title={`วิดีโอไกด์ ${guide.name}`}
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
className="size-full"
/>
</div>
<Button
variant="outline"
render={
<a
href={youtubeVideo.watchUrl}
target="_blank"
rel="noopener noreferrer"
/>
}
nativeButton={false}
>
ดูวิดีโอต้นฉบับบน YouTube
<ArrowUpRightIcon data-icon="inline-end" />
</Button>
</section>
) : null}
</div>
);
}