mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 16:32:09 +00:00
feat: added settings in room to kick
This commit is contained in:
@@ -1,17 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
import { Check, Copy, LogOut } from '@lucide/vue';
|
||||
import { Link, useForm } from '@inertiajs/vue3';
|
||||
import { Check, Copy, LogOut, Settings, UserMinus, Users } from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { logout as embeddedLogout } from '@/routes/embedded';
|
||||
import { favorite } from '@/routes/room';
|
||||
import type { RoomHeaderEmits, RoomHeaderProps } from '@/types/room';
|
||||
import { destroy as kickRoomPlayer } from '@/routes/room/player';
|
||||
import type {
|
||||
RoomHeaderEmits,
|
||||
RoomHeaderProps,
|
||||
RoomPlayer,
|
||||
} from '@/types/room';
|
||||
|
||||
const props = defineProps<RoomHeaderProps>();
|
||||
const emit = defineEmits<RoomHeaderEmits>();
|
||||
|
||||
const copied = ref(false);
|
||||
const participantsDialogOpen = ref(false);
|
||||
const selectedPlayer = ref<RoomPlayer | null>(null);
|
||||
const kickForm = useForm({});
|
||||
|
||||
function kickPlayer(): void {
|
||||
if (selectedPlayer.value === null || kickForm.processing) {
|
||||
return;
|
||||
}
|
||||
|
||||
kickForm.delete(
|
||||
kickRoomPlayer({
|
||||
room: props.id,
|
||||
player: selectedPlayer.value.id,
|
||||
}).url,
|
||||
{
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
selectedPlayer.value = null;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function setKickDialogOpen(open: boolean): void {
|
||||
if (!open && !kickForm.processing) {
|
||||
selectedPlayer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyInviteLink(): Promise<void> {
|
||||
const inviteUrl = new URL('/room/join', window.location.origin);
|
||||
@@ -26,79 +74,225 @@ async function copyInviteLink(): Promise<void> {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="mx-auto flex w-full max-w-6xl flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold sm:text-3xl">
|
||||
{{ title ?? 'Scrum Poker' }}
|
||||
</h1>
|
||||
<div class="contents">
|
||||
<header
|
||||
class="mx-auto flex w-full max-w-6xl flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold sm:text-3xl">
|
||||
{{ title ?? 'Scrum Poker' }}
|
||||
</h1>
|
||||
|
||||
<Link
|
||||
v-if="canFavorite"
|
||||
:href="favorite(id)"
|
||||
method="post"
|
||||
as="button"
|
||||
class="mt-2 inline-flex items-center gap-2"
|
||||
>
|
||||
<span>Favorit</span>
|
||||
<span class="material-icons" aria-hidden="true">
|
||||
{{ is_favorite ? 'favorite' : 'favorite_border' }}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<Link
|
||||
v-if="embedded"
|
||||
:href="
|
||||
embeddedLogout({
|
||||
query: {
|
||||
join_code,
|
||||
player_name: playerName,
|
||||
},
|
||||
})
|
||||
"
|
||||
method="post"
|
||||
as="button"
|
||||
class="inline-flex items-center gap-2 text-sm text-slate-300 transition hover:text-white"
|
||||
>
|
||||
<LogOut class="size-4" />
|
||||
Abmelden
|
||||
</Link>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-fit items-center gap-3 rounded-xl border border-slate-700 bg-slate-800 px-4 py-2.5 text-left transition hover:bg-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-300"
|
||||
@click="copyInviteLink"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="block text-sm tracking-[0.18em] text-slate-400"
|
||||
>
|
||||
Raumcode
|
||||
<Link
|
||||
v-if="canFavorite"
|
||||
:href="favorite(id)"
|
||||
method="post"
|
||||
as="button"
|
||||
class="mt-2 inline-flex items-center gap-2"
|
||||
>
|
||||
<span>Favorit</span>
|
||||
<span class="material-icons" aria-hidden="true">
|
||||
{{ is_favorite ? 'favorite' : 'favorite_border' }}
|
||||
</span>
|
||||
<strong class="font-mono text-lg tracking-[0.2em]">
|
||||
{{ join_code }}
|
||||
</strong>
|
||||
</span>
|
||||
<Check v-if="copied" class="size-5 text-emerald-300" />
|
||||
<Copy v-else class="size-5 text-slate-300" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<label
|
||||
for="presenter-mode"
|
||||
class="flex cursor-pointer items-center gap-2 text-sm text-slate-300"
|
||||
>
|
||||
<Checkbox
|
||||
id="presenter-mode"
|
||||
:model-value="presenterMode"
|
||||
@update:model-value="
|
||||
emit('update:presenterMode', $event === true)
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<Link
|
||||
v-if="embedded"
|
||||
:href="
|
||||
embeddedLogout({
|
||||
query: {
|
||||
join_code,
|
||||
player_name: playerName,
|
||||
},
|
||||
})
|
||||
"
|
||||
/>
|
||||
<span>Präsentatormodus</span>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
method="post"
|
||||
as="button"
|
||||
class="inline-flex items-center gap-2 text-sm text-slate-300 transition hover:text-white"
|
||||
>
|
||||
<LogOut class="size-4" />
|
||||
Abmelden
|
||||
</Link>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<DropdownMenu v-if="canManageParticipants">
|
||||
<DropdownMenuTrigger :as-child="true">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex size-11 items-center justify-center rounded-xl border border-slate-700 bg-slate-800 text-slate-300 transition hover:bg-slate-700 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-300"
|
||||
aria-label="Raumeinstellungen öffnen"
|
||||
>
|
||||
<Settings class="size-5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
class="w-52 border-slate-700 bg-slate-800 text-slate-100"
|
||||
>
|
||||
<DropdownMenuLabel class="text-slate-400">
|
||||
Einstellungen
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
class="focus:bg-slate-700 focus:text-white"
|
||||
@select="participantsDialogOpen = true"
|
||||
>
|
||||
<Users class="size-4" />
|
||||
Teilnehmer
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-fit items-center gap-3 rounded-xl border border-slate-700 bg-slate-800 px-4 py-2.5 text-left transition hover:bg-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-300"
|
||||
@click="copyInviteLink"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="block text-sm tracking-[0.18em] text-slate-400"
|
||||
>
|
||||
Raumcode
|
||||
</span>
|
||||
<strong class="font-mono text-lg tracking-[0.2em]">
|
||||
{{ join_code }}
|
||||
</strong>
|
||||
</span>
|
||||
<Check v-if="copied" class="size-5 text-emerald-300" />
|
||||
<Copy v-else class="size-5 text-slate-300" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label
|
||||
for="presenter-mode"
|
||||
class="flex cursor-pointer items-center gap-2 text-sm text-slate-300"
|
||||
>
|
||||
<Checkbox
|
||||
id="presenter-mode"
|
||||
:model-value="presenterMode"
|
||||
@update:model-value="
|
||||
emit('update:presenterMode', $event === true)
|
||||
"
|
||||
/>
|
||||
<span>Präsentatormodus</span>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Dialog v-model:open="participantsDialogOpen">
|
||||
<DialogContent
|
||||
class="max-h-[calc(100dvh-2rem)] overflow-hidden border-slate-700 bg-slate-800 text-slate-100 sm:max-w-xl"
|
||||
overlay-class="bg-black/70 backdrop-blur-sm"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Teilnehmer</DialogTitle>
|
||||
<DialogDescription class="text-slate-300">
|
||||
Verwalte die aktiven Teilnehmer in diesem Raum.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ul
|
||||
v-if="players.length > 0"
|
||||
class="flex max-h-[min(60dvh,32rem)] flex-col gap-2 overflow-y-auto pr-1"
|
||||
>
|
||||
<li
|
||||
v-for="player in players"
|
||||
:key="player.id"
|
||||
class="flex items-center justify-between gap-3 rounded-md bg-slate-900/60 px-3 py-2"
|
||||
>
|
||||
<span class="min-w-0">
|
||||
<span
|
||||
class="block truncate text-sm"
|
||||
:class="
|
||||
player.is_moderator
|
||||
? 'text-amber-300'
|
||||
: 'text-slate-100'
|
||||
"
|
||||
>
|
||||
{{ player.name }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="
|
||||
player.id === currentPlayerId ||
|
||||
player.is_moderator
|
||||
"
|
||||
class="shrink-0 text-xs text-slate-400"
|
||||
>
|
||||
{{
|
||||
player.id === currentPlayerId
|
||||
? player.is_moderator
|
||||
? 'Du · Admin'
|
||||
: 'Du'
|
||||
: 'Admin'
|
||||
}}
|
||||
</span>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="inline-flex shrink-0 items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-semibold text-rose-300 transition hover:bg-rose-500/20 hover:text-rose-200"
|
||||
@click="selectedPlayer = player"
|
||||
>
|
||||
<UserMinus class="size-3.5" />
|
||||
Kicken
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p v-else class="text-sm text-slate-400">
|
||||
Aktuell sind keine Teilnehmer im Raum.
|
||||
</p>
|
||||
|
||||
<DialogFooter>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-slate-600 px-4 py-2 text-sm font-semibold text-slate-200 transition hover:bg-slate-700"
|
||||
@click="participantsDialogOpen = false"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
:open="selectedPlayer !== null"
|
||||
@update:open="setKickDialogOpen"
|
||||
>
|
||||
<DialogContent
|
||||
class="border-slate-700 bg-slate-800 text-slate-100 sm:max-w-md"
|
||||
overlay-class="bg-black/70 backdrop-blur-sm"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Teilnehmer wirklich kicken?</DialogTitle>
|
||||
<DialogDescription class="text-slate-300">
|
||||
{{ selectedPlayer?.name }} wird aus „{{
|
||||
title ?? 'Scrum Poker'
|
||||
}}“ entfernt und zur Startseite weitergeleitet.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter class="gap-6 space-x-4 sm:gap-0">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-slate-600 px-4 py-2 text-sm font-semibold text-slate-200 transition hover:bg-slate-700"
|
||||
:disabled="kickForm.processing"
|
||||
@click="selectedPlayer = null"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-rose-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-rose-500 disabled:opacity-50"
|
||||
:disabled="kickForm.processing"
|
||||
@click="kickPlayer"
|
||||
>
|
||||
{{ kickForm.processing ? 'Wird entfernt …' : 'Kicken' }}
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user