This commit is contained in:
ben
2026-08-19 12:10:02 +02:00
parent bfab69cbb4
commit 8fae1ffb3d
5 changed files with 127 additions and 49 deletions
+8 -2
View File
@@ -339,8 +339,14 @@ def api_update_member_role(
data: MemberRoleUpdate, data: MemberRoleUpdate,
current_user: int = Depends(get_current_user), current_user: int = Depends(get_current_user),
): ):
current_role = get_member_role(room_id, current_user) role_levels = {
target_role = get_member_role(room_id, user_id) "member": 1,
"admin": 2,
"owner": 3,
}
current_role = role_levels.get(get_role(room_id, current_user), 0)
target_role = role_levels.get(get_role(room_id, user_id), 0)
if current_role is None: if current_role is None:
raise HTTPException( raise HTTPException(
@@ -46,6 +46,7 @@ export function EventDetailDialog({
}: EventDetailDialogProps) { }: EventDetailDialogProps) {
const [busy, setBusy] = useState<string | null>(null) const [busy, setBusy] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [deleteConfirming, setDeleteConfirming] = useState(false)
const { data: responses, mutate: mutateResponses } = useSWR( const { data: responses, mutate: mutateResponses } = useSWR(
["responses", event.id], ["responses", event.id],
@@ -171,32 +172,71 @@ export function EventDetailDialog({
{/* Management actions */} {/* Management actions */}
{canManage && ( {canManage && (
<div className="flex flex-wrap gap-2 border-t border-white/15 pt-4"> <div className="flex flex-col gap-3 border-t border-white/15 pt-4">
{event.status !== "confirmed" && event.status !== "cancelled" && ( <div className="flex flex-wrap gap-2">
<button {event.status !== "confirmed" && event.status !== "cancelled" && (
onClick={() => run("confirm", () => api.confirmEvent(event.id))} <button
disabled={busy !== null} onClick={() => run("confirm", () => api.confirmEvent(event.id))}
className="flex items-center gap-1.5 rounded-xl bg-emerald-500/80 px-3 py-2 text-sm font-medium transition-colors hover:bg-emerald-500 disabled:opacity-70" disabled={busy !== null}
> className="flex items-center gap-1.5 rounded-xl bg-emerald-500/80 px-3 py-2 text-sm font-medium transition-colors hover:bg-emerald-500 disabled:opacity-70"
<CheckCircle2 className="h-4 w-4" /> Confirm >
</button> <CheckCircle2 className="h-4 w-4" /> Confirm
)} </button>
{event.status !== "cancelled" && ( )}
<button {event.status !== "cancelled" && (
onClick={() => run("cancel", () => api.cancelEvent(event.id))} <button
disabled={busy !== null} onClick={() => run("cancel", () => api.cancelEvent(event.id))}
className="flex items-center gap-1.5 rounded-xl bg-white/10 px-3 py-2 text-sm font-medium transition-colors hover:bg-white/20 disabled:opacity-70" disabled={busy !== null}
> className="flex items-center gap-1.5 rounded-xl bg-white/10 px-3 py-2 text-sm font-medium transition-colors hover:bg-white/20 disabled:opacity-70"
<Ban className="h-4 w-4" /> Cancel >
</button> <Ban className="h-4 w-4" /> Cancel
)} </button>
<button )}
onClick={() => run("delete", () => api.deleteEvent(event.id))} </div>
disabled={busy !== null}
className="ml-auto flex items-center gap-1.5 rounded-xl bg-red-500/80 px-3 py-2 text-sm font-medium transition-colors hover:bg-red-500 disabled:opacity-70" {/* Delete lives in its own fixed spot, separate from Confirm/Cancel,
> so its position never shifts depending on which of those show. */}
<Trash2 className="h-4 w-4" /> Delete <div className="border-t border-white/10 pt-3">
</button> {!deleteConfirming ? (
<button
onClick={() => setDeleteConfirming(true)}
disabled={busy !== null}
className="flex items-center gap-1.5 rounded-xl border border-red-400/30 bg-red-500/10 px-3 py-2 text-sm font-medium text-red-200 transition-colors hover:bg-red-500/20 disabled:opacity-70"
>
<Trash2 className="h-4 w-4" /> Delete event
</button>
) : (
<div className="rounded-xl border border-red-400/40 bg-red-500/10 p-3">
<p className="mb-3 text-sm text-white/90">
Weet je zeker dat je dit evenement wilt verwijderen? Dit kan niet ongedaan
worden gemaakt.
</p>
<div className="flex flex-col gap-2">
{/* Destructive action sits on top, off the "usual" spot. */}
<button
onClick={() => run("delete", () => api.deleteEvent(event.id))}
disabled={busy !== null}
className="flex items-center justify-center gap-1.5 rounded-xl bg-red-500 px-3 py-2.5 text-sm font-medium transition-colors hover:bg-red-600 disabled:opacity-70"
>
{busy === "delete" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
Ja, verwijderen
</button>
{/* Safe action sits below, in the spot people click fast out of habit. */}
<button
onClick={() => setDeleteConfirming(false)}
disabled={busy !== null}
className="rounded-xl border border-white/20 bg-white/10 px-3 py-2.5 text-sm font-medium text-white transition-colors hover:bg-white/20 disabled:opacity-70"
>
OK, annuleren
</button>
</div>
</div>
)}
</div>
</div> </div>
)} )}
</div> </div>
@@ -43,6 +43,7 @@ export function RoomSettingsDialog({
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [busy, setBusy] = useState<string | null>(null) const [busy, setBusy] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [deleteConfirming, setDeleteConfirming] = useState(false)
const isOwner = room.role === "owner" const isOwner = room.role === "owner"
const isAdmin = room.role === "admin" || isOwner const isAdmin = room.role === "admin" || isOwner
@@ -241,28 +242,58 @@ export function RoomSettingsDialog({
)} )}
{/* Danger zone */} {/* Danger zone */}
<div className="flex flex-wrap gap-2 border-t border-white/15 pt-4"> <div className="flex flex-col gap-3 border-t border-white/15 pt-4">
<button <button
onClick={leave} onClick={leave}
disabled={busy === "leave"} disabled={busy === "leave"}
className="flex items-center gap-1.5 rounded-xl bg-white/10 px-3 py-2 text-sm font-medium transition-colors hover:bg-white/20 disabled:opacity-70" className="flex items-center gap-1.5 self-start rounded-xl bg-white/10 px-3 py-2 text-sm font-medium transition-colors hover:bg-white/20 disabled:opacity-70"
> >
{busy === "leave" ? <Loader2 className="h-4 w-4 animate-spin" /> : <LogOut className="h-4 w-4" />} {busy === "leave" ? <Loader2 className="h-4 w-4 animate-spin" /> : <LogOut className="h-4 w-4" />}
Leave room Leave room
</button> </button>
{isOwner && ( {isOwner && (
<button <div className="border-t border-white/10 pt-3">
onClick={destroy} {!deleteConfirming ? (
disabled={busy === "delete"} <button
className="ml-auto flex items-center gap-1.5 rounded-xl bg-red-500/80 px-3 py-2 text-sm font-medium transition-colors hover:bg-red-500 disabled:opacity-70" onClick={() => setDeleteConfirming(true)}
> disabled={busy !== null}
{busy === "delete" ? ( className="flex items-center gap-1.5 rounded-xl border border-red-400/30 bg-red-500/10 px-3 py-2 text-sm font-medium text-red-200 transition-colors hover:bg-red-500/20 disabled:opacity-70"
<Loader2 className="h-4 w-4 animate-spin" /> >
<Trash2 className="h-4 w-4" /> Delete room
</button>
) : ( ) : (
<Trash2 className="h-4 w-4" /> <div className="rounded-xl border border-red-400/40 bg-red-500/10 p-3">
<p className="mb-3 text-sm text-white/90">
Weet je zeker dat je deze room wilt verwijderen? Dit kan niet ongedaan worden
gemaakt.
</p>
<div className="flex flex-col gap-2">
{/* Destructive action sits on top, off the "usual" spot. */}
<button
onClick={destroy}
disabled={busy !== null}
className="flex items-center justify-center gap-1.5 rounded-xl bg-red-500 px-3 py-2.5 text-sm font-medium transition-colors hover:bg-red-600 disabled:opacity-70"
>
{busy === "delete" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
Ja, verwijderen
</button>
{/* Safe action sits below, in the spot people click fast out of habit. */}
<button
onClick={() => setDeleteConfirming(false)}
disabled={busy !== null}
className="rounded-xl border border-white/20 bg-white/10 px-3 py-2.5 text-sm font-medium text-white transition-colors hover:bg-white/20 disabled:opacity-70"
>
OK, annuleren
</button>
</div>
</div>
)} )}
Delete room </div>
</button>
)} )}
</div> </div>
</div> </div>
@@ -42,9 +42,10 @@ export function getRoomColor(roomId: number | null | undefined) {
// ---------- Event coloring by type / status / RSVP ---------- // ---------- Event coloring by type / status / RSVP ----------
// //
// Confirmed and busy events each pick a color from a small themed pool, // Confirmed and busy events each pick a color from a small themed pool,
// deterministically by room so the same room always looks the same. // deterministically by event id — so color varies event-to-event rather
// Private, proposed, declined and cancelled get one fixed look each so // than clustering by room, but a given event still looks the same on
// they're recognizable at a glance regardless of room. // every render/refresh. Private, proposed, declined and cancelled get
// one fixed look each so they're recognizable at a glance.
export interface EventStyle { export interface EventStyle {
bg: string bg: string
@@ -73,9 +74,9 @@ const BUSY_PALETTE = [
function pickFromPalette( function pickFromPalette(
palette: { bg: string; ring: string }[], palette: { bg: string; ring: string }[],
roomId: number | null | undefined, seed: number | null | undefined,
) { ) {
const id = typeof roomId === "number" && Number.isFinite(roomId) ? roomId : 0 const id = typeof seed === "number" && Number.isFinite(seed) ? seed : 0
const idx = ((id % palette.length) + palette.length) % palette.length const idx = ((id % palette.length) + palette.length) % palette.length
return palette[idx] return palette[idx]
} }
@@ -98,17 +99,17 @@ export const STRIPE_STYLE: CSSProperties = {
} }
export function getEventStyle( export function getEventStyle(
event: Pick<CalendarEvent, "type" | "status" | "room_id">, event: Pick<CalendarEvent, "id" | "type" | "status" | "room_id">,
myResponse: SignupStatus | undefined, myResponse: SignupStatus | undefined,
): EventStyle { ): EventStyle {
if (event.type === "private") return PRIVATE_STYLE if (event.type === "private") return PRIVATE_STYLE
if (event.type === "busy") return pickFromPalette(BUSY_PALETTE, event.room_id) if (event.type === "busy") return pickFromPalette(BUSY_PALETTE, event.id)
// Proposal-type events: colored by where they stand. // Proposal-type events: colored by where they stand.
if (event.status === "cancelled") return CANCELLED_STYLE if (event.status === "cancelled") return CANCELLED_STYLE
if (myResponse === "declined") return DECLINED_STYLE if (myResponse === "declined") return DECLINED_STYLE
if (event.status === "proposed" || event.status === "draft") return PROPOSED_STYLE if (event.status === "proposed" || event.status === "draft") return PROPOSED_STYLE
// Confirmed proposal: pick from the confirmed palette, by room. // Confirmed proposal: pick from the confirmed palette, per event.
return pickFromPalette(CONFIRMED_PALETTE, event.room_id) return pickFromPalette(CONFIRMED_PALETTE, event.id)
} }
Binary file not shown.