feat: Warm Industrial UI redesign + social footer + code deduplication

Design System (Phase A):
- New palette: warm charcoal #111110 / amber gold #c9954a / sage green #7a9a8a
- Fonts: Cabinet Grotesk (heading) + Geist (body) + JetBrains Mono (data)
- SVG grain texture overlay, collapsed sidebar with hover expand
- Bento grid dashboard with WideContinueCard + PosterCard + QuickLink
- Backward-compatible CSS aliases for glass-panel/neon-button/input-base

Social Footer (Phase B):
- AppFooter component with GitHub repo / author homepage / TG group links
- Embedded in LoginPage, Layout (global), and SettingsPage
- TG group badge added to README.md

Code Cleanup:
- Removed deprecated DownloadClientCard / NotifyChannelCard components
- Merged downloadClient/notify API clients into unified modules
- Route deduplication: removed authed-group duplicates for admin endpoints
- Removed redundant AdminPage tabs (sites/downloads/notify/scheduler)
- UI fixes: blank pages (discover/download-clients/notify-channels) nil slice defense
- Email channel support in NotifyChannelsPage
@
This commit is contained in:
ShukeBta
2026-05-17 23:13:26 +08:00
parent 9be81222cc
commit 828ca36d7f
18 changed files with 732 additions and 910 deletions
+28 -15
View File
@@ -1,14 +1,10 @@
import { Link } from 'react-router-dom'
import { Film } from 'lucide-react'
import { Film, Play } from 'lucide-react'
import { imageURL } from '../api/client'
import type { Media } from '../types'
// Compact poster tile used by every listing surface (home / library / search).
//
// Props:
// - media: the Media row to render
// - progress: 0..1 fractional resume position (optional)
/** Compact poster tile — used by library, search, favourites, poster wall, etc. */
export function MediaCard({
media,
progress,
@@ -19,34 +15,51 @@ export function MediaCard({
return (
<Link
to={`/media/${media.id}`}
className="group block overflow-hidden rounded-xl border border-white/5 bg-surface-800/60 transition hover:border-primary-400/50 hover:shadow-lg hover:shadow-primary-400/20"
className="group block overflow-hidden rounded-lg border border-cream-900/15 bg-surface-400 transition-all hover:border-brand-500/30 hover:bg-surface-300"
>
<div className="relative aspect-[2/3] w-full bg-surface-900">
{/* Poster */}
<div className="relative aspect-[2/3] w-full overflow-hidden bg-surface-600">
{media.poster_url ? (
<img
src={imageURL(media.poster_url)}
alt={media.title}
loading="lazy"
className="h-full w-full object-cover transition group-hover:scale-105"
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
referrerPolicy="no-referrer"
/>
) : (
<div className="flex h-full w-full items-center justify-center text-slate-600">
<div className="flex h-full w-full items-center justify-center text-cream-900/30">
<Film size={48} />
</div>
)}
{/* Hover overlay */}
<div className="absolute inset-0 flex items-end bg-gradient-to-t from-black/60 via-transparent to-transparent p-3 opacity-0 transition-opacity group-hover:opacity-100">
<span className="flex items-center gap-1.5 text-xs text-white/90">
<Play size={12} />
播放
</span>
</div>
{/* Progress bar */}
{progress !== undefined && progress > 0 && progress < 1 && (
<div className="absolute inset-x-0 bottom-0 h-1 bg-black/60">
<div className="absolute inset-x-0 bottom-0 h-1 bg-black/40">
<div
className="h-full bg-primary-400"
className="h-full bg-brand-500/80 transition-colors group-hover:bg-brand-400"
style={{ width: `${Math.round(progress * 100)}%` }}
/>
</div>
)}
</div>
<div className="px-3 py-2">
<p className="truncate text-sm font-medium text-white">{media.title}</p>
{media.year > 0 && <p className="text-xs text-slate-400">{media.year}</p>}
{/* Info */}
<div className="px-3 py-2.5">
<p className="truncate text-sm font-medium text-cream-200 group-hover:text-cream-100">
{media.title}
</p>
{media.year > 0 && (
<p className="mt-0.5 text-xs text-cream-500">{media.year}</p>
)}
</div>
</Link>
)