feat: intégration de @tanstack/vue-query pour la gestion des requêtes dans l'application Vue, ajout de nouveaux composables pour les chapitres et les détails des mangas, et mise à jour du store pour une meilleure gestion des états de chargement et d'erreur

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-03-30 16:58:05 +02:00
parent fd2d3cd640
commit 71242433e6
10 changed files with 355 additions and 169 deletions

View File

@@ -1,81 +1,82 @@
import {defineStore} from 'pinia'; import { defineStore } from 'pinia';
import {ApiMangaRepository} from '../../infrastructure/api/apiMangaRepository'; import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
const mangaRepository = new ApiMangaRepository(); const mangaRepository = new ApiMangaRepository();
export const useMangaStore = defineStore('manga', { // Helper pour comparer la collection (peut être supprimé si non utilisé ailleurs)
state: () => ({ const deepCompare = (obj1, obj2) => {
// État pour la collection try {
collection: null, if (obj1 == null && obj2 == null) return true;
// État pour les détails if (obj1 == null || obj2 == null) return false;
currentManga: null, return JSON.stringify(obj1) === JSON.stringify(obj2);
chapters: [], } catch (e) {
loading: false, console.error("Erreur lors de la comparaison d'objets:", e);
error: null, return false;
isBackgroundLoading: false }
}), };
actions: { export const useMangaStore = defineStore('manga', {
// Actions pour la collection state: () => ({
async loadCollection() { // --- Collection State ---
if (this.loading) return; collection: null,
loadingCollection: false,
this.loading = true; errorCollection: null,
this.error = null; isBackgroundLoadingCollection: false,
try { // --- Selected Manga State ---
this.collection = await mangaRepository.getCollection(); // Gardé pour savoir quel manga est sélectionné dans l'UI,
} catch (err) { // mais les données détaillées ne sont plus stockées ici.
this.error = err.message; currentMangaId: null
} finally { }),
this.loading = false;
} getters: {
}, // Plus de getters spécifiques aux détails/chapitres ici
},
async refreshCollectionInBackground() {
if (this.isBackgroundLoading) return; actions: {
// --- Collection Actions ---
this.isBackgroundLoading = true; async loadCollection() {
if (this.loadingCollection) return;
try { this.loadingCollection = true;
this.collection = await mangaRepository.getCollection(); this.errorCollection = null;
} catch (err) { try {
console.error('Failed to refresh collection:', err); const newCollection = await mangaRepository.getCollection();
} finally { // On garde la comparaison pour éviter màj inutile de la collection
this.isBackgroundLoading = false; if (!deepCompare(this.collection, newCollection)) {
} this.collection = newCollection;
}, }
} catch (err) {
// Actions pour les détails du manga this.errorCollection = err.message;
async fetchMangaDetails(mangaId) { } finally {
this.loading = true; this.loadingCollection = false;
this.error = null; }
try { },
this.currentManga = await mangaRepository.getMangaById(mangaId);
} catch (error) { async refreshCollectionInBackground() {
this.error = error.message; if (this.isBackgroundLoadingCollection) return;
} finally { this.isBackgroundLoadingCollection = true;
this.loading = false; try {
} const newCollection = await mangaRepository.getCollection();
}, if (!deepCompare(this.collection, newCollection)) {
this.collection = newCollection;
async fetchMangaChapters(mangaId) { }
this.loading = true; } catch (err) {
this.error = null; console.error('Failed to refresh collection:', err);
try { } finally {
const response = await mangaRepository.getChapters(mangaId); this.isBackgroundLoadingCollection = false;
this.chapters = Array.isArray(response) ? response : }
(response.items ? response.items : []); },
} catch (error) {
this.error = error.message; // --- Selected Manga Actions ---
} finally { setCurrentMangaId(mangaId) {
this.loading = false; // Met simplement à jour l'ID sélectionné
} this.currentMangaId = mangaId;
}, },
clearCurrentManga() { clearCurrentMangaFocus() {
this.currentManga = null; this.currentMangaId = null;
this.chapters = []; }
// Plus d'actions fetchMangaDetails / fetchMangaChapters ici
} }
}
}); });

View File

@@ -0,0 +1,23 @@
import { computed } from 'vue';
import { useQuery } from '@tanstack/vue-query';
import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
export function useMangaChapters(mangaId) {
const mangaRepository = new ApiMangaRepository();
const query = useQuery({
queryKey: ['manga', mangaId, 'chapters'],
queryFn: async () => {
if (!mangaId.value) {
return Promise.resolve([]); // Retourne un tableau vide si pas d'ID
}
const response = await mangaRepository.getChapters(mangaId.value);
// Assure de toujours retourner un tableau
return Array.isArray(response) ? response : response?.items ?? [];
},
enabled: computed(() => !!mangaId.value)
});
// Retourne le résultat de useQuery (contenant data, isLoading, etc.)
return query;
}

