This commit is contained in:
2026-08-22 02:59:16 +02:00
commit 6a5bb1d699
100 changed files with 17409 additions and 0 deletions
@@ -0,0 +1,126 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { DashboardLayout } from '../../components/dashboard/DashboardLayout';
import { GalleryCard } from '../../components/dashboard/GalleryCard';
import { StudioStat } from '../../components/dashboard/StudioStat';
import { useAuth } from '../../features/auth/useAuth';
import { deleteGallery, getGalleries } from '../../lib/api';
import { formatBytes } from '../../lib/format';
import type { GallerySummary } from '../../types/gallery';
export default function DashboardPage() {
const { user } = useAuth();
const [galleries, setGalleries] = useState<GallerySummary[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
getGalleries()
.then(setGalleries)
.catch((reason: unknown) =>
setError(reason instanceof Error ? reason.message : 'Could not load galleries.'),
)
.finally(() => setLoading(false));
}, []);
async function removeGallery(gallery: GallerySummary) {
if (!window.confirm(`Delete "${gallery.title}"? This cannot be undone.`)) return;
try {
await deleteGallery(gallery.id);
setGalleries((current) => current.filter((item) => item.id !== gallery.id));
} catch (reason) {
setError(reason instanceof Error ? reason.message : 'Could not delete gallery.');
}
}
const photos = galleries.reduce((total, gallery) => total + gallery.photoCount, 0);
const storage = galleries.reduce((total, gallery) => total + gallery.totalBytes, 0);
const published = galleries.filter((gallery) => gallery.status === 'published').length;
return (
<DashboardLayout>
<div className="studio-page">
<header className="studio-page__header">
<div>
<p className="studio-kicker">{user?.name || 'Studio'} / overview</p>
<h1 className="studio-display">
Make the handoff <em>matter.</em>
</h1>
<p className="studio-page__lede">
Everything your clients need, in one beautiful place.
</p>
</div>
<Link className="platform-button platform-button--accent" to="/dashboard/galleries/new">
<span>New gallery</span>
<span aria-hidden="true">+</span>
</Link>
</header>
<section className="studio-stats" aria-label="Studio overview">
<StudioStat
label="Galleries"
value={String(galleries.length).padStart(2, '0')}
note="All your work, in one place"
accent="coral"
/>
<StudioStat
label="Published"
value={String(published).padStart(2, '0')}
note="Currently out in the world"
accent="violet"
/>
<StudioStat
label="Photographs"
value={String(photos).padStart(2, '0')}
note="Ready to be remembered"
accent="gold"
/>
<StudioStat
label="Storage used"
value={formatBytes(storage)}
note="Across every gallery"
accent="ink"
/>
</section>
<section className="studio-section studio-section--recent">
<div className="studio-section__heading">
<div>
<p className="studio-kicker">Your latest work</p>
<h2 className="studio-heading">Recent galleries</h2>
</div>
<Link className="inline-link" to="/dashboard/galleries">
View all galleries <span aria-hidden="true">&#8599;</span>
</Link>
</div>
{error && <p className="studio-alert studio-alert--error">{error}</p>}
{loading ? (
<div className="studio-list-loading">
<span />
<span />
<span />
</div>
) : galleries.length === 0 ? (
<div className="studio-empty">
<span className="studio-empty__mark">+</span>
<div>
<h3>Your first gallery starts here.</h3>
<p>Give finished work a place that feels as considered as the work itself.</p>
</div>
<Link className="inline-link" to="/dashboard/galleries/new">
Create a gallery <span aria-hidden="true">&#8599;</span>
</Link>
</div>
) : (
<div className="gallery-grid gallery-grid--dashboard">
{galleries.slice(0, 3).map((gallery) => (
<GalleryCard key={gallery.id} gallery={gallery} onDelete={removeGallery} />
))}
</div>
)}
</section>
</div>
</DashboardLayout>
);
}