3e86e0ffc0
- GalleryAlbum + GalleryItem models + migration (publish gate, cover image, ordered items) - GalleryModule: admin CRUD for albums + items - Member endpoints: GET /my/memories (published albums w/ cover + count), GET /my/memories/:id (album + photos) - /my/memories page: album grid with cover thumbnails - /my/memories/[id] page: photo grid with captions - Memories nav item; register GalleryModule Completes Phase 2 (Intelligence) of the member portal — Sprints 9-11. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
import { getMemberToken, getApiBaseUrl } from '../../_lib/api';
|
||
import Link from 'next/link';
|
||
|
||
interface Album {
|
||
id: string;
|
||
title: string;
|
||
description: string | null;
|
||
coverUrl: string | null;
|
||
itemCount: number;
|
||
createdAt: string;
|
||
}
|
||
|
||
async function fetchAlbums(token: string): Promise<Album[]> {
|
||
const res = await fetch(`${getApiBaseUrl()}/my/memories`, {
|
||
headers: { Authorization: `Bearer ${token}` },
|
||
cache: 'no-store',
|
||
}).catch(() => null);
|
||
if (!res || !res.ok) return [];
|
||
return (await res.json()) as Album[];
|
||
}
|
||
|
||
export default async function MemoriesPage() {
|
||
const token = await getMemberToken();
|
||
if (!token) return null;
|
||
|
||
const albums = await fetchAlbums(token);
|
||
|
||
return (
|
||
<div className="max-w-2xl mx-auto space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<h1 className="text-xl font-semibold text-gray-900">Memories</h1>
|
||
<Link href="/my" className="text-sm text-indigo-600 hover:underline">← Dashboard</Link>
|
||
</div>
|
||
|
||
{albums.length === 0 ? (
|
||
<div className="text-center py-16 text-gray-400">
|
||
<p className="text-sm">No photo albums yet.</p>
|
||
<p className="text-xs mt-1">Your chapter admin will share event photos and memories here.</p>
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{albums.map((a) => (
|
||
<Link
|
||
key={a.id}
|
||
href={`/my/memories/${a.id}`}
|
||
className="group bg-white rounded-xl border border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
|
||
>
|
||
<div className="aspect-[4/3] bg-gray-100 overflow-hidden">
|
||
{a.coverUrl ? (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img
|
||
src={a.coverUrl}
|
||
alt={a.title}
|
||
className="w-full h-full object-cover group-hover:scale-105 transition-transform"
|
||
/>
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center text-3xl text-gray-300">🖼️</div>
|
||
)}
|
||
</div>
|
||
<div className="p-3">
|
||
<p className="text-sm font-semibold text-gray-900 truncate">{a.title}</p>
|
||
<p className="text-xs text-gray-400 mt-0.5">{a.itemCount} photo{a.itemCount !== 1 ? 's' : ''}</p>
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|