Add portfolio projects to catch-all route
- Updated [...slug] to handle both product categories and portfolio projects - Added 15 portfolio project pages - Added PortfolioProject type to types/index.ts - Build now generates 64 static pages (38 products + 15 portfolio + 3 blog + 8 main)
This commit is contained in:
@@ -1,51 +1,97 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { productCategories } from '@/data/site-config';
|
import { productCategories, portfolioProjects } from '@/data/site-config';
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: { slug: string[] };
|
params: { slug: string[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate all possible paths from product categories
|
// Generate all possible paths from product categories and portfolio projects
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const paths: { slug: string[] }[] = [];
|
const paths: { slug: string[] }[] = [];
|
||||||
|
|
||||||
|
// Add product category paths
|
||||||
productCategories.forEach((product) => {
|
productCategories.forEach((product) => {
|
||||||
// Remove leading slash and split the href
|
// Remove leading slash and split the href
|
||||||
const pathParts = product.href.replace(/^\//, '').split('/');
|
const pathParts = product.href.replace(/^\//, '').replace(/\/$/, '').split('/');
|
||||||
|
paths.push({ slug: pathParts });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add portfolio project paths
|
||||||
|
portfolioProjects.forEach((project) => {
|
||||||
|
const pathParts = project.href.replace(/^\//, '').replace(/\/$/, '').split('/');
|
||||||
paths.push({ slug: pathParts });
|
paths.push({ slug: pathParts });
|
||||||
});
|
});
|
||||||
|
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findProductBySlug(slug: string[]) {
|
type ContentType = 'product' | 'portfolio';
|
||||||
const fullPath = '/' + slug.join('/');
|
|
||||||
return productCategories.find((p) => p.href === fullPath);
|
function findContentBySlug(slug: string[]): { type: ContentType; data: typeof productCategories[0] | typeof portfolioProjects[0] } | null {
|
||||||
|
const fullPath = '/' + slug.join('/') + '/';
|
||||||
|
|
||||||
|
// Check products first
|
||||||
|
const product = productCategories.find((p) => p.href === fullPath);
|
||||||
|
if (product) {
|
||||||
|
return { type: 'product', data: product };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check portfolio projects
|
||||||
|
const portfolio = portfolioProjects.find((p) => p.href === fullPath);
|
||||||
|
if (portfolio) {
|
||||||
|
return { type: 'portfolio', data: portfolio };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params }: Props) {
|
export async function generateMetadata({ params }: Props) {
|
||||||
const product = findProductBySlug(params.slug);
|
const content = findContentBySlug(params.slug);
|
||||||
|
|
||||||
if (!product) {
|
if (!content) {
|
||||||
return { title: 'ไม่พบหน้า' };
|
return { title: 'ไม่พบหน้า' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { type, data } = content;
|
||||||
|
|
||||||
|
if (type === 'product') {
|
||||||
|
const product = data as typeof productCategories[0];
|
||||||
|
return {
|
||||||
|
title: `${product.name} - ${product.nameEn}`,
|
||||||
|
description: product.description,
|
||||||
|
keywords: product.keywords,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${product.name} - ${product.nameEn}`,
|
title: data.name,
|
||||||
description: product.description,
|
description: data.description,
|
||||||
keywords: product.keywords,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductDetailPage({ params }: Props) {
|
export default function DynamicPage({ params }: Props) {
|
||||||
const product = findProductBySlug(params.slug);
|
const content = findContentBySlug(params.slug);
|
||||||
|
|
||||||
if (!product) {
|
if (!content) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { type, data } = content;
|
||||||
|
|
||||||
|
// Render portfolio project page
|
||||||
|
if (type === 'portfolio') {
|
||||||
|
return <PortfolioPage project={data as typeof portfolioProjects[0]} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render product page
|
||||||
|
return <ProductPage product={data as typeof productCategories[0]} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product Page Component
|
||||||
|
function ProductPage({ product }: { product: typeof productCategories[0] }) {
|
||||||
// Find related products in same category
|
// Find related products in same category
|
||||||
const relatedProducts = productCategories
|
const relatedProducts = productCategories
|
||||||
.filter((p) => p.slug === product.slug && p.id !== product.id)
|
.filter((p) => p.slug === product.slug && p.id !== product.id)
|
||||||
@@ -110,18 +156,6 @@ export default function ProductDetailPage({ params }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SEO Content */}
|
|
||||||
{product.seoContent && (
|
|
||||||
<div className="mt-12">
|
|
||||||
<div className="bg-white rounded-xl p-8 shadow-card">
|
|
||||||
<div
|
|
||||||
className="prose prose-lg max-w-none prose-headings:font-bold prose-headings:text-secondary-900 prose-p:text-secondary-600 prose-a:text-primary-600 prose-strong:text-secondary-900 prose-ul:text-secondary-600 prose-li:text-secondary-600"
|
|
||||||
dangerouslySetInnerHTML={{ __html: product.seoContent }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Related Products */}
|
{/* Related Products */}
|
||||||
{relatedProducts.length > 0 && (
|
{relatedProducts.length > 0 && (
|
||||||
<div className="mt-16">
|
<div className="mt-16">
|
||||||
@@ -160,3 +194,94 @@ export default function ProductDetailPage({ params }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Portfolio Page Component
|
||||||
|
function PortfolioPage({ project }: { project: typeof portfolioProjects[0] }) {
|
||||||
|
return (
|
||||||
|
<div className="pt-32 pb-16">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<nav className="mb-6">
|
||||||
|
<ol className="flex items-center gap-2 text-sm">
|
||||||
|
<li>
|
||||||
|
<Link href="/" className="text-secondary-500 hover:text-primary-600">
|
||||||
|
หน้าแรก
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className="text-secondary-400">/</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/portfolio" className="text-secondary-500 hover:text-primary-600">
|
||||||
|
ผลงาน
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className="text-secondary-400">/</li>
|
||||||
|
<li className="text-primary-600 font-medium">{project.name}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||||
|
{/* Project Image */}
|
||||||
|
<div className="relative aspect-video bg-secondary-100 rounded-xl overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={project.image}
|
||||||
|
alt={project.name}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Project Info */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold text-secondary-900 mb-4">
|
||||||
|
{project.name}
|
||||||
|
</h1>
|
||||||
|
<p className="text-secondary-600 text-lg mb-6">
|
||||||
|
{project.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
<Link href="/contact-us" className="btn-primary">
|
||||||
|
ติดต่อเรา
|
||||||
|
</Link>
|
||||||
|
<Link href="/portfolio" className="btn-outline">
|
||||||
|
ดูผลงานอื่นๆ
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Other Projects */}
|
||||||
|
<div className="mt-16">
|
||||||
|
<h2 className="text-2xl font-bold text-secondary-900 mb-6">
|
||||||
|
ผลงานอื่นๆ
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
{portfolioProjects.filter(p => p.id !== project.id).slice(0, 4).map((other) => (
|
||||||
|
<Link
|
||||||
|
key={other.id}
|
||||||
|
href={other.href}
|
||||||
|
className="card group"
|
||||||
|
>
|
||||||
|
<div className="relative aspect-video bg-secondary-100">
|
||||||
|
<Image
|
||||||
|
src={other.image}
|
||||||
|
alt={other.name}
|
||||||
|
fill
|
||||||
|
className="object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<h3 className="text-lg font-bold text-secondary-900 group-hover:text-primary-600 transition-colors">
|
||||||
|
{other.name}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,6 +64,14 @@ export interface PortfolioItem {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PortfolioProject {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
image: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Contact Form Types
|
// Contact Form Types
|
||||||
export interface ContactFormData {
|
export interface ContactFormData {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user