mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 16:32:09 +00:00
Add presenter mode and app version display
This commit is contained in:
@@ -3,10 +3,12 @@ import { Link } from '@inertiajs/vue3';
|
||||
import { Check, Copy } from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { favorite } from '@/routes/room';
|
||||
import type { RoomHeaderProps } from '@/types/room';
|
||||
import type { RoomHeaderEmits, RoomHeaderProps } from '@/types/room';
|
||||
|
||||
const props = defineProps<RoomHeaderProps>();
|
||||
const emit = defineEmits<RoomHeaderEmits>();
|
||||
|
||||
const copied = ref(false);
|
||||
|
||||
@@ -45,21 +47,39 @@ async function copyInviteLink(): Promise<void> {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
@click="copyInviteLink"
|
||||
>
|
||||
<span>
|
||||
<span class="block text-sm tracking-[0.18em] text-slate-400">
|
||||
Raumcode
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<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>
|
||||
<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>
|
||||
<Check v-if="copied" class="size-5 text-emerald-300" />
|
||||
<Copy v-else class="size-5 text-slate-300" />
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -106,4 +106,24 @@ function resetVotes(): void {
|
||||
left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-height: 800px) {
|
||||
.moderator-controls,
|
||||
.reset-controls {
|
||||
bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.moderator-controls {
|
||||
right: 0.75rem;
|
||||
}
|
||||
|
||||
.reset-controls {
|
||||
left: 0.75rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,7 +39,7 @@ function playerVote(): string | null {
|
||||
:aria-label="`${player.name} hat eine Karte gewählt`"
|
||||
>
|
||||
<div
|
||||
v-if="isCurrentPlayer()"
|
||||
v-if="isCurrentPlayer() && !presenterMode"
|
||||
class="own-vote-card"
|
||||
:class="{ 'is-revealed': revealed }"
|
||||
>
|
||||
@@ -313,6 +313,27 @@ function playerVote(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-height: 800px) {
|
||||
.player-seat {
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
.player-details {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.seat-card {
|
||||
top: 1.5rem;
|
||||
width: 2.75rem;
|
||||
height: 3.8rem;
|
||||
}
|
||||
|
||||
.own-vote-card,
|
||||
.flip-card-face {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.flip-card-inner {
|
||||
transition: none;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import RoomModeratorControls from '@/components/rooms/RoomModeratorControls.vue';
|
||||
@@ -11,6 +12,9 @@ import type {
|
||||
} from '@/types/room';
|
||||
|
||||
const props = defineProps<RoomTableProps>();
|
||||
const hasCompactHeight = useMediaQuery(
|
||||
'(min-width: 768px) and (max-height: 800px)',
|
||||
);
|
||||
|
||||
const gameBoard = ref<HTMLElement | null>(null);
|
||||
const { activePaperBalls, hitPlayerIds, throwingAtPlayerId, throwPaperBall } =
|
||||
@@ -36,8 +40,8 @@ const positionedPlayers = computed<PositionedRoomPlayer[]>(() => {
|
||||
return orderedPlayers.map((player, index) => {
|
||||
const angle = Math.PI / 2 + (index * Math.PI * 2) / playerCount;
|
||||
const horizontalRadius = 39;
|
||||
const verticalRadius = 40;
|
||||
const cardDistance = 96;
|
||||
const verticalRadius = hasCompactHeight.value ? 34 : 40;
|
||||
const cardDistance = hasCompactHeight.value ? 72 : 96;
|
||||
|
||||
return {
|
||||
...player,
|
||||
@@ -58,7 +62,7 @@ const voteCount = computed(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="mt-5 w-full">
|
||||
<section class="mt-5 w-full md:mt-3 md:min-h-0 md:flex-1">
|
||||
<div ref="gameBoard" class="game-board" aria-label="Scrum-Poker-Tisch">
|
||||
<div class="poker-table" aria-hidden="true">
|
||||
<div class="table-highlight"></div>
|
||||
@@ -109,6 +113,7 @@ const voteCount = computed(
|
||||
:current-vote="currentPlayer.current_vote"
|
||||
:cards="room.deck.cards"
|
||||
:revealed="room.revealed"
|
||||
:presenter-mode="presenterMode"
|
||||
:is-paper-ball-hit="hitPlayerIds.includes(player.id)"
|
||||
:throw-disabled="throwingAtPlayerId !== null"
|
||||
@throw-paper-ball="throwPaperBall"
|
||||
@@ -132,6 +137,13 @@ const voteCount = computed(
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.game-board {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.poker-table {
|
||||
position: absolute;
|
||||
inset: 20% 15% 21%;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Keyboard } from '@lucide/vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { store as storeVote } from '@/routes/room/vote';
|
||||
@@ -7,6 +9,22 @@ import type { RoomVoteDeckProps } from '@/types/room';
|
||||
|
||||
const props = defineProps<RoomVoteDeckProps>();
|
||||
|
||||
const cardShortcuts = [
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'0',
|
||||
...Array.from({ length: 24 }, (_, index) =>
|
||||
String.fromCharCode('a'.charCodeAt(0) + index),
|
||||
),
|
||||
];
|
||||
|
||||
const voteForm = useForm({
|
||||
value: props.currentVote ?? '',
|
||||
});
|
||||
@@ -33,10 +51,52 @@ function submitVote(value: string): void {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function shortcutForCard(index: number): string | null {
|
||||
return cardShortcuts[index] ?? null;
|
||||
}
|
||||
|
||||
function isEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
target.isContentEditable ||
|
||||
['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName)
|
||||
);
|
||||
}
|
||||
|
||||
function handleKeyboardVote(event: KeyboardEvent): void {
|
||||
if (
|
||||
!props.presenterMode ||
|
||||
props.revealed ||
|
||||
voteForm.processing ||
|
||||
event.repeat ||
|
||||
event.altKey ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
isEditableTarget(event.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cardIndex = cardShortcuts.indexOf(event.key.toLowerCase());
|
||||
const card = props.cards[cardIndex];
|
||||
|
||||
if (card === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
submitVote(card.key);
|
||||
}
|
||||
|
||||
useEventListener('keydown', handleKeyboardVote);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="mx-auto mt-6 max-w-4xl text-center">
|
||||
<section class="mx-auto mt-6 max-w-4xl text-center md:mt-3 md:shrink-0">
|
||||
<div class="mb-3 flex items-center justify-center gap-3">
|
||||
<span class="h-px w-10 bg-white/10"></span>
|
||||
<h2
|
||||
@@ -52,17 +112,28 @@ function submitVote(value: string): void {
|
||||
class="flex flex-wrap justify-center gap-2.5 sm:gap-3"
|
||||
>
|
||||
<button
|
||||
v-for="card in cards"
|
||||
v-for="(card, index) in cards"
|
||||
:key="card.key"
|
||||
type="button"
|
||||
class="deck-card"
|
||||
:class="{ 'is-selected': voteForm.value === card.key }"
|
||||
:class="{
|
||||
'is-selected':
|
||||
!presenterMode && voteForm.value === card.key,
|
||||
}"
|
||||
:disabled="voteForm.processing"
|
||||
:aria-pressed="voteForm.value === card.key"
|
||||
:aria-label="`Karte ${card.label} wählen`"
|
||||
@click="submitVote(card.key)"
|
||||
>
|
||||
<span>{{ card.label }}</span>
|
||||
<span class="card-label">{{ card.label }}</span>
|
||||
<span
|
||||
v-if="presenterMode && shortcutForCard(index)"
|
||||
class="card-shortcut"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Keyboard class="size-3" />
|
||||
<kbd>{{ shortcutForCard(index) }}</kbd>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -78,6 +149,7 @@ function submitVote(value: string): void {
|
||||
|
||||
<style scoped>
|
||||
.deck-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 3.75rem;
|
||||
aspect-ratio: 3 / 4;
|
||||
@@ -94,11 +166,30 @@ function submitVote(value: string): void {
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.deck-card span {
|
||||
.deck-card > .card-label {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.card-shortcut {
|
||||
position: absolute;
|
||||
bottom: 0.25rem;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
color: #64748b;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.card-shortcut kbd {
|
||||
font: inherit;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.deck-card:hover:not(:disabled) {
|
||||
transform: translateY(-0.35rem);
|
||||
border-color: rgb(103 232 249 / 80%);
|
||||
@@ -122,6 +213,12 @@ function submitVote(value: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-height: 800px) {
|
||||
.deck-card {
|
||||
width: 3.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.deck-card {
|
||||
transition: none;
|
||||
|
||||
Reference in New Issue
Block a user