eS v1
All checks were successful
Deploy App / build (push) Successful in 2m14s
Deploy App / deploy (push) Successful in 14s

This commit is contained in:
valere
2025-10-16 00:42:38 +02:00
parent ce73155cfa
commit 3ad8cb8795
21 changed files with 752 additions and 332 deletions

View File

@@ -1,16 +1,13 @@
<template>
<article class="box box-scene z-10" ref="scene">
<div class="box-object" ref="box">
<div ref="domBox" class="box-object" :class="{ 'is-draggable': isDraggable }">
<div class="face front relative" ref="frontFace">
<img v-if="compilation.duration" class="cover absolute" :src="`/${compilation.id}/cover.jpg`" alt="">
<div class="size-full flex justify-center items-center text-7xl" v-else>
{{ compilation.description }}
</div>
<img v-if="box.duration" class="cover absolute" :src="`/${box.id}/cover.jpg`" alt="">
<div class="size-full flex-col justify-center items-center text-7xl" v-html="box.description" v-else />
</div>
<div class="face back flex flex-col flex-wrap items-start p-4 overflow-hidden" ref="backFace">
<li class="list-none text-xxs w-1/2 flex flex-row"
v-for="track in dataStore.getTracksByCompilationId(compilation.id).slice(0, -1)" :key="track.id"
:track="track">
v-for="track in dataStore.getTracksByboxId(box.id).slice(0, -1)" :key="track.id" :track="track">
<span class="" v-if="isNotManifesto">
{{ track.order }}.
</span>
@@ -24,9 +21,9 @@
<div class="face right" ref="rightFace" />
<div class="face left" ref="leftFace" />
<div class="face top" ref="topFace">
<template v-if="compilation.duration !== 0">
<template v-if="box.duration !== 0">
<img class="logo h-full p-1" src="/logo.svg" alt="">
<img class="absolute block h-1/2" style="left:5%;" :src="`/${compilation.id}/title.svg`" alt="">
<img class="absolute block h-1/2" style="left:5%;" :src="`/${box.id}/title.svg`" alt="">
</template>
<template v-else>
<span class="absolute block h-1/2 right-6">
@@ -34,7 +31,7 @@
</span>
<img class="logo h-full p-1" src="/favicon.svg" alt="">
<span class="absolute block h-1/2" style="left:5%;">
{{ compilation.name }}
{{ box.name }}
</span>
</template>
</div>
@@ -45,24 +42,23 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import type { Compilation, BoxState } from '~~/types/types'
import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue'
import type { Box, BoxState } from '~~/types/types'
import { useDataStore } from '~/store/data'
const props = withDefaults(
defineProps<{
compilation: Compilation
BoxState?: BoxState
}>(),
{ BoxState: 'list' }
)
const props = defineProps<{
box: Box
}>();
const { $isMobile } = useNuxtApp()
const dataStore = useDataStore()
const isDraggable = computed(() => !['list', 'hidden'].includes(BoxState.value()))
const isNotManifesto = computed(() => !props.compilation.id.startsWith('ES00'))
const isDraggable = computed(() => !['box-list', 'box-hidden'].includes(props.box.state))
const isNotManifesto = computed(() => !props.box.id.startsWith('ES00'))
// --- Réfs ---
const scene = ref<HTMLElement>()
const box = ref<HTMLElement>()
const domBox = ref<HTMLElement>()
const frontFace = ref<HTMLElement>()
const backFace = ref<HTMLElement>()
const rightFace = ref<HTMLElement>()
@@ -81,36 +77,36 @@ let lastPointer = { x: 0, y: 0, time: 0 }
let velocity = { x: 0, y: 0 }
let raf: number | null = null
const sensitivity = 0.3
const sensitivity = $isMobile ? 0.5 : 0.15
const friction = 0.95
const minVelocity = 0.02
const enableInertia = true
// --- Transformations ---
function applyTransform(duration = 0.5) {
if (!box.value) return
if (!domBox.value) return
rotateX.value = Math.round(rotateX.value)
rotateY.value = Math.round(rotateY.value)
rotateZ.value = Math.round(rotateZ.value)
box.value.style.transition = `transform ${duration}s ease`
box.value.style.transform = `rotateX(${rotateX.value}deg) rotateY(${rotateY.value}deg) rotateZ(${rotateZ.value}deg)`
domBox.value.style.transition = `transform ${duration}s ease`
domBox.value.style.transform = `rotateX(${rotateX.value}deg) rotateY(${rotateY.value}deg) rotateZ(${rotateZ.value}deg)`
}
// --- Gestion BoxState ---
function applyBoxState() {
switch (props.BoxState) {
case 'list':
switch (props.box.state) {
case 'box-list':
rotateX.value = 76
rotateY.value = 0
rotateZ.value = 150
break
case 'selected':
case 'box-selected':
rotateX.value = -20
rotateY.value = 20
rotateZ.value = 0
break
case 'hide':
case 'box-hidden':
rotateX.value = 76
rotateY.value = 0
rotateZ.value = 150
@@ -123,12 +119,12 @@ function applyBoxState() {
function applyColor() {
if (!frontFace.value || !backFace.value || !leftFace.value || !topFace.value || !bottomFace.value) return
frontFace.value.style.background = props.compilation.color2
backFace.value.style.background = `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`
leftFace.value.style.background = `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`
rightFace.value.style.background = `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`
topFace.value.style.background = `linear-gradient(to top, ${props.compilation.color2}, ${props.compilation.color2})`
bottomFace.value.style.background = props.compilation.color1
frontFace.value.style.background = props.box.color2
backFace.value.style.background = `linear-gradient(to top, ${props.box.color1}, ${props.box.color2})`
leftFace.value.style.background = `linear-gradient(to top, ${props.box.color1}, ${props.box.color2})`
rightFace.value.style.background = `linear-gradient(to top, ${props.box.color1}, ${props.box.color2})`
topFace.value.style.background = `linear-gradient(to top, ${props.box.color2}, ${props.box.color2})`
bottomFace.value.style.background = props.box.color1
}
// --- Inertie ---
@@ -157,7 +153,7 @@ let listenersAttached = false
const down = (ev: PointerEvent) => {
ev.preventDefault()
dragging = true
box.value?.setPointerCapture(ev.pointerId)
domBox.value?.setPointerCapture(ev.pointerId)
lastPointer = { x: ev.clientX, y: ev.clientY, time: performance.now() }
velocity = { x: 0, y: 0 }
if (raf) { cancelAnimationFrame(raf); raf = null }
@@ -185,36 +181,36 @@ const move = (ev: PointerEvent) => {
const end = (ev: PointerEvent) => {
if (!dragging) return
dragging = false
try { box.value?.releasePointerCapture(ev.pointerId) } catch { }
try { domBox.value?.releasePointerCapture(ev.pointerId) } catch { }
if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
if (!raf) raf = requestAnimationFrame(tickInertia)
}
}
function addListeners() {
if (!box.value || listenersAttached) return
box.value.addEventListener('pointerdown', down)
box.value.addEventListener('pointermove', move)
box.value.addEventListener('pointerup', end)
box.value.addEventListener('pointercancel', end)
box.value.addEventListener('pointerleave', end)
if (!domBox.value || listenersAttached) return
domBox.value.addEventListener('pointerdown', down)
domBox.value.addEventListener('pointermove', move)
domBox.value.addEventListener('pointerup', end)
domBox.value.addEventListener('pointercancel', end)
domBox.value.addEventListener('pointerleave', end)
listenersAttached = true
}
function removeListeners() {
if (!box.value || !listenersAttached) return
box.value.removeEventListener('pointerdown', down)
box.value.removeEventListener('pointermove', move)
box.value.removeEventListener('pointerup', end)
box.value.removeEventListener('pointercancel', end)
box.value.removeEventListener('pointerleave', end)
if (!domBox.value || !listenersAttached) return
domBox.value.removeEventListener('pointerdown', down)
domBox.value.removeEventListener('pointermove', move)
domBox.value.removeEventListener('pointerup', end)
domBox.value.removeEventListener('pointercancel', end)
domBox.value.removeEventListener('pointerleave', end)
listenersAttached = false
}
onMounted(() => {
applyColor()
applyBoxState()
if (isDraggable) addListeners()
if (isDraggable.value) addListeners()
})
onBeforeUnmount(() => {
@@ -223,9 +219,9 @@ onBeforeUnmount(() => {
})
// --- Watchers ---
watch(() => props.BoxState, () => applyBoxState())
watch(() => props.compilation, () => applyColor(), { deep: true })
watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners())
watch(() => props.box.state, () => applyBoxState())
watch(() => props.box, () => applyColor(), { deep: true })
watch(isDraggable, (enabled) => (enabled ? addListeners() : removeListeners()))
</script>
@@ -235,23 +231,29 @@ watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners
--height: calc(var(--size) * (100 / 3));
--width: calc(var(--size) * 50);
--depth: calc(var(--size) * 10);
transition: all .5s;
transition: height .5s ease, opacity .5s ease;
&.hide {
height: 0;
opacity: 0;
z-index: 0;
&.box-list {
height: calc(var(--size) * 20);
@apply hover:scale-105;
transition: all .5s ease;
will-change: transform;
}
&.list {
@apply hover:scale-105;
&.box-selected {
height: calc(var(--size) * 34);
}
&-scene {
height: calc(var(--size) * 20);
perspective: 1000px;
}
&.box-hidden {
height: 0;
opacity: 0;
z-index: 0;
}
&-object {
width: var(--width);
height: var(--height);
@@ -260,11 +262,11 @@ watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners
margin: auto;
user-select: none;
.list & {
.box-list & {
cursor: pointer;
}
.selected & {
.box-selected & {
cursor: grab;
}
@@ -339,5 +341,31 @@ watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners
height: 100%;
object-fit: cover;
}
/* Deck fade in/out purely in CSS */
.box-page {
opacity: 0;
transition: opacity .25s ease;
pointer-events: none;
}
&.box-selected .box-page {
opacity: 1;
pointer-events: auto;
}
/* for tabindex */
&:focus-visible {
outline: 0;
@apply scale-105 outline-none;
.face {
border: 4px solid rgba(0, 0, 0, 0.5);
}
}
:deep(.indice) {
@apply text-xl p-2 relative bg-black/50 rounded-full backdrop-blur-md;
}
}
</style>

74
app/components/boxes.vue Normal file
View File

@@ -0,0 +1,74 @@
<template>
<div class="flex flex-col-reverse mt-16">
<box v-for="(box, i) in dataStore.boxes.slice()" :key="box.id" :tabindex="dataStore.boxes.length - i" :box="box"
@click="onBoxClick(box)" class="text-center" :class="box.state" :id="box.id">
<button @click.stop="playSelectedBox(box)" v-if="box.state === 'box-selected'"
class="relative z-40 rounded-full size-24 bottom-1/2 text-4xl tex-bold backdrop-blur-sm bg-black/25">
{{ !playerStore.isPaused && playerStore.currentTrack?.boxId === box.id ? 'I I' : '▶' }}
</button>
<deck :box="box" class="box-page" v-if="box.state === 'box-selected'" @click.stop />
</box>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { Box } from '~~/types/types'
import { useDataStore } from '~/store/data'
import { usePlayerStore } from '~/store/player'
import { useUiStore } from '~/store/ui'
const dataStore = useDataStore()
const playerStore = usePlayerStore()
const uiStore = useUiStore()
function openBox(id: string) {
uiStore.selectBox(id)
// Scroll to the top smoothly
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
function onBoxClick(b: Box) {
if (b.state !== 'box-selected') {
openBox(b.id)
}
}
function playSelectedBox(b: Box) {
playerStore.playBox(b)
}
function KeyboardAction(e: KeyboardEvent) {
switch (e.key) {
case 'Escape':
uiStore.closeBox()
break;
case 'ArrowUp':
break;
case 'Enter':
if (document.activeElement?.id) {
openBox(document.activeElement.id)
}
break;
case 'ArrowDown':
break;
case 'ArrowLeft':
break;
case 'ArrowRight':
break;
default:
break;
}
}
onMounted(async () => {
const dataStore = await useDataStore()
await dataStore.loadData()
window.addEventListener('keydown', KeyboardAction)
})
</script>

View File

@@ -8,7 +8,7 @@
<div class="flex items-center justify-center size-7 absolute top-7 right-7" v-if="isPlaylistTrack">
<div class="suit text-7xl absolute"
:class="[isRedCard ? 'text-red-600' : 'text-slate-800', props.track.card?.suit]">
{{ props.track.card?.suit }}
<img :src="`/${props.track.card?.suit}.svg`" />
</div>
<div class="rank text-white font-bold absolute -mt-1">
{{ props.track.card?.rank }}
@@ -58,9 +58,9 @@ const props = withDefaults(defineProps<{ track: Track; isFaceUp?: boolean }>(),
isFaceUp: false
})
const playerStore = usePlayerStore()
const isManifesto = computed(() => props.track.compilationId.startsWith('ES00'))
const isManifesto = computed(() => props.track.boxId.startsWith('ES00'))
const isOrder = computed(() => props.track.order && !isManifesto)
const isPlaylistTrack = computed(() => props.track.compilationId.length === 6)
const isPlaylistTrack = computed(() => props.track.type === 'playlist')
const isRedCard = computed(() => props.track.card?.suit === '♥' || props.track.card?.suit === '♦')
const coverUrl = props.track.coverId.startsWith('http')
? props.track.coverId
@@ -111,20 +111,11 @@ const coverUrl = props.track.coverId.startsWith('http')
transform: rotateY(180deg);
}
. {
@apply -mt-2;
}
. {
@apply text-7xl -mt-2;
}
. {
@apply -mt-3;
}
.,
.,
.,
. {
@apply text-5xl
@apply text-5xl size-14;
}
}
</style>

View File

@@ -1,63 +0,0 @@
<template>
<div class="flex flex-col-reverse mt-16">
<box v-for="compilation in dataStore.getAllCompilations.slice()" :key="compilation.id" :compilation="compilation"
:BoxState="boxStates[compilation.id]" @click="() => openCompilation(compilation.id)"
:class="boxStates[compilation.id]" class="text-center">
<button @click="playerStore.playCompilation(compilation.id)" v-if="boxStates[compilation.id] === 'selected'"
class="relative z-40 rounded-full size-24 bottom-1/2 text-2xl">
{{ !playerStore.isPaused && playerStore.currentTrack?.compilationId === compilation.id ? 'II' : '▶' }}
</button>
<deck :compilation="compilation" class="box-page" v-if="boxStates[compilation.id] === 'selected'" />
</box>
</div>
</template>
<script setup lang="ts">
import { useDataStore } from '~/store/data'
import type { BoxState } from '~~/types/types'
import { usePlayerStore } from '~/store/player'
const dataStore = useDataStore()
const boxStates = ref<Record<string, BoxState>>({})
const playerStore = usePlayerStore()
function openCompilation(id: string) {
if (boxStates.value[id] === 'list') {
for (const key in boxStates.value) {
boxStates.value[key] = (key === id) ? 'selected' : 'hide'
}
// Scroll to the top smoothly
window.scrollTo({
top: 0,
behavior: 'smooth'
});
// navigateTo(`/box/${id}`)
}
}
function closeCompilation(e: KeyboardEvent) {
if (e.key === 'Escape') {
for (const key in boxStates.value) {
boxStates.value[key] = 'list'
}
}
}
onMounted(async () => {
const dataStore = await useDataStore()
await dataStore.loadData()
dataStore.getAllCompilations.forEach(c => {
if (!(c.id in boxStates.value)) boxStates.value[c.id] = 'hide'
})
window.addEventListener('keydown', closeCompilation)
setTimeout(() => {
dataStore.getAllCompilations.forEach(c => {
boxStates.value[c.id] = 'list'
})
}, 333)
})
</script>

View File

@@ -6,21 +6,22 @@
<button @click="setDisplay('holdem')">holdem</button>
</div>
<div ref="deck" class="deck flex flex-wrap justify-center gap-4">
<card v-for="track in tracks" :key="track.id" :track="track" />
<card v-for="(track, i) in tracks" :key="track.id" :track="track" tabindex="i" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useDataStore } from '~/store/data'
import type { Compilation } from '~~/types/types'
import type { Box } from '~~/types/types'
const props = defineProps<{
compilation: Compilation
box: Box
}>()
const dataStore = useDataStore()
const deck = ref()
const tracks = props.compilation.duration ? dataStore.getTracksByCompilationId(props.compilation.id) : dataStore.getPlaylistTracksByCompilationId(props.compilation.id)
const tracks = computed(() => dataStore.getTracksByboxId(props.box.id))
function setDisplay(displayMode) {
deck.value.classList.remove('pile', 'plateau', 'holdem')

View File

@@ -1,7 +1,11 @@
<template>
<div class="fixed left-0 bottom-0 opacity-1 z-50 w-full bg-white transition-all"
:class="{ '-bottom-20 opacity-0': !playerStore.currentTrack }">
<audio ref="audioRef" class="w-full" :src="playerStore.currentTrack?.url || ''" controls />
<div class="flex items-center gap-3 p-2">
<img v-if="playerStore.getCurrentCoverUrl" :src="playerStore.getCurrentCoverUrl as string" alt="Current cover"
class="size-16 object-cover object-center rounded" />
<audio ref="audioRef" class="flex-1" controls />
</div>
</div>
</template>
@@ -14,7 +18,7 @@ const audioRef = ref<HTMLAudioElement | null>(null)
onMounted(() => {
if (audioRef.value) {
playerStore.audio = audioRef.value
playerStore.attachAudio(audioRef.value)
audioRef.value.addEventListener("timeupdate", playerStore.updateTime)
}
})