x
This commit is contained in:
1 parent
3c4bb06003
commit
f7aa4d099f
36 files changed
+526
-178
No files matched your search
@@ -1,12 +1,11 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer'
|
||||
import { getCategoriesResult, getCmsPageResult } from '../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../lib/cms-sections'
|
||||
|
||||
const lang = 'all'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
|
||||
// lang is extracted from params
|
||||
const reservedSlugs = new Set([
|
||||
'about',
|
||||
'article',
|
||||
@@ -32,8 +31,8 @@ function isReservedSlug(slug: string) {
|
||||
return reservedSlugs.has(normalizeSlug(slug))
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
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' }
|
||||
|
||||
const pageResult = await getCmsPageResult(slug, lang)
|
||||
@@ -44,8 +43,8 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
|
||||
}
|
||||
}
|
||||
|
||||
export default async function CmsCustomPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params
|
||||
export default async function CmsCustomPage({ params }: { params: Promise<{ slug: string, lang: string }> }) {
|
||||
const { slug, lang } = await params
|
||||
if (isReservedSlug(slug)) notFound()
|
||||
|
||||
const [pageResult, categoriesResult] = await Promise.all([
|
||||
@@ -1,4 +1,4 @@
|
||||
import Link from 'next/link'
|
||||
import Link from '@/components/LocalizedLink'
|
||||
|
||||
const PILLARS = [
|
||||
{
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import Link from 'next/link'
|
||||
import Link from '@/components/LocalizedLink'
|
||||
import { notFound } from 'next/navigation'
|
||||
import type { Metadata } from 'next'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
import type { Article } from '@homaroma/types'
|
||||
|
||||
import { API_BASE } from '../../../lib/api'
|
||||
import { API_BASE } from '@/lib/api'
|
||||
|
||||
async function getArticle(slug: string): Promise<Article | null> {
|
||||
try {
|
||||
+14
-15
@@ -1,30 +1,29 @@
|
||||
import Link from 'next/link'
|
||||
import Link from '@/components/LocalizedLink'
|
||||
import { notFound } from 'next/navigation'
|
||||
import type { Metadata } from 'next'
|
||||
import type { Category, Product } from '@homaroma/types'
|
||||
import { getCategoryImage } from '@homaroma/types'
|
||||
import DynamicPageRenderer from '../../../components/dynamic/DynamicPageRenderer'
|
||||
import ProductCard from '../../../components/ProductCard'
|
||||
import { type ApiResult, fetchJsonResult, getCategoriesResult, getCmsPageResult } from '../../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../../lib/cms-sections'
|
||||
import { resolveLocalized } from '../../../lib/localization'
|
||||
|
||||
const lang = 'all'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import ProductCard from '@/components/ProductCard'
|
||||
import { type ApiResult, fetchJsonResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
import { resolveLocalized } from '@/lib/localization'
|
||||
|
||||
// lang is extracted from params
|
||||
type CategoryProductsResponse = {
|
||||
category?: Category
|
||||
data?: Product[]
|
||||
}
|
||||
|
||||
async function getCategoryProducts(id: string): Promise<ApiResult<CategoryProductsResponse>> {
|
||||
async function getCategoryProducts(id: string, lang: string): Promise<ApiResult<CategoryProductsResponse>> {
|
||||
return await fetchJsonResult<CategoryProductsResponse>(`/categories/${id}/products?limit=1000&lang=${lang}`, {
|
||||
next: { revalidate: 60 },
|
||||
})
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
||||
const { id } = await params
|
||||
const result = await getCategoryProducts(id)
|
||||
export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise<Metadata> {
|
||||
const { id, lang } = await params
|
||||
const result = await getCategoryProducts(id, lang)
|
||||
const categoryName = result.ok && result.data.category ? resolveLocalized(result.data.category.name, lang) : ''
|
||||
|
||||
return {
|
||||
@@ -33,10 +32,10 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
||||
}
|
||||
}
|
||||
|
||||
export default async function CategoryPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
export default async function CategoryPage({ params }: { params: Promise<{ id: string, lang: string }> }) {
|
||||
const { id, lang } = await params
|
||||
const [categoryProductsResult, categoriesResult, cmsLayoutResult] = await Promise.all([
|
||||
getCategoryProducts(id),
|
||||
getCategoryProducts(id, lang),
|
||||
getCategoriesResult(lang),
|
||||
getCmsPageResult('category-layout', lang),
|
||||
])
|
||||
+7
-7
@@ -1,18 +1,18 @@
|
||||
import { CreditCard } from 'lucide-react'
|
||||
|
||||
import CheckoutClient from '../../components/CheckoutClient'
|
||||
import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer'
|
||||
import { getCategoriesResult, getCmsPageResult } from '../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../lib/cms-sections'
|
||||
|
||||
const lang = 'all'
|
||||
import CheckoutClient from '@/components/CheckoutClient'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
|
||||
// lang is extracted from params
|
||||
export const metadata = {
|
||||
title: 'Checkout | HomAroma',
|
||||
description: 'ตรวจสอบรายการสินค้า กรอกข้อมูลจัดส่ง และสร้างคำสั่งซื้อ HomAroma',
|
||||
}
|
||||
|
||||
export default async function CheckoutPage() {
|
||||
export default async function CheckoutPage({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params
|
||||
const [categoriesResult, cmsLayoutResult] = await Promise.all([
|
||||
getCategoriesResult(lang),
|
||||
getCmsPageResult('checkout-layout', lang),
|
||||
File renamed without changes.
File renamed without changes.
+7
-7
@@ -1,11 +1,10 @@
|
||||
import Link from 'next/link'
|
||||
import Link from '@/components/LocalizedLink'
|
||||
import { Ticket, ArrowRight } from 'lucide-react'
|
||||
import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer'
|
||||
import { getActiveCouponsResult, getCategoriesResult, getCmsPageResult } from '../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../lib/cms-sections'
|
||||
|
||||
const lang = 'all'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import { getActiveCouponsResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
|
||||
// lang is extracted from params
|
||||
function formatDiscount(type: string, value: number) {
|
||||
if (type === 'PERCENTAGE') return `${value}% OFF`
|
||||
if (type === 'FIXED_AMOUNT') return `ลด ${value} ฿`
|
||||
@@ -18,7 +17,8 @@ export const metadata = {
|
||||
description: 'รวมรหัสส่วนลดและโปรโมชั่นพิเศษจาก HomAroma สำหรับการสั่งซื้อน้ำหอมและผลิตภัณฑ์อโรม่าที่คุณชื่นชอบ',
|
||||
}
|
||||
|
||||
export default async function CouponsPage() {
|
||||
export default async function CouponsPage({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params
|
||||
const [couponsResult, categoriesResult, cmsLayoutResult] = await Promise.all([
|
||||
getActiveCouponsResult(),
|
||||
getCategoriesResult(lang),
|
||||
File renamed without changes.
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import Link from '@/components/LocalizedLink'
|
||||
|
||||
const FAQS = [
|
||||
{
|
||||
@@ -1,21 +1,22 @@
|
||||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
import Navbar from '../components/Navbar';
|
||||
import Footer from '../components/Footer';
|
||||
import ClientProviders from '../components/ClientProviders';
|
||||
import { API_BASE } from '../lib/api';
|
||||
import type { MegaMenuPayload } from '../components/navigation/menuTypes';
|
||||
import '../globals.css';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import Footer from '@/components/Footer';
|
||||
import ClientProviders from '@/components/ClientProviders';
|
||||
import { API_BASE } from '@/lib/api';
|
||||
import type { MegaMenuPayload } from '@/components/navigation/menuTypes';
|
||||
import type { MenuItem, StoreSettings } from '@homaroma/types';
|
||||
|
||||
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'),
|
||||
};
|
||||
|
||||
// Fetch categories for Navbar and Footer on the server (lang=all for client-side switching)
|
||||
async function getCategories() {
|
||||
// Fetch categories for Navbar and Footer on the server
|
||||
async function getCategories(lang: string) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/categories?lang=all`, { next: { revalidate: 3600 } });
|
||||
const res = await fetch(`${API_BASE}/categories?lang=${lang}`, { next: { revalidate: 3600 } });
|
||||
if (!res.ok) return [];
|
||||
return await res.json();
|
||||
} catch (error) {
|
||||
@@ -25,9 +26,9 @@ async function getCategories() {
|
||||
}
|
||||
|
||||
// Fetch mega menu on the server — eliminates client-side flash
|
||||
async function getMegaMenu(): Promise<MegaMenuPayload[]> {
|
||||
async function getMegaMenu(lang: string): Promise<MegaMenuPayload[]> {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/menus/main`, { next: { revalidate: 300 } });
|
||||
const res = await fetch(`${API_BASE}/menus/main?lang=${lang}`, { next: { revalidate: 300 } });
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json() as { payload?: MegaMenuPayload[] };
|
||||
return Array.isArray(data.payload) ? data.payload : [];
|
||||
@@ -38,10 +39,10 @@ async function getMegaMenu(): Promise<MegaMenuPayload[]> {
|
||||
}
|
||||
|
||||
// Fetch footer menus (explore + support) on the server
|
||||
async function getFooterMenus(): Promise<{ exploreMenu: MenuItem[]; supportMenu: MenuItem[] }> {
|
||||
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}`, { next: { revalidate: 300 } });
|
||||
const res = await fetch(`${API_BASE}/menus/${slug}?lang=${lang}`, { next: { revalidate: 300 } });
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json() as { items?: MenuItem[] };
|
||||
return Array.isArray(data.items)
|
||||
@@ -61,9 +62,9 @@ async function getFooterMenus(): Promise<{ exploreMenu: MenuItem[]; supportMenu:
|
||||
}
|
||||
|
||||
// Fetch store settings (social links, footer texts) on the server
|
||||
async function getStoreSettings(): Promise<StoreSettings | null> {
|
||||
async function getStoreSettings(lang: string): Promise<StoreSettings | null> {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/settings`, { next: { revalidate: 600 } });
|
||||
const res = await fetch(`${API_BASE}/settings?lang=${lang}`, { next: { revalidate: 600 } });
|
||||
if (!res.ok) return null;
|
||||
return await res.json() as StoreSettings;
|
||||
} catch (error) {
|
||||
@@ -74,18 +75,22 @@ async function getStoreSettings(): Promise<StoreSettings | null> {
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ lang: string }>;
|
||||
}) {
|
||||
const { lang } = await params;
|
||||
|
||||
const [categories, initialMenu, footerMenus, settings] = await Promise.all([
|
||||
getCategories(),
|
||||
getMegaMenu(),
|
||||
getFooterMenus(),
|
||||
getStoreSettings(),
|
||||
getCategories(lang),
|
||||
getMegaMenu(lang),
|
||||
getFooterMenus(lang),
|
||||
getStoreSettings(lang),
|
||||
]);
|
||||
|
||||
return (
|
||||
<html lang="th" className="scroll-smooth">
|
||||
<html lang={lang} className="scroll-smooth">
|
||||
<body className="min-h-screen bg-pop-bg text-pop-black font-sans antialiased">
|
||||
<ClientProviders>
|
||||
<Navbar categories={categories} initialMenu={initialMenu} settings={settings} />
|
||||
+7
-8
@@ -1,15 +1,14 @@
|
||||
import Link from 'next/link'
|
||||
import Link from '@/components/LocalizedLink'
|
||||
import { notFound } from 'next/navigation'
|
||||
import type { Metadata } from 'next'
|
||||
import { CheckCircle2, Clock, PackageCheck, Truck } from 'lucide-react'
|
||||
import { formatPrice } from '@homaroma/types'
|
||||
|
||||
import DynamicPageRenderer from '../../../components/dynamic/DynamicPageRenderer'
|
||||
import { getCategoriesResult, getCmsPageResult, getOrderResult } from '../../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../../lib/cms-sections'
|
||||
|
||||
const lang = 'all'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import { getCategoriesResult, getCmsPageResult, getOrderResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
|
||||
// lang is extracted from params
|
||||
function orderLabel(id: number) {
|
||||
return `#${id.toString().padStart(4, '0')}`
|
||||
}
|
||||
@@ -47,8 +46,8 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
||||
}
|
||||
}
|
||||
|
||||
export default async function OrderStatusPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
export default async function OrderStatusPage({ params }: { params: Promise<{ id: string, lang: string }> }) {
|
||||
const { id, lang } = await params
|
||||
const [orderResult, categoriesResult, cmsLayoutResult] = await Promise.all([
|
||||
getOrderResult(id),
|
||||
getCategoriesResult(lang),
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { Metadata } from 'next'
|
||||
import DynamicPageRenderer from '../components/dynamic/DynamicPageRenderer'
|
||||
import { getCategories, getCmsPage } from '../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../lib/cms-sections'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import { getCategories, getCmsPage } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
|
||||
const lang = 'all'
|
||||
type Props = {
|
||||
params: Promise<{ lang: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { lang } = await params
|
||||
const page = await getCmsPage('home', lang)
|
||||
|
||||
return {
|
||||
@@ -14,7 +17,9 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
}
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
export default async function HomePage({ params }: Props) {
|
||||
const { lang } = await params
|
||||
|
||||
const [categories, page] = await Promise.all([
|
||||
getCategories(lang),
|
||||
getCmsPage('home', lang),
|
||||
+13
-15
@@ -1,24 +1,22 @@
|
||||
import Link from 'next/link'
|
||||
import Link from '@/components/LocalizedLink'
|
||||
import { notFound } from 'next/navigation'
|
||||
import type { Metadata } from 'next'
|
||||
import { getCategoryImage, type Product } from '@homaroma/types'
|
||||
import AddToCartSection from '../../../components/AddToCartSection'
|
||||
import DynamicPageRenderer from '../../../components/dynamic/DynamicPageRenderer'
|
||||
import ProductCard from '../../../components/ProductCard'
|
||||
import { getCategoriesResult, getCmsPageResult, getProduct, getProductResult, getProductsResult } from '../../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../../lib/cms-sections'
|
||||
import { resolveCategoryName, resolveLocalized } from '../../../lib/localization'
|
||||
import AddToCartSection from '@/components/AddToCartSection'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import ProductCard from '@/components/ProductCard'
|
||||
import { getCategoriesResult, getCmsPageResult, getProduct, getProductResult, getProductsResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
import { resolveCategoryName, resolveLocalized } from '@/lib/localization'
|
||||
|
||||
const lang = 'all'
|
||||
|
||||
async function getRelated(categoryId: number, excludeId: number): Promise<Product[]> {
|
||||
async function getRelated(categoryId: number, excludeId: number, lang: string): Promise<Product[]> {
|
||||
const result = await getProductsResult(`categoryId=${categoryId}`, lang)
|
||||
if (!result.ok) return []
|
||||
return result.data.filter((product) => product.id !== excludeId).slice(0, 6)
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
||||
const { id } = await params
|
||||
export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise<Metadata> {
|
||||
const { id, lang } = await params
|
||||
const product = await getProduct(id, lang)
|
||||
|
||||
if (!product) return { title: 'Product Not Found | HomAroma' }
|
||||
@@ -38,8 +36,8 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ProductDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
export default async function ProductDetailPage({ params }: { params: Promise<{ id: string, lang: string }> }) {
|
||||
const { id, lang } = await params
|
||||
const [productResult, categoriesResult, cmsLayoutResult] = await Promise.all([
|
||||
getProductResult(id, lang),
|
||||
getCategoriesResult(lang),
|
||||
@@ -73,7 +71,7 @@ export default async function ProductDetailPage({ params }: { params: Promise<{
|
||||
}
|
||||
|
||||
const product = productResult.data
|
||||
const related = await getRelated(product.category.id, product.id)
|
||||
const related = await getRelated(product.category.id, product.id, lang)
|
||||
const productName = resolveLocalized(product.name, lang) || product.scent.name
|
||||
const categoryName = resolveCategoryName(product.category, lang)
|
||||
const details = resolveLocalized(product.details, lang)
|
||||
+7
-7
@@ -1,18 +1,18 @@
|
||||
import { User } from 'lucide-react'
|
||||
|
||||
import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer'
|
||||
import ProfileClient from '../../components/ProfileClient'
|
||||
import { getCategoriesResult, getCmsPageResult } from '../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../lib/cms-sections'
|
||||
|
||||
const lang = 'all'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import ProfileClient from '@/components/ProfileClient'
|
||||
import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
|
||||
// lang is extracted from params
|
||||
export const metadata = {
|
||||
title: 'โปรไฟล์สมาชิก | HomAroma',
|
||||
description: 'จัดการโปรไฟล์สมาชิก HomAroma ประวัติคำสั่งซื้อ คะแนนสะสม และสมุดที่อยู่จัดส่ง',
|
||||
}
|
||||
|
||||
export default async function ProfilePage() {
|
||||
export default async function ProfilePage({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params
|
||||
const [categoriesResult, cmsLayoutResult] = await Promise.all([
|
||||
getCategoriesResult(lang),
|
||||
getCmsPageResult('profile-layout', lang),
|
||||
+7
-7
@@ -1,16 +1,16 @@
|
||||
import Link from 'next/link'
|
||||
import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer'
|
||||
import { getArticlesResult, getCategoriesResult, getCmsPageResult } from '../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../lib/cms-sections'
|
||||
|
||||
const lang = 'all'
|
||||
import Link from '@/components/LocalizedLink'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import { getArticlesResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
|
||||
// lang is extracted from params
|
||||
export const metadata = {
|
||||
title: 'Scent Guide | HomAroma',
|
||||
description: 'Explore our collection of aroma articles, guides, and sensory knowledge to find your perfect scent.',
|
||||
}
|
||||
|
||||
export default async function ScentGuidePage() {
|
||||
export default async function ScentGuidePage({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params
|
||||
const [articlesResult, categoriesResult, cmsLayoutResult] = await Promise.all([
|
||||
getArticlesResult(),
|
||||
getCategoriesResult(lang),
|
||||
File renamed without changes.
+10
-11
@@ -1,20 +1,19 @@
|
||||
import Link from 'next/link'
|
||||
import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer'
|
||||
import ProductCard from '../../components/ProductCard'
|
||||
import { getCategoriesResult, getCmsPageResult, getProductsResult } from '../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../lib/cms-sections'
|
||||
import { resolveLocalized } from '../../lib/localization'
|
||||
|
||||
const lang = 'all'
|
||||
import Link from '@/components/LocalizedLink'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import ProductCard from '@/components/ProductCard'
|
||||
import { getCategoriesResult, getCmsPageResult, getProductsResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
import { resolveLocalized } from '@/lib/localization'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Search Products | HomAroma',
|
||||
description: 'Search HomAroma products by scent, category, or keyword.',
|
||||
}
|
||||
|
||||
export default async function SearchPage({ searchParams }: { searchParams: Promise<{ q?: string | string[] }> }) {
|
||||
const params = await searchParams
|
||||
const rawQuery = Array.isArray(params.q) ? params.q[0] : params.q
|
||||
export default async function SearchPage({ searchParams, params }: { searchParams: Promise<{ q?: string | string[] }>, params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params
|
||||
const sParams = await searchParams
|
||||
const rawQuery = Array.isArray(sParams.q) ? sParams.q[0] : sParams.q
|
||||
const query = rawQuery?.trim() ?? ''
|
||||
const [categoriesResult, cmsLayoutResult, productsResult] = await Promise.all([
|
||||
getCategoriesResult(lang),
|
||||
File renamed without changes.
@@ -1,21 +1,21 @@
|
||||
import Link from 'next/link'
|
||||
import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer'
|
||||
import ProductCard from '../../components/ProductCard'
|
||||
import ShopCouponBridge from '../../components/ShopCouponBridge'
|
||||
import { getCategoriesResult, getCmsPageResult, getProductsResult } from '../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../lib/cms-sections'
|
||||
import { resolveLocalized } from '../../lib/localization'
|
||||
|
||||
const lang = 'all'
|
||||
import Link from '@/components/LocalizedLink'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import ProductCard from '@/components/ProductCard'
|
||||
import ShopCouponBridge from '@/components/ShopCouponBridge'
|
||||
import { getCategoriesResult, getCmsPageResult, getProductsResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
import { resolveLocalized } from '@/lib/localization'
|
||||
|
||||
// lang is extracted from params
|
||||
export const metadata = {
|
||||
title: 'Shop Aroma Products | HomAroma',
|
||||
description: 'Browse HomAroma aroma oils, reed diffusers, room sprays, and premium scent products.',
|
||||
}
|
||||
|
||||
export default async function ShopPage({ searchParams }: { searchParams: Promise<{ coupon?: string | string[] }> }) {
|
||||
const params = await searchParams
|
||||
const coupon = Array.isArray(params.coupon) ? params.coupon[0] : params.coupon
|
||||
export default async function ShopPage({ searchParams, params }: { searchParams: Promise<{ coupon?: string | string[] }>, params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params
|
||||
const sParams = await searchParams
|
||||
const coupon = Array.isArray(sParams.coupon) ? sParams.coupon[0] : sParams.coupon
|
||||
const [categoriesResult, productsResult, cmsLayoutResult] = await Promise.all([
|
||||
getCategoriesResult(lang),
|
||||
getProductsResult('limit=1000', lang),
|
||||
@@ -1,18 +1,18 @@
|
||||
import { MapPin } from 'lucide-react'
|
||||
|
||||
import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer'
|
||||
import TrackOrderClient from '../../components/TrackOrderClient'
|
||||
import { getCategoriesResult, getCmsPageResult } from '../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../lib/cms-sections'
|
||||
|
||||
const lang = 'all'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import TrackOrderClient from '@/components/TrackOrderClient'
|
||||
import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
|
||||
// lang is extracted from params
|
||||
export const metadata = {
|
||||
title: 'ติดตามคำสั่งซื้อ | HomAroma',
|
||||
description: 'ตรวจสอบสถานะคำสั่งซื้อ HomAroma ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ',
|
||||
}
|
||||
|
||||
export default async function TrackPage() {
|
||||
export default async function TrackPage({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params
|
||||
const [categoriesResult, cmsLayoutResult] = await Promise.all([
|
||||
getCategoriesResult(lang),
|
||||
getCmsPageResult('track-order-layout', lang),
|
||||
+7
-7
@@ -1,18 +1,18 @@
|
||||
import { Heart } from 'lucide-react'
|
||||
|
||||
import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer'
|
||||
import WishlistClient from '../../components/WishlistClient'
|
||||
import { getCategoriesResult, getCmsPageResult } from '../../lib/cms'
|
||||
import { splitCmsLayoutSections } from '../../lib/cms-sections'
|
||||
|
||||
const lang = 'all'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import WishlistClient from '@/components/WishlistClient'
|
||||
import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
|
||||
// lang is extracted from params
|
||||
export const metadata = {
|
||||
title: 'Wishlist | HomAroma',
|
||||
description: 'รายการสินค้าที่คุณบันทึกไว้ใน Wishlist ของ HomAroma',
|
||||
}
|
||||
|
||||
export default async function WishlistPage() {
|
||||
export default async function WishlistPage({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params
|
||||
const [categoriesResult, cmsLayoutResult] = await Promise.all([
|
||||
getCategoriesResult(lang),
|
||||
getCmsPageResult('wishlist-layout', lang),
|
||||
@@ -0,0 +1,14 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: ['/api/', '/_next/', '/profile/', '/checkout/', '/order/'],
|
||||
},
|
||||
sitemap: `${baseUrl}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
import { getCategories, getProducts, getArticlesResult } from '../lib/cms';
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||
|
||||
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
|
||||
|
||||
function createAlternates(path: string) {
|
||||
const alternates: Record<string, string> = {};
|
||||
for (const lang of languages) {
|
||||
alternates[lang] = `${baseUrl}/${lang}${path}`;
|
||||
}
|
||||
return alternates;
|
||||
}
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const [categories, products, articlesResult] = await Promise.all([
|
||||
getCategories(),
|
||||
getProducts('limit=1000'),
|
||||
getArticlesResult(),
|
||||
]);
|
||||
|
||||
const staticPages = [
|
||||
'',
|
||||
'/shop',
|
||||
'/scent-guide',
|
||||
'/about',
|
||||
'/contact',
|
||||
'/faq',
|
||||
];
|
||||
|
||||
const routes: MetadataRoute.Sitemap = staticPages.map((route) => ({
|
||||
url: `${baseUrl}/th${route}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'daily',
|
||||
priority: route === '' ? 1 : 0.8,
|
||||
alternates: {
|
||||
languages: createAlternates(route),
|
||||
},
|
||||
}));
|
||||
|
||||
const categoryRoutes: MetadataRoute.Sitemap = categories.map((category) => ({
|
||||
url: `${baseUrl}/th/category/${category.id}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.7,
|
||||
alternates: {
|
||||
languages: createAlternates(`/category/${category.id}`),
|
||||
},
|
||||
}));
|
||||
|
||||
const productRoutes: MetadataRoute.Sitemap = products.map((product) => ({
|
||||
url: `${baseUrl}/th/products/${product.id}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'daily',
|
||||
priority: 0.9,
|
||||
alternates: {
|
||||
languages: createAlternates(`/products/${product.id}`),
|
||||
},
|
||||
}));
|
||||
|
||||
const articleRoutes: MetadataRoute.Sitemap = [];
|
||||
if (articlesResult.ok && articlesResult.data) {
|
||||
articlesResult.data.forEach((article) => {
|
||||
articleRoutes.push({
|
||||
url: `${baseUrl}/th/article/${article.slug}`,
|
||||
lastModified: new Date(article.updatedAt || article.createdAt),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.6,
|
||||
alternates: {
|
||||
languages: createAlternates(`/article/${article.slug}`),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return [...routes, ...categoryRoutes, ...productRoutes, ...articleRoutes];
|
||||
}
|
||||
@@ -7,11 +7,13 @@ import i18n from '../lib/i18n';
|
||||
|
||||
export default function ClientProviders({ children }: { children: ReactNode }) {
|
||||
useEffect(() => {
|
||||
const savedLang = localStorage.getItem('homaroma_lang')
|
||||
// With Locale Routing, the URL is the source of truth for language.
|
||||
// Sync i18n to match the current URL if needed.
|
||||
const pathLang = window.location.pathname.split('/')[1];
|
||||
const targetLang = pathLang && ['th', 'en', 'ja', 'zh', 'vi', 'ko'].includes(pathLang) ? pathLang : 'th';
|
||||
|
||||
// Hydrate from the user's persisted language only after the first client render.
|
||||
if (savedLang && savedLang !== i18n.language) {
|
||||
void i18n.changeLanguage(savedLang)
|
||||
if (targetLang !== i18n.language) {
|
||||
void i18n.changeLanguage(targetLang);
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import Link from './LocalizedLink'
|
||||
import type { Category, MenuItem, StoreSettings } from '@homaroma/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { resolveLocalized } from '../lib/localization'
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import Link, { LinkProps } from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { getPathWithLocale, SUPPORTED_LOCALES } from '../lib/locales';
|
||||
|
||||
interface LocalizedLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps>, LinkProps {
|
||||
children: React.ReactNode;
|
||||
activeClassName?: string;
|
||||
exact?: boolean;
|
||||
}
|
||||
|
||||
export default function LocalizedLink({
|
||||
href,
|
||||
children,
|
||||
className = '',
|
||||
activeClassName = '',
|
||||
exact = false,
|
||||
...props
|
||||
}: LocalizedLinkProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Extract current locale from pathname
|
||||
const currentLocale = useMemo(() => {
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
if (segments.length > 0 && SUPPORTED_LOCALES.includes(segments[0] as any)) {
|
||||
return segments[0];
|
||||
}
|
||||
return 'th'; // Fallback
|
||||
}, [pathname]);
|
||||
|
||||
// Resolve the localized href
|
||||
const localizedHref = useMemo(() => {
|
||||
if (typeof href === 'string') {
|
||||
return getPathWithLocale(href, currentLocale);
|
||||
}
|
||||
if (typeof href === 'object' && href.pathname) {
|
||||
return {
|
||||
...href,
|
||||
pathname: getPathWithLocale(href.pathname, currentLocale)
|
||||
};
|
||||
}
|
||||
return href;
|
||||
}, [href, currentLocale]);
|
||||
|
||||
// Check if link is active
|
||||
const isActive = useMemo(() => {
|
||||
const targetPath = typeof localizedHref === 'string' ? localizedHref : localizedHref.pathname || '';
|
||||
if (exact) {
|
||||
return pathname === targetPath;
|
||||
}
|
||||
return pathname.startsWith(targetPath) && targetPath !== `/${currentLocale}`;
|
||||
}, [pathname, localizedHref, exact, currentLocale]);
|
||||
|
||||
const combinedClassName = `${className} ${isActive ? activeClassName : ''}`.trim();
|
||||
|
||||
return (
|
||||
<Link href={localizedHref} className={combinedClassName} {...props}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import Link from 'next/link'
|
||||
import Link from './LocalizedLink'
|
||||
import { getPathWithoutLocale, getPathWithLocale } from '../lib/locales'
|
||||
import { useRouter, usePathname } from 'next/navigation'
|
||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||
import type { Category } from '@homaroma/types'
|
||||
@@ -89,7 +90,13 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
|
||||
const changeLanguage = (code: string) => {
|
||||
i18n.changeLanguage(code)
|
||||
localStorage.setItem('homaroma_lang', code)
|
||||
document.cookie = `homaroma_lang=${code}; path=/; max-age=31536000`
|
||||
|
||||
const currentPathWithoutLang = getPathWithoutLocale(pathname)
|
||||
const newPath = getPathWithLocale(currentPathWithoutLang, code)
|
||||
|
||||
setLangSheetOpen(false)
|
||||
router.push(newPath)
|
||||
}
|
||||
|
||||
const loginWithLine = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import Link from 'next/link'
|
||||
import Link from '../LocalizedLink'
|
||||
import { useState, useRef } from 'react'
|
||||
import type { MegaMenuPayload } from './menuTypes'
|
||||
import { getLocalizedMenuText } from './menuTypes'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import Link from '../LocalizedLink'
|
||||
import { ChevronLeft, ChevronRight, Globe, X } from 'lucide-react'
|
||||
import type { MegaMenuPayload, MenuColumn, MenuLink } from './menuTypes'
|
||||
import { getLocalizedMenuText } from './menuTypes'
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import i18n from '../lib/i18n'
|
||||
import { useParams, usePathname } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../lib/locales';
|
||||
|
||||
/**
|
||||
* Returns a hydration-safe 2-char language code.
|
||||
*
|
||||
* WHY this pattern is necessary:
|
||||
* - i18n.ts reads localStorage synchronously at module-load time (before React hydrates)
|
||||
* - So i18n.language may already be 'ja', 'ko', etc. when client components first render
|
||||
* - useTranslation() subscribes to this and causes components to render the user's language
|
||||
* DURING hydration, but the server rendered 'th' — causing a mismatch error
|
||||
*
|
||||
* SOLUTION:
|
||||
* - Always return 'th' until the component has mounted (matching the server)
|
||||
* - After mount, sync to the real language and subscribe to future changes
|
||||
* - Components using this hook NEVER cause a hydration mismatch
|
||||
* Returns the current active language based on the URL.
|
||||
* With Locale Routing, the language is dictated by the first path segment
|
||||
* (e.g., /en/shop -> 'en').
|
||||
*/
|
||||
export function useLang(): string {
|
||||
// Always 'th' on first render (matches SSR) — updated post-hydration
|
||||
const [lang, setLang] = useState('th')
|
||||
const params = useParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// Sync immediately after hydration
|
||||
const current = i18n.language ? i18n.language.substring(0, 2) : 'th'
|
||||
setLang(current)
|
||||
|
||||
// Subscribe to future language changes (user clicks language button)
|
||||
const handler = (lng: string) => setLang(lng.substring(0, 2))
|
||||
i18n.on('languageChanged', handler)
|
||||
return () => { i18n.off('languageChanged', handler) }
|
||||
}, [])
|
||||
|
||||
return lang
|
||||
return useMemo(() => {
|
||||
// 1. Try to get it from route params
|
||||
if (params?.lang && typeof params.lang === 'string') {
|
||||
if (SUPPORTED_LOCALES.includes(params.lang as any)) {
|
||||
return params.lang;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback: extract from pathname if params are not available yet
|
||||
if (pathname) {
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
if (segments.length > 0 && SUPPORTED_LOCALES.includes(segments[0] as any)) {
|
||||
return segments[0];
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_LOCALE;
|
||||
}, [params, pathname]);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export const SUPPORTED_LOCALES = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] as const;
|
||||
export type Locale = typeof SUPPORTED_LOCALES[number];
|
||||
|
||||
export const DEFAULT_LOCALE: Locale = 'th';
|
||||
|
||||
export function isSupportedLocale(locale: string): locale is Locale {
|
||||
return SUPPORTED_LOCALES.includes(locale as Locale);
|
||||
}
|
||||
|
||||
export function getPathWithoutLocale(path: string): string {
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
if (segments.length > 0 && isSupportedLocale(segments[0])) {
|
||||
segments.shift();
|
||||
}
|
||||
return '/' + segments.join('/');
|
||||
}
|
||||
|
||||
export function getPathWithLocale(path: string, locale: string): string {
|
||||
if (!path) return `/${locale}`;
|
||||
if (path.startsWith('http')) return path; // Skip absolute external URLs
|
||||
|
||||
const pathWithoutLocale = getPathWithoutLocale(path);
|
||||
if (pathWithoutLocale === '/') return `/${locale}`;
|
||||
return `/${locale}${pathWithoutLocale}`;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from './lib/locales';
|
||||
|
||||
// Paths that should not be localized
|
||||
const PUBLIC_FILE = /\.(.*)$/;
|
||||
const SKIP_PATHS = ['/_next', '/api', '/favicon.ico', '/robots.txt', '/sitemap.xml', '/sitemap'];
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// Skip Next.js internals, APIs, and public files
|
||||
if (
|
||||
SKIP_PATHS.some((p) => pathname.startsWith(p)) ||
|
||||
PUBLIC_FILE.test(pathname)
|
||||
) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Check if the pathname already has a locale
|
||||
const pathnameHasLocale = SUPPORTED_LOCALES.some(
|
||||
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
|
||||
);
|
||||
|
||||
if (pathnameHasLocale) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Determine the preferred locale
|
||||
// 1. Check cookie
|
||||
const cookieLocale = request.cookies.get('homaroma_lang')?.value;
|
||||
let targetLocale = DEFAULT_LOCALE;
|
||||
|
||||
if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as any)) {
|
||||
targetLocale = cookieLocale as any;
|
||||
} else {
|
||||
// 2. Check Accept-Language header
|
||||
const acceptLang = request.headers.get('accept-language');
|
||||
if (acceptLang) {
|
||||
const preferred = acceptLang.split(',')[0].split('-')[0].toLowerCase();
|
||||
if (SUPPORTED_LOCALES.includes(preferred as any)) {
|
||||
targetLocale = preferred as any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to the localized path
|
||||
const newUrl = request.nextUrl.clone();
|
||||
newUrl.pathname = `/${targetLocale}${pathname === '/' ? '' : pathname}`;
|
||||
|
||||
return NextResponse.redirect(newUrl);
|
||||
}
|
||||
|
||||
// Next.js 16 Proxy configuration
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - api (API routes)
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
|
||||
*/
|
||||
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function walkDir(dir, callback) {
|
||||
fs.readdirSync(dir).forEach(f => {
|
||||
let dirPath = path.join(dir, f);
|
||||
let isDirectory = fs.statSync(dirPath).isDirectory();
|
||||
isDirectory ? walkDir(dirPath, callback) : callback(dirPath);
|
||||
});
|
||||
}
|
||||
|
||||
walkDir('apps/website/src/app/[lang]', (filePath) => {
|
||||
if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
let changed = false;
|
||||
|
||||
// Replace import ... from '../../lib/...' to import ... from '../../../lib/...'
|
||||
// Wait, the files were moved 1 level deeper. So any import starting with '../' needs an extra '../'
|
||||
// To be safe, let's just find and replace `from '../` with `from '../../` etc?
|
||||
// Actually, converting to `@/` is safer:
|
||||
// from '../../components/ => from '@/components/
|
||||
// from '../../../components/ => from '@/components/
|
||||
// from '../../lib/ => from '@/lib/
|
||||
// from '../../../lib/ => from '@/lib/
|
||||
// from '../components/ => from '@/components/
|
||||
// from '../lib/ => from '@/lib/
|
||||
|
||||
const newContent = content.replace(/from '(\.\.\/)+components\//g, "from '@/components/")
|
||||
.replace(/from '(\.\.\/)+lib\//g, "from '@/lib/")
|
||||
.replace(/from '(\.\.\/)+store\//g, "from '@/store/")
|
||||
.replace(/from '(\.\.\/)+hooks\//g, "from '@/hooks/");
|
||||
|
||||
if (newContent !== content) {
|
||||
fs.writeFileSync(filePath, newContent, 'utf8');
|
||||
console.log('Fixed', filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function walkDir(dir, callback) {
|
||||
fs.readdirSync(dir).forEach(f => {
|
||||
let dirPath = path.join(dir, f);
|
||||
let isDirectory = fs.statSync(dirPath).isDirectory();
|
||||
isDirectory ? walkDir(dirPath, callback) : callback(dirPath);
|
||||
});
|
||||
}
|
||||
|
||||
walkDir('apps/website/src/app/[lang]', (filePath) => {
|
||||
if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const newContent = content.replace(/import Link from 'next\/link'/g, "import Link from '@/components/LocalizedLink'");
|
||||
|
||||
if (newContent !== content) {
|
||||
fs.writeFileSync(filePath, newContent, 'utf8');
|
||||
console.log('Fixed link in', filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function walkDir(dir, callback) {
|
||||
fs.readdirSync(dir).forEach(f => {
|
||||
let dirPath = path.join(dir, f);
|
||||
let isDirectory = fs.statSync(dirPath).isDirectory();
|
||||
isDirectory ? walkDir(dirPath, callback) : callback(dirPath);
|
||||
});
|
||||
}
|
||||
|
||||
walkDir('apps/website/src/app/[lang]', (filePath) => {
|
||||
if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
let newContent = content.replace(/\/\/ lang is extracted from paramsexport/g, "// lang is extracted from params\nexport")
|
||||
.replace(/\/\/ lang is extracted from paramsfunction/g, "// lang is extracted from params\nfunction")
|
||||
.replace(/\/\/ lang is extracted from paramstype/g, "// lang is extracted from params\ntype")
|
||||
.replace(/\/\/ lang is extracted from paramsconst/g, "// lang is extracted from params\nconst");
|
||||
|
||||
if (filePath.endsWith('layout.tsx')) {
|
||||
newContent = newContent.replace("import './globals.css';", "import '../globals.css';");
|
||||
}
|
||||
|
||||
if (newContent !== content) {
|
||||
fs.writeFileSync(filePath, newContent, 'utf8');
|
||||
console.log('Fixed syntax in', filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user