feat(media): scaffold mvp1 media and portfolio foundation
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { AdminSectionPlaceholder } from "@/components/admin-section-placeholder"
|
||||
import { getMediaFoundationSummary, listMediaAssets } from "@cms/db"
|
||||
|
||||
import { AdminShell } from "@/components/admin-shell"
|
||||
import { requirePermissionForRoute } from "@/lib/route-guards"
|
||||
|
||||
@@ -10,6 +11,7 @@ export default async function MediaManagementPage() {
|
||||
permission: "media:read",
|
||||
scope: "team",
|
||||
})
|
||||
const [summary, assets] = await Promise.all([getMediaFoundationSummary(), listMediaAssets(20)])
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
@@ -17,18 +19,70 @@ export default async function MediaManagementPage() {
|
||||
activePath="/media"
|
||||
badge="Admin App"
|
||||
title="Media"
|
||||
description="Prepare media library and enrichment workflows."
|
||||
description="Media foundation baseline for assets, artwork renditions, and grouping metadata."
|
||||
>
|
||||
<AdminSectionPlaceholder
|
||||
feature="Media Library"
|
||||
summary="This route is ready for media browsing, upload, and metadata refinement features."
|
||||
requiredPermission="media:read (team)"
|
||||
nextSteps={[
|
||||
"Add media upload and asset listing.",
|
||||
"Add enrichment fields (alt text, source, tags).",
|
||||
"Add artwork-specific refinement fields.",
|
||||
]}
|
||||
/>
|
||||
<section className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<article className="rounded-xl border border-neutral-200 p-4">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-neutral-500">Media Assets</p>
|
||||
<p className="mt-2 text-3xl font-semibold">{summary.mediaAssets}</p>
|
||||
</article>
|
||||
<article className="rounded-xl border border-neutral-200 p-4">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-neutral-500">Artworks</p>
|
||||
<p className="mt-2 text-3xl font-semibold">{summary.artworks}</p>
|
||||
</article>
|
||||
<article className="rounded-xl border border-neutral-200 p-4">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-neutral-500">Groups</p>
|
||||
<p className="mt-2 text-3xl font-semibold">
|
||||
{summary.galleries + summary.albums + summary.categories + summary.tags}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-neutral-500">
|
||||
{summary.galleries} galleries · {summary.albums} albums · {summary.categories}{" "}
|
||||
categories{" · "}
|
||||
{summary.tags} tags
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border border-neutral-200 p-6">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h2 className="text-xl font-medium">Recent Media Assets</h2>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-neutral-500">
|
||||
MVP1 Foundation
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<table className="min-w-full text-left text-sm">
|
||||
<thead className="text-xs uppercase tracking-wide text-neutral-500">
|
||||
<tr>
|
||||
<th className="py-2 pr-4">Title</th>
|
||||
<th className="py-2 pr-4">Type</th>
|
||||
<th className="py-2 pr-4">Published</th>
|
||||
<th className="py-2 pr-4">Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{assets.length === 0 ? (
|
||||
<tr>
|
||||
<td className="py-3 text-neutral-500" colSpan={4}>
|
||||
No media assets yet. Upload workflows land in `todo/mvp1-media-upload-pipeline`.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
assets.map((asset) => (
|
||||
<tr key={asset.id} className="border-t border-neutral-200">
|
||||
<td className="py-3 pr-4">{asset.title}</td>
|
||||
<td className="py-3 pr-4">{asset.type}</td>
|
||||
<td className="py-3 pr-4">{asset.isPublished ? "yes" : "no"}</td>
|
||||
<td className="py-3 pr-4 text-neutral-600">
|
||||
{asset.updatedAt.toLocaleDateString("en-US")}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
)
|
||||
}
|
||||
|
||||
69
apps/admin/src/app/portfolio/page.tsx
Normal file
69
apps/admin/src/app/portfolio/page.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { listArtworks } from "@cms/db"
|
||||
|
||||
import { AdminShell } from "@/components/admin-shell"
|
||||
import { requirePermissionForRoute } from "@/lib/route-guards"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function PortfolioPage() {
|
||||
const role = await requirePermissionForRoute({
|
||||
nextPath: "/portfolio",
|
||||
permission: "media:read",
|
||||
scope: "team",
|
||||
})
|
||||
const artworks = await listArtworks(30)
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
role={role}
|
||||
activePath="/portfolio"
|
||||
badge="Admin App"
|
||||
title="Portfolio"
|
||||
description="Artwork foundation with rendition slots and grouping relations."
|
||||
>
|
||||
<section className="rounded-xl border border-neutral-200 p-6">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h2 className="text-xl font-medium">Artworks</h2>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-neutral-500">
|
||||
MVP1 Foundation
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<table className="min-w-full text-left text-sm">
|
||||
<thead className="text-xs uppercase tracking-wide text-neutral-500">
|
||||
<tr>
|
||||
<th className="py-2 pr-4">Title</th>
|
||||
<th className="py-2 pr-4">Slug</th>
|
||||
<th className="py-2 pr-4">Published</th>
|
||||
<th className="py-2 pr-4">Renditions</th>
|
||||
<th className="py-2 pr-4">Groups</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{artworks.length === 0 ? (
|
||||
<tr>
|
||||
<td className="py-3 text-neutral-500" colSpan={5}>
|
||||
No artworks yet. Add creation flows after media upload pipeline lands.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
artworks.map((artwork) => (
|
||||
<tr key={artwork.id} className="border-t border-neutral-200">
|
||||
<td className="py-3 pr-4">{artwork.title}</td>
|
||||
<td className="py-3 pr-4 font-mono text-xs">{artwork.slug}</td>
|
||||
<td className="py-3 pr-4">{artwork.isPublished ? "yes" : "no"}</td>
|
||||
<td className="py-3 pr-4">{artwork.renditions.length}</td>
|
||||
<td className="py-3 pr-4 text-neutral-600">
|
||||
g:{artwork.galleryLinks.length} a:{artwork.albumLinks.length} c:
|
||||
{artwork.categoryLinks.length} t:{artwork.tagLinks.length}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
)
|
||||
}
|
||||
@@ -27,6 +27,7 @@ const navItems: NavItem[] = [
|
||||
{ href: "/", label: "Dashboard", permission: "dashboard:read", scope: "global" },
|
||||
{ href: "/pages", label: "Pages", permission: "pages:read", scope: "team" },
|
||||
{ href: "/media", label: "Media", permission: "media:read", scope: "team" },
|
||||
{ href: "/portfolio", label: "Portfolio", permission: "media:read", scope: "team" },
|
||||
{ href: "/users", label: "Users", permission: "users:read", scope: "own" },
|
||||
{ href: "/commissions", label: "Commissions", permission: "commissions:read", scope: "own" },
|
||||
{ href: "/settings", label: "Settings", permission: "users:manage_roles", scope: "global" },
|
||||
|
||||
@@ -31,6 +31,10 @@ describe("admin route access rules", () => {
|
||||
permission: "media:read",
|
||||
scope: "team",
|
||||
})
|
||||
expect(getRequiredPermission("/portfolio")).toEqual({
|
||||
permission: "media:read",
|
||||
scope: "team",
|
||||
})
|
||||
expect(getRequiredPermission("/users")).toEqual({
|
||||
permission: "users:read",
|
||||
scope: "own",
|
||||
|
||||
@@ -57,6 +57,13 @@ const guardRules: GuardRule[] = [
|
||||
scope: "team",
|
||||
},
|
||||
},
|
||||
{
|
||||
route: /^\/portfolio(?:\/|$)/,
|
||||
requirement: {
|
||||
permission: "media:read",
|
||||
scope: "team",
|
||||
},
|
||||
},
|
||||
{
|
||||
route: /^\/users(?:\/|$)/,
|
||||
requirement: {
|
||||
|
||||
Reference in New Issue
Block a user