View File

@@ -0,0 +1,31 @@
import { computed } from 'vue';
import { useQuery } from '@tanstack/vue-query';
import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
// Accepte un ID de manga (peut être une ref, un computed ref, ou une valeur simple)
export function useMangaDetails(mangaId) {
const mangaRepository = new ApiMangaRepository();
// Assure que mangaId est une ref ou une computed pour la réactivité de la queryKey
// Si ce n'est pas déjà le cas, mais généralement on passera une computed( () => route.params.id )
// const mangaIdRef = computed(() => unref(mangaId)); // unref est utile si on accepte des valeurs simples aussi
const query = useQuery({
// La queryKey doit être réactive à mangaId
queryKey: ['manga', mangaId], // mangaId est déjà une computed ref, donc c'est bon
queryFn: () => {
// Vérifier que l'ID a une valeur avant d'appeler l'API
if (!mangaId.value) {
// Retourner null ou undefined si pas d'ID, pour éviter un appel API invalide
// TanStack Query gère aussi l'option 'enabled' pour cela
return Promise.resolve(null); // ou throw new Error("ID manquant");
}
return mangaRepository.getMangaById(mangaId.value);
},
// Activer la requête seulement si mangaId a une valeur truthy
enabled: computed(() => !!mangaId.value)
});
// Retourne tous les états et données fournis par useQuery
return query;
}

View File

@@ -0,0 +1,51 @@
import { computed } from 'vue';
import { useMangaChapters } from './useMangaChapters'; // Importe le composable des chapitres
export function useMangaVolumes(mangaId) {
// Utilise le composable des chapitres pour récupérer les données brutes et les états
const { data: rawChaptersData, isLoading, isFetching, error, status } = useMangaChapters(mangaId);
// Calcule les volumes à partir des données des chapitres
const volumes = computed(() => {
const chaptersArray = rawChaptersData.value || []; // Utilise la data retournée par useMangaChapters
if (chaptersArray.length === 0) return [];
const volumeMap = new Map();
chaptersArray.forEach(chapter => {
const volumeNumber = chapter.volume || 'Unknown';
if (!volumeMap.has(volumeNumber)) {
volumeMap.set(volumeNumber, {
number: volumeNumber,
downloadedChapter: 0,
totalChapter: 0,
chapters: []
});
}
const volumeEntry = volumeMap.get(volumeNumber);
volumeEntry.chapters.push({
...chapter,
isAvailable: Boolean(chapter.isAvailable)
});
volumeEntry.totalChapter++;
if (chapter.isAvailable) {
volumeEntry.downloadedChapter++;
}
});
return Array.from(volumeMap.values()).sort((a, b) => {
const numA = a.number === 'Unknown' ? -Infinity : Number(a.number);
const numB = b.number === 'Unknown' ? -Infinity : Number(b.number);
return numB - numA;
});
});
// Retourne les volumes calculés et propage les états pertinents de useMangaChapters
return {
volumes, // Les données transformées
isLoading, // L'état de chargement initial des chapitres
isFetching, // L'état de rafraîchissement des chapitres
error, // L'erreur potentielle lors du fetch des chapitres
status // L'état global ('pending', 'error', 'success')
// On pourrait aussi retourner rawChaptersData si nécessaire ailleurs
};
}

View File

