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

No files matched your search

+1
View File
@@ -1 +1,2 @@
VITE_API_URL="https://api.homaroma.shop/api"
VITE_FRONTEND_URL="https://homaroma.shop"
+60 -2
View File
@@ -14,7 +14,8 @@ import {
Loader2,
X,
Settings2,
CheckCircle2
CheckCircle2,
ExternalLink
} from 'lucide-react';
import { API_BASE, type Page, type PageSection, type Category } from '@homaroma/types';
@@ -24,6 +25,7 @@ interface Scent {
}
export default function PageBuilderPage() {
const frontendBase = (import.meta.env.VITE_FRONTEND_URL || window.location.origin).replace(/\/$/, '');
const [page, setPage] = useState<Page | null>(null);
const [sections, setSections] = useState<PageSection[]>([]);
const [categories, setCategories] = useState<Category[]>([]);
@@ -40,6 +42,7 @@ export default function PageBuilderPage() {
const [isPageModalOpen, setIsPageModalOpen] = useState(false);
const [newPageData, setNewPageData] = useState({ title: '', slug: '' });
const [isSavingPage, setIsSavingPage] = useState(false);
const getPageUrl = (slug: string) => `${frontendBase}/pages/${slug}`;
const fetchPagesList = async () => {
try {
@@ -67,7 +70,7 @@ export default function PageBuilderPage() {
setLoading(true);
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();
if (data && !data.error) {
setPage(data);
@@ -466,6 +469,19 @@ export default function PageBuilderPage() {
))}
</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
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"
@@ -508,6 +524,48 @@ export default function PageBuilderPage() {
</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' && (
<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" />
+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({
where: { slug },
include: {
sections: {
where: { isActive: true },
where: includeInactive ? undefined : { isActive: true },
orderBy: { order: "asc" }
}
}
@@ -1,5 +1,3 @@
import React from 'react';
interface DynamicHeroArticleProps {
payload: any;
}
@@ -1,4 +1,3 @@
import React from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Pagination, Navigation } from 'swiper/modules';
import ProductCard from './ProductCard';
@@ -1,5 +1,3 @@
import React from 'react';
interface DynamicQuoteCalloutProps {
payload: any;
}
@@ -1,5 +1,3 @@
import React from 'react';
interface DynamicScentNotesBreakdownProps {
payload: any;
}
+3 -2
View File
@@ -10,9 +10,10 @@ import { useTranslation } from 'react-i18next'
interface NavbarProps {
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 [mobileOpen, setMobileOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
@@ -71,7 +72,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
<>
{/* ── NAVBAR BAR ── */}
<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'
}`}
>
+18 -3
View File
@@ -12,6 +12,13 @@ export default function CustomPage() {
const [loading, setLoading] = useState(true);
const [categoriesLoading, setCategoriesLoading] = useState(true);
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(() => {
const fetchCategories = async () => {
@@ -56,7 +63,15 @@ export default function CustomPage() {
return (
<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 ? (
<div className="min-h-[60vh] flex items-center justify-center">
@@ -78,9 +93,9 @@ export default function CustomPage() {
</Link>
</div>
) : (
<div className="animate-fade-in">
<div className={`animate-fade-in ${announcementSections.length > 0 ? 'pt-[32px] md:pt-[40px]' : ''}`}>
<DynamicPageRenderer
sections={sections}
sections={contentSections}
categories={categories}
categoriesLoading={categoriesLoading}
/>