evilSpins v1
All checks were successful
Deploy App / build (push) Successful in 43s
Deploy App / deploy (push) Successful in 41s

This commit is contained in:
valere
2025-11-04 22:41:41 +01:00
parent deb15b3ea1
commit 34d22b3b17
49 changed files with 5791 additions and 2447 deletions

View File

@@ -3,11 +3,9 @@
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useUiStore } from '~/store/ui'
import { useDataStore } from '~/store/data'
import { usePlayerStore } from '~/store/player'
// Configuration du layout
definePageMeta({
@@ -23,13 +21,8 @@ onMounted(async () => {
const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
if (typeof idParam === 'string' && idParam.length > 0) {
uiStore.selectBox(idParam)
// Lire automatiquement la box si on est sur la page d'une box
const box = dataStore.boxes.find(b => b.id === idParam)
if (box) {
const player = usePlayerStore()
player.playBox(box).catch(console.error)
}
// player.playBox(box).catch(console.error)
// }
}
})
</script>

45
app/pages/defaultDev.vue Normal file
View File

@@ -0,0 +1,45 @@
<template>
<div class="deck">
<card
v-for="track in tracks"
:key="track.id"
:track="track"
:is-face-up="track.isFaceUp"
@click="flipCard(track)"
/>
</div>
</template>
<script setup>
import { useDataStore } from '~/store/data'
const tracks = ref([])
definePageMeta({
layout: 'default'
})
onMounted(async () => {
const dataStore = useDataStore()
await dataStore.loadData()
tracks.value = dataStore.getTracksByboxId('ES2025')
})
function flipCard(track) {
track.isFaceUp = !track.isFaceUp
}
</script>
<style lang="scss" scoped>
.logo {
filter: drop-shadow(3px 3px 0 rgb(0 0 0 / 0.7));
}
.deck {
position: relative;
height: 80vh;
.card {
z-index: 10;
position: relative;
}
}
</style>

190
app/pages/dev.vue Normal file
View File

@@ -0,0 +1,190 @@
<template>
<div>
<button
class="fixed bottom-4 right-4 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full shadow-lg z-50 transition-colors"
title="Ranger les cartes"
@click="arrangeCards"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16m-7 6h7"
/>
</svg>
</button>
<div ref="deck" class="deck">
<card
v-for="track in tracks"
:key="track.id"
:track="track"
:class="['card', 'id-' + track.id, { dragging: dragging === track }]"
:style="{
top: track.y + 'px',
left: track.x + 'px',
zIndex: track.zIndex || 1,
transform: `rotate(${track.rotation || 0}deg)`
}"
:is-face-up="track.isFaceUp"
@mousedown="startDrag($event, track)"
@click="flipCard(track)"
/>
</div>
</div>
</template>
<script setup>
import { useDataStore } from '~/store/data'
const tracks = ref([])
const deck = ref(null)
let zIndexCounter = 1
let dragging = null
let offset = { x: 0, y: 0 }
let velocity = { x: 0, y: 0 }
let lastTime = 0
const dataStore = useDataStore()
definePageMeta({ layout: 'default' })
onMounted(async () => {
await dataStore.loadData()
tracks.value = dataStore.getTracksByboxId('ES2025').map((t, i) => ({
...t,
x: t.x ?? 50 + i * 20,
y: t.y ?? 50 + i * 20,
zIndex: t.zIndex ?? i + 1,
rotation: 0,
isFaceUp: t.isFaceUp ?? false
}))
window.addEventListener('mousemove', onDrag)
window.addEventListener('mouseup', stopDrag)
})
onBeforeUnmount(() => {
window.removeEventListener('mousemove', onDrag)
window.removeEventListener('mouseup', stopDrag)
})
function startDrag(e, track) {
dragging = track
const rect = deck.value.getBoundingClientRect()
offset.x = e.clientX - rect.left - track.x
offset.y = e.clientY - rect.top - track.y
lastTime = performance.now()
velocity = { x: 0, y: 0 }
zIndexCounter += 1
track.zIndex = zIndexCounter
}
function onDrag(e) {
if (!dragging) return
const rect = deck.value.getBoundingClientRect()
const newX = e.clientX - rect.left - offset.x
const newY = e.clientY - rect.top - offset.y
const now = performance.now()
const dt = now - lastTime
velocity.x = (newX - dragging.x) / dt
velocity.y = (newY - dragging.y) / dt
lastTime = now
dragging.x = newX
dragging.y = newY
// Rotation dynamique selon la position horizontale
const centerX = rect.width / 2
const dx = dragging.x - centerX
dragging.rotation = dx * 0.05
}
function stopDrag() {
if (!dragging) return
const track = dragging
dragging = null
// Inertie douce
const decay = 0.95
function animateInertia() {
if (Math.abs(velocity.x) < 0.02 && Math.abs(velocity.y) < 0.02) return
track.x += velocity.x * 16
track.y += velocity.y * 16
velocity.x *= decay
velocity.y *= decay
requestAnimationFrame(animateInertia)
}
animateInertia()
}
function flipCard(track) {
track.isFaceUp = true
}
function arrangeCards() {
const deckRect = deck.value.getBoundingClientRect()
const cardWidth = 224
const cardHeight = 320
const padding = 20
const cardsPerRow = Math.max(
1,
Math.floor((deckRect.width - padding * 2) / (cardWidth + padding))
)
tracks.value.forEach((track, index) => {
const row = Math.floor(index / cardsPerRow)
const col = index % cardsPerRow
track.x = padding + col * (cardWidth + padding)
track.y = padding + row * (cardHeight / 3)
track.zIndex = index + 1
track.rotation = 0
zIndexCounter = Math.max(zIndexCounter, track.zIndex)
})
}
</script>
<style lang="scss" scoped>
.deck {
position: relative;
width: 100%;
min-height: 80vh;
height: 100%;
overflow-y: auto;
padding: 1rem;
box-sizing: border-box;
}
.card {
position: absolute;
cursor: grab;
transition:
transform 0.1s ease,
box-shadow 0.2s ease;
}
.card:active {
cursor: grabbing;
}
.card.dragging {
z-index: 999;
cursor: grabbing;
transform: scale(1.05) rotate(0deg);
/* rotation sera remplacée dynamiquement */
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5);
}
</style>