@@ -31,7 +31,12 @@
const router = useRouter(); const router = useRouter();
const mangaStore = useMangaStore(); const mangaStore = useMangaStore();
const { collection, loading, error, isBackgroundLoading } = storeToRefs(mangaStore); const {
collection,
loadingCollection: loading,
errorCollection: error,
isBackgroundLoadingCollection: isBackgroundLoading
} = storeToRefs(mangaStore);
onMounted(() => { onMounted(() => {
mangaStore.loadCollection(); mangaStore.loadCollection();

View File

@@ -3,84 +3,68 @@
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div> <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div> </div>
<div v-else-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded"> <div v-else-if="error && !currentManga" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{{ error }} {{ error.message || 'Une erreur est survenue.' }}
</div> </div>
<div v-else-if="currentManga" class="relative"> <div v-else-if="currentManga" class="relative">
<div v-if="isRefreshing" class="absolute top-2 right-2 text-gray-500">
<ArrowPathIcon class="h-5 w-5 animate-spin" />
</div>
<MangaHeader :manga="currentManga" /> <MangaHeader :manga="currentManga" />
<MangaVolumeList :volumes="volumes" :manga-slug="currentManga.slug" /> <MangaVolumeList :volumes="volumes" :manga-slug="currentManga.slug" />
</div> </div>
<div v-else class="text-center text-gray-500 py-10"> Aucun manga sélectionné ou trouvé. </div>
</template> </template>
<script setup> <script setup>
import { computed, onMounted, onUnmounted, watch } from 'vue'; import { computed, onUnmounted, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useMangaStore } from '../../application/store/mangaStore'; import { ArrowPathIcon } from '@heroicons/vue/24/outline';
import { storeToRefs } from 'pinia';
import { useMangaDetails } from '../composables/useMangaDetails';
import { useMangaVolumes } from '../composables/useMangaVolumes';
import MangaHeader from '../components/MangaHeader.vue'; import MangaHeader from '../components/MangaHeader.vue';
import MangaVolumeList from '../components/MangaVolumeList.vue'; import MangaVolumeList from '../components/MangaVolumeList.vue';
import { useMangaStore } from '../../application/store/mangaStore';
const route = useRoute(); const route = useRoute();
const store = useMangaStore(); const mangaStore = useMangaStore();
const { currentManga, chapters, loading, error } = storeToRefs(store);
const volumes = computed(() => { const mangaId = computed(() => route.params.id || null);
if (!chapters.value) return [];
const chaptersArray = Array.isArray(chapters.value) const {
? chapters.value data: currentManga,
: chapters.value.items isLoading: isLoadingDetails,
? chapters.value.items isFetching: isRefreshingDetails,
: []; error: errorDetails
} = useMangaDetails(mangaId);
const volumeMap = new Map(); const {
volumes,
isLoading: isLoadingVolumes,
isFetching: isRefreshingVolumes,
error: errorVolumes
} = useMangaVolumes(mangaId);
chaptersArray.forEach(chapter => { const loading = computed(() => isLoadingDetails.value || isLoadingVolumes.value);
const volumeNumber = chapter.volume || 'Unknown'; const isRefreshing = computed(() => isRefreshingDetails.value || isRefreshingVolumes.value);
if (!volumeMap.has(volumeNumber)) { const error = computed(() => errorDetails.value || errorVolumes.value);
volumeMap.set(volumeNumber, {
number: volumeNumber,
downloadedChapter: 0,
totalChapter: 0,
chapters: []
});
}
volumeMap.get(volumeNumber).chapters.push({
...chapter,
isAvailable: Boolean(chapter.isAvailable)
});
});
for (const volume of volumeMap.values()) {
volume.downloadedChapter = volume.chapters.filter(c => c.isAvailable).length;
volume.totalChapter = volume.chapters.length;
}
return Array.from(volumeMap.values()).sort((a, b) => b.number - a.number);
});
const loadData = async () => {
const mangaId = route.params.id;
await Promise.all([store.fetchMangaDetails(mangaId), store.fetchMangaChapters(mangaId)]);
};
// Ajouter le watcher sur l'ID de la route
watch( watch(
() => route.params.id, mangaId,
(newId, oldId) => { newId => {
if (newId !== oldId) { if (newId) {
loadData(); mangaStore.setCurrentMangaId(newId);
} }
} },
{ immediate: true }
); );
onMounted(() => {
loadData();
});
onUnmounted(() => { onUnmounted(() => {
store.clearCurrentManga(); mangaStore.clearCurrentMangaFocus();
}); });
</script> </script>

View File

@@ -1,18 +1,18 @@
import { createApp } from 'vue' import { createApp } from 'vue';
import { createPinia } from 'pinia' import { createPinia } from 'pinia';
import App from './App.vue' import App from './App.vue';
import { router } from './router' import { router } from './router';
import '../../styles/app.scss' import '../../styles/app.scss';
import { installVueQuery } from './shared/plugin/vueQuery';
// Création du store // Création du store
const pinia = createPinia() const pinia = createPinia();
// Création de l'application // Création de l'application
const app = createApp(App) const app = createApp(App);
// Installation des plugins // Installation des plugins
app.use(router) app.use(router);
app.use(pinia) app.use(pinia);
app.use(installVueQuery);
// Montage de l'application // Montage de l'application
app.mount('#vue-app') app.mount('#vue-app');

View File

@@ -0,0 +1,25 @@
import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query';
// Créez une instance de QueryClient
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Options par défaut pour toutes les requêtes (queries)
staleTime: 5 * 60 * 1000, // 5 minutes: temps pendant lequel les données sont considérées "fraîches" (ne déclenche pas de re-fetch en arrière-plan juste en montant le composant)
gcTime: 10 * 60 * 1000, // 10 minutes: temps avant que les données inactives soient supprimées du cache (garbage collected)
retry: 1, // Nombre de tentatives en cas d'erreur
refetchOnWindowFocus: true // Re-fetcher quand la fenêtre reprend le focus (bon pour garder les données à jour)
}
}
});
export const vueQueryPluginOptions = {
queryClient // Fournir le client au plugin
};
export const installVueQuery = app => {
app.use(VueQueryPlugin, vueQueryPluginOptions);
};
// Exportez aussi le client si vous avez besoin d'y accéder directement ailleurs (rare)
export { queryClient };

