UI-Feinschliff: Settings-Tabs funktionieren, Details-Knopf tut etwas
Ampel / ampel (push) Successful in 33s
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:
@@ -1,5 +1,5 @@
|
||||
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 Dashboard from './pages/Dashboard'
|
||||
import MetadataPreview from './pages/MetadataPreview'
|
||||
@@ -61,12 +61,8 @@ function App() {
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-slate-800 dark:border-slate-700">
|
||||
<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">
|
||||
<LogOut size={20} />
|
||||
<span className="text-sm">Abmelden</span>
|
||||
</button>
|
||||
</div>
|
||||
{/* Abmelden-Knopf entfernt (23.07.): es gibt (noch) keinen
|
||||
Login-Flow im UI — ein toter Knopf gaukelt nur einen vor. */}
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
|
||||
@@ -16,6 +16,9 @@ interface Device {
|
||||
export default function DeviceDiscovery() {
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
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 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(() => {
|
||||
refreshDevices()
|
||||
const interval = setInterval(refreshDevices, 5000)
|
||||
@@ -146,10 +176,56 @@ export default function DeviceDiscovery() {
|
||||
</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'}`}>
|
||||
Details
|
||||
<button
|
||||
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>
|
||||
</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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { useDarkMode } from '../context/ThemeContext'
|
||||
import DeviceDiscovery from '../components/DeviceDiscovery'
|
||||
@@ -16,14 +16,6 @@ interface Job {
|
||||
title?: string
|
||||
}
|
||||
|
||||
interface Device {
|
||||
id: string
|
||||
name: string
|
||||
type: 'cd' | 'dvd' | 'bluray'
|
||||
path: string
|
||||
status: 'empty' | 'ready' | 'ripping'
|
||||
}
|
||||
|
||||
async function fetchJobs(): Promise<Job[]> {
|
||||
try {
|
||||
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 }) {
|
||||
const { theme } = useDarkMode()
|
||||
const styles = theme === 'dark'
|
||||
@@ -136,19 +119,13 @@ function ProgressBar({ progress }: { progress: number }) {
|
||||
|
||||
export default function Dashboard() {
|
||||
const [jobs, setJobs] = useState<Job[]>([])
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { theme } = useDarkMode()
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [jobsData, devicesData] = await Promise.all([
|
||||
fetchJobs(),
|
||||
fetchDevices()
|
||||
])
|
||||
setJobs(jobsData)
|
||||
setDevices(devicesData)
|
||||
setJobs(await fetchJobs())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -220,93 +197,10 @@ export default function Dashboard() {
|
||||
/>
|
||||
</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 */}
|
||||
{/* 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={`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>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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 { useDarkMode } from '../context/ThemeContext'
|
||||
|
||||
interface SettingsState {
|
||||
tmdbApiKey: string
|
||||
tvdbApiKey: string
|
||||
omdbApiKey: string
|
||||
outputDir: string
|
||||
movieDir: string
|
||||
seriesDir: string
|
||||
@@ -19,6 +20,7 @@ interface SettingsState {
|
||||
const defaultSettings: SettingsState = {
|
||||
tmdbApiKey: '',
|
||||
tvdbApiKey: '',
|
||||
omdbApiKey: '',
|
||||
outputDir: '/app/media',
|
||||
movieDir: 'movies',
|
||||
seriesDir: 'series',
|
||||
@@ -29,8 +31,11 @@ const defaultSettings: SettingsState = {
|
||||
notificationWebhook: '',
|
||||
}
|
||||
|
||||
type SettingsTab = 'ripping' | 'verzeichnisse' | 'apis' | 'benachrichtigungen'
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [settings, setSettings] = useState<SettingsState>(defaultSettings)
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>('ripping')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = 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'}`}>
|
||||
{/* 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`}>
|
||||
<TabButton icon={Disc} label="Ripping" isActive={true} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={Cpu} label="Verarbeitung" isActive={false} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={Database} label="Datenbank" isActive={false} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={Globe} label="APIs" isActive={false} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={AlertCircle} label="Benachrichtigungen" isActive={false} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={Disc} label="Ripping" isActive={activeTab === 'ripping'} onClick={() => setActiveTab('ripping')} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={Database} label="Verzeichnisse" isActive={activeTab === 'verzeichnisse'} onClick={() => setActiveTab('verzeichnisse')} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={Globe} label="APIs" isActive={activeTab === 'apis'} onClick={() => setActiveTab('apis')} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={AlertCircle} label="Benachrichtigungen" isActive={activeTab === 'benachrichtigungen'} onClick={() => setActiveTab('benachrichtigungen')} darkMode={theme === 'dark'} />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-8 transition-colors duration-300">
|
||||
{/* 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'}`}>
|
||||
<Disc size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} />
|
||||
Ripping-Einstellungen
|
||||
@@ -160,10 +164,10 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>}
|
||||
|
||||
{/* 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'}`}>
|
||||
<Database size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} />
|
||||
Ausgabeverzeichnisse
|
||||
@@ -223,10 +227,10 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>}
|
||||
|
||||
{/* 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'}`}>
|
||||
<Globe size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} />
|
||||
API-Schlüssel
|
||||
@@ -261,11 +265,27 @@ export default function SettingsPage() {
|
||||
placeholder="Dein TVDb API Key"
|
||||
/>
|
||||
</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>
|
||||
</section>}
|
||||
|
||||
{/* 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'}`}>
|
||||
<AlertCircle size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} />
|
||||
Webhook-Benachrichtigungen
|
||||
@@ -286,7 +306,7 @@ export default function SettingsPage() {
|
||||
Optional: URL für Benachrichtigungen bei Job-Ende (Discord, Slack, etc.)
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>}
|
||||
|
||||
{/* 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`}>
|
||||
@@ -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()
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`px-6 py-3 text-sm font-medium transition-colors border-b-2 ${
|
||||
isActive
|
||||
? `${darkMode ? 'border-indigo-500 text-indigo-400' : 'border-indigo-600 text-indigo-600'}`
|
||||
|
||||
Reference in New Issue
Block a user