feat: add media assets library
This commit is contained in:
@@ -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 (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||
<path d="M10 3v9m0 0 4-4m-4 4L6 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M4 15.5h12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ category: string; locale: Locale; subcategory: string }>
|
||||
}): Promise<Metadata> {
|
||||
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 (
|
||||
<div className="mx-auto max-w-page px-6 pb-12 pt-4 md:px-12 md:pb-16 md:pt-5">
|
||||
<MediaAssetBreadcrumbs
|
||||
items={[
|
||||
{ href: `/${locale}/media-assets`, label: locale === 'zh' ? '媒体资料' : 'Media Assets' },
|
||||
{ href: pageData.category.href, label: pageData.category.name },
|
||||
{ label: pageData.subcategory.name },
|
||||
]}
|
||||
/>
|
||||
|
||||
<header className="grid gap-8 bg-[linear-gradient(135deg,#f7f7f3_0%,#ebefee_100%)] px-6 py-8 shadow-[0_18px_60px_rgba(18,24,31,0.045)] ring-1 ring-black/[0.045] md:px-8 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end">
|
||||
<div className="max-w-reading">
|
||||
<div className="text-sm uppercase tracking-[0.18em] text-text-muted">
|
||||
{locale === 'zh' ? '媒体资料包' : 'Media Asset Kit'}
|
||||
</div>
|
||||
<h1 className="mt-2 text-4xl font-medium tracking-tight md:text-6xl">
|
||||
{pageData.subcategory.name}
|
||||
</h1>
|
||||
<p className="mt-4 text-lg leading-8 text-text-secondary">
|
||||
{pageData.subcategory.description ||
|
||||
(locale === 'zh'
|
||||
? '点击缩略图预览原始资源,或通过标题悬停菜单查看格式、大小并下载。'
|
||||
: 'Open thumbnails to preview the original file, or hover titles to inspect format, size, and download quickly.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{pageData.archiveDownloadUrl ? (
|
||||
<div className="flex flex-col items-start gap-3 lg:items-end">
|
||||
{archiveMeta ? (
|
||||
<span className="text-xs font-medium uppercase tracking-[0.16em] text-text-muted">
|
||||
{archiveMeta}
|
||||
</span>
|
||||
) : null}
|
||||
<a
|
||||
href={pageData.archiveDownloadUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full bg-black px-5 py-3 text-sm font-medium text-white transition hover:bg-[#2c2925]"
|
||||
>
|
||||
<DownloadIcon className="h-4 w-4" />
|
||||
{locale === 'zh' ? '下载压缩包' : 'Download All'}
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<section className="mt-8">
|
||||
{pageData.assets.length > 0 ? (
|
||||
<MediaAssetGallery items={pageData.assets} locale={locale} />
|
||||
) : (
|
||||
<MediaAssetEmptyState>
|
||||
{locale === 'zh'
|
||||
? '这个资料包还没有添加三级资源。请在后台为该二级分类添加图片、PDF 或视频条目。'
|
||||
: 'This kit does not have downloadable assets yet. Add image, PDF, or video entries to this secondary category in the admin.'}
|
||||
<div className="mt-4">
|
||||
<Link href={pageData.category.href} className="font-medium text-text-primary underline underline-offset-4">
|
||||
{locale === 'zh' ? '返回上级分类' : 'Back to category'}
|
||||
</Link>
|
||||
</div>
|
||||
</MediaAssetEmptyState>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<Metadata> {
|
||||
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 (
|
||||
<div className="mx-auto max-w-page px-6 pb-12 pt-4 md:px-12 md:pb-16 md:pt-5">
|
||||
<MediaAssetBreadcrumbs
|
||||
items={[
|
||||
{ href: `/${locale}/media-assets`, label: locale === 'zh' ? '媒体资料' : 'Media Assets' },
|
||||
{ label: pageData.category.name },
|
||||
]}
|
||||
/>
|
||||
|
||||
<MediaAssetPageHeader
|
||||
eyebrow={locale === 'zh' ? '资料分类' : 'Asset Category'}
|
||||
title={pageData.category.name}
|
||||
description={
|
||||
pageData.category.description ||
|
||||
(locale === 'zh'
|
||||
? '选择下方资料包继续查看可下载的图片、PDF 与视频素材。'
|
||||
: 'Choose a kit below to view downloadable images, PDFs, and video assets.')
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="mt-8">
|
||||
{pageData.children.length > 0 ? (
|
||||
<MediaAssetCategoryGrid categories={pageData.children} locale={locale} />
|
||||
) : (
|
||||
<MediaAssetEmptyState>
|
||||
{locale === 'zh'
|
||||
? '这个一级分类下还没有二级资料包。请在后台创建一个带上级分类的媒体资料包。'
|
||||
: 'This category does not have secondary kits yet. Add a media asset kit with this parent category in the admin.'}
|
||||
</MediaAssetEmptyState>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||
<path d="M10 3v9m0 0 4-4m-4 4L6 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M4 15.5h12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function CloseGlyph({ className = '' }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||
<path d="m5 5 10 10M15 5 5 15" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<img
|
||||
src={item.externalUrl}
|
||||
alt={item.title}
|
||||
className="max-h-[76vh] w-full object-contain"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'pdf') {
|
||||
return (
|
||||
<iframe
|
||||
src={item.externalUrl}
|
||||
title={item.title}
|
||||
className="h-[76vh] w-full bg-white"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const embedUrl = getVideoEmbedUrl(item.externalUrl)
|
||||
|
||||
if (embedUrl) {
|
||||
return (
|
||||
<iframe
|
||||
src={embedUrl}
|
||||
title={item.title}
|
||||
className="aspect-video w-full bg-black"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<video
|
||||
src={item.externalUrl}
|
||||
className="max-h-[76vh] w-full bg-black"
|
||||
controls
|
||||
playsInline
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function typeLabel(type: MediaAssetFileItem['type'], locale: Locale) {
|
||||
if (type === 'pdf') return 'PDF'
|
||||
if (type === 'video') return locale === 'zh' ? '视频' : 'Video'
|
||||
return locale === 'zh' ? '图片' : 'Image'
|
||||
}
|
||||
|
||||
export function MediaAssetGallery({
|
||||
items,
|
||||
locale,
|
||||
}: {
|
||||
items: MediaAssetFileItem[]
|
||||
locale: Locale
|
||||
}) {
|
||||
const [activeId, setActiveId] = useState<string | null>(null)
|
||||
const activeItem = useMemo(
|
||||
() => items.find((item) => item.id === activeId) ?? null,
|
||||
[activeId, items],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeItem) return
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setActiveId(null)
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
document.body.style.overflow = 'hidden'
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
}, [activeItem])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{items.map((item) => (
|
||||
<article
|
||||
key={item.id}
|
||||
className="group overflow-visible bg-white shadow-[0_18px_60px_rgba(18,24,31,0.06)] ring-1 ring-black/[0.05]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveId(item.id)}
|
||||
className="relative block aspect-[4/3] w-full overflow-hidden bg-[#edf0ee] text-left"
|
||||
>
|
||||
{item.thumbnailUrl ? (
|
||||
<img
|
||||
src={item.thumbnailUrl}
|
||||
alt={item.title}
|
||||
className="h-full w-full object-cover transition duration-500 group-hover:scale-[1.035]"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center bg-[linear-gradient(135deg,#e4e8e7_0%,#f8f8f5_100%)] text-sm uppercase tracking-[0.16em] text-text-muted">
|
||||
{item.fileFormat}
|
||||
</span>
|
||||
)}
|
||||
<span className="absolute right-4 top-4 bg-black/80 px-3 py-1 text-[11px] font-medium uppercase tracking-[0.16em] text-white">
|
||||
{typeLabel(item.type, locale)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className="p-5 md:p-6">
|
||||
<div className="group/title relative inline-flex max-w-full flex-col">
|
||||
<div className="pointer-events-none absolute bottom-full left-0 z-10 mb-3 flex translate-y-1 items-center gap-2 bg-black px-3 py-2 text-[11px] font-medium uppercase tracking-[0.14em] text-white opacity-0 shadow-[0_16px_36px_rgba(0,0,0,0.22)] transition duration-200 group-hover/title:pointer-events-auto group-hover/title:translate-y-0 group-hover/title:opacity-100">
|
||||
<span>{item.fileFormat}</span>
|
||||
{item.fileSize ? <span className="text-white/65">{item.fileSize}</span> : null}
|
||||
<a
|
||||
href={item.externalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-1 inline-flex h-7 w-7 items-center justify-center rounded-full bg-white text-black transition hover:bg-[#d8cbb6]"
|
||||
aria-label={locale === 'zh' ? `下载 ${item.title}` : `Download ${item.title}`}
|
||||
>
|
||||
<DownloadGlyph className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
<h3 className="max-w-full text-xl font-medium leading-tight tracking-tight text-text-primary">
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{item.description ? (
|
||||
<p className="mt-3 line-clamp-2 text-sm leading-6 text-text-secondary">
|
||||
{item.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeItem ? (
|
||||
<div
|
||||
className="fixed inset-0 z-[120] bg-black/72 px-4 py-4 backdrop-blur-sm md:px-8 md:py-8"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={activeItem.title}
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) setActiveId(null)
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto grid h-full max-w-[1440px] overflow-hidden bg-[#f5f5f1] shadow-[0_30px_120px_rgba(0,0,0,0.35)] lg:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div className="flex min-h-0 items-center justify-center overflow-auto bg-[#101112] p-4 md:p-8">
|
||||
<PreviewFrame item={activeItem} />
|
||||
</div>
|
||||
|
||||
<aside className="relative flex min-h-0 flex-col bg-white p-6 md:p-8">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveId(null)}
|
||||
className="absolute right-5 top-5 inline-flex h-10 w-10 items-center justify-center rounded-full border border-black/10 text-text-primary transition hover:bg-subtle"
|
||||
aria-label={locale === 'zh' ? '关闭预览' : 'Close preview'}
|
||||
>
|
||||
<CloseGlyph className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<div className="pr-12">
|
||||
<div className="text-xs font-medium uppercase tracking-[0.18em] text-text-muted">
|
||||
{typeLabel(activeItem.type, locale)}
|
||||
</div>
|
||||
<h2 className="mt-3 text-3xl font-medium leading-tight tracking-tight text-text-primary">
|
||||
{activeItem.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<dl className="mt-8 grid gap-4 border-y border-black/10 py-6 text-sm">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<dt className="text-text-muted">{locale === 'zh' ? '格式' : 'Format'}</dt>
|
||||
<dd className="font-medium text-text-primary">{activeItem.fileFormat}</dd>
|
||||
</div>
|
||||
{activeItem.fileSize ? (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<dt className="text-text-muted">{locale === 'zh' ? '大小' : 'Size'}</dt>
|
||||
<dd className="font-medium text-text-primary">{activeItem.fileSize}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
|
||||
{activeItem.description ? (
|
||||
<p className="mt-6 text-sm leading-7 text-text-secondary">{activeItem.description}</p>
|
||||
) : null}
|
||||
|
||||
<div className="mt-auto pt-8">
|
||||
<a
|
||||
href={activeItem.externalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-full bg-black px-5 py-3 text-sm font-medium text-white transition hover:bg-[#2c2925]"
|
||||
>
|
||||
<DownloadGlyph className="h-4 w-4" />
|
||||
{locale === 'zh' ? '下载' : 'Download'}
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import Link from 'next/link'
|
||||
import type { Locale } from '@/i18n/config'
|
||||
import type { MediaAssetCategoryCard } from '@/lib/media-assets'
|
||||
|
||||
export function MediaAssetPageHeader({
|
||||
description,
|
||||
eyebrow,
|
||||
title,
|
||||
}: {
|
||||
description: string
|
||||
eyebrow: string
|
||||
title: string
|
||||
}) {
|
||||
return (
|
||||
<header className="max-w-reading">
|
||||
<div className="text-sm uppercase tracking-[0.18em] text-text-muted">{eyebrow}</div>
|
||||
<h1 className="mt-2 text-4xl font-medium tracking-tight md:text-6xl">{title}</h1>
|
||||
<p className="mt-4 text-lg leading-8 text-text-secondary">{description}</p>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export function MediaAssetBreadcrumbs({
|
||||
items,
|
||||
}: {
|
||||
items: Array<{ href?: string; label: string }>
|
||||
}) {
|
||||
return (
|
||||
<nav aria-label="Breadcrumb" className="mb-6 flex flex-wrap items-center gap-2 text-sm text-text-muted">
|
||||
{items.map((item, index) => (
|
||||
<span key={`${item.label}-${index}`} className="inline-flex items-center gap-2">
|
||||
{index > 0 ? <span aria-hidden="true">/</span> : null}
|
||||
{item.href ? (
|
||||
<Link href={item.href} className="transition hover:text-text-primary">
|
||||
{item.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-text-primary">{item.label}</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export function MediaAssetCategoryGrid({
|
||||
categories,
|
||||
locale,
|
||||
}: {
|
||||
categories: MediaAssetCategoryCard[]
|
||||
locale: Locale
|
||||
}) {
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{categories.map((category) => {
|
||||
const countLabel =
|
||||
category.childCount > 0
|
||||
? locale === 'zh'
|
||||
? `${category.childCount} 个分类`
|
||||
: `${category.childCount} categories`
|
||||
: locale === 'zh'
|
||||
? `${category.assetCount} 个资源`
|
||||
: `${category.assetCount} assets`
|
||||
|
||||
return (
|
||||
<article
|
||||
key={category.id}
|
||||
className="group overflow-hidden bg-white shadow-[0_18px_60px_rgba(18,24,31,0.055)] ring-1 ring-black/[0.05]"
|
||||
>
|
||||
<Link href={category.href} className="block">
|
||||
<div className="relative aspect-[16/10] overflow-hidden bg-[#ecefed]">
|
||||
{category.imageUrl ? (
|
||||
<img
|
||||
src={category.imageUrl}
|
||||
alt={category.name}
|
||||
className="h-full w-full object-cover transition duration-500 group-hover:scale-[1.035]"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-[linear-gradient(135deg,#e4e8e7_0%,#faf9f5_100%)] px-8 text-center text-sm uppercase tracking-[0.18em] text-text-muted">
|
||||
{category.name}
|
||||
</div>
|
||||
)}
|
||||
<span className="absolute bottom-4 left-4 bg-white/92 px-3 py-1 text-xs font-medium text-text-secondary shadow-[0_10px_24px_rgba(0,0,0,0.08)]">
|
||||
{countLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-medium tracking-tight text-text-primary transition group-hover:opacity-70">
|
||||
{category.name}
|
||||
</h2>
|
||||
{category.description ? (
|
||||
<p className="mt-3 line-clamp-2 text-sm leading-7 text-text-secondary">
|
||||
{category.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Link>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MediaAssetEmptyState({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-8 border border-dashed border-black/15 bg-white/70 px-6 py-10 text-sm leading-7 text-text-secondary md:px-8">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { setRequestLocale } from 'next-intl/server'
|
||||
import type { Locale } from '@/i18n/config'
|
||||
import { getMediaAssetLanding } from '@/lib/media-assets'
|
||||
import { buildLocaleAlternates } from '@/lib/seo'
|
||||
import {
|
||||
MediaAssetCategoryGrid,
|
||||
MediaAssetEmptyState,
|
||||
MediaAssetPageHeader,
|
||||
} from './media-assets-shared'
|
||||
|
||||
export const revalidate = 300
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params
|
||||
|
||||
return {
|
||||
alternates: buildLocaleAlternates('/media-assets', locale),
|
||||
description:
|
||||
locale === 'zh'
|
||||
? '下载 Eversolo 产品图片、媒体资料、PDF 与视频素材。'
|
||||
: 'Download Eversolo product images, press materials, PDFs, and video assets.',
|
||||
title: locale === 'zh' ? '媒体资料' : 'Media Assets',
|
||||
}
|
||||
}
|
||||
|
||||
export default async function MediaAssetsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
|
||||
const categories = await getMediaAssetLanding(locale)
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-page px-6 pb-12 pt-4 md:px-12 md:pb-16 md:pt-5">
|
||||
<MediaAssetPageHeader
|
||||
eyebrow={locale === 'zh' ? '媒体中心' : 'Media Library'}
|
||||
title={locale === 'zh' ? '媒体资料' : 'Media Assets'}
|
||||
description={
|
||||
locale === 'zh'
|
||||
? '按资料包浏览 Eversolo 官方图片、文档与视频资源,适合媒体报道、渠道页面和品牌素材下载。'
|
||||
: 'Browse official Eversolo image, document, and video kits for press coverage, channel pages, and brand use.'
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="mt-8">
|
||||
{categories.length > 0 ? (
|
||||
<MediaAssetCategoryGrid categories={categories} locale={locale} />
|
||||
) : (
|
||||
<MediaAssetEmptyState>
|
||||
{locale === 'zh'
|
||||
? '后台还没有添加媒体资料包分类。添加一级分类后,这里会自动显示入口。'
|
||||
: 'No media asset categories have been added yet. Top-level categories will appear here automatically.'}
|
||||
</MediaAssetEmptyState>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user