x
This commit is contained in:
1 parent
a5f6e3171b
commit
f39a0c83dd
9 files changed
+86
-17
No files matched your search
@@ -1 +1,2 @@
|
||||
VITE_API_URL="https://api.homaroma.shop/api"
|
||||
VITE_FRONTEND_URL="https://homaroma.shop"
|
||||
@@ -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"
|
||||
@@ -483,7 +499,7 @@ export default function PageBuilderPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
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"
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user