Add presenter mode and app version display

This commit is contained in:
2026-08-24 23:50:06 +02:00
parent b516eef403
commit fc3c5777ef
13 changed files with 239 additions and 29 deletions
+102 -5
View File
@@ -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;