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:
Kunthawat Greethong
2026-02-26 07:18:18 +07:00
parent b4b344e6ae
commit 90917b85d0
2 changed files with 159 additions and 26 deletions

View File

@@ -1,51 +1,97 @@
import { notFound } from 'next/navigation';
import Image from 'next/image';
import Link from 'next/link';
import { productCategories } from '@/data/site-config';
import { productCategories, portfolioProjects } from '@/data/site-config';
interface Props {
params: { slug: string[] };
}
// Generate all possible paths from product categories
// Generate all possible paths from product categories and portfolio projects
export async function generateStaticParams() {
const paths: { slug: string[] }[] = [];
// Add product category paths
productCategories.forEach((product) => {
// 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 });
});
return paths;
}
function findProductBySlug(slug: string[]) {
const fullPath = '/' + slug.join('/');
return productCategories.find((p) => p.href === fullPath);
type ContentType = 'product' | 'portfolio';
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) {
const product = findProductBySlug(params.slug);
const content = findContentBySlug(params.slug);
if (!product) {
if (!content) {
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 {
title: `${product.name} - ${product.nameEn}`,
description: product.description,
keywords: product.keywords,
title: data.name,
description: data.description,
};
}
export default function ProductDetailPage({ params }: Props) {
const product = findProductBySlug(params.slug);
export default function DynamicPage({ params }: Props) {
const content = findContentBySlug(params.slug);
if (!product) {
if (!content) {
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
const relatedProducts = productCategories
.filter((p) => p.slug === product.slug && p.id !== product.id)
@@ -110,18 +156,6 @@ export default function ProductDetailPage({ params }: Props) {
</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 */}
{relatedProducts.length > 0 && (
<div className="mt-16">
@@ -160,3 +194,94 @@ export default function ProductDetailPage({ params }: Props) {
</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>
);
}

View File

@@ -64,6 +64,14 @@ export interface PortfolioItem {
description: string;
}
export interface PortfolioProject {
id: string;
name: string;
href: string;
image: string;
description: string;
}
// Contact Form Types
export interface ContactFormData {
name: string;