Compare commits
	
		
			3 Commits
		
	
	
		
			8ebda83a22
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | deb15b3ea1 | ||
|  | 9771c799f2 | ||
|  | 25d56ec4ef | 
							
								
								
									
										2
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
								
							| @@ -12,7 +12,7 @@ jobs: | ||||
|       - name: Prepare and build app | ||||
|         run: | | ||||
|           REPO_NAME="${GITHUB_REPOSITORY##*/}" | ||||
|           APP_DIR="/var/docker-web/apps/${REPO_NAME}" | ||||
|           APP_DIR="/var/docker-web/store/apps/${REPO_NAME}" | ||||
|           bash /var/docker-web/src/cli.sh down "${REPO_NAME}" | ||||
|           rm -rf "$APP_DIR" | ||||
|           mkdir "$APP_DIR" | ||||
|   | ||||
							
								
								
									
										10
									
								
								app/app.vue
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								app/app.vue
									
									
									
									
									
								
							| @@ -1,17 +1,13 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <NuxtRouteAnnouncer /> | ||||
|     <NuxtPage /> | ||||
|     <SearchModal /> | ||||
|     <Loader /> | ||||
|     <Player /> | ||||
|     <NuxtLayout> | ||||
|       <NuxtPage /> | ||||
|     </NuxtLayout> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import SearchModal from '~/components/SearchModal.vue' | ||||
| import Player from '~/components/player.vue' | ||||
| import Loader from '~/components/Loader.vue' | ||||
| import { useUiStore } from '~/store/ui' | ||||
| import { usePlayerStore } from '~/store/player' | ||||
| import { watch, computed } from 'vue' | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <div class="flex flex-col-reverse mt-16" :class="!!playerStore.currentTrack ? 'mb-36' : 'mb-16'"> | ||||
|   <div class="boxes"> | ||||
|     <box v-for="(box, i) in dataStore.boxes.slice()" :key="box.id" :tabindex="dataStore.boxes.length - i" :box="box" | ||||
|       @click="onBoxClick(box)" class="text-center" :class="box.state" :id="box.id"> | ||||
|       <button @click.stop="playSelectedBox(box)" v-if="box.state === 'box-selected'" | ||||
| @@ -12,7 +12,6 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { ref, onMounted } from 'vue' | ||||
| import type { Box } from '~~/types/types' | ||||
| import { useDataStore } from '~/store/data' | ||||
| import { usePlayerStore } from '~/store/player' | ||||
| @@ -40,32 +39,10 @@ function onBoxClick(b: Box) { | ||||
| function playSelectedBox(b: Box) { | ||||
|   playerStore.playBox(b) | ||||
| } | ||||
|  | ||||
| function KeyboardAction(e: KeyboardEvent) { | ||||
|   switch (e.key) { | ||||
|     case 'Escape': | ||||
|       uiStore.closeBox() | ||||
|       break; | ||||
|     case 'ArrowUp': | ||||
|       break; | ||||
|  | ||||
|     case 'Enter': | ||||
|       if (document.activeElement?.id) { | ||||
|         openBox(document.activeElement.id) | ||||
|       } | ||||
|       break; | ||||
|     case 'ArrowDown': | ||||
|       break; | ||||
|     case 'ArrowLeft': | ||||
|       break; | ||||
|     case 'ArrowRight': | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| onMounted(async () => { | ||||
|   window.addEventListener('keydown', KeyboardAction) | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style lang="scss"> | ||||
| .boxes { | ||||
|   @apply flex flex-col-reverse; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| <template> | ||||
|   <article @click="() => playerStore.playTrack(props.track).catch(err => console.error(err))" | ||||
|     class="card flip-card isplaying w-56 h-80" :class="isFaceUp ? 'face-up' : 'face-down'"> | ||||
|   <article class="card isplaying w-56 h-80" :class="isFaceUp ? 'face-up' : 'face-down'"> | ||||
|     <div class="flip-inner"> | ||||
|       <!-- Face-Up --> | ||||
|       <main | ||||
|         class="flip-front backdrop-blur-sm border-1 -mt-12 z-10 card w-56 h-80 p-3 bg-opacity-40 hover:bg-opacity-80 hover:shadow-xl transition-all bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden"> | ||||
|         class="face-up backdrop-blur-sm border-1 z-10 card w-56 h-80 p-3 bg-opacity-40 hover:bg-opacity-80 hover:shadow-xl transition-all bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden"> | ||||
|         <div class="flex items-center justify-center size-7 absolute top-7 right-7" v-if="isPlaylistTrack"> | ||||
|           <div class="suit text-7xl absolute" | ||||
|             :class="[isRedCard ? 'text-red-600' : 'text-slate-800', props.track.card?.suit]"> | ||||
| @@ -37,7 +36,7 @@ | ||||
|  | ||||
|       <!-- Face-Down --> | ||||
|       <footer | ||||
|         class="flip-back 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"> | ||||
|         class="face-down backdrop-blur-sm z-10 card w-56 h-80 p-3 bg-opacity-10 bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden"> | ||||
|         <div class="h-full flex p-16 text-center bg-slate-800 rounded-xl"> | ||||
|           <img src="/favicon.svg" /> | ||||
|           <div class="label label--id" v-if="isOrder"> | ||||
| @@ -52,12 +51,10 @@ | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import type { Track } from '~~/types/types' | ||||
| import { usePlayerStore } from '~/store/player' | ||||
|  | ||||
| const props = withDefaults(defineProps<{ track: Track; isFaceUp?: boolean }>(), { | ||||
|   isFaceUp: false | ||||
| }) | ||||
| 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') | ||||
| @@ -75,7 +72,7 @@ const coverUrl = props.track.coverId.startsWith('http') | ||||
| } | ||||
|  | ||||
| /* Flip effect */ | ||||
| .flip-card { | ||||
| .card { | ||||
|   perspective: 1000px; | ||||
|  | ||||
|   .flip-inner { | ||||
| @@ -94,8 +91,8 @@ const coverUrl = props.track.coverId.startsWith('http') | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .flip-front, | ||||
|   .flip-back { | ||||
|   .face-down, | ||||
|   .face-up { | ||||
|     position: absolute; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
| @@ -103,11 +100,11 @@ const coverUrl = props.track.coverId.startsWith('http') | ||||
|     will-change: transform; | ||||
|   } | ||||
|  | ||||
|   .flip-front { | ||||
|   .face-up { | ||||
|     transform: rotateY(0deg); | ||||
|   } | ||||
|  | ||||
|   .flip-back { | ||||
|   .face-down { | ||||
|     transform: rotateY(180deg); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="z-50 tools fixed top-0 -left-0 hidden"> | ||||
|       <button @click="setDisplay('pile')">pile</button> | ||||
|       <button @click="setDisplay('plateau')">plateau</button> | ||||
|       <button @click="setDisplay('holdem')">holdem</button> | ||||
|     <div class="deck-order"> | ||||
|       <button @click="orderDeck('pile')">pile</button> | ||||
|       <button @click="orderDeck('plateau')">plateau</button> | ||||
|       <button @click="orderDeck('holdem')">holdem</button> | ||||
|     </div> | ||||
|     <div ref="deck" class="deck flex flex-wrap justify-center gap-4"> | ||||
|       <card v-for="(track, i) in tracks" :key="track.id" :track="track" tabindex="i" /> | ||||
|       <card v-for="(track, i) in tracks" :key="track.id" :track="track" tabindex="i" | ||||
|         @click="() => playerStore.playTrack(track).catch(err => console.error(err))" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -15,6 +16,7 @@ | ||||
| import { computed, ref } from 'vue' | ||||
| import { useDataStore } from '~/store/data' | ||||
| import type { Box } from '~~/types/types' | ||||
| import { usePlayerStore } from '~/store/player' | ||||
|  | ||||
| const props = defineProps<{ | ||||
|   box: Box | ||||
| @@ -22,10 +24,11 @@ const props = defineProps<{ | ||||
| const dataStore = useDataStore() | ||||
| const deck = ref() | ||||
| const tracks = computed(() => dataStore.getTracksByboxId(props.box.id)) | ||||
| const playerStore = usePlayerStore() | ||||
|  | ||||
| function setDisplay(displayMode) { | ||||
| function orderDeck(order: string) { | ||||
|   deck.value.classList.remove('pile', 'plateau', 'holdem') | ||||
|   deck.value.classList.add(displayMode) | ||||
|   deck.value.classList.add(order) | ||||
| } | ||||
| </script> | ||||
|  | ||||
|   | ||||
							
								
								
									
										32
									
								
								app/layouts/default.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/layouts/default.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| <template> | ||||
|   <div class="w-full min-h-screen flex flex-col items-center bg-gray-50"> | ||||
|     <!-- Header avec logo --> | ||||
|     <header class="w-full py-4 px-6 bg-white shadow-sm"> | ||||
|       <div class="max-w-7xl mx-auto w-full flex justify-center"> | ||||
|         <div @click="navigateToHome" class="cursor-pointer inline-block"> | ||||
|           <logo /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </header> | ||||
|  | ||||
|     <!-- Contenu principal --> | ||||
|     <main class="w-full max-w-7xl flex-1 p-6"> | ||||
|       <slot /> | ||||
|     </main> | ||||
|  | ||||
|     <!-- Player de musique fixe en bas --> | ||||
|     <SearchModal /> | ||||
|     <Loader /> | ||||
|     <Player class="w-full border-t border-gray-200" /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { useRouter } from 'vue-router' | ||||
|  | ||||
| const router = useRouter() | ||||
|  | ||||
| const navigateToHome = () => { | ||||
|   router.push('/') | ||||
| } | ||||
| </script> | ||||
| @@ -1,12 +1,5 @@ | ||||
| <template> | ||||
|   <div class="w-full flex flex-col items-center"> | ||||
|     <div @click="uiStore.closeBox()" class="cursor-pointer"> | ||||
|       <logo /> | ||||
|     </div> | ||||
|     <main> | ||||
|       <boxes /> | ||||
|     </main> | ||||
|   </div> | ||||
|   <boxes /> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| @@ -14,6 +7,12 @@ 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({ | ||||
|   layout: 'default' | ||||
| }) | ||||
|  | ||||
| const uiStore = useUiStore() | ||||
| const dataStore = useDataStore() | ||||
| @@ -24,6 +23,13 @@ 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) | ||||
|     } | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|   | ||||
| @@ -1,17 +1,16 @@ | ||||
| <template> | ||||
|   <div class="w-full flex flex-col items-center"> | ||||
|     <div @click="uiStore.closeBox()" class="cursor-pointer"> | ||||
|       <logo /> | ||||
|     </div> | ||||
|     <main> | ||||
|       <boxes /> | ||||
|     </main> | ||||
|   </div> | ||||
|   <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 () => { | ||||
|   | ||||
| @@ -1,12 +1,5 @@ | ||||
| <template> | ||||
|   <div class="w-full flex flex-col items-center"> | ||||
|     <div @click="uiStore.closeBox()" class="cursor-pointer"> | ||||
|       <logo /> | ||||
|     </div> | ||||
|     <main> | ||||
|       <boxes /> | ||||
|     </main> | ||||
|   </div> | ||||
|   <boxes /> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| @@ -14,6 +7,11 @@ import { useUiStore } from '~/store/ui' | ||||
| import { useDataStore } from '~/store/data' | ||||
| import { usePlayerStore } from '~/store/player' | ||||
|  | ||||
| // Configuration du layout | ||||
| definePageMeta({ | ||||
|   layout: 'default' | ||||
| }) | ||||
|  | ||||
| const uiStore = useUiStore() | ||||
| const dataStore = useDataStore() | ||||
| const playerStore = usePlayerStore() | ||||
|   | ||||
| @@ -1,17 +0,0 @@ | ||||
| import { useUiStore } from '~/store/ui' | ||||
| export default defineNuxtPlugin((nuxtApp) => { | ||||
|   const ui = useUiStore() | ||||
|   const isMobile = nuxtApp.$isMobile as boolean | undefined | ||||
|  | ||||
|   const onKeyDown = (e: KeyboardEvent) => { | ||||
|     if ((e.metaKey || e.ctrlKey) && (e.key === 'f' || e.key === 'F')) { | ||||
|       if (isMobile) return | ||||
|       e.preventDefault() | ||||
|       if (!ui.showSearch) ui.openSearch() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (process.client) { | ||||
|     window.addEventListener('keydown', onKeyDown) | ||||
|   } | ||||
| }) | ||||
							
								
								
									
										106
									
								
								app/plugins/shortcut.client.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								app/plugins/shortcut.client.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| import { useUiStore } from '~/store/ui' | ||||
| import { usePlayerStore } from '~/store/player' | ||||
| import { onBeforeUnmount, onUnmounted, onMounted } from 'vue' | ||||
| import { useRoute } from 'vue-router' | ||||
| import { useDataStore } from '~/store/data' | ||||
|  | ||||
| export default defineNuxtPlugin((nuxtApp) => { | ||||
|   // Ne s'exécuter que côté client | ||||
|   if (process.server) return | ||||
|  | ||||
|   const ui = useUiStore() | ||||
|   const player = usePlayerStore() | ||||
|   const route = useRoute() | ||||
|   const dataStore = useDataStore() | ||||
|  | ||||
|   function isInputElement(target: EventTarget | null): boolean { | ||||
|     return ( | ||||
|       target instanceof HTMLInputElement || | ||||
|       target instanceof HTMLTextAreaElement || | ||||
|       target instanceof HTMLSelectElement || | ||||
|       (target instanceof HTMLElement && target.isContentEditable) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   function handleKeyDown(e: KeyboardEvent) { | ||||
|     console.log('Key pressed:', e.code, 'Key:', e.key, 'Target:', e.target) | ||||
|  | ||||
|     // Ne pas interférer avec les champs de formulaire | ||||
|     if (isInputElement(e.target as HTMLElement)) { | ||||
|       console.log('Input element, ignoring key') | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // Gestion du raccourci de recherche (Ctrl+F / Cmd+F) | ||||
|     if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'f') { | ||||
|       e.preventDefault() | ||||
|       if (!ui.showSearch) { | ||||
|         ui.openSearch() | ||||
|       } | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // Gestion des autres touches uniquement si pas de touche de contrôle enfoncée | ||||
|     if (e.ctrlKey || e.altKey || e.metaKey) { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     switch (e.code) { | ||||
|       // Gestion de la barre d'espace pour play/pause | ||||
|       case 'Space': | ||||
|         console.log('Space pressed, toggling play/pause') | ||||
|         e.preventDefault() | ||||
|         e.stopPropagation() | ||||
|         if (player.currentTrack) { | ||||
|           console.log('Toggling play state') | ||||
|           player.togglePlay() | ||||
|         } else { | ||||
|           console.log('No current track to play/pause') | ||||
|         } | ||||
|         return false | ||||
|  | ||||
|       // Gestion de la touche Échap pour fermer la boîte | ||||
|       case 'Escape': | ||||
|         e.preventDefault() | ||||
|         ui.closeBox() | ||||
|         break | ||||
|  | ||||
|       // Gestion de la touche Entrée pour ouvrir une boîte | ||||
|       case 'Enter': | ||||
|         if (document.activeElement?.id) { | ||||
|           e.preventDefault() | ||||
|           ui.selectBox(document.activeElement.id) | ||||
|           window.scrollTo({ top: 0, behavior: 'smooth' }) | ||||
|         } | ||||
|         break | ||||
|  | ||||
|       // Gestion des touches fléchées (à implémenter si nécessaire) | ||||
|       case 'ArrowUp': | ||||
|       case 'ArrowDown': | ||||
|       case 'ArrowLeft': | ||||
|       case 'ArrowRight': | ||||
|         // Implémentation future de la navigation au clavier | ||||
|         break | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Ajout de l'écouteur d'événements avec capture pour intercepter l'événement plus tôt | ||||
|   window.addEventListener('keydown', handleKeyDown, { capture: true, passive: false }) | ||||
|   console.log('Keyboard event listener added') | ||||
|  | ||||
|   // Nettoyage lors de la destruction | ||||
|   const stop = () => { | ||||
|     window.removeEventListener('keydown', handleKeyDown) | ||||
|   } | ||||
|  | ||||
|   // Nettoyage quand le composant est démonté | ||||
|   onUnmounted(stop) | ||||
|  | ||||
|   // Nettoyage quand la page est déchargée | ||||
|   if (process.client) { | ||||
|     window.addEventListener('unload', stop) | ||||
|     onBeforeUnmount(() => { | ||||
|       window.removeEventListener('unload', stop) | ||||
|     }) | ||||
|   } | ||||
| }) | ||||
		Reference in New Issue
	
	Block a user