115
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@tanstack/vue-query": "^5.71.0",
"alpinejs": "^3.13.3", "alpinejs": "^3.13.3",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"axios": "^1.7.9", "axios": "^1.7.9",
@@ -420,7 +421,6 @@
"version": "7.25.9", "version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -477,7 +477,6 @@
"version": "7.26.8", "version": "7.26.8",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz",
"integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==", "integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.26.8" "@babel/types": "^7.26.8"
@@ -1899,7 +1898,6 @@
"version": "7.26.8", "version": "7.26.8",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz",
"integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==", "integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.25.9", "@babel/helper-string-parser": "^7.25.9",
@@ -3401,6 +3399,32 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/@tanstack/match-sorter-utils": {
"version": "8.19.4",
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz",
"integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==",
"license": "MIT",
"dependencies": {
"remove-accents": "0.5.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.71.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.71.0.tgz",
"integrity": "sha512-p4+T7CIEe1kMhii4booWiw42nuaiYI9La/bRCNzBaj1P3PDb0dEZYDhc/7oBifKJfHYN+mtS1ynW1qsmzQW7Og==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/virtual-core": { "node_modules/@tanstack/virtual-core": {
"version": "3.13.5", "version": "3.13.5",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.5.tgz", "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.5.tgz",
@@ -3412,6 +3436,63 @@
"url": "https://github.com/sponsors/tannerlinsley" "url": "https://github.com/sponsors/tannerlinsley"
} }
}, },
"node_modules/@tanstack/vue-query": {
"version": "5.71.0",
"resolved": "https://registry.npmjs.org/@tanstack/vue-query/-/vue-query-5.71.0.tgz",
"integrity": "sha512-/jjuDka26wbuhJi4X7DBbbgp3Fh+ZX/MG4/SKAEQOht3+MKucoEQm2uwUKv739m2td8kvFDNSFysWYiT6J+a5w==",
"license": "MIT",
"dependencies": {
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/query-core": "5.71.0",
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@vue/composition-api": "^1.1.2",
"vue": "^2.6.0 || ^3.3.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@tanstack/vue-query/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@tanstack/vue-query/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@tanstack/vue-virtual": { "node_modules/@tanstack/vue-virtual": {
"version": "3.13.5", "version": "3.13.5",
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.5.tgz", "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.5.tgz",
@@ -3941,7 +4022,6 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.25.3", "@babel/parser": "^7.25.3",
@@ -3955,14 +4035,12 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/compiler-core/node_modules/entities": { "node_modules/@vue/compiler-core/node_modules/entities": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
@@ -3975,7 +4053,6 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.5.13", "@vue/compiler-core": "3.5.13",
@@ -3986,14 +4063,12 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/compiler-sfc": { "node_modules/@vue/compiler-sfc": {
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.25.3", "@babel/parser": "^7.25.3",
@@ -4011,14 +4086,12 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/compiler-ssr": { "node_modules/@vue/compiler-ssr": {
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.13", "@vue/compiler-dom": "3.5.13",
@@ -4029,7 +4102,6 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/devtools-api": { "node_modules/@vue/devtools-api": {
@@ -4080,7 +4152,6 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.13", "@vue/reactivity": "3.5.13",
@@ -4091,7 +4162,6 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/shared": "3.5.13" "@vue/shared": "3.5.13"
@@ -4101,14 +4171,12 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/runtime-dom": { "node_modules/@vue/runtime-dom": {
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.13", "@vue/reactivity": "3.5.13",
@@ -4121,7 +4189,6 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/shared": "3.5.13" "@vue/shared": "3.5.13"
@@ -4131,14 +4198,12 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/server-renderer": { "node_modules/@vue/server-renderer": {
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.5.13", "@vue/compiler-ssr": "3.5.13",
@@ -4152,7 +4217,6 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/shared": { "node_modules/@vue/shared": {
@@ -5908,7 +5972,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/culori": { "node_modules/culori": {
@@ -6454,7 +6517,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/esutils": { "node_modules/esutils": {
@@ -8091,7 +8153,6 @@
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
@@ -10187,6 +10248,12 @@
"jsesc": "bin/jsesc" "jsesc": "bin/jsesc"
} }
}, },
"node_modules/remove-accents": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
"license": "MIT"
},
"node_modules/renderkid": { "node_modules/renderkid": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
@@ -11762,7 +11829,6 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.13", "@vue/compiler-dom": "3.5.13",
@@ -11906,7 +11972,6 @@
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/watchpack": { "node_modules/watchpack": {

View File

@@ -42,6 +42,7 @@
"@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@tanstack/vue-query": "^5.71.0",
"alpinejs": "^3.13.3", "alpinejs": "^3.13.3",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"axios": "^1.7.9", "axios": "^1.7.9",