fix(ui): Zentrierung fuer Anleitung & Einstellungen gefixt, zufaellige Mockup-Filme durch echte API-Daten ersetzt
Ampel / ampel (push) Successful in 28s
Ampel / ampel (push) Successful in 28s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Cpu, HardDrive, Disc, Download, Trash2, RotateCcw, Activity } from 'lucide-react'
|
||||
import { Cpu, HardDrive, Disc, Download, Trash2, Activity, Film } from 'lucide-react'
|
||||
import { api } from '../lib/api'
|
||||
import { useToast } from '../context/ToastContext'
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card'
|
||||
@@ -93,17 +93,10 @@ export default function Dashboard() {
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const aktiverJob = jobs.find(j => j.status === 'processing' || j.status === 'transcoding') || jobs[0] || null
|
||||
const queueJobs = jobs.filter(j => j.id !== aktiverJob?.id && j.status === 'pending')
|
||||
const aktiverJob = jobs.find(j => j.status === 'processing' || j.status === 'transcoding')
|
||||
const queueJobs = jobs.filter(j => j.status === 'pending')
|
||||
const completedJobs = jobs.filter(j => j.status === 'completed')
|
||||
|
||||
// Sample data fallback for Recently Ripped if list is short to match SC1 visual richness
|
||||
const recentlyRippedList = completedJobs.length > 0 ? completedJobs.slice(0, 3) : [
|
||||
{ id: 'sample-1', title: 'Everything Everywhere All At Once', year: 2021, poster: 'https://images.unsplash.com/photo-1536440136628-849c177e76a1?q=80&w=400&auto=format&fit=crop' },
|
||||
{ id: 'sample-2', title: 'Top Gun: Maverick', year: 2023, poster: 'https://images.unsplash.com/photo-1517604931442-7e0c8ed2963c?q=80&w=400&auto=format&fit=crop' },
|
||||
{ id: 'sample-3', title: 'Black Panther: Wakanda Forever', year: 2022, poster: 'https://images.unsplash.com/photo-1626814026160-2237a95fc5a0?q=80&w=400&auto=format&fit=crop' },
|
||||
]
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
@@ -115,10 +108,15 @@ export default function Dashboard() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* 1. Top Hero Movie Wall Carousel (Identisch zu SC1 Top Banner) */}
|
||||
<HeroCinemaBanner />
|
||||
{/* 1. Top Hero Cinema Showcase (Nur echte API Daten!) */}
|
||||
<HeroCinemaBanner
|
||||
aktiverJob={aktiverJob}
|
||||
completedJobs={completedJobs}
|
||||
onDetailClick={id => setDetailJobId(id)}
|
||||
onCancelClick={id => api.post(`/jobs/${id}/cancel`).catch(() => {})}
|
||||
/>
|
||||
|
||||
{/* 2. Main 2-Column Grid Layout (Matching SC1 1:1) */}
|
||||
{/* 2. Main 2-Column Grid Layout */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
|
||||
{/* LEFT COLUMN (lg:col-span-7) */}
|
||||
@@ -128,14 +126,13 @@ export default function Dashboard() {
|
||||
<Card className="border border-amber-500/30 bg-[#0f1422] shadow-xl">
|
||||
<CardHeader className="border-b border-slate-800">
|
||||
<CardTitle className="text-amber-400 flex items-center gap-2">
|
||||
<Disc size={18} className="text-amber-400 animate-disc-spin" />
|
||||
<Disc size={18} className={aktiverJob ? 'text-amber-400 animate-disc-spin' : 'text-slate-500'} />
|
||||
Current Job
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-5">
|
||||
{aktiverJob ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Poster Thumbnail */}
|
||||
{posterUrl(aktiverJob.meta) ? (
|
||||
<img
|
||||
src={posterUrl(aktiverJob.meta)!}
|
||||
@@ -156,7 +153,6 @@ export default function Dashboard() {
|
||||
<TypeBadge type={aktiverJob.type} />
|
||||
</div>
|
||||
|
||||
{/* Amber Progress Bar */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-xs text-slate-400 font-mono">
|
||||
<span>Status: {aktiverJob.status === 'transcoding' ? 'Komprimieren' : 'Extraktion'}</span>
|
||||
@@ -171,8 +167,8 @@ export default function Dashboard() {
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-400 space-y-0.5 font-mono pt-1">
|
||||
<p>Ripping status: {aktiverJob.status === 'transcoding' ? 'HandBrake H.265' : 'MakeMKV Lossless Extraktion'}</p>
|
||||
<p>Disc type: <strong className="text-amber-300">{aktiverJob.type.toUpperCase()} Blu-ray</strong></p>
|
||||
<p>Status: {aktiverJob.status === 'transcoding' ? 'HandBrake H.265' : 'MakeMKV Lossless Extraktion'}</p>
|
||||
<p>Disc typ: <strong className="text-amber-300">{aktiverJob.type.toUpperCase()}</strong></p>
|
||||
<p>Gerät: {aktiverJob.device} · Gestartet: {new Date(aktiverJob.startTime).toLocaleTimeString('de-DE')}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,51 +181,25 @@ export default function Dashboard() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Ripping Queue Card */}
|
||||
{/* Ripping Queue Card (NUR ECHTE QUEUED JOBS) */}
|
||||
<Card className="border border-slate-800 bg-[#0f1422]">
|
||||
<CardHeader className="border-b border-slate-800">
|
||||
<CardTitle className="text-slate-200">Ripping Queue</CardTitle>
|
||||
<CardTitle className="text-slate-200">Ripping Queue ({queueJobs.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 space-y-3">
|
||||
{queueJobs.length === 0 ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between p-3 rounded-xl bg-slate-900/60 border border-slate-800 text-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-amber-400 font-bold">2.</span>
|
||||
<span className="font-medium text-slate-300">The Menu</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 font-mono">Queue 1</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-xl bg-slate-900/60 border border-slate-800 text-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-amber-400 font-bold">3.</span>
|
||||
<span className="font-medium text-slate-300">Glass Onion</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 font-mono">Queue 2</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-xl bg-slate-900/60 border border-slate-800 text-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-amber-400 font-bold">4.</span>
|
||||
<span className="font-medium text-slate-300">Babylon</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 font-mono">Queue 3</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-xl bg-slate-900/60 border border-slate-800 text-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-amber-400 font-bold">5.</span>
|
||||
<span className="font-medium text-slate-300">Avatar: The Way of Water</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 font-mono">Queue 4</span>
|
||||
</div>
|
||||
<div className="text-center py-8 text-slate-500 text-xs font-mono">
|
||||
Warteschlange ist leer (0 Jobs eingereiht)
|
||||
</div>
|
||||
) : (
|
||||
queueJobs.map((j, idx) => (
|
||||
<div key={j.id} className="flex items-center justify-between p-3 rounded-xl bg-slate-900/60 border border-slate-800 text-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-amber-400 font-bold">{idx + 2}.</span>
|
||||
<span className="font-mono text-amber-400 font-bold">{idx + 1}.</span>
|
||||
<span className="font-medium text-slate-200">{j.title || 'Unbekannt'}</span>
|
||||
<TypeBadge type={j.type} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 font-mono">Queue {idx + 1}</span>
|
||||
<span className="text-xs text-slate-400 font-mono">Wartend</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -241,48 +211,45 @@ export default function Dashboard() {
|
||||
{/* RIGHT COLUMN (lg:col-span-5) */}
|
||||
<div className="lg:col-span-5 space-y-6">
|
||||
|
||||
{/* Server Status Card with Live Sparkline Graph */}
|
||||
{/* Server Status Card mit Live Sparkline Graph */}
|
||||
<Card className="border border-slate-800 bg-[#0f1422]">
|
||||
<CardHeader className="border-b border-slate-800">
|
||||
<CardTitle className="text-slate-200">Server Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-5">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-6">
|
||||
|
||||
{/* Left: Usage Bars */}
|
||||
<div className="space-y-4 flex-1 w-full">
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><Cpu size={14} className="text-amber-400" /> CPU Usage:</span>
|
||||
<span className="text-amber-400 font-bold">42%</span>
|
||||
<span className="text-amber-400 font-bold">Aktiv</span>
|
||||
</div>
|
||||
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
|
||||
<div className="h-full bg-amber-500 rounded-full w-[42%]" />
|
||||
<div className="h-full bg-amber-500 rounded-full w-[35%]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><Activity size={14} className="text-amber-400" /> RAM Usage:</span>
|
||||
<span className="text-slate-200">6.1GB / 18GB</span>
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><Activity size={14} className="text-amber-400" /> Status:</span>
|
||||
<span className="text-slate-200">Bereit</span>
|
||||
</div>
|
||||
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
|
||||
<div className="h-full bg-amber-500/80 rounded-full w-[34%]" />
|
||||
<div className="h-full bg-amber-500/80 rounded-full w-[20%]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><HardDrive size={14} className="text-amber-400" /> Storage Used:</span>
|
||||
<span className="text-slate-200">2.1TB / 8TB</span>
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><HardDrive size={14} className="text-amber-400" /> Speichermounts:</span>
|
||||
<span className="text-slate-200">{plaetze.length ? `${plaetze[0].frei_gb} GB frei` : 'Aktiv'}</span>
|
||||
</div>
|
||||
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
|
||||
<div className="h-full bg-amber-500/70 rounded-full w-[26%]" />
|
||||
<div className="h-full bg-amber-500/70 rounded-full w-[50%]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Live Amber Sparkline Graph */}
|
||||
<div className="w-full sm:w-36 h-24 flex-shrink-0 flex items-end justify-center pt-2">
|
||||
<svg viewBox="0 0 100 50" className="w-full h-full stroke-amber-400 fill-amber-500/10">
|
||||
<path
|
||||
@@ -295,43 +262,55 @@ export default function Dashboard() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recently Ripped Card (3 Poster Grid matching SC1) */}
|
||||
{/* Recently Ripped Card (NUR ECHTE GERIPPTE JOBS) */}
|
||||
<Card className="border border-slate-800 bg-[#0f1422]">
|
||||
<CardHeader className="border-b border-slate-800">
|
||||
<CardTitle className="text-slate-200">Recently Ripped</CardTitle>
|
||||
<CardHeader className="border-b border-slate-800 flex items-center justify-between">
|
||||
<CardTitle className="text-slate-200">Zuletzt gerippt ({completedJobs.length})</CardTitle>
|
||||
{completedJobs.length > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setAufraeumenOffen(true)}>
|
||||
<Trash2 size={13} /> Aufräumen
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="p-4">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{recentlyRippedList.map((item: any) => {
|
||||
const poster = item.poster || posterUrl(item.meta)
|
||||
return (
|
||||
<div key={item.id} className="group relative rounded-xl overflow-hidden bg-slate-900 border border-slate-800 flex flex-col justify-between p-2">
|
||||
<div className="relative aspect-[2/3] rounded-lg overflow-hidden mb-2 bg-slate-800">
|
||||
{poster ? (
|
||||
<img src={poster} alt={item.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-slate-500">
|
||||
<Disc size={24} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{completedJobs.length === 0 ? (
|
||||
<div className="text-center py-8 text-slate-500 text-xs font-mono">
|
||||
Noch keine gerippten Medien in der Datenbank
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{completedJobs.slice(0, 3).map((item) => {
|
||||
const poster = posterUrl(item.meta)
|
||||
return (
|
||||
<div key={item.id} className="group relative rounded-xl overflow-hidden bg-slate-900 border border-slate-800 flex flex-col justify-between p-2">
|
||||
<div className="relative aspect-[2/3] rounded-lg overflow-hidden mb-2 bg-slate-800">
|
||||
{poster ? (
|
||||
<img src={poster} alt={item.title || ''} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center text-slate-500 p-2 text-center">
|
||||
<Film size={24} className="text-amber-400 mb-1" />
|
||||
<span className="text-[9px] uppercase font-bold text-slate-400">{item.type}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-left">
|
||||
<h4 className="text-xs font-bold text-slate-100 truncate" title={item.title}>{item.title}</h4>
|
||||
<p className="text-[10px] text-slate-400 font-mono">{item.year || 2023}</p>
|
||||
<Button
|
||||
variant="amber"
|
||||
size="sm"
|
||||
className="w-full text-[10px] py-1 h-auto"
|
||||
onClick={() => setDetailJobId(item.id)}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<div className="space-y-1 text-left">
|
||||
<h4 className="text-xs font-bold text-slate-100 truncate" title={item.title}>{item.title || 'Unbekannt'}</h4>
|
||||
<p className="text-[10px] text-slate-400 font-mono">{new Date(item.startTime).toLocaleDateString('de-DE')}</p>
|
||||
<Button
|
||||
variant="amber"
|
||||
size="sm"
|
||||
className="w-full text-[10px] py-1 h-auto"
|
||||
onClick={() => setDetailJobId(item.id)}
|
||||
>
|
||||
Details
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user