Mass replace irelax with homaroma across project
This commit is contained in:
1 parent
5e6cb851de
commit
d9c79aa900
57 files changed
+117
-117
No files matched your search
+11
-11
@@ -1,4 +1,4 @@
|
|||||||
# รายละเอียดโปรเจกต์ iRelax Aroma
|
# รายละเอียดโปรเจกต์ HomAroma
|
||||||
|
|
||||||
โปรเจกต์นี้เป็นระบบ E-commerce สำหรับผลิตภัณฑ์เครื่องหอมและน้ำมันหอมระเหย (Aroma & Essential Oils) โดยมีการออกแบบสถาปัตยกรรมแบบ Monorepo ที่แบ่งส่วนการทำงานออกเป็น 4 ส่วนหลักในโฟลเดอร์ `apps/` ดังนี้:
|
โปรเจกต์นี้เป็นระบบ E-commerce สำหรับผลิตภัณฑ์เครื่องหอมและน้ำมันหอมระเหย (Aroma & Essential Oils) โดยมีการออกแบบสถาปัตยกรรมแบบ Monorepo ที่แบ่งส่วนการทำงานออกเป็น 4 ส่วนหลักในโฟลเดอร์ `apps/` ดังนี้:
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ graph TD
|
|||||||
git push server master
|
git push server master
|
||||||
```
|
```
|
||||||
* **Remote Name:** `server`
|
* **Remote Name:** `server`
|
||||||
* **Remote URL:** `ti@kitizone.com:/home/ti/git/irelax.git`
|
* **Remote URL:** `ti@kitizone.com:/home/ti/git/homaroma.git`
|
||||||
|
|
||||||
### ขั้นตอนการทำงานของสคริปต์ Deployment (`post-receive`)
|
### ขั้นตอนการทำงานของสคริปต์ Deployment (`post-receive`)
|
||||||
เมื่อมีการ Push โค้ด สคริปต์จะทำงานโดยอัตโนมัติดังนี้:
|
เมื่อมีการ Push โค้ด สคริปต์จะทำงานโดยอัตโนมัติดังนี้:
|
||||||
@@ -86,21 +86,21 @@ git push server master
|
|||||||
1. **Checkout & Build**: ดึงโค้ดล่าสุดมายังโฟลเดอร์ชั่วคราวและรัน `bun install`
|
1. **Checkout & Build**: ดึงโค้ดล่าสุดมายังโฟลเดอร์ชั่วคราวและรัน `bun install`
|
||||||
2. **Frontend Deployment**:
|
2. **Frontend Deployment**:
|
||||||
* Build `website` และ `admin` (Vite)
|
* Build `website` และ `admin` (Vite)
|
||||||
* Deploy ไฟล์ `dist` ไปยัง Nginx paths (`/home/ti/projects/MyNginxMain/nginx/html/irelaxaroma.shop/`)
|
* Deploy ไฟล์ `dist` ไปยัง Nginx paths (`/home/ti/projects/MyNginxMain/nginx/html/homaroma.shop/`)
|
||||||
3. **Backend Deployment**:
|
3. **Backend Deployment**:
|
||||||
* Sync โค้ดไปยัง `/home/ti/projects/irelax-api`
|
* Sync โค้ดไปยัง `/home/ti/projects/homaroma-api`
|
||||||
* รัน `prisma generate` และ `prisma migrate deploy` เพื่ออัปเดตฐานข้อมูล
|
* รัน `prisma generate` และ `prisma migrate deploy` เพื่ออัปเดตฐานข้อมูล
|
||||||
* Restart บริการด้วย **PM2** (ชื่อ process: `irelax-backend`)
|
* Restart บริการด้วย **PM2** (ชื่อ process: `homaroma-backend`)
|
||||||
4. **SSR Deployment**:
|
4. **SSR Deployment**:
|
||||||
* Build Next.js app และ Sync ไปยัง `/home/ti/projects/irelax-ssr`
|
* Build Next.js app และ Sync ไปยัง `/home/ti/projects/homaroma-ssr`
|
||||||
* Restart บริการด้วย **PM2** (ชื่อ process: `irelax-ssr`) บนพอร์ต `3001`
|
* Restart บริการด้วย **PM2** (ชื่อ process: `homaroma-ssr`) บนพอร์ต `3001`
|
||||||
5. **Cleanup**: ลบไฟล์ชั่วคราวที่ใช้ในการ Build ออก
|
5. **Cleanup**: ลบไฟล์ชั่วคราวที่ใช้ในการ Build ออก
|
||||||
|
|
||||||
### สรุป URL การเข้าใช้งาน (Production)
|
### สรุป URL การเข้าใช้งาน (Production)
|
||||||
* **Website (SPA):** [https://irelaxaroma.shop](https://irelaxaroma.shop)
|
* **Website (SPA):** [https://homaroma.shop](https://homaroma.shop)
|
||||||
* **Admin Panel:** [https://admin.irelaxaroma.shop](https://admin.irelaxaroma.shop)
|
* **Admin Panel:** [https://admin.homaroma.shop](https://admin.homaroma.shop)
|
||||||
* **API Backend:** [https://api.irelaxaroma.shop](https://api.irelaxaroma.shop)
|
* **API Backend:** [https://api.homaroma.shop](https://api.homaroma.shop)
|
||||||
* **SSR Next.js:** [https://ssr.irelaxaroma.shop](https://ssr.irelaxaroma.shop)
|
* **SSR Next.js:** [https://ssr.homaroma.shop](https://ssr.homaroma.shop)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Navigate, Outlet } from 'react-router-dom'
|
import { Navigate, Outlet } from 'react-router-dom'
|
||||||
|
|
||||||
export default function ProtectedRoute() {
|
export default function ProtectedRoute() {
|
||||||
const token = localStorage.getItem('irelax_admin_token')
|
const token = localStorage.getItem('homaroma_admin_token')
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
// If no token, redirect to login page
|
// If no token, redirect to login page
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export default function Sidebar() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('irelax_admin_token')
|
localStorage.removeItem('homaroma_admin_token')
|
||||||
localStorage.removeItem('irelax_admin_user')
|
localStorage.removeItem('homaroma_admin_user')
|
||||||
navigate('/login')
|
navigate('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ export default function Sidebar() {
|
|||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<div className="text-xl font-bold tracking-widest text-white uppercase italic">
|
<div className="text-xl font-bold tracking-widest text-white uppercase italic">
|
||||||
iRelax <span className="text-cyan-glow text-xs not-italic font-light opacity-60">Admin</span>
|
HomAroma <span className="text-cyan-glow text-xs not-italic font-light opacity-60">Admin</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* iRelax Admin - Professional Dark Palette */
|
/* HomAroma Admin - Professional Dark Palette */
|
||||||
--color-night-950: #050608;
|
--color-night-950: #050608;
|
||||||
--color-night-900: #0B0C10;
|
--color-night-900: #0B0C10;
|
||||||
--color-night-800: #1F2833;
|
--color-night-800: #1F2833;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function DashboardPage() {
|
|||||||
<div className="flex justify-between items-end">
|
<div className="flex justify-between items-end">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="text-3xl font-bold">System Overview</h1>
|
<h1 className="text-3xl font-bold">System Overview</h1>
|
||||||
<p className="text-slate-500 text-sm">Real-time stats from the iRelax platform.</p>
|
<p className="text-slate-500 text-sm">Real-time stats from the HomAroma platform.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-xs uppercase tracking-widest text-slate-500">Last updated</p>
|
<p className="text-xs uppercase tracking-widest text-slate-500">Last updated</p>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const PRESETS: { key: Preset; label: string }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token')}` };
|
||||||
}
|
}
|
||||||
function fmt(val: number, dec = 0) {
|
function fmt(val: number, dec = 0) {
|
||||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replac
|
|||||||
const COLORS = ['#3b82f6', '#8b5cf6', '#ec4899', '#eab308', '#f97316', '#22d3ee', '#10b981'];
|
const COLORS = ['#3b82f6', '#8b5cf6', '#ec4899', '#eab308', '#f97316', '#22d3ee', '#10b981'];
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token')}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmt(val: number, decimals = 0) {
|
function fmt(val: number, decimals = 0) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const LAZADA_SHOPS = [
|
|||||||
{ id: 'hom-aroma', label: 'Hom Aroma', color: 'text-blue-500', activeBg: 'bg-blue-500/15', activeBorder: 'border-blue-500/40' },
|
{ id: 'hom-aroma', label: 'Hom Aroma', color: 'text-blue-500', activeBg: 'bg-blue-500/15', activeBorder: 'border-blue-500/40' },
|
||||||
{ id: 'shop-01', label: 'Annlina', color: 'text-indigo-400', activeBg: 'bg-indigo-500/15', activeBorder: 'border-indigo-500/40' },
|
{ id: 'shop-01', label: 'Annlina', color: 'text-indigo-400', activeBg: 'bg-indigo-500/15', activeBorder: 'border-indigo-500/40' },
|
||||||
{ id: 'pakasai', label: 'Pakasai', color: 'text-pink-400', activeBg: 'bg-pink-500/15', activeBorder: 'border-pink-500/40' },
|
{ id: 'pakasai', label: 'Pakasai', color: 'text-pink-400', activeBg: 'bg-pink-500/15', activeBorder: 'border-pink-500/40' },
|
||||||
{ id: 'irelaxshop', label: 'Relax', color: 'text-yellow-400', activeBg: 'bg-yellow-500/15', activeBorder: 'border-yellow-500/40' },
|
{ id: 'homaromashop', label: 'Relax', color: 'text-yellow-400', activeBg: 'bg-yellow-500/15', activeBorder: 'border-yellow-500/40' },
|
||||||
{ id: 'conner', label: 'Conner', color: 'text-white', activeBg: 'bg-white/15', activeBorder: 'border-white/40' },
|
{ id: 'conner', label: 'Conner', color: 'text-white', activeBg: 'bg-white/15', activeBorder: 'border-white/40' },
|
||||||
{ id: 'december', label: 'December', color: 'text-white', activeBg: 'bg-white/15', activeBorder: 'border-white/40' },
|
{ id: 'december', label: 'December', color: 'text-white', activeBg: 'bg-white/15', activeBorder: 'border-white/40' },
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export const LAZADA_SHOPS = [
|
|||||||
function getShop(id: string) { return LAZADA_SHOPS.find(s => s.id === id) || LAZADA_SHOPS[0]; }
|
function getShop(id: string) { return LAZADA_SHOPS.find(s => s.id === id) || LAZADA_SHOPS[0]; }
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token')}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Summary Cards ─────────────────────────────────────────────────────────────
|
// ── Summary Cards ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const SHOP_HEX: Record<string, string> = {
|
|||||||
'hom-aroma': '#3b82f6', // blue-500
|
'hom-aroma': '#3b82f6', // blue-500
|
||||||
'shop-01': '#818cf8', // indigo-400
|
'shop-01': '#818cf8', // indigo-400
|
||||||
'pakasai': '#f472b6', // pink-400
|
'pakasai': '#f472b6', // pink-400
|
||||||
'irelaxshop':'#facc15', // yellow-400
|
'homaromashop':'#facc15', // yellow-400
|
||||||
'conner': '#94a3b8', // slate-400
|
'conner': '#94a3b8', // slate-400
|
||||||
'december': '#cbd5e1', // slate-300
|
'december': '#cbd5e1', // slate-300
|
||||||
};
|
};
|
||||||
@@ -59,7 +59,7 @@ const PRESETS: { key: Preset; label: string }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token')}` };
|
||||||
}
|
}
|
||||||
function fmt(val: number, dec = 0) {
|
function fmt(val: number, dec = 0) {
|
||||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store token
|
// Store token
|
||||||
localStorage.setItem('irelax_admin_token', data.token)
|
localStorage.setItem('homaroma_admin_token', data.token)
|
||||||
localStorage.setItem('irelax_admin_user', JSON.stringify(data.user))
|
localStorage.setItem('homaroma_admin_user', JSON.stringify(data.user))
|
||||||
|
|
||||||
// Redirect to dashboard
|
// Redirect to dashboard
|
||||||
navigate('/')
|
navigate('/')
|
||||||
@@ -53,7 +53,7 @@ export default function LoginPage() {
|
|||||||
Authorized Access Only
|
Authorized Access Only
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl font-bold tracking-tighter text-white">
|
<h1 className="text-4xl font-bold tracking-tighter text-white">
|
||||||
iRelax <span className="text-cyan-glow">Admin</span>
|
HomAroma <span className="text-cyan-glow">Admin</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-slate-500 text-sm">Welcome back to the sensory command center.</p>
|
<p className="text-slate-500 text-sm">Welcome back to the sensory command center.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,7 +115,7 @@ export default function LoginPage() {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="text-center text-[10px] text-slate-600 uppercase tracking-widest">
|
<p className="text-center text-[10px] text-slate-600 uppercase tracking-widest">
|
||||||
© 2026 iRelax Aroma & Essential Oils
|
© 2026 HomAroma & Essential Oils
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replac
|
|||||||
const COLORS = ['#22d3ee', '#4ade80', '#f97316', '#818cf8', '#f472b6', '#facc15', '#fb923c'];
|
const COLORS = ['#22d3ee', '#4ade80', '#f97316', '#818cf8', '#f472b6', '#facc15', '#fb923c'];
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token')}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmt(val: number, decimals = 0) {
|
function fmt(val: number, decimals = 0) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replac
|
|||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return {
|
return {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}`,
|
Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token')}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const PRESETS: { key: Preset; label: string }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token')}` };
|
||||||
}
|
}
|
||||||
function fmt(val: number, dec = 0) {
|
function fmt(val: number, dec = 0) {
|
||||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replac
|
|||||||
// ── Shop Config ───────────────────────────────────────────────────────────────
|
// ── Shop Config ───────────────────────────────────────────────────────────────
|
||||||
export const SHOPEE_SHOPS = [
|
export const SHOPEE_SHOPS = [
|
||||||
{ id: 'hom-aroma', label: 'Hom Aroma', color: 'text-orange-400', bg: 'bg-orange-500/15', border: 'border-orange-500/20', activeBg: 'bg-orange-500/20', activeBorder: 'border-orange-500/30' },
|
{ id: 'hom-aroma', label: 'Hom Aroma', color: 'text-orange-400', bg: 'bg-orange-500/15', border: 'border-orange-500/20', activeBg: 'bg-orange-500/20', activeBorder: 'border-orange-500/30' },
|
||||||
{ id: 'shop-01', label: 'iRelax', color: 'text-violet-400', bg: 'bg-violet-500/15', border: 'border-violet-500/20', activeBg: 'bg-violet-500/20', activeBorder: 'border-violet-500/30' },
|
{ id: 'shop-01', label: 'HomAroma', color: 'text-violet-400', bg: 'bg-violet-500/15', border: 'border-violet-500/20', activeBg: 'bg-violet-500/20', activeBorder: 'border-violet-500/30' },
|
||||||
{ id: 'shop-02', label: 'Shop-02', color: 'text-sky-400', bg: 'bg-sky-500/15', border: 'border-sky-500/20', activeBg: 'bg-sky-500/20', activeBorder: 'border-sky-500/30' },
|
{ id: 'shop-02', label: 'Shop-02', color: 'text-sky-400', bg: 'bg-sky-500/15', border: 'border-sky-500/20', activeBg: 'bg-sky-500/20', activeBorder: 'border-sky-500/30' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ function getShop(id: string) { return SHOPEE_SHOPS.find(s => s.id === id) ?? SHO
|
|||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return {
|
return {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}`,
|
Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token')}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function formatCurrency(val: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('homaroma_admin_token')}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLORS = ['#22d3ee', '#818cf8', '#c084fc', '#f472b6', '#fb923c', '#facc15', '#4ade80'];
|
const COLORS = ['#22d3ee', '#818cf8', '#c084fc', '#f472b6', '#fb923c', '#facc15', '#4ade80'];
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replac
|
|||||||
const COLORS = ['#22d3ee', '#4ade80', '#f97316', '#818cf8', '#f472b6', '#facc15', '#fb923c'];
|
const COLORS = ['#22d3ee', '#4ade80', '#f97316', '#818cf8', '#f472b6', '#facc15', '#fb923c'];
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token')}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmt(val: number, decimals = 0) {
|
function fmt(val: number, decimals = 0) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const TIKTOK_SHOPS = [
|
|||||||
function getShop(id: string) { return TIKTOK_SHOPS.find(s => s.id === id) ?? TIKTOK_SHOPS[0]; }
|
function getShop(id: string) { return TIKTOK_SHOPS.find(s => s.id === id) ?? TIKTOK_SHOPS[0]; }
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token')}` };
|
||||||
}
|
}
|
||||||
function fmt(val: number) {
|
function fmt(val: number) {
|
||||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(val);
|
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(val);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const PRESETS: { key: Preset; label: string }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token')}` };
|
||||||
}
|
}
|
||||||
function fmt(val: number, dec = 0) {
|
function fmt(val: number, dec = 0) {
|
||||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function formatCurrency(val: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('homaroma_admin_token')}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── TSV parser: handles quoted fields (embedded tabs/newlines) + skip rows ────
|
// ─── TSV parser: handles quoted fields (embedded tabs/newlines) + skip rows ────
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "iRelax API — Elysia.js + Prisma 7",
|
"description": "HomAroma API — Elysia.js + Prisma 7",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ model StoreSettings {
|
|||||||
id Int @id @default(1)
|
id Int @id @default(1)
|
||||||
shippingFee Float @default(35)
|
shippingFee Float @default(35)
|
||||||
bankName String @default("KASIKORNBANK (KBANK)")
|
bankName String @default("KASIKORNBANK (KBANK)")
|
||||||
bankAccountName String @default("iRelax Aroma Co., Ltd.")
|
bankAccountName String @default("HomAroma Co., Ltd.")
|
||||||
bankAccountNumber String @default("012-3-45678-9")
|
bankAccountNumber String @default("012-3-45678-9")
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 0. Seed Admin User
|
// 0. Seed Admin User
|
||||||
const adminPassword = await bcrypt.hash('irelax123', 10);
|
const adminPassword = await bcrypt.hash('homaroma123', 10);
|
||||||
await prisma.user.upsert({
|
await prisma.user.upsert({
|
||||||
where: { username: 'admin' },
|
where: { username: 'admin' },
|
||||||
update: { password: adminPassword },
|
update: { password: adminPassword },
|
||||||
@@ -96,7 +96,7 @@ async function main() {
|
|||||||
role: 'ADMIN'
|
role: 'ADMIN'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
process.stdout.write('✅ Seeded Admin User (admin / irelax123).\n');
|
process.stdout.write('✅ Seeded Admin User (admin / homaroma123).\n');
|
||||||
|
|
||||||
// 1. Seed Scents & Categories
|
// 1. Seed Scents & Categories
|
||||||
const scents = [];
|
const scents = [];
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const authRoute = new Elysia({ prefix: "/auth" })
|
|||||||
.use(
|
.use(
|
||||||
jwt({
|
jwt({
|
||||||
name: "jwt",
|
name: "jwt",
|
||||||
secret: process.env.JWT_SECRET || "irelax_secret_2026_scent",
|
secret: process.env.JWT_SECRET || "homaroma_secret_2026_scent",
|
||||||
exp: "24h",
|
exp: "24h",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: ()
|
|||||||
const [couponValid, setCouponValid] = React.useState<boolean>(false)
|
const [couponValid, setCouponValid] = React.useState<boolean>(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const code = localStorage.getItem('irelax_active_coupon')
|
const code = localStorage.getItem('homaroma_active_coupon')
|
||||||
setSavedCoupon(code)
|
setSavedCoupon(code)
|
||||||
if (code && subtotal > 0) {
|
if (code && subtotal > 0) {
|
||||||
fetch(`${API_BASE}/coupons/validate`, {
|
fetch(`${API_BASE}/coupons/validate`, {
|
||||||
@@ -149,7 +149,7 @@ function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: ()
|
|||||||
}, [subtotal])
|
}, [subtotal])
|
||||||
|
|
||||||
const removeCoupon = () => {
|
const removeCoupon = () => {
|
||||||
localStorage.removeItem('irelax_active_coupon')
|
localStorage.removeItem('homaroma_active_coupon')
|
||||||
setSavedCoupon(null)
|
setSavedCoupon(null)
|
||||||
setDiscountAmount(0)
|
setDiscountAmount(0)
|
||||||
setIsFreeShipping(false)
|
setIsFreeShipping(false)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function Footer({ categories = [] }: FooterProps) {
|
|||||||
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-12">
|
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-12">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-2xl font-bold tracking-widest">
|
<div className="text-2xl font-bold tracking-widest">
|
||||||
iRelax <span className="text-cyan-glow text-sm font-light italic">Essential</span>
|
HomAroma <span className="text-cyan-glow text-sm font-light italic">Essential</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-slate-500 text-sm leading-relaxed">
|
<p className="text-slate-500 text-sm leading-relaxed">
|
||||||
{t('footer.catchphrase')}
|
{t('footer.catchphrase')}
|
||||||
@@ -64,7 +64,7 @@ export default function Footer({ categories = [] }: FooterProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-slate-600 uppercase tracking-widest">
|
<p className="text-[10px] text-slate-600 uppercase tracking-widest">
|
||||||
© {new Date().getFullYear()} iRelax Essential. {t('footer.rights')}
|
© {new Date().getFullYear()} HomAroma. {t('footer.rights')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
|||||||
const toggleLanguage = () => {
|
const toggleLanguage = () => {
|
||||||
const newLang = i18n.language.startsWith('en') ? 'th' : 'en'
|
const newLang = i18n.language.startsWith('en') ? 'th' : 'en'
|
||||||
i18n.changeLanguage(newLang)
|
i18n.changeLanguage(newLang)
|
||||||
localStorage.setItem('irelax_lang', newLang)
|
localStorage.setItem('homaroma_lang', newLang)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect scroll for navbar bg change
|
// Detect scroll for navbar bg change
|
||||||
@@ -36,7 +36,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
|||||||
window.addEventListener('scroll', onScroll, { passive: true })
|
window.addEventListener('scroll', onScroll, { passive: true })
|
||||||
|
|
||||||
// Check local orders periodically (or just on mount)
|
// Check local orders periodically (or just on mount)
|
||||||
const stored = localStorage.getItem('irelax_local_orders')
|
const stored = localStorage.getItem('homaroma_local_orders')
|
||||||
if (stored) {
|
if (stored) {
|
||||||
try { setLocalOrdersCount(JSON.parse(stored).length) } catch (e) {}
|
try { setLocalOrdersCount(JSON.parse(stored).length) } catch (e) {}
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
|||||||
>
|
>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-white uppercase italic shrink-0">
|
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-white uppercase italic shrink-0">
|
||||||
iRelax <span className="hidden md:inline text-cyan-glow text-xs md:text-sm not-italic font-light opacity-60 ml-1">Essential</span>
|
HomAroma <span className="hidden md:inline text-cyan-glow text-xs md:text-sm not-italic font-light opacity-60 ml-1">Essential</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* ── DESKTOP NAV ── */}
|
{/* ── DESKTOP NAV ── */}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import enTranslation from './locales/en/translation.json'
|
|||||||
import thTranslation from './locales/th/translation.json'
|
import thTranslation from './locales/th/translation.json'
|
||||||
|
|
||||||
// Retrieve default lang from localStorage if set
|
// Retrieve default lang from localStorage if set
|
||||||
const savedLang = localStorage.getItem('irelax_lang') || 'th'
|
const savedLang = localStorage.getItem('homaroma_lang') || 'th'
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function AboutPage() {
|
|||||||
Born from a passion for <span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-glow to-night-700 italic font-serif">stillness</span>
|
Born from a passion for <span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-glow to-night-700 italic font-serif">stillness</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-slate-300 text-lg leading-relaxed">
|
<p className="text-slate-300 text-lg leading-relaxed">
|
||||||
iRelax began in 2019 as a one-person studio in Bangkok, crafting small batches of botanical aroma blends for friends and family. What started as a personal journey toward calm became a philosophy — that our environment shapes how we feel, think, and rest.
|
HomAroma began in 2019 as a one-person studio in Bangkok, crafting small batches of botanical aroma blends for friends and family. What started as a personal journey toward calm became a philosophy — that our environment shapes how we feel, think, and rest.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -81,7 +81,7 @@ export default function AboutPage() {
|
|||||||
We believe that scent is the fastest pathway to emotional transformation. A single breath of the right blend can dissolve anxiety, sharpen focus, or unlock creativity. Our mission is to make that transformation accessible — through products that are as beautiful as they are effective.
|
We believe that scent is the fastest pathway to emotional transformation. A single breath of the right blend can dissolve anxiety, sharpen focus, or unlock creativity. Our mission is to make that transformation accessible — through products that are as beautiful as they are effective.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-slate-400 leading-relaxed text-sm">
|
<p className="text-slate-400 leading-relaxed text-sm">
|
||||||
Every iRelax product is designed with intention: the glass bottles are reusable, the packaging is recycled, and the formulas are written to be understood by anyone — not hidden behind chemical jargon.
|
Every HomAroma product is designed with intention: the glass bottles are reusable, the packaging is recycled, and the formulas are written to be understood by anyone — not hidden behind chemical jargon.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-80 rounded-2xl overflow-hidden glass border border-white/5">
|
<div className="relative h-80 rounded-2xl overflow-hidden glass border border-white/5">
|
||||||
@@ -89,7 +89,7 @@ export default function AboutPage() {
|
|||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="text-center space-y-3">
|
<div className="text-center space-y-3">
|
||||||
<div className="text-7xl">🌿</div>
|
<div className="text-7xl">🌿</div>
|
||||||
<p className="text-white/30 text-sm uppercase tracking-widest">iRelax Studio, Bangkok</p>
|
<p className="text-white/30 text-sm uppercase tracking-widest">HomAroma Studio, Bangkok</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function CheckoutPage() {
|
|||||||
.then(setCategories)
|
.then(setCategories)
|
||||||
|
|
||||||
// Check if there's an active coupon saved in localStorage
|
// Check if there's an active coupon saved in localStorage
|
||||||
const savedCoupon = localStorage.getItem('irelax_active_coupon')
|
const savedCoupon = localStorage.getItem('homaroma_active_coupon')
|
||||||
if (savedCoupon) {
|
if (savedCoupon) {
|
||||||
setCouponCode(savedCoupon)
|
setCouponCode(savedCoupon)
|
||||||
// We will trigger handleApplyCoupon but we need to wait for items/subtotal to be ready.
|
// We will trigger handleApplyCoupon but we need to wait for items/subtotal to be ready.
|
||||||
@@ -80,7 +80,7 @@ export default function CheckoutPage() {
|
|||||||
|
|
||||||
// Auto-apply saved coupon if subtotal is ready
|
// Auto-apply saved coupon if subtotal is ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedCoupon = localStorage.getItem('irelax_active_coupon')
|
const savedCoupon = localStorage.getItem('homaroma_active_coupon')
|
||||||
if (savedCoupon && subtotal > 0 && !appliedCoupon && !couponError && !couponLoading) {
|
if (savedCoupon && subtotal > 0 && !appliedCoupon && !couponError && !couponLoading) {
|
||||||
setCouponCode(savedCoupon)
|
setCouponCode(savedCoupon)
|
||||||
// trigger fetch to validate
|
// trigger fetch to validate
|
||||||
@@ -96,7 +96,7 @@ export default function CheckoutPage() {
|
|||||||
setDiscountAmount(data.discountAmount)
|
setDiscountAmount(data.discountAmount)
|
||||||
setIsFreeShipping(data.isFreeShipping)
|
setIsFreeShipping(data.isFreeShipping)
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('irelax_active_coupon') // Remove invalid
|
localStorage.removeItem('homaroma_active_coupon') // Remove invalid
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
@@ -160,7 +160,7 @@ export default function CheckoutPage() {
|
|||||||
setDiscountAmount(0)
|
setDiscountAmount(0)
|
||||||
setIsFreeShipping(false)
|
setIsFreeShipping(false)
|
||||||
setCouponError('')
|
setCouponError('')
|
||||||
localStorage.removeItem('irelax_active_coupon')
|
localStorage.removeItem('homaroma_active_coupon')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
@@ -200,10 +200,10 @@ export default function CheckoutPage() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
clearCart()
|
clearCart()
|
||||||
|
|
||||||
const existingStr = localStorage.getItem('irelax_local_orders')
|
const existingStr = localStorage.getItem('homaroma_local_orders')
|
||||||
const existing = existingStr ? JSON.parse(existingStr) : []
|
const existing = existingStr ? JSON.parse(existingStr) : []
|
||||||
if (!existing.includes(responseData.id)) {
|
if (!existing.includes(responseData.id)) {
|
||||||
localStorage.setItem('irelax_local_orders', JSON.stringify([responseData.id, ...existing]))
|
localStorage.setItem('homaroma_local_orders', JSON.stringify([responseData.id, ...existing]))
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(`/order/${responseData.id}`)
|
navigate(`/order/${responseData.id}`)
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ export default function ContactPage() {
|
|||||||
<h2 className="font-bold text-xl mb-6">Reach Us Directly</h2>
|
<h2 className="font-bold text-xl mb-6">Reach Us Directly</h2>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{[
|
{[
|
||||||
{ icon: '📧', label: 'Email', value: 'hello@irelax.co.th', href: 'mailto:hello@irelax.co.th' },
|
{ icon: '📧', label: 'Email', value: 'hello@homaroma.shop', href: 'mailto:hello@homaroma.shop' },
|
||||||
{ icon: '📱', label: 'LINE Official', value: '@irelaxessential', href: '#' },
|
{ icon: '📱', label: 'LINE Official', value: '@homaroma', href: '#' },
|
||||||
{ icon: '📸', label: 'Instagram', value: '@irelax.essential', href: '#' },
|
{ icon: '📸', label: 'Instagram', value: '@homaroma', href: '#' },
|
||||||
{ icon: '📍', label: 'Studio', value: 'Bangkok, Thailand', href: '#' },
|
{ icon: '📍', label: 'Studio', value: 'Bangkok, Thailand', href: '#' },
|
||||||
].map(item => (
|
].map(item => (
|
||||||
<a key={item.label} href={item.href} className="flex items-start gap-4 group">
|
<a key={item.label} href={item.href} className="flex items-start gap-4 group">
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function CouponsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleUseNow = (code: string) => {
|
const handleUseNow = (code: string) => {
|
||||||
localStorage.setItem('irelax_active_coupon', code)
|
localStorage.setItem('homaroma_active_coupon', code)
|
||||||
navigate('/shop')
|
navigate('/shop')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const FAQS = [
|
|||||||
category: 'Products',
|
category: 'Products',
|
||||||
icon: '🌿',
|
icon: '🌿',
|
||||||
items: [
|
items: [
|
||||||
{ q: 'What are the ingredients in your aroma products?', a: 'All iRelax products are crafted from 100% natural botanical extracts, pure essential oils, and pharmaceutical-grade carrier bases. We never use synthetic fragrances, parabens, or harmful chemicals. Each product label lists full ingredient transparency.' },
|
{ q: 'What are the ingredients in your aroma products?', a: 'All HomAroma products are crafted from 100% natural botanical extracts, pure essential oils, and pharmaceutical-grade carrier bases. We never use synthetic fragrances, parabens, or harmful chemicals. Each product label lists full ingredient transparency.' },
|
||||||
{ q: 'How long does each product last?', a: 'Our Reed Diffusers last 4–8 weeks depending on room size and airflow. Aroma Oils (30ml) last approximately 3–4 months with normal use. Room Sprays (100ml) provide 60–80 uses. Car Diffusers cartridges last up to 45 days.' },
|
{ q: 'How long does each product last?', a: 'Our Reed Diffusers last 4–8 weeks depending on room size and airflow. Aroma Oils (30ml) last approximately 3–4 months with normal use. Room Sprays (100ml) provide 60–80 uses. Car Diffusers cartridges last up to 45 days.' },
|
||||||
{ q: 'Are your products suitable for sensitive skin or allergies?', a: 'We recommend patch testing before topical use. Our Aroma Oils are formulated for diffuser use primarily. If you have known fragrance allergies, consult our scent guide or contact us with your specific concerns — we are happy to help identify suitable options.' },
|
{ q: 'Are your products suitable for sensitive skin or allergies?', a: 'We recommend patch testing before topical use. Our Aroma Oils are formulated for diffuser use primarily. If you have known fragrance allergies, consult our scent guide or contact us with your specific concerns — we are happy to help identify suitable options.' },
|
||||||
{ q: 'What is the difference between Aroma Oil and Oil Ozone?', a: 'Aroma Oils are pure essential oil blends designed for aromatherapy diffusers and therapeutic use. Oil Ozone is our premium line featuring ozone-infused botanical extracts — designed for air purification and deeper space transformation, ideal for large rooms and commercial spaces.' },
|
{ q: 'What is the difference between Aroma Oil and Oil Ozone?', a: 'Aroma Oils are pure essential oil blends designed for aromatherapy diffusers and therapeutic use. Oil Ozone is our premium line featuring ozone-infused botanical extracts — designed for air purification and deeper space transformation, ideal for large rooms and commercial spaces.' },
|
||||||
@@ -29,7 +29,7 @@ const FAQS = [
|
|||||||
items: [
|
items: [
|
||||||
{ q: 'How long does shipping take?', a: 'Standard delivery within Bangkok takes 1–2 business days. Provincial orders take 2–4 business days. Express same-day delivery is available in Bangkok for orders placed before 12:00 PM.' },
|
{ q: 'How long does shipping take?', a: 'Standard delivery within Bangkok takes 1–2 business days. Provincial orders take 2–4 business days. Express same-day delivery is available in Bangkok for orders placed before 12:00 PM.' },
|
||||||
{ q: 'Do you ship internationally?', a: 'Currently we ship within Thailand only. International shipping is planned for late 2025. Sign up for our newsletter to be notified when we expand to your region.' },
|
{ q: 'Do you ship internationally?', a: 'Currently we ship within Thailand only. International shipping is planned for late 2025. Sign up for our newsletter to be notified when we expand to your region.' },
|
||||||
{ q: 'What is your return policy?', a: 'We accept returns within 14 days of purchase for unopened, undamaged products in original packaging. For defective or damaged products, we offer full replacement at no cost. Contact us at hello@irelax.co.th with your order number.' },
|
{ q: 'What is your return policy?', a: 'We accept returns within 14 days of purchase for unopened, undamaged products in original packaging. For defective or damaged products, we offer full replacement at no cost. Contact us at hello@homaroma.shop with your order number.' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -64,7 +64,7 @@ export default function FaqPage() {
|
|||||||
<p className="text-cyan-glow uppercase tracking-[0.4em] text-xs font-semibold opacity-70">Support</p>
|
<p className="text-cyan-glow uppercase tracking-[0.4em] text-xs font-semibold opacity-70">Support</p>
|
||||||
<h1 className="text-5xl font-bold">Frequently Asked Questions</h1>
|
<h1 className="text-5xl font-bold">Frequently Asked Questions</h1>
|
||||||
<p className="text-slate-400 max-w-lg mx-auto">
|
<p className="text-slate-400 max-w-lg mx-auto">
|
||||||
Everything you need to know about iRelax products, shipping, and care.
|
Everything you need to know about HomAroma products, shipping, and care.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function ShopPage() {
|
|||||||
const params = new URLSearchParams(location.search)
|
const params = new URLSearchParams(location.search)
|
||||||
const coupon = params.get('coupon')
|
const coupon = params.get('coupon')
|
||||||
if (coupon) {
|
if (coupon) {
|
||||||
localStorage.setItem('irelax_active_coupon', coupon)
|
localStorage.setItem('homaroma_active_coupon', coupon)
|
||||||
// Optionally clean URL
|
// Optionally clean URL
|
||||||
window.history.replaceState({}, document.title, window.location.pathname)
|
window.history.replaceState({}, document.title, window.location.pathname)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function TrackOrderPage() {
|
|||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
|
|
||||||
// Load local orders
|
// Load local orders
|
||||||
const storedStr = localStorage.getItem('irelax_local_orders')
|
const storedStr = localStorage.getItem('homaroma_local_orders')
|
||||||
if (storedStr) {
|
if (storedStr) {
|
||||||
setLocalOrders(JSON.parse(storedStr))
|
setLocalOrders(JSON.parse(storedStr))
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ export default function TrackOrderPage() {
|
|||||||
if (!localOrders.includes(data.orderId)) {
|
if (!localOrders.includes(data.orderId)) {
|
||||||
const newOrders = [data.orderId, ...localOrders]
|
const newOrders = [data.orderId, ...localOrders]
|
||||||
setLocalOrders(newOrders)
|
setLocalOrders(newOrders)
|
||||||
localStorage.setItem('irelax_local_orders', JSON.stringify(newOrders))
|
localStorage.setItem('homaroma_local_orders', JSON.stringify(newOrders))
|
||||||
}
|
}
|
||||||
navigate(`/order/${data.orderId}`)
|
navigate(`/order/${data.orderId}`)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
logout: () => set({ token: null, customer: null }),
|
logout: () => set({ token: null, customer: null }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "irelax-auth-storage",
|
name: "homaroma-auth-storage",
|
||||||
partialize: (state) => ({ token: state.token, customer: state.customer }), // don't persist isLiffInitialized
|
partialize: (state) => ({ token: state.token, customer: state.customer }), // don't persist isLiffInitialized
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const useCartStore = create<CartState>()(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'irelax-cart-storage',
|
name: 'homaroma-cart-storage',
|
||||||
partialize: (state) => ({ items: state.items }), // Only persist items, not UI state
|
partialize: (state) => ({ items: state.items }), // Only persist items, not UI state
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ const STATS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'About Us | iRelax Aroma',
|
title: 'About Us | HomAroma',
|
||||||
description: 'Born from a passion for stillness. iRelax began in 2019 as a Bangkok studio crafting botanical aroma blends.',
|
description: 'Born from a passion for stillness. HomAroma began in 2019 as a Bangkok studio crafting botanical aroma blends.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
@@ -46,7 +46,7 @@ export default function AboutPage() {
|
|||||||
Born from a passion for <span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-glow to-night-700 italic font-serif">stillness</span>
|
Born from a passion for <span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-glow to-night-700 italic font-serif">stillness</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-slate-300 text-lg leading-relaxed">
|
<p className="text-slate-300 text-lg leading-relaxed">
|
||||||
iRelax began in 2019 as a one-person studio in Bangkok, crafting small batches of botanical aroma blends for friends and family. What started as a personal journey toward calm became a philosophy — that our environment shapes how we feel, think, and rest.
|
HomAroma began in 2019 as a one-person studio in Bangkok, crafting small batches of botanical aroma blends for friends and family. What started as a personal journey toward calm became a philosophy — that our environment shapes how we feel, think, and rest.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -73,7 +73,7 @@ export default function AboutPage() {
|
|||||||
We believe that scent is the fastest pathway to emotional transformation. A single breath of the right blend can dissolve anxiety, sharpen focus, or unlock creativity. Our mission is to make that transformation accessible — through products that are as beautiful as they are effective.
|
We believe that scent is the fastest pathway to emotional transformation. A single breath of the right blend can dissolve anxiety, sharpen focus, or unlock creativity. Our mission is to make that transformation accessible — through products that are as beautiful as they are effective.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-slate-400 leading-relaxed text-sm">
|
<p className="text-slate-400 leading-relaxed text-sm">
|
||||||
Every iRelax product is designed with intention: the glass bottles are reusable, the packaging is recycled, and the formulas are written to be understood by anyone — not hidden behind chemical jargon.
|
Every HomAroma product is designed with intention: the glass bottles are reusable, the packaging is recycled, and the formulas are written to be understood by anyone — not hidden behind chemical jargon.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-80 rounded-2xl overflow-hidden glass border border-white/5">
|
<div className="relative h-80 rounded-2xl overflow-hidden glass border border-white/5">
|
||||||
@@ -81,7 +81,7 @@ export default function AboutPage() {
|
|||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="text-center space-y-3">
|
<div className="text-center space-y-3">
|
||||||
<div className="text-7xl">🌿</div>
|
<div className="text-7xl">🌿</div>
|
||||||
<p className="text-white/30 text-sm uppercase tracking-widest">iRelax Studio, Bangkok</p>
|
<p className="text-white/30 text-sm uppercase tracking-widest">HomAroma Studio, Bangkok</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ async function getArticle(slug: string): Promise<Article | null> {
|
|||||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||||
const { slug } = await params
|
const { slug } = await params
|
||||||
const article = await getArticle(slug)
|
const article = await getArticle(slug)
|
||||||
if (!article) return { title: 'Article Not Found | iRelax' }
|
if (!article) return { title: 'Article Not Found | HomAroma' }
|
||||||
return {
|
return {
|
||||||
title: `${article.title} | iRelax Scent Guide`,
|
title: `${article.title} | HomAroma Scent Guide`,
|
||||||
description: article.content.replace(/<[^>]+>/g, '').slice(0, 160),
|
description: article.content.replace(/<[^>]+>/g, '').slice(0, 160),
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: article.title,
|
title: article.title,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Contact Us | iRelax Aroma',
|
title: 'Contact Us | HomAroma',
|
||||||
description: 'Get in touch with iRelax. We respond to all inquiries within 2 hours.',
|
description: 'Get in touch with HomAroma. We respond to all inquiries within 2 hours.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ContactLayout({ children }: { children: React.ReactNode }) {
|
export default function ContactLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ export default function ContactPage() {
|
|||||||
<h2 className="font-bold text-xl mb-6">Reach Us Directly</h2>
|
<h2 className="font-bold text-xl mb-6">Reach Us Directly</h2>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{[
|
{[
|
||||||
{ icon: '📧', label: 'Email', value: 'hello@irelax.co.th', href: 'mailto:hello@irelax.co.th' },
|
{ icon: '📧', label: 'Email', value: 'hello@homaroma.shop', href: 'mailto:hello@homaroma.shop' },
|
||||||
{ icon: '📱', label: 'LINE Official', value: '@irelaxessential', href: '#' },
|
{ icon: '📱', label: 'LINE Official', value: '@homaroma', href: '#' },
|
||||||
{ icon: '📸', label: 'Instagram', value: '@irelax.essential', href: '#' },
|
{ icon: '📸', label: 'Instagram', value: '@homaroma', href: '#' },
|
||||||
{ icon: '📍', label: 'Studio', value: 'Bangkok, Thailand', href: '#' },
|
{ icon: '📍', label: 'Studio', value: 'Bangkok, Thailand', href: '#' },
|
||||||
].map(item => (
|
].map(item => (
|
||||||
<a key={item.label} href={item.href} className="flex items-start gap-4 group">
|
<a key={item.label} href={item.href} className="flex items-start gap-4 group">
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ function formatDiscount(type: string, value: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'โปรโมชั่นและคูปองส่วนลด | iRelax',
|
title: 'โปรโมชั่นและคูปองส่วนลด | HomAroma',
|
||||||
description: 'รวมรหัสส่วนลดและโปรโมชั่นพิเศษจาก iRelax สำหรับการสั่งซื้อน้ำหอมและผลิตภัณฑ์อโรม่าที่คุณชื่นชอบ',
|
description: 'รวมรหัสส่วนลดและโปรโมชั่นพิเศษจาก HomAroma สำหรับการสั่งซื้อน้ำหอมและผลิตภัณฑ์อโรม่าที่คุณชื่นชอบ',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function CouponsPage() {
|
export default async function CouponsPage() {
|
||||||
@@ -48,7 +48,7 @@ export default async function CouponsPage() {
|
|||||||
<h2 className="text-xl font-bold text-white mb-2">ยังไม่มีคูปองส่วนลดในขณะนี้</h2>
|
<h2 className="text-xl font-bold text-white mb-2">ยังไม่มีคูปองส่วนลดในขณะนี้</h2>
|
||||||
<p className="text-slate-400 mb-6">กรุณากลับมาตรวจสอบโปรโมชั่นใหม่ในภายหลัง</p>
|
<p className="text-slate-400 mb-6">กรุณากลับมาตรวจสอบโปรโมชั่นใหม่ในภายหลัง</p>
|
||||||
<a
|
<a
|
||||||
href="https://irelaxaroma.shop/shop"
|
href="https://homaroma.shop/shop"
|
||||||
className="inline-flex px-8 py-3 bg-cyan-glow/10 text-cyan-glow hover:bg-cyan-glow hover:text-night-900 font-bold rounded-xl transition-all"
|
className="inline-flex px-8 py-3 bg-cyan-glow/10 text-cyan-glow hover:bg-cyan-glow hover:text-night-900 font-bold rounded-xl transition-all"
|
||||||
>
|
>
|
||||||
ไปช้อปปิ้งกันก่อน
|
ไปช้อปปิ้งกันก่อน
|
||||||
@@ -92,7 +92,7 @@ export default async function CouponsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={`https://irelaxaroma.shop/shop?coupon=${coupon.code}`}
|
href={`https://homaroma.shop/shop?coupon=${coupon.code}`}
|
||||||
className="w-full py-4 bg-white/5 hover:bg-cyan-glow text-white hover:text-night-900 font-bold rounded-xl flex items-center justify-center gap-2 transition-all duration-300"
|
className="w-full py-4 bg-white/5 hover:bg-cyan-glow text-white hover:text-night-900 font-bold rounded-xl flex items-center justify-center gap-2 transition-all duration-300"
|
||||||
>
|
>
|
||||||
ช้อปพร้อมใช้คูปองเลย
|
ช้อปพร้อมใช้คูปองเลย
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'FAQ | iRelax Aroma',
|
title: 'FAQ | HomAroma',
|
||||||
description: 'Everything you need to know about iRelax products, shipping, and care.',
|
description: 'Everything you need to know about HomAroma products, shipping, and care.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FaqLayout({ children }: { children: React.ReactNode }) {
|
export default function FaqLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const FAQS = [
|
|||||||
{
|
{
|
||||||
category: 'Products', icon: '🌿',
|
category: 'Products', icon: '🌿',
|
||||||
items: [
|
items: [
|
||||||
{ q: 'What are the ingredients in your aroma products?', a: 'All iRelax products are crafted from 100% natural botanical extracts, pure essential oils, and pharmaceutical-grade carrier bases. We never use synthetic fragrances, parabens, or harmful chemicals. Each product label lists full ingredient transparency.' },
|
{ q: 'What are the ingredients in your aroma products?', a: 'All HomAroma products are crafted from 100% natural botanical extracts, pure essential oils, and pharmaceutical-grade carrier bases. We never use synthetic fragrances, parabens, or harmful chemicals. Each product label lists full ingredient transparency.' },
|
||||||
{ q: 'How long does each product last?', a: 'Our Reed Diffusers last 4–8 weeks depending on room size and airflow. Aroma Oils (30ml) last approximately 3–4 months with normal use. Room Sprays (100ml) provide 60–80 uses. Car Diffusers cartridges last up to 45 days.' },
|
{ q: 'How long does each product last?', a: 'Our Reed Diffusers last 4–8 weeks depending on room size and airflow. Aroma Oils (30ml) last approximately 3–4 months with normal use. Room Sprays (100ml) provide 60–80 uses. Car Diffusers cartridges last up to 45 days.' },
|
||||||
{ q: 'Are your products suitable for sensitive skin or allergies?', a: 'We recommend patch testing before topical use. Our Aroma Oils are formulated for diffuser use primarily. If you have known fragrance allergies, consult our scent guide or contact us with your specific concerns.' },
|
{ q: 'Are your products suitable for sensitive skin or allergies?', a: 'We recommend patch testing before topical use. Our Aroma Oils are formulated for diffuser use primarily. If you have known fragrance allergies, consult our scent guide or contact us with your specific concerns.' },
|
||||||
{ q: 'What is the difference between Aroma Oil and Oil Ozone?', a: 'Aroma Oils are pure essential oil blends designed for aromatherapy diffusers and therapeutic use. Oil Ozone is our premium line featuring ozone-infused botanical extracts — designed for air purification and deeper space transformation.' },
|
{ q: 'What is the difference between Aroma Oil and Oil Ozone?', a: 'Aroma Oils are pure essential oil blends designed for aromatherapy diffusers and therapeutic use. Oil Ozone is our premium line featuring ozone-infused botanical extracts — designed for air purification and deeper space transformation.' },
|
||||||
@@ -48,7 +48,7 @@ export default function FaqPage() {
|
|||||||
<div className="pt-32 pb-16 px-6 text-center space-y-4 border-b border-white/5 bg-night-800/20">
|
<div className="pt-32 pb-16 px-6 text-center space-y-4 border-b border-white/5 bg-night-800/20">
|
||||||
<p className="text-cyan-glow uppercase tracking-[0.4em] text-xs font-semibold opacity-70">Support</p>
|
<p className="text-cyan-glow uppercase tracking-[0.4em] text-xs font-semibold opacity-70">Support</p>
|
||||||
<h1 className="text-5xl font-bold">Frequently Asked Questions</h1>
|
<h1 className="text-5xl font-bold">Frequently Asked Questions</h1>
|
||||||
<p className="text-slate-400 max-w-lg mx-auto">Everything you need to know about iRelax products, shipping, and care.</p>
|
<p className="text-slate-400 max-w-lg mx-auto">Everything you need to know about HomAroma products, shipping, and care.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto px-6 py-16 space-y-10">
|
<div className="max-w-4xl mx-auto px-6 py-16 space-y-10">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import ClientProviders from '../components/ClientProviders';
|
|||||||
import { API_BASE } from '../lib/api';
|
import { API_BASE } from '../lib/api';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'iRelax Aroma',
|
title: 'HomAroma',
|
||||||
description: 'Pure Origins, Artisan Craft, Zero Compromise',
|
description: 'Pure Origins, Artisan Craft, Zero Compromise',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ async function getRelated(categoryId: number, excludeId: number): Promise<Produc
|
|||||||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
const product = await getProduct(id)
|
const product = await getProduct(id)
|
||||||
if (!product) return { title: 'Product Not Found | iRelax' }
|
if (!product) return { title: 'Product Not Found | HomAroma' }
|
||||||
return {
|
return {
|
||||||
title: `${product.scent.name} ${product.category.name} | iRelax Aroma`,
|
title: `${product.scent.name} ${product.category.name} | HomAroma`,
|
||||||
description: product.description ?? `Premium ${product.category.name} — ${product.scent.name}. Shop iRelax Aroma for high-quality botanical scents.`,
|
description: product.description ?? `Premium ${product.category.name} — ${product.scent.name}. Shop HomAroma for high-quality botanical scents.`,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: product.scent.name,
|
title: product.scent.name,
|
||||||
description: product.description ?? '',
|
description: product.description ?? '',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Article } from '@homaroma/types'
|
|||||||
import { API_BASE } from '../../lib/api'
|
import { API_BASE } from '../../lib/api'
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Scent Guide | iRelax Aroma',
|
title: 'Scent Guide | HomAroma',
|
||||||
description: 'Explore our collection of aroma articles, guides, and sensory knowledge to find your perfect scent.',
|
description: 'Explore our collection of aroma articles, guides, and sensory knowledge to find your perfect scent.',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Search Products | iRelax Aroma',
|
title: 'Search Products | HomAroma',
|
||||||
description: 'Search our collection of premium aroma products by scent or product name.',
|
description: 'Search our collection of premium aroma products by scent or product name.',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Shop All Products | iRelax Aroma',
|
title: 'Shop All Products | HomAroma',
|
||||||
description: 'Browse our full range of premium aroma products — crafted to elevate every space.',
|
description: 'Browse our full range of premium aroma products — crafted to elevate every space.',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default function Footer({ categories = [] }: FooterProps) {
|
|||||||
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-12">
|
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-12">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-2xl font-bold tracking-widest">
|
<div className="text-2xl font-bold tracking-widest">
|
||||||
iRelax <span className="text-cyan-glow text-sm font-light italic">Essential</span>
|
HomAroma <span className="text-cyan-glow text-sm font-light italic">Essential</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-slate-500 text-sm leading-relaxed">
|
<p className="text-slate-500 text-sm leading-relaxed">
|
||||||
{t('footer.catchphrase')}
|
{t('footer.catchphrase')}
|
||||||
@@ -65,7 +65,7 @@ export default function Footer({ categories = [] }: FooterProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-slate-600 uppercase tracking-widest">
|
<p className="text-[10px] text-slate-600 uppercase tracking-widest">
|
||||||
© {new Date().getFullYear()} iRelax Essential. {t('footer.rights')}
|
© {new Date().getFullYear()} HomAroma. {t('footer.rights')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
|||||||
const toggleLanguage = () => {
|
const toggleLanguage = () => {
|
||||||
const newLang = i18n.language.startsWith('en') ? 'th' : 'en'
|
const newLang = i18n.language.startsWith('en') ? 'th' : 'en'
|
||||||
i18n.changeLanguage(newLang)
|
i18n.changeLanguage(newLang)
|
||||||
localStorage.setItem('irelax_lang', newLang)
|
localStorage.setItem('homaroma_lang', newLang)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect scroll for navbar bg change
|
// Detect scroll for navbar bg change
|
||||||
@@ -36,7 +36,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
|||||||
window.addEventListener('scroll', onScroll, { passive: true })
|
window.addEventListener('scroll', onScroll, { passive: true })
|
||||||
|
|
||||||
// Check local orders periodically (or just on mount)
|
// Check local orders periodically (or just on mount)
|
||||||
const stored = localStorage.getItem('irelax_local_orders')
|
const stored = localStorage.getItem('homaroma_local_orders')
|
||||||
if (stored) {
|
if (stored) {
|
||||||
try { setLocalOrdersCount(JSON.parse(stored).length) } catch (e) {}
|
try { setLocalOrdersCount(JSON.parse(stored).length) } catch (e) {}
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
|||||||
>
|
>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link href="/" onClick={() => setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-white uppercase italic shrink-0">
|
<Link href="/" onClick={() => setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-white uppercase italic shrink-0">
|
||||||
iRelax <span className="text-cyan-glow text-xs md:text-sm not-italic font-light opacity-60">Essential</span>
|
HomAroma <span className="text-cyan-glow text-xs md:text-sm not-italic font-light opacity-60">Essential</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* ── DESKTOP NAV ── */}
|
{/* ── DESKTOP NAV ── */}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* 3. Fallback dev — http://localhost:3000/api
|
* 3. Fallback dev — http://localhost:3000/api
|
||||||
*
|
*
|
||||||
* Dev: NEXT_PUBLIC_API_URL=http://localhost:3000/api
|
* Dev: NEXT_PUBLIC_API_URL=http://localhost:3000/api
|
||||||
* Production: NEXT_PUBLIC_API_URL=https://api.irelaxaroma.shop/api
|
* Production: NEXT_PUBLIC_API_URL=https://api.homaroma.shop/api
|
||||||
*/
|
*/
|
||||||
export const API_BASE =
|
export const API_BASE =
|
||||||
process.env.NEXT_PUBLIC_API_URL ||
|
process.env.NEXT_PUBLIC_API_URL ||
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import enTranslation from '../locales/en/translation.json'
|
|||||||
import thTranslation from '../locales/th/translation.json'
|
import thTranslation from '../locales/th/translation.json'
|
||||||
|
|
||||||
// Ensure we only access localStorage on the client side
|
// Ensure we only access localStorage on the client side
|
||||||
const savedLang = typeof window !== 'undefined' ? (localStorage.getItem('irelax_lang') || 'th') : 'th'
|
const savedLang = typeof window !== 'undefined' ? (localStorage.getItem('homaroma_lang') || 'th') : 'th'
|
||||||
|
|
||||||
// Check if it's already initialized to prevent re-initialization on HMR
|
// Check if it's already initialized to prevent re-initialization on HMR
|
||||||
if (!i18n.isInitialized) {
|
if (!i18n.isInitialized) {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const useCartStore = create<CartState>()(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'irelax-cart-storage',
|
name: 'homaroma-cart-storage',
|
||||||
partialize: (state) => ({ items: state.items }), // Only persist items, not UI state
|
partialize: (state) => ({ items: state.items }), // Only persist items, not UI state
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const CONFIG = {
|
|||||||
email: 'ti.ddc.2026@gmail.com', // อีเมลที่ใช้สมัคร Cloudflare
|
email: 'ti.ddc.2026@gmail.com', // อีเมลที่ใช้สมัคร Cloudflare
|
||||||
apiKey: 'cfut_fZ4ct5szqJbJxpdEfPT0Q5790SqjKZuU4DlbTukn769a34d8',
|
apiKey: 'cfut_fZ4ct5szqJbJxpdEfPT0Q5790SqjKZuU4DlbTukn769a34d8',
|
||||||
zoneId: '82a3c279a9f53ab8a5531848031c7629',
|
zoneId: '82a3c279a9f53ab8a5531848031c7629',
|
||||||
recordName: 'irelaxaroma.shop',
|
recordName: 'homaroma.shop',
|
||||||
proxied: false
|
proxied: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
# --- ตั้งค่าตรงนี้ ---
|
# --- ตั้งค่าตรงนี้ ---
|
||||||
SERVER_USER="ti"
|
SERVER_USER="ti"
|
||||||
SERVER_HOST="192.168.1.167" # <-- เปลี่ยนเป็น IP หรือ Domain ของ server
|
SERVER_HOST="192.168.1.167" # <-- เปลี่ยนเป็น IP หรือ Domain ของ server
|
||||||
REMOTE_HOOK_PATH="/home/ti/git/irelax.git/hooks/post-receive"
|
REMOTE_HOOK_PATH="/home/ti/git/homaroma.git/hooks/post-receive"
|
||||||
# ---------------------
|
# ---------------------
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# สรุปความแตกต่างและเป้าหมายการใช้งาน: `apps/ssr` vs `apps/website`
|
# สรุปความแตกต่างและเป้าหมายการใช้งาน: `apps/ssr` vs `apps/website`
|
||||||
|
|
||||||
เอกสารนี้สรุปความแตกต่างเชิงสถาปัตยกรรม (Architecture) และจุดประสงค์การใช้งานระหว่าง Frontend ทั้งสองระบบในโปรเจกต์ iRelax เพื่อให้ทีมพัฒนาเข้าใจว่าควรเลือกใช้หรือพัฒนาต่อยอดที่โปรเจกต์ใด
|
เอกสารนี้สรุปความแตกต่างเชิงสถาปัตยกรรม (Architecture) และจุดประสงค์การใช้งานระหว่าง Frontend ทั้งสองระบบในโปรเจกต์ HomAroma เพื่อให้ทีมพัฒนาเข้าใจว่าควรเลือกใช้หรือพัฒนาต่อยอดที่โปรเจกต์ใด
|
||||||
|
|
||||||
## ตารางเปรียบเทียบ (Comparison Table)
|
## ตารางเปรียบเทียบ (Comparison Table)
|
||||||
|
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
| **จุดเด่น** | ทำงานลื่นไหลเหมือนแอปมือถือ (ไม่กระตุกตอนเปลี่ยนหน้า), เหมาะกับ Webview ในแอปพลิเคชัน | ทำ SEO ได้ดีมาก Bot สามารถอ่านข้อมูลสินค้าได้ทันที, โหลดหน้าแรกได้ไว |
|
| **จุดเด่น** | ทำงานลื่นไหลเหมือนแอปมือถือ (ไม่กระตุกตอนเปลี่ยนหน้า), เหมาะกับ Webview ในแอปพลิเคชัน | ทำ SEO ได้ดีมาก Bot สามารถอ่านข้อมูลสินค้าได้ทันที, โหลดหน้าแรกได้ไว |
|
||||||
| **จุดด้อย** | ทำ SEO ได้ยาก (Bot ของ Google อาจจะอ่านข้อมูลไม่ครบเพราะต้องรอ JavaScript ทำงาน) | ต้องใช้ทรัพยากรฝั่ง Server ในการรัน (Node.js Process), ซับซ้อนกว่าในการตั้งค่า |
|
| **จุดด้อย** | ทำ SEO ได้ยาก (Bot ของ Google อาจจะอ่านข้อมูลไม่ครบเพราะต้องรอ JavaScript ทำงาน) | ต้องใช้ทรัพยากรฝั่ง Server ในการรัน (Node.js Process), ซับซ้อนกว่าในการตั้งค่า |
|
||||||
| **รูปแบบการ Deploy** | **Static Files** (HTML/CSS/JS) เสิร์ฟผ่าน Nginx โดยตรง ไม่ต้องมี Node.js รันเป็นพื้นหลัง | **Node.js Server** ต้องใช้ PM2 รัน `next start` และใช้ Nginx ทำ Reverse Proxy |
|
| **รูปแบบการ Deploy** | **Static Files** (HTML/CSS/JS) เสิร์ฟผ่าน Nginx โดยตรง ไม่ต้องมี Node.js รันเป็นพื้นหลัง | **Node.js Server** ต้องใช้ PM2 รัน `next start` และใช้ Nginx ทำ Reverse Proxy |
|
||||||
| **ตำแหน่งไฟล์บน Server**| `/home/ti/projects/MyNginxMain/nginx/html/irelaxaroma.shop/website` | `/home/ti/projects/irelax-ssr` |
|
| **ตำแหน่งไฟล์บน Server**| `/home/ti/projects/MyNginxMain/nginx/html/homaroma.shop/website` | `/home/ti/projects/homaroma-ssr` |
|
||||||
| **คำสั่ง Build** | `bun run build` (ได้โฟลเดอร์ `dist`) | `bun run build` (ได้โฟลเดอร์ `.next`) |
|
| **คำสั่ง Build** | `bun run build` (ได้โฟลเดอร์ `dist`) | `bun run build` (ได้โฟลเดอร์ `.next`) |
|
||||||
| **Service ที่ทำงาน**| ไม่มี (Nginx จัดการทั้งหมด) | **PM2** ชื่อ process: `irelax-ssr` รันบนพอร์ต `3001` |
|
| **Service ที่ทำงาน**| ไม่มี (Nginx จัดการทั้งหมด) | **PM2** ชื่อ process: `homaroma-ssr` รันบนพอร์ต `3001` |
|
||||||
| **การจัดการ Routing** | `react-router-dom` (ประมวลผลที่ฝั่ง Client ทั้งหมด) | Next.js App Router (มีระบบ File-based routing) |
|
| **การจัดการ Routing** | `react-router-dom` (ประมวลผลที่ฝั่ง Client ทั้งหมด) | Next.js App Router (มีระบบ File-based routing) |
|
||||||
| **การจัดการ State** | Zustand | React Context / Zustand / Server Components |
|
| **การจัดการ State** | Zustand | React Context / Zustand / Server Components |
|
||||||
| **ความเหมาะสมของระบบ Login**| เหมาะสมกับ **LINE LIFF Login** และ Token-based auth เพราะมี Context แบบ SPA | เหมาะกับ Cookie-based Auth (NextAuth/Auth.js) หรือ Session |
|
| **ความเหมาะสมของระบบ Login**| เหมาะสมกับ **LINE LIFF Login** และ Token-based auth เพราะมี Context แบบ SPA | เหมาะกับ Cookie-based Auth (NextAuth/Auth.js) หรือ Session |
|
||||||
@@ -44,13 +44,13 @@
|
|||||||
- **คำสั่ง Build:** `cd apps/website && bun run build`
|
- **คำสั่ง Build:** `cd apps/website && bun run build`
|
||||||
- **สิ่งที่ได้จากการ Build:** โฟลเดอร์ `dist/`
|
- **สิ่งที่ได้จากการ Build:** โฟลเดอร์ `dist/`
|
||||||
- **ตำแหน่งไฟล์ปลายทาง (Target Directory):**
|
- **ตำแหน่งไฟล์ปลายทาง (Target Directory):**
|
||||||
`/home/ti/projects/MyNginxMain/nginx/html/irelaxaroma.shop/website`
|
`/home/ti/projects/MyNginxMain/nginx/html/homaroma.shop/website`
|
||||||
- **การจัดการ Environment:** ต้องประกาศตัวแปรแวดล้อมก่อนสั่ง Build เสมอ (เช่น `export VITE_LIFF_ID=...`) เพื่อให้ Vite ฝังค่าเข้าไปในไฟล์โค้ด
|
- **การจัดการ Environment:** ต้องประกาศตัวแปรแวดล้อมก่อนสั่ง Build เสมอ (เช่น `export VITE_LIFF_ID=...`) เพื่อให้ Vite ฝังค่าเข้าไปในไฟล์โค้ด
|
||||||
- **การตั้งค่า Nginx ที่จำเป็น (Routing Configuration):**
|
- **การตั้งค่า Nginx ที่จำเป็น (Routing Configuration):**
|
||||||
เพื่อให้ระบบ React Router ทำงานได้โดยไม่เกิด 404 Error Nginx ต้องถูกตั้งค่าให้โยน Path ที่ไม่รู้จักกลับไปหา `index.html` เสมอ
|
เพื่อให้ระบบ React Router ทำงานได้โดยไม่เกิด 404 Error Nginx ต้องถูกตั้งค่าให้โยน Path ที่ไม่รู้จักกลับไปหา `index.html` เสมอ
|
||||||
```nginx
|
```nginx
|
||||||
location / {
|
location / {
|
||||||
root /usr/share/nginx/html/irelaxaroma.shop/website;
|
root /usr/share/nginx/html/homaroma.shop/website;
|
||||||
index index.html;
|
index index.html;
|
||||||
try_files $uri $uri/ /index.html; # <--- บังคับให้ SPA จัดการ Route เอง
|
try_files $uri $uri/ /index.html; # <--- บังคับให้ SPA จัดการ Route เอง
|
||||||
}
|
}
|
||||||
@@ -61,11 +61,11 @@
|
|||||||
- **คำสั่ง Build:** `cd apps/ssr && bun run build`
|
- **คำสั่ง Build:** `cd apps/ssr && bun run build`
|
||||||
- **สิ่งที่ได้จากการ Build:** โฟลเดอร์ `.next/` และไฟล์ Source Code
|
- **สิ่งที่ได้จากการ Build:** โฟลเดอร์ `.next/` และไฟล์ Source Code
|
||||||
- **ตำแหน่งไฟล์ปลายทาง (Target Directory):**
|
- **ตำแหน่งไฟล์ปลายทาง (Target Directory):**
|
||||||
`/home/ti/projects/irelax-ssr`
|
`/home/ti/projects/homaroma-ssr`
|
||||||
- **การจัดการ Service (PM2):**
|
- **การจัดการ Service (PM2):**
|
||||||
ต้องสั่งรันให้แอปทำงานอยู่เบื้องหลังเสมอด้วยคำสั่ง:
|
ต้องสั่งรันให้แอปทำงานอยู่เบื้องหลังเสมอด้วยคำสั่ง:
|
||||||
```bash
|
```bash
|
||||||
pm2 start /home/ti/projects/irelax-ssr/node_modules/.bin/next --name irelax-ssr -- start -p 3001
|
pm2 start /home/ti/projects/homaroma-ssr/node_modules/.bin/next --name homaroma-ssr -- start -p 3001
|
||||||
```
|
```
|
||||||
- **การตั้งค่า Nginx ที่จำเป็น (Reverse Proxy):**
|
- **การตั้งค่า Nginx ที่จำเป็น (Reverse Proxy):**
|
||||||
Nginx จะไม่ได้เสิร์ฟไฟล์ตรงๆ เหมือน SPA แต่ทำหน้าที่เป็นประตูส่ง Request ต่อไปให้ PM2 (ที่พอร์ต 3001)
|
Nginx จะไม่ได้เสิร์ฟไฟล์ตรงๆ เหมือน SPA แต่ทำหน้าที่เป็นประตูส่ง Request ต่อไปให้ PM2 (ที่พอร์ต 3001)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# iRelax Project - Integrated Deployment Script (post-receive hook)
|
# HomAroma Project - Integrated Deployment Script (post-receive hook)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 1. TARGET PATHS (Nginx & PM2)
|
# 1. TARGET PATHS (Nginx & PM2)
|
||||||
@@ -34,7 +34,7 @@ SSR_PORT=3002
|
|||||||
# START DEPLOYMENT
|
# START DEPLOYMENT
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
set -e
|
set -e
|
||||||
echo "------- 🚀 Starting Full Deployment: iRelax Platform -------"
|
echo "------- 🚀 Starting Full Deployment: HomAroma Platform -------"
|
||||||
|
|
||||||
# A. Prepare Temp Source from Checkout
|
# A. Prepare Temp Source from Checkout
|
||||||
echo "Cloning branch $BRANCH to temporary directory: $TEMP_DIR"
|
echo "Cloning branch $BRANCH to temporary directory: $TEMP_DIR"
|
||||||
|
|||||||
Reference in new issue
Block a user