This commit is contained in:
@@ -1,327 +0,0 @@
|
||||
<template>
|
||||
<!-- scène 3D -->
|
||||
<div ref="scene" class="scene z-10">
|
||||
<div ref="box" class="box">
|
||||
<div class="face front relative" ref="frontFace">
|
||||
<img class="cover absolute" :src="`/${compilation.id}/cover.jpg`" alt="">
|
||||
</div>
|
||||
<div class="face back" ref="backFace" />
|
||||
<div class="face right" ref="rightFace" />
|
||||
<div class="face left" ref="leftFace" />
|
||||
<div class="face top" ref="topFace">
|
||||
<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="">
|
||||
</div>
|
||||
<div class="face bottom" ref="bottomFace" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Compilation, BoxPosition } from '~~/types/types';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
compilation: Compilation
|
||||
position?: BoxPosition
|
||||
size?: number
|
||||
}>(),
|
||||
{
|
||||
position: () => ({ x: 0, y: 0, z: 0 }),
|
||||
size: 6,
|
||||
}
|
||||
)
|
||||
|
||||
// States
|
||||
|
||||
const angleX = ref(props.position.x)
|
||||
const angleY = ref(props.position.y)
|
||||
const angleZ = ref(props.position.z)
|
||||
|
||||
const frontFace = ref()
|
||||
const backFace = ref()
|
||||
const rightFace = ref()
|
||||
const leftFace = ref()
|
||||
const topFace = ref()
|
||||
const bottomFace = ref()
|
||||
|
||||
const box = ref()
|
||||
const scene = ref()
|
||||
|
||||
/*
|
||||
ÉTATS POUR LE DRAG + INERTIE
|
||||
*/
|
||||
let dragging = false
|
||||
let lastPointer = { x: 0, y: 0, time: 0 } // position précédente du pointeur
|
||||
let velocity = { x: 0, y: 0 } // vitesse calculée pour inertie
|
||||
let raf = null // id du requestAnimationFrame
|
||||
|
||||
/*
|
||||
PARAMÈTRES DE RÉGLAGE
|
||||
*/
|
||||
const sensitivity = 0.3 // combien de degrés par pixel de mouvement
|
||||
const friction = 0.95 // inertie : 1 = sans perte, plus bas = ralentit vite
|
||||
const minVelocity = 0.02 // seuil sous lequel on arrête l’inertie
|
||||
const enableInertia = true // true = inertie activée, false = rotation immédiate sans suite
|
||||
|
||||
/*
|
||||
Applique la transformation CSS à la box
|
||||
*/
|
||||
function applyTransform() {
|
||||
angleX.value = Math.round(angleX.value)
|
||||
angleY.value = Math.round(angleY.value)
|
||||
angleZ.value = Math.round(angleZ.value)
|
||||
|
||||
box.value.style.transform = `rotateX(${angleX.value}deg) rotateY(${angleY.value}deg) rotateZ(${angleZ.value}deg)`
|
||||
}
|
||||
|
||||
function applySize() {
|
||||
updateCssVar('--height', `${props.size * (100 / 3)}px`, scene.value)
|
||||
updateCssVar('--width', `${props.size * 50}px`, scene.value)
|
||||
updateCssVar('--depth', `${props.size * 10}px`, scene.value)
|
||||
}
|
||||
function applyColor() {
|
||||
frontFace.value.style.setProperty('background', `${props.compilation.color2}`)
|
||||
backFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`)
|
||||
rightFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`)
|
||||
leftFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`)
|
||||
topFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.color2}, ${props.compilation.color2})`)
|
||||
bottomFace.value.style.setProperty('background', `${props.compilation.color1}`)
|
||||
}
|
||||
|
||||
/*
|
||||
Fonction utilitaire : place la box directement à une rotation donnée
|
||||
*/
|
||||
function applyRotation() {
|
||||
angleX.value = props.position.x
|
||||
angleY.value = props.position.y
|
||||
angleZ.value = props.position.z
|
||||
applyTransform()
|
||||
|
||||
box.value.style.setProperty('transition', 'transform 800ms ease-in-out')
|
||||
setTimeout(() => {
|
||||
box.value.style.setProperty('transition', 'transform 120ms linear')
|
||||
}, 120)
|
||||
}
|
||||
|
||||
/*
|
||||
Boucle d’inertie après un drag
|
||||
- reprend la vitesse calculée à la release
|
||||
- diminue petit à petit avec friction
|
||||
- stoppe quand vitesse < minVelocity
|
||||
*/
|
||||
function tickInertia() {
|
||||
if (!enableInertia) return
|
||||
|
||||
// appliquer la friction
|
||||
velocity.x *= friction
|
||||
velocity.y *= friction
|
||||
|
||||
// appliquer au box
|
||||
angleX.value += velocity.y
|
||||
angleY.value += velocity.x
|
||||
|
||||
// clamp angleX pour éviter de retourner la box trop loin
|
||||
angleX.value = Math.max(-80, Math.min(80, angleX.value))
|
||||
|
||||
applyTransform()
|
||||
|
||||
// continuer tant qu’il reste du mouvement
|
||||
if (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity) {
|
||||
raf = requestAnimationFrame(tickInertia)
|
||||
} else {
|
||||
raf = null
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Mise en place des listeners une fois le composant monté
|
||||
*/
|
||||
onMounted(() => {
|
||||
applySize()
|
||||
applyColor()
|
||||
applyTransform()
|
||||
|
||||
// pointerdown = début du drag
|
||||
const down = (ev) => {
|
||||
ev.preventDefault()
|
||||
dragging = true
|
||||
scene.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 }
|
||||
}
|
||||
|
||||
// pointermove = on bouge la souris ou le doigt
|
||||
const move = (ev) => {
|
||||
if (!dragging) return
|
||||
ev.preventDefault()
|
||||
const now = performance.now()
|
||||
const dx = ev.clientX - lastPointer.x
|
||||
const dy = ev.clientY - lastPointer.y
|
||||
|
||||
// mise à jour des angles
|
||||
angleY.value += dx * sensitivity
|
||||
angleX.value -= dy * sensitivity
|
||||
angleX.value = Math.max(-80, Math.min(80, angleX.value))
|
||||
|
||||
// calcul vitesse pour inertie
|
||||
const dt = Math.max(1, now - lastPointer.time)
|
||||
velocity.x = (dx / dt) * 16 * sensitivity
|
||||
velocity.y = (-dy / dt) * 16 * sensitivity
|
||||
|
||||
lastPointer = { x: ev.clientX, y: ev.clientY, time: now }
|
||||
|
||||
applyTransform()
|
||||
}
|
||||
|
||||
// pointerup = fin du drag
|
||||
const end = (ev) => {
|
||||
if (!dragging) return
|
||||
dragging = false
|
||||
try { scene.value.releasePointerCapture(ev.pointerId) } catch { }
|
||||
if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
|
||||
if (!raf) raf = requestAnimationFrame(tickInertia)
|
||||
}
|
||||
}
|
||||
|
||||
// attach des events
|
||||
scene.value.addEventListener('pointerdown', down)
|
||||
scene.value.addEventListener('pointermove', move)
|
||||
scene.value.addEventListener('pointerup', end)
|
||||
scene.value.addEventListener('pointercancel', end)
|
||||
scene.value.addEventListener('pointerleave', end)
|
||||
|
||||
// cleanup au démontage
|
||||
onBeforeUnmount(() => {
|
||||
cancelAnimationFrame(raf)
|
||||
})
|
||||
})
|
||||
|
||||
watch(() => props.position, () => {
|
||||
applyRotation()
|
||||
}, { deep: true })
|
||||
|
||||
watch(() => props.compilation, () => {
|
||||
applyColor()
|
||||
}, { deep: true })
|
||||
|
||||
watch(() => props.size, () => {
|
||||
applySize()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* scène avec perspective */
|
||||
.scene {
|
||||
height: var(--height);
|
||||
width: var(--width);
|
||||
perspective: 1000px;
|
||||
touch-action: none;
|
||||
/* essentiel pour empêcher le scroll pendant le drag */
|
||||
/* height: 20px; */
|
||||
transition: all .5s;
|
||||
}
|
||||
|
||||
/* l'objet 3D (box simple) */
|
||||
.box {
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 120ms linear;
|
||||
transition: height 120ms linear;
|
||||
/* légère smoothing quand on lâche */
|
||||
margin: auto;
|
||||
user-select: none;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.box:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* faces du box */
|
||||
.face {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
backface-visibility: hidden;
|
||||
/* border: 2px solid rgba(255, 255, 255, 0.06); */
|
||||
box-sizing: border-box;
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.front,
|
||||
.back {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.face.top,
|
||||
.face.bottom {
|
||||
width: var(--width);
|
||||
height: var(--depth);
|
||||
}
|
||||
|
||||
.face.left,
|
||||
.face.right {
|
||||
width: var(--depth);
|
||||
height: var(--height);
|
||||
}
|
||||
|
||||
.face.front {
|
||||
transform: translateX(0px) translateY(0px) translateZ(0px);
|
||||
}
|
||||
|
||||
.face.back {
|
||||
transform: rotateY(180deg) translateX(var(--width)) translateY(0px) translateZ(var(--depth));
|
||||
}
|
||||
|
||||
.face.right {
|
||||
transform: rotateY(90deg) translateX(0px) translateY(0px) translateZ(var(--width));
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.face.left {
|
||||
transform: rotateY(-90deg) translateX(0) translateY(0px) translateZ(var(--depth));
|
||||
}
|
||||
|
||||
.face.top {
|
||||
transform: rotateX(90deg) translateX(0px) translateY(calc(var(--depth) * -1)) translateZ(0px);
|
||||
}
|
||||
|
||||
.face.top>* {
|
||||
@apply rotate-180;
|
||||
}
|
||||
|
||||
.face.bottom {
|
||||
transform: rotateX(-90deg) translateX(0) translateY(0px) translateZ(calc(var(--height)));
|
||||
}
|
||||
|
||||
.cover {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.logo {
|
||||
filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8));
|
||||
}
|
||||
</style>
|
||||
296
app/components/molecule/box.vue
Normal file
296
app/components/molecule/box.vue
Normal file
@@ -0,0 +1,296 @@
|
||||
<template>
|
||||
<article class="box box-scene z-10" ref="scene">
|
||||
<div class="box-object" ref="box">
|
||||
<div class="face front relative" ref="frontFace">
|
||||
<img class="cover absolute" :src="`/${compilation.id}/cover.jpg`" alt="">
|
||||
</div>
|
||||
<div class="face back" ref="backFace">
|
||||
{{ compilation.description }}
|
||||
</div>
|
||||
<div class="face right" ref="rightFace" />
|
||||
<div class="face left" ref="leftFace" />
|
||||
<div class="face top" ref="topFace">
|
||||
<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="">
|
||||
</div>
|
||||
<div class="face bottom" ref="bottomFace" />
|
||||
</div>
|
||||
<OrganismCompilationPage :compilation="compilation" class="box-page" v-if="props.BoxState === 'selected'" />
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import type { Compilation, BoxState } from '~~/types/types'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
compilation: Compilation
|
||||
BoxState?: BoxState
|
||||
}>(),
|
||||
{ BoxState: 'list' }
|
||||
)
|
||||
|
||||
// --- Réfs ---
|
||||
const scene = ref<HTMLElement>()
|
||||
const box = ref<HTMLElement>()
|
||||
const frontFace = ref<HTMLElement>()
|
||||
const backFace = ref<HTMLElement>()
|
||||
const rightFace = ref<HTMLElement>()
|
||||
const leftFace = ref<HTMLElement>()
|
||||
const topFace = ref<HTMLElement>()
|
||||
const bottomFace = ref<HTMLElement>()
|
||||
|
||||
// --- Angles ---
|
||||
const rotateX = ref(0)
|
||||
const rotateY = ref(0)
|
||||
const rotateZ = ref(0)
|
||||
|
||||
// --- Drag + inertie ---
|
||||
let dragging = false
|
||||
let lastPointer = { x: 0, y: 0, time: 0 }
|
||||
let velocity = { x: 0, y: 0 }
|
||||
let raf: number | null = null
|
||||
|
||||
const sensitivity = 0.3
|
||||
const friction = 0.95
|
||||
const minVelocity = 0.02
|
||||
const enableInertia = true
|
||||
|
||||
// --- Transformations ---
|
||||
function applyTransform(duration = 0.5) {
|
||||
if (!box.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)`
|
||||
}
|
||||
|
||||
// --- Gestion BoxState ---
|
||||
function applyBoxState() {
|
||||
switch (props.BoxState) {
|
||||
case 'list':
|
||||
rotateX.value = 76
|
||||
rotateY.value = 0
|
||||
rotateZ.value = 150
|
||||
break
|
||||
case 'selected':
|
||||
rotateX.value = -20
|
||||
rotateY.value = 20
|
||||
rotateZ.value = 0
|
||||
break
|
||||
case 'hide':
|
||||
rotateX.value = 76
|
||||
rotateY.value = 0
|
||||
rotateZ.value = 150
|
||||
break
|
||||
}
|
||||
applyTransform(0.8) // transition fluide
|
||||
}
|
||||
|
||||
// --- Couleurs ---
|
||||
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
|
||||
}
|
||||
|
||||
// --- Inertie ---
|
||||
function tickInertia() {
|
||||
if (!enableInertia) return
|
||||
|
||||
velocity.x *= friction
|
||||
velocity.y *= friction
|
||||
|
||||
rotateX.value += velocity.y
|
||||
rotateY.value += velocity.x
|
||||
rotateX.value = Math.max(-80, Math.min(80, rotateX.value))
|
||||
|
||||
applyTransform(0.05) // court duration pour inertie fluide
|
||||
|
||||
if (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity) {
|
||||
raf = requestAnimationFrame(tickInertia)
|
||||
} else {
|
||||
raf = null
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pointer events ---
|
||||
onMounted(() => {
|
||||
applyColor()
|
||||
applyBoxState()
|
||||
|
||||
const down = (ev: PointerEvent) => {
|
||||
ev.preventDefault()
|
||||
dragging = true
|
||||
scene.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 }
|
||||
}
|
||||
|
||||
const move = (ev: PointerEvent) => {
|
||||
if (!dragging) return
|
||||
ev.preventDefault()
|
||||
const now = performance.now()
|
||||
const dx = ev.clientX - lastPointer.x
|
||||
const dy = ev.clientY - lastPointer.y
|
||||
const dt = Math.max(1, now - lastPointer.time)
|
||||
|
||||
rotateY.value += dx * sensitivity
|
||||
rotateX.value -= dy * sensitivity
|
||||
rotateX.value = Math.max(-80, Math.min(80, rotateX.value))
|
||||
|
||||
velocity.x = (dx / dt) * 16 * sensitivity
|
||||
velocity.y = (-dy / dt) * 16 * sensitivity
|
||||
|
||||
lastPointer = { x: ev.clientX, y: ev.clientY, time: now }
|
||||
applyTransform(0) // immédiat pendant drag
|
||||
}
|
||||
|
||||
const end = (ev: PointerEvent) => {
|
||||
if (!dragging) return
|
||||
dragging = false
|
||||
try { scene.value?.releasePointerCapture(ev.pointerId) } catch { }
|
||||
if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
|
||||
if (!raf) raf = requestAnimationFrame(tickInertia)
|
||||
}
|
||||
}
|
||||
|
||||
scene.value?.addEventListener('pointerdown', down)
|
||||
scene.value?.addEventListener('pointermove', move)
|
||||
scene.value?.addEventListener('pointerup', end)
|
||||
scene.value?.addEventListener('pointercancel', end)
|
||||
scene.value?.addEventListener('pointerleave', end)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cancelAnimationFrame(raf!)
|
||||
})
|
||||
})
|
||||
|
||||
// --- Watchers ---
|
||||
watch(() => props.BoxState, () => applyBoxState())
|
||||
watch(() => props.compilation, () => applyColor(), { deep: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
--size: 6px;
|
||||
--height: calc(var(--size) * (100 / 3));
|
||||
--width: calc(var(--size) * 50);
|
||||
--depth: calc(var(--size) * 10);
|
||||
transition: all .5s;
|
||||
|
||||
&.hide {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&.list {
|
||||
@apply hover:scale-105;
|
||||
}
|
||||
|
||||
&-scene {
|
||||
height: calc(var(--size) * 20);
|
||||
width: var(--width);
|
||||
perspective: 1000px;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
&-object {
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
margin: auto;
|
||||
user-select: none;
|
||||
|
||||
.list & {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selected & {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.face {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
backface-visibility: hidden;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.front,
|
||||
.back {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.face.top,
|
||||
.face.bottom {
|
||||
width: var(--width);
|
||||
height: var(--depth);
|
||||
}
|
||||
|
||||
.face.left,
|
||||
.face.right {
|
||||
width: var(--depth);
|
||||
height: var(--height);
|
||||
}
|
||||
|
||||
.face.front {
|
||||
transform: translateX(0) translateY(0) translateZ(var(--depth));
|
||||
}
|
||||
|
||||
.face.back {
|
||||
transform: rotateY(180deg) translateX(0) translateY(0) translateZ(0);
|
||||
}
|
||||
|
||||
.face.right {
|
||||
transform: rotateY(90deg) translateX(calc(var(--depth)*-1)) translateY(0px) translateZ(var(--width));
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.face.left {
|
||||
transform: rotateY(-90deg) translateX(calc(var(--depth)/2)) translateY(0) translateZ(calc(var(--depth)/2));
|
||||
}
|
||||
|
||||
.face.top {
|
||||
transform: rotateX(90deg) translateX(0px) translateY(calc(var(--depth)/2)) translateZ(calc(var(--depth)/2));
|
||||
}
|
||||
|
||||
.face.top>* {
|
||||
@apply rotate-180;
|
||||
}
|
||||
|
||||
.face.bottom {
|
||||
transform: rotateX(-90deg) translateX(0px) translateY(calc(var(--depth)* -0.5)) translateZ(calc(var(--height) - var(--depth)/2));
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
app/components/molecule/card.vue
Normal file
25
app/components/molecule/card.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<article
|
||||
class="backdrop-blur-sm -mt-12 z-10 card w-56 h-80 p-3 bg-opacity-10 bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden">
|
||||
<!-- Cover -->
|
||||
<figure class="flex-1 overflow-hidden rounded-t-xl">
|
||||
<img :src="coverUrl" alt="Pochette de l'album" class="w-full h-full object-cover object-center" />
|
||||
</figure>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="p-3 text-center bg-white rounded-b-xl">
|
||||
<h2 class="text-base text-neutral-800 font-bold truncate">{{ props.track.title }}</h2>
|
||||
<p class="text-sm text-neutral-500 truncate">
|
||||
{{ props.track.artist.name }}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Track } from '~~/types/types'
|
||||
|
||||
const props = defineProps<{ track: Track }>()
|
||||
const coverUrl = `https://f4.bcbits.com/img/${props.track.artist.coverId}_4.jpg`
|
||||
</script>
|
||||
53
app/components/organism/compilationList.vue
Normal file
53
app/components/organism/compilationList.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="flex flex-col-reverse mt-16">
|
||||
<molecule-box v-for="compilation in dataStore.getAllCompilations.slice().reverse()" :key="compilation.id"
|
||||
:compilation="compilation" :BoxState="boxStates[compilation.id]" @click="() => openCompilation(compilation.id)"
|
||||
:class="boxStates[compilation.id]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDataStore } from '~/store/data'
|
||||
import type { BoxState } from '~~/types/types'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const dataStore = useDataStore()
|
||||
const router = useRouter()
|
||||
const boxStates = ref<Record<string, BoxState>>({})
|
||||
|
||||
function openCompilation(id: string) {
|
||||
if (boxStates.value[id] === 'list') {
|
||||
for (const key in boxStates.value) {
|
||||
boxStates.value[key] = (key === id) ? 'selected' : 'hide'
|
||||
}
|
||||
window.history.pushState({}, '', '/compilation/' + id)
|
||||
}
|
||||
}
|
||||
|
||||
function closeCompilation(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
for (const key in boxStates.value) {
|
||||
boxStates.value[key] = 'list'
|
||||
}
|
||||
}
|
||||
window.history.pushState({}, '', '/')
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
17
app/components/organism/compilationPage.vue
Normal file
17
app/components/organism/compilationPage.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="mt-8 p-8 w-96">
|
||||
<MoleculeCard v-for="track in dataStore.getTracksByCompilationId(compilation.id)" :key="track.id" :track="track" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDataStore } from '~/store/data'
|
||||
import type { Compilation } from '~~/types/types'
|
||||
|
||||
const props = defineProps<{
|
||||
compilation: Compilation
|
||||
}>()
|
||||
|
||||
const dataStore = useDataStore()
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user