x
This commit is contained in:
1 parent
a5f6e3171b
commit
f39a0c83dd
9 files changed
+85
-16
No files matched your search
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in new issue
Block a user