diff --git a/app/admin/admin.css b/app/admin/admin.css index 34f7639..2f651f9 100644 --- a/app/admin/admin.css +++ b/app/admin/admin.css @@ -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); diff --git a/app/admin/components/PageBuilder.tsx b/app/admin/components/PageBuilder.tsx index b5f258d..b4869d9 100644 --- a/app/admin/components/PageBuilder.tsx +++ b/app/admin/components/PageBuilder.tsx @@ -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(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,216 +561,284 @@ export default function PageBuilder() {

)} - {gallery.subpages.map((sp, spIndex) => ( -
-
-
-
- - + {/* Collapsed overview 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 ( +
+ setExpandedSubpage(expandedSubpage === spIndex ? null : spIndex) + } + > +
+ {firstThumb ? ( + + ) : ( +
📂
+ )} +
+
+ {sp.title || sp.name} + + {totalAlbums} album{totalAlbums !== 1 ? 's' : ''} +
- updateSubpage(spIndex, { name: e.target.value })} - placeholder="Page name (used for URL)" - />
-
- -
-
+ ); + })} +
- {/* Subpage metadata */} -
-
-
- - updateSubpage(spIndex, { title: e.target.value || undefined })} - placeholder="Display title (defaults to name)" - /> -
-
- - - updateSubpage(spIndex, { subtitle: e.target.value || undefined }) - } - placeholder="Subtitle text" - /> -
-
-
-
- - - updateSubpage(spIndex, { password: e.target.value || undefined }) - } - placeholder="Leave empty for public" - /> -
-
-
- - {/* Albums (if no sections) */} - {(!sp.sections || sp.sections.length === 0) && ( -
-
- Albums - -
-
- {sp.albums.map((album, aIndex) => ( - 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(); - }} - /> - ))} -
-
- )} - - {/* Sections */} - {sp.sections && sp.sections.length > 0 && ( -
- {sp.sections.map((sec, secIndex) => ( -
-
+ {/* Expanded subpage detail */} + {expandedSubpage !== null && gallery.subpages[expandedSubpage] && ( +
+ {(() => { + const sp = gallery.subpages[expandedSubpage]; + const spIndex = expandedSubpage; + return ( + <> +
+
+
+ + +
- updateSection(spIndex, secIndex, { title: e.target.value }) - } - placeholder="Section title" + className="subpage-name-input" + value={sp.name} + onChange={(e) => updateSubpage(spIndex, { name: e.target.value })} + placeholder="Page name (used for URL)" + onClick={(e) => e.stopPropagation()} /> +
+
+
-
- - updateSection(spIndex, secIndex, { - description: e.target.value || undefined, - }) - } - placeholder="Section description (optional)" - className="section-desc-input" - /> -
-
- {sec.albums.map((album, aIndex) => ( - 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(); - }} +
+ + {/* Subpage metadata */} +
+
+
+ + + updateSubpage(spIndex, { title: e.target.value || undefined }) + } + placeholder="Display title (defaults to name)" /> +
+
+ + + updateSubpage(spIndex, { subtitle: e.target.value || undefined }) + } + placeholder="Subtitle text" + /> +
+
+
+
+ + + updateSubpage(spIndex, { password: e.target.value || undefined }) + } + placeholder="Leave empty for public" + /> +
+
+
+ + {/* Albums (if no sections) */} + {(!sp.sections || sp.sections.length === 0) && ( +
+
+ Albums + +
+
+ {sp.albums.map((album, aIndex) => ( + 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(); + }} + /> + ))} +
+
+ )} + + {/* Sections */} + {sp.sections && sp.sections.length > 0 && ( +
+ {sp.sections.map((sec, secIndex) => ( +
+
+ + updateSection(spIndex, secIndex, { title: e.target.value }) + } + placeholder="Section title" + /> + +
+
+ + updateSection(spIndex, secIndex, { + description: e.target.value || undefined, + }) + } + placeholder="Section description (optional)" + className="section-desc-input" + /> +
+
+ {sec.albums.map((album, aIndex) => ( + 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(); + }} + /> + ))} +
+ +
))}
+ )} + +
+ {sp.sections && sp.sections.length > 0 && ( + + )}
- ))} -
- )} - -
- - {sp.sections && sp.sections.length > 0 && ( - - )} -
+ + ); + })()}
- ))} + )} {/* Album Picker Modal */}