feat(admin): visual theme cards, album search, and status dashboard (#241)

* feat(admin): implement visual theme cards, album search, and system status diagnostics

* fix(admin): resolve type-checking build errors in SettingsEditor and status route

---------

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
This commit is contained in:
Ralksta
2026-05-31 21:17:52 +02:00
committed by GitHub
parent e24cd33a09
commit 02f5e6a505
6 changed files with 647 additions and 55 deletions

View File

@@ -266,6 +266,7 @@ export default function PageBuilder() {
sectionIndex?: number;
} | null>(null);
const [showHeroPicker, setShowHeroPicker] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
// DnD sensors
const sensors = useSensors(
@@ -717,6 +718,39 @@ export default function PageBuilder() {
);
}
// Filter standalone albums
const filteredAlbums = gallery.albums.filter((album) => {
if (!searchQuery) return true;
const name = getAlbumName(album.id).toLowerCase();
const description = (album.description || '').toLowerCase();
const overrideTitle = (album.title || '').toLowerCase();
const query = searchQuery.toLowerCase();
return name.includes(query) || description.includes(query) || overrideTitle.includes(query) || album.id.toLowerCase().includes(query);
});
// Filter subpages
const filteredSubpages = gallery.subpages
.map((sp, index) => ({ sp, index }))
.filter(({ sp }) => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
const name = sp.name.toLowerCase();
const title = (sp.title || '').toLowerCase();
const subtitle = (sp.subtitle || '').toLowerCase();
if (name.includes(query) || title.includes(query) || subtitle.includes(query)) return true;
const albums = sp.albums || [];
const hasMatchingAlbum = albums.some((a) => {
const aName = getAlbumName(a.id).toLowerCase();
const aTitle = (a.title || '').toLowerCase();
const aDesc = (a.description || '').toLowerCase();
return aName.includes(query) || aTitle.includes(query) || aDesc.includes(query) || a.id.toLowerCase().includes(query);
});
return hasMatchingAlbum;
});
return (
<div className="page-builder">
{/* Save Bar */}
@@ -752,6 +786,25 @@ export default function PageBuilder() {
</div>
</div>
{/* Search Bar */}
<div className="builder-search-container">
<div className="builder-search-wrapper">
<span className="builder-search-icon">🔍</span>
<input
type="text"
className="builder-search-input"
placeholder="Search albums or subpages..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{searchQuery && (
<button className="builder-search-clear" onClick={() => setSearchQuery('')} title="Clear search">
×
</button>
)}
</div>
</div>
{/* Hero Section */}
<section className="builder-section">
<div className="builder-section-header">
@@ -805,34 +858,40 @@ export default function PageBuilder() {
onDragEnd={handleAlbumDragEnd}
>
<SortableContext
items={gallery.albums.map((a, i) => `album-${a.id}-${i}`)}
items={filteredAlbums.map((a) => {
const originalIndex = gallery.albums.findIndex((x) => x.id === a.id);
return `album-${a.id}-${originalIndex}`;
})}
strategy={verticalListSortingStrategy}
>
<div className="album-list">
{gallery.albums.length === 0 && (
{filteredAlbums.length === 0 && (
<p className="empty-hint">
No standalone albums. These show directly on the homepage.
{searchQuery ? 'No matching standalone albums found.' : 'No standalone albums. These show directly on the homepage.'}
</p>
)}
{gallery.albums.map((album, i) => (
<SortableAlbumCard
key={`${album.id}-${i}`}
album={album}
index={i}
name={getAlbumName(album.id)}
count={getAlbumCount(album.id)}
thumbnailId={getAlbumThumbnailId(album.id)}
onRemove={() => removeStandaloneAlbum(i)}
onUpdate={(updates) => {
setGallery((g) => {
const albums = [...g.albums];
albums[i] = { ...albums[i], ...updates };
return { ...g, albums };
});
markDirty();
}}
/>
))}
{filteredAlbums.map((album) => {
const originalIndex = gallery.albums.findIndex((a) => a.id === album.id);
return (
<SortableAlbumCard
key={`${album.id}-${originalIndex}`}
album={album}
index={originalIndex}
name={getAlbumName(album.id)}
count={getAlbumCount(album.id)}
thumbnailId={getAlbumThumbnailId(album.id)}
onRemove={() => removeStandaloneAlbum(originalIndex)}
onUpdate={(updates) => {
setGallery((g) => {
const albums = [...g.albums];
albums[originalIndex] = { ...albums[originalIndex], ...updates };
return { ...g, albums };
});
markDirty();
}}
/>
);
})}
</div>
</SortableContext>
</DndContext>
@@ -853,6 +912,12 @@ export default function PageBuilder() {
</p>
)}
{gallery.subpages.length > 0 && filteredSubpages.length === 0 && (
<p className="empty-hint">
No matching subpages found.
</p>
)}
{/* Collapsed overview grid with DnD */}
<DndContext
sensors={sensors}
@@ -860,17 +925,17 @@ export default function PageBuilder() {
onDragEnd={handleSubpageDragEnd}
>
<SortableContext
items={gallery.subpages.map((_, i) => `subpage-${i}`)}
items={filteredSubpages.map(({ index }) => `subpage-${index}`)}
strategy={horizontalListSortingStrategy}
>
<div className="subpage-grid">
{gallery.subpages.map((sp, spIndex) => (
{filteredSubpages.map(({ sp, index }) => (
<SortableSubpageTile
key={`subpage-${spIndex}`}
key={`subpage-${index}`}
sp={sp}
spIndex={spIndex}
isActive={expandedSubpage === spIndex}
onClick={() => setExpandedSubpage(expandedSubpage === spIndex ? null : spIndex)}
spIndex={index}
isActive={expandedSubpage === index}
onClick={() => setExpandedSubpage(expandedSubpage === index ? null : index)}
getFirstThumb={getFirstSubpageThumb}
/>
))}