Compare commits
1 Commits
todo/mvp1-
...
todo/mvp1-
| Author | SHA1 | Date | |
|---|---|---|---|
|
81983cfe40
|
3
TODO.md
3
TODO.md
@@ -143,7 +143,7 @@ This file is the single source of truth for roadmap and delivery progress.
|
|||||||
- [x] [P1] Media enrichment metadata (alt text, copyright, author, source, tags, licensing, usage context)
|
- [x] [P1] Media enrichment metadata (alt text, copyright, author, source, tags, licensing, usage context)
|
||||||
- [x] [P1] Portfolio grouping primitives (galleries, albums, categories, tags) with ordering/visibility controls
|
- [x] [P1] Portfolio grouping primitives (galleries, albums, categories, tags) with ordering/visibility controls
|
||||||
- [x] [P1] Artwork refinement fields (medium, dimensions, year, framing, availability, price visibility)
|
- [x] [P1] Artwork refinement fields (medium, dimensions, year, framing, availability, price visibility)
|
||||||
- [ ] [P1] Artwork rendition management (thumbnail, card, full, retina/custom sizes)
|
- [x] [P1] Artwork rendition management (thumbnail, card, full, retina/custom sizes)
|
||||||
- [ ] [P1] Type-specific processing presets (artwork/banner/promo/video/gif) with validation rules
|
- [ ] [P1] Type-specific processing presets (artwork/banner/promo/video/gif) with validation rules
|
||||||
- [ ] [P1] Users management (invite, roles, status)
|
- [ ] [P1] Users management (invite, roles, status)
|
||||||
- [ ] [P1] Disable/ban user function and enforcement in auth/session checks
|
- [ ] [P1] Disable/ban user function and enforcement in auth/session checks
|
||||||
@@ -363,6 +363,7 @@ This file is the single source of truth for roadmap and delivery progress.
|
|||||||
- [2026-02-12] Public portfolio baseline added with `/{locale}/portfolio` and `/{locale}/portfolio/{slug}`, including published-artwork filters (gallery/album/category/tag), rendition image streaming via web `/api/media/file/:id`, and media-aware artwork detail rendering.
|
- [2026-02-12] Public portfolio baseline added with `/{locale}/portfolio` and `/{locale}/portfolio/{slug}`, including published-artwork filters (gallery/album/category/tag), rendition image streaming via web `/api/media/file/:id`, and media-aware artwork detail rendering.
|
||||||
- [2026-02-12] Portfolio grouping controls completed in admin `/portfolio`: galleries/albums/categories/tags now support visibility and sort-order management (create/update/delete), and public tag filters now respect visibility.
|
- [2026-02-12] Portfolio grouping controls completed in admin `/portfolio`: galleries/albums/categories/tags now support visibility and sort-order management (create/update/delete), and public tag filters now respect visibility.
|
||||||
- [2026-02-12] Artwork refinement baseline completed: admin `/portfolio` now captures/edits medium, dimensions, year, framing, availability, publish state, and optional price visibility (`priceAmountCents` + `priceCurrency`), with public artwork detail rendering visible prices only.
|
- [2026-02-12] Artwork refinement baseline completed: admin `/portfolio` now captures/edits medium, dimensions, year, framing, availability, publish state, and optional price visibility (`priceAmountCents` + `priceCurrency`), with public artwork detail rendering visible prices only.
|
||||||
|
- [2026-02-12] Artwork rendition management completed: admin `/portfolio` supports `thumbnail/card/full/retina/custom` slot assignment with dimensions and primary flag, plus per-artwork rendition listing and delete controls.
|
||||||
- [2026-02-12] Public UX pass: commission request flow now reports explicit invalid budget range errors, and header navigation now falls back to localized defaults (`home`, `portfolio`, `news`, `commissions`) when no CMS menu exists; seed data now creates those default menu entries.
|
- [2026-02-12] Public UX pass: commission request flow now reports explicit invalid budget range errors, and header navigation now falls back to localized defaults (`home`, `portfolio`, `news`, `commissions`) when no CMS menu exists; seed data now creates those default menu entries.
|
||||||
- [2026-02-12] Added `e2e/public-rendering.pw.ts` web coverage for fallback navigation visibility, portfolio routes, and commission submission validation (invalid budget range + successful submission path).
|
- [2026-02-12] Added `e2e/public-rendering.pw.ts` web coverage for fallback navigation visibility, portfolio routes, and commission submission validation (invalid budget range + successful submission path).
|
||||||
- [2026-02-12] Testing execution is temporarily paused for delivery velocity: root test scripts are stubbed and CI test steps are disabled; all testing backlog is consolidated under `MVP 3: Testing and Quality`.
|
- [2026-02-12] Testing execution is temporarily paused for delivery velocity: root test scripts are stubbed and CI test steps are disabled; all testing backlog is consolidated under `MVP 3: Testing and Quality`.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
createCategory,
|
createCategory,
|
||||||
createGallery,
|
createGallery,
|
||||||
createTag,
|
createTag,
|
||||||
|
deleteArtworkRendition,
|
||||||
deleteGrouping,
|
deleteGrouping,
|
||||||
linkArtworkToGrouping,
|
linkArtworkToGrouping,
|
||||||
listArtworks,
|
listArtworks,
|
||||||
@@ -316,6 +317,21 @@ async function attachRenditionAction(formData: FormData) {
|
|||||||
redirectWithState({ notice: "Rendition attached." })
|
redirectWithState({ notice: "Rendition attached." })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteRenditionAction(formData: FormData) {
|
||||||
|
"use server"
|
||||||
|
|
||||||
|
await requireWritePermission()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteArtworkRendition(readField(formData, "renditionId"))
|
||||||
|
} catch {
|
||||||
|
redirectWithState({ error: "Failed to delete rendition." })
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath("/portfolio")
|
||||||
|
redirectWithState({ notice: "Rendition deleted." })
|
||||||
|
}
|
||||||
|
|
||||||
export default async function PortfolioPage({
|
export default async function PortfolioPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
@@ -641,6 +657,7 @@ export default async function PortfolioPage({
|
|||||||
<option value="thumbnail">thumbnail</option>
|
<option value="thumbnail">thumbnail</option>
|
||||||
<option value="card">card</option>
|
<option value="card">card</option>
|
||||||
<option value="full">full</option>
|
<option value="full">full</option>
|
||||||
|
<option value="retina">retina</option>
|
||||||
<option value="custom">custom</option>
|
<option value="custom">custom</option>
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
@@ -719,7 +736,40 @@ export default async function PortfolioPage({
|
|||||||
? `price: ${(artwork.priceAmountCents / 100).toFixed(2)} ${artwork.priceCurrency} (${artwork.isPriceVisible ? "visible" : "hidden"})`
|
? `price: ${(artwork.priceAmountCents / 100).toFixed(2)} ${artwork.priceCurrency} (${artwork.isPriceVisible ? "visible" : "hidden"})`
|
||||||
: "price: -"}
|
: "price: -"}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4">{artwork.renditions.length}</td>
|
<td className="py-3 pr-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{artwork.renditions.length === 0 ? (
|
||||||
|
<span className="text-xs text-neutral-500">0</span>
|
||||||
|
) : (
|
||||||
|
artwork.renditions.map((rendition) => (
|
||||||
|
<form
|
||||||
|
key={rendition.id}
|
||||||
|
action={deleteRenditionAction}
|
||||||
|
className="flex items-center gap-2 text-xs"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="renditionId" value={rendition.id} />
|
||||||
|
<span className="rounded bg-neutral-100 px-2 py-1 font-mono">
|
||||||
|
{rendition.slot}
|
||||||
|
</span>
|
||||||
|
<span className="text-neutral-500">
|
||||||
|
{rendition.width ?? "-"}x{rendition.height ?? "-"}
|
||||||
|
</span>
|
||||||
|
{rendition.isPrimary ? (
|
||||||
|
<span className="rounded bg-emerald-100 px-2 py-1 text-emerald-700">
|
||||||
|
primary
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="rounded border border-red-300 px-2 py-1 text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="py-3 pr-4 text-neutral-600">
|
<td className="py-3 pr-4 text-neutral-600">
|
||||||
g:{artwork.galleryLinks.length} a:{artwork.albumLinks.length} c:
|
g:{artwork.galleryLinks.length} a:{artwork.albumLinks.length} c:
|
||||||
{artwork.categoryLinks.length} t:{artwork.tagLinks.length}
|
{artwork.categoryLinks.length} t:{artwork.tagLinks.length}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const mediaAssetTypeSchema = z.enum([
|
|||||||
"generic",
|
"generic",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const artworkRenditionSlotSchema = z.enum(["thumbnail", "card", "full", "custom"])
|
export const artworkRenditionSlotSchema = z.enum(["thumbnail", "card", "full", "retina", "custom"])
|
||||||
|
|
||||||
export const createMediaAssetInputSchema = z.object({
|
export const createMediaAssetInputSchema = z.object({
|
||||||
id: z.string().uuid().optional(),
|
id: z.string().uuid().optional(),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export {
|
|||||||
createGallery,
|
createGallery,
|
||||||
createMediaAsset,
|
createMediaAsset,
|
||||||
createTag,
|
createTag,
|
||||||
|
deleteArtworkRendition,
|
||||||
deleteGrouping,
|
deleteGrouping,
|
||||||
deleteMediaAsset,
|
deleteMediaAsset,
|
||||||
getMediaAssetById,
|
getMediaAssetById,
|
||||||
|
|||||||
@@ -33,10 +33,14 @@ export async function listArtworks(limit = 24) {
|
|||||||
take: limit,
|
take: limit,
|
||||||
include: {
|
include: {
|
||||||
renditions: {
|
renditions: {
|
||||||
|
orderBy: [{ isPrimary: "desc" }, { updatedAt: "desc" }],
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
slot: true,
|
slot: true,
|
||||||
mediaAssetId: true,
|
mediaAssetId: true,
|
||||||
|
width: true,
|
||||||
|
height: true,
|
||||||
|
isPrimary: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
galleryLinks: {
|
galleryLinks: {
|
||||||
@@ -340,6 +344,12 @@ export async function attachArtworkRendition(input: unknown) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteArtworkRendition(id: string) {
|
||||||
|
return db.artworkRendition.delete({
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function getMediaFoundationSummary() {
|
export async function getMediaFoundationSummary() {
|
||||||
const [mediaAssets, artworks, galleries, albums, categories, tags] = await Promise.all([
|
const [mediaAssets, artworks, galleries, albums, categories, tags] = await Promise.all([
|
||||||
db.mediaAsset.count(),
|
db.mediaAsset.count(),
|
||||||
@@ -473,6 +483,7 @@ export async function listPublishedArtworks(input: ListPublishedArtworksInput =
|
|||||||
isPublished: true,
|
isPublished: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
orderBy: [{ isPrimary: "desc" }, { updatedAt: "desc" }],
|
||||||
include: {
|
include: {
|
||||||
mediaAsset: {
|
mediaAsset: {
|
||||||
select: {
|
select: {
|
||||||
@@ -547,6 +558,7 @@ export async function getPublishedArtworkBySlug(slug: string) {
|
|||||||
isPublished: true,
|
isPublished: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
orderBy: [{ isPrimary: "desc" }, { updatedAt: "desc" }],
|
||||||
include: {
|
include: {
|
||||||
mediaAsset: {
|
mediaAsset: {
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
Reference in New Issue
Block a user