This commit is contained in:
Your Name committed 2026-06-21 01:52:57 -04:00
1 parent c449de2482
commit 7e4e89e33c
50 files changed
+306 -140

No files matched your search

@@ -2,7 +2,7 @@ import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
export const metadata: Metadata = {
title: 'Page Not Found | HomAroma',
title: 'Page Not Found',
}
export default function NestedLocaleNotFound() {
+5 -5
View File
@@ -34,14 +34,14 @@ function isReservedSlug(slug: string) {
export async function generateMetadata({ params }: { params: Promise<{ slug: string, lang: string }> }): Promise<Metadata> {
const { slug, lang } = await params
if (isReservedSlug(slug)) return { title: 'Page Not Found | HomAroma' }
if (isReservedSlug(slug)) return { title: 'Page Not Found' }
const pageResult = await getCmsPageResult(slug, lang)
if (!pageResult.ok || !pageResult.data.isPublished) return { title: 'Page Not Found | HomAroma' }
if (!pageResult.ok || !pageResult.data.isPublished) return { title: 'Page Not Found' }
return generateSeoMetadata({
title: `${pageResult.data.title} | HomAroma`,
description: `HomAroma - ${pageResult.data.title}`,
title: `${pageResult.data.title}`,
description: `${pageResult.data.title}`,
path: `/${slug}`,
lang,
})
@@ -100,7 +100,7 @@ export default async function CmsCustomPage({ params }: { params: Promise<{ slug
</>
) : (
<div className="mx-auto max-w-4xl px-6 py-24 text-center">
<p className="text-xs font-bold uppercase tracking-[0.28em] text-brand-primary">HomAroma</p>
<p className="text-xs font-bold uppercase tracking-[0.28em] text-brand-primary"></p>
<h1 className="mt-4 text-4xl font-semibold text-text-main md:text-5xl">{page.title}</h1>
<p className="mt-5 text-muted-text">
หน้านี้ถูกเผยแพร่แล้ว แต่ยังไม่มี section ที่เปิดใช้งานใน Page Builder
+8 -13
View File
@@ -1,5 +1,5 @@
import Link from '@/components/LocalizedLink'
import { generateSeoMetadata } from '@/lib/seo'
import { API_BASE } from '@/lib/api'
const PILLARS = [
{
@@ -26,17 +26,12 @@ const STATS = [
{ value: '100%', label: 'Natural Extracts' },
]
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
export default async function AboutPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
return generateSeoMetadata({
title: 'About Us | HomAroma',
description: 'Born from a passion for stillness. HomAroma began in 2019 as a Bangkok studio crafting botanical aroma blends.',
path: '/about',
lang,
});
}
const res = await fetch(`${API_BASE}/settings?lang=${lang}`, { next: { revalidate: 600 } });
const settings = res.ok ? await res.json() : {};
const brandName = settings?.brandName || '';
export default function AboutPage() {
return (
<div className="min-h-screen bg-surface-page text-text-main">
<section className="relative flex min-h-[60vh] items-center justify-center overflow-hidden bg-surface-card pt-24">
@@ -47,7 +42,7 @@ export default function AboutPage() {
Born from a passion for <span className="italic text-brand-primary">stillness</span>
</h1>
<p className="text-lg leading-relaxed text-muted-text">
HomAroma began in 2019 as a one-person studio in Bangkok, crafting small batches of botanical aroma blends for friends and family. What started as a personal journey toward calm became a philosophy — that our environment shapes how we feel, think, and rest.
{brandName} began in 2019 as a one-person studio in Bangkok, crafting small batches of botanical aroma blends for friends and family. What started as a personal journey toward calm became a philosophy — that our environment shapes how we feel, think, and rest.
</p>
</div>
</section>
@@ -72,7 +67,7 @@ export default function AboutPage() {
We believe that scent is the fastest pathway to emotional transformation. A single breath of the right blend can dissolve anxiety, sharpen focus, or unlock creativity. Our mission is to make that transformation accessible — through products that are as beautiful as they are effective.
</p>
<p className="text-sm leading-relaxed text-muted-text">
Every HomAroma product is designed with intention: the glass bottles are reusable, the packaging is recycled, and the formulas are written to be understood by anyone — not hidden behind chemical jargon.
Every {brandName} product is designed with intention: the glass bottles are reusable, the packaging is recycled, and the formulas are written to be understood by anyone — not hidden behind chemical jargon.
</p>
</div>
<div className="relative h-80 overflow-hidden rounded-3xl border border-line-primary/20 bg-surface-card shadow-card">
@@ -80,7 +75,7 @@ export default function AboutPage() {
<div className="absolute inset-0 flex items-center justify-center">
<div className="space-y-3 text-center">
<div className="text-7xl">🌿</div>
<p className="text-sm uppercase tracking-widest text-muted-text">HomAroma Studio, Bangkok</p>
<p className="text-sm uppercase tracking-widest text-muted-text">{brandName} Studio, Bangkok</p>
</div>
</div>
</div>
@@ -18,9 +18,9 @@ async function getArticle(slug: string): Promise<Article | null> {
export async function generateMetadata({ params }: { params: Promise<{ slug: string, lang: string }> }): Promise<Metadata> {
const { slug, lang } = await params
const article = await getArticle(slug)
if (!article) return { title: 'Article Not Found | HomAroma' }
if (!article) return { title: 'Article Not Found' }
return generateSeoMetadata({
title: `${article.title} | HomAroma Scent Guide`,
title: `${article.title} Scent Guide`,
description: article.content.replace(/<[^>]+>/g, '').slice(0, 160),
path: `/article/${slug}`,
lang,
@@ -29,8 +29,8 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
const categoryName = result.ok && result.data.category ? resolveLocalized(result.data.category.name, lang) : ''
return generateSeoMetadata({
title: categoryName ? `${categoryName} | HomAroma` : 'Category | HomAroma',
description: categoryName ? `Shop ${categoryName} products from HomAroma.` : 'Shop HomAroma category products.',
title: categoryName ? `${categoryName}` : 'Category',
description: categoryName ? `Shop ${categoryName} products from .` : 'Shop category products.',
path: `/category/${id}`,
lang,
})
@@ -10,8 +10,8 @@ import { splitCmsLayoutSections } from '@/lib/cms-sections'
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
return generateSeoMetadata({
title: 'Checkout | HomAroma',
description: 'ตรวจสอบรายการสินค้า กรอกข้อมูลจัดส่ง และสร้างคำสั่งซื้อ HomAroma',
title: 'Checkout',
description: 'ตรวจสอบรายการสินค้า กรอกข้อมูลจัดส่ง และสร้างคำสั่งซื้อ ',
path: '/checkout',
lang,
});
@@ -1,8 +1,8 @@
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Contact Us | HomAroma',
description: 'Get in touch with HomAroma. We respond to all inquiries within 2 hours.',
title: 'Contact Us',
description: 'Get in touch with . We respond to all inquiries within 2 hours.',
}
export default function ContactLayout({ children }: { children: React.ReactNode }) {
+32 -4
View File
@@ -1,13 +1,41 @@
"use client"
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useParams } from 'next/navigation'
type FormState = { name: string; email: string; subject: string; message: string }
type Status = 'idle' | 'sending' | 'sent' | 'error'
const SUBJECTS = ['Product Inquiry', 'Order Support', 'Custom Blending', 'Wholesale', 'Partnership', 'Other']
export default function ContactPage() {
const params = useParams()
const lang = params?.lang as string || 'en'
const [form, setForm] = useState<FormState>({ name: '', email: '', subject: '', message: '' })
const [status, setStatus] = useState<Status>('idle')
const [brandName, setBrandName] = useState('')
const [socials, setSocials] = useState({ line: '@homaroma', ig: '@homaroma', email: 'hello@homaroma.shop' })
useEffect(() => {
fetch(`/api/settings?lang=${lang}`)
.then(res => res.ok ? res.json() : {})
.then(data => {
const brand = data.brandName || ''
setBrandName(brand)
const username = brand.toLowerCase().replace(/\s+/g, '')
let ig = `@${username}`
if (data.instagramUrl) {
const match = data.instagramUrl.match(/instagram\.com\/([^/?]+)/)
if (match) ig = `@${match[1]}`
}
setSocials({
line: `@${username}`,
ig,
email: `hello@${username}.shop`
})
})
.catch(() => {})
}, [lang])
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }))
@@ -35,9 +63,9 @@ export default function ContactPage() {
<h2 className="mb-6 text-xl font-bold text-text-main">Reach Us Directly</h2>
<div className="space-y-5">
{[
{ icon: '📧', label: 'Email', value: 'hello@homaroma.shop', href: 'mailto:hello@homaroma.shop' },
{ icon: '📱', label: 'LINE Official', value: '@homaroma', href: '#' },
{ icon: '📸', label: 'Instagram', value: '@homaroma', href: '#' },
{ icon: '📧', label: 'Email', value: socials.email, href: `mailto:${socials.email}` },
{ icon: '📱', label: 'LINE Official', value: socials.line, href: '#' },
{ icon: '📸', label: 'Instagram', value: socials.ig, href: '#' },
{ icon: '📍', label: 'Studio', value: 'Bangkok, Thailand', href: '#' },
].map(item => (
<a key={item.label} href={item.href} className="group flex items-start gap-4">
+2 -2
View File
@@ -17,8 +17,8 @@ function formatDiscount(type: string, value: number) {
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
return generateSeoMetadata({
title: 'โปรโมชั่นและคูปองส่วนลด | HomAroma',
description: 'รวมรหัสส่วนลดและโปรโมชั่นพิเศษจาก HomAroma สำหรับการสั่งซื้อน้ำหอมและผลิตภัณฑ์อโรม่าที่คุณชื่นชอบ',
title: 'โปรโมชั่นและคูปองส่วนลด',
description: 'รวมรหัสส่วนลดและโปรโมชั่นพิเศษจาก สำหรับการสั่งซื้อน้ำหอมและผลิตภัณฑ์อโรม่าที่คุณชื่นชอบ',
path: '/coupons',
lang,
});
+2 -2
View File
@@ -1,8 +1,8 @@
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'FAQ | HomAroma',
description: 'Everything you need to know about HomAroma products, shipping, and care.',
title: 'FAQ',
description: 'Everything you need to know about products, shipping, and care.',
}
export default function FaqLayout({ children }: { children: React.ReactNode }) {
+31 -7
View File
@@ -1,12 +1,13 @@
"use client"
import { useState } from 'react'
import { useState, useEffect } from 'react'
import Link from '@/components/LocalizedLink'
import { useParams } from 'next/navigation'
const FAQS = [
{
category: 'Products', icon: '🌿',
items: [
{ q: 'What are the ingredients in your aroma products?', a: 'All HomAroma products are crafted from 100% natural botanical extracts, pure essential oils, and pharmaceutical-grade carrier bases. We never use synthetic fragrances, parabens, or harmful chemicals. Each product label lists full ingredient transparency.' },
{ q: 'What are the ingredients in your aroma products?', a: 'All __BRAND__ products are crafted from 100% natural botanical extracts, pure essential oils, and pharmaceutical-grade carrier bases. We never use synthetic fragrances, parabens, or harmful chemicals. Each product label lists full ingredient transparency.' },
{ q: 'How long does each product last?', a: 'Our Reed Diffusers last 4–8 weeks depending on room size and airflow. Aroma Oils (30ml) last approximately 3–4 months with normal use. Room Sprays (100ml) provide 60–80 uses. Car Diffusers cartridges last up to 45 days.' },
{ q: 'Are your products suitable for sensitive skin or allergies?', a: 'We recommend patch testing before topical use. Our Aroma Oils are formulated for diffuser use primarily. If you have known fragrance allergies, consult our scent guide or contact us with your specific concerns.' },
{ q: 'What is the difference between Aroma Oil and Oil Ozone?', a: 'Aroma Oils are pure essential oil blends designed for aromatherapy diffusers and therapeutic use. Oil Ozone is our premium line featuring ozone-infused botanical extracts — designed for air purification and deeper space transformation.' },
@@ -38,22 +39,45 @@ const FAQS = [
]
export default function FaqPage() {
const params = useParams()
const lang = params?.lang as string || 'en'
const [brandName, setBrandName] = useState('')
const [openItems, setOpenItems] = useState<Record<string, boolean>>({})
const [activeCategory, setActiveCategory] = useState<string>('Products')
useEffect(() => {
fetch(`/api/settings?lang=${lang}`)
.then(res => res.ok ? res.json() : {})
.then(data => {
if (data.brandName) setBrandName(data.brandName)
})
.catch(() => {})
}, [lang])
const toggle = (key: string) => setOpenItems(prev => ({ ...prev, [key]: !prev[key] }))
const activeFaqs = FAQS.find(f => f.category === activeCategory)
const processedFaqs = FAQS.map(f => ({
...f,
items: f.items.map(i => ({
...i,
q: i.q.replace(/__BRAND__/g, brandName),
a: i.a.replace(/__BRAND__/g, brandName)
}))
}))
const activeFaqs = processedFaqs.find(f => f.category === activeCategory)
return (
<div className="min-h-screen bg-surface-page text-text-main">
<div className="space-y-4 border-b border-line-primary/20 bg-surface-card px-6 pb-16 pt-32 text-center">
<p className="ui-eyebrow">Support</p>
<h1 className="text-5xl font-bold text-text-main">Frequently Asked Questions</h1>
<p className="mx-auto max-w-lg text-muted-text">Everything you need to know about HomAroma products, shipping, and care.</p>
<p className="mx-auto max-w-lg text-muted-text">Everything you need to know about {brandName} products, shipping, and care.</p>
</div>
<div className="mx-auto max-w-4xl space-y-10 px-6 py-16">
<div className="flex flex-wrap gap-3">
{FAQS.map(({ category, icon }) => (
{processedFaqs.map(({ category, icon }) => (
<button
key={category}
onClick={() => setActiveCategory(category)}
@@ -82,8 +106,8 @@ export default function FaqPage() {
</button>
{isOpen && (
<div className="px-6 pb-6">
<div className="mb-4 h-px w-full bg-line-primary/10" />
<p className="text-sm leading-relaxed text-muted-text">{item.a}</p>
<div className="mb-4 h-px w-full bg-line-primary/10" />
<p className="text-sm leading-relaxed text-muted-text">{item.a}</p>
</div>
)}
</div>
+21 -9
View File
@@ -25,16 +25,28 @@ const kanit = Kanit({
variable: '--font-kanit',
});
export const metadata: Metadata = {
title: 'HomAroma',
description: 'Pure Origins, Artisan Craft, Zero Compromise',
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'),
};
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params;
const settings = await getStoreSettings(lang);
const brandName = settings?.brandName || '';
return {
title: {
template: `%s | ${brandName}`,
default: brandName,
},
description: 'Pure Origins, Artisan Craft, Zero Compromise',
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'),
};
}
// Cache configuration: 0 for dev (no cache), strict numbers for production
const getCacheTime = (prodSeconds: number) => process.env.NODE_ENV === 'development' ? 0 : prodSeconds;
// Fetch categories for Navbar and Footer on the server
async function getCategories(lang: string) {
try {
const res = await fetch(`${API_BASE}/categories?lang=${lang}`, { next: { revalidate: 3600 } });
const res = await fetch(`${API_BASE}/categories?lang=${lang}`, { next: { revalidate: getCacheTime(3600) } });
if (!res.ok) return [];
return await res.json();
} catch (error) {
@@ -46,7 +58,7 @@ async function getCategories(lang: string) {
// Fetch mega menu on the server — eliminates client-side flash
async function getMegaMenu(lang: string): Promise<MegaMenuPayload[]> {
try {
const res = await fetch(`${API_BASE}/menus/main?lang=${lang}`, { next: { revalidate: 300 } });
const res = await fetch(`${API_BASE}/menus/main?lang=${lang}`, { next: { revalidate: getCacheTime(300) } });
if (!res.ok) return [];
const data = await res.json() as { payload?: MegaMenuPayload[] };
return Array.isArray(data.payload) ? data.payload : [];
@@ -60,7 +72,7 @@ async function getMegaMenu(lang: string): Promise<MegaMenuPayload[]> {
async function getFooterMenus(lang: string): Promise<{ exploreMenu: MenuItem[]; supportMenu: MenuItem[] }> {
const fetchMenu = async (slug: string): Promise<MenuItem[]> => {
try {
const res = await fetch(`${API_BASE}/menus/${slug}?lang=${lang}`, { next: { revalidate: 300 } });
const res = await fetch(`${API_BASE}/menus/${slug}?lang=${lang}`, { next: { revalidate: getCacheTime(300) } });
if (!res.ok) return [];
const data = await res.json() as { items?: MenuItem[] };
return Array.isArray(data.items)
@@ -82,7 +94,7 @@ async function getFooterMenus(lang: string): Promise<{ exploreMenu: MenuItem[];
// Fetch store settings (social links, footer texts) on the server
async function getStoreSettings(lang: string): Promise<StoreSettings | null> {
try {
const res = await fetch(`${API_BASE}/settings?lang=${lang}`, { next: { revalidate: 600 } });
const res = await fetch(`${API_BASE}/settings?lang=${lang}`, { next: { revalidate: getCacheTime(600) } });
if (!res.ok) return null;
return await res.json() as StoreSettings;
} catch (error) {
@@ -33,11 +33,11 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
const { id, lang } = await params
const result = await getOrderResult(id)
if (!result.ok) return { title: 'Order Not Found | HomAroma' }
if (!result.ok) return { title: 'Order Not Found' }
return generateSeoMetadata({
title: `Order ${orderLabel(result.data.id)} | HomAroma`,
description: `ตรวจสอบสถานะคำสั่งซื้อ ${orderLabel(result.data.id)} จาก HomAroma`,
title: `Order ${orderLabel(result.data.id)}`,
description: `ตรวจสอบสถานะคำสั่งซื้อ ${orderLabel(result.data.id)} จาก `,
path: `/order/${id}`,
lang,
})
@@ -129,7 +129,7 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
<div key={item.id} className="flex items-center gap-4 rounded-2xl bg-surface-page p-4">
<div className="relative h-14 w-14 shrink-0 overflow-hidden rounded-xl bg-surface-card">
<Image
src={item.productImage || '/images/oil_ozone.png'}
src={item.productImage || '/images/fallback-product.svg'}
alt={item.productName}
fill
unoptimized
+3 -3
View File
@@ -13,8 +13,8 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const page = await getCmsPage('home', lang)
return generateSeoMetadata({
title: page?.title ? `${page.title} | HomAroma` : 'HomAroma',
description: 'Premium aromatherapy, home fragrance, and botanical scent products from HomAroma.',
title: page?.title ? `${page.title}` : undefined,
description: 'Premium aromatherapy, home fragrance, and botanical scent products from .',
path: '',
lang,
})
@@ -48,7 +48,7 @@ export default async function HomePage({ params }: Props) {
) : (
<section className="mx-auto flex min-h-[70vh] max-w-5xl flex-col items-center justify-center px-6 text-center">
<p className="mb-4 ui-eyebrow">
HomAroma
</p>
<h1 className="text-5xl font-semibold text-text-main md:text-7xl">
Premium scents for calmer spaces
@@ -21,15 +21,15 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
const { id, lang } = await params
const product = await getProduct(id, lang)
if (!product) return { title: 'Product Not Found | HomAroma' }
if (!product) return { title: 'Product Not Found' }
const productName = resolveLocalized(product.name, lang) || product.scent.name
const categoryName = resolveCategoryName(product.category, lang)
const description = resolveLocalized(product.description, lang)
return generateSeoMetadata({
title: `${productName} ${categoryName ? `| ${categoryName}` : ''} | HomAroma`,
description: description || `Premium ${categoryName} from HomAroma.`,
title: `${productName} ${categoryName ? `| ${categoryName}` : ''}`,
description: description || `Premium ${categoryName} from .`,
path: `/products/${id}`,
lang,
openGraph: {
+2 -2
View File
@@ -10,8 +10,8 @@ import { splitCmsLayoutSections } from '@/lib/cms-sections'
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
return generateSeoMetadata({
title: 'โปรไฟล์สมาชิก | HomAroma',
description: 'จัดการโปรไฟล์สมาชิก HomAroma ประวัติคำสั่งซื้อ คะแนนสะสม และสมุดที่อยู่จัดส่ง',
title: 'โปรไฟล์สมาชิก',
description: 'จัดการโปรไฟล์สมาชิก ประวัติคำสั่งซื้อ คะแนนสะสม และสมุดที่อยู่จัดส่ง',
path: '/profile',
lang,
});
@@ -9,7 +9,7 @@ import { splitCmsLayoutSections } from '@/lib/cms-sections'
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
return generateSeoMetadata({
title: 'Scent Guide | HomAroma',
title: 'Scent Guide',
description: 'Explore our collection of aroma articles, guides, and sensory knowledge to find your perfect scent.',
path: '/scent-guide',
lang,
@@ -1,7 +1,7 @@
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Search Products | HomAroma',
title: 'Search Products',
description: 'Search our collection of premium aroma products by scent or product name.',
}
+2 -2
View File
@@ -9,8 +9,8 @@ import { resolveLocalized } from '@/lib/localization'
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
return generateSeoMetadata({
title: 'Search Products | HomAroma',
description: 'Search HomAroma products by scent, category, or keyword.',
title: 'Search Products',
description: 'Search products by scent, category, or keyword.',
path: '/search',
lang,
});
+1 -1
View File
@@ -1,7 +1,7 @@
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Shop All Products | HomAroma',
title: 'Shop All Products',
description: 'Browse our full range of premium aroma products — crafted to elevate every space.',
}
+2 -2
View File
@@ -11,8 +11,8 @@ import { resolveLocalized } from '@/lib/localization'
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
return generateSeoMetadata({
title: 'Shop Aroma Products | HomAroma',
description: 'Browse HomAroma aroma oils, reed diffusers, room sprays, and premium scent products.',
title: 'Shop Aroma Products',
description: 'Browse aroma oils, reed diffusers, room sprays, and premium scent products.',
path: '/shop',
lang,
});
+2 -2
View File
@@ -10,8 +10,8 @@ import { splitCmsLayoutSections } from '@/lib/cms-sections'
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
return generateSeoMetadata({
title: 'ติดตามคำสั่งซื้อ | HomAroma',
description: 'ตรวจสอบสถานะคำสั่งซื้อ HomAroma ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ',
title: 'ติดตามคำสั่งซื้อ',
description: 'ตรวจสอบสถานะคำสั่งซื้อ ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ',
path: '/track',
lang,
});
@@ -10,8 +10,8 @@ import { splitCmsLayoutSections } from '@/lib/cms-sections'
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
return generateSeoMetadata({
title: 'Wishlist | HomAroma',
description: 'รายการสินค้าที่คุณบันทึกไว้ใน Wishlist ของ HomAroma',
title: 'Wishlist',
description: 'รายการสินค้าที่คุณบันทึกไว้ใน Wishlist ของ ',
path: '/wishlist',
lang,
});
+1 -1
View File
@@ -44,7 +44,7 @@ export default function CartDrawer() {
<div key={item.cartItemId} className="group relative flex gap-4 rounded-2xl border border-line-primary/20 bg-surface-page/60 p-4">
<div className="relative h-24 w-20 flex-shrink-0 overflow-hidden rounded-lg bg-surface-card">
<Image
src={item.productImage || '/images/oil_ozone.png'}
src={item.productImage || '/images/fallback-product.svg'}
alt={item.productName}
fill
unoptimized
@@ -105,12 +105,12 @@ function toOrderItem(item: CartItem) {
function saveLocalOrder(orderId: number) {
try {
const existingValue = window.localStorage.getItem('homaroma_local_orders')
const existingValue = window.localStorage.getItem('app_local_orders')
const existing = existingValue ? JSON.parse(existingValue) : []
const orderIds = Array.isArray(existing) ? existing.filter((id) => typeof id === 'number') : []
if (!orderIds.includes(orderId)) {
window.localStorage.setItem('homaroma_local_orders', JSON.stringify([orderId, ...orderIds]))
window.localStorage.setItem('app_local_orders', JSON.stringify([orderId, ...orderIds]))
}
} catch (error) {
console.warn('[website-checkout] Failed to persist local order history', error)
@@ -193,7 +193,7 @@ export default function CheckoutClient() {
useEffect(() => {
queueMicrotask(() => {
const savedCoupon = window.localStorage.getItem('homaroma_active_coupon')
const savedCoupon = window.localStorage.getItem('app_active_coupon')
if (savedCoupon) setCouponCode(savedCoupon)
})
}, [])
@@ -242,7 +242,7 @@ export default function CheckoutClient() {
useEffect(() => {
if (!couponCode || appliedCoupon || subtotalAfterL2 <= 0) return
const savedCoupon = window.localStorage.getItem('homaroma_active_coupon')
const savedCoupon = window.localStorage.getItem('app_active_coupon')
if (!savedCoupon || savedCoupon !== couponCode) return
const applySavedCoupon = async () => {
@@ -258,10 +258,10 @@ export default function CheckoutClient() {
setDiscountAmount(data.discountAmount ?? 0)
setIsFreeShipping(Boolean(data.isFreeShipping))
} else {
window.localStorage.removeItem('homaroma_active_coupon')
window.localStorage.removeItem('app_active_coupon')
}
} catch {
window.localStorage.removeItem('homaroma_active_coupon')
window.localStorage.removeItem('app_active_coupon')
}
}
@@ -289,7 +289,7 @@ export default function CheckoutClient() {
setAppliedCoupon(data.coupon.code)
setDiscountAmount(data.discountAmount ?? 0)
setIsFreeShipping(Boolean(data.isFreeShipping))
window.localStorage.setItem('homaroma_active_coupon', data.coupon.code)
window.localStorage.setItem('app_active_coupon', data.coupon.code)
} catch (error) {
setAppliedCoupon(null)
setDiscountAmount(0)
@@ -306,7 +306,7 @@ export default function CheckoutClient() {
setDiscountAmount(0)
setIsFreeShipping(false)
setCouponError(null)
window.localStorage.removeItem('homaroma_active_coupon')
window.localStorage.removeItem('app_active_coupon')
}
const selectAddress = (address: CustomerAddress | 'new') => {
@@ -611,7 +611,7 @@ export default function CheckoutClient() {
<div key={item.cartItemId} className="flex gap-3">
<div className="relative h-16 w-14 shrink-0 overflow-hidden rounded-xl bg-surface-page">
<Image
src={item.productImage || '/images/oil_ozone.png'}
src={item.productImage || '/images/fallback-product.svg'}
alt={item.productName}
fill
unoptimized
+2 -2
View File
@@ -35,7 +35,7 @@ const LANGUAGES = [
function setLanguageCookie(code: string) {
if (typeof document !== 'undefined') {
document.cookie = `homaroma_lang=${code}; path=/; max-age=31536000`
document.cookie = `app_lang=${code}; path=/; max-age=31536000`
}
}
@@ -96,7 +96,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
const changeLanguage = (code: string) => {
i18n.changeLanguage(code)
localStorage.setItem('homaroma_lang', code)
localStorage.setItem('app_lang', code)
setLanguageCookie(code)
const currentPathWithoutLang = getPathWithoutLocale(pathname)
@@ -327,7 +327,7 @@ export default function ProfileClient() {
<div className="flex min-w-0 items-center gap-4">
<div className="ui-avatar-shell-sm relative shrink-0">
<Image
src={displayCustomer.pictureUrl || `https://ui-avatars.com/api/?name=${encodeURIComponent(displayCustomer.displayName || 'HomAroma')}`}
src={displayCustomer.pictureUrl || `https://ui-avatars.com/api/?name=${encodeURIComponent(displayCustomer.displayName || 'Member')}`}
alt={displayCustomer.displayName || 'LINE Profile'}
fill
unoptimized
@@ -335,7 +335,7 @@ export default function ProfileClient() {
/>
</div>
<div className="min-w-0">
<h1 className="truncate text-2xl font-semibold text-text-main">{displayCustomer.displayName || 'HomAroma Member'}</h1>
<h1 className="truncate text-2xl font-semibold text-text-main">{displayCustomer.displayName || 'Member'}</h1>
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs font-semibold text-muted-text">
{displayCustomer.phone && <span className="inline-flex items-center gap-1"><Phone className="h-3 w-3" />{displayCustomer.phone}</span>}
{displayCustomer.email && <span className="inline-flex items-center gap-1"><Mail className="h-3 w-3" />{displayCustomer.email}</span>}
@@ -379,7 +379,7 @@ export default function ProfileClient() {
{activeTab === 'orders' && (
<div className="space-y-5">
{orders.length === 0 ? (
<EmptyPanel icon={Package} title="ไม่มีประวัติการสั่งซื้อ" description="คุณยังไม่ได้ทำการสั่งซื้อสินค้าใด ๆ กับ HomAroma" actionHref="/shop" actionText="ไปหน้าช้อปปิ้ง" />
<EmptyPanel icon={Package} title="ไม่มีประวัติการสั่งซื้อ" description="คุณยังไม่ได้ทำการสั่งซื้อสินค้าใด ๆ " actionHref="/shop" actionText="ไปหน้าช้อปปิ้ง" />
) : (
orders.map((order) => (
<article key={order.id} className="ui-panel p-6">
@@ -404,7 +404,7 @@ export default function ProfileClient() {
{order.items.map((item) => (
<div key={item.id} className="flex items-center gap-3 text-sm">
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-xl bg-surface-page">
<Image src={item.productImage || '/images/oil_ozone.png'} alt={item.productName} fill unoptimized className="h-full w-full object-cover" />
<Image src={item.productImage || '/images/fallback-product.svg'} alt={item.productName} fill unoptimized className="h-full w-full object-cover" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-semibold text-text-main">{item.productName}</p>
@@ -16,7 +16,7 @@ export default function ShopCouponBridge({ coupon }: ShopCouponBridgeProps) {
useEffect(() => {
if (!coupon) return
localStorage.setItem('homaroma_active_coupon', coupon)
localStorage.setItem('app_active_coupon', coupon)
router.replace(getPathWithLocale('/shop', lang), { scroll: false })
}, [coupon, router, lang])
@@ -41,14 +41,14 @@ export default function TrackOrderClient() {
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const storedOrders = parseStoredOrders(localStorage.getItem('homaroma_local_orders'))
const storedOrders = parseStoredOrders(localStorage.getItem('app_local_orders'))
queueMicrotask(() => setLocalOrders(storedOrders))
}, [])
const saveLocalOrder = (id: number) => {
setLocalOrders((current) => {
const next = [id, ...current.filter((item) => item !== id)].slice(0, 12)
localStorage.setItem('homaroma_local_orders', JSON.stringify(next))
localStorage.setItem('app_local_orders', JSON.stringify(next))
return next
})
}
@@ -115,7 +115,7 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
<section className="ui-hero-shell">
<Image
src={imageUrl}
alt={title || 'HomAroma'}
alt={title || ''}
fill
unoptimized
className="absolute inset-0 h-full w-full scale-[1.02] object-cover"
@@ -130,7 +130,7 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
</p>
)}
<h1 className="ui-hero-title">
{renderAccentTitle(title || 'HomAroma')}
{renderAccentTitle(title || '')}
</h1>
{description && (
<p className="ui-hero-copy">
+1 -1
View File
@@ -35,7 +35,7 @@ export function proxy(request: NextRequest) {
// Determine the preferred locale
// 1. Check cookie
const cookieLocale = request.cookies.get('homaroma_lang')?.value;
const cookieLocale = request.cookies.get('app_lang')?.value;
let targetLocale = DEFAULT_LOCALE;
if (cookieLocale && (SUPPORTED_LOCALES as readonly string[]).includes(cookieLocale)) {