feat(web): add public portfolio rendering and media streaming

This commit is contained in:
2026-02-12 21:43:53 +01:00
parent 1fddb6d858
commit 958f3ad723
15 changed files with 888 additions and 9 deletions

View File

@@ -27,10 +27,13 @@ export {
deleteMediaAsset,
getMediaAssetById,
getMediaFoundationSummary,
getPublishedArtworkBySlug,
linkArtworkToGrouping,
listArtworks,
listMediaAssets,
listMediaFoundationGroups,
listPublishedArtworks,
listPublishedPortfolioGroups,
updateMediaAsset,
} from "./media-foundation"
export type { PublicNavigationItem } from "./pages-navigation"

View File

@@ -8,11 +8,11 @@ const { mockDb } = vi.hoisted(() => ({
artworkTag: { upsert: vi.fn() },
artworkRendition: { upsert: vi.fn() },
mediaAsset: { create: vi.fn(), findUnique: vi.fn(), update: vi.fn(), delete: vi.fn() },
artwork: { create: vi.fn() },
gallery: { create: vi.fn() },
album: { create: vi.fn() },
category: { create: vi.fn() },
tag: { create: vi.fn() },
artwork: { create: vi.fn(), findMany: vi.fn(), findFirst: vi.fn() },
gallery: { create: vi.fn(), findMany: vi.fn() },
album: { create: vi.fn(), findMany: vi.fn() },
category: { create: vi.fn(), findMany: vi.fn() },
tag: { create: vi.fn(), findMany: vi.fn() },
},
}))
@@ -26,7 +26,10 @@ import {
createMediaAsset,
deleteMediaAsset,
getMediaAssetById,
getPublishedArtworkBySlug,
linkArtworkToGrouping,
listPublishedArtworks,
listPublishedPortfolioGroups,
updateMediaAsset,
} from "./media-foundation"
@@ -42,6 +45,12 @@ describe("media foundation service", () => {
if ("findUnique" in value) {
value.findUnique.mockReset()
}
if ("findMany" in value) {
value.findMany.mockReset()
}
if ("findFirst" in value) {
value.findFirst.mockReset()
}
if ("update" in value) {
value.update.mockReset()
}
@@ -120,4 +129,58 @@ describe("media foundation service", () => {
expect(mockDb.mediaAsset.update).toHaveBeenCalledTimes(1)
expect(mockDb.mediaAsset.delete).toHaveBeenCalledTimes(1)
})
it("lists published artworks with group filters", async () => {
mockDb.artwork.findMany.mockResolvedValue([])
await listPublishedArtworks({
groupType: "gallery",
groupSlug: "showcase",
})
expect(mockDb.artwork.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
isPublished: true,
galleryLinks: {
some: {
gallery: {
slug: "showcase",
isVisible: true,
},
},
},
}),
}),
)
})
it("lists published portfolio groups", async () => {
mockDb.gallery.findMany.mockResolvedValue([])
mockDb.album.findMany.mockResolvedValue([])
mockDb.category.findMany.mockResolvedValue([])
mockDb.tag.findMany.mockResolvedValue([])
await listPublishedPortfolioGroups()
expect(mockDb.gallery.findMany).toHaveBeenCalledTimes(1)
expect(mockDb.album.findMany).toHaveBeenCalledTimes(1)
expect(mockDb.category.findMany).toHaveBeenCalledTimes(1)
expect(mockDb.tag.findMany).toHaveBeenCalledTimes(1)
})
it("loads a published artwork by slug", async () => {
mockDb.artwork.findFirst.mockResolvedValue(null)
await getPublishedArtworkBySlug("artwork-slug")
expect(mockDb.artwork.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: {
slug: "artwork-slug",
isPublished: true,
},
}),
)
})
})

View File

@@ -9,6 +9,14 @@ import {
import { db } from "./client"
type PublicArtworkGroupType = "gallery" | "album" | "category" | "tag"
type ListPublishedArtworksInput = {
groupType?: PublicArtworkGroupType
groupSlug?: string
limit?: number
}
export async function listMediaAssets(limit = 24) {
return db.mediaAsset.findMany({
orderBy: { updatedAt: "desc" },
@@ -280,3 +288,251 @@ export async function getMediaFoundationSummary() {
tags,
}
}
export async function listPublishedPortfolioGroups() {
const [galleries, albums, categories, tags] = await Promise.all([
db.gallery.findMany({
where: {
isVisible: true,
},
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
select: {
id: true,
name: true,
slug: true,
},
}),
db.album.findMany({
where: {
isVisible: true,
},
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
select: {
id: true,
name: true,
slug: true,
},
}),
db.category.findMany({
where: {
isVisible: true,
},
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
select: {
id: true,
name: true,
slug: true,
},
}),
db.tag.findMany({
orderBy: [{ name: "asc" }],
select: {
id: true,
name: true,
slug: true,
},
}),
])
return {
galleries,
albums,
categories,
tags,
}
}
export async function listPublishedArtworks(input: ListPublishedArtworksInput = {}) {
const take = input.limit ?? 36
const where: Record<string, unknown> = {
isPublished: true,
}
if (input.groupType && input.groupSlug) {
if (input.groupType === "gallery") {
where.galleryLinks = {
some: {
gallery: {
slug: input.groupSlug,
isVisible: true,
},
},
}
} else if (input.groupType === "album") {
where.albumLinks = {
some: {
album: {
slug: input.groupSlug,
isVisible: true,
},
},
}
} else if (input.groupType === "category") {
where.categoryLinks = {
some: {
category: {
slug: input.groupSlug,
isVisible: true,
},
},
}
} else if (input.groupType === "tag") {
where.tagLinks = {
some: {
tag: {
slug: input.groupSlug,
},
},
}
}
}
return db.artwork.findMany({
where,
orderBy: [{ updatedAt: "desc" }],
take,
include: {
renditions: {
where: {
mediaAsset: {
isPublished: true,
},
},
include: {
mediaAsset: {
select: {
id: true,
title: true,
altText: true,
mimeType: true,
width: true,
height: true,
},
},
},
},
galleryLinks: {
include: {
gallery: {
select: {
id: true,
name: true,
slug: true,
},
},
},
},
albumLinks: {
include: {
album: {
select: {
id: true,
name: true,
slug: true,
},
},
},
},
categoryLinks: {
include: {
category: {
select: {
id: true,
name: true,
slug: true,
},
},
},
},
tagLinks: {
include: {
tag: {
select: {
id: true,
name: true,
slug: true,
},
},
},
},
},
})
}
export async function getPublishedArtworkBySlug(slug: string) {
return db.artwork.findFirst({
where: {
slug,
isPublished: true,
},
include: {
renditions: {
where: {
mediaAsset: {
isPublished: true,
},
},
include: {
mediaAsset: {
select: {
id: true,
title: true,
altText: true,
mimeType: true,
width: true,
height: true,
source: true,
author: true,
copyright: true,
tags: true,
},
},
},
},
galleryLinks: {
include: {
gallery: {
select: {
id: true,
name: true,
slug: true,
},
},
},
},
albumLinks: {
include: {
album: {
select: {
id: true,
name: true,
slug: true,
},
},
},
},
categoryLinks: {
include: {
category: {
select: {
id: true,
name: true,
slug: true,
},
},
},
},
tagLinks: {
include: {
tag: {
select: {
id: true,
name: true,
slug: true,
},
},
},
},
},
})
}