Files
scrum-poker-internal/resources/js/components/rooms/RoomSeat.vue
T
2026-08-20 23:00:47 +02:00

326 lines
7.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { CircleUserRound, Crown } from '@lucide/vue';
import type { RoomSeatProps } from '@/types/room';
const props = defineProps<RoomSeatProps>();
const emit = defineEmits<{
throwPaperBall: [playerId: number];
}>();
function cardLabel(value: string | null): string {
if (value === null) {
return '–';
}
return props.cards.find((card) => card.key === value)?.label ?? value;
}
function isCurrentPlayer(): boolean {
return props.player.id === props.currentPlayerId;
}
function playerVote(): string | null {
return isCurrentPlayer() ? props.currentVote : props.player.vote;
}
</script>
<template>
<article
class="player-seat"
:class="{ 'is-paper-ball-hit': isPaperBallHit }"
:style="player.seatStyle"
:data-player-id="player.id"
>
<div
v-if="player.has_voted"
class="seat-card"
:aria-label="`${player.name} hat eine Karte gewählt`"
>
<div
v-if="isCurrentPlayer()"
class="own-vote-card"
:class="{ 'is-revealed': revealed }"
>
{{ cardLabel(playerVote()) }}
</div>
<div v-else class="flip-card" :class="{ 'is-revealed': revealed }">
<div class="flip-card-inner">
<div class="flip-card-face flip-card-back">
<span>SP</span>
</div>
<div class="flip-card-face flip-card-front">
{{ cardLabel(playerVote()) }}
</div>
</div>
</div>
</div>
<div
class="mx-auto grid size-12 place-items-center rounded-full border border-slate-600 bg-slate-800 text-slate-300 sm:size-14"
:data-player-avatar="player.id"
>
<CircleUserRound
class="size-7 sm:size-8"
:stroke-width="1.7"
aria-hidden="true"
/>
<span class="sr-only">Profilbild von {{ player.name }}</span>
</div>
<div
class="player-details mt-2 flex flex-col items-center gap-1"
:class="{ 'player-details-above': player.isTopSeat }"
>
<p
class="player-name max-w-32 text-center text-sm leading-tight font-semibold text-slate-100"
>
{{ player.name }}
</p>
<div
v-if="player.is_moderator || 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>
</div>
</div>
<button
v-if="!isCurrentPlayer()"
type="button"
class="throw-paper-ball"
:disabled="throwDisabled"
:aria-label="`Papierknäuel auf ${player.name} werfen`"
:title="`Papierknäuel auf ${player.name} werfen`"
@click.stop="emit('throwPaperBall', player.id)"
>
<span class="paper-ball-mini" aria-hidden="true"></span>
</button>
</article>
</template>
<style scoped>
.player-seat {
position: absolute;
z-index: 2;
width: 8rem;
transform: translate(-50%, -50%);
text-align: center;
}
.player-name {
overflow-wrap: anywhere;
}
.player-details-above {
transform: translateY(calc(-100% - 4.5rem));
}
.throw-paper-ball {
position: absolute;
top: 0.65rem;
right: 0.35rem;
z-index: 8;
display: grid;
width: 2rem;
height: 2rem;
place-items: center;
border: 1px solid #64748b;
border-radius: 0.5rem;
background: #334155;
opacity: 0;
transform: translateY(0.25rem) rotate(-8deg);
pointer-events: none;
transition:
opacity 140ms ease,
transform 140ms ease,
background-color 140ms ease;
}
.player-seat:hover .throw-paper-ball,
.player-seat:focus-within .throw-paper-ball {
opacity: 1;
transform: translateY(0) rotate(-8deg);
pointer-events: auto;
}
.throw-paper-ball:hover:not(:disabled),
.throw-paper-ball:focus-visible {
background: #475569;
transform: translateY(-0.1rem) rotate(4deg);
}
.throw-paper-ball:focus-visible {
outline: 2px solid #67e8f9;
outline-offset: 2px;
}
.throw-paper-ball:disabled {
cursor: not-allowed;
opacity: 0;
pointer-events: none;
}
.paper-ball-mini {
display: block;
width: 0.95rem;
height: 0.95rem;
border: 1px solid rgb(100 116 139 / 65%);
border-radius: 48% 42% 54% 39%;
background:
linear-gradient(
35deg,
transparent 44%,
rgb(100 116 139 / 35%) 46% 51%,
transparent 53%
),
linear-gradient(
120deg,
transparent 35%,
rgb(148 163 184 / 45%) 37% 42%,
transparent 44%
),
#e2e8f0;
transform: rotate(18deg);
}
.player-seat.is-paper-ball-hit [data-player-avatar] {
animation: paper-ball-hit 380ms ease-out;
}
@keyframes paper-ball-hit {
0%,
100% {
transform: translateX(0) rotate(0deg);
}
28% {
transform: translateX(-0.35rem) rotate(-7deg);
}
62% {
transform: translateX(0.25rem) rotate(5deg);
}
}
.seat-card {
position: absolute;
top: 1.75rem;
left: 50%;
width: 3.15rem;
height: 4.35rem;
transform: translate(-50%, -50%) translate(var(--card-x), var(--card-y));
perspective: 800px;
}
.own-vote-card,
.flip-card-face {
display: grid;
place-items: center;
width: 100%;
height: 100%;
border-radius: 0.55rem;
font-size: 1.15rem;
font-weight: 800;
}
.own-vote-card {
border: 2px solid #cbd5e1;
background: #f8fafc;
color: #0f172a;
}
.own-vote-card.is-revealed {
border-color: rgb(252 211 77 / 90%);
background: #fef3c7;
}
.flip-card,
.flip-card-inner {
width: 100%;
height: 100%;
}
.flip-card-inner {
position: relative;
transform-style: preserve-3d;
transition: transform 520ms cubic-bezier(0.2, 0.75, 0.25, 1);
}
.flip-card.is-revealed .flip-card-inner {
transform: rotateY(180deg);
}
.flip-card-face {
position: absolute;
inset: 0;
backface-visibility: hidden;
}
.flip-card-back {
border: 2px solid rgb(103 232 249 / 70%);
background:
linear-gradient(
135deg,
transparent 42%,
rgb(103 232 249 / 20%) 42% 48%,
transparent 48% 92%,
rgb(103 232 249 / 20%) 92%
),
#17324a;
background-size: 1rem 1rem;
color: rgb(207 250 254 / 78%);
font-size: 0.7rem;
letter-spacing: 0.1em;
}
.flip-card-front {
transform: rotateY(180deg);
border: 2px solid rgb(252 211 77 / 90%);
background: #fffbeb;
color: #0f172a;
}
@media (max-width: 640px) {
.player-seat {
width: 6.5rem;
}
.player-details-above {
transform: translateY(calc(-100% - 4rem));
}
.seat-card {
top: 1.5rem;
width: 2.65rem;
height: 3.65rem;
transform: translate(-50%, -50%) translate(var(--card-x), var(--card-y))
scale(0.86);
}
.own-vote-card,
.flip-card-face {
font-size: 0.95rem;
}
}
@media (prefers-reduced-motion: reduce) {
.flip-card-inner {
transition: none;
}
.player-seat.is-paper-ball-hit [data-player-avatar] {
animation: none;
}
}
</style>