Fix blocked dates red styling - add modifiersClassNames

- Add modifiersClassNames to both DayPicker modes (single and multiple)
- Blocked dates now show with rdp-day_blocked class (red background)
- NonWorking and pending modifiers also properly styled
- Improve layout: single column with centered calendar
- Better spacing and hover effects on buttons
This commit is contained in:
Alex Polo
2025-10-31 20:42:37 +01:00
parent d9eb9cb314
commit c931aa5b98
3 changed files with 551 additions and 52 deletions
+1
View File
@@ -13,3 +13,4 @@
2025-10-30T19:58:29: > Ready on http://localhost:3000
2025-10-31T19:17:21: > Ready on http://localhost:3000
2025-10-31T20:05:46: > Ready on http://localhost:3000
2025-10-31T20:42:18: > Ready on http://localhost:3000
+58 -52
View File
@@ -262,64 +262,64 @@ function AvailabilityAdminContent() {
return (
<div className="min-h-screen bg-primary-50 py-8">
<div className="container mx-auto px-4">
<div className="container mx-auto px-4 max-w-6xl">
<h1 className="text-3xl font-bold mb-6">Tilgængelighed & blokeringer</h1>
{loading ? <p>Indlæser...</p> : (
<div className="grid md:grid-cols-2 gap-8">
<div className="space-y-6">
{/* Main calendar section */}
<section className="bg-white rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">Kalender klik for at skifte tilgængelighed</h2>
<p className="text-sm text-primary-600 mb-3">Klik en dato for at markere den som ikke-tilgængelig (klik igen for at åbne).</p>
<div>
<div className="flex items-center gap-4 mb-3">
<label className="inline-flex items-center gap-2">
<input type="checkbox" checked={showOnlyBlocked} onChange={e => setShowOnlyBlocked(e.target.checked)} />
<span className="text-sm">Vis kun blokerede</span>
</label>
<label className="inline-flex items-center gap-2">
<input type="checkbox" checked={multiSelectMode} onChange={e => {
const on = e.target.checked
setMultiSelectMode(on)
if (on) {
// initialize selectedDates with existing blocked dates
setSelectedDates(blocked.map(b => new Date(b.block_date)))
} else {
setSelectedDates(undefined)
<h2 className="text-xl font-semibold mb-2">Kalender klik for at skifte tilgængelighed</h2>
<p className="text-sm text-gray-600 mb-4">Klik en dato for at blokere/åbne den.</p>
<div className="flex flex-wrap gap-3 mb-4">
<label className="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={showOnlyBlocked} onChange={e => setShowOnlyBlocked(e.target.checked)} className="rounded" />
<span className="text-sm">Vis kun blokerede</span>
</label>
<label className="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={multiSelectMode} onChange={e => {
const on = e.target.checked
setMultiSelectMode(on)
if (on) {
setSelectedDates(blocked.map(b => new Date(b.block_date)))
} else {
setSelectedDates(undefined)
}
}} className="rounded" />
<span className="text-sm">Vælg flere til sletning</span>
</label>
{multiSelectMode && (
<button onClick={deleteSelectedBlocked} className="ml-auto px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 disabled:opacity-50 transition-colors" disabled={!selectedDates || selectedDates.length===0 || saving}>
Slet valgte ({selectedDates ? selectedDates.length : 0})
</button>
)}
{!multiSelectMode && pendingDates.length > 0 && (
<button
onClick={async () => {
setSaving(true)
const isos = pendingDates.map(pd => isoFromDate(pd))
setBlocked(prev => [...prev, ...isos.map(i => ({ id: `tmp-${i}`, block_date: i }))])
try {
await Promise.all(pendingDates.map(pd => fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: isoFromDate(pd), note: '' }) })))
addToast(`${pendingDates.length} dage blokeret`, 'success')
setPendingDates([])
} catch (err) {
addToast('Kunne ikke oprette alle blokeringer', 'error')
} finally {
fetchAll()
setSaving(false)
}
}} />
<span className="text-sm">Vælg flere blokerede datoer</span>
</label>
{multiSelectMode && (
<button onClick={deleteSelectedBlocked} className="ml-auto px-3 py-1 bg-red-500 text-white rounded" disabled={!selectedDates || selectedDates.length===0 || saving}>
Slet valgte ({selectedDates ? selectedDates.length : 0})
</button>
)}
{!multiSelectMode && pendingDates.length > 0 && (
<button
onClick={async () => {
setSaving(true)
const isos = pendingDates.map(pd => isoFromDate(pd))
setBlocked(prev => [...prev, ...isos.map(i => ({ id: `tmp-${i}`, block_date: i }))])
try {
await Promise.all(pendingDates.map(pd => fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: isoFromDate(pd), note: '' }) })))
addToast(`${pendingDates.length} dage blokeret`, 'success')
setPendingDates([])
} catch (err) {
addToast('Kunne ikke oprette alle blokeringer', 'error')
} finally {
fetchAll()
setSaving(false)
}
}}
className="ml-auto px-3 py-1 bg-blue-500 text-white rounded"
disabled={saving}
>
Bloker valgte ({pendingDates.length})
</button>
)}
</div>
}}
className="ml-auto px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 transition-colors"
disabled={saving}
>
Bloker valgte ({pendingDates.length})
</button>
)}
</div>
{multiSelectMode ? (
<div className="flex justify-center">{multiSelectMode ? (
<DayPicker
className="admin-calendar"
mode="multiple"
@@ -330,6 +330,10 @@ function AvailabilityAdminContent() {
blocked: blocked.map(b => new Date(b.block_date)),
nonWorking: (day: Date) => !weekly.some(w => w.weekday === day.getDay())
}}
modifiersClassNames={{
blocked: 'rdp-day_blocked',
nonWorking: 'rdp-day_nonWorking',
}}
disabled={showOnlyBlocked ? ((day: Date) => !blocked.some(b => sameDay(new Date(b.block_date), day))) : undefined}
/>
) : (
@@ -393,7 +397,9 @@ function AvailabilityAdminContent() {
pending: pendingDates,
}}
modifiersClassNames={{
blocked: 'rdp-day_blocked',
pending: 'rdp-day_pending',
nonWorking: 'rdp-day_nonWorking',
}}
disabled={showOnlyBlocked ? ((day: Date) => !blocked.some(b => sameDay(new Date(b.block_date), day))) : undefined}
/>
+492
View File
@@ -0,0 +1,492 @@
"use client"
import { useEffect, useState } from 'react'
import { DayPicker } from 'react-day-picker'
import { da } from 'date-fns/locale'
import { ToastProvider, useToast } from '@/components/ui/toast'
type Weekly = { id: string; weekday: number; start_time: string; end_time: string; note?: string }
type Blocked = { id: string; block_date: string; note?: string }
function BlockedList({ blocked, onDelete }: { blocked: Blocked[]; onDelete: (id: string) => void }) {
if (!blocked || blocked.length === 0) return <p>Ingen blokeringer</p>
const sorted = blocked.slice().sort((a, b) => a.block_date.localeCompare(b.block_date))
const fmt = (d: string) => {
try {
const dt = new Date(d)
return dt.toLocaleDateString('da-DK', { weekday: 'short', day: '2-digit', month: 'short', year: 'numeric' })
} catch (e) {
return d
}
}
return (
<ul className="space-y-2">
{sorted.map(b => (
<li key={b.id} className="flex justify-between items-start gap-4 border p-3 rounded">
<div>
<div className="font-medium">{fmt(b.block_date)}</div>
{b.note && <div className="text-sm text-gray-600">{b.note}</div>}
</div>
<div className="flex items-center gap-2">
<button onClick={() => onDelete(b.id)} className="text-red-600">Slet</button>
</div>
</li>
))}
</ul>
)
}
export default function AvailabilityAdmin() {
return (
<ToastProvider>
<AvailabilityAdminContent />
</ToastProvider>
)
}
function AvailabilityAdminContent() {
const { addToast } = useToast()
const [weekly, setWeekly] = useState<Weekly[]>([])
const [blocked, setBlocked] = useState<Blocked[]>([])
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
// form state
const [weekday, setWeekday] = useState(1)
const [startTime, setStartTime] = useState('09:00')
const [endTime, setEndTime] = useState('17:00')
const [note, setNote] = useState('')
const [editingId, setEditingId] = useState<string | null>(null)
const [blockDate, setBlockDate] = useState('')
const [blockNote, setBlockNote] = useState('')
const [multiSelectMode, setMultiSelectMode] = useState(false)
const [selectedDates, setSelectedDates] = useState<Date[] | undefined>(undefined)
const [showOnlyBlocked, setShowOnlyBlocked] = useState(false)
const [lastClickedDate, setLastClickedDate] = useState<Date | null>(null)
const [pendingDates, setPendingDates] = useState<Date[]>([]) // for Ctrl/Cmd+click accumulation
useEffect(() => { fetchAll() }, [])
async function fetchAll() {
setLoading(true)
const [wRes, bRes] = await Promise.all([
fetch('/api/availability/weekly').then(r => r.json()),
fetch('/api/availability/blocked').then(r => r.json())
])
setWeekly(wRes || [])
setBlocked(bRes || [])
setLoading(false)
}
async function addWeekly(e: React.FormEvent) {
e.preventDefault()
e.currentTarget && (e.currentTarget as HTMLFormElement).querySelectorAll('button')
setSaving(true)
try {
if (editingId) {
// send PUT to update
await fetch('/api/availability/weekly', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id: editingId, weekday, start_time: startTime, end_time: endTime, note }) })
addToast('Skema opdateret', 'success')
setEditingId(null)
} else {
await fetch('/api/availability/weekly', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ weekday, start_time: startTime, end_time: endTime, note }) })
addToast('Skema tilføjet', 'success')
}
setNote('')
fetchAll()
} catch (err) {
addToast('Kunne ikke gemme skema', 'error')
} finally {
setSaving(false)
}
}
async function deleteWeekly(id: string) {
if (!confirm('Slet dette skema?')) return
setSaving(true)
try {
await fetch('/api/availability/weekly', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id }) })
addToast('Skema slettet', 'success')
fetchAll()
} catch (err) {
addToast('Kunne ikke slette skema', 'error')
} finally {
setSaving(false)
}
}
function startEdit(w: Weekly) {
setEditingId(w.id)
setWeekday(w.weekday)
setStartTime(w.start_time)
setEndTime(w.end_time)
setNote(w.note || '')
window.scrollTo({ top: 0, behavior: 'smooth' })
}
async function addBlocked(e: React.FormEvent) {
e.preventDefault()
if (!blockDate) {
addToast('Vælg en dato', 'warning')
return
}
setSaving(true)
try {
await fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: blockDate, note: blockNote }) })
setBlockDate('')
setBlockNote('')
addToast('Dato blokeret', 'success')
fetchAll()
} catch (err) {
addToast('Kunne ikke blokere dato', 'error')
} finally {
setSaving(false)
}
}
// Toggle a single date's blocked state when clicked in the calendar
async function toggleBlockedDate(d: Date) {
const iso = isoFromDate(d)
const found = blocked.find(b => b.block_date === iso)
const weeklyForDay = weekly.find(w => w.weekday === d.getDay())
// If this date falls on a working day, confirm user intention to block full day
if (!found && weeklyForDay) {
const proceed = confirm(`Der er allerede et arbejdsskema for denne ugedag (${['Søndag','Mandag','Tirsdag','Onsdag','Torsdag','Fredag','Lørdag'][weeklyForDay.weekday]} ${weeklyForDay.start_time}${weeklyForDay.end_time}).\n\nVil du blokere hele dagen alligevel?`)
if (!proceed) return
}
if (found) {
// optimistic UI: remove locally first
setBlocked(prev => prev.filter(b => b.block_date !== iso))
try {
await fetch('/api/availability/blocked', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id: found.id }) })
addToast('Blokering fjernet', 'success')
} catch (err) {
// revert on failure
fetchAll()
addToast('Kunne ikke fjerne blokering — prøv igen', 'error')
return
}
} else {
// optimistic UI: add temporary entry so calendar turns red immediately
setBlocked(prev => [...prev, { id: `tmp-${iso}`, block_date: iso }])
try {
await fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: iso, note: '' }) })
addToast('Dato blokeret', 'success')
// refresh to pick up real id
fetchAll()
return
} catch (err) {
// revert on failure
setBlocked(prev => prev.filter(b => b.block_date !== iso))
addToast('Kunne ikke oprette blokering — prøv igen', 'error')
return
}
}
fetchAll()
}
// Bulk delete of selected blocked dates (multi-select mode)
async function deleteSelectedBlocked() {
if (!selectedDates || selectedDates.length === 0) return
if (!confirm(`Slet ${selectedDates.length} valgte blokering(er)?`)) return
setSaving(true)
const isos = selectedDates.map(d => {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${dd}`
})
// map isos to ids
const ids = blocked.filter(b => isos.includes(b.block_date)).map(b => b.id)
if (ids.length === 0) {
addToast('Ingen matcher blandt eksisterende blokeringer', 'warning')
setSaving(false)
return
}
// optimistic
setBlocked(prev => prev.filter(b => !isos.includes(b.block_date)))
try {
await Promise.all(ids.map(id => fetch('/api/availability/blocked', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id }) })))
setSelectedDates(undefined)
setMultiSelectMode(false)
addToast(`${ids.length} blokering(er) slettet`, 'success')
} catch (err) {
addToast('Noget gik galt ved sletning — opdaterer listen', 'error')
fetchAll()
} finally {
setSaving(false)
}
}
async function deleteBlocked(id: string) {
if (!confirm('Slet denne blokering?')) return
setSaving(true)
try {
await fetch('/api/availability/blocked', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id }) })
addToast('Blokering slettet', 'success')
fetchAll()
} catch (err) {
addToast('Kunne ikke slette blokering', 'error')
} finally {
setSaving(false)
}
}
function sameDay(a: Date, b: Date) {
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
}
function isoFromDate(d: Date) {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${dd}`
}
// Generate inclusive date range between a and b
function datesBetween(a: Date, b: Date) {
const start = new Date(a.getFullYear(), a.getMonth(), a.getDate())
const end = new Date(b.getFullYear(), b.getMonth(), b.getDate())
const out: Date[] = []
const step = start <= end ? 1 : -1
let cur = new Date(start)
while (true) {
out.push(new Date(cur))
if (cur.getFullYear() === end.getFullYear() && cur.getMonth() === end.getMonth() && cur.getDate() === end.getDate()) break
cur.setDate(cur.getDate() + step)
}
return out
}
return (
<div className="min-h-screen bg-primary-50 py-8">
<div className="container mx-auto px-4 max-w-6xl">
<h1 className="text-3xl font-bold mb-6">Tilgængelighed & blokeringer</h1>
{loading ? <p>Indlæser...</p> : (
<div className="space-y-6">
{/* Main calendar section */}
<section className="bg-white rounded-lg p-6">
<h2 className="text-xl font-semibold mb-2">Kalender — klik for at skifte tilgængelighed</h2>
<p className="text-sm text-gray-600 mb-4">Klik på en dato for at blokere/åbne den.</p>
<div className="flex flex-wrap gap-3 mb-4">
<label className="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={showOnlyBlocked} onChange={e => setShowOnlyBlocked(e.target.checked)} className="rounded" />
<span className="text-sm">Vis kun blokerede</span>
</label>
<label className="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={multiSelectMode} onChange={e => {
const on = e.target.checked
setMultiSelectMode(on)
if (on) {
setSelectedDates(blocked.map(b => new Date(b.block_date)))
} else {
setSelectedDates(undefined)
}
}} className="rounded" />
<span className="text-sm">Vælg flere til sletning</span>
</label>
{multiSelectMode && (
<button onClick={deleteSelectedBlocked} className="ml-auto px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 disabled:opacity-50 transition-colors" disabled={!selectedDates || selectedDates.length===0 || saving}>
Slet valgte ({selectedDates ? selectedDates.length : 0})
</button>
)}
{!multiSelectMode && pendingDates.length > 0 && (
<button
onClick={async () => {
setSaving(true)
const isos = pendingDates.map(pd => isoFromDate(pd))
setBlocked(prev => [...prev, ...isos.map(i => ({ id: `tmp-${i}`, block_date: i }))])
try {
await Promise.all(pendingDates.map(pd => fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: isoFromDate(pd), note: '' }) })))
addToast(`${pendingDates.length} dage blokeret`, 'success')
setPendingDates([])
} catch (err) {
addToast('Kunne ikke oprette alle blokeringer', 'error')
} finally {
fetchAll()
setSaving(false)
}
}}
className="ml-auto px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 transition-colors"
disabled={saving}
>
Bloker valgte ({pendingDates.length})
</button>
)}
</div>
<div className="flex justify-center">{multiSelectMode ? (
<DayPicker
className="admin-calendar"
mode="multiple"
selected={selectedDates}
onSelect={(d: any) => setSelectedDates(d as Date[] | undefined)}
locale={da}
modifiers={{
blocked: blocked.map(b => new Date(b.block_date)),
nonWorking: (day: Date) => !weekly.some(w => w.weekday === day.getDay())
}}
modifiersClassNames={{
blocked: 'rdp-day_blocked',
nonWorking: 'rdp-day_nonWorking',
}}
disabled={showOnlyBlocked ? ((day: Date) => !blocked.some(b => sameDay(new Date(b.block_date), day))) : undefined}
/>
) : (
<DayPicker
className="admin-calendar"
mode="single"
onDayClick={async (d: Date, modifiers: any, e: any) => {
if (!d) return
// Ctrl/Cmd+click: accumulate dates for batch blocking
if (e && (e.ctrlKey || e.metaKey)) {
const iso = isoFromDate(d)
const alreadyPending = pendingDates.some(pd => isoFromDate(pd) === iso)
if (alreadyPending) {
setPendingDates(prev => prev.filter(pd => isoFromDate(pd) !== iso))
} else {
setPendingDates(prev => [...prev, d])
}
setLastClickedDate(d)
return
}
// Shift+click range selection
if (e && e.shiftKey && lastClickedDate) {
setSaving(true)
const range = datesBetween(lastClickedDate, d)
// confirm if any date in range falls on a working weekly schedule
const hasWorking = range.some(rd => weekly.some(w => w.weekday === rd.getDay()))
if (hasWorking) {
const proceed = confirm('Nogle dage i det valgte interval har et arbejdsskema. Vil du blokere alle dagene alligevel?')
if (!proceed) {
setLastClickedDate(d)
setSaving(false)
return
}
}
// optimistic add
const isos = range.map(r => isoFromDate(r))
setBlocked(prev => [...prev, ...isos.map(i => ({ id: `tmp-${i}`, block_date: i }))])
try {
await Promise.all(range.map(r => fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: isoFromDate(r), note: '' }) })))
addToast(`${range.length} dage blokeret`, 'success')
} catch (err) {
addToast('Kunne ikke oprette alle blokeringer — opdaterer listen', 'error')
} finally {
fetchAll()
setSaving(false)
}
setLastClickedDate(d)
return
}
// normal single toggle
await toggleBlockedDate(d)
setLastClickedDate(d)
}}
locale={da}
modifiers={{
blocked: blocked.map(b => new Date(b.block_date)),
nonWorking: (day: Date) => !weekly.some(w => w.weekday === day.getDay()),
pending: pendingDates,
}}
modifiersClassNames={{
blocked: 'rdp-day_blocked',
pending: 'rdp-day_pending',
nonWorking: 'rdp-day_nonWorking',
}}
disabled={showOnlyBlocked ? ((day: Date) => !blocked.some(b => sameDay(new Date(b.block_date), day))) : undefined}
/>
)}
<div className="mt-3 text-sm flex items-center gap-4">
<span className="inline-flex items-center gap-2">
<span className="w-3 h-3 bg-green-400 rounded-sm inline-block" />
<span>Tilgængelig</span>
</span>
<span className="inline-flex items-center gap-2">
<span className="w-3 h-3 bg-red-500 rounded-sm inline-block" />
<span>Blokeret</span>
</span>
<span className="inline-flex items-center gap-2">
<span className="w-3 h-3 bg-amber-500 rounded-sm inline-block" />
<span>Valgt</span>
</span>
<span className="inline-flex items-center gap-2">
<span className="w-3 h-3 bg-blue-400 rounded-sm inline-block" />
<span>Venter (Ctrl+klik)</span>
</span>
</div>
{!multiSelectMode && (
<div className="mt-2 text-xs text-gray-600">
<p>Tips: Hold Shift og klik for at vælge interval. Hold Ctrl/⌘ og klik for at vælge flere enkeltdage.</p>
</div>
)}
</div>
</section>
<section className="bg-white rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">Ugentligt arbejdsskema</h2>
<form onSubmit={addWeekly} className="grid gap-3 mb-4">
<label>Ugedag
<select value={weekday} onChange={e => setWeekday(parseInt(e.target.value))} className="ml-2">
<option value={1}>Mandag</option>
<option value={2}>Tirsdag</option>
<option value={3}>Onsdag</option>
<option value={4}>Torsdag</option>
<option value={5}>Fredag</option>
<option value={6}>Lørdag</option>
<option value={0}>Søndag</option>
</select>
</label>
<div className="flex gap-2">
<label>Start
<input type="time" value={startTime} onChange={e => setStartTime(e.target.value)} className="ml-2" />
</label>
<label>Slut
<input type="time" value={endTime} onChange={e => setEndTime(e.target.value)} className="ml-2" />
</label>
</div>
<input value={note} onChange={e => setNote(e.target.value)} placeholder="Noter (valgfri)" className="border px-2 py-1 rounded" />
<button className="px-4 py-2 bg-accent text-white rounded disabled:opacity-50" disabled={saving}>
{saving ? 'Gemmer...' : (editingId ? 'Opdater' : 'Tilføj')}
</button>
</form>
<h3 className="font-semibold mb-2">Eksisterende skemaer</h3>
<ul className="space-y-2">
{weekly.map(w => (
<li key={w.id} className="flex justify-between items-center border p-2 rounded">
<div>{['Søndag','Mandag','Tirsdag','Onsdag','Torsdag','Fredag','Lørdag'][w.weekday]} {w.start_time}{w.end_time} {w.note ? `· ${w.note}` : ''}</div>
<div className="flex gap-2">
<button onClick={() => startEdit(w)} className="text-primary-600">Rediger</button>
<button onClick={() => deleteWeekly(w.id)} className="text-red-600">Slet</button>
</div>
</li>
))}
</ul>
</section>
<section className="bg-white rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">Blokerede dage</h2>
<form onSubmit={addBlocked} className="grid gap-3 mb-4">
<input type="date" value={blockDate} onChange={e => setBlockDate(e.target.value)} className="border px-2 py-1 rounded" />
<input value={blockNote} onChange={e => setBlockNote(e.target.value)} placeholder="Noter (fx ferie)" className="border px-2 py-1 rounded" />
<button className="px-4 py-2 bg-accent text-white rounded disabled:opacity-50" disabled={saving}>
{saving ? 'Gemmer...' : 'Bloker dato'}
</button>
</form>
<h3 className="font-semibold mb-2">Eksisterende blokeringer</h3>
<BlockedList blocked={blocked} onDelete={deleteBlocked} />
</section>
</div>
)}
</div>
</div>
)
}