Files
dealplustech-astroreal/src/layouts/BaseLayout.astro
hermes fe442790ab feat(homepage): rework homepage + align about-us + footer popular products
## homepage (index.astro)

- Drop '500+ รายการสินค้า' from stat badges and rebuild the trust-badges
  section to show only 15+ ปีประสบการณ์, 400+ ลูกค้าทั่วประเทศ, 98%
  ลูกค้าพึงพอใจ — bigger numbers (text-5xl/6xl), no icons, counter animation
- Add 'ทำไมเลือกเรา' section (4 cards: ส่งฟรี กทม./ปริมณฑล, Lead time 1-3
  วัน, ราคาโรงงาน, ทีมช่างแนะนำ) — no icons, primary/accent border hover
- Add 'หมวดสินค้า' section (8 tiles: 7 categories + 'สินค้าทั้งหมด')
  placed after 'สินค้าแนะนำ' — each tile is a real product photo with
  dark gradient overlay and a CTA link to /all-products?filter=<id>
- Reorder: Hero → ทำไมเลือกเรา → สินค้าแนะนำ → หมวดสินค้า → Stats →
  บทความล่าสุด → CTA

## all-products (filter from URL)

- Script now reads ?filter=<id> on load and applies it without rewriting
  the URL, then on click updates both the filter and the URL via
  history.replaceState so the back/forward buttons work

## footer (BaseLayout.astro)

- Replace productLinks with the curated 6 popular products: ไทยพีพีอาร์,
  เทอร์โมเบรค, กริลแอร์, หัวจ่ายแอร์ Ball Jet, ท่อ HDPE, ท่อ Syler

## about-us

- Stats: 10+/1000+/500+ replaced with 15+ / 400+ + counter animation,
  bigger numbers, rounded-3xl + shadow (matches home)
- Why Choose Us: rebuilt with the same 4 cards + same style as home's
  'ทำไมเลือกเรา' (no icons, pill header, larger h2 + subtitle)
2026-06-08 22:56:52 +07:00

754 lines
34 KiB
Plaintext

