From bf28e37ae5b1a3e423fb5babffdac41b4bc46d84 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 30 Apr 2026 11:31:40 +0800 Subject: [PATCH] feat: add media assets library --- .../[category]/[subcategory]/page.tsx | 121 ++++++++ .../[locale]/media-assets/[category]/page.tsx | 82 ++++++ .../media-assets/media-asset-gallery.tsx | 259 +++++++++++++++++ .../media-assets/media-assets-shared.tsx | 116 ++++++++ .../(frontend)/[locale]/media-assets/page.tsx | 66 +++++ .../admin/_components/admin-cache-actions.ts | 6 + .../admin/_components/admin-home.tsx | 8 + src/app/sitemap.ts | 15 +- src/i18n/config.ts | 3 + src/lib/global-settings.ts | 36 ++- src/lib/media-assets.ts | 270 ++++++++++++++++++ ...60430_090000_add_media_asset_categories.ts | 127 ++++++++ src/migrations/index.ts | 6 + src/payload-types.ts | 81 ++++++ src/payload.config.ts | 2 + .../collections/MediaAssetCategories.ts | 202 +++++++++++++ 16 files changed, 1398 insertions(+), 2 deletions(-) create mode 100644 src/app/(frontend)/[locale]/media-assets/[category]/[subcategory]/page.tsx create mode 100644 src/app/(frontend)/[locale]/media-assets/[category]/page.tsx create mode 100644 src/app/(frontend)/[locale]/media-assets/media-asset-gallery.tsx create mode 100644 src/app/(frontend)/[locale]/media-assets/media-assets-shared.tsx create mode 100644 src/app/(frontend)/[locale]/media-assets/page.tsx create mode 100644 src/lib/media-assets.ts create mode 100644 src/migrations/20260430_090000_add_media_asset_categories.ts create mode 100644 src/payload/collections/MediaAssetCategories.ts diff --git a/src/app/(frontend)/[locale]/media-assets/[category]/[subcategory]/page.tsx b/src/app/(frontend)/[locale]/media-assets/[category]/[subcategory]/page.tsx new file mode 100644 index 0000000..2667907 --- /dev/null +++ b/src/app/(frontend)/[locale]/media-assets/[category]/[subcategory]/page.tsx @@ -0,0 +1,121 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { setRequestLocale } from 'next-intl/server' +import { notFound } from 'next/navigation' +import type { Locale } from '@/i18n/config' +import { getMediaAssetDetailPage } from '@/lib/media-assets' +import { buildLocaleAlternates, trimDescription } from '@/lib/seo' +import { MediaAssetGallery } from '../../media-asset-gallery' +import { MediaAssetBreadcrumbs, MediaAssetEmptyState } from '../../media-assets-shared' + +export const revalidate = 300 + +function DownloadIcon({ className = '' }: { className?: string }) { + return ( + + ) +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ category: string; locale: Locale; subcategory: string }> +}): Promise { + const { category, locale, subcategory } = await params + const pageData = await getMediaAssetDetailPage(locale, category, subcategory) + + if (!pageData) { + return { + title: locale === 'zh' ? '媒体资料' : 'Media Assets', + } + } + + return { + alternates: buildLocaleAlternates(`/media-assets/${category}/${subcategory}`, locale), + description: trimDescription(pageData.subcategory.description) || + (locale === 'zh' ? '下载 Eversolo 媒体资料。' : 'Download Eversolo media assets.'), + title: pageData.subcategory.name, + } +} + +export default async function MediaAssetDetailPage({ + params, +}: { + params: Promise<{ category: string; locale: Locale; subcategory: string }> +}) { + const { category, locale, subcategory } = await params + setRequestLocale(locale) + + const pageData = await getMediaAssetDetailPage(locale, category, subcategory) + if (!pageData) notFound() + + const archiveMeta = [pageData.archiveFormat, pageData.archiveFileSize].filter(Boolean).join(' / ') + + return ( +
+ + +
+
+
+ {locale === 'zh' ? '媒体资料包' : 'Media Asset Kit'} +
+

+ {pageData.subcategory.name} +

+

+ {pageData.subcategory.description || + (locale === 'zh' + ? '点击缩略图预览原始资源,或通过标题悬停菜单查看格式、大小并下载。' + : 'Open thumbnails to preview the original file, or hover titles to inspect format, size, and download quickly.')} +

+
+ + {pageData.archiveDownloadUrl ? ( +
+ {archiveMeta ? ( + + {archiveMeta} + + ) : null} + + + {locale === 'zh' ? '下载压缩包' : 'Download All'} + +
+ ) : null} +
+ +
+ {pageData.assets.length > 0 ? ( + + ) : ( + + {locale === 'zh' + ? '这个资料包还没有添加三级资源。请在后台为该二级分类添加图片、PDF 或视频条目。' + : 'This kit does not have downloadable assets yet. Add image, PDF, or video entries to this secondary category in the admin.'} +
+ + {locale === 'zh' ? '返回上级分类' : 'Back to category'} + +
+
+ )} +
+
+ ) +} diff --git a/src/app/(frontend)/[locale]/media-assets/[category]/page.tsx b/src/app/(frontend)/[locale]/media-assets/[category]/page.tsx new file mode 100644 index 0000000..b8b879e --- /dev/null +++ b/src/app/(frontend)/[locale]/media-assets/[category]/page.tsx @@ -0,0 +1,82 @@ +import type { Metadata } from 'next' +import { setRequestLocale } from 'next-intl/server' +import { notFound } from 'next/navigation' +import type { Locale } from '@/i18n/config' +import { getMediaAssetCategoryPage } from '@/lib/media-assets' +import { buildLocaleAlternates, trimDescription } from '@/lib/seo' +import { + MediaAssetBreadcrumbs, + MediaAssetCategoryGrid, + MediaAssetEmptyState, + MediaAssetPageHeader, +} from '../media-assets-shared' + +export const revalidate = 300 + +export async function generateMetadata({ + params, +}: { + params: Promise<{ category: string; locale: Locale }> +}): Promise { + const { category, locale } = await params + const pageData = await getMediaAssetCategoryPage(locale, category) + + if (!pageData) { + return { + title: locale === 'zh' ? '媒体资料' : 'Media Assets', + } + } + + return { + alternates: buildLocaleAlternates(`/media-assets/${category}`, locale), + description: trimDescription(pageData.category.description) || + (locale === 'zh' ? '浏览 Eversolo 媒体资料分类。' : 'Browse Eversolo media asset categories.'), + title: pageData.category.name, + } +} + +export default async function MediaAssetCategoryPage({ + params, +}: { + params: Promise<{ category: string; locale: Locale }> +}) { + const { category, locale } = await params + setRequestLocale(locale) + + const pageData = await getMediaAssetCategoryPage(locale, category) + if (!pageData) notFound() + + return ( +
+ + + + +
+ {pageData.children.length > 0 ? ( + + ) : ( + + {locale === 'zh' + ? '这个一级分类下还没有二级资料包。请在后台创建一个带上级分类的媒体资料包。' + : 'This category does not have secondary kits yet. Add a media asset kit with this parent category in the admin.'} + + )} +
+
+ ) +} diff --git a/src/app/(frontend)/[locale]/media-assets/media-asset-gallery.tsx b/src/app/(frontend)/[locale]/media-assets/media-asset-gallery.tsx new file mode 100644 index 0000000..2a2c962 --- /dev/null +++ b/src/app/(frontend)/[locale]/media-assets/media-asset-gallery.tsx @@ -0,0 +1,259 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import type { Locale } from '@/i18n/config' +import type { MediaAssetFileItem } from '@/lib/media-assets' + +function DownloadGlyph({ className = '' }: { className?: string }) { + return ( + + ) +} + +function CloseGlyph({ className = '' }: { className?: string }) { + return ( + + ) +} + +function getVideoEmbedUrl(url: string) { + try { + const parsed = new URL(url) + const hostname = parsed.hostname.replace(/^www\./, '') + + if (hostname === 'youtube.com' || hostname === 'm.youtube.com') { + const id = parsed.searchParams.get('v') + return id ? `https://www.youtube.com/embed/${id}?controls=1&rel=0` : null + } + + if (hostname === 'youtu.be') { + const id = parsed.pathname.split('/').filter(Boolean)[0] + return id ? `https://www.youtube.com/embed/${id}?controls=1&rel=0` : null + } + + if (hostname === 'vimeo.com') { + const id = parsed.pathname.split('/').filter(Boolean)[0] + return id ? `https://player.vimeo.com/video/${id}` : null + } + + if (hostname === 'player.vimeo.com' || parsed.pathname.includes('/embed/')) return url + } catch { + return null + } + + return null +} + +function PreviewFrame({ item }: { item: MediaAssetFileItem }) { + if (item.type === 'image') { + return ( + {item.title} + ) + } + + if (item.type === 'pdf') { + return ( +