5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
||||
21
app/app.vue
@@ -4,3 +4,24 @@
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDataStore } from '@/store/dataStore'
|
||||
useHead({
|
||||
bodyAttrs: {
|
||||
class: 'bg-slate-100 dark:bg-slate-900'
|
||||
}
|
||||
})
|
||||
|
||||
// @todo : load datas as plugin/middleware (cant load pinia in plugin/middleware) ?
|
||||
onMounted(async () => {
|
||||
const dataStore = await useDataStore()
|
||||
await dataStore.loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
button {
|
||||
@apply px-4 py-2 m-4 bg-esyellow text-slate-700 rounded-md;
|
||||
}
|
||||
</style>
|
||||
263
app/components/boxItem.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
|
||||
<div class="scene" id="scene">
|
||||
<div class="box" id="box">
|
||||
<div class="face front">
|
||||
<img class="cover" src="https://evilspins.com/ES01A/object.png" alt="">
|
||||
</div>
|
||||
<div class="face back"></div>
|
||||
<div class="face right"></div>
|
||||
<div class="face left"></div>
|
||||
<div class="face top"></div>
|
||||
<div class="face bottom"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/*
|
||||
Rotation 3D au touch / clic maintenu
|
||||
- Utilise Pointer Events (fonctionne pour souris, stylet, touch)
|
||||
- rotateX et rotateY mis à jour pendant le drag
|
||||
- inertie légère à la release (configurable)
|
||||
*/
|
||||
|
||||
(function () {
|
||||
const box = document.getElementById('box');
|
||||
const scene = document.getElementById('scene');
|
||||
|
||||
// état
|
||||
let angleX = -20; // angle initial (pour montrer la 3D)
|
||||
let angleY = 20;
|
||||
let dragging = false;
|
||||
let lastPointer = { x: 0, y: 0, time: 0 };
|
||||
let velocity = { x: 0, y: 0 };
|
||||
let raf = null;
|
||||
|
||||
// paramètres
|
||||
const sensitivity = 0.3; // combien de degrés par px
|
||||
const friction = 0.95; // inertie (0..1) ; 1 = pas de friction
|
||||
const minVelocity = 0.02; // seuil pour arrêter l'inertie
|
||||
const enableInertia = true; // tu peux mettre false pour pas d'inertie
|
||||
|
||||
// applique la transformation (raf-friendly)
|
||||
function applyTransform() {
|
||||
box.style.transform = `rotateX(${angleX}deg) rotateY(${angleY}deg)`;
|
||||
}
|
||||
|
||||
applyTransform();
|
||||
|
||||
// boucle d'inertie
|
||||
function tickInertia() {
|
||||
if (!enableInertia) return;
|
||||
// appliquer friction
|
||||
velocity.x *= friction;
|
||||
velocity.y *= friction;
|
||||
|
||||
angleX += velocity.y;
|
||||
angleY += velocity.x;
|
||||
|
||||
// clamp angleX pour éviter flip complet (optionnel)
|
||||
angleX = Math.max(-80, Math.min(80, angleX));
|
||||
|
||||
applyTransform();
|
||||
|
||||
if (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity) {
|
||||
raf = requestAnimationFrame(tickInertia);
|
||||
} else {
|
||||
raf = null;
|
||||
}
|
||||
}
|
||||
|
||||
// pointerdown
|
||||
scene.addEventListener('pointerdown', (ev) => {
|
||||
ev.preventDefault();
|
||||
dragging = true;
|
||||
scene.setPointerCapture(ev.pointerId);
|
||||
lastPointer.x = ev.clientX;
|
||||
lastPointer.y = ev.clientY;
|
||||
lastPointer.time = performance.now();
|
||||
velocity.x = 0;
|
||||
velocity.y = 0;
|
||||
if (raf) { cancelAnimationFrame(raf); raf = null; } // stop inertie en commençant un nouveau drag
|
||||
});
|
||||
|
||||
// pointermove
|
||||
scene.addEventListener('pointermove', (ev) => {
|
||||
if (!dragging) return;
|
||||
ev.preventDefault();
|
||||
const now = performance.now();
|
||||
const dx = ev.clientX - lastPointer.x;
|
||||
const dy = ev.clientY - lastPointer.y;
|
||||
|
||||
// mise à jour angles (inverser si tu veux sens différent)
|
||||
angleY += dx * sensitivity;
|
||||
angleX -= dy * sensitivity;
|
||||
|
||||
// clamp angleX
|
||||
angleX = Math.max(-80, Math.min(80, angleX));
|
||||
|
||||
// calculer vitesse (px per frame approximative)
|
||||
const dt = Math.max(1, now - lastPointer.time); // ms
|
||||
velocity.x = (dx / dt) * 16 * sensitivity; // normalisé approximativement à 60fps
|
||||
velocity.y = (-dy / dt) * 16 * sensitivity;
|
||||
|
||||
lastPointer.x = ev.clientX;
|
||||
lastPointer.y = ev.clientY;
|
||||
lastPointer.time = now;
|
||||
|
||||
applyTransform();
|
||||
});
|
||||
|
||||
// pointerup / cancel
|
||||
const endDrag = (ev) => {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
try { scene.releasePointerCapture(ev.pointerId); } catch (e) { }
|
||||
// si inertie activée, lancer la boucle
|
||||
if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
|
||||
if (!raf) raf = requestAnimationFrame(tickInertia);
|
||||
}
|
||||
};
|
||||
|
||||
scene.addEventListener('pointerup', endDrag);
|
||||
scene.addEventListener('pointercancel', endDrag);
|
||||
scene.addEventListener('pointerleave', endDrag); // utile sur desktop si tu sors du container
|
||||
|
||||
// accessibility : permettre rotation via clavier (optionnel)
|
||||
scene.tabIndex = 0;
|
||||
scene.addEventListener('keydown', (e) => {
|
||||
const step = 8;
|
||||
if (e.key === 'ArrowLeft') { angleY -= step; applyTransform(); }
|
||||
if (e.key === 'ArrowRight') { angleY += step; applyTransform(); }
|
||||
if (e.key === 'ArrowUp') { angleX -= step; applyTransform(); }
|
||||
if (e.key === 'ArrowDown') { angleX += step; applyTransform(); }
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* H W D
|
||||
k7 5 : 8 : 1
|
||||
CD 12 : 14 : 1
|
||||
VHS 4.5 : 8 : 1
|
||||
DVD 14 : 10 : 1
|
||||
*/
|
||||
|
||||
:root {
|
||||
--size: 100px;
|
||||
--height: 150px;
|
||||
--width: 150px;
|
||||
--depth: 10px;
|
||||
--bg: #0f172a;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
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 {
|
||||
width: calc(var(--height) * 1.2);
|
||||
height: calc(var(--width) * 1.2);
|
||||
perspective: 1000px;
|
||||
touch-action: none;
|
||||
/* essentiel pour empêcher le scroll pendant le drag */
|
||||
}
|
||||
|
||||
/* l'objet 3D (box simple) */
|
||||
.box {
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 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 {
|
||||
background: linear-gradient(180deg, #2563eb, #7dd3fc);
|
||||
transform: translateX(0px) translateY(0px) translateZ(0px);
|
||||
}
|
||||
|
||||
.face.back {
|
||||
background: linear-gradient(180deg, #7c3aed, #a78bfa);
|
||||
transform: rotateY(180deg) translateX(var(--width)) translateY(0px) translateZ(var(--depth));
|
||||
}
|
||||
|
||||
.face.right {
|
||||
background: linear-gradient(180deg, #10b981, #6ee7b7);
|
||||
transform: rotateY(90deg) translateX(0px) translateY(0px) translateZ(var(--width));
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.face.left {
|
||||
background: linear-gradient(180deg, #ef4444, #fca5a5);
|
||||
transform: rotateY(-90deg) translateX(0) translateY(0px) translateZ(var(--depth));
|
||||
}
|
||||
|
||||
.face.top {
|
||||
background: linear-gradient(180deg, #f59e0b, #fcd34d);
|
||||
transform: rotateX(90deg) translateX(0px) translateY(calc(var(--depth) * -1)) translateZ(0px);
|
||||
}
|
||||
|
||||
.face.bottom {
|
||||
background: linear-gradient(180deg, #06b6d4, #67e8f9);
|
||||
transform: rotateX(-90deg) translateX(0) translateY(0px) translateZ(calc(var(--height)));
|
||||
}
|
||||
|
||||
.cover {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
338
app/components/compilationBox.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<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 left-0 w-16 pr-4" :src="`/${compilation.id}/title.svg`" alt="">
|
||||
</div>
|
||||
<div class="face bottom" ref="bottomFace" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="devtool absolute right-4 text-white bg-black rounded-2xl px-4 py-2">
|
||||
<button @click="poser">poser</button>
|
||||
<button @click="face">face</button>
|
||||
<button @click="dos">dos</button>
|
||||
<div>
|
||||
<label class="block">
|
||||
X: {{ angleX }}
|
||||
<input v-model.number="angleX" type="range" step="1" min="-180" max="180" @input="applyTransform">
|
||||
</label>
|
||||
<label class="block">
|
||||
Y: {{ angleY }}
|
||||
<input v-model.number="angleY" type="range" step="1" min="-180" max="180" @input="applyTransform">
|
||||
</label>
|
||||
<label class="block">
|
||||
Z: {{ angleZ }}
|
||||
<input v-model.number="angleZ" type="range" step="1" min="-180" max="180" @input="applyTransform">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import compilations from '~~/server/api/compilations';
|
||||
import type { Compilation } from '~~/types/types';
|
||||
|
||||
const props = defineProps<{ compilation: Compilation }>()
|
||||
|
||||
// States
|
||||
const position = ref('')
|
||||
const angleX = ref(76)
|
||||
const angleY = ref(0)
|
||||
const angleZ = ref(150)
|
||||
|
||||
const frontFace = ref()
|
||||
const backFace = ref()
|
||||
const rightFace = ref()
|
||||
const leftFace = ref()
|
||||
const topFace = ref()
|
||||
const bottomFace = ref()
|
||||
|
||||
function poser() {
|
||||
rotateBox(76, 0, 150)
|
||||
}
|
||||
|
||||
function face() {
|
||||
rotateBox(-20, 20, 0)
|
||||
}
|
||||
|
||||
function dos() {
|
||||
rotateBox(-20, 200, 0)
|
||||
}
|
||||
|
||||
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)`
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Fonction utilitaire : place la box directement à une rotation donnée
|
||||
(utilisée par le bouton reset ou autre logique externe)
|
||||
*/
|
||||
function rotateBox(x, y, z) {
|
||||
angleX.value = x
|
||||
angleY.value = y
|
||||
angleZ.value = z
|
||||
applyTransform()
|
||||
|
||||
box.value.style.setProperty('transition', 'transform 800ms ease-in-out')
|
||||
setTimeout(() => {
|
||||
box.value.style.setProperty('transition', 'transform 120ms linear')
|
||||
console.log('dopne timeout')
|
||||
}, 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(() => {
|
||||
// setup CSS vars pour la scène
|
||||
updateCssVar('--height', '200px', scene.value)
|
||||
updateCssVar('--width', '300px', scene.value)
|
||||
updateCssVar('--depth', '60px', scene.value)
|
||||
|
||||
frontFace.value.style.setProperty('background', `${props.compilation.colorTo}`)
|
||||
backFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.colorFrom}, ${props.compilation.colorTo})`)
|
||||
rightFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.colorFrom}, ${props.compilation.colorTo})`)
|
||||
leftFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.colorFrom}, ${props.compilation.colorTo})`)
|
||||
topFace.value.style.setProperty('background', `linear-gradient(to top, ${props.compilation.colorTo}, ${props.compilation.colorTo})`)
|
||||
bottomFace.value.style.setProperty('background', `${props.compilation.colorFrom}`)
|
||||
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
</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;
|
||||
/* 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>
|
||||
@@ -1,11 +1,17 @@
|
||||
<template>
|
||||
<div class="mt-8 flex flex-wrap justify-center">
|
||||
<div className="bg-page-dark-bg text-white">
|
||||
<div className="bg-gradient-to-r from-primary to-primary-dark border-t-2 border-b-2 border-primary">
|
||||
<div className="mt-8 flex flex-wrap justify-center">
|
||||
<gameBox />
|
||||
<div class="flex flex-wrap justify-center">
|
||||
<div class="bg-page-dark-bg text-white">
|
||||
<div class="flex flex-col-reverse bg-gradient-to-r from-primary to-primary-dark">
|
||||
<div class="mt-8 flex flex-wrap justify-center"
|
||||
v-for="compilation in store.getAllCompilations.slice().reverse()">
|
||||
<compilationBox :compilation="compilation" template="full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDataStore } from '@/store/dataStore'
|
||||
const store = useDataStore()
|
||||
</script>
|
||||
|
||||
51
app/store/dataStore.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Compilation, Artist, Track } from '~/../types/types'
|
||||
|
||||
// stores/data.ts
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useDataStore = defineStore('data', {
|
||||
state: () => ({
|
||||
compilations: [] as Compilation[], // Store your compilation data here
|
||||
artists: [] as Artist[], // Store artist data here
|
||||
tracks: [] as Track[], // Store track data here
|
||||
isLoaded: false, // Remember if data is already loaded
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async loadData() {
|
||||
if (this.isLoaded) return // Avoid re-fetching if already loaded
|
||||
|
||||
// Fetch your data once (e.g., from an API or local JSON)
|
||||
const { data: compilations } = await useFetch('/api/compilations')
|
||||
const { data: artists } = await useFetch('/api/artists')
|
||||
const { data: tracks } = await useFetch('/api/tracks')
|
||||
|
||||
// Set the data in the store
|
||||
this.compilations = compilations.value
|
||||
this.artists = artists.value
|
||||
this.tracks = tracks.value
|
||||
this.isLoaded = true
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
// Obtenir tous les compilations
|
||||
getAllCompilations: (state) => state.compilations,
|
||||
getCompilationById: (state) => {
|
||||
return (id: string) => {
|
||||
return state.compilations.find(compilation => compilation.id === id)
|
||||
}
|
||||
},
|
||||
// Obtenir toutes les pistes d'une compilation donnée
|
||||
getTracksByCompilationId: (state) => (compilationId: string) => {
|
||||
return state.tracks.filter(track => track.compilationId === compilationId)
|
||||
},
|
||||
// Filtrer les artistes selon certains critères
|
||||
getArtistById: (state) => (id: number) => state.artists.find(artist => artist.id === id),
|
||||
|
||||
// Obtenir toutes les pistes d'un artiste donné
|
||||
getTracksByArtistId: (state) => (artistId: string) => {
|
||||
return state.tracks.filter(track => track.artistId === artistId)
|
||||
},
|
||||
},
|
||||
})
|
||||
6
app/utils/cssVars.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// utils/cssVars.js
|
||||
export function updateCssVar(name, value, el) {
|
||||
// if (!import.meta.client) return;
|
||||
const target = el?.$el || el || document.documentElement;
|
||||
target.style.setProperty(name, value);
|
||||
}
|
||||
@@ -1,6 +1,21 @@
|
||||
// @ts-check
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
import withNuxt from "./.nuxt/eslint.config.mjs";
|
||||
|
||||
export default withNuxt(
|
||||
// Your custom configs here
|
||||
)
|
||||
export default withNuxt({
|
||||
rules: {
|
||||
// Garde l'ordre correct : class avant @click
|
||||
"vue/attributes-order": "error",
|
||||
|
||||
// Contrôle du nombre d'attributs par ligne
|
||||
"vue/max-attributes-per-line": [
|
||||
"error",
|
||||
{
|
||||
singleline: 3, // autorise jusqu’à 3 attributs sur une ligne
|
||||
multiline: {
|
||||
max: 1, // si retour à la ligne, 1 attr par ligne
|
||||
allowFirstLine: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
modules: ['@nuxt/eslint', '@nuxtjs/tailwindcss'],
|
||||
modules: ['@nuxt/eslint', '@nuxtjs/tailwindcss', '@pinia/nuxt'],
|
||||
app: {
|
||||
head: {
|
||||
link: [
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"dev": "nuxt dev --host",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
@@ -12,11 +12,13 @@
|
||||
"dependencies": {
|
||||
"@nuxt/eslint": "1.9.0",
|
||||
"@nuxtjs/tailwindcss": "6.14.0",
|
||||
"@pinia/nuxt": "0.11.2",
|
||||
"atropos": "^2.0.2",
|
||||
"eslint": "^9.33.0",
|
||||
"nuxt": "^4.0.3",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.1",
|
||||
"atropos": "^2.0.2"
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=10 <11"
|
||||
|
||||
41
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@nuxtjs/tailwindcss':
|
||||
specifier: 6.14.0
|
||||
version: 6.14.0(magicast@0.3.5)
|
||||
'@pinia/nuxt':
|
||||
specifier: 0.11.2
|
||||
version: 0.11.2(magicast@0.3.5)(pinia@3.0.3(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2)))
|
||||
atropos:
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2
|
||||
@@ -23,6 +26,9 @@ importers:
|
||||
nuxt:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@24.3.0)(@vue/compiler-sfc@3.5.18)(db0@0.3.2)(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.3)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1)
|
||||
pinia:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2))
|
||||
vue:
|
||||
specifier: ^3.5.18
|
||||
version: 3.5.18(typescript@5.9.2)
|
||||
@@ -1142,6 +1148,11 @@ packages:
|
||||
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
'@pinia/nuxt@0.11.2':
|
||||
resolution: {integrity: sha512-CgvSWpbktxxWBV7ModhAcsExsQZqpPq6vMYEe9DexmmY6959ev8ukL4iFhr/qov2Nb9cQAWd7niFDnaWkN+FHg==}
|
||||
peerDependencies:
|
||||
pinia: ^3.0.3
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -1608,6 +1619,9 @@ packages:
|
||||
'@vue/devtools-api@6.6.4':
|
||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||
|
||||
'@vue/devtools-api@7.7.7':
|
||||
resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
|
||||
|
||||
'@vue/devtools-core@7.7.7':
|
||||
resolution: {integrity: sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ==}
|
||||
peerDependencies:
|
||||
@@ -3690,6 +3704,15 @@ packages:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
pinia@3.0.3:
|
||||
resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==}
|
||||
peerDependencies:
|
||||
typescript: '>=4.4.4'
|
||||
vue: ^2.7.0 || ^3.5.11
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
pirates@4.0.7:
|
||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -6106,6 +6129,13 @@ snapshots:
|
||||
'@parcel/watcher-win32-ia32': 2.5.1
|
||||
'@parcel/watcher-win32-x64': 2.5.1
|
||||
|
||||
'@pinia/nuxt@0.11.2(magicast@0.3.5)(pinia@3.0.3(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2)))':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.18.1(magicast@0.3.5)
|
||||
pinia: 3.0.3(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2))
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
@@ -6571,6 +6601,10 @@ snapshots:
|
||||
|
||||
'@vue/devtools-api@6.6.4': {}
|
||||
|
||||
'@vue/devtools-api@7.7.7':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.7
|
||||
|
||||
'@vue/devtools-core@7.7.7(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.7
|
||||
@@ -8923,6 +8957,13 @@ snapshots:
|
||||
|
||||
pify@2.3.0: {}
|
||||
|
||||
pinia@3.0.3(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 7.7.7
|
||||
vue: 3.5.18(typescript@5.9.2)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.2
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
|
||||
BIN
public/ES00A/cover.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
59
public/ES00A/title.svg
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="11.994946mm"
|
||||
height="6.3803077mm"
|
||||
viewBox="0 0 11.994946 6.3803077"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="title.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="1.0075547"
|
||||
inkscape:cx="397.00077"
|
||||
inkscape:cy="561.25984"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1372"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<rect
|
||||
x="3133.6135"
|
||||
y="-1022.8775"
|
||||
width="252.69154"
|
||||
height="45.052521"
|
||||
id="rect1117-2-0-8" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.29158882,0,0,0.29158882,-944.63041,301.73681)"
|
||||
id="text1117-9-2"
|
||||
style="font-size:24.1969px;line-height:0;font-family:Bebas;-inkscape-font-specification:Bebas;text-align:center;letter-spacing:3px;white-space:pre;shape-inside:url(#rect1117-2-0-8);display:inline;fill:#000000;fill-opacity:0.994712;stroke:none;stroke-width:6.04346;stroke-miterlimit:2.3;stroke-dasharray:1.20869, 6.64778;stroke-dashoffset:0;stroke-opacity:1"><tspan
|
||||
x="3240.7435"
|
||||
y="-1016.1585"
|
||||
id="tspan4"><tspan
|
||||
style="font-family:'Punk Typewriter';-inkscape-font-specification:'Punk Typewriter'"
|
||||
id="tspan3">zero</tspan></tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/ES00B/cover.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
59
public/ES00B/title.svg
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="11.994946mm"
|
||||
height="6.3803077mm"
|
||||
viewBox="0 0 11.994946 6.3803077"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="title.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="32.241751"
|
||||
inkscape:cx="27.541929"
|
||||
inkscape:cy="12.142641"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1372"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<rect
|
||||
x="3133.6135"
|
||||
y="-1022.8775"
|
||||
width="252.69154"
|
||||
height="45.052521"
|
||||
id="rect1117-2-0-8" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.29158882,0,0,0.29158882,-944.63041,301.73681)"
|
||||
id="text1117-9-2"
|
||||
style="font-size:24.1969px;line-height:0;font-family:Bebas;-inkscape-font-specification:Bebas;text-align:center;letter-spacing:3px;white-space:pre;shape-inside:url(#rect1117-2-0-8);display:inline;fill:#ffffff;fill-opacity:0.994712;stroke:none;stroke-width:6.04346;stroke-miterlimit:2.3;stroke-dasharray:1.20869, 6.64778;stroke-dashoffset:0;stroke-opacity:1"><tspan
|
||||
x="3240.7435"
|
||||
y="-1016.1585"
|
||||
id="tspan4"><tspan
|
||||
style="font-family:'Punk Typewriter';-inkscape-font-specification:'Punk Typewriter'"
|
||||
id="tspan3">zero</tspan></tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/ES01A/cover.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
50
public/ES01A/title.svg
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
public/ES01B/cover.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
50
public/ES01B/title.svg
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
70
server/api/artists.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export default eventHandler(() => {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
name: "L'efondras",
|
||||
url: "https://leffondras.bandcamp.com/music",
|
||||
coverId: "0024705317"
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: "The kundalini genie",
|
||||
url: "https://the-kundalini-genie.bandcamp.com",
|
||||
coverId: "0012045550"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Fontaines D.C.",
|
||||
url: "https://fontainesdc.bandcamp.com",
|
||||
coverId: "0027327090"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Fontanarosa",
|
||||
url: "https://fontanarosa.bandcamp.com",
|
||||
coverId: "0035380235",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Johnny mafia",
|
||||
url: "https://johnnymafia.bandcamp.com",
|
||||
coverId: "0035009392",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "New candys",
|
||||
url: "https://newcandys.bandcamp.com",
|
||||
coverId: "0033518637",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Magic shoppe",
|
||||
url: "https://magicshoppe.bandcamp.com",
|
||||
coverId: "0030748374"
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Les jaguars",
|
||||
url: "https://radiomartiko.bandcamp.com/album/surf-qu-b-cois",
|
||||
coverId: "0016551336",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "TRAAMS",
|
||||
url: "https://traams.bandcamp.com",
|
||||
coverId: "0028348410",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Blue orchid",
|
||||
url: "https://blue-orchid.bandcamp.com",
|
||||
coverId: "0034796193",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "I love UFO",
|
||||
url: "https://bruitblanc.bandcamp.com",
|
||||
coverId: "a2203158939",
|
||||
}
|
||||
]
|
||||
})
|
||||
36
server/api/compilations.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export default eventHandler(() => {
|
||||
return [
|
||||
{
|
||||
id: 'ES00A',
|
||||
name: 'zero',
|
||||
duration: 2794,
|
||||
description: 'Zero is for manifesto ... ;)',
|
||||
colorFrom: '#ffffff',
|
||||
colorTo: '#48959d',
|
||||
},
|
||||
{
|
||||
id: 'ES00B',
|
||||
name: 'zero b-side',
|
||||
duration: 2470,
|
||||
description: 'Even Zero has a b-side',
|
||||
colorFrom: '#0d01b9',
|
||||
colorTo: '#3b7589',
|
||||
},
|
||||
{
|
||||
id: 'ES01A',
|
||||
name: 'first',
|
||||
duration: 3487,
|
||||
description: '...',
|
||||
colorFrom: '#c7b3aa',
|
||||
colorTo: '#000100',
|
||||
},
|
||||
{
|
||||
id: 'ES01B',
|
||||
name: 'first b-side',
|
||||
duration: 3773,
|
||||
description: '...',
|
||||
colorFrom: '#f7dd01',
|
||||
colorTo: '#010103',
|
||||
}
|
||||
]
|
||||
})
|
||||
23
server/api/playlists/[id].ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const id = event.context.params?.id || ''
|
||||
|
||||
const directoryPath = path.join(process.cwd(), 'media/files/music/' + id) // replace 'your-folder' with the folder you want to list
|
||||
|
||||
try {
|
||||
// Read the directory contents
|
||||
const files = await fs.promises.readdir(directoryPath)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
files: files.filter(file => !file.startsWith('.')) // optional: exclude unwanted files
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
})
|
||||
21
server/api/playlists/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const directoryPath = path.join(process.cwd(), 'media/files/music')
|
||||
|
||||
try {
|
||||
// Read the directory contents
|
||||
const files = await fs.promises.readdir(directoryPath)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
files: files.filter(file => !file.startsWith('.')).reverse() // exclude unwanted files
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
})
|
||||
224
server/api/tracks.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
export default eventHandler(() => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
compilationId: 'ES00A',
|
||||
title: 'The grinding wheel',
|
||||
artist: 0,
|
||||
start: 0,
|
||||
bpm: 0,
|
||||
url: 'https://arakirecords.bandcamp.com/track/the-grinding-wheel',
|
||||
coverId: 'a3236746052',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
compilationId: 'ES00A',
|
||||
title: 'Bleach',
|
||||
artist: 1,
|
||||
start: 393,
|
||||
bpm: 0,
|
||||
url: 'https://the-kundalini-genie.bandcamp.com/track/bleach-2',
|
||||
coverId: 'a1714786533',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
compilationId: 'ES00A',
|
||||
title: 'Televised mind',
|
||||
artist: 2,
|
||||
start: 892,
|
||||
bpm: 0,
|
||||
url: 'https://fontainesdc.bandcamp.com/track/televised-mind',
|
||||
coverId: 'a3772806156'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
compilationId: 'ES00A',
|
||||
title: 'In it',
|
||||
artist: 3,
|
||||
start: 1138,
|
||||
bpm: 0,
|
||||
url: 'https://howlinbananarecords.bandcamp.com/track/in-it',
|
||||
coverId: 'a1720372066',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
compilationId: 'ES00A',
|
||||
title: 'Bad michel',
|
||||
artist: 4,
|
||||
start: 1245,
|
||||
bpm: 0,
|
||||
url: 'https://johnnymafia.bandcamp.com/track/bad-michel-3',
|
||||
coverId: 'a0984622869',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
compilationId: 'ES00A',
|
||||
title: 'Overall',
|
||||
artist: 5,
|
||||
start: 1394,
|
||||
bpm: 0,
|
||||
url: 'https://newcandys.bandcamp.com/track/overall',
|
||||
coverId: 'a0559661270',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
compilationId: 'ES00A',
|
||||
title: 'Blowup',
|
||||
artist: 6,
|
||||
start: 1674,
|
||||
bpm: 0,
|
||||
url: 'https://magicshoppe.bandcamp.com/track/blowup',
|
||||
coverId: 'a1444895293',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
compilationId: 'ES00A',
|
||||
title: 'Guitar jet',
|
||||
artist: 7,
|
||||
start: 1880,
|
||||
bpm: 0,
|
||||
url: 'https://radiomartiko.bandcamp.com/track/guitare-jet',
|
||||
coverId: 'a1494681687',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
compilationId: 'ES00A',
|
||||
title: 'Intercontinental radio waves',
|
||||
artist: 8,
|
||||
start: 2024,
|
||||
bpm: 0,
|
||||
url: 'https://traams.bandcamp.com/track/intercontinental-radio-waves',
|
||||
coverId: 'a0046738552',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
compilationId: 'ES00A',
|
||||
title: 'Here comes the sun',
|
||||
artist: 9,
|
||||
start: 2211,
|
||||
bpm: 0,
|
||||
url: 'https://blue-orchid.bandcamp.com/track/here-come-the-sun',
|
||||
coverId: 'a4102567047',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
compilationId: 'ES00A',
|
||||
title: 'Like in the movies',
|
||||
artist: 10,
|
||||
start: 2559,
|
||||
bpm: 0,
|
||||
url: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies-2',
|
||||
coverId: 'a2203158939',
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
compilationId: 'ES00B',
|
||||
title: 'Ce que révèle l\'éclipse',
|
||||
artist: 0,
|
||||
start: 0,
|
||||
bpm: 0,
|
||||
url: 'https://arakirecords.bandcamp.com/track/ce-que-r-v-le-l-clipse',
|
||||
coverId: 'a3236746052',
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
compilationId: 'ES00B',
|
||||
title: 'Bleedin\' Gums Mushrool',
|
||||
artist: 1,
|
||||
start: 263,
|
||||
bpm: 0,
|
||||
url: 'https://the-kundalini-genie.bandcamp.com/track/bleedin-gums-mushroom',
|
||||
coverId: 'a1714786533',
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
compilationId: 'ES00B',
|
||||
title: 'A lucid dream',
|
||||
artist: 2,
|
||||
start: 554,
|
||||
bpm: 0,
|
||||
url: 'https://fontainesdc.bandcamp.com/track/a-lucid-dream',
|
||||
coverId: 'a3772806156',
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
compilationId: 'ES00B',
|
||||
title: 'Lights off',
|
||||
artist: 3,
|
||||
start: 781,
|
||||
bpm: 0,
|
||||
url: 'https://howlinbananarecords.bandcamp.com/track/lights-off',
|
||||
coverId: 'a1720372066',
|
||||
},
|
||||
{
|
||||
id: 25,
|
||||
compilationId: 'ES00B',
|
||||
title: 'I\'m sentimental',
|
||||
artist: 4,
|
||||
start: 969,
|
||||
bpm: 0,
|
||||
url: 'https://johnnymafia.bandcamp.com/track/im-sentimental-2',
|
||||
coverId: 'a2333676849',
|
||||
},
|
||||
{
|
||||
id: 26,
|
||||
compilationId: 'ES00B',
|
||||
title: 'Thrill or trip',
|
||||
artist: 5,
|
||||
start: 1128,
|
||||
bpm: 0,
|
||||
url: 'https://newcandys.bandcamp.com/track/thrill-or-trip',
|
||||
coverId: 'a0559661270',
|
||||
},
|
||||
{
|
||||
id: 27,
|
||||
compilationId: 'ES00B',
|
||||
title: 'Redhead',
|
||||
artist: 6,
|
||||
start: 1303,
|
||||
bpm: 0,
|
||||
url: 'https://magicshoppe.bandcamp.com/track/redhead',
|
||||
coverId: 'a0594426943',
|
||||
},
|
||||
{
|
||||
id: 28,
|
||||
compilationId: 'ES00B',
|
||||
title: 'Supersonic twist',
|
||||
artist: 7,
|
||||
start: 1584,
|
||||
bpm: 0,
|
||||
url: 'https://open.spotify.com/track/66voQIZAJ3zD3Eju2qtNjF',
|
||||
coverId: 'a1494681687',
|
||||
},
|
||||
{
|
||||
id: 29,
|
||||
compilationId: 'ES00B',
|
||||
title: 'Flowers',
|
||||
artist: 8,
|
||||
start: 1749,
|
||||
bpm: 0,
|
||||
url: 'https://traams.bandcamp.com/track/flowers',
|
||||
coverId: 'a3644668199',
|
||||
},
|
||||
{
|
||||
id: 30,
|
||||
compilationId: 'ES00B',
|
||||
title: 'The shade',
|
||||
artist: 9,
|
||||
start: 1924,
|
||||
bpm: 0,
|
||||
url: 'https://blue-orchid.bandcamp.com/track/the-shade',
|
||||
coverId: 'a0804204790',
|
||||
},
|
||||
{
|
||||
id: 31,
|
||||
compilationId: 'ES00B',
|
||||
title: 'Like in the movies',
|
||||
artist: 10,
|
||||
start: 2185,
|
||||
bpm: 0,
|
||||
url: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies',
|
||||
coverId: 'a3647322740',
|
||||
},
|
||||
]
|
||||
})
|
||||
3
server/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
||||
28
types/types.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// types.ts
|
||||
export interface Compilation {
|
||||
id: string
|
||||
name: string
|
||||
duration: number
|
||||
tracks?: Track[]
|
||||
description: string
|
||||
colorFrom: string
|
||||
colorTo: string
|
||||
}
|
||||
|
||||
export interface Artist {
|
||||
id: number
|
||||
name: string
|
||||
url: string
|
||||
coverId: string
|
||||
}
|
||||
|
||||
export interface Track {
|
||||
id: string
|
||||
compilationId: string
|
||||
title: string
|
||||
artistId: number
|
||||
artist?: Artist
|
||||
start: number
|
||||
link: string
|
||||
coverId: string
|
||||
}
|
||||