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>
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
import { getMemberToken, getApiBaseUrl } from '../../../_lib/api';
|
|
import Link from 'next/link';
|
|
import { notFound } from 'next/navigation';
|
|
|
|
interface AlbumDetail {
|
|
id: string;
|
|
title: string;
|
|
description: string | null;
|
|
items: Array<{ id: string; mediaUrl: string; caption: string | null }>;
|
|
}
|
|
|
|
async function fetchAlbum(token: string, id: string): Promise<AlbumDetail | null> {
|
|
const res = await fetch(`${getApiBaseUrl()}/my/memories/${id}`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
cache: 'no-store',
|
|
}).catch(() => null);
|
|
if (!res || !res.ok) return null;
|
|
return (await res.json()) as AlbumDetail;
|
|
}
|
|
|
|
export default async function AlbumPage({ params }: { params: Promise<{ id: string }> }) {
|
|
const token = await getMemberToken();
|
|
if (!token) return null;
|
|
|
|
const { id } = await params;
|
|
const album = await fetchAlbum(token, id);
|
|
if (!album) notFound();
|
|
|
|
return (
|
|
<div className="max-w-3xl mx-auto space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-xl font-semibold text-gray-900">{album.title}</h1>
|
|
{album.description && <p className="text-sm text-gray-500 mt-0.5">{album.description}</p>}
|
|
</div>
|
|
<Link href="/my/memories" className="text-sm text-indigo-600 hover:underline shrink-0">← Albums</Link>
|
|
</div>
|
|
|
|
{album.items.length === 0 ? (
|
|
<p className="text-center py-16 text-gray-400 text-sm">This album has no photos yet.</p>
|
|
) : (
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
{album.items.map((item) => (
|
|
<figure key={item.id} className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
|
<div className="aspect-square bg-gray-100 overflow-hidden">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img src={item.mediaUrl} alt={item.caption ?? ''} className="w-full h-full object-cover" />
|
|
</div>
|
|
{item.caption && (
|
|
<figcaption className="text-xs text-gray-500 px-2 py-1.5 truncate">{item.caption}</figcaption>
|
|
)}
|
|
</figure>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|