diff --git a/assets/vue/app/domain/manga/application/store/mangaStore.js b/assets/vue/app/domain/manga/application/store/mangaStore.js
index 7400485..458b4b5 100644
--- a/assets/vue/app/domain/manga/application/store/mangaStore.js
+++ b/assets/vue/app/domain/manga/application/store/mangaStore.js
@@ -1,81 +1,82 @@
-import {defineStore} from 'pinia';
-import {ApiMangaRepository} from '../../infrastructure/api/apiMangaRepository';
+import { defineStore } from 'pinia';
+import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
const mangaRepository = new ApiMangaRepository();
-export const useMangaStore = defineStore('manga', {
- state: () => ({
- // État pour la collection
- collection: null,
- // État pour les détails
- currentManga: null,
- chapters: [],
- loading: false,
- error: null,
- isBackgroundLoading: false
- }),
-
- actions: {
- // Actions pour la collection
- async loadCollection() {
- if (this.loading) return;
-
- this.loading = true;
- this.error = null;
-
- try {
- this.collection = await mangaRepository.getCollection();
- } catch (err) {
- this.error = err.message;
- } finally {
- this.loading = false;
- }
- },
-
- async refreshCollectionInBackground() {
- if (this.isBackgroundLoading) return;
-
- this.isBackgroundLoading = true;
-
- try {
- this.collection = await mangaRepository.getCollection();
- } catch (err) {
- console.error('Failed to refresh collection:', err);
- } finally {
- this.isBackgroundLoading = false;
- }
- },
-
- // Actions pour les détails du manga
- async fetchMangaDetails(mangaId) {
- this.loading = true;
- this.error = null;
- try {
- this.currentManga = await mangaRepository.getMangaById(mangaId);
- } catch (error) {
- this.error = error.message;
- } finally {
- this.loading = false;
- }
- },
-
- async fetchMangaChapters(mangaId) {
- this.loading = true;
- this.error = null;
- try {
- const response = await mangaRepository.getChapters(mangaId);
- this.chapters = Array.isArray(response) ? response :
- (response.items ? response.items : []);
- } catch (error) {
- this.error = error.message;
- } finally {
- this.loading = false;
- }
- },
-
- clearCurrentManga() {
- this.currentManga = null;
- this.chapters = [];
+// Helper pour comparer la collection (peut être supprimé si non utilisé ailleurs)
+const deepCompare = (obj1, obj2) => {
+ try {
+ if (obj1 == null && obj2 == null) return true;
+ if (obj1 == null || obj2 == null) return false;
+ return JSON.stringify(obj1) === JSON.stringify(obj2);
+ } catch (e) {
+ console.error("Erreur lors de la comparaison d'objets:", e);
+ return false;
+ }
+};
+
+export const useMangaStore = defineStore('manga', {
+ state: () => ({
+ // --- Collection State ---
+ collection: null,
+ loadingCollection: false,
+ errorCollection: null,
+ isBackgroundLoadingCollection: false,
+
+ // --- Selected Manga State ---
+ // Gardé pour savoir quel manga est sélectionné dans l'UI,
+ // mais les données détaillées ne sont plus stockées ici.
+ currentMangaId: null
+ }),
+
+ getters: {
+ // Plus de getters spécifiques aux détails/chapitres ici
+ },
+
+ actions: {
+ // --- Collection Actions ---
+ async loadCollection() {
+ if (this.loadingCollection) return;
+ this.loadingCollection = true;
+ this.errorCollection = null;
+ try {
+ const newCollection = await mangaRepository.getCollection();
+ // On garde la comparaison pour éviter màj inutile de la collection
+ if (!deepCompare(this.collection, newCollection)) {
+ this.collection = newCollection;
+ }
+ } catch (err) {
+ this.errorCollection = err.message;
+ } finally {
+ this.loadingCollection = false;
+ }
+ },
+
+ async refreshCollectionInBackground() {
+ if (this.isBackgroundLoadingCollection) return;
+ this.isBackgroundLoadingCollection = true;
+ try {
+ const newCollection = await mangaRepository.getCollection();
+ if (!deepCompare(this.collection, newCollection)) {
+ this.collection = newCollection;
+ }
+ } catch (err) {
+ console.error('Failed to refresh collection:', err);
+ } finally {
+ this.isBackgroundLoadingCollection = false;
+ }
+ },
+
+ // --- Selected Manga Actions ---
+ setCurrentMangaId(mangaId) {
+ // Met simplement à jour l'ID sélectionné
+ this.currentMangaId = mangaId;
+ },
+
+ clearCurrentMangaFocus() {
+ this.currentMangaId = null;
+ }
+
+ // Plus d'actions fetchMangaDetails / fetchMangaChapters ici
}
- }
});
diff --git a/assets/vue/app/domain/manga/presentation/composables/useMangaChapters.js b/assets/vue/app/domain/manga/presentation/composables/useMangaChapters.js
new file mode 100644
index 0000000..5492b3d
--- /dev/null
+++ b/assets/vue/app/domain/manga/presentation/composables/useMangaChapters.js
@@ -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;
+}
diff --git a/assets/vue/app/domain/manga/presentation/composables/useMangaDetails.js b/assets/vue/app/domain/manga/presentation/composables/useMangaDetails.js
new file mode 100644
index 0000000..c0f88f7
--- /dev/null
+++ b/assets/vue/app/domain/manga/presentation/composables/useMangaDetails.js
@@ -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;
+}
diff --git a/assets/vue/app/domain/manga/presentation/composables/useMangaVolumes.js b/assets/vue/app/domain/manga/presentation/composables/useMangaVolumes.js
new file mode 100644
index 0000000..dc3fce2
--- /dev/null
+++ b/assets/vue/app/domain/manga/presentation/composables/useMangaVolumes.js
@@ -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
+ };
+}
diff --git a/assets/vue/app/domain/manga/presentation/pages/HomePage.vue b/assets/vue/app/domain/manga/presentation/pages/HomePage.vue
index 7eae228..247fbb6 100644
--- a/assets/vue/app/domain/manga/presentation/pages/HomePage.vue
+++ b/assets/vue/app/domain/manga/presentation/pages/HomePage.vue
@@ -31,7 +31,12 @@
const router = useRouter();
const mangaStore = useMangaStore();
- const { collection, loading, error, isBackgroundLoading } = storeToRefs(mangaStore);
+ const {
+ collection,
+ loadingCollection: loading,
+ errorCollection: error,
+ isBackgroundLoadingCollection: isBackgroundLoading
+ } = storeToRefs(mangaStore);
onMounted(() => {
mangaStore.loadCollection();
diff --git a/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue b/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue
index d8c2e7d..861b7d6 100644
--- a/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue
+++ b/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue
@@ -3,84 +3,68 @@
- {{ error }}
+
+ {{ error.message || 'Une erreur est survenue.' }}
+
+
Aucun manga sélectionné ou trouvé.
diff --git a/assets/vue/app/index.js b/assets/vue/app/index.js
index 66e898d..d98b823 100644
--- a/assets/vue/app/index.js
+++ b/assets/vue/app/index.js
@@ -1,18 +1,18 @@
-import { createApp } from 'vue'
-import { createPinia } from 'pinia'
-import App from './App.vue'
-import { router } from './router'
-import '../../styles/app.scss'
-
+import { createApp } from 'vue';
+import { createPinia } from 'pinia';
+import App from './App.vue';
+import { router } from './router';
+import '../../styles/app.scss';
+import { installVueQuery } from './shared/plugin/vueQuery';
// Création du store
-const pinia = createPinia()
+const pinia = createPinia();
// Création de l'application
-const app = createApp(App)
+const app = createApp(App);
// Installation des plugins
-app.use(router)
-app.use(pinia)
-
+app.use(router);
+app.use(pinia);
+app.use(installVueQuery);
// Montage de l'application
-app.mount('#vue-app')
\ No newline at end of file
+app.mount('#vue-app');
diff --git a/assets/vue/app/shared/plugin/vueQuery.js b/assets/vue/app/shared/plugin/vueQuery.js
new file mode 100644
index 0000000..2317d3d
--- /dev/null
+++ b/assets/vue/app/shared/plugin/vueQuery.js
@@ -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 };
diff --git a/package-lock.json b/package-lock.json
index 98fe511..224383a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
+ "@tanstack/vue-query": "^5.71.0",
"alpinejs": "^3.13.3",
"autoprefixer": "^10.4.14",
"axios": "^1.7.9",
@@ -420,7 +421,6 @@
"version": "7.25.9",
"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==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -477,7 +477,6 @@
"version": "7.26.8",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz",
"integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.8"
@@ -1899,7 +1898,6 @@
"version": "7.26.8",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz",
"integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@@ -3401,6 +3399,32 @@
"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": {
"version": "3.13.5",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.5.tgz",
@@ -3412,6 +3436,63 @@
"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": {
"version": "3.13.5",
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.5.tgz",
@@ -3941,7 +4022,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
@@ -3955,14 +4035,12 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@vue/compiler-core/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -3975,7 +4053,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.13",
@@ -3986,14 +4063,12 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
@@ -4011,14 +4086,12 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.13",
@@ -4029,7 +4102,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@vue/devtools-api": {
@@ -4080,7 +4152,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.13",
@@ -4091,7 +4162,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.13"
@@ -4101,14 +4171,12 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.13",
@@ -4121,7 +4189,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.13"
@@ -4131,14 +4198,12 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@vue/server-renderer": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.13",
@@ -4152,7 +4217,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@vue/shared": {
@@ -5908,7 +5972,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true,
"license": "MIT"
},
"node_modules/culori": {
@@ -6454,7 +6517,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "dev": true,
"license": "MIT"
},
"node_modules/esutils": {
@@ -8091,7 +8153,6 @@
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
@@ -10187,6 +10248,12 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
@@ -11762,7 +11829,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.13",
@@ -11906,7 +11972,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/watchpack": {
diff --git a/package.json b/package.json
index 4b63d2c..05bbbb4 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
+ "@tanstack/vue-query": "^5.71.0",
"alpinejs": "^3.13.3",
"autoprefixer": "^10.4.14",
"axios": "^1.7.9",