x
This commit is contained in:
1 parent
50b93eb629
commit
cd4b89a0fc
1 file changed
+328
-260
@@ -57,11 +57,11 @@ export default function PageBuilderPage() {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [scents, setScents] = useState<Scent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingSection, setEditingSection] = useState<Partial<PageSection> | null>(null);
|
||||
const [uploadTarget, setUploadTarget] = useState<{ type: 'main' | 'slide'; index?: number } | null>(null);
|
||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
||||
const [isSavingPageSettings, setIsSavingPageSettings] = useState(false);
|
||||
const [isReorderModalOpen, setIsReorderModalOpen] = useState(false);
|
||||
|
||||
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false);
|
||||
|
||||
@@ -440,6 +440,7 @@ export default function PageBuilderPage() {
|
||||
|
||||
if (res.ok) {
|
||||
setSaveStatus('saved');
|
||||
setIsReorderModalOpen(false);
|
||||
setTimeout(() => setSaveStatus('idle'), 3000);
|
||||
} else {
|
||||
setSaveStatus('error');
|
||||
@@ -497,12 +498,12 @@ export default function PageBuilderPage() {
|
||||
buttonLink: '/shop'
|
||||
}
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const openEditModal = (section: PageSection) => {
|
||||
setEditingSection(JSON.parse(JSON.stringify(section))); // deep copy
|
||||
setIsModalOpen(true);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const handleTypeChange = (type: string) => {
|
||||
@@ -705,7 +706,7 @@ export default function PageBuilderPage() {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setIsModalOpen(false);
|
||||
setEditingSection(null);
|
||||
if (selectedPageId) fetchPageDetails(selectedPageId);
|
||||
} else {
|
||||
const errData = await res.json();
|
||||
@@ -718,157 +719,33 @@ export default function PageBuilderPage() {
|
||||
|
||||
return (
|
||||
<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="space-y-2">
|
||||
<div className="flex items-center gap-6">
|
||||
<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">
|
||||
<span className="text-xs text-slate-400 uppercase tracking-widest font-bold">Select Page:</span>
|
||||
<select
|
||||
value={selectedPageId || ''}
|
||||
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) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.title} ({p.slug})
|
||||
{p.title} (/{p.slug})
|
||||
</option>
|
||||
))}
|
||||
</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
|
||||
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>
|
||||
|
||||
{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>
|
||||
|
||||
{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>
|
||||
|
||||
{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">
|
||||
@@ -890,149 +767,196 @@ export default function PageBuilderPage() {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-night-950/40 border border-white/5 rounded-2xl overflow-hidden">
|
||||
<div className="p-6 border-b border-white/5 bg-white/[0.01]">
|
||||
<h2 className="text-lg font-bold">Homepage Blocks list</h2>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-white/5">
|
||||
{sections.length === 0 ? (
|
||||
<div className="p-16 text-center text-slate-500 space-y-4">
|
||||
<Layers className="w-12 h-12 mx-auto text-slate-700" />
|
||||
<p>No content sections in this page yet. Start by adding one!</p>
|
||||
</div>
|
||||
) : (
|
||||
sections.map((section, idx) => (
|
||||
<div
|
||||
key={section.id}
|
||||
className={`p-6 flex items-center justify-between transition-all duration-300 hover:bg-white/[0.02] ${
|
||||
!section.isActive ? 'opacity-50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-6">
|
||||
{/* 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>
|
||||
<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 className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
||||
<div className="lg:col-span-5 space-y-4">
|
||||
{/* 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>
|
||||
|
||||
{/* 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}
|
||||
<div className="flex items-center gap-4">
|
||||
<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 focus:ring-offset-slate-900"
|
||||
/>
|
||||
<span className="text-sm font-bold text-slate-200">
|
||||
Publish to Website
|
||||
</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>
|
||||
)}
|
||||
</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'
|
||||
}`}
|
||||
>
|
||||
{page.isPublished ? 'Published' : 'Draft'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-3 w-full">
|
||||
<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'}
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
{section.isActive ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
|
||||
{isSavingPageSettings ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-4 h-4" />
|
||||
)}
|
||||
Save Settings
|
||||
</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"
|
||||
<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`}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
</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
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="p-2 rounded-xl hover:bg-white/5 text-slate-400 hover:text-white transition-all"
|
||||
{/* 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"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{['th', 'en', 'ja', 'zh', 'vi', 'ko'].map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
onClick={() => setEditingLang(lang)}
|
||||
className={`relative pb-3 text-xs font-bold uppercase tracking-widest border-b-2 transition-all whitespace-nowrap ${
|
||||
editingLang === lang
|
||||
? 'border-cyan-glow text-cyan-glow'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-300 hover:border-white/20'
|
||||
}`}
|
||||
>
|
||||
{lang}
|
||||
</button>
|
||||
))}
|
||||
{/* Premium Sub-header for Language Tabs */}
|
||||
<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) => (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
onClick={() => setEditingLang(lang)}
|
||||
className={`relative px-4 py-2 text-xs font-bold uppercase tracking-widest transition-all rounded-lg whitespace-nowrap ${
|
||||
editingLang === lang
|
||||
? 'bg-cyan-glow text-night-900 shadow-sm'
|
||||
: 'text-slate-400 hover:text-white hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
{lang}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleFormSubmit} className="flex-1 flex flex-col min-h-0">
|
||||
@@ -1264,7 +1188,7 @@ export default function PageBuilderPage() {
|
||||
</button>
|
||||
<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"
|
||||
>
|
||||
Cancel
|
||||
@@ -1272,6 +1196,150 @@ export default function PageBuilderPage() {
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user