mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 14:52:09 +00:00
Add room participant moderation
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, useForm } from '@inertiajs/vue3';
|
||||
import { DoorOpen, Plus, Settings as SettingsIcon, Trash2 } from '@lucide/vue';
|
||||
import {
|
||||
DoorOpen,
|
||||
Plus,
|
||||
Settings as SettingsIcon,
|
||||
Trash2,
|
||||
UserMinus,
|
||||
} from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import DashboardRoomCard from '@/components/dashboard/DashboardRoomCard.vue';
|
||||
@@ -13,13 +19,23 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { create, destroy, reopen, show } from '@/routes/room';
|
||||
import { destroy as kickRoomPlayer } from '@/routes/room/player';
|
||||
import { edit as roomSettings } from '@/routes/room/settings';
|
||||
import type { DashboardRoom, OwnedRoomListProps } from '@/types/dashboard';
|
||||
import type {
|
||||
DashboardRoom,
|
||||
DashboardRoomPlayer,
|
||||
OwnedRoomListProps,
|
||||
} from '@/types/dashboard';
|
||||
|
||||
defineProps<OwnedRoomListProps>();
|
||||
|
||||
const selectedRoom = ref<DashboardRoom | null>(null);
|
||||
const selectedPlayer = ref<{
|
||||
room: DashboardRoom;
|
||||
player: DashboardRoomPlayer;
|
||||
} | null>(null);
|
||||
const deleteForm = useForm({});
|
||||
const kickForm = useForm({});
|
||||
|
||||
function setDeleteDialogOpen(open: boolean): void {
|
||||
if (!open && !deleteForm.processing) {
|
||||
@@ -27,6 +43,12 @@ function setDeleteDialogOpen(open: boolean): void {
|
||||
}
|
||||
}
|
||||
|
||||
function setKickDialogOpen(open: boolean): void {
|
||||
if (!open && !kickForm.processing) {
|
||||
selectedPlayer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteRoom(): void {
|
||||
if (selectedRoom.value === null || deleteForm.processing) {
|
||||
return;
|
||||
@@ -39,6 +61,25 @@ function deleteRoom(): void {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function kickPlayer(): void {
|
||||
if (selectedPlayer.value === null || kickForm.processing) {
|
||||
return;
|
||||
}
|
||||
|
||||
kickForm.delete(
|
||||
kickRoomPlayer({
|
||||
room: selectedPlayer.value.room.id,
|
||||
player: selectedPlayer.value.player.id,
|
||||
}).url,
|
||||
{
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
selectedPlayer.value = null;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -108,6 +149,56 @@ function deleteRoom(): void {
|
||||
<Trash2 class="size-4" />
|
||||
Raum löschen
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="mt-2 w-full rounded-md border border-slate-700 bg-slate-900/40 p-3"
|
||||
>
|
||||
<h3 class="text-sm font-semibold">Teilnehmer</h3>
|
||||
|
||||
<ul
|
||||
v-if="room.players.length > 0"
|
||||
class="mt-2 flex flex-col gap-2"
|
||||
>
|
||||
<li
|
||||
v-for="player in room.players"
|
||||
:key="player.id"
|
||||
class="flex items-center justify-between gap-3 rounded-md bg-slate-800 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.is_owner"
|
||||
class="shrink-0 text-xs text-slate-400"
|
||||
>
|
||||
Du
|
||||
</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 = { room, player }"
|
||||
>
|
||||
<UserMinus class="size-3.5" />
|
||||
Kicken
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p v-else class="mt-2 text-sm text-slate-400">
|
||||
Aktuell sind keine Teilnehmer im Raum.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</DashboardRoomCard>
|
||||
</div>
|
||||
@@ -173,5 +264,42 @@ function deleteRoom(): void {
|
||||
</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"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Teilnehmer wirklich kicken?</DialogTitle>
|
||||
<DialogDescription class="text-slate-300">
|
||||
{{ selectedPlayer?.player.name }} wird aus „{{
|
||||
selectedPlayer?.room.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>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { CircleUserRound, Crown } from '@lucide/vue';
|
||||
import { CircleUserRound } from '@lucide/vue';
|
||||
|
||||
import type { RoomSeatProps } from '@/types/room';
|
||||
|
||||
@@ -75,22 +75,18 @@ function playerVote(): string | null {
|
||||
:class="{ 'player-details-above': player.isTopSeat }"
|
||||
>
|
||||
<p
|
||||
class="player-name max-w-32 text-center text-sm leading-tight font-semibold text-slate-100"
|
||||
class="player-name max-w-32 text-center text-sm leading-tight font-semibold"
|
||||
:class="
|
||||
player.is_moderator ? 'text-amber-300' : 'text-slate-100'
|
||||
"
|
||||
>
|
||||
{{ player.name }}
|
||||
</p>
|
||||
<div
|
||||
v-if="player.is_moderator || isCurrentPlayer()"
|
||||
v-if="isCurrentPlayer()"
|
||||
class="flex flex-wrap items-center justify-center gap-x-2 gap-y-0.5 text-[11px]"
|
||||
>
|
||||
<span
|
||||
v-if="player.is_moderator"
|
||||
class="inline-flex items-center gap-1 text-amber-300"
|
||||
>
|
||||
<Crown class="size-3 fill-amber-300" aria-hidden="true" />
|
||||
Moderator
|
||||
</span>
|
||||
<span v-if="isCurrentPlayer()" class="text-cyan-300">Du</span>
|
||||
<span class="text-cyan-300">Du</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,6 +33,15 @@ export function useRoomRealtime(
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.change === 'player_kicked' &&
|
||||
payload.affected_player_id === currentPlayer.value.id
|
||||
) {
|
||||
router.visit(home().url, { replace: true });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.change === 'room_closed') {
|
||||
router.visit(
|
||||
join({ query: { join_code: room.value.join_code } }).url,
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export type DashboardRoomPlayer = {
|
||||
id: number;
|
||||
name: string;
|
||||
is_moderator: boolean;
|
||||
is_owner: boolean;
|
||||
};
|
||||
|
||||
export type DashboardRoom = {
|
||||
id: string;
|
||||
title: string | null;
|
||||
@@ -5,6 +12,7 @@ export type DashboardRoom = {
|
||||
current_round: number;
|
||||
players_count: number;
|
||||
active_player_count: number;
|
||||
players: DashboardRoomPlayer[];
|
||||
last_activity_at: string | null;
|
||||
closed_at: string | null;
|
||||
};
|
||||
|
||||
@@ -160,6 +160,7 @@ export type RoomVoteDeckEmits = {
|
||||
export type RoomStateChange =
|
||||
| 'player_joined'
|
||||
| 'player_updated'
|
||||
| 'player_kicked'
|
||||
| 'vote_changed'
|
||||
| 'cards_revealed'
|
||||
| 'votes_reset'
|
||||
@@ -173,6 +174,7 @@ export type RoomStateChangedPayload = {
|
||||
version: number;
|
||||
change: RoomStateChange;
|
||||
state: RoomRealtimeState | null;
|
||||
affected_player_id?: number;
|
||||
};
|
||||
|
||||
export type RoomRealtimeState = Pick<
|
||||
|
||||
Reference in New Issue
Block a user