x
This commit is contained in:
1 parent
50b93eb629
commit
cd4b89a0fc
1 file changed
+320
-252
@@ -57,11 +57,11 @@ export default function PageBuilderPage() {
|
|||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [scents, setScents] = useState<Scent[]>([]);
|
const [scents, setScents] = useState<Scent[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [editingSection, setEditingSection] = useState<Partial<PageSection> | null>(null);
|
const [editingSection, setEditingSection] = useState<Partial<PageSection> | null>(null);
|
||||||
const [uploadTarget, setUploadTarget] = useState<{ type: 'main' | 'slide'; index?: number } | null>(null);
|
const [uploadTarget, setUploadTarget] = useState<{ type: 'main' | 'slide'; index?: number } | null>(null);
|
||||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
||||||
const [isSavingPageSettings, setIsSavingPageSettings] = useState(false);
|
const [isSavingPageSettings, setIsSavingPageSettings] = useState(false);
|
||||||
|
const [isReorderModalOpen, setIsReorderModalOpen] = useState(false);
|
||||||
|
|
||||||
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false);
|
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false);
|
||||||
|
|
||||||
@@ -440,6 +440,7 @@ export default function PageBuilderPage() {
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setSaveStatus('saved');
|
setSaveStatus('saved');
|
||||||
|
setIsReorderModalOpen(false);
|
||||||
setTimeout(() => setSaveStatus('idle'), 3000);
|
setTimeout(() => setSaveStatus('idle'), 3000);
|
||||||
} else {
|
} else {
|
||||||
setSaveStatus('error');
|
setSaveStatus('error');
|
||||||
@@ -497,12 +498,12 @@ export default function PageBuilderPage() {
|
|||||||
buttonLink: '/shop'
|
buttonLink: '/shop'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setIsModalOpen(true);
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditModal = (section: PageSection) => {
|
const openEditModal = (section: PageSection) => {
|
||||||
setEditingSection(JSON.parse(JSON.stringify(section))); // deep copy
|
setEditingSection(JSON.parse(JSON.stringify(section))); // deep copy
|
||||||
setIsModalOpen(true);
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTypeChange = (type: string) => {
|
const handleTypeChange = (type: string) => {
|
||||||
@@ -705,7 +706,7 @@ export default function PageBuilderPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setIsModalOpen(false);
|
setEditingSection(null);
|
||||||
if (selectedPageId) fetchPageDetails(selectedPageId);
|
if (selectedPageId) fetchPageDetails(selectedPageId);
|
||||||
} else {
|
} else {
|
||||||
const errData = await res.json();
|
const errData = await res.json();
|
||||||
@@ -718,157 +719,33 @@ export default function PageBuilderPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 animate-fade-in text-slate-100">
|
<div className="space-y-8 animate-fade-in text-slate-100">
|
||||||
|
{/* Global Header */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
<div className="space-y-2">
|
<div className="flex items-center gap-6">
|
||||||
<h1 className="text-3xl font-bold">Page Builder (CMS)</h1>
|
<h1 className="text-3xl font-bold">Page Builder (CMS)</h1>
|
||||||
|
<div className="h-6 w-px bg-white/10 hidden md:block"></div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-xs text-slate-400 uppercase tracking-widest font-bold">Select Page:</span>
|
|
||||||
<select
|
<select
|
||||||
value={selectedPageId || ''}
|
value={selectedPageId || ''}
|
||||||
onChange={(e) => setSelectedPageId(Number(e.target.value))}
|
onChange={(e) => setSelectedPageId(Number(e.target.value))}
|
||||||
className="bg-slate-900 border border-white/10 text-white rounded-xl px-3 py-1.5 text-sm focus:outline-none focus:border-cyan-glow cursor-pointer"
|
className="bg-slate-900 border border-white/10 text-white rounded-xl px-4 py-2 text-sm font-medium focus:outline-none focus:border-cyan-glow focus:ring-1 focus:ring-cyan-glow cursor-pointer transition-all min-w-[200px]"
|
||||||
>
|
>
|
||||||
{pages.map((p) => (
|
{pages.map((p) => (
|
||||||
<option key={p.id} value={p.id}>
|
<option key={p.id} value={p.id}>
|
||||||
{p.title} ({p.slug})
|
{p.title} (/{p.slug})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</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
|
|
||||||
type="button"
|
|
||||||
onClick={handleSavePageSettings}
|
|
||||||
disabled={isSavingPageSettings}
|
|
||||||
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-xl bg-white text-night-900 hover:bg-slate-200 disabled:opacity-50 text-xs font-bold uppercase tracking-wider transition-all"
|
|
||||||
>
|
|
||||||
{isSavingPageSettings ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Save className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
Save Page Settings
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<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-4 py-2 bg-transparent border border-dashed border-white/20 hover:border-white/50 text-sm font-bold rounded-xl text-slate-300 hover:text-white transition-all uppercase tracking-wider cursor-pointer flex items-center gap-2"
|
||||||
>
|
>
|
||||||
+ Create Page
|
<Plus className="w-4 h-4" /> Create Page
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{page && page.slug !== 'home' && (
|
|
||||||
<button
|
|
||||||
onClick={handleDeletePage}
|
|
||||||
className="px-3 py-1.5 bg-rose-500/10 border border-rose-500/10 hover:bg-rose-500 hover:text-white text-xs font-bold rounded-xl text-rose-500 transition-all uppercase tracking-wider cursor-pointer"
|
|
||||||
>
|
|
||||||
Delete Page
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{page && (
|
|
||||||
<div className="mt-4 flex items-center gap-3 rounded-2xl border border-white/10 bg-white/5 px-4 py-3">
|
|
||||||
<label className="inline-flex items-center gap-3 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={page.isPublished}
|
|
||||||
onChange={(e) => setPage({ ...page, isPublished: e.target.checked })}
|
|
||||||
className="h-4 w-4 rounded border-white/20 bg-slate-900 text-cyan-glow focus:ring-cyan-glow"
|
|
||||||
/>
|
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-slate-300">
|
|
||||||
Publish to Website
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<span
|
|
||||||
className={`text-[10px] font-bold uppercase tracking-widest ${
|
|
||||||
page.isPublished ? 'text-emerald-400' : 'text-amber-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{page.isPublished ? 'Published' : 'Draft'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
Add Section Block
|
|
||||||
</button>
|
|
||||||
{sections.length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={handleSaveOrder}
|
|
||||||
disabled={saveStatus === 'saving'}
|
|
||||||
className="flex items-center gap-2 px-5 py-2.5 bg-white text-night-900 hover:bg-slate-200 disabled:opacity-50 rounded-xl font-bold text-xs uppercase tracking-wider transition-all"
|
|
||||||
>
|
|
||||||
{saveStatus === 'saving' ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Save className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
{saveStatus === 'saved' ? 'Saved Order!' : 'Save Layout Order'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</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">
|
||||||
@@ -890,150 +767,197 @@ export default function PageBuilderPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-night-950/40 border border-white/5 rounded-2xl overflow-hidden">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
||||||
<div className="p-6 border-b border-white/5 bg-white/[0.01]">
|
<div className="lg:col-span-5 space-y-4">
|
||||||
<h2 className="text-lg font-bold">Homepage Blocks list</h2>
|
{/* Page Configuration Card */}
|
||||||
|
{page && (
|
||||||
|
<div className="bg-night-900/50 border border-white/5 rounded-2xl p-6 shadow-lg backdrop-blur-sm">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
|
||||||
|
Page Title
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={page.title || ''}
|
||||||
|
onChange={(e) => setPage({ ...page, title: e.target.value })}
|
||||||
|
className="bg-slate-900/50 border border-white/10 text-white rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-cyan-glow/50 transition-colors w-full"
|
||||||
|
placeholder="Enter page title..."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-white/5">
|
<div className="flex items-center gap-4">
|
||||||
{sections.length === 0 ? (
|
<label className="inline-flex items-center gap-3 cursor-pointer">
|
||||||
<div className="p-16 text-center text-slate-500 space-y-4">
|
<input
|
||||||
<Layers className="w-12 h-12 mx-auto text-slate-700" />
|
type="checkbox"
|
||||||
<p>No content sections in this page yet. Start by adding one!</p>
|
checked={page.isPublished}
|
||||||
</div>
|
onChange={(e) => setPage({ ...page, isPublished: e.target.checked })}
|
||||||
) : (
|
className="h-4 w-4 rounded border-white/20 bg-slate-900 text-cyan-glow focus:ring-cyan-glow focus:ring-offset-slate-900"
|
||||||
sections.map((section, idx) => (
|
/>
|
||||||
<div
|
<span className="text-sm font-bold text-slate-200">
|
||||||
key={section.id}
|
Publish to Website
|
||||||
className={`p-6 flex items-center justify-between transition-all duration-300 hover:bg-white/[0.02] ${
|
</span>
|
||||||
!section.isActive ? 'opacity-50' : ''
|
</label>
|
||||||
|
<span
|
||||||
|
className={`text-[10px] font-bold uppercase tracking-widest px-2 py-1 rounded-md ${
|
||||||
|
page.isPublished ? 'bg-emerald-500/10 text-emerald-400' : 'bg-amber-500/10 text-amber-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-6">
|
{page.isPublished ? 'Published' : 'Draft'}
|
||||||
{/* Index & Order controls */}
|
|
||||||
<div className="flex flex-col gap-1 items-center">
|
|
||||||
<button
|
|
||||||
onClick={() => handleMove(idx, 'up')}
|
|
||||||
disabled={idx === 0}
|
|
||||||
className="p-1 hover:bg-white/5 rounded-md disabled:opacity-20 text-slate-400 hover:text-white"
|
|
||||||
>
|
|
||||||
<ArrowUp className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<span className="text-xs font-mono text-slate-500 font-bold">
|
|
||||||
{idx + 1}
|
|
||||||
</span>
|
</span>
|
||||||
<button
|
|
||||||
onClick={() => handleMove(idx, 'down')}
|
|
||||||
disabled={idx === sections.length - 1}
|
|
||||||
className="p-1 hover:bg-white/5 rounded-md disabled:opacity-20 text-slate-400 hover:text-white"
|
|
||||||
>
|
|
||||||
<ArrowDown className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Section Description */}
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="text-xs font-bold font-mono px-2 py-0.5 bg-slate-800 rounded border border-white/5 uppercase">
|
|
||||||
{section.type}
|
|
||||||
</span>
|
|
||||||
{!section.isActive && (
|
|
||||||
<span className="text-[10px] uppercase font-bold tracking-widest text-slate-500">
|
|
||||||
Hidden
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<h3 className="text-base font-bold mt-1">
|
|
||||||
{getAdminDisplayString(
|
|
||||||
section.payload?.title
|
|
||||||
|| section.payload?.tagline
|
|
||||||
|| section.payload?.quote
|
|
||||||
|| (section.type?.toUpperCase() === 'ANNOUNCEMENT_BAR' ? section.payload?.slides?.[0] : undefined)
|
|
||||||
) || `Block #${section.id}`}
|
|
||||||
</h3>
|
|
||||||
{section.type?.toUpperCase() === 'PRODUCTS' && (
|
|
||||||
<p className="text-xs text-slate-400 mt-0.5">
|
|
||||||
Type: {section.payload?.type} • Limit: {section.payload?.limit || 3} products
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{section.type?.toUpperCase() === 'ANNOUNCEMENT_BAR' && (
|
|
||||||
<p className="text-xs text-slate-400 mt-0.5">
|
|
||||||
Slides: {section.payload?.slides?.length || 0} messages
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
<div className="flex flex-col sm:flex-row items-center gap-3 w-full">
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => handleToggleActive(section)}
|
|
||||||
className={`p-2.5 rounded-xl border transition-all ${
|
|
||||||
section.isActive
|
|
||||||
? 'bg-cyan-glow/10 border-cyan-glow/20 text-cyan-glow hover:bg-cyan-glow/20'
|
|
||||||
: 'bg-white/5 border-white/5 text-slate-400 hover:bg-white/10 hover:text-white'
|
|
||||||
}`}
|
|
||||||
title={section.isActive ? 'Hide Section' : 'Show Section'}
|
|
||||||
>
|
|
||||||
{section.isActive ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => openEditModal(section)}
|
|
||||||
className="p-2.5 bg-white/5 border border-white/5 rounded-xl text-slate-400 hover:bg-white/10 hover:text-white transition-all"
|
|
||||||
title="Edit Block Content"
|
|
||||||
>
|
|
||||||
<Edit className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDeleteSection(section.id)}
|
|
||||||
className="p-2.5 bg-rose-500/10 border border-rose-500/10 rounded-xl text-rose-500 hover:bg-rose-500 hover:text-white transition-all"
|
|
||||||
title="Delete Block"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Editor Modal */}
|
|
||||||
{isModalOpen && editingSection && (
|
|
||||||
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 md:p-8">
|
|
||||||
<div className="bg-night-900 border border-white/10 rounded-3xl shadow-2xl w-full max-w-5xl flex flex-col max-h-full overflow-hidden animate-pop-in text-slate-200">
|
|
||||||
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
|
|
||||||
<h3 className="text-xl font-bold flex items-center gap-2">
|
|
||||||
<Settings2 className="w-5 h-5 text-cyan-glow" />
|
|
||||||
{editingSection.id ? `Edit ${editingSection.type} Block` : 'Create Layout Block'}
|
|
||||||
</h3>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsModalOpen(false)}
|
onClick={handleSavePageSettings}
|
||||||
|
disabled={isSavingPageSettings}
|
||||||
|
className="inline-flex flex-1 justify-center items-center gap-2 px-5 py-2 rounded-xl bg-white text-night-900 hover:bg-slate-200 disabled:opacity-50 text-xs font-bold uppercase tracking-wider transition-all shadow-md w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{isSavingPageSettings ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
Save Settings
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={getPageUrl(page.slug)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="inline-flex flex-1 justify-center items-center gap-2 px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-white text-xs font-bold uppercase tracking-wider transition-all w-full sm:w-auto"
|
||||||
|
title={`Open ${page.title} live preview`}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
Preview Live
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{page.slug !== 'home' && (
|
||||||
|
<button
|
||||||
|
onClick={handleDeletePage}
|
||||||
|
className="text-xs font-bold text-rose-500 hover:text-rose-400 hover:underline uppercase tracking-wider transition-all mt-2 w-fit mx-auto"
|
||||||
|
>
|
||||||
|
Delete this page
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Section Management Header */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-night-950/40 border border-white/5 p-4 rounded-2xl">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold">Page Sections</h2>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">Manage the content blocks for this page.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsReorderModalOpen(true)}
|
||||||
|
className="flex items-center gap-2 px-5 py-2.5 bg-white/10 hover:bg-white/20 border border-white/5 text-white rounded-xl font-bold text-xs uppercase tracking-wider transition-all"
|
||||||
|
>
|
||||||
|
<Layers className="w-4 h-4" />
|
||||||
|
Reorder Layout
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={openAddModal}
|
||||||
|
className="flex items-center gap-2 px-6 py-2.5 bg-cyan-glow text-night-900 hover:bg-cyan-glow/90 shadow-lg shadow-cyan-glow/20 rounded-xl font-bold text-xs uppercase tracking-wider transition-all"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Add Section
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Block Selection Dropdown */}
|
||||||
|
<div className="bg-night-950/40 border border-white/5 p-4 rounded-2xl space-y-3">
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
|
||||||
|
Select Block to Edit
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={editingSection?.id?.toString() || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const selectedId = e.target.value;
|
||||||
|
if (!selectedId) {
|
||||||
|
setEditingSection(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sec = sections.find(s => s.id?.toString() === selectedId);
|
||||||
|
if (sec) openEditModal(sec);
|
||||||
|
}}
|
||||||
|
className="bg-slate-900 border border-white/10 text-white rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-cyan-glow/50 focus:ring-1 focus:ring-cyan-glow transition-all w-full"
|
||||||
|
>
|
||||||
|
<option value="">-- Choose a block --</option>
|
||||||
|
{sections.map((section, idx) => {
|
||||||
|
const title = getAdminDisplayString(
|
||||||
|
section.payload?.title || section.payload?.tagline || section.payload?.quote || (section.type?.toUpperCase() === 'ANNOUNCEMENT_BAR' ? section.payload?.slides?.[0] : undefined)
|
||||||
|
) || `Block #${section.id}`;
|
||||||
|
return (
|
||||||
|
<option key={section.id} value={section.id?.toString()}>
|
||||||
|
{idx + 1}. {section.type} - {title} {section.isActive ? '' : '(Hidden)'}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-7 sticky top-8">
|
||||||
|
{editingSection ? (
|
||||||
|
<div className="bg-night-900 border border-white/10 rounded-3xl shadow-2xl w-full flex flex-col max-h-[calc(100vh-4rem)] overflow-hidden animate-pop-in text-slate-200">
|
||||||
|
<div className="px-8 py-6 border-b border-white/5 flex items-center justify-between bg-gradient-to-r from-white/[0.03] to-transparent shrink-0">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h3 className="text-xl font-bold flex items-center gap-3">
|
||||||
|
<Settings2 className="w-5 h-5 text-cyan-glow" />
|
||||||
|
{editingSection.id ? 'Edit Block Settings' : 'Create Layout Block'}
|
||||||
|
</h3>
|
||||||
|
{editingSection.id && (
|
||||||
|
<div className="flex items-center gap-2 mt-1 ml-8 flex-wrap">
|
||||||
|
<span className="px-2 py-0.5 text-[10px] font-bold font-mono bg-cyan-glow/10 text-cyan-glow rounded border border-cyan-glow/20 uppercase tracking-wider">
|
||||||
|
Type: {editingSection.type}
|
||||||
|
</span>
|
||||||
|
<span className="px-2 py-0.5 text-[10px] font-bold font-mono bg-white/5 text-slate-400 rounded border border-white/10 tracking-wider">
|
||||||
|
Block ID: {editingSection.id}
|
||||||
|
</span>
|
||||||
|
<div className="w-px h-3 bg-white/10 mx-1"></div>
|
||||||
|
<span className="px-2 py-0.5 text-[10px] font-bold font-mono bg-fuchsia-500/10 text-fuchsia-400 rounded border border-fuchsia-500/20 uppercase tracking-wider">
|
||||||
|
Page: {page?.slug || 'Unknown'}
|
||||||
|
</span>
|
||||||
|
<span className="px-2 py-0.5 text-[10px] font-bold font-mono bg-white/5 text-slate-400 rounded border border-white/10 tracking-wider">
|
||||||
|
Page ID: {page?.id || '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEditingSection(null)}
|
||||||
className="p-2 rounded-xl hover:bg-white/5 text-slate-400 hover:text-white transition-all"
|
className="p-2 rounded-xl hover:bg-white/5 text-slate-400 hover:text-white transition-all"
|
||||||
>
|
>
|
||||||
<X className="w-6 h-6" />
|
<X className="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sub-header for Language Tabs */}
|
{/* Premium Sub-header for Language Tabs */}
|
||||||
<div className="px-8 pt-4 pb-0 border-b border-white/5 bg-night-900/90 backdrop-blur-md flex gap-6 overflow-x-auto hide-scrollbar">
|
<div className="px-8 py-4 border-b border-white/5 bg-night-950/50 flex items-center overflow-x-auto hide-scrollbar">
|
||||||
|
<div className="bg-black/40 p-1 rounded-xl flex gap-1 border border-white/5">
|
||||||
{['th', 'en', 'ja', 'zh', 'vi', 'ko'].map((lang) => (
|
{['th', 'en', 'ja', 'zh', 'vi', 'ko'].map((lang) => (
|
||||||
<button
|
<button
|
||||||
key={lang}
|
key={lang}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEditingLang(lang)}
|
onClick={() => setEditingLang(lang)}
|
||||||
className={`relative pb-3 text-xs font-bold uppercase tracking-widest border-b-2 transition-all whitespace-nowrap ${
|
className={`relative px-4 py-2 text-xs font-bold uppercase tracking-widest transition-all rounded-lg whitespace-nowrap ${
|
||||||
editingLang === lang
|
editingLang === lang
|
||||||
? 'border-cyan-glow text-cyan-glow'
|
? 'bg-cyan-glow text-night-900 shadow-sm'
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-300 hover:border-white/20'
|
: 'text-slate-400 hover:text-white hover:bg-white/5'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{lang}
|
{lang}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleFormSubmit} className="flex-1 flex flex-col min-h-0">
|
<form onSubmit={handleFormSubmit} className="flex-1 flex flex-col min-h-0">
|
||||||
<div className="flex-1 overflow-y-auto p-8 space-y-6">
|
<div className="flex-1 overflow-y-auto p-8 space-y-6">
|
||||||
@@ -1264,7 +1188,7 @@ export default function PageBuilderPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsModalOpen(false)}
|
onClick={() => setEditingSection(null)}
|
||||||
className="px-8 py-3 bg-white/5 hover:bg-white/10 text-white font-bold rounded-xl transition-all"
|
className="px-8 py-3 bg-white/5 hover:bg-white/10 text-white font-bold rounded-xl transition-all"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -1272,6 +1196,150 @@ export default function PageBuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center p-16 text-center text-slate-500 bg-white/[0.02] border border-white/5 rounded-3xl min-h-[400px]">
|
||||||
|
<Settings2 className="w-12 h-12 mx-auto text-slate-700 mb-4" />
|
||||||
|
<p>Select a block from the left to edit its content</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Reorder Layout Modal */}
|
||||||
|
{isReorderModalOpen && (
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||||
|
<div className="bg-night-900 border border-white/10 rounded-3xl shadow-2xl w-full max-w-xl flex flex-col max-h-[85vh] overflow-hidden animate-pop-in text-slate-200">
|
||||||
|
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
|
||||||
|
<h3 className="text-xl font-bold flex items-center gap-2">
|
||||||
|
<Layers className="w-5 h-5 text-cyan-glow" />
|
||||||
|
Reorder Layout
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsReorderModalOpen(false)}
|
||||||
|
className="p-2 rounded-xl hover:bg-white/5 text-slate-400 hover:text-white transition-all"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-3">
|
||||||
|
{sections.length === 0 ? (
|
||||||
|
<div className="p-16 text-center text-slate-500 space-y-4 bg-white/[0.02] border border-white/5 rounded-2xl">
|
||||||
|
<Layers className="w-12 h-12 mx-auto text-slate-700" />
|
||||||
|
<p>No content sections in this page yet.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
sections.map((section, idx) => (
|
||||||
|
<div
|
||||||
|
key={section.id}
|
||||||
|
className={`py-2 px-3 flex items-center justify-between bg-white/[0.02] border rounded-xl transition-all group ${
|
||||||
|
section.isActive ? 'border-l-2 border-l-cyan-glow border-y-white/5 border-r-white/5 hover:border-cyan-glow/30' : 'border-l-2 border-l-slate-600 border-y-white/5 border-r-white/5 opacity-80 hover:border-slate-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 overflow-hidden flex-1">
|
||||||
|
{/* Index & Order controls */}
|
||||||
|
<div className="flex items-center gap-1 bg-black/20 px-1.5 py-1 rounded-lg border border-white/5 shrink-0">
|
||||||
|
<span className="text-[10px] font-mono text-slate-400 font-bold w-3 text-center">{idx + 1}</span>
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<button
|
||||||
|
onClick={() => handleMove(idx, 'up')}
|
||||||
|
disabled={idx === 0}
|
||||||
|
className="hover:text-white disabled:opacity-20 text-slate-400 transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowUp className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleMove(idx, 'down')}
|
||||||
|
disabled={idx === sections.length - 1}
|
||||||
|
className="hover:text-white disabled:opacity-20 text-slate-400 transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowDown className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section Description */}
|
||||||
|
<div className="flex items-center gap-2 truncate">
|
||||||
|
<span className="text-[9px] font-bold font-mono px-1.5 py-0.5 bg-slate-800 rounded border border-white/5 text-slate-300 uppercase tracking-wider shrink-0">
|
||||||
|
{section.type}
|
||||||
|
</span>
|
||||||
|
{!section.isActive && (
|
||||||
|
<span className="text-[9px] uppercase font-bold tracking-widest text-slate-500 bg-slate-900 px-1.5 py-0.5 rounded border border-white/10 shrink-0">
|
||||||
|
Hidden
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<h3 className="text-xs font-bold text-slate-100 truncate">
|
||||||
|
{getAdminDisplayString(
|
||||||
|
section.payload?.title
|
||||||
|
|| section.payload?.tagline
|
||||||
|
|| section.payload?.quote
|
||||||
|
|| (section.type?.toUpperCase() === 'ANNOUNCEMENT_BAR' ? section.payload?.slides?.[0] : undefined)
|
||||||
|
) || `Block #${section.id}`}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-1 opacity-100 sm:opacity-50 group-hover:opacity-100 transition-opacity shrink-0 ml-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleToggleActive(section)}
|
||||||
|
className={`p-1.5 rounded-lg border transition-all ${
|
||||||
|
section.isActive
|
||||||
|
? 'bg-cyan-glow/10 border-cyan-glow/20 text-cyan-glow hover:bg-cyan-glow/20'
|
||||||
|
: 'bg-white/5 border-white/5 text-slate-400 hover:bg-white/10 hover:text-white'
|
||||||
|
}`}
|
||||||
|
title={section.isActive ? 'Hide Section' : 'Show Section'}
|
||||||
|
>
|
||||||
|
{section.isActive ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsReorderModalOpen(false);
|
||||||
|
openEditModal(section);
|
||||||
|
}}
|
||||||
|
className="p-1.5 bg-white/5 border border-white/5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all"
|
||||||
|
title="Edit Block Content"
|
||||||
|
>
|
||||||
|
<Edit className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteSection(section.id)}
|
||||||
|
className="p-1.5 bg-rose-500/10 border border-rose-500/10 rounded-lg text-rose-500 hover:bg-rose-500 hover:text-white transition-all"
|
||||||
|
title="Delete Block"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 border-t border-white/5 flex gap-3 bg-white/[0.02]">
|
||||||
|
{sections.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={handleSaveOrder}
|
||||||
|
disabled={saveStatus === 'saving'}
|
||||||
|
className="flex-1 flex justify-center items-center gap-2 px-5 py-2.5 bg-cyan-glow hover:bg-cyan-glow/90 text-night-900 shadow-lg shadow-cyan-glow/20 disabled:opacity-50 rounded-xl font-bold text-sm uppercase tracking-wider transition-all"
|
||||||
|
>
|
||||||
|
{saveStatus === 'saving' ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
Save Order
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsReorderModalOpen(false)}
|
||||||
|
className="px-6 py-2.5 bg-white/5 hover:bg-white/10 text-white font-bold rounded-xl transition-all text-sm uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user