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;
|
||||
}
|
||||
|
||||
/* Subpage Card */
|
||||
.subpage-card {
|
||||
background: var(--admin-bg);
|
||||
/* Subpage Overview Grid */
|
||||
.subpage-grid {
|
||||
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-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;
|
||||
}
|
||||
|
||||
.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;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -659,7 +732,7 @@
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
}
|
||||
|
||||
.subpage-header-left {
|
||||
.subpage-detail-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -667,9 +740,14 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subpage-detail-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.subpage-move-btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@@ -700,12 +778,6 @@
|
||||
background: var(--admin-bg-input);
|
||||
}
|
||||
|
||||
.subpage-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.subpage-meta {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
|
||||
@@ -88,6 +88,7 @@ export default function PageBuilder() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [saveMessage, setSaveMessage] = useState('');
|
||||
const [expandedSubpage, setExpandedSubpage] = useState<number | null>(null);
|
||||
const [pickerTarget, setPickerTarget] = useState<{
|
||||
type: 'standalone' | 'subpage' | 'section';
|
||||
subpageIndex?: number;
|
||||
@@ -425,6 +426,24 @@ export default function PageBuilder() {
|
||||
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 ───────────────────────────────────────────────────
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -542,26 +561,71 @@ export default function PageBuilder() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{gallery.subpages.map((sp, spIndex) => (
|
||||
<div key={spIndex} className="subpage-card">
|
||||
<div className="subpage-header">
|
||||
<div className="subpage-header-left">
|
||||
{/* Collapsed overview grid */}
|
||||
<div className="subpage-grid">
|
||||
{gallery.subpages.map((sp, spIndex) => {
|
||||
const totalAlbums =
|
||||
sp.albums.length +
|
||||
(sp.sections?.reduce((sum, sec) => sum + sec.albums.length, 0) || 0);
|
||||
const firstThumb = getFirstSubpageThumb(sp);
|
||||
return (
|
||||
<div
|
||||
key={spIndex}
|
||||
className={`subpage-tile ${expandedSubpage === spIndex ? 'active' : ''}`}
|
||||
onClick={() =>
|
||||
setExpandedSubpage(expandedSubpage === spIndex ? null : spIndex)
|
||||
}
|
||||
>
|
||||
<div className="subpage-tile-cover">
|
||||
{firstThumb ? (
|
||||
<img src={`/api/admin/thumbnail/${firstThumb}`} alt="" loading="lazy" />
|
||||
) : (
|
||||
<div className="subpage-tile-placeholder">📂</div>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Expanded subpage detail */}
|
||||
{expandedSubpage !== null && gallery.subpages[expandedSubpage] && (
|
||||
<div className="subpage-detail">
|
||||
{(() => {
|
||||
const sp = gallery.subpages[expandedSubpage];
|
||||
const spIndex = expandedSubpage;
|
||||
return (
|
||||
<>
|
||||
<div className="subpage-detail-header">
|
||||
<div className="subpage-detail-header-left">
|
||||
<div className="subpage-move-btns">
|
||||
<button
|
||||
className="admin-btn-icon"
|
||||
onClick={() => moveSubpage(spIndex, -1)}
|
||||
onClick={() => {
|
||||
moveSubpage(spIndex, -1);
|
||||
setExpandedSubpage(spIndex - 1);
|
||||
}}
|
||||
disabled={spIndex === 0}
|
||||
title="Move up"
|
||||
>
|
||||
↑
|
||||
←
|
||||
</button>
|
||||
<button
|
||||
className="admin-btn-icon"
|
||||
onClick={() => moveSubpage(spIndex, 1)}
|
||||
onClick={() => {
|
||||
moveSubpage(spIndex, 1);
|
||||
setExpandedSubpage(spIndex + 1);
|
||||
}}
|
||||
disabled={spIndex === gallery.subpages.length - 1}
|
||||
title="Move down"
|
||||
>
|
||||
↓
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
@@ -569,12 +633,23 @@ export default function PageBuilder() {
|
||||
value={sp.name}
|
||||
onChange={(e) => updateSubpage(spIndex, { name: e.target.value })}
|
||||
placeholder="Page name (used for URL)"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
<div className="subpage-header-right">
|
||||
<div className="subpage-detail-header-right">
|
||||
<button
|
||||
className="admin-btn-icon"
|
||||
onClick={() => removeSubpage(spIndex)}
|
||||
onClick={() => setExpandedSubpage(null)}
|
||||
title="Collapse"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<button
|
||||
className="admin-btn-icon"
|
||||
onClick={() => {
|
||||
removeSubpage(spIndex);
|
||||
setExpandedSubpage(null);
|
||||
}}
|
||||
title="Delete subpage"
|
||||
>
|
||||
🗑
|
||||
@@ -589,7 +664,9 @@ export default function PageBuilder() {
|
||||
<label>Title (optional)</label>
|
||||
<input
|
||||
value={sp.title || ''}
|
||||
onChange={(e) => updateSubpage(spIndex, { title: e.target.value || undefined })}
|
||||
onChange={(e) =>
|
||||
updateSubpage(spIndex, { title: e.target.value || undefined })
|
||||
}
|
||||
placeholder="Display title (defaults to name)"
|
||||
/>
|
||||
</div>
|
||||
@@ -626,7 +703,9 @@ export default function PageBuilder() {
|
||||
<span>Albums</span>
|
||||
<button
|
||||
className="admin-btn admin-btn-xs"
|
||||
onClick={() => setPickerTarget({ type: 'subpage', subpageIndex: spIndex })}
|
||||
onClick={() =>
|
||||
setPickerTarget({ type: 'subpage', subpageIndex: spIndex })
|
||||
}
|
||||
>
|
||||
+ Add
|
||||
</button>
|
||||
@@ -738,20 +817,28 @@ export default function PageBuilder() {
|
||||
)}
|
||||
|
||||
<div className="subpage-actions">
|
||||
<button className="admin-btn admin-btn-xs" onClick={() => addSection(spIndex)}>
|
||||
<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 })}
|
||||
onClick={() =>
|
||||
setPickerTarget({ type: 'subpage', subpageIndex: spIndex })
|
||||
}
|
||||
>
|
||||
+ Add Album (outside sections)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Album Picker Modal */}
|
||||
|
||||
Reference in New Issue
Block a user