This commit is contained in:
Your Name committed 2026-06-10 05:27:39 +07:00
1 parent a5f6e3171b
commit f39a0c83dd
9 files changed
+86 -17

No files matched your search

+1
View File
@@ -1 +1,2 @@
VITE_API_URL="https://api.homaroma.shop/api" VITE_API_URL="https://api.homaroma.shop/api"
VITE_FRONTEND_URL="https://homaroma.shop"
+61 -3
View File
@@ -14,7 +14,8 @@ import {
Loader2, Loader2,
X, X,
Settings2, Settings2,
CheckCircle2 CheckCircle2,
ExternalLink
} from 'lucide-react'; } from 'lucide-react';
import { API_BASE, type Page, type PageSection, type Category } from '@homaroma/types'; import { API_BASE, type Page, type PageSection, type Category } from '@homaroma/types';
@@ -24,6 +25,7 @@ interface Scent {
} }
export default function PageBuilderPage() { export default function PageBuilderPage() {
const frontendBase = (import.meta.env.VITE_FRONTEND_URL || window.location.origin).replace(/\/$/, '');
const [page, setPage] = useState<Page | null>(null); const [page, setPage] = useState<Page | null>(null);
const [sections, setSections] = useState<PageSection[]>([]); const [sections, setSections] = useState<PageSection[]>([]);
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
@@ -40,6 +42,7 @@ export default function PageBuilderPage() {
const [isPageModalOpen, setIsPageModalOpen] = useState(false); const [isPageModalOpen, setIsPageModalOpen] = useState(false);
const [newPageData, setNewPageData] = useState({ title: '', slug: '' }); const [newPageData, setNewPageData] = useState({ title: '', slug: '' });
const [isSavingPage, setIsSavingPage] = useState(false); const [isSavingPage, setIsSavingPage] = useState(false);
const getPageUrl = (slug: string) => `${frontendBase}/pages/${slug}`;
const fetchPagesList = async () => { const fetchPagesList = async () => {
try { try {
@@ -67,7 +70,7 @@ export default function PageBuilderPage() {
setLoading(true); setLoading(true);
try { try {
const res = await fetch(`${API_BASE}/pages/${pageObj.slug}`); const res = await fetch(`${API_BASE}/pages/${pageObj.slug}?includeInactive=true`);
const data = await res.json(); const data = await res.json();
if (data && !data.error) { if (data && !data.error) {
setPage(data); setPage(data);
@@ -466,6 +469,19 @@ export default function PageBuilderPage() {
))} ))}
</select> </select>
{page && (
<a
href={getPageUrl(page.slug)}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-xl bg-cyan-glow/10 border border-cyan-glow/20 text-cyan-glow hover:bg-cyan-glow/20 text-xs font-bold uppercase tracking-wider transition-all"
title={`Open ${page.title} live preview`}
>
<ExternalLink className="w-4 h-4" />
Open Live
</a>
)}
<button <button
onClick={() => setIsPageModalOpen(true)} onClick={() => setIsPageModalOpen(true)}
className="px-3 py-1.5 bg-white/5 border border-white/5 hover:bg-white/10 text-xs font-bold rounded-xl text-slate-300 hover:text-white transition-all uppercase tracking-wider cursor-pointer" className="px-3 py-1.5 bg-white/5 border border-white/5 hover:bg-white/10 text-xs font-bold rounded-xl text-slate-300 hover:text-white transition-all uppercase tracking-wider cursor-pointer"
@@ -483,7 +499,7 @@ export default function PageBuilderPage() {
)} )}
</div> </div>
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
onClick={openAddModal} onClick={openAddModal}
className="flex items-center gap-2 px-5 py-2.5 bg-cyan-glow/10 border border-cyan-glow/30 hover:bg-cyan-glow/20 text-cyan-glow rounded-xl font-bold text-xs uppercase tracking-wider transition-all" className="flex items-center gap-2 px-5 py-2.5 bg-cyan-glow/10 border border-cyan-glow/30 hover:bg-cyan-glow/20 text-cyan-glow rounded-xl font-bold text-xs uppercase tracking-wider transition-all"
@@ -508,6 +524,48 @@ export default function PageBuilderPage() {
</div> </div>
</div> </div>
{pages.length > 0 && (
<div className="bg-night-950/40 border border-white/5 rounded-2xl p-4">
<div className="flex items-center justify-between gap-4 mb-3">
<div>
<h2 className="text-sm font-bold uppercase tracking-widest text-slate-400">Quick Page Links</h2>
<p className="text-xs text-slate-500 mt-1">Open any page directly in the storefront.</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
{pages.map((p) => (
<div
key={p.id}
className={`flex items-center gap-2 px-3 py-2 rounded-xl border text-xs transition-all ${
selectedPageId === p.id
? 'bg-cyan-glow/10 border-cyan-glow/20 text-cyan-glow'
: 'bg-white/5 border-white/5 text-slate-300 hover:bg-white/10'
}`}
>
<button
type="button"
onClick={() => setSelectedPageId(p.id)}
className="font-bold"
title={`Select ${p.title}`}
>
{p.title}
</button>
<span className="text-slate-500">/ {p.slug}</span>
<a
href={getPageUrl(p.slug)}
target="_blank"
rel="noreferrer"
className="ml-1 inline-flex items-center gap-1 hover:text-white transition-colors"
title={`Open ${p.title} live`}
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
</div>
))}
</div>
</div>
)}
{saveStatus === 'saved' && ( {saveStatus === 'saved' && (
<div className="p-4 bg-cyan-glow/10 border border-cyan-glow/30 text-cyan-glow rounded-xl flex items-center gap-2"> <div className="p-4 bg-cyan-glow/10 border border-cyan-glow/30 text-cyan-glow rounded-xl flex items-center gap-2">
<CheckCircle2 className="w-5 h-5" /> <CheckCircle2 className="w-5 h-5" />
+3 -2
View File
@@ -18,12 +18,13 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
} }
}); });
}) })
.get("/:slug", async ({ params: { slug }, set }) => { .get("/:slug", async ({ params: { slug }, query, set }) => {
const includeInactive = String((query as any)?.includeInactive || "").toLowerCase() === "true";
const page = await prisma.page.findUnique({ const page = await prisma.page.findUnique({
where: { slug }, where: { slug },
include: { include: {
sections: { sections: {
where: { isActive: true }, where: includeInactive ? undefined : { isActive: true },
orderBy: { order: "asc" } orderBy: { order: "asc" }
} }
} }
@@ -1,5 +1,3 @@
import React from 'react';
interface DynamicHeroArticleProps { interface DynamicHeroArticleProps {
payload: any; payload: any;
} }
@@ -1,4 +1,3 @@
import React from 'react';
import { Swiper, SwiperSlide } from 'swiper/react'; import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Pagination, Navigation } from 'swiper/modules'; import { Autoplay, Pagination, Navigation } from 'swiper/modules';
import ProductCard from './ProductCard'; import ProductCard from './ProductCard';
@@ -1,5 +1,3 @@
import React from 'react';
interface DynamicQuoteCalloutProps { interface DynamicQuoteCalloutProps {
payload: any; payload: any;
} }
@@ -1,5 +1,3 @@
import React from 'react';
interface DynamicScentNotesBreakdownProps { interface DynamicScentNotesBreakdownProps {
payload: any; payload: any;
} }
+3 -2
View File
@@ -10,9 +10,10 @@ import { useTranslation } from 'react-i18next'
interface NavbarProps { interface NavbarProps {
categories?: Category[] categories?: Category[]
topOffsetClassName?: string
} }
export default function Navbar({ categories = [] }: NavbarProps) { export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }: NavbarProps) {
const [searchOpen, setSearchOpen] = useState(false) const [searchOpen, setSearchOpen] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
@@ -71,7 +72,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
<> <>
{/* ── NAVBAR BAR ── */} {/* ── NAVBAR BAR ── */}
<nav <nav
className={`fixed top-0 w-full z-50 transition-all duration-300 px-4 md:px-6 py-3.5 md:py-4 flex justify-between items-center bg-white/80 backdrop-blur-xl border-b border-black/5 ${ className={`fixed ${topOffsetClassName} w-full z-50 transition-all duration-300 px-4 md:px-6 py-3.5 md:py-4 flex justify-between items-center bg-white/80 backdrop-blur-xl border-b border-black/5 ${
scrolled ? 'shadow-lg shadow-black/5' : 'shadow-none' scrolled ? 'shadow-lg shadow-black/5' : 'shadow-none'
}`} }`}
> >
+18 -3
View File
@@ -12,6 +12,13 @@ export default function CustomPage() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [categoriesLoading, setCategoriesLoading] = useState(true); const [categoriesLoading, setCategoriesLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const announcementSections = sections.filter(
(section) => section.isActive && section.type.toUpperCase() === 'ANNOUNCEMENT_BAR'
);
const topOffsetClassName = announcementSections.length > 0 ? 'top-[32px] md:top-[40px]' : 'top-0';
const contentSections = sections.filter(
(section) => !(section.isActive && section.type.toUpperCase() === 'ANNOUNCEMENT_BAR')
);
useEffect(() => { useEffect(() => {
const fetchCategories = async () => { const fetchCategories = async () => {
@@ -56,7 +63,15 @@ export default function CustomPage() {
return ( return (
<div className="min-h-screen bg-[#FDFDF9] text-[#1F3D2D]"> <div className="min-h-screen bg-[#FDFDF9] text-[#1F3D2D]">
<Navbar categories={categories} /> {announcementSections.map((section) => (
<DynamicPageRenderer
key={section.id}
sections={[section]}
categories={categories}
categoriesLoading={categoriesLoading}
/>
))}
<Navbar categories={categories} topOffsetClassName={topOffsetClassName} />
{loading ? ( {loading ? (
<div className="min-h-[60vh] flex items-center justify-center"> <div className="min-h-[60vh] flex items-center justify-center">
@@ -78,9 +93,9 @@ export default function CustomPage() {
</Link> </Link>
</div> </div>
) : ( ) : (
<div className="animate-fade-in"> <div className={`animate-fade-in ${announcementSections.length > 0 ? 'pt-[32px] md:pt-[40px]' : ''}`}>
<DynamicPageRenderer <DynamicPageRenderer
sections={sections} sections={contentSections}
categories={categories} categories={categories}
categoriesLoading={categoriesLoading} categoriesLoading={categoriesLoading}
/> />