|
|
@@ -0,0 +1,91 @@
|
|
|
+<template>
|
|
|
+ <component
|
|
|
+ :is="tag"
|
|
|
+ :href="tag === 'a' ? '#' : undefined"
|
|
|
+ :type="tag === 'button' ? 'button' : undefined"
|
|
|
+ :class="classe"
|
|
|
+ :title="titre"
|
|
|
+ :data-bs-toggle="tooltip ? 'tooltip' : undefined"
|
|
|
+ :aria-busy="enChargement"
|
|
|
+ @click.prevent="switchEtat"
|
|
|
+ >
|
|
|
+ <i :class="iconeCourante"></i>
|
|
|
+ </component>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { computed, ref } from 'vue';
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ // Route du contrôleur qui bascule l'état
|
|
|
+ path: { type: String, required: true },
|
|
|
+ // Identifiant du film concerné
|
|
|
+ idFilm: { type: [String, Number], required: true },
|
|
|
+ // Classes Font Awesome de l'icône quand actif / inactif
|
|
|
+ iconeActif: { type: String, required: true },
|
|
|
+ iconeInactif: { type: String, required: true },
|
|
|
+ // État initial (définit quelle icône est affichée au montage)
|
|
|
+ actif: { type: Boolean, default: false },
|
|
|
+ // Texte de l'infobulle décrivant l'action
|
|
|
+ titre: { type: String, default: '' },
|
|
|
+ // Active l'attribut data-bs-toggle="tooltip" de Bootstrap
|
|
|
+ tooltip: { type: Boolean, default: false },
|
|
|
+ // Balise racine : 'a' (par défaut) ou 'button'
|
|
|
+ tag: { type: String, default: 'a' },
|
|
|
+ // Classes CSS additionnelles pour la balise (ex: 'btn text-primary')
|
|
|
+ classe: { type: String, default: '' },
|
|
|
+ // Animation Font Awesome appliquée à l'icône cible pendant la requête
|
|
|
+ // (fa-beat-fade, fa-beat, fa-flip, ... — vide pour désactiver)
|
|
|
+ animationChargement: { type: String, default: 'fa-beat-fade' },
|
|
|
+});
|
|
|
+
|
|
|
+const estActif = ref(props.actif);
|
|
|
+const enChargement = ref(false);
|
|
|
+
|
|
|
+// L'icône cible est celle qui sera affichée si la requête réussit
|
|
|
+const iconeCible = computed(() => (estActif.value ? props.iconeInactif : props.iconeActif));
|
|
|
+
|
|
|
+// Préserve les classes de taille (fa-lg, fa-2xl, ...) pendant le chargement
|
|
|
+const classesTaille = computed(() => {
|
|
|
+ const icone = enChargement.value ? iconeCible.value : (estActif.value ? props.iconeActif : props.iconeInactif);
|
|
|
+ return icone.split(' ').filter((c) => /^fa-(2?xs|sm|lg|2?xl|[0-9]x|fw)$/.test(c));
|
|
|
+});
|
|
|
+
|
|
|
+const iconeCourante = computed(() => {
|
|
|
+ if (enChargement.value) {
|
|
|
+ return [iconeCible.value, props.animationChargement, ...classesTaille.value]
|
|
|
+ .filter(Boolean)
|
|
|
+ .join(' ');
|
|
|
+ }
|
|
|
+ return estActif.value ? props.iconeActif : props.iconeInactif;
|
|
|
+});
|
|
|
+
|
|
|
+async function switchEtat() {
|
|
|
+ if (enChargement.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ enChargement.value = true;
|
|
|
+ try {
|
|
|
+ const response = await fetch(props.path, {
|
|
|
+ method: 'PATCH',
|
|
|
+ body: JSON.stringify({ id_film: props.idFilm }),
|
|
|
+ });
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`Erreur Http ${response.status}`);
|
|
|
+ }
|
|
|
+ await response.json();
|
|
|
+ estActif.value = !estActif.value;
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
+ } finally {
|
|
|
+ enChargement.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+i {
|
|
|
+ display: inline-block;
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
+}
|
|
|
+</style>
|