feat(creator-packs): use branded pack banners on the cards
Replace the generic colored-block cards with the branded 1060x175 banners we build into each pack ZIM, shown on both the settings page and the Easy Setup wizard step (and the Content Explorer block, via the shared card). - Bundle the seed-pack banners as local webp under admin/public/creator-packs/ so they render offline (no external image dependency, matching the existing /rogue-support-banner.webp convention). - CreatorPackCard is now banner-forward: the banner is the hero, with a compact status/metadata footer (videos · size, Installed/Downloading/Selected/Install, update pill). Falls back to a simple header if an image is ever missing. - Prefer an optional catalog `banner_url` (future remote creators) over the bundled-by-id path; add banner_url to the CreatorPack type + validator. - Widen the card grids to 1–2 columns since banners are wide. Browser-tested on NOMAD3: banners render on the settings page and wizard step; selection/installed/downloading states and storage projection still correct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
326f947b2b
commit
9a864cb41b
|
|
@ -86,6 +86,7 @@ export const creatorPacksSpecSchema = vine.object({
|
|||
video_count: vine.number().min(0),
|
||||
size_mb: vine.number().min(0),
|
||||
license_id: vine.string(),
|
||||
banner_url: vine.string().url().optional(),
|
||||
poster_url: vine.string().url().optional(),
|
||||
logo_url: vine.string().url().optional(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useState } from 'react'
|
||||
import { formatBytes } from '~/lib/util'
|
||||
import type { CreatorPackWithStatus } from '../../types/collections'
|
||||
import classNames from 'classnames'
|
||||
|
|
@ -22,72 +23,75 @@ const CreatorPackCard: React.FC<CreatorPackCardProps> = ({ pack, selected, onCli
|
|||
const sizeBytes = pack.size_mb * 1024 * 1024
|
||||
|
||||
// Installed packs are inert unless a newer version is available (click = update).
|
||||
// Selected (wizard) and available packs are always clickable.
|
||||
const clickable = selected || !isInstalled || hasUpdate
|
||||
|
||||
const highlighted = selected || isDownloading || (isInstalled && !hasUpdate)
|
||||
|
||||
// Prefer a catalog-supplied banner (future remote creators); otherwise the
|
||||
// banner bundled with the app by pack id. Both are the branded 1060x175 art we
|
||||
// build into the ZIM. Fall back to a simple header only if the image is absent.
|
||||
const [bannerFailed, setBannerFailed] = useState(false)
|
||||
const bannerSrc = pack.banner_url || `/creator-packs/${pack.id}.webp`
|
||||
|
||||
const statusBadge = selected ? (
|
||||
<span className="flex items-center text-lime-600 dark:text-lime-400 text-sm font-medium">
|
||||
<IconCircleCheck className="w-5 h-5 mr-1" />
|
||||
Selected
|
||||
</span>
|
||||
) : isDownloading ? (
|
||||
<span className="flex items-center text-lime-600 dark:text-lime-400 text-sm font-medium">
|
||||
<IconLoader2 className="w-5 h-5 mr-1 animate-spin" />
|
||||
Downloading
|
||||
</span>
|
||||
) : isInstalled ? (
|
||||
<span className="flex items-center text-lime-600 dark:text-lime-400 text-sm font-medium">
|
||||
<IconCircleCheck className="w-5 h-5 mr-1" />
|
||||
Installed
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center text-text-muted text-sm font-medium">
|
||||
Install
|
||||
<IconChevronRight className="w-5 h-5 ml-1" />
|
||||
</span>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col bg-desert-green rounded-lg p-6 text-white border shadow-sm transition-shadow h-80',
|
||||
highlighted ? 'border-lime-400 border-2' : 'border-desert-green',
|
||||
clickable ? 'cursor-pointer hover:shadow-lg' : 'opacity-65 cursor-not-allowed'
|
||||
'flex flex-col rounded-lg overflow-hidden bg-surface-primary border shadow-sm transition-shadow',
|
||||
highlighted ? 'border-lime-400 border-2' : 'border-border-subtle',
|
||||
clickable ? 'cursor-pointer hover:shadow-lg' : 'opacity-70 cursor-not-allowed'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!clickable) return
|
||||
onClick?.(pack)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div className="flex items-center min-w-0">
|
||||
{pack.logo_url ? (
|
||||
<img
|
||||
src={pack.logo_url}
|
||||
alt=""
|
||||
className="w-6 h-6 mr-2 rounded object-cover shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<IconMovie className="w-6 h-6 mr-2 shrink-0" />
|
||||
)}
|
||||
<h3 className="text-lg font-semibold truncate">{pack.name}</h3>
|
||||
</div>
|
||||
{selected ? (
|
||||
<div className="flex items-center shrink-0">
|
||||
<IconCircleCheck className="w-5 h-5 text-lime-400" />
|
||||
<span className="text-lime-400 text-sm ml-1">Selected</span>
|
||||
</div>
|
||||
) : isDownloading ? (
|
||||
<div className="flex items-center shrink-0">
|
||||
<IconLoader2 className="w-5 h-5 text-lime-400 animate-spin" />
|
||||
<span className="text-lime-400 text-sm ml-1">Downloading</span>
|
||||
</div>
|
||||
) : isInstalled ? (
|
||||
<div className="flex items-center shrink-0">
|
||||
<IconCircleCheck className="w-5 h-5 text-lime-400" />
|
||||
<span className="text-lime-400 text-sm ml-1">Installed</span>
|
||||
</div>
|
||||
) : (
|
||||
<IconChevronRight className="w-5 h-5 text-white opacity-70 shrink-0" />
|
||||
)}
|
||||
{!bannerFailed ? (
|
||||
<img
|
||||
src={bannerSrc}
|
||||
alt={pack.name}
|
||||
className="w-full block aspect-[1060/175] object-cover"
|
||||
onError={() => setBannerFailed(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 bg-desert-green text-white px-5 py-6">
|
||||
<IconMovie className="w-6 h-6 shrink-0" />
|
||||
<h3 className="text-lg font-semibold truncate">{pack.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-300 text-xs mb-3">by {pack.creator}</p>
|
||||
|
||||
<p className="text-gray-200 grow">{pack.description}</p>
|
||||
|
||||
{hasUpdate && (
|
||||
<span className="self-start mt-2 text-xs px-2 py-1 rounded bg-lime-500/30 text-lime-200">
|
||||
Update available
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-white/20">
|
||||
<p className="text-gray-300 text-xs">
|
||||
{pack.video_count} videos | {formatBytes(sizeBytes, 0)}
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-3 px-5 py-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-text-secondary truncate">
|
||||
{pack.video_count} videos · {formatBytes(sizeBytes, 0)}
|
||||
</p>
|
||||
{hasUpdate && (
|
||||
<span className="inline-block mt-1 text-xs px-2 py-0.5 rounded bg-lime-500/20 text-lime-700 dark:text-lime-300">
|
||||
Update available
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{statusBadge}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const CreatorPacksSection: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{packs.length > 0 ? (
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="mt-4 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{packs.map((pack) => (
|
||||
<CreatorPackCard key={pack.id} pack={pack} onClick={setPackToInstall} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1037,7 +1037,7 @@ export default function EasySetupWizard(props: {
|
|||
</div>
|
||||
|
||||
{creatorPacks.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{creatorPacks.map((pack) => (
|
||||
<CreatorPackCard
|
||||
key={pack.id}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
|
|
@ -77,7 +77,13 @@ export type CreatorPack = {
|
|||
video_count: number
|
||||
size_mb: number
|
||||
license_id: string
|
||||
/** Optional card art (Phase 2). The Kiwix grid icon is baked into the ZIM. */
|
||||
/**
|
||||
* Optional branded 1060x175 banner (the card hero). When omitted, the app
|
||||
* uses the banner it bundles by pack id (`/creator-packs/<id>.webp`). The
|
||||
* Kiwix grid icon is baked into the ZIM separately.
|
||||
*/
|
||||
banner_url?: string
|
||||
/** Optional card art. */
|
||||
poster_url?: string
|
||||
logo_url?: string
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue