commit
This commit is contained in:
+8
-2
@@ -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,7 +172,8 @@ 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">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
{event.status !== "confirmed" && event.status !== "cancelled" && (
|
{event.status !== "confirmed" && event.status !== "cancelled" && (
|
||||||
<button
|
<button
|
||||||
onClick={() => run("confirm", () => api.confirmEvent(event.id))}
|
onClick={() => run("confirm", () => api.confirmEvent(event.id))}
|
||||||
@@ -190,13 +192,51 @@ export function EventDetailDialog({
|
|||||||
<Ban className="h-4 w-4" /> Cancel
|
<Ban className="h-4 w-4" /> Cancel
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delete lives in its own fixed spot, separate from Confirm/Cancel,
|
||||||
|
so its position never shifts depending on which of those show. */}
|
||||||
|
<div className="border-t border-white/10 pt-3">
|
||||||
|
{!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
|
<button
|
||||||
onClick={() => run("delete", () => api.deleteEvent(event.id))}
|
onClick={() => run("delete", () => api.deleteEvent(event.id))}
|
||||||
disabled={busy !== null}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" /> Delete
|
{busy === "delete" ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Ja, verwijderen
|
||||||
</button>
|
</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 && (
|
||||||
|
<div className="border-t border-white/10 pt-3">
|
||||||
|
{!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 room
|
||||||
|
</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 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
|
<button
|
||||||
onClick={destroy}
|
onClick={destroy}
|
||||||
disabled={busy === "delete"}
|
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"
|
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" ? (
|
{busy === "delete" ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
Delete room
|
Ja, verwijderen
|
||||||
</button>
|
</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>
|
||||||
|
|||||||
@@ -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.
Reference in New Issue
Block a user