SwitchEtat.vue 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. <template>
  2. <component
  3. :is="tag"
  4. :href="tag === 'a' ? '#' : undefined"
  5. :type="tag === 'button' ? 'button' : undefined"
  6. :class="classe"
  7. :title="titre"
  8. :data-bs-toggle="tooltip ? 'tooltip' : undefined"
  9. :aria-busy="enChargement"
  10. @click.prevent="switchEtat"
  11. >
  12. <i :class="iconeCourante"></i>
  13. </component>
  14. </template>
  15. <script setup>
  16. import { computed, ref } from 'vue';
  17. const props = defineProps({
  18. // Route du contrôleur qui bascule l'état
  19. path: { type: String, required: true },
  20. // Identifiant du film concerné
  21. idFilm: { type: [String, Number], required: true },
  22. // Classes Font Awesome de l'icône quand actif / inactif
  23. iconeActif: { type: String, required: true },
  24. iconeInactif: { type: String, required: true },
  25. // État initial (définit quelle icône est affichée au montage)
  26. actif: { type: Boolean, default: false },
  27. // Texte de l'infobulle décrivant l'action
  28. titre: { type: String, default: '' },
  29. // Active l'attribut data-bs-toggle="tooltip" de Bootstrap
  30. tooltip: { type: Boolean, default: false },
  31. // Balise racine : 'a' (par défaut) ou 'button'
  32. tag: { type: String, default: 'a' },
  33. // Classes CSS additionnelles pour la balise (ex: 'btn text-primary')
  34. classe: { type: String, default: '' },
  35. // Animation Font Awesome appliquée à l'icône cible pendant la requête
  36. // (fa-beat-fade, fa-beat, fa-flip, ... — vide pour désactiver)
  37. animationChargement: { type: String, default: 'fa-beat-fade' },
  38. });
  39. const estActif = ref(props.actif);
  40. const enChargement = ref(false);
  41. // L'icône cible est celle qui sera affichée si la requête réussit
  42. const iconeCible = computed(() => (estActif.value ? props.iconeInactif : props.iconeActif));
  43. // Préserve les classes de taille (fa-lg, fa-2xl, ...) pendant le chargement
  44. const classesTaille = computed(() => {
  45. const icone = enChargement.value ? iconeCible.value : (estActif.value ? props.iconeActif : props.iconeInactif);
  46. return icone.split(' ').filter((c) => /^fa-(2?xs|sm|lg|2?xl|[0-9]x|fw)$/.test(c));
  47. });
  48. const iconeCourante = computed(() => {
  49. if (enChargement.value) {
  50. return [iconeCible.value, props.animationChargement, ...classesTaille.value]
  51. .filter(Boolean)
  52. .join(' ');
  53. }
  54. return estActif.value ? props.iconeActif : props.iconeInactif;
  55. });
  56. async function switchEtat() {
  57. if (enChargement.value) {
  58. return;
  59. }
  60. enChargement.value = true;
  61. try {
  62. const response = await fetch(props.path, {
  63. method: 'PATCH',
  64. body: JSON.stringify({ id_film: props.idFilm }),
  65. });
  66. if (!response.ok) {
  67. throw new Error(`Erreur Http ${response.status}`);
  68. }
  69. await response.json();
  70. estActif.value = !estActif.value;
  71. } catch (error) {
  72. console.error(error);
  73. } finally {
  74. enChargement.value = false;
  75. }
  76. }
  77. </script>
  78. <style scoped>
  79. i {
  80. display: inline-block;
  81. transition: opacity 0.3s ease;
  82. }
  83. </style>