From 8514342c2f5c82d16b91ecc410cce5e0e01018fc Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 3 Jun 2026 09:37:40 +0700 Subject: [PATCH] Rename ssr to website, website to spa, add spa nginx config --- apps/spa/.env.production | 2 + apps/spa/.gitignore | 26 +++ apps/spa/README.md | 73 ++++++ apps/{website => spa}/eslint.config.js | 0 apps/{website => spa}/index.html | 0 apps/spa/package.json | 41 ++++ apps/{website => spa}/postcss.config.js | 0 apps/{ssr => spa}/public/favicon.svg | 0 apps/{ssr => spa}/public/icons.svg | 0 apps/{ssr => spa}/public/images/hero.png | Bin apps/{ssr => spa}/public/images/oil_ozone.png | Bin .../public/images/reed_diffuser.png | Bin apps/{website => spa}/src/App.css | 0 apps/{website => spa}/src/App.tsx | 0 apps/{website => spa}/src/assets/hero.png | Bin apps/{website => spa}/src/assets/react.svg | 0 apps/{website => spa}/src/assets/vite.svg | 0 apps/spa/src/components/CartDrawer.tsx | 214 ++++++++++++++++++ apps/{ssr => spa}/src/components/Footer.tsx | 19 +- apps/{ssr => spa}/src/components/Navbar.tsx | 160 +++++++++---- .../src/components/ProductCard.tsx | 10 +- apps/spa/src/components/ScrollToTop.tsx | 16 ++ apps/{website => spa}/src/i18n.ts | 0 apps/{website => spa}/src/index.css | 0 apps/{website => spa}/src/lib/liff.ts | 0 .../src/locales/en/translation.json | 0 .../src/locales/th/translation.json | 0 apps/{website => spa}/src/main.tsx | 0 apps/{website => spa}/src/pages/AboutPage.tsx | 0 .../src/pages/ArticleReaderPage.tsx | 0 .../src/pages/CategoryPage.tsx | 0 .../src/pages/CheckoutPage.tsx | 0 .../src/pages/ContactPage.tsx | 0 .../src/pages/CouponsPage.tsx | 0 apps/{website => spa}/src/pages/FaqPage.tsx | 0 apps/{website => spa}/src/pages/HomePage.tsx | 0 .../src/pages/NotFoundPage.tsx | 0 .../src/pages/OrderSuccessPage.tsx | 0 .../src/pages/ProductDetailPage.tsx | 0 .../src/pages/ScentGuidePage.tsx | 0 .../{website => spa}/src/pages/SearchPage.tsx | 0 apps/{website => spa}/src/pages/ShopPage.tsx | 0 .../src/pages/TrackOrderPage.tsx | 0 apps/{website => spa}/src/store/authStore.ts | 0 apps/{ssr => spa}/src/store/cartStore.ts | 0 apps/{website => spa}/src/types/index.ts | 0 apps/{website => spa}/tailwind.config.js | 0 apps/{website => spa}/tsconfig.app.json | 0 apps/spa/tsconfig.json | 7 + apps/{website => spa}/tsconfig.node.json | 0 apps/{website => spa}/vite.config.ts | 0 apps/ssr/.env.production | 2 - apps/ssr/.gitignore | 44 ---- apps/ssr/README.md | 36 --- apps/ssr/package.json | 40 ---- apps/ssr/src/components/CartDrawer.tsx | 120 ---------- apps/ssr/src/components/ScrollToTop.tsx | 16 -- apps/ssr/tsconfig.json | 34 --- apps/website/.env.production | 4 +- apps/website/.gitignore | 58 +++-- apps/{ssr => website}/AGENTS.md | 0 apps/{ssr => website}/CLAUDE.md | 0 apps/website/README.md | 95 +++----- apps/{ssr => website}/eslint.config.mjs | 0 apps/{ssr => website}/next.config.ts | 0 apps/website/package.json | 63 +++--- apps/{ssr => website}/postcss.config.mjs | 0 apps/{ssr => website}/public/file.svg | 0 apps/{ssr => website}/public/globe.svg | 0 apps/{ssr => website}/public/next.svg | 0 apps/{ssr => website}/public/vercel.svg | 0 apps/{ssr => website}/public/window.svg | 0 apps/{ssr => website}/src/app/about/page.tsx | 0 .../src/app/article/[slug]/page.tsx | 0 .../src/app/category/[id]/page.tsx | 0 .../src/app/contact/layout.tsx | 0 .../{ssr => website}/src/app/contact/page.tsx | 0 .../{ssr => website}/src/app/coupons/page.tsx | 0 apps/{ssr => website}/src/app/faq/layout.tsx | 0 apps/{ssr => website}/src/app/faq/page.tsx | 0 apps/{ssr => website}/src/app/favicon.ico | Bin apps/{ssr => website}/src/app/globals.css | 0 apps/{ssr => website}/src/app/layout.tsx | 0 apps/{ssr => website}/src/app/page.tsx | 0 .../src/app/products/[id]/page.tsx | 0 .../src/app/scent-guide/page.tsx | 0 .../src/app/search/layout.tsx | 0 apps/{ssr => website}/src/app/search/page.tsx | 0 apps/{ssr => website}/src/app/shop/layout.tsx | 0 apps/{ssr => website}/src/app/shop/page.tsx | 0 apps/{ssr => website}/src/app/track/page.tsx | 0 .../src/components/AddToCartSection.tsx | 0 apps/website/src/components/CartDrawer.tsx | 172 ++++---------- .../src/components/ClientProviders.tsx | 0 apps/website/src/components/Footer.tsx | 19 +- apps/website/src/components/Navbar.tsx | 160 ++++--------- apps/website/src/components/ProductCard.tsx | 10 +- apps/website/src/components/ScrollToTop.tsx | 8 +- apps/{ssr => website}/src/lib/api.ts | 0 apps/{ssr => website}/src/lib/i18n.ts | 0 apps/website/tsconfig.json | 37 ++- serverinfo/post-receive | 50 ++-- serverinfo/spa.homaroma.shop.conf | 33 +++ 103 files changed, 801 insertions(+), 768 deletions(-) create mode 100644 apps/spa/.env.production create mode 100644 apps/spa/.gitignore create mode 100644 apps/spa/README.md rename apps/{website => spa}/eslint.config.js (100%) rename apps/{website => spa}/index.html (100%) create mode 100644 apps/spa/package.json rename apps/{website => spa}/postcss.config.js (100%) rename apps/{ssr => spa}/public/favicon.svg (100%) rename apps/{ssr => spa}/public/icons.svg (100%) rename apps/{ssr => spa}/public/images/hero.png (100%) rename apps/{ssr => spa}/public/images/oil_ozone.png (100%) rename apps/{ssr => spa}/public/images/reed_diffuser.png (100%) rename apps/{website => spa}/src/App.css (100%) rename apps/{website => spa}/src/App.tsx (100%) rename apps/{website => spa}/src/assets/hero.png (100%) rename apps/{website => spa}/src/assets/react.svg (100%) rename apps/{website => spa}/src/assets/vite.svg (100%) create mode 100644 apps/spa/src/components/CartDrawer.tsx rename apps/{ssr => spa}/src/components/Footer.tsx (76%) rename apps/{ssr => spa}/src/components/Navbar.tsx (69%) rename apps/{ssr => spa}/src/components/ProductCard.tsx (88%) create mode 100644 apps/spa/src/components/ScrollToTop.tsx rename apps/{website => spa}/src/i18n.ts (100%) rename apps/{website => spa}/src/index.css (100%) rename apps/{website => spa}/src/lib/liff.ts (100%) rename apps/{ssr => spa}/src/locales/en/translation.json (100%) rename apps/{ssr => spa}/src/locales/th/translation.json (100%) rename apps/{website => spa}/src/main.tsx (100%) rename apps/{website => spa}/src/pages/AboutPage.tsx (100%) rename apps/{website => spa}/src/pages/ArticleReaderPage.tsx (100%) rename apps/{website => spa}/src/pages/CategoryPage.tsx (100%) rename apps/{website => spa}/src/pages/CheckoutPage.tsx (100%) rename apps/{website => spa}/src/pages/ContactPage.tsx (100%) rename apps/{website => spa}/src/pages/CouponsPage.tsx (100%) rename apps/{website => spa}/src/pages/FaqPage.tsx (100%) rename apps/{website => spa}/src/pages/HomePage.tsx (100%) rename apps/{website => spa}/src/pages/NotFoundPage.tsx (100%) rename apps/{website => spa}/src/pages/OrderSuccessPage.tsx (100%) rename apps/{website => spa}/src/pages/ProductDetailPage.tsx (100%) rename apps/{website => spa}/src/pages/ScentGuidePage.tsx (100%) rename apps/{website => spa}/src/pages/SearchPage.tsx (100%) rename apps/{website => spa}/src/pages/ShopPage.tsx (100%) rename apps/{website => spa}/src/pages/TrackOrderPage.tsx (100%) rename apps/{website => spa}/src/store/authStore.ts (100%) rename apps/{ssr => spa}/src/store/cartStore.ts (100%) rename apps/{website => spa}/src/types/index.ts (100%) rename apps/{website => spa}/tailwind.config.js (100%) rename apps/{website => spa}/tsconfig.app.json (100%) create mode 100644 apps/spa/tsconfig.json rename apps/{website => spa}/tsconfig.node.json (100%) rename apps/{website => spa}/vite.config.ts (100%) delete mode 100644 apps/ssr/.env.production delete mode 100644 apps/ssr/.gitignore delete mode 100644 apps/ssr/README.md delete mode 100644 apps/ssr/package.json delete mode 100644 apps/ssr/src/components/CartDrawer.tsx delete mode 100644 apps/ssr/src/components/ScrollToTop.tsx delete mode 100644 apps/ssr/tsconfig.json rename apps/{ssr => website}/AGENTS.md (100%) rename apps/{ssr => website}/CLAUDE.md (100%) rename apps/{ssr => website}/eslint.config.mjs (100%) rename apps/{ssr => website}/next.config.ts (100%) rename apps/{ssr => website}/postcss.config.mjs (100%) rename apps/{ssr => website}/public/file.svg (100%) rename apps/{ssr => website}/public/globe.svg (100%) rename apps/{ssr => website}/public/next.svg (100%) rename apps/{ssr => website}/public/vercel.svg (100%) rename apps/{ssr => website}/public/window.svg (100%) rename apps/{ssr => website}/src/app/about/page.tsx (100%) rename apps/{ssr => website}/src/app/article/[slug]/page.tsx (100%) rename apps/{ssr => website}/src/app/category/[id]/page.tsx (100%) rename apps/{ssr => website}/src/app/contact/layout.tsx (100%) rename apps/{ssr => website}/src/app/contact/page.tsx (100%) rename apps/{ssr => website}/src/app/coupons/page.tsx (100%) rename apps/{ssr => website}/src/app/faq/layout.tsx (100%) rename apps/{ssr => website}/src/app/faq/page.tsx (100%) rename apps/{ssr => website}/src/app/favicon.ico (100%) rename apps/{ssr => website}/src/app/globals.css (100%) rename apps/{ssr => website}/src/app/layout.tsx (100%) rename apps/{ssr => website}/src/app/page.tsx (100%) rename apps/{ssr => website}/src/app/products/[id]/page.tsx (100%) rename apps/{ssr => website}/src/app/scent-guide/page.tsx (100%) rename apps/{ssr => website}/src/app/search/layout.tsx (100%) rename apps/{ssr => website}/src/app/search/page.tsx (100%) rename apps/{ssr => website}/src/app/shop/layout.tsx (100%) rename apps/{ssr => website}/src/app/shop/page.tsx (100%) rename apps/{ssr => website}/src/app/track/page.tsx (100%) rename apps/{ssr => website}/src/components/AddToCartSection.tsx (100%) rename apps/{ssr => website}/src/components/ClientProviders.tsx (100%) rename apps/{ssr => website}/src/lib/api.ts (100%) rename apps/{ssr => website}/src/lib/i18n.ts (100%) create mode 100644 serverinfo/spa.homaroma.shop.conf diff --git a/apps/spa/.env.production b/apps/spa/.env.production new file mode 100644 index 0000000..4d396c4 --- /dev/null +++ b/apps/spa/.env.production @@ -0,0 +1,2 @@ +VITE_LIFF_ID="2009944751-5IYZT5kH" +VITE_API_URL="https://api.homaroma.shop/api" diff --git a/apps/spa/.gitignore b/apps/spa/.gitignore new file mode 100644 index 0000000..e49999d --- /dev/null +++ b/apps/spa/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +!.env.production diff --git a/apps/spa/README.md b/apps/spa/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/apps/spa/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/apps/website/eslint.config.js b/apps/spa/eslint.config.js similarity index 100% rename from apps/website/eslint.config.js rename to apps/spa/eslint.config.js diff --git a/apps/website/index.html b/apps/spa/index.html similarity index 100% rename from apps/website/index.html rename to apps/spa/index.html diff --git a/apps/spa/package.json b/apps/spa/package.json new file mode 100644 index 0000000..206ca95 --- /dev/null +++ b/apps/spa/package.json @@ -0,0 +1,41 @@ +{ + "name": "homaroma-spa", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@homaroma/types": "workspace:*", + "@line/liff": "^2.28.0", + "i18next": "^26.0.4", + "lucide-react": "^1.17.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-i18next": "^17.0.2", + "react-router-dom": "^7.14.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@tailwindcss/postcss": "^4.2.2", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.4.27", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.2", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^8.0.1" + } +} diff --git a/apps/website/postcss.config.js b/apps/spa/postcss.config.js similarity index 100% rename from apps/website/postcss.config.js rename to apps/spa/postcss.config.js diff --git a/apps/ssr/public/favicon.svg b/apps/spa/public/favicon.svg similarity index 100% rename from apps/ssr/public/favicon.svg rename to apps/spa/public/favicon.svg diff --git a/apps/ssr/public/icons.svg b/apps/spa/public/icons.svg similarity index 100% rename from apps/ssr/public/icons.svg rename to apps/spa/public/icons.svg diff --git a/apps/ssr/public/images/hero.png b/apps/spa/public/images/hero.png similarity index 100% rename from apps/ssr/public/images/hero.png rename to apps/spa/public/images/hero.png diff --git a/apps/ssr/public/images/oil_ozone.png b/apps/spa/public/images/oil_ozone.png similarity index 100% rename from apps/ssr/public/images/oil_ozone.png rename to apps/spa/public/images/oil_ozone.png diff --git a/apps/ssr/public/images/reed_diffuser.png b/apps/spa/public/images/reed_diffuser.png similarity index 100% rename from apps/ssr/public/images/reed_diffuser.png rename to apps/spa/public/images/reed_diffuser.png diff --git a/apps/website/src/App.css b/apps/spa/src/App.css similarity index 100% rename from apps/website/src/App.css rename to apps/spa/src/App.css diff --git a/apps/website/src/App.tsx b/apps/spa/src/App.tsx similarity index 100% rename from apps/website/src/App.tsx rename to apps/spa/src/App.tsx diff --git a/apps/website/src/assets/hero.png b/apps/spa/src/assets/hero.png similarity index 100% rename from apps/website/src/assets/hero.png rename to apps/spa/src/assets/hero.png diff --git a/apps/website/src/assets/react.svg b/apps/spa/src/assets/react.svg similarity index 100% rename from apps/website/src/assets/react.svg rename to apps/spa/src/assets/react.svg diff --git a/apps/website/src/assets/vite.svg b/apps/spa/src/assets/vite.svg similarity index 100% rename from apps/website/src/assets/vite.svg rename to apps/spa/src/assets/vite.svg diff --git a/apps/spa/src/components/CartDrawer.tsx b/apps/spa/src/components/CartDrawer.tsx new file mode 100644 index 0000000..831221a --- /dev/null +++ b/apps/spa/src/components/CartDrawer.tsx @@ -0,0 +1,214 @@ +import React from 'react' +import { X, ShoppingBag, Plus, Minus, Trash2, Tag, ArrowRight } from 'lucide-react' +import { Link } from 'react-router-dom' +import { useCartStore } from '../store/cartStore' +import { formatPrice } from '@homaroma/types' +import { useTranslation } from 'react-i18next' +import { API_BASE } from '../types' + +export default function CartDrawer() { + const { t } = useTranslation() + const { isCartOpen, toggleCart, items, updateQuantity, removeFromCart, getCartTotal } = useCartStore() + + if (!isCartOpen) return null + + return ( +
+ {/* Backdrop */} +
toggleCart(false)} + /> + + {/* Drawer */} +
+ {/* Header */} +
+

+ + {t('cart.title')} + + {t('cart.items', { count: items.length })} + +

+ +
+ + {/* Cart Items */} +
+ {items.length === 0 ? ( +
+ +

{t('cart.empty')}

+ +
+ ) : ( + items.map((item) => ( +
+
+ {item.productName} +
+ +
+

{item.productName}

+ {item.variantSize && ( +

{item.variantSize}

+ )} + + {/* Price row */} +
+ {formatPrice(item.price)} + × + {item.quantity} + = + {formatPrice(item.price * item.quantity)} +
+ + {/* Quantity controls */} +
+
+ + {item.quantity} + +
+
+ + +
+
+ )) + )} +
+ + {/* Footer */} + {items.length > 0 && ( + toggleCart(false)} t={t} /> + )} +
+
+ ) +} + +function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: () => void; t?: any }) { + const [savedCoupon, setSavedCoupon] = React.useState(null) + const [discountAmount, setDiscountAmount] = React.useState(0) + const [isFreeShipping, setIsFreeShipping] = React.useState(false) + const [couponValid, setCouponValid] = React.useState(false) + + React.useEffect(() => { + const code = localStorage.getItem('irelax_active_coupon') + setSavedCoupon(code) + if (code && subtotal > 0) { + fetch(`${API_BASE}/coupons/validate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code, cartTotal: subtotal }) + }) + .then(r => r.json()) + .then(data => { + if (data.valid) { + setCouponValid(true) + setDiscountAmount(data.discountAmount || 0) + setIsFreeShipping(data.isFreeShipping || false) + } else { + setCouponValid(false) + } + }) + .catch(() => setCouponValid(false)) + } + }, [subtotal]) + + const removeCoupon = () => { + localStorage.removeItem('irelax_active_coupon') + setSavedCoupon(null) + setDiscountAmount(0) + setIsFreeShipping(false) + setCouponValid(false) + } + + + + return ( +
+ + {/* Totals */} +
+
+ ราคาสินค้า + {formatPrice(subtotal)} +
+ {(discountAmount > 0 || isFreeShipping) && savedCoupon && ( +
+ + ส่วนลดคูปอง + + {savedCoupon} + + + + {isFreeShipping ? 'ส่งฟรี' : `- ${formatPrice(discountAmount)}`} +
+ )} + {savedCoupon && !couponValid && ( +
+ + + คูปอง {savedCoupon} (ยังไม่ถึงยอดขั้นต่ำ) + + +
+ )} +
+ รวมสุทธิ + {formatPrice(Math.max(0, subtotal - discountAmount))} +
+
+ + {/* Checkout button */} + + + ดำเนินการชำระเงิน + + +
+ ) +} + diff --git a/apps/ssr/src/components/Footer.tsx b/apps/spa/src/components/Footer.tsx similarity index 76% rename from apps/ssr/src/components/Footer.tsx rename to apps/spa/src/components/Footer.tsx index 808d723..e6b957f 100644 --- a/apps/ssr/src/components/Footer.tsx +++ b/apps/spa/src/components/Footer.tsx @@ -1,6 +1,5 @@ -"use client"; -import Link from 'next/link' -import type { Category } from '@homaroma/types' +import { Link } from 'react-router-dom' +import type { Category } from '../types' import { useTranslation } from 'react-i18next' interface FooterProps { @@ -31,12 +30,12 @@ export default function Footer({ categories = [] }: FooterProps) {

{t('footer.explore')}

- {t('nav.home')} - {t('nav.shop')} - {t('nav.scentGuide')} - {t('nav.about')} - {t('nav.contact')} - {t('nav.faq')} + {t('nav.home')} + {t('nav.shop')} + {t('nav.scentGuide')} + {t('nav.about')} + {t('nav.contact')} + {t('nav.faq')}
@@ -44,7 +43,7 @@ export default function Footer({ categories = [] }: FooterProps) {

{t('nav.collections')}

{categories.map((c) => ( - + {c.name} ))} diff --git a/apps/ssr/src/components/Navbar.tsx b/apps/spa/src/components/Navbar.tsx similarity index 69% rename from apps/ssr/src/components/Navbar.tsx rename to apps/spa/src/components/Navbar.tsx index a1cd190..8eb8800 100644 --- a/apps/ssr/src/components/Navbar.tsx +++ b/apps/spa/src/components/Navbar.tsx @@ -1,10 +1,10 @@ -"use client"; -import Link from 'next/link' -import { useRouter, usePathname } from 'next/navigation' +import { Link, NavLink, useNavigate } from 'react-router-dom' import { useState, useEffect } from 'react' -import type { Category } from '@homaroma/types' +import type { Category } from '../types' import { ShoppingBag, MapPin } from 'lucide-react' import { useCartStore } from '../store/cartStore' +import { useAuthStore } from '../store/authStore' +import { logoutLiff, loginLiff } from '../lib/liff' import { useTranslation } from 'react-i18next' interface NavbarProps { @@ -17,11 +17,11 @@ export default function Navbar({ categories = [] }: NavbarProps) { const [searchQuery, setSearchQuery] = useState('') const [scrolled, setScrolled] = useState(false) const [localOrdersCount, setLocalOrdersCount] = useState(0) - const router = useRouter() - const pathname = usePathname() + const navigate = useNavigate() const { toggleCart, getCartCount } = useCartStore() const cartCount = getCartCount() + const { customer } = useAuthStore() const { t, i18n } = useTranslation() const toggleLanguage = () => { @@ -45,7 +45,7 @@ export default function Navbar({ categories = [] }: NavbarProps) { }, []) // Close mobile menu on route change - useEffect(() => { setMobileOpen(false) }, [pathname]) + useEffect(() => { setMobileOpen(false) }, [navigate]) // Prevent body scroll when overlays open useEffect(() => { document.body.style.overflow = (searchOpen || mobileOpen) ? 'hidden' : '' @@ -55,15 +55,15 @@ export default function Navbar({ categories = [] }: NavbarProps) { const handleSearch = (e: React.FormEvent) => { e.preventDefault() if (searchQuery.trim()) { - router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`) + navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`) setSearchOpen(false) setMobileOpen(false) setSearchQuery('') } } - const navLinkClass = (path: string) => - pathname === path ? 'text-cyan-glow' : 'text-slate-300 hover:text-white transition-colors' + const navLinkClass = ({ isActive }: { isActive: boolean }) => + isActive ? 'text-cyan-glow' : 'text-slate-300 hover:text-white transition-colors' return ( <> @@ -74,15 +74,15 @@ export default function Navbar({ categories = [] }: NavbarProps) { }`} > {/* Logo */} - setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-white uppercase italic shrink-0"> - iRelax Essential + setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-white uppercase italic shrink-0"> + iRelax Essential {/* ── DESKTOP NAV ── */}
- {t('nav.home')} - {t('nav.shop')} - 🔥 {t('nav.promotions') || 'Promotions'} + {t('nav.home')} + {t('nav.shop')} + 🔥 {t('nav.promotions') || 'Promotions'} {/* Collections dropdown */} {categories.length > 0 && ( @@ -98,7 +98,7 @@ export default function Navbar({ categories = [] }: NavbarProps) { {categories.map((cat) => ( {cat.name} @@ -112,14 +112,14 @@ export default function Navbar({ categories = [] }: NavbarProps) {
)} - {t('nav.scentGuide')} - {t('nav.about')} + {t('nav.scentGuide')} + {t('nav.about')}
{/* ── DESKTOP RIGHT ACTIONS ── */}
{t('nav.shopNow')} @@ -165,39 +165,91 @@ export default function Navbar({ categories = [] }: NavbarProps) { > {i18n.language.startsWith('en') ? 'EN' : 'TH'} + + {customer ? ( +
+ Profile +
+
+
+

Signed in as

+

{customer.displayName}

+
+ +
+
+
+ ) : ( + + )}
{/* ── MOBILE RIGHT ACTIONS ── */} -
+
+ {customer ? ( +
+ Profile setMobileOpen(true)} + className="w-7 h-7 rounded-full border-2 border-cyan-glow/50 object-cover cursor-pointer" + /> +
+ ) : ( + + )} + setMobileOpen(false)} - className="relative p-2 rounded-full glass hover:bg-white/10 transition-all" + className="relative p-1.5 rounded-full glass hover:bg-white/10 transition-all" aria-label="Track Orders" > {localOrdersCount > 0 && ( - + )}
@@ -234,6 +286,9 @@ export default function Navbar({ categories = [] }: NavbarProps) {
{t('nav.menu')}
+ {customer && ( + + )} @@ -248,7 +303,7 @@ export default function Navbar({ categories = [] }: NavbarProps) { {/* Drawer Nav Links */}
{[ - { to: '/', label: t('nav.home') }, + { to: '/', label: t('nav.home'), end: true }, { to: '/shop', label: t('nav.shop') }, { to: '/coupons', label: `🔥 ${t('nav.promotions') || 'Promotions'}` }, { to: '/scent-guide', label: `✨ ${t('nav.scentGuide')}` }, @@ -256,18 +311,19 @@ export default function Navbar({ categories = [] }: NavbarProps) { { to: '/faq', label: t('nav.faq') }, { to: '/contact', label: t('nav.contact') }, ].map(item => ( - setMobileOpen(false)} - className={ + className={({ isActive }) => `block px-4 py-3 rounded-xl text-sm font-medium tracking-wide transition-all duration-200 ${ - pathname === item.to ? 'bg-cyan-glow/10 text-cyan-glow border border-cyan-glow/20' : 'hover:bg-white/5 text-slate-300 hover:text-white' + isActive ? 'bg-cyan-glow/10 text-cyan-glow border border-cyan-glow/20' : 'hover:bg-white/5 text-slate-300 hover:text-white' }` } > {item.label} - + ))} {/* Collections sub-section */} @@ -277,7 +333,7 @@ export default function Navbar({ categories = [] }: NavbarProps) { {categories.map(cat => ( setMobileOpen(false)} className="flex items-center justify-between px-4 py-2.5 rounded-xl text-sm text-slate-400 hover:text-white hover:bg-white/5 transition-all" > @@ -294,19 +350,35 @@ export default function Navbar({ categories = [] }: NavbarProps) { {/* Drawer Footer CTA */}
setMobileOpen(false)} className="block w-full py-3 bg-white text-night-900 font-bold text-center rounded-xl text-xs uppercase tracking-widest hover:bg-cyan-glow transition-colors duration-300" > {t('nav.shopNow')} setMobileOpen(false)} className="block w-full py-3 glass text-center rounded-xl text-xs font-bold uppercase tracking-widest hover:bg-white/10 transition-colors border border-white/10" > {t('nav.findMyScent')} + + {customer ? ( + + ) : ( + + )}
@@ -356,7 +428,7 @@ export default function Navbar({ categories = [] }: NavbarProps) { ].map(item => ( setSearchOpen(false)} className="glass px-4 py-1.5 rounded-full text-xs font-medium hover:bg-white/10 hover:text-cyan-glow transition-all border border-white/5" > diff --git a/apps/ssr/src/components/ProductCard.tsx b/apps/spa/src/components/ProductCard.tsx similarity index 88% rename from apps/ssr/src/components/ProductCard.tsx rename to apps/spa/src/components/ProductCard.tsx index 8ccd5be..c1e5138 100644 --- a/apps/ssr/src/components/ProductCard.tsx +++ b/apps/spa/src/components/ProductCard.tsx @@ -1,6 +1,6 @@ -import Link from 'next/link' -import type { Product } from '@homaroma/types' -import { getCategoryImage, formatPrice } from '@homaroma/types' +import { Link } from 'react-router-dom' +import type { Product } from '../types' +import { getCategoryImage, formatPrice } from '../types' interface ProductCardProps { product: Product @@ -11,7 +11,7 @@ export default function ProductCard({ product }: ProductCardProps) {
{product.scent.name} @@ -52,7 +52,7 @@ export default function ProductCard({ product }: ProductCardProps) {
Discover diff --git a/apps/spa/src/components/ScrollToTop.tsx b/apps/spa/src/components/ScrollToTop.tsx new file mode 100644 index 0000000..af21865 --- /dev/null +++ b/apps/spa/src/components/ScrollToTop.tsx @@ -0,0 +1,16 @@ +import { useEffect } from 'react' +import { useLocation } from 'react-router-dom' + +/** + * Scrolls window to top on every route change. + * Place this inside so it has access to location context. + */ +export default function ScrollToTop() { + const { pathname } = useLocation() + + useEffect(() => { + window.scrollTo({ top: 0, behavior: 'instant' }) + }, [pathname]) + + return null +} diff --git a/apps/website/src/i18n.ts b/apps/spa/src/i18n.ts similarity index 100% rename from apps/website/src/i18n.ts rename to apps/spa/src/i18n.ts diff --git a/apps/website/src/index.css b/apps/spa/src/index.css similarity index 100% rename from apps/website/src/index.css rename to apps/spa/src/index.css diff --git a/apps/website/src/lib/liff.ts b/apps/spa/src/lib/liff.ts similarity index 100% rename from apps/website/src/lib/liff.ts rename to apps/spa/src/lib/liff.ts diff --git a/apps/ssr/src/locales/en/translation.json b/apps/spa/src/locales/en/translation.json similarity index 100% rename from apps/ssr/src/locales/en/translation.json rename to apps/spa/src/locales/en/translation.json diff --git a/apps/ssr/src/locales/th/translation.json b/apps/spa/src/locales/th/translation.json similarity index 100% rename from apps/ssr/src/locales/th/translation.json rename to apps/spa/src/locales/th/translation.json diff --git a/apps/website/src/main.tsx b/apps/spa/src/main.tsx similarity index 100% rename from apps/website/src/main.tsx rename to apps/spa/src/main.tsx diff --git a/apps/website/src/pages/AboutPage.tsx b/apps/spa/src/pages/AboutPage.tsx similarity index 100% rename from apps/website/src/pages/AboutPage.tsx rename to apps/spa/src/pages/AboutPage.tsx diff --git a/apps/website/src/pages/ArticleReaderPage.tsx b/apps/spa/src/pages/ArticleReaderPage.tsx similarity index 100% rename from apps/website/src/pages/ArticleReaderPage.tsx rename to apps/spa/src/pages/ArticleReaderPage.tsx diff --git a/apps/website/src/pages/CategoryPage.tsx b/apps/spa/src/pages/CategoryPage.tsx similarity index 100% rename from apps/website/src/pages/CategoryPage.tsx rename to apps/spa/src/pages/CategoryPage.tsx diff --git a/apps/website/src/pages/CheckoutPage.tsx b/apps/spa/src/pages/CheckoutPage.tsx similarity index 100% rename from apps/website/src/pages/CheckoutPage.tsx rename to apps/spa/src/pages/CheckoutPage.tsx diff --git a/apps/website/src/pages/ContactPage.tsx b/apps/spa/src/pages/ContactPage.tsx similarity index 100% rename from apps/website/src/pages/ContactPage.tsx rename to apps/spa/src/pages/ContactPage.tsx diff --git a/apps/website/src/pages/CouponsPage.tsx b/apps/spa/src/pages/CouponsPage.tsx similarity index 100% rename from apps/website/src/pages/CouponsPage.tsx rename to apps/spa/src/pages/CouponsPage.tsx diff --git a/apps/website/src/pages/FaqPage.tsx b/apps/spa/src/pages/FaqPage.tsx similarity index 100% rename from apps/website/src/pages/FaqPage.tsx rename to apps/spa/src/pages/FaqPage.tsx diff --git a/apps/website/src/pages/HomePage.tsx b/apps/spa/src/pages/HomePage.tsx similarity index 100% rename from apps/website/src/pages/HomePage.tsx rename to apps/spa/src/pages/HomePage.tsx diff --git a/apps/website/src/pages/NotFoundPage.tsx b/apps/spa/src/pages/NotFoundPage.tsx similarity index 100% rename from apps/website/src/pages/NotFoundPage.tsx rename to apps/spa/src/pages/NotFoundPage.tsx diff --git a/apps/website/src/pages/OrderSuccessPage.tsx b/apps/spa/src/pages/OrderSuccessPage.tsx similarity index 100% rename from apps/website/src/pages/OrderSuccessPage.tsx rename to apps/spa/src/pages/OrderSuccessPage.tsx diff --git a/apps/website/src/pages/ProductDetailPage.tsx b/apps/spa/src/pages/ProductDetailPage.tsx similarity index 100% rename from apps/website/src/pages/ProductDetailPage.tsx rename to apps/spa/src/pages/ProductDetailPage.tsx diff --git a/apps/website/src/pages/ScentGuidePage.tsx b/apps/spa/src/pages/ScentGuidePage.tsx similarity index 100% rename from apps/website/src/pages/ScentGuidePage.tsx rename to apps/spa/src/pages/ScentGuidePage.tsx diff --git a/apps/website/src/pages/SearchPage.tsx b/apps/spa/src/pages/SearchPage.tsx similarity index 100% rename from apps/website/src/pages/SearchPage.tsx rename to apps/spa/src/pages/SearchPage.tsx diff --git a/apps/website/src/pages/ShopPage.tsx b/apps/spa/src/pages/ShopPage.tsx similarity index 100% rename from apps/website/src/pages/ShopPage.tsx rename to apps/spa/src/pages/ShopPage.tsx diff --git a/apps/website/src/pages/TrackOrderPage.tsx b/apps/spa/src/pages/TrackOrderPage.tsx similarity index 100% rename from apps/website/src/pages/TrackOrderPage.tsx rename to apps/spa/src/pages/TrackOrderPage.tsx diff --git a/apps/website/src/store/authStore.ts b/apps/spa/src/store/authStore.ts similarity index 100% rename from apps/website/src/store/authStore.ts rename to apps/spa/src/store/authStore.ts diff --git a/apps/ssr/src/store/cartStore.ts b/apps/spa/src/store/cartStore.ts similarity index 100% rename from apps/ssr/src/store/cartStore.ts rename to apps/spa/src/store/cartStore.ts diff --git a/apps/website/src/types/index.ts b/apps/spa/src/types/index.ts similarity index 100% rename from apps/website/src/types/index.ts rename to apps/spa/src/types/index.ts diff --git a/apps/website/tailwind.config.js b/apps/spa/tailwind.config.js similarity index 100% rename from apps/website/tailwind.config.js rename to apps/spa/tailwind.config.js diff --git a/apps/website/tsconfig.app.json b/apps/spa/tsconfig.app.json similarity index 100% rename from apps/website/tsconfig.app.json rename to apps/spa/tsconfig.app.json diff --git a/apps/spa/tsconfig.json b/apps/spa/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/apps/spa/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/website/tsconfig.node.json b/apps/spa/tsconfig.node.json similarity index 100% rename from apps/website/tsconfig.node.json rename to apps/spa/tsconfig.node.json diff --git a/apps/website/vite.config.ts b/apps/spa/vite.config.ts similarity index 100% rename from apps/website/vite.config.ts rename to apps/spa/vite.config.ts diff --git a/apps/ssr/.env.production b/apps/ssr/.env.production deleted file mode 100644 index e2d63b2..0000000 --- a/apps/ssr/.env.production +++ /dev/null @@ -1,2 +0,0 @@ -# Production - ชี้ไป Backend จริงบน server -NEXT_PUBLIC_API_URL=https://api.homaroma.shop/api diff --git a/apps/ssr/.gitignore b/apps/ssr/.gitignore deleted file mode 100644 index f8d9b7b..0000000 --- a/apps/ssr/.gitignore +++ /dev/null @@ -1,44 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files -.env -.env.local # secret ไว้บน dev เครื่องตัวเอง -.env*.local -# .env.production ← อนุญาตให้ commit (ไม่มี secret ใดๆ) - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/apps/ssr/README.md b/apps/ssr/README.md deleted file mode 100644 index e215bc4..0000000 --- a/apps/ssr/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/apps/ssr/package.json b/apps/ssr/package.json deleted file mode 100644 index 148b614..0000000 --- a/apps/ssr/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "ssr", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev -p 3001", - "build": "next build", - "start": "next start", - "lint": "eslint" - }, - "dependencies": { - "next": "16.2.4", - "react": "19.2.4", - "react-dom": "19.2.4", - "@homaroma/types": "workspace:*", - "lucide-react": "^0.475.0", - "zustand": "^5.0.0", - "i18next": "^24.0.0", - "react-i18next": "^15.0.0", - "axios": "^1.7.0" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", - "eslint-config-next": "16.2.4", - "tailwindcss": "^4", - "typescript": "^5" - }, - "ignoreScripts": [ - "sharp", - "unrs-resolver" - ], - "trustedDependencies": [ - "sharp", - "unrs-resolver" - ] -} diff --git a/apps/ssr/src/components/CartDrawer.tsx b/apps/ssr/src/components/CartDrawer.tsx deleted file mode 100644 index 9cf87b6..0000000 --- a/apps/ssr/src/components/CartDrawer.tsx +++ /dev/null @@ -1,120 +0,0 @@ -"use client"; -import { X, ShoppingBag, Plus, Minus, Trash2 } from 'lucide-react' -import Link from 'next/link' -import { useCartStore } from '../store/cartStore' -import { formatPrice } from '@homaroma/types' -import { useTranslation } from 'react-i18next' - -export default function CartDrawer() { - const { t } = useTranslation() - const { isCartOpen, toggleCart, items, updateQuantity, removeFromCart, getCartTotal } = useCartStore() - - if (!isCartOpen) return null - - return ( -
- {/* Backdrop */} -
toggleCart(false)} - /> - - {/* Drawer */} -
- {/* Header */} -
-

- - {t('cart.title')} - - {t('cart.items', { count: items.length })} - -

- -
- - {/* Cart Items */} -
- {items.length === 0 ? ( -
- -

{t('cart.empty')}

- -
- ) : ( - items.map((item) => ( -
-
- {item.productName} -
- -
-

{item.productName}

- {item.variantSize && ( -

{item.variantSize}

- )} -

{formatPrice(item.price)}

- -
- - {item.quantity} - -
- - -
-
- )) - )} -
- - {/* Footer */} - {items.length > 0 && ( -
-
- {t('cart.subtotal')} - {formatPrice(getCartTotal())} -
-

{t('cart.shippingNote')}

- toggleCart(false)} - className="btn-primary w-full py-4 text-center block uppercase tracking-widest text-sm" - > - {t('cart.proceed')} - -
- )} -
-
- ) -} diff --git a/apps/ssr/src/components/ScrollToTop.tsx b/apps/ssr/src/components/ScrollToTop.tsx deleted file mode 100644 index 86e1187..0000000 --- a/apps/ssr/src/components/ScrollToTop.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; -import { useEffect } from 'react' -import { usePathname } from 'next/navigation' - -/** - * Scrolls window to top on every route change (Next.js version). - */ -export default function ScrollToTop() { - const pathname = usePathname() - - useEffect(() => { - window.scrollTo({ top: 0, behavior: 'instant' }) - }, [pathname]) - - return null -} diff --git a/apps/ssr/tsconfig.json b/apps/ssr/tsconfig.json deleted file mode 100644 index cf9c65d..0000000 --- a/apps/ssr/tsconfig.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "react-jsx", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts", - "**/*.mts" - ], - "exclude": ["node_modules"] -} diff --git a/apps/website/.env.production b/apps/website/.env.production index 4d396c4..e2d63b2 100644 --- a/apps/website/.env.production +++ b/apps/website/.env.production @@ -1,2 +1,2 @@ -VITE_LIFF_ID="2009944751-5IYZT5kH" -VITE_API_URL="https://api.homaroma.shop/api" +# Production - ชี้ไป Backend จริงบน server +NEXT_PUBLIC_API_URL=https://api.homaroma.shop/api diff --git a/apps/website/.gitignore b/apps/website/.gitignore index e49999d..f8d9b7b 100644 --- a/apps/website/.gitignore +++ b/apps/website/.gitignore @@ -1,26 +1,44 @@ -# Logs -logs -*.log +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug npm-debug.log* yarn-debug.log* yarn-error.log* -pnpm-debug.log* -lerna-debug.log* +.pnpm-debug.log* -node_modules -dist -dist-ssr -*.local +# env files +.env +.env.local # secret ไว้บน dev เครื่องตัวเอง +.env*.local +# .env.production ← อนุญาตให้ commit (ไม่มี secret ใดๆ) -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +# vercel +.vercel -!.env.production +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/ssr/AGENTS.md b/apps/website/AGENTS.md similarity index 100% rename from apps/ssr/AGENTS.md rename to apps/website/AGENTS.md diff --git a/apps/ssr/CLAUDE.md b/apps/website/CLAUDE.md similarity index 100% rename from apps/ssr/CLAUDE.md rename to apps/website/CLAUDE.md diff --git a/apps/website/README.md b/apps/website/README.md index 7dbf7eb..e215bc4 100644 --- a/apps/website/README.md +++ b/apps/website/README.md @@ -1,73 +1,36 @@ -# React + TypeScript + Vite +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +## Getting Started -Currently, two official plugins are available: +First, run the development server: -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/apps/ssr/eslint.config.mjs b/apps/website/eslint.config.mjs similarity index 100% rename from apps/ssr/eslint.config.mjs rename to apps/website/eslint.config.mjs diff --git a/apps/ssr/next.config.ts b/apps/website/next.config.ts similarity index 100% rename from apps/ssr/next.config.ts rename to apps/website/next.config.ts diff --git a/apps/website/package.json b/apps/website/package.json index b0b3ec4..7f86303 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -1,41 +1,40 @@ { - "name": "website", + "name": "homaroma-website", + "version": "0.1.0", "private": true, - "version": "0.0.0", - "type": "module", "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" + "dev": "next dev -p 3001", + "build": "next build", + "start": "next start", + "lint": "eslint" }, "dependencies": { + "next": "16.2.4", + "react": "19.2.4", + "react-dom": "19.2.4", "@homaroma/types": "workspace:*", - "@line/liff": "^2.28.0", - "i18next": "^26.0.4", - "lucide-react": "^1.17.0", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "react-i18next": "^17.0.2", - "react-router-dom": "^7.14.0", - "zustand": "^5.0.12" + "lucide-react": "^0.475.0", + "zustand": "^5.0.0", + "i18next": "^24.0.0", + "react-i18next": "^15.0.0", + "axios": "^1.7.0" }, "devDependencies": { - "@eslint/js": "^9.39.4", - "@tailwindcss/postcss": "^4.2.2", - "@types/node": "^24.12.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "autoprefixer": "^10.4.27", - "eslint": "^9.39.4", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.4.0", - "postcss": "^8.5.8", - "tailwindcss": "^4.2.2", - "typescript": "~5.9.3", - "typescript-eslint": "^8.57.0", - "vite": "^8.0.1" - } + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.4", + "tailwindcss": "^4", + "typescript": "^5" + }, + "ignoreScripts": [ + "sharp", + "unrs-resolver" + ], + "trustedDependencies": [ + "sharp", + "unrs-resolver" + ] } diff --git a/apps/ssr/postcss.config.mjs b/apps/website/postcss.config.mjs similarity index 100% rename from apps/ssr/postcss.config.mjs rename to apps/website/postcss.config.mjs diff --git a/apps/ssr/public/file.svg b/apps/website/public/file.svg similarity index 100% rename from apps/ssr/public/file.svg rename to apps/website/public/file.svg diff --git a/apps/ssr/public/globe.svg b/apps/website/public/globe.svg similarity index 100% rename from apps/ssr/public/globe.svg rename to apps/website/public/globe.svg diff --git a/apps/ssr/public/next.svg b/apps/website/public/next.svg similarity index 100% rename from apps/ssr/public/next.svg rename to apps/website/public/next.svg diff --git a/apps/ssr/public/vercel.svg b/apps/website/public/vercel.svg similarity index 100% rename from apps/ssr/public/vercel.svg rename to apps/website/public/vercel.svg diff --git a/apps/ssr/public/window.svg b/apps/website/public/window.svg similarity index 100% rename from apps/ssr/public/window.svg rename to apps/website/public/window.svg diff --git a/apps/ssr/src/app/about/page.tsx b/apps/website/src/app/about/page.tsx similarity index 100% rename from apps/ssr/src/app/about/page.tsx rename to apps/website/src/app/about/page.tsx diff --git a/apps/ssr/src/app/article/[slug]/page.tsx b/apps/website/src/app/article/[slug]/page.tsx similarity index 100% rename from apps/ssr/src/app/article/[slug]/page.tsx rename to apps/website/src/app/article/[slug]/page.tsx diff --git a/apps/ssr/src/app/category/[id]/page.tsx b/apps/website/src/app/category/[id]/page.tsx similarity index 100% rename from apps/ssr/src/app/category/[id]/page.tsx rename to apps/website/src/app/category/[id]/page.tsx diff --git a/apps/ssr/src/app/contact/layout.tsx b/apps/website/src/app/contact/layout.tsx similarity index 100% rename from apps/ssr/src/app/contact/layout.tsx rename to apps/website/src/app/contact/layout.tsx diff --git a/apps/ssr/src/app/contact/page.tsx b/apps/website/src/app/contact/page.tsx similarity index 100% rename from apps/ssr/src/app/contact/page.tsx rename to apps/website/src/app/contact/page.tsx diff --git a/apps/ssr/src/app/coupons/page.tsx b/apps/website/src/app/coupons/page.tsx similarity index 100% rename from apps/ssr/src/app/coupons/page.tsx rename to apps/website/src/app/coupons/page.tsx diff --git a/apps/ssr/src/app/faq/layout.tsx b/apps/website/src/app/faq/layout.tsx similarity index 100% rename from apps/ssr/src/app/faq/layout.tsx rename to apps/website/src/app/faq/layout.tsx diff --git a/apps/ssr/src/app/faq/page.tsx b/apps/website/src/app/faq/page.tsx similarity index 100% rename from apps/ssr/src/app/faq/page.tsx rename to apps/website/src/app/faq/page.tsx diff --git a/apps/ssr/src/app/favicon.ico b/apps/website/src/app/favicon.ico similarity index 100% rename from apps/ssr/src/app/favicon.ico rename to apps/website/src/app/favicon.ico diff --git a/apps/ssr/src/app/globals.css b/apps/website/src/app/globals.css similarity index 100% rename from apps/ssr/src/app/globals.css rename to apps/website/src/app/globals.css diff --git a/apps/ssr/src/app/layout.tsx b/apps/website/src/app/layout.tsx similarity index 100% rename from apps/ssr/src/app/layout.tsx rename to apps/website/src/app/layout.tsx diff --git a/apps/ssr/src/app/page.tsx b/apps/website/src/app/page.tsx similarity index 100% rename from apps/ssr/src/app/page.tsx rename to apps/website/src/app/page.tsx diff --git a/apps/ssr/src/app/products/[id]/page.tsx b/apps/website/src/app/products/[id]/page.tsx similarity index 100% rename from apps/ssr/src/app/products/[id]/page.tsx rename to apps/website/src/app/products/[id]/page.tsx diff --git a/apps/ssr/src/app/scent-guide/page.tsx b/apps/website/src/app/scent-guide/page.tsx similarity index 100% rename from apps/ssr/src/app/scent-guide/page.tsx rename to apps/website/src/app/scent-guide/page.tsx diff --git a/apps/ssr/src/app/search/layout.tsx b/apps/website/src/app/search/layout.tsx similarity index 100% rename from apps/ssr/src/app/search/layout.tsx rename to apps/website/src/app/search/layout.tsx diff --git a/apps/ssr/src/app/search/page.tsx b/apps/website/src/app/search/page.tsx similarity index 100% rename from apps/ssr/src/app/search/page.tsx rename to apps/website/src/app/search/page.tsx diff --git a/apps/ssr/src/app/shop/layout.tsx b/apps/website/src/app/shop/layout.tsx similarity index 100% rename from apps/ssr/src/app/shop/layout.tsx rename to apps/website/src/app/shop/layout.tsx diff --git a/apps/ssr/src/app/shop/page.tsx b/apps/website/src/app/shop/page.tsx similarity index 100% rename from apps/ssr/src/app/shop/page.tsx rename to apps/website/src/app/shop/page.tsx diff --git a/apps/ssr/src/app/track/page.tsx b/apps/website/src/app/track/page.tsx similarity index 100% rename from apps/ssr/src/app/track/page.tsx rename to apps/website/src/app/track/page.tsx diff --git a/apps/ssr/src/components/AddToCartSection.tsx b/apps/website/src/components/AddToCartSection.tsx similarity index 100% rename from apps/ssr/src/components/AddToCartSection.tsx rename to apps/website/src/components/AddToCartSection.tsx diff --git a/apps/website/src/components/CartDrawer.tsx b/apps/website/src/components/CartDrawer.tsx index 831221a..9cf87b6 100644 --- a/apps/website/src/components/CartDrawer.tsx +++ b/apps/website/src/components/CartDrawer.tsx @@ -1,10 +1,9 @@ -import React from 'react' -import { X, ShoppingBag, Plus, Minus, Trash2, Tag, ArrowRight } from 'lucide-react' -import { Link } from 'react-router-dom' +"use client"; +import { X, ShoppingBag, Plus, Minus, Trash2 } from 'lucide-react' +import Link from 'next/link' import { useCartStore } from '../store/cartStore' import { formatPrice } from '@homaroma/types' import { useTranslation } from 'react-i18next' -import { API_BASE } from '../types' export default function CartDrawer() { const { t } = useTranslation() @@ -40,7 +39,7 @@ export default function CartDrawer() {
{/* Cart Items */} -
+
{items.length === 0 ? (
@@ -55,7 +54,7 @@ export default function CartDrawer() { ) : ( items.map((item) => (
-
+
{item.productName}
-
-

{item.productName}

+
+

{item.productName}

{item.variantSize && ( -

{item.variantSize}

+

{item.variantSize}

)} - - {/* Price row */} -
- {formatPrice(item.price)} - × - {item.quantity} - = - {formatPrice(item.price * item.quantity)} -
+

{formatPrice(item.price)}

- {/* Quantity controls */} -
-
- - {item.quantity} - -
+
+ + {item.quantity} +
@@ -112,103 +99,22 @@ export default function CartDrawer() { {/* Footer */} {items.length > 0 && ( - toggleCart(false)} t={t} /> +
+
+ {t('cart.subtotal')} + {formatPrice(getCartTotal())} +
+

{t('cart.shippingNote')}

+ toggleCart(false)} + className="btn-primary w-full py-4 text-center block uppercase tracking-widest text-sm" + > + {t('cart.proceed')} + +
)}
) } - -function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: () => void; t?: any }) { - const [savedCoupon, setSavedCoupon] = React.useState(null) - const [discountAmount, setDiscountAmount] = React.useState(0) - const [isFreeShipping, setIsFreeShipping] = React.useState(false) - const [couponValid, setCouponValid] = React.useState(false) - - React.useEffect(() => { - const code = localStorage.getItem('irelax_active_coupon') - setSavedCoupon(code) - if (code && subtotal > 0) { - fetch(`${API_BASE}/coupons/validate`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code, cartTotal: subtotal }) - }) - .then(r => r.json()) - .then(data => { - if (data.valid) { - setCouponValid(true) - setDiscountAmount(data.discountAmount || 0) - setIsFreeShipping(data.isFreeShipping || false) - } else { - setCouponValid(false) - } - }) - .catch(() => setCouponValid(false)) - } - }, [subtotal]) - - const removeCoupon = () => { - localStorage.removeItem('irelax_active_coupon') - setSavedCoupon(null) - setDiscountAmount(0) - setIsFreeShipping(false) - setCouponValid(false) - } - - - - return ( -
- - {/* Totals */} -
-
- ราคาสินค้า - {formatPrice(subtotal)} -
- {(discountAmount > 0 || isFreeShipping) && savedCoupon && ( -
- - ส่วนลดคูปอง - - {savedCoupon} - - - - {isFreeShipping ? 'ส่งฟรี' : `- ${formatPrice(discountAmount)}`} -
- )} - {savedCoupon && !couponValid && ( -
- - - คูปอง {savedCoupon} (ยังไม่ถึงยอดขั้นต่ำ) - - -
- )} -
- รวมสุทธิ - {formatPrice(Math.max(0, subtotal - discountAmount))} -
-
- - {/* Checkout button */} - - - ดำเนินการชำระเงิน - - -
- ) -} - diff --git a/apps/ssr/src/components/ClientProviders.tsx b/apps/website/src/components/ClientProviders.tsx similarity index 100% rename from apps/ssr/src/components/ClientProviders.tsx rename to apps/website/src/components/ClientProviders.tsx diff --git a/apps/website/src/components/Footer.tsx b/apps/website/src/components/Footer.tsx index e6b957f..808d723 100644 --- a/apps/website/src/components/Footer.tsx +++ b/apps/website/src/components/Footer.tsx @@ -1,5 +1,6 @@ -import { Link } from 'react-router-dom' -import type { Category } from '../types' +"use client"; +import Link from 'next/link' +import type { Category } from '@homaroma/types' import { useTranslation } from 'react-i18next' interface FooterProps { @@ -30,12 +31,12 @@ export default function Footer({ categories = [] }: FooterProps) {

{t('footer.explore')}

- {t('nav.home')} - {t('nav.shop')} - {t('nav.scentGuide')} - {t('nav.about')} - {t('nav.contact')} - {t('nav.faq')} + {t('nav.home')} + {t('nav.shop')} + {t('nav.scentGuide')} + {t('nav.about')} + {t('nav.contact')} + {t('nav.faq')}
@@ -43,7 +44,7 @@ export default function Footer({ categories = [] }: FooterProps) {

{t('nav.collections')}

{categories.map((c) => ( - + {c.name} ))} diff --git a/apps/website/src/components/Navbar.tsx b/apps/website/src/components/Navbar.tsx index 8eb8800..a1cd190 100644 --- a/apps/website/src/components/Navbar.tsx +++ b/apps/website/src/components/Navbar.tsx @@ -1,10 +1,10 @@ -import { Link, NavLink, useNavigate } from 'react-router-dom' +"use client"; +import Link from 'next/link' +import { useRouter, usePathname } from 'next/navigation' import { useState, useEffect } from 'react' -import type { Category } from '../types' +import type { Category } from '@homaroma/types' import { ShoppingBag, MapPin } from 'lucide-react' import { useCartStore } from '../store/cartStore' -import { useAuthStore } from '../store/authStore' -import { logoutLiff, loginLiff } from '../lib/liff' import { useTranslation } from 'react-i18next' interface NavbarProps { @@ -17,11 +17,11 @@ export default function Navbar({ categories = [] }: NavbarProps) { const [searchQuery, setSearchQuery] = useState('') const [scrolled, setScrolled] = useState(false) const [localOrdersCount, setLocalOrdersCount] = useState(0) - const navigate = useNavigate() + const router = useRouter() + const pathname = usePathname() const { toggleCart, getCartCount } = useCartStore() const cartCount = getCartCount() - const { customer } = useAuthStore() const { t, i18n } = useTranslation() const toggleLanguage = () => { @@ -45,7 +45,7 @@ export default function Navbar({ categories = [] }: NavbarProps) { }, []) // Close mobile menu on route change - useEffect(() => { setMobileOpen(false) }, [navigate]) + useEffect(() => { setMobileOpen(false) }, [pathname]) // Prevent body scroll when overlays open useEffect(() => { document.body.style.overflow = (searchOpen || mobileOpen) ? 'hidden' : '' @@ -55,15 +55,15 @@ export default function Navbar({ categories = [] }: NavbarProps) { const handleSearch = (e: React.FormEvent) => { e.preventDefault() if (searchQuery.trim()) { - navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`) + router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`) setSearchOpen(false) setMobileOpen(false) setSearchQuery('') } } - const navLinkClass = ({ isActive }: { isActive: boolean }) => - isActive ? 'text-cyan-glow' : 'text-slate-300 hover:text-white transition-colors' + const navLinkClass = (path: string) => + pathname === path ? 'text-cyan-glow' : 'text-slate-300 hover:text-white transition-colors' return ( <> @@ -74,15 +74,15 @@ export default function Navbar({ categories = [] }: NavbarProps) { }`} > {/* Logo */} - setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-white uppercase italic shrink-0"> - iRelax Essential + setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-white uppercase italic shrink-0"> + iRelax Essential {/* ── DESKTOP NAV ── */}
- {t('nav.home')} - {t('nav.shop')} - 🔥 {t('nav.promotions') || 'Promotions'} + {t('nav.home')} + {t('nav.shop')} + 🔥 {t('nav.promotions') || 'Promotions'} {/* Collections dropdown */} {categories.length > 0 && ( @@ -98,7 +98,7 @@ export default function Navbar({ categories = [] }: NavbarProps) { {categories.map((cat) => ( {cat.name} @@ -112,14 +112,14 @@ export default function Navbar({ categories = [] }: NavbarProps) {
)} - {t('nav.scentGuide')} - {t('nav.about')} + {t('nav.scentGuide')} + {t('nav.about')}
{/* ── DESKTOP RIGHT ACTIONS ── */}
{t('nav.shopNow')} @@ -165,91 +165,39 @@ export default function Navbar({ categories = [] }: NavbarProps) { > {i18n.language.startsWith('en') ? 'EN' : 'TH'} - - {customer ? ( -
- Profile -
-
-
-

Signed in as

-

{customer.displayName}

-
- -
-
-
- ) : ( - - )}
{/* ── MOBILE RIGHT ACTIONS ── */} -
- {customer ? ( -
- Profile setMobileOpen(true)} - className="w-7 h-7 rounded-full border-2 border-cyan-glow/50 object-cover cursor-pointer" - /> -
- ) : ( - - )} - +
setMobileOpen(false)} - className="relative p-1.5 rounded-full glass hover:bg-white/10 transition-all" + className="relative p-2 rounded-full glass hover:bg-white/10 transition-all" aria-label="Track Orders" > {localOrdersCount > 0 && ( - + )}
@@ -286,9 +234,6 @@ export default function Navbar({ categories = [] }: NavbarProps) {
{t('nav.menu')}
- {customer && ( - - )} @@ -303,7 +248,7 @@ export default function Navbar({ categories = [] }: NavbarProps) { {/* Drawer Nav Links */}
{[ - { to: '/', label: t('nav.home'), end: true }, + { to: '/', label: t('nav.home') }, { to: '/shop', label: t('nav.shop') }, { to: '/coupons', label: `🔥 ${t('nav.promotions') || 'Promotions'}` }, { to: '/scent-guide', label: `✨ ${t('nav.scentGuide')}` }, @@ -311,19 +256,18 @@ export default function Navbar({ categories = [] }: NavbarProps) { { to: '/faq', label: t('nav.faq') }, { to: '/contact', label: t('nav.contact') }, ].map(item => ( - setMobileOpen(false)} - className={({ isActive }) => + className={ `block px-4 py-3 rounded-xl text-sm font-medium tracking-wide transition-all duration-200 ${ - isActive ? 'bg-cyan-glow/10 text-cyan-glow border border-cyan-glow/20' : 'hover:bg-white/5 text-slate-300 hover:text-white' + pathname === item.to ? 'bg-cyan-glow/10 text-cyan-glow border border-cyan-glow/20' : 'hover:bg-white/5 text-slate-300 hover:text-white' }` } > {item.label} - + ))} {/* Collections sub-section */} @@ -333,7 +277,7 @@ export default function Navbar({ categories = [] }: NavbarProps) { {categories.map(cat => ( setMobileOpen(false)} className="flex items-center justify-between px-4 py-2.5 rounded-xl text-sm text-slate-400 hover:text-white hover:bg-white/5 transition-all" > @@ -350,35 +294,19 @@ export default function Navbar({ categories = [] }: NavbarProps) { {/* Drawer Footer CTA */}
setMobileOpen(false)} className="block w-full py-3 bg-white text-night-900 font-bold text-center rounded-xl text-xs uppercase tracking-widest hover:bg-cyan-glow transition-colors duration-300" > {t('nav.shopNow')} setMobileOpen(false)} className="block w-full py-3 glass text-center rounded-xl text-xs font-bold uppercase tracking-widest hover:bg-white/10 transition-colors border border-white/10" > {t('nav.findMyScent')} - - {customer ? ( - - ) : ( - - )}
@@ -428,7 +356,7 @@ export default function Navbar({ categories = [] }: NavbarProps) { ].map(item => ( setSearchOpen(false)} className="glass px-4 py-1.5 rounded-full text-xs font-medium hover:bg-white/10 hover:text-cyan-glow transition-all border border-white/5" > diff --git a/apps/website/src/components/ProductCard.tsx b/apps/website/src/components/ProductCard.tsx index c1e5138..8ccd5be 100644 --- a/apps/website/src/components/ProductCard.tsx +++ b/apps/website/src/components/ProductCard.tsx @@ -1,6 +1,6 @@ -import { Link } from 'react-router-dom' -import type { Product } from '../types' -import { getCategoryImage, formatPrice } from '../types' +import Link from 'next/link' +import type { Product } from '@homaroma/types' +import { getCategoryImage, formatPrice } from '@homaroma/types' interface ProductCardProps { product: Product @@ -11,7 +11,7 @@ export default function ProductCard({ product }: ProductCardProps) {
{product.scent.name} @@ -52,7 +52,7 @@ export default function ProductCard({ product }: ProductCardProps) {
Discover diff --git a/apps/website/src/components/ScrollToTop.tsx b/apps/website/src/components/ScrollToTop.tsx index af21865..86e1187 100644 --- a/apps/website/src/components/ScrollToTop.tsx +++ b/apps/website/src/components/ScrollToTop.tsx @@ -1,12 +1,12 @@ +"use client"; import { useEffect } from 'react' -import { useLocation } from 'react-router-dom' +import { usePathname } from 'next/navigation' /** - * Scrolls window to top on every route change. - * Place this inside so it has access to location context. + * Scrolls window to top on every route change (Next.js version). */ export default function ScrollToTop() { - const { pathname } = useLocation() + const pathname = usePathname() useEffect(() => { window.scrollTo({ top: 0, behavior: 'instant' }) diff --git a/apps/ssr/src/lib/api.ts b/apps/website/src/lib/api.ts similarity index 100% rename from apps/ssr/src/lib/api.ts rename to apps/website/src/lib/api.ts diff --git a/apps/ssr/src/lib/i18n.ts b/apps/website/src/lib/i18n.ts similarity index 100% rename from apps/ssr/src/lib/i18n.ts rename to apps/website/src/lib/i18n.ts diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index 1ffef60..cf9c65d 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -1,7 +1,34 @@ { - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] } diff --git a/serverinfo/post-receive b/serverinfo/post-receive index a27625a..9f46031 100644 --- a/serverinfo/post-receive +++ b/serverinfo/post-receive @@ -5,10 +5,10 @@ # ============================================================================== # 1. TARGET PATHS (Nginx & PM2) -TARGET_WEBSITE="/home/ti/projects/MyNginxMain/nginx/html/homaroma.shop/website" +TARGET_SPA="/home/ti/projects/MyNginxMain/nginx/html/homaroma.shop/spa" TARGET_ADMIN="/home/ti/projects/MyNginxMain/nginx/html/homaroma.shop/admin" TARGET_BACKEND="/home/ti/projects/homaroma-api" -TARGET_SSR="/home/ti/projects/homaroma-ssr" # <-- Next.js SSR App +TARGET_WEBSITE="/home/ti/projects/homaroma-website" # <-- Next.js SSR App # 2. REPOSITORY CONFIG REPO="/home/ti/git/homaroma.git" @@ -19,7 +19,7 @@ TEMP_DIR="/tmp/homaroma_build_$(date +%s)" BUN="/home/ti/.bun/bin/bun" PM2="/home/ti/.nvm/versions/node/v22.17.1/bin/pm2" PM2_NAME_BACKEND="homaroma-backend" -PM2_NAME_SSR="homaroma-ssr" # <-- PM2 process name สำหรับ SSR +PM2_NAME_WEBSITE="homaroma-website" # <-- PM2 process name สำหรับ SSR export PATH="$PATH:/home/ti/.bun/bin:/home/ti/.nvm/versions/node/v22.17.1/bin" # 4. BUILD ENVIRONMENT @@ -45,14 +45,14 @@ cd $TEMP_DIR echo "Installing monorepo dependencies..." $BUN install -# B. Build & Deploy WEBSITE (Frontend SPA) -echo ">>> Building Website..." -cd apps/website +# B. Build & Deploy SPA (Frontend สำหรับ LINE LIFF) +echo ">>> Building SPA..." +cd apps/spa $BUN run build -echo "Deploying Website to $TARGET_WEBSITE..." -mkdir -p $TARGET_WEBSITE -rm -rf $TARGET_WEBSITE/* -cp -rv dist/* $TARGET_WEBSITE/ +echo "Deploying SPA to $TARGET_SPA..." +mkdir -p $TARGET_SPA +rm -rf $TARGET_SPA/* +cp -rv dist/* $TARGET_SPA/ cd ../.. # C. Build & Deploy ADMIN PANEL @@ -107,34 +107,34 @@ $PM2 restart $PM2_NAME_BACKEND 2>/dev/null || \ $PM2 start src/index.ts --name $PM2_NAME_BACKEND --interpreter $BUN echo "✅ PM2 process '$PM2_NAME_BACKEND' is running." -# G. Build & Deploy SSR (Next.js) +# G. Build & Deploy WEBSITE (Next.js SSR) echo ">>> Building Next.js SSR App inside monorepo temp dir..." -cd $TEMP_DIR/apps/ssr +cd $TEMP_DIR/apps/website $BUN run build # build จาก monorepo (workspace:* resolve ได้) # E. Prepare Next.js SSR echo ">>> Preparing Next.js SSR App..." rsync -av --delete \ - $TEMP_DIR/apps/ssr/.next/standalone/ $TARGET_SSR/ + $TEMP_DIR/apps/website/.next/standalone/ $TARGET_WEBSITE/ rsync -av \ - $TEMP_DIR/apps/ssr/public/ $TARGET_SSR/apps/ssr/public/ + $TEMP_DIR/apps/website/public/ $TARGET_WEBSITE/apps/website/public/ rsync -av \ - $TEMP_DIR/apps/ssr/.next/static/ $TARGET_SSR/apps/ssr/.next/static/ + $TEMP_DIR/apps/website/.next/static/ $TARGET_WEBSITE/apps/website/.next/static/ # F. Fix Permissions echo ">>> Fixing Permissions..." -chown -R ti:ti $TARGET_BACKEND $TARGET_SSR +chown -R ti:ti $TARGET_BACKEND $TARGET_WEBSITE # H. PM2 Management - SSR Next.js -cd $TARGET_SSR +cd $TARGET_WEBSITE echo ">>> Creating symlink for .next..." -ln -sf apps/ssr/.next .next -cd apps/ssr -echo ">>> Managing PM2 Process: $PM2_NAME_SSR..." -$PM2 delete $PM2_NAME_SSR 2>/dev/null || true -PORT=$SSR_PORT $PM2 start $BUN --name $PM2_NAME_SSR \ +ln -sf apps/website/.next .next +cd apps/website +echo ">>> Managing PM2 Process: $PM2_NAME_WEBSITE..." +$PM2 delete $PM2_NAME_WEBSITE 2>/dev/null || true +PORT=$SSR_PORT $PM2 start $BUN --name $PM2_NAME_WEBSITE \ -- server.js -echo "✅ PM2 process '$PM2_NAME_SSR' is running on port $SSR_PORT." +echo "✅ PM2 process '$PM2_NAME_WEBSITE' is running on port $SSR_PORT." # I. Cleanup echo "Cleaning up build artifacts..." @@ -143,7 +143,7 @@ rm -rf $TEMP_DIR echo "------- ✅ ALL DEPLOYMENTS COMPLETED SUCCESSFULLY -------" echo "" echo "📋 Service Summary:" -echo " 🌐 Website (SPA) → https://homaroma.shop" +echo " 🌐 Website (SSR) → https://homaroma.shop (PM2: $PM2_NAME_WEBSITE, Port: $SSR_PORT)" +echo " 📱 SPA (LIFF) → https://spa.homaroma.shop" echo " 🔧 Admin Panel → https://admin.homaroma.shop" echo " 🔌 API Backend → https://api.homaroma.shop (PM2: $PM2_NAME_BACKEND)" -echo " 🚀 SSR Next.js → https://ssr.homaroma.shop (PM2: $PM2_NAME_SSR, Port: $SSR_PORT)" diff --git a/serverinfo/spa.homaroma.shop.conf b/serverinfo/spa.homaroma.shop.conf new file mode 100644 index 0000000..98bbd6c --- /dev/null +++ b/serverinfo/spa.homaroma.shop.conf @@ -0,0 +1,33 @@ +server { + listen 80; + server_name spa.homaroma.shop; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name spa.homaroma.shop; + + ssl_certificate /etc/letsencrypt/live/homaroma.shop/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/homaroma.shop/privkey.pem; + + root /home/ti/projects/MyNginxMain/nginx/html/homaroma.shop/spa; + index index.html index.htm; + + location / { + try_files $uri $uri/ /index.html; + } + + # Caching for static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires max; + log_not_found off; + } +}