UI-Feinschliff: Settings-Tabs funktionieren, Details-Knopf tut etwas
Ampel / ampel (push) Successful in 33s

- Settings: Tabs waren reine Dekoration (kein onClick, isActive fest) —
  jetzt echte Tab-Navigation (Ripping/Verzeichnisse/APIs/Benachrichtigungen)
  + OMDb-Key-Feld im APIs-Tab
- Laufwerk-Details: aufklappbares Panel (Pfad/Modell/Seriennummer/Disc)
  mit "Rippen starten" (POST /jobs) und "Auswerfen" (Eject-Endpoint)
- Placebos raus: 4 tote Schnellaktionen, doppelte Geraete-Sektion mit
  zweitem totem Details-Knopf, toter Abmelden-Knopf

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-23 15:30:09 +02:00
parent 5528e0f652
commit 1aa51ca943
4 changed files with 123 additions and 136 deletions
+3 -7
View File
@@ -1,5 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import { LayoutDashboard, Disc, Settings, LogOut, Sun, Moon, Terminal } from 'lucide-react' import { LayoutDashboard, Disc, Settings, Sun, Moon, Terminal } from 'lucide-react'
import { useDarkMode } from './context/ThemeContext' import { useDarkMode } from './context/ThemeContext'
import Dashboard from './pages/Dashboard' import Dashboard from './pages/Dashboard'
import MetadataPreview from './pages/MetadataPreview' import MetadataPreview from './pages/MetadataPreview'
@@ -61,12 +61,8 @@ function App() {
))} ))}
</nav> </nav>
<div className="p-4 border-t border-slate-800 dark:border-slate-700"> {/* Abmelden-Knopf entfernt (23.07.): es gibt (noch) keinen
<button className="w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors text-slate-400 hover:text-white hover:bg-slate-800 dark:text-slate-400 dark:hover:text-white dark:hover:bg-slate-700"> Login-Flow im UI — ein toter Knopf gaukelt nur einen vor. */}
<LogOut size={20} />
<span className="text-sm">Abmelden</span>
</button>
</div>
</aside> </aside>
{/* Main Content */} {/* Main Content */}
+78 -2
View File
@@ -16,6 +16,9 @@ interface Device {
export default function DeviceDiscovery() { export default function DeviceDiscovery() {
const [devices, setDevices] = useState<Device[]>([]) const [devices, setDevices] = useState<Device[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [expandedId, setExpandedId] = useState<string | null>(null)
const [actionFeedback, setActionFeedback] = useState<string | null>(null)
const [actionBusy, setActionBusy] = useState(false)
const { theme } = useDarkMode() const { theme } = useDarkMode()
const refreshDevices = async () => { const refreshDevices = async () => {
@@ -29,6 +32,33 @@ export default function DeviceDiscovery() {
} }
} }
const startRip = async (device: Device) => {
setActionBusy(true)
setActionFeedback(null)
try {
const response = await api.post('/jobs', { device_path: device.path })
setActionFeedback(`✓ Job angelegt (${response.data.id.slice(0, 8)}…) — Fortschritt im Dashboard`)
} catch (error: any) {
setActionFeedback(`${error?.response?.data?.detail || 'Job konnte nicht angelegt werden'}`)
} finally {
setActionBusy(false)
}
}
const ejectDisc = async (device: Device) => {
setActionBusy(true)
setActionFeedback(null)
try {
await api.post(`/devices/${device.id}/eject`)
setActionFeedback('✓ Disc ausgeworfen')
refreshDevices()
} catch (error: any) {
setActionFeedback(`${error?.response?.data?.detail || 'Auswurf fehlgeschlagen'}`)
} finally {
setActionBusy(false)
}
}
useEffect(() => { useEffect(() => {
refreshDevices() refreshDevices()
const interval = setInterval(refreshDevices, 5000) const interval = setInterval(refreshDevices, 5000)
@@ -146,10 +176,56 @@ export default function DeviceDiscovery() {
</div> </div>
</div> </div>
</div> </div>
<button className={`text-sm font-medium transition-colors ${theme === 'dark' ? 'text-indigo-400 hover:text-indigo-300' : 'text-indigo-600 hover:text-indigo-700'}`}> <button
Details onClick={() => {
setExpandedId(expandedId === device.id ? null : device.id)
setActionFeedback(null)
}}
className={`text-sm font-medium transition-colors ${theme === 'dark' ? 'text-indigo-400 hover:text-indigo-300' : 'text-indigo-600 hover:text-indigo-700'}`}
>
{expandedId === device.id ? 'Schließen' : 'Details'}
</button> </button>
</div> </div>
{expandedId === device.id && (
<div className={`mt-4 pt-4 border-t space-y-3 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'}`}>
<div className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Gerätepfad</span>
<span className={`font-mono ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>{device.path}</span>
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Modell</span>
<span className={theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}>{device.model || 'unbekannt'}</span>
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Seriennummer</span>
<span className={`font-mono text-xs ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>{device.serial || '—'}</span>
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Disc</span>
<span className={theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}>
{device.status === 'ready' ? `eingelegt (${device.type.toUpperCase()})` : 'keine'}
</span>
</div>
<div className="flex items-center gap-3 pt-1">
<button
onClick={() => startRip(device)}
disabled={actionBusy || device.status !== 'ready'}
className="px-4 py-2 rounded-lg bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>
Rippen starten
</button>
<button
onClick={() => ejectDisc(device)}
disabled={actionBusy || device.status !== 'ready'}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${theme === 'dark' ? 'bg-slate-700 text-slate-200 hover:bg-slate-600' : 'bg-slate-200 text-slate-700 hover:bg-slate-300'}`}
>
Auswerfen
</button>
</div>
{actionFeedback && (
<p className={`text-sm ${actionFeedback.startsWith('✓') ? 'text-emerald-500' : 'text-rose-500'}`}>
{actionFeedback}
</p>
)}
</div>
)}
</div> </div>
))} ))}
</div> </div>
+5 -111
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Clock, Activity, HardDrive, BarChart, AlertCircle, CheckCircle, Disc, Play, Pause, SkipForward } from 'lucide-react' import { Clock, Activity, AlertCircle, CheckCircle, Disc } from 'lucide-react'
import { api } from '../lib/api' import { api } from '../lib/api'
import { useDarkMode } from '../context/ThemeContext' import { useDarkMode } from '../context/ThemeContext'
import DeviceDiscovery from '../components/DeviceDiscovery' import DeviceDiscovery from '../components/DeviceDiscovery'
@@ -16,14 +16,6 @@ interface Job {
title?: string title?: string
} }
interface Device {
id: string
name: string
type: 'cd' | 'dvd' | 'bluray'
path: string
status: 'empty' | 'ready' | 'ripping'
}
async function fetchJobs(): Promise<Job[]> { async function fetchJobs(): Promise<Job[]> {
try { try {
const response = await api.get('/jobs') const response = await api.get('/jobs')
@@ -33,15 +25,6 @@ async function fetchJobs(): Promise<Job[]> {
} }
} }
async function fetchDevices(): Promise<Device[]> {
try {
const response = await api.get('/devices')
return response.data
} catch {
return []
}
}
function StatusBadge({ status }: { status: string }) { function StatusBadge({ status }: { status: string }) {
const { theme } = useDarkMode() const { theme } = useDarkMode()
const styles = theme === 'dark' const styles = theme === 'dark'
@@ -136,19 +119,13 @@ function ProgressBar({ progress }: { progress: number }) {
export default function Dashboard() { export default function Dashboard() {
const [jobs, setJobs] = useState<Job[]>([]) const [jobs, setJobs] = useState<Job[]>([])
const [devices, setDevices] = useState<Device[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const { theme } = useDarkMode() const { theme } = useDarkMode()
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
try { try {
const [jobsData, devicesData] = await Promise.all([ setJobs(await fetchJobs())
fetchJobs(),
fetchDevices()
])
setJobs(jobsData)
setDevices(devicesData)
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -220,93 +197,10 @@ export default function Dashboard() {
/> />
</div> </div>
{/* Quick Actions */}
<div className={`rounded-xl shadow-sm border p-6 transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
<h2 className={`text-lg font-semibold mb-4 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>Schnellaktionen</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 transition-colors duration-300">
<button className="flex flex-col items-center justify-center gap-3 p-4 rounded-lg border transition-all group hover:border-indigo-500 transition-colors duration-300">
<div className={`p-3 rounded-full transition-colors ${theme === 'dark' ? 'bg-indigo-900/30 text-indigo-400' : 'bg-indigo-100 text-indigo-600'} group-hover:bg-indigo-200`}>
<Disc size={24} />
</div>
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>Neuer Job</span>
</button>
<button className="flex flex-col items-center justify-center gap-3 p-4 rounded-lg border transition-all group hover:border-emerald-500 transition-colors duration-300">
<div className={`p-3 rounded-full transition-colors ${theme === 'dark' ? 'bg-emerald-900/30 text-emerald-400' : 'bg-emerald-100 text-emerald-600'} group-hover:bg-emerald-200`}>
<Play size={24} />
</div>
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>Alle starten</span>
</button>
<button className="flex flex-col items-center justify-center gap-3 p-4 rounded-lg border transition-all group hover:border-amber-500 transition-colors duration-300">
<div className={`p-3 rounded-full transition-colors ${theme === 'dark' ? 'bg-amber-900/30 text-amber-400' : 'bg-amber-100 text-amber-600'} group-hover:bg-amber-200`}>
<Pause size={24} />
</div>
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>Pausieren</span>
</button>
<button className="flex flex-col items-center justify-center gap-3 p-4 rounded-lg border transition-all group hover:border-rose-500 transition-colors duration-300">
<div className={`p-3 rounded-full transition-colors ${theme === 'dark' ? 'bg-rose-900/30 text-rose-400' : 'bg-rose-100 text-rose-600'} group-hover:bg-rose-200`}>
<SkipForward size={24} />
</div>
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>Überspringen</span>
</button>
</div>
</div>
{/* Devices */}
<div className={`rounded-xl shadow-sm border overflow-hidden transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
<div className={`px-6 py-4 border-b transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'}`}>
<h2 className={`text-lg font-semibold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>Geräte</h2>
</div>
<div className="p-6 transition-colors duration-300">
{devices.length === 0 ? (
<div className="text-center py-12 transition-colors duration-300">
<div className={`mx-auto w-16 h-16 rounded-full flex items-center justify-center mb-4 ${theme === 'dark' ? 'bg-slate-700' : 'bg-slate-100'}`}>
<HardDrive className={theme === 'dark' ? 'text-slate-400' : 'text-slate-400'} size={32} />
</div>
<p className={`text-sm ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Keine Geräte gefunden</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 transition-colors duration-300">
{devices.map(device => (
<div key={device.id} className={`rounded-lg p-4 border transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-900 border-slate-700' : 'bg-slate-50 border-slate-200'}`}>
<div className="flex items-start justify-between mb-3 transition-colors duration-300">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg transition-colors duration-300 ${
device.type === 'cd' ? theme === 'dark' ? 'bg-blue-900/30 text-blue-400' : 'bg-blue-100 text-blue-600' :
device.type === 'dvd' ? theme === 'dark' ? 'bg-purple-900/30 text-purple-400' : 'bg-purple-100 text-purple-600' :
theme === 'dark' ? 'bg-pink-900/30 text-pink-400' : 'bg-pink-100 text-pink-600'
}`}>
<Disc size={20} />
</div>
<div>
<h3 className={`font-semibold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>{device.name}</h3>
<p className={`text-xs font-mono ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>{device.path}</p>
</div>
</div>
<TypeBadge type={device.type} />
</div>
<div className="flex items-center justify-between mt-4 transition-colors duration-300">
<span className={`px-2.5 py-1 rounded-md text-xs font-medium transition-colors duration-300 ${
device.status === 'empty' ? theme === 'dark' ? 'bg-slate-700 text-slate-300' : 'bg-slate-100 text-slate-600' :
device.status === 'ready' ? theme === 'dark' ? 'bg-emerald-900/30 text-emerald-400' : 'bg-emerald-100 text-emerald-700' :
theme === 'dark' ? 'bg-blue-900/30 text-blue-400' : 'bg-blue-100 text-blue-700'
}`}>
{device.status === 'empty' ? 'Leer' :
device.status === 'ready' ? 'Bereit' :
'Rippt'}
</span>
<button className={`text-sm font-medium transition-colors duration-300 ${theme === 'dark' ? 'text-indigo-400 hover:text-indigo-300' : 'text-indigo-600 hover:text-indigo-700'}`}>
Details
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Recent Jobs */} {/* Recent Jobs */}
{/* Schnellaktionen + doppelte Geräte-Sektion sind raus (23.07.): alle
vier Knöpfe und der zweite Details-Button waren funktionslos —
die echte Geräte-Steuerung lebt in DeviceDiscovery oben. */}
<div className={`rounded-xl shadow-sm border overflow-hidden transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}> <div className={`rounded-xl shadow-sm border overflow-hidden transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
<div className={`px-6 py-4 border-b transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'} flex items-center justify-between`}> <div className={`px-6 py-4 border-b transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'} flex items-center justify-between`}>
<h2 className={`text-lg font-semibold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>Neueste Jobs</h2> <h2 className={`text-lg font-semibold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>Neueste Jobs</h2>
+37 -16
View File
@@ -1,11 +1,12 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Settings, Save, Disc, Cpu, Database, Globe, AlertCircle, CheckCircle } from 'lucide-react' import { Save, Disc, Database, Globe, AlertCircle, CheckCircle } from 'lucide-react'
import { api } from '../lib/api' import { api } from '../lib/api'
import { useDarkMode } from '../context/ThemeContext' import { useDarkMode } from '../context/ThemeContext'
interface SettingsState { interface SettingsState {
tmdbApiKey: string tmdbApiKey: string
tvdbApiKey: string tvdbApiKey: string
omdbApiKey: string
outputDir: string outputDir: string
movieDir: string movieDir: string
seriesDir: string seriesDir: string
@@ -19,6 +20,7 @@ interface SettingsState {
const defaultSettings: SettingsState = { const defaultSettings: SettingsState = {
tmdbApiKey: '', tmdbApiKey: '',
tvdbApiKey: '', tvdbApiKey: '',
omdbApiKey: '',
outputDir: '/app/media', outputDir: '/app/media',
movieDir: 'movies', movieDir: 'movies',
seriesDir: 'series', seriesDir: 'series',
@@ -29,8 +31,11 @@ const defaultSettings: SettingsState = {
notificationWebhook: '', notificationWebhook: '',
} }
type SettingsTab = 'ripping' | 'verzeichnisse' | 'apis' | 'benachrichtigungen'
export default function SettingsPage() { export default function SettingsPage() {
const [settings, setSettings] = useState<SettingsState>(defaultSettings) const [settings, setSettings] = useState<SettingsState>(defaultSettings)
const [activeTab, setActiveTab] = useState<SettingsTab>('ripping')
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false) const [saved, setSaved] = useState(false)
@@ -90,19 +95,18 @@ export default function SettingsPage() {
)} )}
<div className={`rounded-xl shadow-sm overflow-hidden transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border border-slate-700' : 'bg-white border border-slate-200'}`}> <div className={`rounded-xl shadow-sm overflow-hidden transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border border-slate-700' : 'bg-white border border-slate-200'}`}>
{/* Tabs */} {/* Tabs — echte Navigation (vorher reine Dekoration ohne onClick) */}
<div className={`flex transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'} border-b`}> <div className={`flex transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'} border-b`}>
<TabButton icon={Disc} label="Ripping" isActive={true} darkMode={theme === 'dark'} /> <TabButton icon={Disc} label="Ripping" isActive={activeTab === 'ripping'} onClick={() => setActiveTab('ripping')} darkMode={theme === 'dark'} />
<TabButton icon={Cpu} label="Verarbeitung" isActive={false} darkMode={theme === 'dark'} /> <TabButton icon={Database} label="Verzeichnisse" isActive={activeTab === 'verzeichnisse'} onClick={() => setActiveTab('verzeichnisse')} darkMode={theme === 'dark'} />
<TabButton icon={Database} label="Datenbank" isActive={false} darkMode={theme === 'dark'} /> <TabButton icon={Globe} label="APIs" isActive={activeTab === 'apis'} onClick={() => setActiveTab('apis')} darkMode={theme === 'dark'} />
<TabButton icon={Globe} label="APIs" isActive={false} darkMode={theme === 'dark'} /> <TabButton icon={AlertCircle} label="Benachrichtigungen" isActive={activeTab === 'benachrichtigungen'} onClick={() => setActiveTab('benachrichtigungen')} darkMode={theme === 'dark'} />
<TabButton icon={AlertCircle} label="Benachrichtigungen" isActive={false} darkMode={theme === 'dark'} />
</div> </div>
{/* Content */} {/* Content */}
<div className="p-6 space-y-8 transition-colors duration-300"> <div className="p-6 space-y-8 transition-colors duration-300">
{/* Ripping Settings */} {/* Ripping Settings */}
<section> {activeTab === 'ripping' && <section>
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 transition-colors duration-300 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}> <h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 transition-colors duration-300 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>
<Disc size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} /> <Disc size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} />
Ripping-Einstellungen Ripping-Einstellungen
@@ -160,10 +164,10 @@ export default function SettingsPage() {
/> />
</div> </div>
</div> </div>
</section> </section>}
{/* Output Directories */} {/* Output Directories */}
<section> {activeTab === 'verzeichnisse' && <section>
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 transition-colors duration-300 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}> <h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 transition-colors duration-300 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>
<Database size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} /> <Database size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} />
Ausgabeverzeichnisse Ausgabeverzeichnisse
@@ -223,10 +227,10 @@ export default function SettingsPage() {
/> />
</div> </div>
</div> </div>
</section> </section>}
{/* API Keys */} {/* API Keys */}
<section> {activeTab === 'apis' && <section>
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 transition-colors duration-300 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}> <h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 transition-colors duration-300 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>
<Globe size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} /> <Globe size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} />
API-Schlüssel API-Schlüssel
@@ -261,11 +265,27 @@ export default function SettingsPage() {
placeholder="Dein TVDb API Key" placeholder="Dein TVDb API Key"
/> />
</div> </div>
<div className="space-y-2 transition-colors duration-300">
<label className={`block text-sm font-medium ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>
OMDb API Key (Fallback)
</label>
<input
type="password"
value={settings.omdbApiKey}
onChange={(e) => handleChange('omdbApiKey', e.target.value)}
className={`w-full px-3 py-2 ${theme === 'dark' ? 'bg-slate-800 border-slate-600 text-slate-200' : 'bg-white border-slate-300 text-slate-900'} border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all`}
placeholder="Dein OMDb API Key"
/>
<p className={`text-xs ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>
Zweite Quelle, wenn TMDB nichts findet kostenloser Key auf <a href="https://www.omdbapi.com/apikey.aspx" target="_blank" rel="noopener noreferrer" className={`${theme === 'dark' ? 'text-indigo-400 hover:underline' : 'text-indigo-600 hover:underline'}`}>omdbapi.com</a>
</p>
</div> </div>
</section> </div>
</section>}
{/* Webhook */} {/* Webhook */}
<section> {activeTab === 'benachrichtigungen' && <section>
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 transition-colors duration-300 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}> <h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 transition-colors duration-300 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>
<AlertCircle size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} /> <AlertCircle size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} />
Webhook-Benachrichtigungen Webhook-Benachrichtigungen
@@ -286,7 +306,7 @@ export default function SettingsPage() {
Optional: URL für Benachrichtigungen bei Job-Ende (Discord, Slack, etc.) Optional: URL für Benachrichtigungen bei Job-Ende (Discord, Slack, etc.)
</p> </p>
</div> </div>
</section> </section>}
{/* Save Button */} {/* Save Button */}
<div className={`flex items-center justify-end gap-4 pt-6 transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'} border-t`}> <div className={`flex items-center justify-end gap-4 pt-6 transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'} border-t`}>
@@ -314,10 +334,11 @@ export default function SettingsPage() {
) )
} }
function TabButton({ icon: Icon, label, isActive, darkMode }: { icon: any, label: string, isActive: boolean, darkMode: boolean }) { function TabButton({ icon: Icon, label, isActive, onClick, darkMode }: { icon: any, label: string, isActive: boolean, onClick: () => void, darkMode: boolean }) {
const { theme } = useDarkMode() const { theme } = useDarkMode()
return ( return (
<button <button
onClick={onClick}
className={`px-6 py-3 text-sm font-medium transition-colors border-b-2 ${ className={`px-6 py-3 text-sm font-medium transition-colors border-b-2 ${
isActive isActive
? `${darkMode ? 'border-indigo-500 text-indigo-400' : 'border-indigo-600 text-indigo-600'}` ? `${darkMode ? 'border-indigo-500 text-indigo-400' : 'border-indigo-600 text-indigo-600'}`