feat: member portal Sprint 11 — Memories / Gallery
- 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>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user