---
import '@/styles/global.css';
import Layout from './Layout.astro';
import JsonLd from '@/components/seo/JsonLd.astro';
const companyInfo = {
name: "ดีล พลัส เทค จำกัด",
phone: "090-555-1415",
email: "dealplustech@gmail.com",
line: "@JPPSELECTION",
address: "9/70 ซอยนครลุง 17 แขวงบางไผ่ เขตบางแค กรุงเทพมหานคร 10160",
hours: "จันทร์-ศุกร์ 08:00-18:00 เสาร์ 08:00-17:00"
};
interface Props {
title: string;
description?: string;
ogImage?: string;
canonical?: string;
ogType?: 'website' | 'article' | 'product';
publishedTime?: string;
author?: string;
robots?: string;
product?: { name: string; image: string; brand?: string; description?: string; sku?: string };
faq?: { question: string; answer: string }[];
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
/** HowTo schema for installation/selection steps.
* Provide a name, optional description, totalTime (ISO 8601 e.g. PT30M),
* and an array of step strings. */
howTo?: { name: string; description?: string; totalTime?: string; steps: string[] };
product?: {
name?: string;
description?: string;
image: string;
sku?: string;
brand?: string;
priceCurrency?: string;
price?: number;
availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
url?: string;
};
/**
* FAQ schema. Pass an array of {question, answer} pairs to render
* a FAQPage JSON-LD. Answer may be plain text or HTML.
*/
faq?: Array<{ question: string; answer: string }>;
/**
* Override the auto-generated breadcrumb. If omitted, BaseLayout
* builds a breadcrumb automatically from the categories tree.
*/
breadcrumb?: Array<{ name: string; url: string }>;
}
const {
title,
description = "ดีล พลัส เทค - ระบบน้ำคุณภาพสูง ราคาโรงงาน",
ogImage,
canonical,
ogType = 'website',
publishedTime,
author,
robots,
jsonLd,
product: productProp,
faq: faqProp,
breadcrumb: breadcrumbProp,
howTo: howToProp,
} = Astro.props;
// Auto-standardize title suffix: "Foo" -> "Foo | ดีล พลัส เทค"
// Skip when the title already ends with the brand name (case-insensitive).
const BRAND = "ดีล พลัส เทค";
const trimmed = title.trim();
const lowerTitle = trimmed.toLowerCase();
const lowerBrand = BRAND.toLowerCase();
const hasBrandSuffix = lowerTitle.endsWith(lowerBrand)
|| lowerTitle.endsWith(lowerBrand + ' - ' + lowerBrand)
|| lowerTitle === lowerBrand;
const fullTitle = hasBrandSuffix ? trimmed : `${trimmed} | ${BRAND}`;
// Organization JSON-LD (rendered on every page so Google can verify the
// publisher of the site). LocalBusiness subtype for Maps/3-pack SEO.
const siteUrl = 'https://dealplustech.com';
const organizationSchema = {
'@context': 'https://schema.org',
'@type': 'LocalBusiness',
'@id': `${siteUrl}/#organization`,
name: companyInfo.name,
url: siteUrl,
logo: `${siteUrl}/images/logo/dealplustech-logo.png`,
image: `${siteUrl}/images/og/default-og.jpg`,
description: 'ผู้นำเข้าและจัดจำหน่ายระบบน้ำคุณภาพสูง ราคาโรงงาน',
telephone: companyInfo.phone,
email: companyInfo.email,
priceRange: '฿฿',
address: {
'@type': 'PostalAddress',
streetAddress: '9/70 ซอยนครลุง 17',
addressLocality: 'แขวงบางไผ่',
addressRegion: 'เขตบางแค',
addressCountry: 'TH',
postalCode: '10160',
},
openingHoursSpecification: [
{
'@type': 'OpeningHoursSpecification',
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
opens: '08:00',
closes: '18:00',
},
{
'@type': 'OpeningHoursSpecification',
dayOfWeek: 'Saturday',
opens: '08:00',
closes: '17:00',
},
],
sameAs: [
`https://line.me/ti/p/~${companyInfo.line.replace('@', '')}`,
],
};
const categories = [
{
name: 'ท่อพีพีอาร์',
slug: '/ท่อ-ppr-thai-ppr',
subcategories: [
{ name: 'ไทยพีพีอาร์', slug: '/ท่อ-ppr-thai-ppr' },
{ name: 'ท่อ PPR ตราช้าง', slug: '/ท่อ-ppr-scg' },
{ name: 'ท่อ HDPE', slug: '/ท่อ-hdpe' },
{ name: 'ท่อ UPVC', slug: '/ท่อ-upvc' },
{ name: 'ท่อ Syler', slug: '/ท่อ-syler' },
{ name: 'ท่อ XYLENT', slug: '/ท่อ-xy-lent' },
]
},
{
name: 'เครื่องเชื่อมท่อ',
slug: '/เครื่องเชื่อม-hdpe',
subcategories: [
{ name: 'เครื่องเชื่อม HDPE', slug: '/เครื่องเชื่อม-hdpe' },
{ name: 'เครื่องเชื่อม PPR', slug: '/เครื่องเชื่อม-ppr' },
{ name: 'Pipe Coupling', slug: '/pipe-coupling' },
{ name: 'เม็ดกรู๊ฟ คับปลิ้ง', slug: '/เม็กกรู๊ฟ-คับปลิ้ง' },
]
},
{
name: 'ระบบน้ำ',
slug: '/water-pump',
subcategories: [
{ name: 'วาล์ว', slug: '/วาล์ว-valve' },
{ name: 'ปั๊มน้ำ', slug: '/water-pump' },
{ name: 'ระบบกรองน้ำ', slug: '/water-treatment' },
]
},
{
name: 'อุปกรณ์ปรับอากาศ',
slug: '/grilles',
subcategories: [
{ name: 'กริลแอร์', slug: '/grilles' },
{ name: 'DURGO วาล์วเติมอากาศ', slug: '/durgo-avvs' },
{ name: 'หัวจ่ายแอร์ Ball Jet', slug: '/หัวจ่าย-ball-jet' },
]
},
{
name: 'อุปกรณ์ดับเพลิง',
slug: '/ตู้ดับเพลิง',
subcategories: [
{ name: 'ตู้ดับเพลิง', slug: '/ตู้ดับเพลิง' },
{ name: 'Realflex', slug: '/realflex' },
]
},
{
name: 'ฉนวนหุ้มท่อ',
slug: '/armflex',
subcategories: [
{ name: 'Armaflex', slug: '/armflex' },
{ name: 'Aeroflex', slug: '/aeroflex' },
{ name: 'Maxflex', slug: '/maxflex' },
{ name: 'เทอร์โมเบรค', slug: '/เทอร์โมเบรค-thermobreak' },
]
},
{
name: 'ระบบรั้ว',
slug: '/รั้วเทวดา',
subcategories: [
{ name: 'รั้วเทวดา', slug: '/รั้วเทวดา' },
{ name: 'ระบบรั้วไวน์แมน', slug: '/ระบบรั้วไวน์แมน' },
]
},
];
// ---------------------------------------------------------------------------
// Schema builders
// ---------------------------------------------------------------------------
/**
* Build a BreadcrumbList from the current URL by walking the categories tree.
* Pages outside the product tree (e.g. /about-us, /contact-us) get a 1-item
* breadcrumb (Home). Pages in the product tree get: Home > Category > Page.
*/
function buildBreadcrumb() {
if (breadcrumbProp) return breadcrumbProp;
// Normalise pathname: URL-decode (Astro returns encoded in static build),
// then strip trailing slash so it matches the slugs in the categories
// tree (which are stored decoded and have no trailing slash).
const rawPath = Astro.url.pathname;
const decoded = (() => {
try { return decodeURIComponent(rawPath); } catch { return rawPath; }
})();
const path = decoded.replace(/\/+$/, '') || '/';
if (path === '/') return [];
// Find the subcategory whose slug matches the current path
for (const cat of categories) {
const sub = cat.subcategories.find(s => s.slug === path);
if (sub) {
return [
{ name: 'หน้าแรก', url: '/' },
{ name: cat.name, url: cat.slug },
{ name: sub.name, url: sub.slug },
];
}
}
// Page not in product tree → just Home
return [{ name: 'หน้าแรก', url: '/' }];
}
const breadcrumbItems = buildBreadcrumb();
const breadcrumbSchema = breadcrumbItems.length > 0 ? {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: breadcrumbItems.map((item, idx) => ({
'@type': 'ListItem',
position: idx + 1,
name: item.name,
item: `${siteUrl}${item.url}`,
})),
} : null;
/**
* Build a Product JSON-LD from the `product` prop. Uses page title/description
* as defaults so callers only need to pass `image`.
*/
const productSchema = productProp ? {
'@context': 'https://schema.org',
'@type': 'Product',
name: productProp.name ?? trimmed,
description: productProp.description ?? description,
image: /^https?:\/\//.test(productProp.image)
? productProp.image
: `${siteUrl}${productProp.image}`,
...(productProp.sku && { sku: productProp.sku }),
...(productProp.brand && {
brand: { '@type': 'Brand', name: productProp.brand },
}),
offers: {
'@type': 'Offer',
url: productProp.url ?? `${siteUrl}${Astro.url.pathname}`,
priceCurrency: productProp.priceCurrency ?? 'THB',
...(productProp.price !== undefined && { price: productProp.price }),
availability: `https://schema.org/${productProp.availability ?? 'InStock'}`,
seller: { '@id': `${siteUrl}/#organization` },
},
} : null;
/**
* Build a FAQPage JSON-LD from the `faq` prop. Strips HTML from answers
* for the schema text field (Google prefers plain text).
*/
const faqSchema = faqProp && faqProp.length > 0 ? {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqProp.map(item => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer.replace(/<[^>]+>/g, '').trim(),
},
})),
} : null;
// HowTo schema — installation/selection steps as ordered list
const howToSchema = howToProp && howToProp.steps.length > 0 ? {
'@context': 'https://schema.org',
'@type': 'HowTo',
name: howToProp.name,
...(howToProp.description && { description: howToProp.description }),
...(howToProp.totalTime && { totalTime: howToProp.totalTime }),
step: howToProp.steps.map((text, i) => ({
'@type': 'HowToStep',
position: i + 1,
name: `ขั้นตอนที่ ${i + 1}`,
text: text,
})),
} : null;
/**
* Collect all JSON-LD payloads to render on this page.
* Always includes the Organization schema. Plus: breadcrumb, product, faq,
* and any caller-supplied custom JSON-LD.
*/
const jsonLdPayloads: Record<string, unknown>[] = [organizationSchema];
if (breadcrumbSchema) jsonLdPayloads.push(breadcrumbSchema);
if (productSchema) jsonLdPayloads.push(productSchema);
if (faqSchema) jsonLdPayloads.push(faqSchema);
if (howToSchema) jsonLdPayloads.push(howToSchema);
if (jsonLd) {
if (Array.isArray(jsonLd)) {
jsonLdPayloads.push(...jsonLd);
} else {
jsonLdPayloads.push(jsonLd);
}
}
const productLinks = [
{ title: "ไทยพีพีอาร์", href: "/ท่อ-ppr-thai-ppr" },
{ title: "เทอร์โมเบรค", href: "/เทอร์โมเบรค-thermobreak" },
{ title: "กริลแอร์", href: "/grilles" },
{ title: "หัวจ่ายแอร์ Ball Jet", href: "/หัวจ่าย-ball-jet" },
{ title: "ท่อ HDPE", href: "/ท่อ-hdpe" },
{ title: "ท่อ Syler", href: "/ท่อ-syler" },
];
---
<Layout title={fullTitle} description={description} ogImage={ogImage} canonical={canonical} ogType={ogType} publishedTime={publishedTime} author={author} robots={robots}>
{jsonLdPayloads.map((data, idx) => (
<JsonLd data={data} id={idx === 0 ? 'organization-schema' : `schema-${idx}`} />
))}
<!-- Header with Scroll Effects -->
<header id="main-header" class="bg-white/80 backdrop-blur-md shadow-sm sticky top-0 z-50 transition-all duration-300">
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16 lg:h-20">
<a href="/" class="flex items-center group">
<img
src="/images/logo/dealplustech-logo.png"
alt="ดีล พลัส เทค"
class="h-10 lg:h-14 w-auto transition-transform duration-300 group-hover:scale-105"
/>
</a>
<div class="hidden lg:flex items-center gap-6">
<a href="/" class="nav-link text-neutral-600 hover:text-primary-600 font-medium relative">
หน้าแรก
<span class="nav-underline"></span>
</a>
<a href="/about-us" class="nav-link text-neutral-600 hover:text-primary-600 font-medium relative">
เกี่ยวกับเรา
<span class="nav-underline"></span>
</a>
<a href="/%E0%B8%9A%E0%B8%97%E0%B8%84%E0%B8%A7%E0%B8%B2%E0%B8%A1" class="nav-link text-neutral-600 hover:text-primary-600 font-medium relative">
บทความ
<span class="nav-underline"></span>
</a>
<div class="relative group">
<button class="nav-link flex items-center gap-1 text-neutral-600 hover:text-primary-600 font-medium">
<span>สินค้า</span>
<svg class="w-4 h-4 transition-transform duration-200 group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<!-- Mega Menu with Stagger Animation -->
<div class="absolute top-full left-1/2 -translate-x-1/2 mt-0 w-[650px] max-w-[95vw] bg-white/95 backdrop-blur-xl rounded-b-2xl shadow-2xl border border-neutral-100 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300 transform group-hover:translate-y-0 translate-y-4 z-50">
<div class="grid grid-cols-4 gap-5 p-6" data-animate-stagger>
{categories.map(cat => (
<div class="space-y-2 group/sub">
<span class="block text-sm font-bold text-primary-700 cursor-default hover:text-primary-600 transition-colors">
{cat.name}
</span>
<ul class="space-y-1">
{cat.subcategories.map(sub => (
<li>
<a
href={sub.slug}
class="block text-sm text-neutral-600 hover:text-primary-600 hover:translate-x-1 transition-all duration-200"
>
{sub.name}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div class="border-t border-neutral-100 px-6 py-4 bg-gradient-to-r from-primary-50 to-accent-50 rounded-b-2xl">
<a href="/all-products" class="inline-flex items-center gap-2 text-sm font-semibold text-primary-600 hover:text-primary-700 transition-colors group/btn">
ดูสินค้าทั้งหมด
<svg class="w-4 h-4 transition-transform duration-200 group-hover/btn:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
</div>
</div>
</div>
<a href="/portfolio" class="nav-link text-neutral-600 hover:text-primary-600 font-medium relative">
ผลงาน
<span class="nav-underline"></span>
</a>
<a
href="/contact-us"
class="bg-primary-600 hover:bg-primary-700 text-white py-2 px-5 rounded-lg font-medium transition-all duration-300 hover:shadow-lg hover:shadow-primary-600/25 hover:-translate-y-0.5 magnetic-btn"
>
ติดต่อเรา
</a>
</div>
<!-- Mobile Menu Button -->
<button id="mobile-menu-btn" class="lg:hidden p-2 rounded-lg hover:bg-neutral-100 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</nav>
<!-- Mobile Menu -->
<div id="mobile-menu" class="lg:hidden hidden bg-white border-t border-neutral-100">
<div class="px-4 py-6 space-y-4">
<a href="/" class="block text-neutral-600 hover:text-primary-600 font-medium py-2">หน้าแรก</a>
<a href="/about-us" class="block text-neutral-600 hover:text-primary-600 font-medium py-2">เกี่ยวกับเรา</a>
<a href="/%E0%B8%9A%E0%B8%97%E0%B8%84%E0%B8%A7%E0%B8%B2%E0%B8%A1" class="block text-neutral-600 hover:text-primary-600 font-medium py-2">บทความ</a>
<a href="/all-products" class="block text-neutral-600 hover:text-primary-600 font-medium py-2">สินค้าทั้งหมด</a>
<a href="/portfolio" class="block text-neutral-600 hover:text-primary-600 font-medium py-2">ผลงาน</a>
<a href="/contact-us" class="block bg-primary-600 text-white text-center py-3 rounded-lg font-medium mt-4">ติดต่อเรา</a>
</div>
</div>
</header>
<slot />
<!-- Footer -->
<footer class="text-neutral-800 bg-gradient-to-b from-white to-neutral-50" data-animate="fade-up">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-12">
<!-- Company Info -->
<div class="lg:col-span-1" data-animate="fade-left" data-animate-delay="0">
<img src="/images/logo/dealplustech-logo.png" alt="ดีล พลัส เทค" class="h-10 w-auto mb-4 transition-transform hover:scale-105 duration-300" />
<div class="space-y-3 text-sm text-neutral-600">
<p class="flex items-start gap-2 hover:text-primary-600 transition-colors">
<svg class="w-5 h-5 shrink-0 mt-0.5 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{companyInfo.address}</span>
</p>
<p class="flex items-center gap-2 hover:text-primary-600 transition-colors">
<svg class="w-5 h-5 shrink-0 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<a href={`tel:${companyInfo.phone}`}>{companyInfo.phone}</a>
</p>
<p class="flex items-center gap-2 hover:text-primary-600 transition-colors">
<svg class="w-5 h-5 shrink-0 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<a href={`mailto:${companyInfo.email}`}>{companyInfo.email}</a>
</p>
<p class="flex items-center gap-2 hover:text-primary-600 transition-colors">
<svg class="w-5 h-5 shrink-0 text-primary-600" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314"/>
</svg>
<span>LINE: {companyInfo.line}</span>
</p>
<p class="text-neutral-400 text-xs">{companyInfo.hours}</p>
</div>
</div>
<!-- Quick Links -->
<div data-animate="fade-up" data-animate-delay="100">
<h3 class="font-semibold text-lg mb-4 text-primary-700">ลิงก์ด่วน</h3>
<ul class="space-y-2 text-sm text-neutral-600">
<li><a href="/" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">หน้าแรก</a></li>
<li><a href="/about-us" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">เกี่ยวกับเรา</a></li>
<li><a href="/%E0%B8%9A%E0%B8%97%E0%B8%84%E0%B8%A7%E0%B8%B2%E0%B8%A1" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">บทความ</a></li>
<li><a href="/all-products" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">สินค้าทั้งหมด</a></li>
<li><a href="/portfolio" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">ผลงาน</a></li>
<li><a href="/contact-us" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">ติดต่อเรา</a></li>
</ul>
</div>
<!-- Products -->
<div data-animate="fade-up" data-animate-delay="200">
<h3 class="font-semibold text-lg mb-4 text-primary-700">สินค้ายอดนิยม</h3>
<ul class="space-y-2 text-sm text-neutral-600">
{productLinks.map(link => (
<li>
<a href={link.href} class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">{link.title}</a>
</li>
))}
</ul>
</div>
<!-- Contact CTA -->
<div data-animate="fade-right" data-animate-delay="300">
<h3 class="font-semibold text-lg mb-4 text-primary-700">ติดต่อเรา</h3>
<div class="space-y-3">
<a
href={`tel:${companyInfo.phone}`}
class="flex items-center justify-center gap-2 w-full bg-primary-600 hover:bg-primary-700 text-white py-3 px-4 rounded-xl font-medium transition-all hover:shadow-lg hover:shadow-primary-600/25 hover:-translate-y-0.5 magnetic-btn btn-shimmer"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
โทรเลย
</a>
<div class="flex flex-col items-center gap-2">
<img
src="/images/line-qr.svg"
alt="LINE QR Code"
class="w-28 h-28 rounded-xl bg-white p-2 shadow-lg hover:shadow-xl transition-shadow"
/>
<a
href="https://line.me/ti/p/~JPPSELECTION"
target="_blank"
rel="noopener"
class="flex items-center justify-center gap-2 w-full bg-accent-500 hover:bg-accent-600 text-white py-3 px-4 rounded-xl font-medium transition-all hover:shadow-lg hover:shadow-accent-500/25 hover:-translate-y-0.5 magnetic-btn btn-shimmer"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314"/>
</svg>
แอดไลน์
</a>
</div>
</div>
</div>
</div>
<!-- Bottom bar -->
<div class="mt-12 pt-8 border-t border-neutral-200">
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-neutral-500">
<p>&copy; {new Date().getFullYear()} {companyInfo.name}. สงวนลิขสิทธิ์.</p>
<div class="flex gap-6">
<a href="/privacy-policy" class="hover:text-primary-600 transition-colors">นโยบายความเป็นส่วนตัว</a>
<a href="/terms-and-conditions" class="hover:text-primary-600 transition-colors">ข้อกำหนดการใช้งาน</a>
</div>
</div>
</div>
</div>
</footer>
<!-- Animation Initialization Scripts -->
<script>
// ============================================
// SCROLL-TRIGGERED ANIMATIONS (Intersection Observer)
// ============================================
document.addEventListener('DOMContentLoaded', () => {
const animatedElements = document.querySelectorAll('[data-animate]');
const staggerElements = document.querySelectorAll('[data-animate-stagger]');
const observerOptions = {
root: null,
rootMargin: '0px 0px -50px 0px',
threshold: 0.1
};
const animateOnScroll = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
// Unobserve after animation to save resources
animateOnScroll.unobserve(entry.target);
}
});
}, observerOptions);
// Observe regular animated elements
animatedElements.forEach(el => {
animateOnScroll.observe(el);
});
// Observe stagger containers
const staggerObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
staggerObserver.unobserve(entry.target);
}
});
}, observerOptions);
staggerElements.forEach(el => {
staggerObserver.observe(el);
});
});
// ============================================
// HEADER SCROLL EFFECTS
// ============================================
const header = document.getElementById('main-header');
let lastScroll = 0;
window.addEventListener('scroll', () => {
const currentScroll = window.pageYOffset;
// Add shadow on scroll
if (currentScroll > 10) {
header?.classList.add('shadow-md');
header?.classList.remove('shadow-sm');
} else {
header?.classList.remove('shadow-md');
header?.classList.add('shadow-sm');
}
// Hide/show header on scroll direction
if (currentScroll > lastScroll && currentScroll > 100) {
header?.classList.add('-translate-y-full');
} else {
header?.classList.remove('-translate-y-full');
}
lastScroll = currentScroll;
});
// ============================================
// MOBILE MENU TOGGLE
// ============================================
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
mobileMenuBtn?.addEventListener('click', () => {
mobileMenu?.classList.toggle('hidden');
});
// ============================================
// MAGNETIC BUTTON EFFECT
// ============================================
const magneticBtns = document.querySelectorAll('.magnetic-btn');
magneticBtns.forEach(btn => {
btn.addEventListener('mousemove', (e: MouseEvent) => {
const rect = (btn as HTMLElement).getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
(btn as HTMLElement).style.transform = `translate(${x * 0.1}px, ${y * 0.1}px)`;
});
btn.addEventListener('mouseleave', () => {
(btn as HTMLElement).style.transform = 'translate(0, 0)';
});
});
// ============================================
// RIPPLE EFFECT
// ============================================
const rippleBtns = document.querySelectorAll('.ripple');
rippleBtns.forEach(btn => {
btn.addEventListener('click', (e: MouseEvent) => {
const target = e.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const ripple = document.createElement('span');
ripple.className = 'ripple-effect';
ripple.style.left = `${x}px`;
ripple.style.top = `${y}px`;
target.appendChild(ripple);
setTimeout(() => ripple.remove(), 600);
});
});
// ============================================
// 3D TILT EFFECT FOR CARDS
// ============================================
const tiltCards = document.querySelectorAll('.tilt-card');
tiltCards.forEach(card => {
card.addEventListener('mousemove', (e: MouseEvent) => {
const target = card as HTMLElement;
const rect = target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateX = (y - centerY) / 10;
const rotateY = (centerX - x) / 10;
target.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
});
card.addEventListener('mouseleave', () => {
(card as HTMLElement).style.transform = 'perspective(1000px) rotateX(0) rotateY(0)';
});
});
// ============================================
// COUNTER ANIMATION
// ============================================
const counters = document.querySelectorAll('[data-counter]');
const counterObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const target = entry.target as HTMLElement;
const end = parseInt(target.getAttribute('data-counter') || '0');
const duration = 2000;
const step = end / (duration / 16);
let current = 0;
const updateCounter = () => {
current += step;
if (current < end) {
target.textContent = Math.floor(current).toString();
requestAnimationFrame(updateCounter);
} else {
target.textContent = end.toString();
}
};
updateCounter();
counterObserver.unobserve(target);
}
});
}, { threshold: 0.5 });
counters.forEach(counter => {
counterObserver.observe(counter);
});
// ============================================
// SMOOTH SCROLL FOR ANCHOR LINKS
// ============================================
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', (e) => {
e.preventDefault();
const target = document.querySelector((anchor as HTMLAnchorElement).getAttribute('href') || '');
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
</script>
<script>
// Price button auto-detection
document.addEventListener('DOMContentLoaded', () => {
const priceButton = document.querySelector('[data-price-button]');
if (!priceButton) return;
const hasPricelist = document.getElementById('pricelist') !== null;
if (hasPricelist) {
priceButton.classList.remove('hidden');
priceButton.removeAttribute('disabled');
} else {
priceButton.classList.add('hidden');
}
});
</script>
</Layout>