Files
site-builder/craft/src/panels/left/PagesPanel.tsx
T

510 lines
16 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import { usePages } from '../../state/PageContext';
import { clickableProps } from '../../utils/a11y';
export const PagesPanel: React.FC = () => {
const {
pages,
activePageId,
isEditingHeader,
isEditingFooter,
switchPage,
editHeader,
editFooter,
addPage,
deletePage,
renamePage,
} = usePages();
const [isAdding, setIsAdding] = useState(false);
const [newName, setNewName] = useState('');
const [newSlug, setNewSlug] = useState('');
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState('');
const [editSlug, setEditSlug] = useState('');
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
const handleAdd = () => {
if (!newName.trim()) return;
addPage(newName.trim(), newSlug.trim());
setNewName('');
setNewSlug('');
setIsAdding(false);
};
const handleRename = (pageId: string) => {
if (!editName.trim()) return;
renamePage(pageId, editName.trim(), editSlug.trim());
setEditingId(null);
};
const handleDelete = (pageId: string) => {
deletePage(pageId);
setDeleteConfirmId(null);
};
const startEditing = (page: { id: string; name: string; slug: string }) => {
setEditingId(page.id);
setEditName(page.name);
setEditSlug(page.slug);
setDeleteConfirmId(null);
};
const autoSlug = (name: string): string => {
return name
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
};
/* ---------- Zone button style ---------- */
const zoneButtonStyle = (isActive: boolean): React.CSSProperties => ({
display: 'flex',
alignItems: 'center',
gap: 8,
width: '100%',
padding: '10px 12px',
fontSize: 12,
fontWeight: 600,
color: isActive ? '#f59e0b' : '#fbbf24',
background: isActive ? 'rgba(245, 158, 11, 0.15)' : 'rgba(245, 158, 11, 0.06)',
border: `1px solid ${isActive ? 'rgba(245, 158, 11, 0.5)' : 'rgba(245, 158, 11, 0.2)'}`,
borderRadius: 'var(--radius-md)',
cursor: 'pointer',
transition: 'all var(--transition-fast)',
textAlign: 'left' as const,
});
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{/* Header/Footer zone buttons */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 8 }}>
<button
onClick={editHeader}
style={zoneButtonStyle(isEditingHeader)}
>
<i className="fa fa-window-maximize" style={{ fontSize: 13 }} />
<div style={{ flex: 1 }}>
<div>Edit Header</div>
<div style={{ fontSize: 10, opacity: 0.7, fontWeight: 400, marginTop: 1 }}>
Appears on all pages
</div>
</div>
{isEditingHeader && (
<span style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.5px',
background: 'rgba(245, 158, 11, 0.25)',
padding: '2px 6px',
borderRadius: 'var(--radius-sm)',
}}>
Editing
</span>
)}
</button>
<button
onClick={editFooter}
style={zoneButtonStyle(isEditingFooter)}
>
<i className="fa fa-window-minimize" style={{ fontSize: 13 }} />
<div style={{ flex: 1 }}>
<div>Edit Footer</div>
<div style={{ fontSize: 10, opacity: 0.7, fontWeight: 400, marginTop: 1 }}>
Appears on all pages
</div>
</div>
{isEditingFooter && (
<span style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.5px',
background: 'rgba(245, 158, 11, 0.25)',
padding: '2px 6px',
borderRadius: 'var(--radius-sm)',
}}>
Editing
</span>
)}
</button>
</div>
{/* Section label for pages */}
<div style={{
fontSize: 10,
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.5px',
color: 'var(--color-text-dim)',
padding: '0 2px',
}}>
Pages
</div>
{/* Page list */}
{pages.map((page, pageIndex) => {
const isLanding = pageIndex === 0;
return (
<div key={page.id}>
{editingId === page.id ? (
/* Editing mode */
<div
style={{
padding: 10,
background: 'var(--color-bg-elevated)',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--color-accent)',
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
<input
type="text"
value={editName}
onChange={(e) => {
setEditName(e.target.value);
setEditSlug(autoSlug(e.target.value));
}}
placeholder="Page name"
className="control-input"
style={{ fontSize: 12 }}
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') handleRename(page.id);
if (e.key === 'Escape') setEditingId(null);
}}
/>
{isLanding ? (
<div style={{
fontSize: 10,
color: 'var(--color-text-dim)',
padding: '4px 2px',
fontStyle: 'italic',
}}>
Landing page URL locked to <code>/</code>
</div>
) : (
<input
type="text"
value={editSlug}
onChange={(e) => setEditSlug(e.target.value)}
placeholder="page-slug"
className="control-input"
style={{ fontSize: 11 }}
/>
)}
<div style={{ display: 'flex', gap: 6 }}>
<button
onClick={() => handleRename(page.id)}
style={{
flex: 1,
padding: '5px 10px',
fontSize: 11,
fontWeight: 600,
color: '#fff',
background: 'var(--color-accent)',
border: 'none',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
}}
>
Save
</button>
<button
onClick={() => setEditingId(null)}
style={{
flex: 1,
padding: '5px 10px',
fontSize: 11,
fontWeight: 600,
color: 'var(--color-text-muted)',
background: 'var(--color-bg-base)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
}}
>
Cancel
</button>
</div>
</div>
) : deleteConfirmId === page.id ? (
/* Delete confirmation */
<div
style={{
padding: 10,
background: 'var(--color-bg-elevated)',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--color-danger)',
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
<div style={{ fontSize: 12, color: 'var(--color-text)' }}>
Delete "{page.name}"?
</div>
<div style={{ display: 'flex', gap: 6 }}>
<button
onClick={() => handleDelete(page.id)}
style={{
flex: 1,
padding: '5px 10px',
fontSize: 11,
fontWeight: 600,
color: '#fff',
background: 'var(--color-danger)',
border: 'none',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
}}
>
Delete
</button>
<button
onClick={() => setDeleteConfirmId(null)}
style={{
flex: 1,
padding: '5px 10px',
fontSize: 11,
fontWeight: 600,
color: 'var(--color-text-muted)',
background: 'var(--color-bg-base)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
}}
>
Cancel
</button>
</div>
</div>
) : (
/* Normal page item */
<div
{...clickableProps(() => switchPage(page.id))}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 10px',
background:
page.id === activePageId
? 'var(--color-accent-subtle)'
: 'var(--color-bg-elevated)',
border: `1px solid ${page.id === activePageId ? 'var(--color-accent)' : 'var(--color-border)'}`,
borderRadius: 'var(--radius-md)',
cursor: 'pointer',
transition: 'all var(--transition-fast)',
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 12,
fontWeight: page.id === activePageId ? 600 : 500,
color:
page.id === activePageId
? 'var(--color-accent)'
: 'var(--color-text)',
display: 'flex',
alignItems: 'center',
gap: 6,
overflow: 'hidden',
}}
>
<span style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>{page.name}</span>
{isLanding && (
<span
title="This is the landing page — published as the root URL (index.html)"
style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.5px',
color: '#fbbf24',
background: 'rgba(245, 158, 11, 0.15)',
border: '1px solid rgba(245, 158, 11, 0.35)',
padding: '1px 5px',
borderRadius: 'var(--radius-sm)',
flexShrink: 0,
}}
>
<i className="fa fa-home" style={{ marginRight: 3 }} />Landing
</span>
)}
</div>
<div
style={{
fontSize: 10,
color: 'var(--color-text-dim)',
marginTop: 2,
}}
>
{isLanding ? '/' : '/' + page.slug}
</div>
</div>
<div
style={{ display: 'flex', gap: 4, flexShrink: 0 }}
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => startEditing(page)}
title="Rename"
style={{
width: 24,
height: 24,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 11,
color: 'var(--color-text-muted)',
background: 'transparent',
border: 'none',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
}}
>
&#9998;
</button>
{pages.length > 1 && !isLanding && (
<button
onClick={() => setDeleteConfirmId(page.id)}
title="Delete"
style={{
width: 24,
height: 24,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 11,
color: 'var(--color-text-muted)',
background: 'transparent',
border: 'none',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
}}
>
<i className="fa fa-trash" />
</button>
)}
</div>
</div>
)}
</div>
);
})}
{/* Add page section */}
{isAdding ? (
<div
style={{
padding: 10,
background: 'var(--color-bg-elevated)',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--color-border)',
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
<input
type="text"
value={newName}
onChange={(e) => {
setNewName(e.target.value);
setNewSlug(autoSlug(e.target.value));
}}
placeholder="Page name"
className="control-input"
style={{ fontSize: 12 }}
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') handleAdd();
if (e.key === 'Escape') {
setIsAdding(false);
setNewName('');
setNewSlug('');
}
}}
/>
<input
type="text"
value={newSlug}
onChange={(e) => setNewSlug(e.target.value)}
placeholder="page-slug"
className="control-input"
style={{ fontSize: 11 }}
/>
<div style={{ display: 'flex', gap: 6 }}>
<button
onClick={handleAdd}
disabled={!newName.trim()}
style={{
flex: 1,
padding: '5px 10px',
fontSize: 11,
fontWeight: 600,
color: '#fff',
background: newName.trim() ? 'var(--color-accent)' : 'var(--color-bg-active)',
border: 'none',
borderRadius: 'var(--radius-sm)',
cursor: newName.trim() ? 'pointer' : 'not-allowed',
}}
>
Add Page
</button>
<button
onClick={() => {
setIsAdding(false);
setNewName('');
setNewSlug('');
}}
style={{
flex: 1,
padding: '5px 10px',
fontSize: 11,
fontWeight: 600,
color: 'var(--color-text-muted)',
background: 'var(--color-bg-base)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
}}
>
Cancel
</button>
</div>
</div>
) : (
<button
onClick={() => setIsAdding(true)}
style={{
width: '100%',
padding: '8px 12px',
fontSize: 12,
fontWeight: 600,
color: 'var(--color-accent)',
background: 'var(--color-accent-subtle)',
border: '1px dashed var(--color-accent)',
borderRadius: 'var(--radius-md)',
cursor: 'pointer',
transition: 'all var(--transition-fast)',
}}
>
+ Add Page
</button>
)}
</div>
);
};