mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 17:12:08 +00:00
268 lines
7.8 KiB
Vue
268 lines
7.8 KiB
Vue
<script setup lang="ts">
|
||
import { useMediaQuery } from '@vueuse/core';
|
||
import { computed, ref } from 'vue';
|
||
|
||
import RoomModeratorControls from '@/components/rooms/RoomModeratorControls.vue';
|
||
import RoomSeat from '@/components/rooms/RoomSeat.vue';
|
||
import { usePaperBallToss } from '@/composables/usePaperBallToss';
|
||
import type {
|
||
PositionedRoomPlayer,
|
||
RoomTableProps,
|
||
SeatStyle,
|
||
} 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 } =
|
||
usePaperBallToss({
|
||
roomId: () => props.room.id,
|
||
currentPlayerId: () => props.currentPlayer.id,
|
||
currentPlayerIsModerator: () => props.currentPlayer.is_moderator,
|
||
board: gameBoard,
|
||
});
|
||
|
||
const positionedPlayers = computed<PositionedRoomPlayer[]>(() => {
|
||
const currentPlayerIndex = props.room.players.findIndex(
|
||
(player) => player.id === props.currentPlayer.id,
|
||
);
|
||
const orderedPlayers =
|
||
currentPlayerIndex === -1
|
||
? props.room.players
|
||
: [
|
||
...props.room.players.slice(currentPlayerIndex),
|
||
...props.room.players.slice(0, currentPlayerIndex),
|
||
];
|
||
const playerCount = Math.max(orderedPlayers.length, 1);
|
||
|
||
return orderedPlayers.map((player, index) => {
|
||
const angle = Math.PI / 2 + (index * Math.PI * 2) / playerCount;
|
||
const horizontalRadius = 39;
|
||
const verticalRadius = hasCompactHeight.value ? 34 : 40;
|
||
const cardDistance = hasCompactHeight.value ? 72 : 96;
|
||
|
||
return {
|
||
...player,
|
||
isTopSeat: Math.sin(angle) < -0.25,
|
||
seatStyle: {
|
||
left: `${50 + Math.cos(angle) * horizontalRadius}%`,
|
||
top: `${48 + Math.sin(angle) * verticalRadius}%`,
|
||
'--card-x': `${-Math.cos(angle) * cardDistance}px`,
|
||
'--card-y': `${-Math.sin(angle) * cardDistance}px`,
|
||
} as SeatStyle,
|
||
};
|
||
});
|
||
});
|
||
|
||
const voteCount = computed(
|
||
() => props.room.players.filter((player) => player.has_voted).length,
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<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>
|
||
<div class="table-center">
|
||
<span
|
||
v-if="!room.revealed"
|
||
class="text-sm font-medium text-emerald-50/75"
|
||
>
|
||
{{ voteCount }} von {{ room.players.length }} haben
|
||
gewählt
|
||
</span>
|
||
<div v-else class="grid grid-cols-2 gap-8 text-emerald-50">
|
||
<div class="flex flex-col gap-1">
|
||
<span class="text-xs text-emerald-100/65">
|
||
Durchschnitt
|
||
</span>
|
||
<strong class="text-xl">
|
||
{{ room.statistics?.average ?? '–' }}
|
||
</strong>
|
||
</div>
|
||
<div class="flex flex-col gap-1">
|
||
<span class="text-xs text-emerald-100/65">
|
||
Median
|
||
</span>
|
||
<strong class="text-xl">
|
||
{{ room.statistics?.median ?? '–' }}
|
||
</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-for="paperBall in activePaperBalls"
|
||
:key="paperBall.id"
|
||
class="paper-ball-flight"
|
||
:style="paperBall.style"
|
||
aria-hidden="true"
|
||
>
|
||
<span class="paper-ball"></span>
|
||
</div>
|
||
|
||
<RoomSeat
|
||
v-for="player in positionedPlayers"
|
||
:key="player.id"
|
||
:player="player"
|
||
:current-player-id="currentPlayer.id"
|
||
: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="
|
||
!currentPlayer.is_moderator && throwingAtPlayerId !== null
|
||
"
|
||
@throw-paper-ball="throwPaperBall"
|
||
/>
|
||
|
||
<RoomModeratorControls
|
||
v-if="currentPlayer.is_moderator"
|
||
:room-id="room.id"
|
||
:revealed="room.revealed"
|
||
:has-votes="voteCount > 0"
|
||
/>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.game-board {
|
||
position: relative;
|
||
width: 100%;
|
||
min-height: max(34rem, calc(100svh - 11rem));
|
||
isolation: isolate;
|
||
}
|
||
|
||
@media (min-width: 768px) {
|
||
.game-board {
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
}
|
||
|
||
.poker-table {
|
||
position: absolute;
|
||
inset: 20% 15% 21%;
|
||
overflow: hidden;
|
||
border: 0.65rem solid #513b2d;
|
||
border-radius: 50%;
|
||
background:
|
||
radial-gradient(
|
||
circle at 50% 42%,
|
||
rgb(16 126 91 / 38%),
|
||
transparent 45%
|
||
),
|
||
linear-gradient(145deg, #136c54, #074c3d);
|
||
box-shadow:
|
||
inset 0 0 0 0.2rem rgb(255 255 255 / 8%),
|
||
inset 0 -1.5rem 3rem rgb(0 0 0 / 22%),
|
||
0 1.5rem 3rem rgb(0 0 0 / 35%);
|
||
}
|
||
|
||
.table-highlight {
|
||
position: absolute;
|
||
inset: 6%;
|
||
border: 1px solid rgb(255 255 255 / 8%);
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.table-center {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: grid;
|
||
place-items: center;
|
||
padding: 2rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.paper-ball-flight {
|
||
position: absolute;
|
||
z-index: 40;
|
||
width: 1.65rem;
|
||
height: 1.65rem;
|
||
pointer-events: none;
|
||
will-change: transform;
|
||
animation: paper-ball-flight 780ms linear forwards;
|
||
}
|
||
|
||
.paper-ball {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
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;
|
||
}
|
||
|
||
@keyframes paper-ball-flight {
|
||
0% {
|
||
transform: translate(-50%, -50%) translate(0, 0) rotate(0deg)
|
||
scale(0.72);
|
||
}
|
||
|
||
25% {
|
||
transform: translate(-50%, -50%)
|
||
translate(var(--travel-quarter-x), var(--travel-quarter-y))
|
||
rotate(195deg) scale(0.92);
|
||
}
|
||
|
||
50% {
|
||
transform: translate(-50%, -50%)
|
||
translate(var(--travel-mid-x), var(--travel-mid-y)) rotate(390deg)
|
||
scale(1.08);
|
||
}
|
||
|
||
75% {
|
||
transform: translate(-50%, -50%)
|
||
translate(
|
||
var(--travel-three-quarter-x),
|
||
var(--travel-three-quarter-y)
|
||
)
|
||
rotate(585deg) scale(1);
|
||
}
|
||
|
||
100% {
|
||
transform: translate(-50%, -50%)
|
||
translate(var(--travel-x), var(--travel-y)) rotate(780deg)
|
||
scale(0.82);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.game-board {
|
||
min-height: max(28rem, calc(100svh - 10rem));
|
||
}
|
||
|
||
.poker-table {
|
||
inset: 24% 17% 23%;
|
||
border-width: 0.45rem;
|
||
}
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.paper-ball-flight {
|
||
animation-duration: 1ms;
|
||
}
|
||
}
|
||
</style>
|