compose elt v0.1
This commit is contained in:
@@ -11,10 +11,6 @@
|
||||
<deckPlaylist :box="box" class="box-page" v-if="box.type === 'playlist'" @click.stop="" />
|
||||
</template>
|
||||
</box>
|
||||
<nav class="fixed top-0">
|
||||
<a class="button m-4 bg-esyellow" href="/draggable">Draggable</a>
|
||||
<a class="button m-4 bg-esyellow" href="/mix">Mix</a>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,36 +1,49 @@
|
||||
<template>
|
||||
<div class="bucket" :class="{ 'drag-over': isDragOver }" @dragenter.prevent="onDragEnter"
|
||||
@dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
|
||||
<div class="bucket-content">
|
||||
<div v-if="cards.length === 0" class="bucket-empty">
|
||||
Drop cards here
|
||||
</div>
|
||||
<div v-else class="bucket-cards">
|
||||
<Card v-for="(card, index) in cards" :key="index" :track="card" class="bucket-card" isFaceUp />
|
||||
</div>
|
||||
<div v-if="tracks.length === 0" class="bucket-empty">
|
||||
Drop cards here
|
||||
</div>
|
||||
<draggable v-else v-model="tracks" item-key="id" class="bucket-cards" @start="onDragStart" @end="onDragEnd"
|
||||
:touch-start-threshold="50">
|
||||
<template #item="{ element: track }">
|
||||
<card :track="track" tabindex="0" is-face-up class="bucket-card" @click="flipCard(track)" />
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import { useDataStore } from '~/store/data'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: any[]
|
||||
boxId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'card-dropped'])
|
||||
|
||||
const dataStore = useDataStore()
|
||||
const isDragOver = ref(false)
|
||||
const cards = ref<any[]>(props.modelValue || [])
|
||||
const drag = ref(false)
|
||||
const tracks = ref<any[]>(props.modelValue || [])
|
||||
const touchedCard = ref<any>(null)
|
||||
const touchStartPos = ref<{ x: number, y: number } | null>(null)
|
||||
|
||||
// Mettre à jour les cartes quand la prop change
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue) {
|
||||
cards.value = [...newValue]
|
||||
tracks.value = [...newValue]
|
||||
}
|
||||
})
|
||||
|
||||
if (props.boxId) {
|
||||
onMounted(async () => {
|
||||
await dataStore.loadData()
|
||||
tracks.value = dataStore.getTracksByboxId(props.boxId)
|
||||
})
|
||||
}
|
||||
|
||||
// Gestion du drag and drop desktop
|
||||
const onDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = true
|
||||
@@ -45,76 +58,79 @@ const onDragLeave = () => {
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const onDragStart = () => {
|
||||
drag.value = true
|
||||
}
|
||||
|
||||
const onDragEnd = () => {
|
||||
drag.value = false
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const onDrop = (e: DragEvent) => {
|
||||
isDragOver.value = false
|
||||
|
||||
// Récupérer les données de la carte depuis l'événement de glisser
|
||||
const cardData = e.dataTransfer?.getData('application/json')
|
||||
if (cardData) {
|
||||
try {
|
||||
const card = JSON.parse(cardData)
|
||||
cards.value = [...cards.value, card]
|
||||
emit('update:modelValue', cards.value)
|
||||
emit('card-dropped', card)
|
||||
const track = JSON.parse(cardData)
|
||||
if (!tracks.value.some(t => t.id === track.id)) {
|
||||
tracks.value.push(track)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Invalid card data', e)
|
||||
console.error('Erreur lors du traitement de la carte déposée', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const flipCard = (track: any) => {
|
||||
track.isFaceUp = !track.isFaceUp
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bucket {
|
||||
width: 120px;
|
||||
min-height: 160px;
|
||||
min-height: 200px;
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f9f9f9;
|
||||
transition: all 0.2s ease;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.bucket.drag-over {
|
||||
border-color: #4f46e5;
|
||||
background-color: #eef2ff;
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.bucket-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
border-color: #4CAF50;
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
.bucket-empty {
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.bucket-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bucket-card {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: transform 0.2s;
|
||||
cursor: move;
|
||||
touch-action: none;
|
||||
/* Important pour le touch */
|
||||
}
|
||||
|
||||
.bucket-card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.bucket-card:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -71,22 +71,25 @@ import type { Track } from '~~/types/types'
|
||||
import { usePlayerStore } from '~/store/player'
|
||||
import { useDataStore } from '~/store/data';
|
||||
|
||||
const props = withDefaults(defineProps<{ track: Track; isFaceUp?: boolean }>(), {
|
||||
isFaceUp: false
|
||||
const props = withDefaults(defineProps<{
|
||||
track?: Track;
|
||||
isFaceUp?: boolean;
|
||||
}>(), {
|
||||
track: () => {
|
||||
const dataStore = useDataStore();
|
||||
return dataStore.getRandomPlaylistTrack() || {} as Track;
|
||||
},
|
||||
isFaceUp: true,
|
||||
})
|
||||
|
||||
const playerStore = usePlayerStore()
|
||||
const isManifesto = computed(() => props.track.boxId.startsWith('ES00'))
|
||||
const isOrder = computed(() => props.track.order && !isManifesto)
|
||||
const isPlaylistTrack = computed(() => props.track.type === 'playlist')
|
||||
const isRedCard = computed(() => props.track.card?.suit === '♥' || props.track.card?.suit === '♦')
|
||||
const isRedCard = computed(() => (props.track.card?.suit === '♥' || props.track.card?.suit === '♦'))
|
||||
const dataStore = useDataStore()
|
||||
const cardColor = computed(() => dataStore.getYearColor(props.track.year || 0))
|
||||
const coverUrl = computed(() => {
|
||||
if (!props.track.coverId) return ''
|
||||
return props.track.coverId.startsWith('http')
|
||||
? props.track.coverId
|
||||
: `https://f4.bcbits.com/img/${props.track.coverId}_4.jpg`
|
||||
})
|
||||
const coverUrl = computed(() => props.track.coverId || '/card-dock.svg')
|
||||
|
||||
const dragStart = (event: DragEvent) => {
|
||||
if (event.dataTransfer) {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<form
|
||||
class="h-screen flex justify-center items-center flex-col absolute top-0 left-1/2 -translate-x-1/2"
|
||||
>
|
||||
<label for="email" class="block text-xl"> be notify when's evilSpins open : </label>
|
||||
<div>
|
||||
<input id="email" type="email" name="" placeholder="email" >
|
||||
<button>ok</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="platine relative">
|
||||
<div class="disc" :style="{ 'background-image': `url(${track?.coverId})` }" ref="discRef" id="disc">
|
||||
<div class="platine" :class="{ 'drag-over': isDragOver }" @dragenter.prevent="onDragEnter"
|
||||
@dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
|
||||
<div class="disc" ref="discRef" :style="'background-image: url(' + coverUrl + ')'" id="disc">
|
||||
<div
|
||||
class="bobine bg-slate-800 bg-opacity-50 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full"
|
||||
:style="{ height: progressPercentage + '%', width: progressPercentage + '%' }">
|
||||
@@ -26,14 +27,19 @@ import Disc from '@/platine-tools/disc'
|
||||
import Sampler from '@/platine-tools/sampler'
|
||||
import type { Track } from '~~/types/types'
|
||||
|
||||
const props = withDefaults(defineProps<{ track: Track | undefined }>(), {})
|
||||
const props = withDefaults(defineProps<{ track?: Track }>(), {})
|
||||
const discRef = ref<HTMLElement>()
|
||||
const currentTurns = ref(0)
|
||||
const totalTurns = ref(0)
|
||||
const progressPercentage = ref(0)
|
||||
const isPlaying = ref(false)
|
||||
const isLoadingTrack = ref(false)
|
||||
const drag = ref(false)
|
||||
const isDragOver = ref(false)
|
||||
const isFirstDrag = ref(true)
|
||||
const isLoadingTrack = ref(false)
|
||||
const isPlaying = ref(false)
|
||||
|
||||
const coverUrl = computed(() => props.track?.coverId || '/card-dock.svg')
|
||||
|
||||
let disc: Disc | null = null
|
||||
let sampler: Sampler | null = null
|
||||
|
||||
@@ -79,12 +85,12 @@ const handleRewind = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadTrack = async () => {
|
||||
if (!sampler || !props.track) return
|
||||
const loadTrack = async (track: Track) => {
|
||||
if (!sampler || !track) return
|
||||
|
||||
isLoadingTrack.value = true
|
||||
try {
|
||||
await sampler.loadTrack(props.track.url)
|
||||
await sampler.loadTrack(track.url)
|
||||
if (disc) {
|
||||
disc.setDuration(sampler.duration)
|
||||
updateTurns(disc)
|
||||
@@ -96,6 +102,44 @@ const loadTrack = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion du drag and drop
|
||||
const onDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const onDragLeave = () => {
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const onDragStart = () => {
|
||||
drag.value = true
|
||||
}
|
||||
|
||||
const onDragEnd = () => {
|
||||
drag.value = false
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const onDrop = (e: DragEvent) => {
|
||||
isDragOver.value = false
|
||||
const cardData = e.dataTransfer?.getData('application/json')
|
||||
if (cardData) {
|
||||
try {
|
||||
const newTrack = JSON.parse(cardData)
|
||||
if (newTrack && newTrack.url) {
|
||||
loadTrack(newTrack)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du traitement de la carte déposée', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
onMounted(async () => {
|
||||
disc = new Disc(discRef.value!)
|
||||
sampler = new Sampler()
|
||||
@@ -131,8 +175,10 @@ onMounted(async () => {
|
||||
|
||||
})
|
||||
|
||||
watch(() => props.track, () => {
|
||||
loadTrack()
|
||||
watch(() => props.track, (newTrack) => {
|
||||
if (newTrack) {
|
||||
loadTrack(newTrack)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -149,12 +195,15 @@ onUnmounted(() => {
|
||||
<style lang="scss">
|
||||
.platine {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.disc {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
aspect-ratio: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -162,6 +211,11 @@ onUnmounted(() => {
|
||||
cursor: grab;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.dragoOver & {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
}
|
||||
|
||||
.disc.is-scratching {
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
<template>
|
||||
<div class="deck-playlist">
|
||||
<div class="flex flex-col fixed right-0 top-0 z-50">
|
||||
<button @click="closeDatBox" v-if="uiStore.isBoxSelected"
|
||||
class="px-4 py-2 text-black hover:text-black bg-esyellow transition-colors z-50" aria-label="close the box">
|
||||
close
|
||||
</button>
|
||||
<Bucket v-model="bucketCards" @card-dropped="onCardDropped" />
|
||||
</div>
|
||||
<div class="controls flex justify-center z-50 relative">
|
||||
<button class="px-4 py-2 text-black hover:text-black bg-esyellow transition-colors" @click="toggleAllCards">
|
||||
{{ allCardsRevealed ? 'Hide All' : 'Reveal All' }}
|
||||
</button>
|
||||
<SearchInput @search="onSearch" />
|
||||
<SelectCardRank @change="onRankChange" />
|
||||
<SelectCardSuit @change="onSuitChange" />
|
||||
</div>
|
||||
<div ref="deck" class="deck flex flex-wrap justify-center gap-4" :class="{ 'pb-36': playerStore.currentTrack }">
|
||||
<card v-for="(track, i) in filteredTracks" :key="track.id" :track="track" :tabindex="i"
|
||||
:is-face-up="isCardRevealed(track.id)" />
|
||||
</div>
|
||||
<div class="flex flex-col fixed right-0 top-0 z-50">
|
||||
<button @click="closeDatBox" v-if="uiStore.isBoxSelected"
|
||||
class="px-4 py-2 text-black hover:text-black bg-esyellow transition-colors z-50" aria-label="close the box">
|
||||
close
|
||||
</button>
|
||||
</div>
|
||||
<div class="controls flex justify-center z-50 relative">
|
||||
<button class="px-4 py-2 text-black hover:text-black bg-esyellow transition-colors" @click="toggleAllCards">
|
||||
{{ allCardsRevealed ? 'Hide All' : 'Reveal All' }}
|
||||
</button>
|
||||
<SearchInput @search="onSearch" />
|
||||
<SelectCardRank @change="onRankChange" />
|
||||
<SelectCardSuit @change="onSuitChange" />
|
||||
</div>
|
||||
<div ref="deck" class="deck flex flex-wrap justify-center gap-4" :class="{ 'pb-36': playerStore.currentTrack }">
|
||||
<card v-for="(track, i) in filteredTracks" :key="track.id" :track="track" :tabindex="i"
|
||||
:is-face-up="isCardRevealed(track.id)" />
|
||||
</div>
|
||||
<div class="dock">
|
||||
<Bucket @card-dropped="onCardDropped" />
|
||||
<Platine />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useDataStore } from '~/store/data'
|
||||
import { useCardStore } from '~/store/card'
|
||||
import { usePlayerStore } from '~/store/player'
|
||||
@@ -45,7 +46,6 @@ const uiStore = useUiStore()
|
||||
const deck = ref()
|
||||
const tracks = computed(() => dataStore.getTracksByboxId(props.box.id))
|
||||
const filteredTracks = ref(tracks.value)
|
||||
const bucketCards = ref([])
|
||||
|
||||
// Variables réactives pour les filtres
|
||||
const selectedSuit = ref('')
|
||||
@@ -1,8 +1,3 @@
|
||||
<template>
|
||||
<div class="w-full flex flex-col items-center">
|
||||
<slot />
|
||||
<searchModal />
|
||||
<loader />
|
||||
<Player />
|
||||
</div>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full flex flex-col items-center">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
27
app/pages/boxes.vue
Normal file
27
app/pages/boxes.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<boxes />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUiStore } from '~/store/ui'
|
||||
import { useDataStore } from '~/store/data'
|
||||
|
||||
// Configuration du layout
|
||||
definePageMeta({
|
||||
layout: 'default'
|
||||
})
|
||||
|
||||
const uiStore = useUiStore()
|
||||
|
||||
onMounted(async () => {
|
||||
const dataStore = useDataStore()
|
||||
await dataStore.loadData()
|
||||
uiStore.listBoxes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.logo {
|
||||
filter: drop-shadow(3px 3px 0 rgb(0 0 0 / 0.7));
|
||||
}
|
||||
</style>
|
||||
@@ -1,169 +0,0 @@
|
||||
<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('ESPLAYLIST').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>
|
||||
@@ -1,27 +1,4 @@
|
||||
<template>
|
||||
<boxes />
|
||||
<card />
|
||||
<platine />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUiStore } from '~/store/ui'
|
||||
import { useDataStore } from '~/store/data'
|
||||
|
||||
// Configuration du layout
|
||||
definePageMeta({
|
||||
layout: 'default'
|
||||
})
|
||||
|
||||
const uiStore = useUiStore()
|
||||
|
||||
onMounted(async () => {
|
||||
const dataStore = useDataStore()
|
||||
await dataStore.loadData()
|
||||
uiStore.listBoxes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.logo {
|
||||
filter: drop-shadow(3px 3px 0 rgb(0 0 0 / 0.7));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap justify-center items-center h-screen">
|
||||
<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">
|
||||
<!-- <box :box="box" /> -->
|
||||
<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>
|
||||
<!-- <label class="block">
|
||||
size: {{ size }}
|
||||
<input v-model.number="size" type="range" step="1" min="1" max="14">
|
||||
</label> -->
|
||||
</div>
|
||||
<!-- <div>
|
||||
<label class="block">
|
||||
X: {{ currentPosition.x }}
|
||||
<input v-model.number="currentPosition.x" type="range" step="1" min="-180" max="180">
|
||||
</label>
|
||||
<label class="block">
|
||||
Y: {{ currentPosition.y }}
|
||||
<input v-model.number="currentPosition.y" type="range" step="1" min="-180" max="180">
|
||||
</label>
|
||||
<label class="block">
|
||||
Z: {{ currentPosition.z }}
|
||||
<input v-model.number="currentPosition.z" type="range" step="1" min="-180" max="180">
|
||||
</label>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<card :track="track" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Box, Track } from '~~/types/types'
|
||||
|
||||
const box = ref<Box>({
|
||||
id: 'ES00A',
|
||||
name: 'zero',
|
||||
duration: 2794,
|
||||
description: 'Zero is for manifesto',
|
||||
color1: '#ffffff',
|
||||
color2: '#48959d',
|
||||
color3: '#00ff00',
|
||||
type: 'compilation'
|
||||
})
|
||||
|
||||
const track = ref<Track>({
|
||||
id: 1,
|
||||
boxId: 'ES00A',
|
||||
title: 'The grinding wheel',
|
||||
artist: {
|
||||
id: 0,
|
||||
name: "L'Efondras",
|
||||
url: '',
|
||||
coverId: '0024705317'
|
||||
},
|
||||
start: 0,
|
||||
url: 'https://arakirecords.bandcamp.com/track/the-grinding-wheel',
|
||||
coverId: 'a3236746052',
|
||||
type: 'compilation'
|
||||
})
|
||||
|
||||
//from-slate-800 to-zinc-900
|
||||
</script>
|
||||
@@ -1,19 +1,9 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { Track } from '~~/types/types'
|
||||
interface CardPosition {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
type CardPositions = Record<string, Record<number, CardPosition>>
|
||||
|
||||
export const useCardStore = defineStore('card', {
|
||||
state: () => ({
|
||||
// Stocke les IDs des cartes déjà révélées
|
||||
revealedCards: new Set<number>(),
|
||||
// Stocke les positions personnalisées des cartes par box
|
||||
// Format: { [boxId]: { [trackId]: { x: number, y: number } } }
|
||||
cardPositions: {} as CardPositions
|
||||
revealedCards: new Set<number>()
|
||||
}),
|
||||
|
||||
actions: {
|
||||
@@ -33,20 +23,6 @@ export const useCardStore = defineStore('card', {
|
||||
return this.revealedCards.has(trackId)
|
||||
},
|
||||
|
||||
// Définir la position d'une carte dans une box
|
||||
setCardPosition(boxId: string, trackId: number, position: { x: number; y: number }) {
|
||||
if (!this.cardPositions[boxId]) {
|
||||
this.cardPositions[boxId] = {}
|
||||
}
|
||||
this.cardPositions[boxId][trackId] = position
|
||||
this.saveToLocalStorage()
|
||||
},
|
||||
|
||||
// Obtenir la position d'une carte dans une box
|
||||
getCardPosition(boxId: string, trackId: number): { x: number; y: number } | undefined {
|
||||
return this.cardPositions[boxId]?.[trackId]
|
||||
},
|
||||
|
||||
// Basculer l'état de révélation de toutes les cartes
|
||||
revealAllCards(tracks: Track[]) {
|
||||
tracks.forEach((track) => {
|
||||
@@ -69,8 +45,7 @@ export const useCardStore = defineStore('card', {
|
||||
localStorage.setItem(
|
||||
'cardStore',
|
||||
JSON.stringify({
|
||||
revealedCards: Array.from(this.revealedCards),
|
||||
cardPositions: this.cardPositions
|
||||
revealedCards: Array.from(this.revealedCards)
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -85,15 +60,12 @@ export const useCardStore = defineStore('card', {
|
||||
try {
|
||||
const saved = localStorage.getItem('cardStore')
|
||||
if (saved) {
|
||||
const { revealedCards, cardPositions } = JSON.parse(saved)
|
||||
const { revealedCards } = JSON.parse(saved)
|
||||
if (Array.isArray(revealedCards)) {
|
||||
this.revealedCards = new Set(
|
||||
revealedCards.filter((id): id is number => typeof id === 'number')
|
||||
)
|
||||
}
|
||||
if (cardPositions && typeof cardPositions === 'object') {
|
||||
this.cardPositions = cardPositions
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load card store from localStorage', e)
|
||||
@@ -111,11 +83,6 @@ export const useCardStore = defineStore('card', {
|
||||
// Getter pour la réactivité dans les templates
|
||||
isRevealed: (state) => (trackId: number) => {
|
||||
return state.revealedCards.has(trackId)
|
||||
},
|
||||
|
||||
// Obtenir toutes les positions des cartes d'une box
|
||||
getBoxCardPositions: (state) => (boxId: string) => {
|
||||
return state.cardPositions[boxId] || {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
1
public/card-dock.svg
Normal file
1
public/card-dock.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="224" xmlns="http://www.w3.org/2000/svg" height="320" id="screenshot-0fc9bf1a-468d-8062-8007-12c4a2492f68" viewBox="0 0 224 320" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-0fc9bf1a-468d-8062-8007-12c4a2492f68"><defs><clipPath id="frame-clip-0fc9bf1a-468d-8062-8007-12c4a2492f68-render-1" class="frame-clip frame-clip-def"><rect rx="0" ry="0" x="0" y="0" width="224" height="320" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"/></clipPath></defs><g class="frame-container-wrapper"><g class="frame-container-blur"><g class="frame-container-shadows"><g clip-path="url(#frame-clip-0fc9bf1a-468d-8062-8007-12c4a2492f68-render-1)" fill="none"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c4a2492f68"><rect width="224" height="320" class="frame-background" x="0" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(255, 255, 255); fill-opacity: 1;" ry="0" rx="0" y="0"/></g><g class="frame-children"><g id="shape-0fc9bf1a-468d-8062-8007-12c9d568a6f9"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c9d568a6f9"><rect rx="20" ry="20" x="0" y="0" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" width="224" height="320" style="fill: rgb(0, 0, 0); fill-opacity: 0.15;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c8693c73a0"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c8693c73a0"><ellipse cx="112" cy="160.00000000000003" rx="144" ry="144" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c85a924e31"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c85a924e31"><ellipse cx="112" cy="160.00000000000003" rx="112.5" ry="112.50000000000004" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c7cdce5e0a"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c7cdce5e0a"><ellipse cx="112" cy="160" rx="82" ry="82.00000000000003" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c820876e0e"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c820876e0e"><ellipse cx="112" cy="160" rx="51" ry="51" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g></g></g></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
Reference in New Issue
Block a user