feat: Migrate Astro to root - Replace Next.js completely
- Removed Next.js project (src, app, components, etc.) - Moved Astro from dealplustech-astro/ to root - Updated Dockerfile for Astro root deployment - All PDPA compliance features preserved: * Cookie consent banner * Consent logging API with SQLite * Admin dashboard (/admin/consent-logs) * Privacy Policy (Thai, PDPA-compliant) * Terms & Conditions (Thai) - 15 pages: homepage, 6 products, 3 blog posts, legal pages, admin - Build: 660ms, all pages generated successfully
This commit is contained in:
51
src/components/BlogCard.astro
Normal file
51
src/components/BlogCard.astro
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
|
||||
interface Props {
|
||||
post: CollectionEntry<'blog'>;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { title, excerpt, date, author, category, categories, image, featuredImage } = post.data;
|
||||
|
||||
// Support both 'category' and 'categories' field names
|
||||
const postCategory = category || (Array.isArray(categories) ? categories[0] : 'ทั่วไป');
|
||||
// Support both 'image' and 'featuredImage' field names
|
||||
const postImage = image || featuredImage || '/images/2021/03/ppr-pipe_000C.jpg';
|
||||
---
|
||||
|
||||
<a href={`/blog/${post.slug}`} class="card group">
|
||||
<div class="relative aspect-video bg-secondary-100 overflow-hidden">
|
||||
<img
|
||||
src={postImage}
|
||||
alt={title}
|
||||
class="object-cover w-full h-48 group-hover:scale-105 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div class="absolute top-4 left-4">
|
||||
<span class="industrial-badge">{postCategory}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<time class="text-sm text-secondary-500">
|
||||
{new Date(date).toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}
|
||||
</time>
|
||||
|
||||
<h3 class="mt-2 text-xl font-bold text-secondary-900 group-hover:text-primary-600 transition-colors line-clamp-2">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{excerpt && (
|
||||
<p class="mt-3 text-secondary-600 text-sm line-clamp-3">
|
||||
{excerpt}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div class="mt-4 flex items-center text-primary-600 font-medium">
|
||||
<span>อ่านต่อ</span>
|
||||
<svg class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
247
src/components/CookieConsentBanner.astro
Normal file
247
src/components/CookieConsentBanner.astro
Normal file
@@ -0,0 +1,247 @@
|
||||
---
|
||||
// Cookie Consent Banner Component - PDPA Compliant
|
||||
// Displays on first visit, allows users to accept/reject cookie categories
|
||||
---
|
||||
|
||||
<div
|
||||
id="cookie-consent-banner"
|
||||
class="fixed bottom-0 left-0 right-0 z-50 bg-white border-t-2 border-primary-600 shadow-2xl p-6 md:p-8 transform translate-y-full transition-transform duration-300 ease-in-out"
|
||||
role="region"
|
||||
aria-labelledby="consent-title"
|
||||
aria-describedby="consent-description"
|
||||
style="display: none;"
|
||||
>
|
||||
<div class="container mx-auto px-4 max-w-7xl">
|
||||
<div class="flex flex-col lg:flex-row gap-6 items-start lg:items-center justify-between">
|
||||
<!-- Consent Content -->
|
||||
<div class="flex-1">
|
||||
<h2 id="consent-title" class="text-xl md:text-2xl font-bold text-secondary-900 mb-3">
|
||||
เรายึดถือความเป็นส่วนตัวของคุณ
|
||||
</h2>
|
||||
<p id="consent-description" class="text-base md:text-lg text-secondary-700 leading-relaxed mb-4">
|
||||
เราใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งาน วิเคราะห์การเข้าใช้งาน และแสดงเนื้อหาที่ตรงใจคุณ
|
||||
คุณสามารถเลือกยอมรับหรือปฏิเสธคุกกี้ที่ไม่จำเป็นได้
|
||||
</p>
|
||||
|
||||
<!-- Cookie Categories -->
|
||||
<div class="space-y-3 mt-4">
|
||||
<!-- Essential Cookies (Always On) -->
|
||||
<div class="flex items-center gap-3 bg-secondary-50 p-3 rounded-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="consent-essential"
|
||||
checked
|
||||
disabled
|
||||
class="w-5 h-5 accent-primary-600 rounded"
|
||||
/>
|
||||
<label for="consent-essential" class="flex-1 cursor-pointer">
|
||||
<span class="font-semibold text-secondary-900">คุกกี้จำเป็น</span>
|
||||
<span class="text-sm text-secondary-600 block">ใช้สำหรับการทำงานของเว็บไซต์ ไม่สามารถปิดได้</span>
|
||||
</label>
|
||||
<span class="text-xs px-2 py-1 bg-primary-600 text-white rounded">จำเป็น</span>
|
||||
</div>
|
||||
|
||||
<!-- Analytics Cookies -->
|
||||
<div class="flex items-center gap-3 bg-secondary-50 p-3 rounded-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="consent-analytics"
|
||||
class="w-5 h-5 accent-primary-600 rounded consent-checkbox"
|
||||
/>
|
||||
<label for="consent-analytics" class="flex-1 cursor-pointer">
|
||||
<span class="font-semibold text-secondary-900">คุกกี้วิเคราะห์ข้อมูล</span>
|
||||
<span class="text-sm text-secondary-600 block">ช่วยให้เราเข้าใจพฤติกรรมการใช้งาน</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Marketing Cookies -->
|
||||
<div class="flex items-center gap-3 bg-secondary-50 p-3 rounded-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="consent-marketing"
|
||||
class="w-5 h-5 accent-primary-600 rounded consent-checkbox"
|
||||
/>
|
||||
<label for="consent-marketing" class="flex-1 cursor-pointer">
|
||||
<span class="font-semibold text-secondary-900">คุกกี้การตลาด</span>
|
||||
<span class="text-sm text-secondary-600 block">ใช้สำหรับแสดงโฆษณาที่เกี่ยวข้อง</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 flex-shrink-0">
|
||||
<button
|
||||
id="consent-reject"
|
||||
type="button"
|
||||
class="bg-secondary-800 hover:bg-secondary-900 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
|
||||
>
|
||||
ปฏิเสธทั้งหมด
|
||||
</button>
|
||||
<button
|
||||
id="consent-accept"
|
||||
type="button"
|
||||
class="bg-primary-600 hover:bg-primary-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
|
||||
>
|
||||
ยอมรับทั้งหมด
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Policy Link -->
|
||||
<div class="mt-6 pt-6 border-t border-secondary-200 text-center">
|
||||
<p class="text-sm text-secondary-600">
|
||||
การใช้งานคุกกี้ของเราเป็นไปตาม
|
||||
<a href="/privacy-policy/" class="text-primary-600 hover:underline font-medium">นโยบายความเป็นส่วนตัว</a>
|
||||
และ
|
||||
<a href="/terms-and-conditions/" class="text-primary-600 hover:underline font-medium">ข้อกำหนดการใช้งาน</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
interface ConsentPreferences {
|
||||
essential: boolean;
|
||||
analytics: boolean;
|
||||
marketing: boolean;
|
||||
timestamp: string;
|
||||
policyVersion: string;
|
||||
}
|
||||
|
||||
const POLICY_VERSION = '1.0.0';
|
||||
const CONSENT_STORAGE_KEY = 'consent-preferences';
|
||||
const CONSENT_LOG_API = '/api/consent';
|
||||
|
||||
function generateSessionId(): string {
|
||||
return 'ses_' + Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
function getStoredConsent(): ConsentPreferences | null {
|
||||
const stored = localStorage.getItem(CONSENT_STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
}
|
||||
|
||||
async function saveConsent(consent: ConsentPreferences) {
|
||||
localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(consent));
|
||||
|
||||
// Log consent to database (PDPA requirement)
|
||||
try {
|
||||
let sessionId = sessionStorage.getItem('consent_session_id');
|
||||
if (!sessionId) {
|
||||
sessionId = generateSessionId();
|
||||
sessionStorage.setItem('consent_session_id', sessionId);
|
||||
}
|
||||
|
||||
await fetch(CONSENT_LOG_API, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
consent,
|
||||
policyVersion: POLICY_VERSION,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to log consent:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function showBanner() {
|
||||
const banner = document.getElementById('cookie-consent-banner') as HTMLElement;
|
||||
if (banner) {
|
||||
banner.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
banner.classList.remove('translate-y-full');
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
function hideBanner() {
|
||||
const banner = document.getElementById('cookie-consent-banner') as HTMLElement;
|
||||
if (banner) {
|
||||
banner.classList.add('translate-y-full');
|
||||
setTimeout(() => {
|
||||
banner.style.display = 'none';
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
function initConsent() {
|
||||
const stored = getStoredConsent();
|
||||
|
||||
if (stored) {
|
||||
// Consent already given - restore checkboxes
|
||||
const analyticsCheckbox = document.getElementById('consent-analytics') as HTMLInputElement;
|
||||
const marketingCheckbox = document.getElementById('consent-marketing') as HTMLInputElement;
|
||||
|
||||
if (analyticsCheckbox) analyticsCheckbox.checked = stored.analytics;
|
||||
if (marketingCheckbox) marketingCheckbox.checked = stored.marketing;
|
||||
|
||||
// Load Umami if consented
|
||||
if (stored.analytics && import.meta.env.PUBLIC_UMAMI_WEBSITE_ID) {
|
||||
const script = document.createElement('script');
|
||||
script.defer = true;
|
||||
script.src = 'https://analytics.moreminimore.com/script.js';
|
||||
script.setAttribute('data-website-id', import.meta.env.PUBLIC_UMAMI_WEBSITE_ID);
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// First visit - show banner after delay
|
||||
setTimeout(showBanner, 500);
|
||||
}
|
||||
|
||||
function handleAccept() {
|
||||
const analytics = (document.getElementById('consent-analytics') as HTMLInputElement)?.checked ?? false;
|
||||
const marketing = (document.getElementById('consent-marketing') as HTMLInputElement)?.checked ?? false;
|
||||
|
||||
const consent: ConsentPreferences = {
|
||||
essential: true,
|
||||
analytics: analytics || true,
|
||||
marketing: marketing || true,
|
||||
timestamp: new Date().toISOString(),
|
||||
policyVersion: POLICY_VERSION,
|
||||
};
|
||||
|
||||
saveConsent(consent);
|
||||
hideBanner();
|
||||
}
|
||||
|
||||
function handleReject() {
|
||||
const consent: ConsentPreferences = {
|
||||
essential: true,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
policyVersion: POLICY_VERSION,
|
||||
};
|
||||
|
||||
saveConsent(consent);
|
||||
hideBanner();
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initConsent();
|
||||
|
||||
document.getElementById('consent-accept')?.addEventListener('click', handleAccept);
|
||||
document.getElementById('consent-reject')?.addEventListener('click', handleReject);
|
||||
});
|
||||
|
||||
// Expose function for footer link
|
||||
(window as any).openConsentPreferences = () => {
|
||||
showBanner();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Cookie consent banner styles */
|
||||
#cookie-consent-banner {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
29
src/components/FloatingContact.astro
Normal file
29
src/components/FloatingContact.astro
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
import { siteConfig } from '../data/site-config';
|
||||
---
|
||||
|
||||
<div class="fixed bottom-6 right-6 z-40 flex flex-col gap-3">
|
||||
<!-- LINE -->
|
||||
<a
|
||||
href={`https://line.me/ti/p/${siteConfig.lineId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-14 h-14 bg-[#00B900] rounded-full flex items-center justify-center shadow-lg hover:scale-110 transition-transform"
|
||||
aria-label="ติดต่อผ่าน LINE"
|
||||
>
|
||||
<svg class="w-7 h-7 text-white" viewBox="0 0 24 24" fill="currentColor">
|
||||
<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>
|
||||
|
||||
<!-- Phone -->
|
||||
<a
|
||||
href={`tel:${siteConfig.phone}`}
|
||||
class="w-14 h-14 bg-primary-600 rounded-full flex items-center justify-center shadow-lg hover:scale-110 transition-transform"
|
||||
aria-label="โทรหาเรา"
|
||||
>
|
||||
<svg class="w-7 h-7 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
114
src/components/Footer.astro
Normal file
114
src/components/Footer.astro
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
import { siteConfig, workHours, mainNavigation } from '../data/site-config';
|
||||
---
|
||||
|
||||
<footer class="bg-secondary-50 text-secondary-900">
|
||||
<!-- Main Footer -->
|
||||
<div class="container mx-auto px-4 py-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<!-- Company Info -->
|
||||
<div>
|
||||
<img
|
||||
src="/images/2021/02/13523630950840.png"
|
||||
alt="Deal Plus Tech"
|
||||
class="h-10 w-auto mb-4"
|
||||
/>
|
||||
<p class="text-secondary-600 text-sm mb-4">
|
||||
{siteConfig.description}
|
||||
</p>
|
||||
<div class="flex gap-3">
|
||||
<a
|
||||
href={siteConfig.facebookUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-10 h-10 bg-secondary-200 rounded-lg flex items-center justify-center text-secondary-700 hover:bg-primary-600 hover:text-white transition-colors"
|
||||
aria-label="Facebook"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href={`https://line.me/ti/p/${siteConfig.lineId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-10 h-10 bg-secondary-200 rounded-lg flex items-center justify-center text-secondary-700 hover:bg-primary-600 hover:text-white transition-colors"
|
||||
aria-label="LINE"
|
||||
>
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<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>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div>
|
||||
<h3 class="text-lg font-bold mb-4 text-primary-600">ลิงก์ด่วน</h3>
|
||||
<ul class="space-y-2">
|
||||
{mainNavigation.slice(0, 5).map((item) => (
|
||||
<li>
|
||||
<a href={item.href} class="text-secondary-600 hover:text-primary-600 transition-colors">
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div>
|
||||
<h3 class="text-lg font-bold mb-4 text-primary-600">ติดต่อเรา</h3>
|
||||
<ul class="space-y-3">
|
||||
<li class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-primary-500 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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 class="text-secondary-600 text-sm">{siteConfig.address}</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-3">
|
||||
<svg class="w-5 h-5 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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:${siteConfig.phone}`} class="text-secondary-600 hover:text-primary-600 text-sm">
|
||||
{siteConfig.phone}
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex items-center gap-3">
|
||||
<svg class="w-5 h-5 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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:${siteConfig.email}`} class="text-secondary-600 hover:text-primary-600 text-sm">
|
||||
{siteConfig.email}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Business Hours -->
|
||||
<div>
|
||||
<h3 class="text-lg font-bold mb-4 text-primary-600">เวลาทำการ</h3>
|
||||
<ul class="space-y-2">
|
||||
{workHours.map((item) => (
|
||||
<li class="flex justify-between text-sm">
|
||||
<span class="text-secondary-600">{item.day}</span>
|
||||
<span class={item.isClosed ? 'text-red-500' : 'text-secondary-900 font-medium'}>
|
||||
{item.hours}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Bar -->
|
||||
<div class="border-t border-secondary-200">
|
||||
<div class="container mx-auto px-4 py-4">
|
||||
<p class="text-center text-secondary-500 text-sm">
|
||||
© {new Date().getFullYear()} {siteConfig.name}. สงวนลิขสิทธิ์.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
222
src/components/Header.astro
Normal file
222
src/components/Header.astro
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
import { siteConfig, mainNavigation } from '../data/site-config';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
interface NavItemWithChildren {
|
||||
label: string;
|
||||
href: string;
|
||||
children?: Array<{
|
||||
label: string;
|
||||
href: string;
|
||||
children?: Array<{ label: string; href: string }>;
|
||||
}>;
|
||||
}
|
||||
|
||||
const navItems = mainNavigation as NavItemWithChildren[];
|
||||
---
|
||||
|
||||
<header class="fixed top-0 left-0 right-0 z-50 bg-white shadow-md">
|
||||
<!-- Top Bar -->
|
||||
<div class="bg-primary-600 py-2">
|
||||
<div class="container mx-auto px-4 flex justify-between items-center text-sm">
|
||||
<div class="flex items-center gap-6 text-white">
|
||||
<a href={`tel:${siteConfig.phone}`} class="flex items-center gap-2 hover:text-primary-100">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
{siteConfig.phone}
|
||||
</a>
|
||||
<a href={`mailto:${siteConfig.email}`} class="flex items-center gap-2 hover:text-primary-100">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
{siteConfig.email}
|
||||
</a>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center gap-4">
|
||||
<a href={`https://line.me/ti/p/${siteConfig.lineId}`} class="flex items-center gap-1 text-white hover:text-primary-100">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<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>
|
||||
LINE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Navigation -->
|
||||
<nav class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Logo -->
|
||||
<a href="/" class="flex items-center gap-3">
|
||||
<img
|
||||
src="/images/2021/02/13523630950840.png"
|
||||
alt="Deal Plus Tech"
|
||||
class="h-12 w-auto"
|
||||
loading="priority"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden lg:flex items-center gap-1">
|
||||
{navItems.map((item) => (
|
||||
<div class="relative group">
|
||||
<a
|
||||
href={item.href}
|
||||
class:list={[
|
||||
"px-4 py-2 text-secondary-700 font-medium hover:text-primary-600 transition-colors flex items-center gap-1",
|
||||
item.children && "pr-2"
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
{item.children && (
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
)}
|
||||
</a>
|
||||
|
||||
<!-- Dropdown -->
|
||||
{item.children && (
|
||||
<div class="absolute top-full left-0 pt-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
||||
<div class="absolute -top-2 left-0 right-0 h-2" />
|
||||
<div class="min-w-[600px] bg-white shadow-xl rounded-lg py-4 border border-secondary-100">
|
||||
<div class="grid grid-cols-2 gap-1 px-4">
|
||||
{item.children.map((child) => (
|
||||
<div class="relative group/sub">
|
||||
<a
|
||||
href={child.href}
|
||||
class="block px-3 py-2 text-secondary-700 hover:bg-primary-50 hover:text-primary-700 transition-colors rounded font-medium"
|
||||
>
|
||||
{child.label}
|
||||
</a>
|
||||
{child.children && (
|
||||
<div class="hidden group-hover/sub:block absolute left-full top-0 w-56 !bg-white shadow-xl rounded-lg py-2 border border-secondary-100 max-h-96 overflow-y-auto z-50">
|
||||
<div class="absolute -top-2 -bottom-2 -left-2 w-2" />
|
||||
{child.children.map((subChild) => (
|
||||
<a
|
||||
href={subChild.href}
|
||||
class="block px-4 py-2 text-secondary-600 hover:bg-primary-50 hover:text-primary-700 text-sm"
|
||||
>
|
||||
{subChild.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<a href="/contact-us" class="btn-primary ml-4">
|
||||
ติดต่อเรา
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button
|
||||
id="mobile-menu-button"
|
||||
class="lg:hidden text-secondary-900 p-2"
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div id="mobile-menu" class="lg:hidden py-4 border-t border-secondary-200 max-h-[80vh] overflow-y-auto hidden">
|
||||
{navItems.map((item) => (
|
||||
item.children ? (
|
||||
<div class="border-b border-secondary-100">
|
||||
<div class="px-4 py-3 font-semibold text-secondary-900 bg-secondary-50">
|
||||
{item.label}
|
||||
</div>
|
||||
<div class="pl-4">
|
||||
{item.children.map((child) => (
|
||||
<div>
|
||||
<a
|
||||
href={child.href}
|
||||
class="block px-4 py-2 text-secondary-700 hover:text-primary-600 hover:bg-primary-50 mobile-link"
|
||||
>
|
||||
{child.label}
|
||||
</a>
|
||||
{child.children && (
|
||||
<div class="pl-4 bg-secondary-50">
|
||||
{child.children.map((subChild) => (
|
||||
<a
|
||||
href={subChild.href}
|
||||
class="block px-4 py-2 text-secondary-600 hover:text-primary-600 text-sm mobile-link"
|
||||
>
|
||||
{subChild.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
href={item.href}
|
||||
class="block px-4 py-3 text-secondary-700 hover:text-primary-600 font-medium mobile-link"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
)
|
||||
))}
|
||||
<div class="p-4">
|
||||
<a
|
||||
href="/contact-us"
|
||||
class="btn-primary block text-center mobile-link"
|
||||
>
|
||||
ติดต่อเรา
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<script>
|
||||
// Mobile menu toggle
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
let mobileMenuOpen = false;
|
||||
|
||||
mobileMenuButton?.addEventListener('click', () => {
|
||||
mobileMenuOpen = !mobileMenuOpen;
|
||||
if (mobileMenuOpen) {
|
||||
mobileMenu?.classList.remove('hidden');
|
||||
mobileMenuButton.innerHTML = `
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
`;
|
||||
} else {
|
||||
mobileMenu?.classList.add('hidden');
|
||||
mobileMenuButton.innerHTML = `
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking links
|
||||
document.querySelectorAll('.mobile-link').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
mobileMenuOpen = false;
|
||||
mobileMenu?.classList.add('hidden');
|
||||
mobileMenuButton.innerHTML = `
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
37
src/components/ProductCard.astro
Normal file
37
src/components/ProductCard.astro
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
|
||||
interface Props {
|
||||
product: CollectionEntry<'products'>;
|
||||
}
|
||||
|
||||
const { product } = Astro.props;
|
||||
const { name, shortDescription, image } = product.data;
|
||||
---
|
||||
|
||||
<a href={`/products/${product.data.slug}`} class="card group">
|
||||
<div class="aspect-w-16 aspect-h-9 overflow-hidden bg-secondary-100">
|
||||
<img
|
||||
src={image || '/placeholder.jpg'}
|
||||
alt={name}
|
||||
class="object-cover w-full h-48 group-hover:scale-105 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-bold text-secondary-900 group-hover:text-primary-600 transition-colors">
|
||||
{name}
|
||||
</h3>
|
||||
{shortDescription && (
|
||||
<p class="mt-2 text-sm text-secondary-600 line-clamp-2">
|
||||
{shortDescription}
|
||||
</p>
|
||||
)}
|
||||
<div class="mt-4 flex items-center text-primary-600 font-medium">
|
||||
<span>ดูรายละเอียด</span>
|
||||
<svg class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -1,31 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Script from 'next/script';
|
||||
|
||||
const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID;
|
||||
|
||||
export default function GoogleAnalytics() {
|
||||
if (!GA_MEASUREMENT_ID || GA_MEASUREMENT_ID === 'G-XXXXXXXXXX') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script id="google-analytics" strategy="afterInteractive">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${GA_MEASUREMENT_ID}', {
|
||||
page_title: document.title,
|
||||
page_location: window.location.href,
|
||||
});
|
||||
`}
|
||||
</Script>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { siteConfig } from '@/data/site-config';
|
||||
|
||||
export default function FloatingContact() {
|
||||
return (
|
||||
<div className="fixed bottom-6 right-6 z-40 flex flex-col gap-3">
|
||||
{/* LINE */}
|
||||
<a
|
||||
href={`https://line.me/ti/p/${siteConfig.lineId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-14 h-14 bg-[#00B900] rounded-full flex items-center justify-center shadow-lg hover:scale-110 transition-transform"
|
||||
aria-label="ติดต่อผ่าน LINE"
|
||||
>
|
||||
<svg className="w-7 h-7 text-white" viewBox="0 0 24 24" fill="currentColor">
|
||||
<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>
|
||||
|
||||
{/* Phone */}
|
||||
<a
|
||||
href={`tel:${siteConfig.phone}`}
|
||||
className="w-14 h-14 bg-primary-600 rounded-full flex items-center justify-center shadow-lg hover:scale-110 transition-transform"
|
||||
aria-label="โทรหาเรา"
|
||||
>
|
||||
<svg className="w-7 h-7 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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>
|
||||
);
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { siteConfig, workHours, mainNavigation } from '@/data/site-config';
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-secondary-50 text-secondary-900">
|
||||
{/* Main Footer */}
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{/* Company Info */}
|
||||
<div>
|
||||
<Image
|
||||
src="/images/2021/02/13523630950840.png"
|
||||
alt="Deal Plus Tech"
|
||||
width={150}
|
||||
height={50}
|
||||
className="h-10 w-auto mb-4"
|
||||
/>
|
||||
<p className="text-secondary-600 text-sm mb-4">
|
||||
{siteConfig.description}
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<a
|
||||
href={siteConfig.facebookUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 bg-secondary-200 rounded-lg flex items-center justify-center text-secondary-700 hover:bg-primary-600 hover:text-white transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href={`https://line.me/ti/p/${siteConfig.lineId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 bg-secondary-200 rounded-lg flex items-center justify-center text-secondary-700 hover:bg-primary-600 hover:text-white transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<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>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-4 text-primary-600">ลิงก์ด่วน</h3>
|
||||
<ul className="space-y-2">
|
||||
{mainNavigation.slice(0, 5).map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link href={item.href} className="text-secondary-600 hover:text-primary-600 transition-colors">
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-4 text-primary-600">ติดต่อเรา</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-3">
|
||||
<svg className="w-5 h-5 text-primary-500 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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 strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span className="text-secondary-600 text-sm">{siteConfig.address}</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-3">
|
||||
<svg className="w-5 h-5 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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:${siteConfig.phone}`} className="text-secondary-600 hover:text-primary-600 text-sm">
|
||||
{siteConfig.phone}
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-center gap-3">
|
||||
<svg className="w-5 h-5 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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:${siteConfig.email}`} className="text-secondary-600 hover:text-primary-600 text-sm">
|
||||
{siteConfig.email}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Business Hours */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-4 text-primary-600">เวลาทำการ</h3>
|
||||
<ul className="space-y-2">
|
||||
{workHours.map((item) => (
|
||||
<li key={item.day} className="flex justify-between text-sm">
|
||||
<span className="text-secondary-600">{item.day}</span>
|
||||
<span className={item.isClosed ? 'text-red-500' : 'text-secondary-900 font-medium'}>
|
||||
{item.hours}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-secondary-200">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<p className="text-center text-secondary-500 text-sm">
|
||||
© {new Date().getFullYear()} {siteConfig.name}. สงวนลิขสิทธิ์.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { siteConfig, mainNavigation } from '@/data/site-config';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function Header() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-white shadow-md">
|
||||
{/* Top Bar */}
|
||||
<div className="bg-primary-600 py-2">
|
||||
<div className="container mx-auto px-4 flex justify-between items-center text-sm">
|
||||
<div className="flex items-center gap-6 text-white">
|
||||
<a href={`tel:${siteConfig.phone}`} className="flex items-center gap-2 hover:text-primary-100">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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>
|
||||
{siteConfig.phone}
|
||||
</a>
|
||||
<a href={`mailto:${siteConfig.email}`} className="flex items-center gap-2 hover:text-primary-100">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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>
|
||||
{siteConfig.email}
|
||||
</a>
|
||||
</div>
|
||||
<div className="hidden md:flex items-center gap-4">
|
||||
<a href={`https://line.me/ti/p/${siteConfig.lineId}`} className="flex items-center gap-1 text-white hover:text-primary-100">
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<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>
|
||||
LINE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Navigation */}
|
||||
<nav className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<Image
|
||||
src="/images/2021/02/13523630950840.png"
|
||||
alt="Deal Plus Tech"
|
||||
width={150}
|
||||
height={50}
|
||||
className="h-12 w-auto"
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden lg:flex items-center gap-1">
|
||||
{mainNavigation.map((item) => (
|
||||
<div
|
||||
key={item.href}
|
||||
className="relative group"
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"px-4 py-2 text-secondary-700 font-medium hover:text-primary-600 transition-colors flex items-center gap-1",
|
||||
item.children && "pr-2"
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
{item.children && (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Dropdown - CSS hover with invisible bridge */}
|
||||
{item.children && (
|
||||
<div className="absolute top-full left-0 pt-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
||||
{/* Invisible bridge to prevent gap */}
|
||||
<div className="absolute -top-2 left-0 right-0 h-2" />
|
||||
<div className="min-w-[600px] bg-white shadow-xl rounded-lg py-4 border border-secondary-100">
|
||||
<div className="grid grid-cols-2 gap-1 px-4">
|
||||
{item.children.map((child) => (
|
||||
<div key={child.href} className="relative group/sub">
|
||||
<Link
|
||||
href={child.href}
|
||||
className="block px-3 py-2 text-secondary-700 hover:bg-primary-50 hover:text-primary-700 transition-colors rounded font-medium"
|
||||
>
|
||||
{child.label}
|
||||
</Link>
|
||||
{child.children && (
|
||||
<div className="hidden group-hover/sub:block absolute left-full top-0 w-56 !bg-white shadow-xl rounded-lg py-2 border border-secondary-100 max-h-96 overflow-y-auto z-50">
|
||||
{/* Invisible bridge to prevent gap */}
|
||||
<div className="absolute -top-2 -bottom-2 -left-2 w-2" />
|
||||
{child.children.map((subChild) => (
|
||||
<Link
|
||||
key={subChild.href}
|
||||
href={subChild.href}
|
||||
className="block px-4 py-2 text-secondary-600 hover:bg-primary-50 hover:text-primary-700 text-sm"
|
||||
>
|
||||
{subChild.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Link href="/contact-us" className="btn-primary ml-4">
|
||||
ติดต่อเรา
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
className="lg:hidden text-secondary-900 p-2"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
{mobileMenuOpen ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="lg:hidden py-4 border-t border-secondary-200 max-h-[80vh] overflow-y-auto">
|
||||
{mainNavigation.map((item) => (
|
||||
<div key={item.href}>
|
||||
{item.children ? (
|
||||
<div className="border-b border-secondary-100">
|
||||
<div className="px-4 py-3 font-semibold text-secondary-900 bg-secondary-50">
|
||||
{item.label}
|
||||
</div>
|
||||
<div className="pl-4">
|
||||
{item.children.map((child) => (
|
||||
<div key={child.href}>
|
||||
<Link
|
||||
href={child.href}
|
||||
className="block px-4 py-2 text-secondary-700 hover:text-primary-600 hover:bg-primary-50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{child.label}
|
||||
</Link>
|
||||
{child.children && (
|
||||
<div className="pl-4 bg-secondary-50">
|
||||
{child.children.map((subChild) => (
|
||||
<Link
|
||||
key={subChild.href}
|
||||
href={subChild.href}
|
||||
className="block px-4 py-2 text-secondary-600 hover:text-primary-600 text-sm"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{subChild.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href={item.href}
|
||||
className="block px-4 py-3 text-secondary-700 hover:text-primary-600 font-medium"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="p-4">
|
||||
<Link
|
||||
href="/contact-us"
|
||||
className="btn-primary block text-center"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
ติดต่อเรา
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { HTMLAttributes, forwardRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
variant?: 'default' | 'primary' | 'success' | 'warning' | 'error';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
({ className, variant = 'default', size = 'md', children, ...props }, ref) => {
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center font-medium rounded-full',
|
||||
{
|
||||
'bg-secondary-100 text-secondary-700': variant === 'default',
|
||||
'bg-primary-100 text-primary-700': variant === 'primary',
|
||||
'bg-green-100 text-green-700': variant === 'success',
|
||||
'bg-yellow-100 text-yellow-700': variant === 'warning',
|
||||
'bg-red-100 text-red-700': variant === 'error',
|
||||
},
|
||||
{
|
||||
'px-2 py-0.5 text-xs': size === 'sm',
|
||||
'px-3 py-1 text-sm': size === 'md',
|
||||
'px-4 py-1.5 text-base': size === 'lg',
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Badge.displayName = 'Badge';
|
||||
|
||||
export { Badge };
|
||||
export type { BadgeProps };
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = 'primary', size = 'md', children, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 disabled:pointer-events-none disabled:opacity-50',
|
||||
// Variants
|
||||
{
|
||||
'bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800': variant === 'primary',
|
||||
'bg-secondary-100 text-secondary-900 hover:bg-secondary-200': variant === 'secondary',
|
||||
'border-2 border-primary-600 text-primary-600 hover:bg-primary-50': variant === 'outline',
|
||||
'text-secondary-600 hover:bg-secondary-100': variant === 'ghost',
|
||||
},
|
||||
// Sizes
|
||||
{
|
||||
'px-3 py-1.5 text-sm rounded': size === 'sm',
|
||||
'px-4 py-2 text-base rounded-md': size === 'md',
|
||||
'px-6 py-3 text-lg rounded-lg': size === 'lg',
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button };
|
||||
export type { ButtonProps };
|
||||
@@ -1,63 +0,0 @@
|
||||
import { HTMLAttributes, forwardRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
variant?: 'default' | 'bordered' | 'elevated';
|
||||
}
|
||||
|
||||
const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className, variant = 'default', children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-white rounded-xl overflow-hidden',
|
||||
{
|
||||
'shadow-sm': variant === 'default',
|
||||
'border border-secondary-200': variant === 'bordered',
|
||||
'shadow-lg': variant === 'elevated',
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-4 border-b border-secondary-100', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-4', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-4 border-t border-secondary-100 bg-secondary-50', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export { Card, CardHeader, CardContent, CardFooter };
|
||||
export type { CardProps };
|
||||
@@ -1,11 +0,0 @@
|
||||
// UI Components - Reusable components for Deal Plus Tech website
|
||||
// These components follow the project's design system using Tailwind CSS
|
||||
|
||||
export { Button } from './Button';
|
||||
export type { ButtonProps } from './Button';
|
||||
|
||||
export { Card, CardHeader, CardContent, CardFooter } from './Card';
|
||||
export type { CardProps } from './Card';
|
||||
|
||||
export { Badge } from './Badge';
|
||||
export type { BadgeProps } from './Badge';
|
||||
Reference in New Issue
Block a user