feat: bootstrap MediaStationGo (Go + React rewrite of MediaStation)

Adopt the cropflre/nowen-video tech stack and rebuild the project from
scratch:

  - Backend: Go 1.25 + Gin + GORM + SQLite (WAL) + JWT + WebSocket hub.
    Layered packages config / database / model / repository / service /
    middleware / handler. Default admin (admin/admin123) seeded on first
    run; /api routes for auth, libraries, media, stream and admin
    panels. WebSocket scan-progress events at /api/ws.
  - Frontend: React 18 + Vite 5 + Tailwind 3.4 + Zustand + axios +
    react-router 6 + lucide-react + framer-motion + hls.js. Pages for
    Login / Home / Library / Search / MediaDetail / Player / Admin
    (Library, Users, Settings tabs).
  - Distribution: multi-arch Dockerfile (frontend -> backend -> Alpine
    runtime), docker-compose.yml, GitHub Actions for CI and GHCR
    publish, Makefile, env-prefixed config (MEDIASTATION_*).
  - Docs: README, CONTRIBUTING, .env.example, config.example.yaml.

Backend builds, vets and tests pass. Frontend builds via tsc -b && vite
build (250 kB JS / 16 kB CSS, gzipped 84 / 4 kB).
This commit is contained in:
Kiro
2026-05-14 15:26:29 +00:00
commit d5cf5fb4b2
60 changed files with 7592 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
import { Link } from 'react-router-dom'
import { Film } from 'lucide-react'
import type { Media } from '../types'
// Compact poster tile used by every listing surface (home / library / search).
export function MediaCard({ media }: { media: Media }) {
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"
>
<div className="aspect-[2/3] w-full bg-surface-900">
{media.poster_url ? (
<img
src={media.poster_url}
alt={media.title}
loading="lazy"
className="h-full w-full object-cover transition group-hover:scale-105"
/>
) : (
<div className="flex h-full w-full items-center justify-center text-slate-600">
<Film size={48} />
</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>}
</div>
</Link>
)
}