feat: redesign album drawer into a gorgeous centered two-column modal (#243)

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
This commit is contained in:
Ralksta
2026-05-31 21:30:00 +02:00
committed by GitHub
parent 8964877f6a
commit a504f7296a
2 changed files with 214 additions and 135 deletions

View File

@@ -1276,7 +1276,7 @@ export default function PageBuilder() {
/>
)}
{/* Slide-over Drawer for Album Details */}
{/* Slide-over Drawer for Album Details (Centered 2-Column Modal) */}
{(() => {
if (!editingAlbumAddress) return null;
const info = getEditingAlbumInfo(editingAlbumAddress);
@@ -1300,122 +1300,144 @@ export default function PageBuilder() {
</button>
</div>
<div className="album-drawer-body">
{/* Album Cover / Hero Preview */}
<div className="drawer-hero-section">
<div className="drawer-hero-cover">
{/* Left Column - Visual Info & Stats */}
<div className="modal-left-column">
<div className="modal-cover-container">
{heroThumb ? (
<img src={`/api/admin/thumbnail/${heroThumb}`} alt="" loading="lazy" />
) : (
<div className="drawer-hero-placeholder">
<div className="modal-cover-placeholder">
<Icons.Camera />
</div>
)}
</div>
<div className="drawer-hero-meta">
<span className="drawer-hero-name">{name}</span>
<span className="drawer-hero-count">{count} photos</span>
<div className="modal-meta-box">
<span className="modal-album-title">{album.title || name}</span>
<span className="modal-album-subtitle">Original: {name}</span>
</div>
<div className="modal-info-list">
<div className="modal-info-item">
<span className="modal-info-label">Photo Count:</span>
<span className="modal-info-value">{count} photos</span>
</div>
<div className="modal-info-item">
<span className="modal-info-label">Status:</span>
<span className="modal-info-value">
{album.password ? 'Password Protected' : 'Public Access'}
</span>
</div>
<div className="modal-info-item">
<span className="modal-info-label">Custom Hero:</span>
<span className="modal-info-value">
{album.heroImage ? 'Yes' : 'No'}
</span>
</div>
</div>
{/* Technical UUID */}
<div className="drawer-uuid-section">
<label className="admin-field-label">IMMICH ALBUM UUID</label>
<div className="uuid-copy-box">
<code>{album.id}</code>
<button
className="uuid-copy-btn"
onClick={() => {
navigator.clipboard.writeText(album.id);
}}
title="Copy UUID"
>
<Icons.Copy />
</button>
</div>
</div>
</div>
{/* Form Fields */}
<div className="admin-field">
<label>Title override</label>
<input
value={album.title || ''}
onChange={(e) => onUpdate({ title: e.target.value || undefined })}
placeholder={name}
/>
</div>
<div className="admin-field">
<label>Description</label>
<textarea
value={album.description || ''}
onChange={(e) => onUpdate({ description: e.target.value || undefined })}
placeholder="Optional description for visitors"
rows={3}
/>
</div>
<div className="admin-field">
<label>Password protection</label>
<div className="input-with-icon">
{/* Right Column - Form Fields */}
<div className="modal-right-column">
<div className="admin-field">
<label>Title override</label>
<input
type="password"
value={album.password || ''}
onChange={(e) => onUpdate({ password: e.target.value || undefined })}
placeholder="Leave empty for public access"
value={album.title || ''}
onChange={(e) => onUpdate({ title: e.target.value || undefined })}
placeholder={name}
/>
</div>
</div>
{/* Hero Image Selection */}
<div className="admin-field">
<label>Custom Hero Image</label>
<div className="album-hero-field">
{album.heroImage ? (
<div className="album-hero-preview">
<img src={`/api/admin/thumbnail/${album.heroImage}`} alt="Hero" />
<button
className="album-hero-remove"
onClick={() => onUpdate({ heroImage: undefined })}
title="Remove custom hero image"
>
<Icons.Close />
</button>
</div>
) : (
<span className="album-hero-empty">Using default album cover</span>
)}
<button
className="admin-btn admin-btn-sm"
onClick={() =>
setHeroPickerTarget({
albumId: album.id,
onSelect: (assetId) => {
onUpdate({ heroImage: assetId });
setHeroPickerTarget(null);
},
currentAssetIds: album.heroImage ? [album.heroImage] : [],
title: `Pick Hero Image for ${name}`,
})
}
>
<Icons.Image /> {album.heroImage ? 'Change Image' : 'Pick Hero Image'}
</button>
<div className="admin-field">
<label>Description</label>
<textarea
value={album.description || ''}
onChange={(e) => onUpdate({ description: e.target.value || undefined })}
placeholder="Optional description for visitors"
rows={4}
/>
</div>
</div>
{/* Technical UUID */}
<div className="drawer-uuid-section">
<label className="admin-field-label">IMMICH ALBUM UUID</label>
<div className="uuid-copy-box">
<code>{album.id}</code>
<button
className="uuid-copy-btn"
onClick={() => {
navigator.clipboard.writeText(album.id);
}}
title="Copy UUID"
>
<Icons.Copy />
</button>
<div className="admin-field">
<label>Password protection</label>
<div className="input-with-icon">
<input
type="password"
value={album.password || ''}
onChange={(e) => onUpdate({ password: e.target.value || undefined })}
placeholder="Leave empty for public access"
/>
</div>
</div>
{/* Hero Image Selection */}
<div className="admin-field">
<label>Custom Hero Image</label>
<div className="album-hero-field">
{album.heroImage ? (
<div className="album-hero-preview">
<img src={`/api/admin/thumbnail/${album.heroImage}`} alt="Hero" />
<button
className="album-hero-remove"
onClick={() => onUpdate({ heroImage: undefined })}
title="Remove custom hero image"
>
<Icons.Close />
</button>
</div>
) : (
<span className="album-hero-empty">Using default album cover</span>
)}
<button
className="admin-btn admin-btn-sm"
onClick={() =>
setHeroPickerTarget({
albumId: album.id,
onSelect: (assetId) => {
onUpdate({ heroImage: assetId });
setHeroPickerTarget(null);
},
currentAssetIds: album.heroImage ? [album.heroImage] : [],
title: `Pick Hero Image for ${name}`,
})
}
>
<Icons.Image /> {album.heroImage ? 'Change Image' : 'Pick Hero Image'}
</button>
</div>
</div>
</div>
</div>
<div className="album-drawer-footer">
<button
className="admin-btn admin-btn-primary"
onClick={() => setEditingAlbumAddress(null)}
>
Done
</button>
<button
className="admin-btn admin-btn-ghost admin-btn-danger"
onClick={onRemove}
style={{ maxWidth: '160px' }}
>
<Icons.Trash /> Remove Album
</button>
<button
className="admin-btn admin-btn-primary"
onClick={() => setEditingAlbumAddress(null)}
style={{ maxWidth: '140px' }}
>
Done
</button>
</div>
</div>
</div>