feat: redesign subpages as collapsible tile grid for better overview
- Subpages shown as compact visual tiles in a grid (like folders) - Each tile shows the first album's cover image + name + album count - Click a tile to expand/edit that subpage's details below - Active tile highlighted with accent border - Only one subpage expanded at a time — no more endless scrolling - Move arrows changed to ← → (horizontal) matching the grid layout - Smooth slide-down animation when expanding a subpage
This commit is contained in:
@@ -641,16 +641,89 @@
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subpage Card */
|
/* Subpage Overview Grid */
|
||||||
.subpage-card {
|
.subpage-grid {
|
||||||
background: var(--admin-bg);
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-tile {
|
||||||
|
background: var(--admin-bg-raised);
|
||||||
border: 1px solid var(--admin-border);
|
border: 1px solid var(--admin-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 1rem;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-tile:hover {
|
||||||
|
border-color: var(--admin-border-hover);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-tile.active {
|
||||||
|
border-color: var(--admin-accent);
|
||||||
|
box-shadow: 0 0 0 1px var(--admin-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-tile-cover {
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
background: var(--admin-bg-elevated);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subpage-header {
|
.subpage-tile-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-tile-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-tile-info {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-tile-name {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-tile-meta {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subpage Expanded Detail */
|
||||||
|
.subpage-detail {
|
||||||
|
background: var(--admin-bg);
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: slideDown 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from { opacity: 0; transform: translateY(-4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -659,7 +732,7 @@
|
|||||||
border-bottom: 1px solid var(--admin-border);
|
border-bottom: 1px solid var(--admin-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subpage-header-left {
|
.subpage-detail-header-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -667,9 +740,14 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subpage-detail-header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.subpage-move-btns {
|
.subpage-move-btns {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,12 +778,6 @@
|
|||||||
background: var(--admin-bg-input);
|
background: var(--admin-bg-input);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subpage-header-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subpage-meta {
|
.subpage-meta {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-bottom: 1px solid var(--admin-border);
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export default function PageBuilder() {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [saveMessage, setSaveMessage] = useState('');
|
const [saveMessage, setSaveMessage] = useState('');
|
||||||
|
const [expandedSubpage, setExpandedSubpage] = useState<number | null>(null);
|
||||||
const [pickerTarget, setPickerTarget] = useState<{
|
const [pickerTarget, setPickerTarget] = useState<{
|
||||||
type: 'standalone' | 'subpage' | 'section';
|
type: 'standalone' | 'subpage' | 'section';
|
||||||
subpageIndex?: number;
|
subpageIndex?: number;
|
||||||
@@ -425,6 +426,24 @@ export default function PageBuilder() {
|
|||||||
return found?.thumbnailAssetId || null;
|
return found?.thumbnailAssetId || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFirstSubpageThumb(sp: Subpage): string | null {
|
||||||
|
// Try albums first
|
||||||
|
for (const album of sp.albums) {
|
||||||
|
const thumb = getAlbumThumbnailId(album.id);
|
||||||
|
if (thumb) return thumb;
|
||||||
|
}
|
||||||
|
// Try sections
|
||||||
|
if (sp.sections) {
|
||||||
|
for (const sec of sp.sections) {
|
||||||
|
for (const album of sec.albums) {
|
||||||
|
const thumb = getAlbumThumbnailId(album.id);
|
||||||
|
if (thumb) return thumb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Render ───────────────────────────────────────────────────
|
// ── Render ───────────────────────────────────────────────────
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -542,216 +561,284 @@ export default function PageBuilder() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{gallery.subpages.map((sp, spIndex) => (
|
{/* Collapsed overview grid */}
|
||||||
<div key={spIndex} className="subpage-card">
|
<div className="subpage-grid">
|
||||||
<div className="subpage-header">
|
{gallery.subpages.map((sp, spIndex) => {
|
||||||
<div className="subpage-header-left">
|
const totalAlbums =
|
||||||
<div className="subpage-move-btns">
|
sp.albums.length +
|
||||||
<button
|
(sp.sections?.reduce((sum, sec) => sum + sec.albums.length, 0) || 0);
|
||||||
className="admin-btn-icon"
|
const firstThumb = getFirstSubpageThumb(sp);
|
||||||
onClick={() => moveSubpage(spIndex, -1)}
|
return (
|
||||||
disabled={spIndex === 0}
|
<div
|
||||||
title="Move up"
|
key={spIndex}
|
||||||
>
|
className={`subpage-tile ${expandedSubpage === spIndex ? 'active' : ''}`}
|
||||||
↑
|
onClick={() =>
|
||||||
</button>
|
setExpandedSubpage(expandedSubpage === spIndex ? null : spIndex)
|
||||||
<button
|
}
|
||||||
className="admin-btn-icon"
|
>
|
||||||
onClick={() => moveSubpage(spIndex, 1)}
|
<div className="subpage-tile-cover">
|
||||||
disabled={spIndex === gallery.subpages.length - 1}
|
{firstThumb ? (
|
||||||
title="Move down"
|
<img src={`/api/admin/thumbnail/${firstThumb}`} alt="" loading="lazy" />
|
||||||
>
|
) : (
|
||||||
↓
|
<div className="subpage-tile-placeholder">📂</div>
|
||||||
</button>
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="subpage-tile-info">
|
||||||
|
<span className="subpage-tile-name">{sp.title || sp.name}</span>
|
||||||
|
<span className="subpage-tile-meta">
|
||||||
|
{totalAlbums} album{totalAlbums !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
className="subpage-name-input"
|
|
||||||
value={sp.name}
|
|
||||||
onChange={(e) => updateSubpage(spIndex, { name: e.target.value })}
|
|
||||||
placeholder="Page name (used for URL)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="subpage-header-right">
|
);
|
||||||
<button
|
})}
|
||||||
className="admin-btn-icon"
|
</div>
|
||||||
onClick={() => removeSubpage(spIndex)}
|
|
||||||
title="Delete subpage"
|
|
||||||
>
|
|
||||||
🗑
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Subpage metadata */}
|
{/* Expanded subpage detail */}
|
||||||
<div className="subpage-meta">
|
{expandedSubpage !== null && gallery.subpages[expandedSubpage] && (
|
||||||
<div className="admin-field-row">
|
<div className="subpage-detail">
|
||||||
<div className="admin-field">
|
{(() => {
|
||||||
<label>Title (optional)</label>
|
const sp = gallery.subpages[expandedSubpage];
|
||||||
<input
|
const spIndex = expandedSubpage;
|
||||||
value={sp.title || ''}
|
return (
|
||||||
onChange={(e) => updateSubpage(spIndex, { title: e.target.value || undefined })}
|
<>
|
||||||
placeholder="Display title (defaults to name)"
|
<div className="subpage-detail-header">
|
||||||
/>
|
<div className="subpage-detail-header-left">
|
||||||
</div>
|
<div className="subpage-move-btns">
|
||||||
<div className="admin-field">
|
<button
|
||||||
<label>Subtitle</label>
|
className="admin-btn-icon"
|
||||||
<input
|
onClick={() => {
|
||||||
value={sp.subtitle || ''}
|
moveSubpage(spIndex, -1);
|
||||||
onChange={(e) =>
|
setExpandedSubpage(spIndex - 1);
|
||||||
updateSubpage(spIndex, { subtitle: e.target.value || undefined })
|
}}
|
||||||
}
|
disabled={spIndex === 0}
|
||||||
placeholder="Subtitle text"
|
title="Move up"
|
||||||
/>
|
>
|
||||||
</div>
|
←
|
||||||
</div>
|
</button>
|
||||||
<div className="admin-field-row">
|
<button
|
||||||
<div className="admin-field">
|
className="admin-btn-icon"
|
||||||
<label>Password (optional)</label>
|
onClick={() => {
|
||||||
<input
|
moveSubpage(spIndex, 1);
|
||||||
type="password"
|
setExpandedSubpage(spIndex + 1);
|
||||||
value={sp.password || ''}
|
}}
|
||||||
onChange={(e) =>
|
disabled={spIndex === gallery.subpages.length - 1}
|
||||||
updateSubpage(spIndex, { password: e.target.value || undefined })
|
title="Move down"
|
||||||
}
|
>
|
||||||
placeholder="Leave empty for public"
|
→
|
||||||
/>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Albums (if no sections) */}
|
|
||||||
{(!sp.sections || sp.sections.length === 0) && (
|
|
||||||
<div className="subpage-albums">
|
|
||||||
<div className="subpage-albums-header">
|
|
||||||
<span>Albums</span>
|
|
||||||
<button
|
|
||||||
className="admin-btn admin-btn-xs"
|
|
||||||
onClick={() => setPickerTarget({ type: 'subpage', subpageIndex: spIndex })}
|
|
||||||
>
|
|
||||||
+ Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="album-list">
|
|
||||||
{sp.albums.map((album, aIndex) => (
|
|
||||||
<AlbumCard
|
|
||||||
key={`${album.id}-${aIndex}`}
|
|
||||||
album={album}
|
|
||||||
name={getAlbumName(album.id)}
|
|
||||||
count={getAlbumCount(album.id)}
|
|
||||||
thumbnailId={getAlbumThumbnailId(album.id)}
|
|
||||||
onRemove={() => removeSubpageAlbum(spIndex, aIndex)}
|
|
||||||
onUpdate={(updates) => {
|
|
||||||
setGallery((g) => {
|
|
||||||
const subpages = [...g.subpages];
|
|
||||||
const sp2 = { ...subpages[spIndex] };
|
|
||||||
const albums = [...sp2.albums];
|
|
||||||
albums[aIndex] = { ...albums[aIndex], ...updates };
|
|
||||||
sp2.albums = albums;
|
|
||||||
subpages[spIndex] = sp2;
|
|
||||||
return { ...g, subpages };
|
|
||||||
});
|
|
||||||
markDirty();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Sections */}
|
|
||||||
{sp.sections && sp.sections.length > 0 && (
|
|
||||||
<div className="subpage-sections">
|
|
||||||
{sp.sections.map((sec, secIndex) => (
|
|
||||||
<div key={secIndex} className="section-card">
|
|
||||||
<div className="section-header">
|
|
||||||
<input
|
<input
|
||||||
className="section-title-input"
|
className="subpage-name-input"
|
||||||
value={sec.title}
|
value={sp.name}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateSubpage(spIndex, { name: e.target.value })}
|
||||||
updateSection(spIndex, secIndex, { title: e.target.value })
|
placeholder="Page name (used for URL)"
|
||||||
}
|
onClick={(e) => e.stopPropagation()}
|
||||||
placeholder="Section title"
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="subpage-detail-header-right">
|
||||||
<button
|
<button
|
||||||
className="admin-btn-icon"
|
className="admin-btn-icon"
|
||||||
onClick={() => removeSection(spIndex, secIndex)}
|
onClick={() => setExpandedSubpage(null)}
|
||||||
title="Remove section"
|
title="Collapse"
|
||||||
>
|
>
|
||||||
×
|
✕
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="admin-btn-icon"
|
||||||
|
onClick={() => {
|
||||||
|
removeSubpage(spIndex);
|
||||||
|
setExpandedSubpage(null);
|
||||||
|
}}
|
||||||
|
title="Delete subpage"
|
||||||
|
>
|
||||||
|
🗑
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-field">
|
</div>
|
||||||
<input
|
|
||||||
value={sec.description || ''}
|
{/* Subpage metadata */}
|
||||||
onChange={(e) =>
|
<div className="subpage-meta">
|
||||||
updateSection(spIndex, secIndex, {
|
<div className="admin-field-row">
|
||||||
description: e.target.value || undefined,
|
<div className="admin-field">
|
||||||
})
|
<label>Title (optional)</label>
|
||||||
}
|
<input
|
||||||
placeholder="Section description (optional)"
|
value={sp.title || ''}
|
||||||
className="section-desc-input"
|
onChange={(e) =>
|
||||||
/>
|
updateSubpage(spIndex, { title: e.target.value || undefined })
|
||||||
</div>
|
}
|
||||||
<div className="album-list">
|
placeholder="Display title (defaults to name)"
|
||||||
{sec.albums.map((album, aIndex) => (
|
|
||||||
<AlbumCard
|
|
||||||
key={`${album.id}-${aIndex}`}
|
|
||||||
album={album}
|
|
||||||
name={getAlbumName(album.id)}
|
|
||||||
count={getAlbumCount(album.id)}
|
|
||||||
thumbnailId={getAlbumThumbnailId(album.id)}
|
|
||||||
onRemove={() => removeSectionAlbum(spIndex, secIndex, aIndex)}
|
|
||||||
onUpdate={(updates) => {
|
|
||||||
setGallery((g) => {
|
|
||||||
const subpages = [...g.subpages];
|
|
||||||
const sp2 = { ...subpages[spIndex] };
|
|
||||||
const sections = [...(sp2.sections || [])];
|
|
||||||
const sec2 = { ...sections[secIndex] };
|
|
||||||
const albums = [...sec2.albums];
|
|
||||||
albums[aIndex] = { ...albums[aIndex], ...updates };
|
|
||||||
sec2.albums = albums;
|
|
||||||
sections[secIndex] = sec2;
|
|
||||||
sp2.sections = sections;
|
|
||||||
subpages[spIndex] = sp2;
|
|
||||||
return { ...g, subpages };
|
|
||||||
});
|
|
||||||
markDirty();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="admin-field">
|
||||||
|
<label>Subtitle</label>
|
||||||
|
<input
|
||||||
|
value={sp.subtitle || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSubpage(spIndex, { subtitle: e.target.value || undefined })
|
||||||
|
}
|
||||||
|
placeholder="Subtitle text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-field-row">
|
||||||
|
<div className="admin-field">
|
||||||
|
<label>Password (optional)</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={sp.password || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSubpage(spIndex, { password: e.target.value || undefined })
|
||||||
|
}
|
||||||
|
placeholder="Leave empty for public"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Albums (if no sections) */}
|
||||||
|
{(!sp.sections || sp.sections.length === 0) && (
|
||||||
|
<div className="subpage-albums">
|
||||||
|
<div className="subpage-albums-header">
|
||||||
|
<span>Albums</span>
|
||||||
|
<button
|
||||||
|
className="admin-btn admin-btn-xs"
|
||||||
|
onClick={() =>
|
||||||
|
setPickerTarget({ type: 'subpage', subpageIndex: spIndex })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+ Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="album-list">
|
||||||
|
{sp.albums.map((album, aIndex) => (
|
||||||
|
<AlbumCard
|
||||||
|
key={`${album.id}-${aIndex}`}
|
||||||
|
album={album}
|
||||||
|
name={getAlbumName(album.id)}
|
||||||
|
count={getAlbumCount(album.id)}
|
||||||
|
thumbnailId={getAlbumThumbnailId(album.id)}
|
||||||
|
onRemove={() => removeSubpageAlbum(spIndex, aIndex)}
|
||||||
|
onUpdate={(updates) => {
|
||||||
|
setGallery((g) => {
|
||||||
|
const subpages = [...g.subpages];
|
||||||
|
const sp2 = { ...subpages[spIndex] };
|
||||||
|
const albums = [...sp2.albums];
|
||||||
|
albums[aIndex] = { ...albums[aIndex], ...updates };
|
||||||
|
sp2.albums = albums;
|
||||||
|
subpages[spIndex] = sp2;
|
||||||
|
return { ...g, subpages };
|
||||||
|
});
|
||||||
|
markDirty();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sections */}
|
||||||
|
{sp.sections && sp.sections.length > 0 && (
|
||||||
|
<div className="subpage-sections">
|
||||||
|
{sp.sections.map((sec, secIndex) => (
|
||||||
|
<div key={secIndex} className="section-card">
|
||||||
|
<div className="section-header">
|
||||||
|
<input
|
||||||
|
className="section-title-input"
|
||||||
|
value={sec.title}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSection(spIndex, secIndex, { title: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Section title"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="admin-btn-icon"
|
||||||
|
onClick={() => removeSection(spIndex, secIndex)}
|
||||||
|
title="Remove section"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-field">
|
||||||
|
<input
|
||||||
|
value={sec.description || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSection(spIndex, secIndex, {
|
||||||
|
description: e.target.value || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Section description (optional)"
|
||||||
|
className="section-desc-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="album-list">
|
||||||
|
{sec.albums.map((album, aIndex) => (
|
||||||
|
<AlbumCard
|
||||||
|
key={`${album.id}-${aIndex}`}
|
||||||
|
album={album}
|
||||||
|
name={getAlbumName(album.id)}
|
||||||
|
count={getAlbumCount(album.id)}
|
||||||
|
thumbnailId={getAlbumThumbnailId(album.id)}
|
||||||
|
onRemove={() => removeSectionAlbum(spIndex, secIndex, aIndex)}
|
||||||
|
onUpdate={(updates) => {
|
||||||
|
setGallery((g) => {
|
||||||
|
const subpages = [...g.subpages];
|
||||||
|
const sp2 = { ...subpages[spIndex] };
|
||||||
|
const sections = [...(sp2.sections || [])];
|
||||||
|
const sec2 = { ...sections[secIndex] };
|
||||||
|
const albums = [...sec2.albums];
|
||||||
|
albums[aIndex] = { ...albums[aIndex], ...updates };
|
||||||
|
sec2.albums = albums;
|
||||||
|
sections[secIndex] = sec2;
|
||||||
|
sp2.sections = sections;
|
||||||
|
subpages[spIndex] = sp2;
|
||||||
|
return { ...g, subpages };
|
||||||
|
});
|
||||||
|
markDirty();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="admin-btn admin-btn-xs"
|
||||||
|
onClick={() =>
|
||||||
|
setPickerTarget({
|
||||||
|
type: 'section',
|
||||||
|
subpageIndex: spIndex,
|
||||||
|
sectionIndex: secIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+ Add Album
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="subpage-actions">
|
||||||
<button
|
<button
|
||||||
className="admin-btn admin-btn-xs"
|
className="admin-btn admin-btn-xs"
|
||||||
onClick={() =>
|
onClick={() => addSection(spIndex)}
|
||||||
setPickerTarget({
|
|
||||||
type: 'section',
|
|
||||||
subpageIndex: spIndex,
|
|
||||||
sectionIndex: secIndex,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
+ Add Album
|
+ Add Section
|
||||||
</button>
|
</button>
|
||||||
|
{sp.sections && sp.sections.length > 0 && (
|
||||||
|
<button
|
||||||
|
className="admin-btn admin-btn-xs"
|
||||||
|
onClick={() =>
|
||||||
|
setPickerTarget({ type: 'subpage', subpageIndex: spIndex })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+ Add Album (outside sections)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</>
|
||||||
</div>
|
);
|
||||||
)}
|
})()}
|
||||||
|
|
||||||
<div className="subpage-actions">
|
|
||||||
<button className="admin-btn admin-btn-xs" onClick={() => addSection(spIndex)}>
|
|
||||||
+ Add Section
|
|
||||||
</button>
|
|
||||||
{sp.sections && sp.sections.length > 0 && (
|
|
||||||
<button
|
|
||||||
className="admin-btn admin-btn-xs"
|
|
||||||
onClick={() => setPickerTarget({ type: 'subpage', subpageIndex: spIndex })}
|
|
||||||
>
|
|
||||||
+ Add Album (outside sections)
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Album Picker Modal */}
|
{/* Album Picker Modal */}
|
||||||
|
|||||||
Reference in New Issue
Block a user