130
app/pages/draggable.vue Normal file
View File

@@ -0,0 +1,130 @@
<template>
<div class="deck">
<draggable
v-model="tracks"
item-key="id"
class="draggable-container"
@start="drag = true"
@end="onDragEnd"
>
<template #item="{ element: track }">
<card
:key="track.id"
:track="track"
tabindex="0"
:is-face-up="track.isFaceUp"
class="draggable-item"
@click="flipCard(track)"
/>
</template>
</draggable>
</div>
</template>
<script setup>
import { useDataStore } from '~/store/data'
import draggable from 'vuedraggable'
const drag = ref(false)
const tracks = ref([])
// Configuration du layout
definePageMeta({
layout: 'default'
})
onMounted(async () => {
const dataStore = useDataStore()
await dataStore.loadData()
tracks.value = dataStore.getTracksByboxId('ES2025')
})
function flipCard(track) {
track.isFaceUp = !track.isFaceUp
}
function onDragEnd() {
drag.value = false
// Ici vous pouvez ajouter une logique supplémentaire après le drop si nécessaire
}
</script>
<style lang="scss" scoped>
.logo {
filter: drop-shadow(3px 3px 0 rgb(0 0 0 / 0.7));
}
.deck {
position: relative;
height: 80vh;
.draggable-container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
padding: 1rem;
min-height: 100%;
}
.draggable-item {
cursor: grab;
transition: transform 0.2s;
&:active {
cursor: grabbing;
}
&.sortable-ghost {
opacity: 0.5;
background: #c8ebfb;
border-radius: 1rem;
}
&.sortable-drag {
transform: rotate(2deg);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
}
.card {
z-index: 10;
position: relative;
}
/* noise tools */
$size: 130px;
$scale: 1.05;
$border-radius: calc($size / 2);
$grad-position: 100% 0;
$grad-start: 25%;
$grad-stop: 65%;
$duration: 3.5s;
$noise: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEEAAABBCAMAAAC5KTl3AAAAgVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtFS1lAAAAK3RSTlMWi3QSa1uQOKBWCTwcb6V4gWInTWYOqQSGfa6XLyszmyABlFFJXySxQ0BGn2PQBgAAC4NJREFUWMMV1kWO5UAQRdFk5kwzs/33v8Cunr7ZUehKAdaRUAse99ozDjF5BqswrPKm7btzJ2tRziN3rMYXC236humIV5Our7nHWnVdFOBojW2XVnkeu1IZHNJH5OPHj9TjgVxBGBwAAmp60WoA1gBBvg3XMFhxUQ4KuLqx0CritYZPPXinsOqB7I76+OHaZlPzLEcftrqOlOwjeXvuEuH6t6emkaofgVUDIb4fEZB6CmRAeFCTq11lxbAgUyx4rXkqlH9I4bTUDRRVD1xjbqb9HyUBn7rhtr1x+x9Y0e3BdX31/loYvZaLxqnjbRuokz+pPG7WebnSNKE3yE6Tka4aDEDMVYr6Neq126c+ZR2nzzm3yyiC7PGWG/1uueqZudrVGYNdsgOMDvt1cI8CXu63QIcPvYNY8z870WwYazTS7DqpDEknZqS0AFXObWUxTaw0q5pnHlq4oQImakpLfJkmErdvAfhsc7lod0DVT4tuob25C0tQjzdiFObCz7U7eaKGP3s6yQVgQ/y+q+nY6K5dfV75iXzcNlGIP38aj22sVwtWWKMRb7B5HoHPaBvI1Ve5TSXATi66vV6utxsV+aZNFu+93VvlrG/oj8Wp67YT8l+Oq6PjwdGatFm7SEAP13kE0y9CEcf9qhtEWCMIq5AGq71moEAI9vrmFcmO8+7ZyDnmRN/VUaFkM2ce8KuBGFzDMmY6myLfQGra2ofgHhbJRXuRDZ4H+HmliWBHXQ0ysLGfv6FetbxtxzRgIZWjIsGVFl5imPXeyvVyayNek+dSWzjXd4t310YBdaF8sXeKs481PjsXbAtIru2+wHbv3GVh3sQY6Dnu6pF3pZ714VYdDi9A5GkXR/6xgaZN/tpQ8wVV3zeBuB+njoBNE4wjc+uA523ysXGd/P2sntmOb3OdHNWP5OVrxD3eJHdtH8QVkEIAqCor3hReR96yqt6PkTQfenllooQ447h6tOrnnuzwA8fMpq+jqg1oW8fTYYIncAYpVeTvkEFr/khQSbjoE8ykx9049OkE5MQEO9lC24tT7DwThQgf4Fhf8nGgAo3GYaON3crODpOr2pu5dBABz69t7F5yJBBo+r6QJdeLDWEoO7r1tceR3haA7gc7eZrCvpxSXXeKpo4P+hRixo9DeOFbqQVjKyWfBg9pnrEZKzK7R437YTTwhfoySG/YOCt3fs4aXlU3FjKortqQ6XyXaD0+Y/8VoqpyU9TRW45eN4oBxAH8Y/jLnNXfELJW+/p/MgO9Z+mBli2qqAP7dV/Arc2+YZRZwtBW8/p32y5ZsEuCS4O5AAgfR7Dde7zhiGfgvurQkfAXIrUG61rmxc2EZo18ph4vaWZI+QM0JdsbNlBJlPlwf9uguujQJy0j7TgTHdtRnjybTg55Hkk9S6l2rpYahumSewKHVosa1bh2Y6r9JGkdKvIDN/eeAwScrfjoLkCxWJuFZQ53FNP5w9XbQd1HhgHcVB/0fATG3sUUid1RTfc2+7pZVKldFSsaEK0v4k90tapQOk2HIbMhaJQtrUEL5+3sDanh8sOpbYRoQoqXWu6SQcUTQL9jzOrXNPWCJwXge4U7tlU1hkF012cAmvp8llQxf1IEMcw14pURxVOWATz4ITnYQjuF+vDXg5hgoiqXzO6mS91FQUBheURHIJxUeU1i3P0WOMpsm7vFYk0JJi/Ev+X3FwYD69cARPuP5GIc0PxoAFjcLRbNur0iMTrQmBBNYJ2ngU4x7SWfdTRl52Bqv7LmYW3C1CyTCPTHeWWIAM/Whm32COHsaj+2UQ739XB9t6NV0o9E9b7CW3XNiXzi9e0KiE+3rntukdIDBWrU2jsfQWuyFJRANxq8StHVv1JPy2C3Byco7qdNbASrnNXZ8G0L/Wp/pif4Ai9aEZ9Bb+TRx+REBdGlkF/s0dUdMSMr+6YCbuGxqPWdzcdqutvqkBzCksFcwAtjf55TeuH79M6AQa7r5PLeXxMFIlQKrXP9VJ275WGX+ptpf+tvTDBsecPnYQAlAWrVbRVJ7K2pRHwIjtSpbX96Y/lbKk6ZWXlBmh15r8yAWQsYxXgBOXYMAfHnUXF+rDqnB8bXDRtAn7bCziIqetSboK3NexMePvsCRLvmsoREA+kH8j4HWFpnNEaWgOmR7xyXHfTaz3slHc/YA6H6tl/L8d5tPcIwwD0tjvRaq3Y5BmYBSDClpv0VIX4s8D0XK3sPdpAb94HjPLkgboEz9EdZATW6ZdcmQvtKUwoWw+nAVKA7IcdY1UHnvNnIBplKci+knzewLz5/GGnzkGuuGky+0LTjtGBGR85EQICDqKChnm5pH3Z44nnWAk1YRdyu3g7QoFZ0h8jkr2ffjKmi+Qvsp+9GvNGZHmgW+YQAGUw7PPt8IPKbdy432vhKtRJjKWcSqq7helj81o3nfmaxVZ7Sqie8OOBk9WsyTD/ab7fQ5aWwQeJvnH6+ayo4IdIkOSBJjzXkgr+1TPhAx1AXDsxtCCj3TzQTLA1p782f7a8vdgPfwwrXmZxxbqo2h+6Zlo6mcMY4V7cFBOLm17VCvx9Qa2tAnkxEB+KYyQgbgAAnmNDOdOO6y2Cb+lke1MWQc9o+EMdQf7ubIG3Ek8GZ4k1PtGjbhwgOMPp5Em59JMVk/jU8/aF73Xcrd3UBNZyueQu0/xz2aGtZT8CRziOax2BWFXaeDzgZNV7oRtUzFoijoETf3xkAFFk3OMb7SgPh5wxU1+MygDIp9gZChH2qEcpgLh8pBIK90PXT1ZSU+ZExFK4Vm4GL/J7+K13lS5dQkW4HQwl6GX4yLqu8GhGWS2k75yel5IZIfFNdAL0NpKr2N5dQesBnxa42DLgJd6agS1jJsp1mO1dip7PU4P6diLLoTsZ4m3Q0QweiqeFfIGPLgF6v6mSVv6xe85VBD/1Mpe3AurRbcJ9SEo8NszNVy8rOCEexyIFcJRvYAlI/wk2I7r3p60FFLQXoH2q9xri/m41svRPbW0/EnPn2DWsmk0IiPpB60aa3+hiFfWuC8ZvWKEd9LxAk3HcOof6d77RewPaPsGw5lQAHcZN2vx1448u9pLfMLGQ3BSRRjBzRhKt7HcCw/7aqjtCDs5q76b4ZGphxN2th1WeXYlfnozX3ebKtX4Te11hf1tZP1diiGjIDAB1cR4Sb9rcFPC/nBARjlgDxd+tCBb1t91j71xJcgGjT3g/dUFnXXNiDrxkyoHANPk58ACPUa42hj8tgGrhiXOCmygxFZBiT2wyAJTDJ4wJEPmp6JIrDaSWYNqv4xH2wwdSTGYb3E0pXnS39nmLUsqoVZxzSoegqzd0o06wdbTXsaHGL+IF4JtIcXddTcD/dCd8hVf+fWPSV553kjMmMEULLS8HcgmptDO955dLGX78PjiDA6IsTHPm5IA6bc5ha0gaGkoEttXuxU11B2dOJ65/Q08tEF1+Y9cr2Nh/VECfQ33GyvR/gsdN1LuIeLpKMCAF2yRr769g9/4aJLZNRI71m2S91+Kp+Q0zubTcxoG2/6gm1Q79wkMj2XNO2ui7nWw8ULtu27CCvqTGX2PffD+xcwgh/TrOKvGZMM5jRFGDTn4NO/lwnDR/GY/waDZtkWDUPI0O8ztcFVqp6r2ZW+2bvkJ3raptYagFqu95VdIaml2CIp6CKets34x+fH2C+zH4cVFO7vj+6k2FU39PtRhWluYeZ3gDz1TLB9K2v7SD9gJU1qDxoRDrAWcrFGLyndhdtd0505+gEP79adK8fmFCWNYC+ahzVNcRH79E8dA1iqX/N0qq22xcOc20ALxLDspEj4QCFBQMgaIwoKbxr0Bd7Sbws6GiRK6tqoPfpiCle23axejRLyO1I+ahsEpWrzT5ZsCyS5RcY9jMfENFxSnhKsrfW8JHH6/rdQUMfmQPT3Uz9gY0C/pu1yuCnrPUvio0a1qMEosA/EwIzzid7cqsAAAAASUVORK5CYII=');
@mixin dithered-gradient($position, $start, $stop, $color) {
background: radial-gradient(circle at $position, transparent $start, $color $stop);
mask: $noise, radial-gradient(circle at $position, transparent $start, #000 ($stop + 10%));
}
&::before,
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
z-index: 0;
display: block;
width: 100%;
height: 100%;
}
&::before {
@include dithered-gradient(50%, 30%, 60%, #6cc8ff);
}
&::after {
mask-image:
$noise, linear-gradient(45deg, #000 0%, transparent 25%, transparent 75%, #000 100%);
background: linear-gradient(45deg, #6d6dff 10%, transparent 25%, transparent 75%, #6af789 90%);
}
}
</style>

View File

@@ -4,17 +4,19 @@
<div class="flex flex-col-reverse bg-gradient-to-r from-primary to-primary-dark">
<div class="mt-8 flex flex-wrap justify-center">
<!-- <box :box="box" /> -->
<div class="devtool absolute right-4 text-white bg-black rounded-2xl px-4 py-2">
<div class="devtool absolute right-4 text-white bg-black rounded-2xl px-4 py-2">
<!-- <button @click="currentPosition = boxPositions.side">side</button>
<button @click="currentPosition = boxPositions.front">front</button>
<button @click="currentPosition = boxPositions.back">back</button> -->
<div class="w-full block">
<input class="w-1/2" type="color" name="color1" id="color1" v-model="box.color1">
<input class="w-1/2" type="color" name="color1" id="color1" v-model="box.color2">
<div class="block w-full h-32" :style="{
background: `linear-gradient(to top, ${box.color1}, ${box.color2})`
}">
</div>
<input class="w-1/2" type="color" name="color1" id="color1" v-model="box.color1" />
<input class="w-1/2" type="color" name="color1" id="color1" v-model="box.color2" />
<div
class="block w-full h-32"
:style="{
background: `linear-gradient(to top, ${box.color1}, ${box.color2})`
}"
></div>
<!-- <label class="block">
size: {{ size }}
<input v-model.number="size" type="range" step="1" min="1" max="14">
@@ -62,15 +64,15 @@ const track = ref<Track>({
title: 'The grinding wheel',
artist: {
id: 0,
name: 'L\'Efondras',
name: "L'Efondras",
url: '',
coverId: '0024705317',
coverId: '0024705317'
},
start: 0,
url: 'https://arakirecords.bandcamp.com/track/the-grinding-wheel',
coverId: 'a3236746052',
type: 'compilation',
type: 'compilation'
})
//from-slate-800 to-zinc-900
//from-slate-800 to-zinc-900
</script>

View File

@@ -26,7 +26,7 @@ onMounted(async () => {
if (track) {
// Open the box containing this track without changing global UI flow/animations
uiStore.selectBox(track.boxId)
playerStore.playTrack(track)
playerStore.loadTrack(track)
}
}
})