mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 14:52:09 +00:00
326 lines
7.7 KiB
Vue
326 lines
7.7 KiB
Vue
<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>
|