Explorar o código

add vue-advanced-cropper setup input image modal and cropper

Olivier Massot %!s(int64=2) %!d(string=hai) anos
pai
achega
163d4da6f7

+ 2 - 2
components/Layout/Header/Menu.vue

@@ -17,7 +17,7 @@ header principal (configuration, paramètres du compte...)
           size="30"
           size="30"
       >
       >
         <UiImage
         <UiImage
-            :id="menu.icon.avatarId"
+            :imageId="menu.icon.avatarId"
             :defaultImage="menu.icon.avatarByDefault"
             :defaultImage="menu.icon.avatarByDefault"
             :width="30"
             :width="30"
         />
         />
@@ -56,7 +56,7 @@ header principal (configuration, paramètres du compte...)
               >
               >
                 <span v-if="child.icon" class="pr-2 d-flex align-center">
                 <span v-if="child.icon" class="pr-2 d-flex align-center">
                   <v-avatar v-if="menu.icon.avatarId || child.icon.avatarByDefault" size="30" >
                   <v-avatar v-if="menu.icon.avatarId || child.icon.avatarByDefault" size="30" >
-                    <UiImage :id="child.icon.avatarId" :defaultImage="child.icon.avatarByDefault" :width="30" />
+                    <UiImage :imageId="child.icon.avatarId" :defaultImage="child.icon.avatarByDefault" :width="30" />
                   </v-avatar>
                   </v-avatar>
                   <v-icon v-else class="on-primary" size="small">
                   <v-icon v-else class="on-primary" size="small">
                     {{ child.icon.name }}
                     {{ child.icon.name }}

+ 11 - 6
components/Layout/Parameters/General.vue

@@ -61,12 +61,16 @@
               label="students_are_also_association_members"
               label="students_are_also_association_members"
           />
           />
 
 
-          <!-- TODO: reprendre l'UiInput -->
-          <UiInputImage
-              v-model="parameters['qrCode_id']"
-              field="qrCode_id"
-              label="licenceQrCode"
-          />
+          <div class="d-flex flex-column">
+            <span class="mb-1">{{ $t('qrCode')}} </span>
+            <UiInputImage
+                v-if="organizationProfile.isCMFCentralService"
+                v-model="parameters['qrCode']"
+                field="qrCode"
+                label="licenceQrCode"
+                :width="120"
+            />
+          </div>
         </v-col>
         </v-col>
       </v-row>
       </v-row>
     </UiForm>
     </UiForm>
@@ -88,6 +92,7 @@ if (organizationProfile.parametersId === null) {
 }
 }
 
 
 const { data: parameters, pending } = fetch(Parameters, organizationProfile.parametersId) as AsyncData<Parameters, Parameters | true>
 const { data: parameters, pending } = fetch(Parameters, organizationProfile.parametersId) as AsyncData<Parameters, Parameters | true>
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 53 - 19
components/Ui/Image.vue

@@ -1,8 +1,6 @@
 <!--
 <!--
-Composant Image permettant de charger et d'afficher une image stockée sur les serveurs Opentalent à partir de son id.
+Composant Image permettant d'afficher une image stockée sur les serveurs Opentalent à partir de son id.
 Permet d'afficher une image par défaut si l'image demandée n'est pas disponible ou invalide.
 Permet d'afficher une image par défaut si l'image demandée n'est pas disponible ou invalide.
-
-Si la propriété 'upload' est à 'true', propose aussi un input pour uploader une nouvelle image.
 -->
 -->
 <template>
 <template>
   <main>
   <main>
@@ -27,47 +25,54 @@ Si la propriété 'upload' est à 'true', propose aussi un input pour uploader u
             />
             />
           </v-row>
           </v-row>
         </template>
         </template>
+
+        <div v-if="!pending && overlayIcon" class="overlay" @click="emit('overlay-clicked')">
+          <v-icon>fas fa-upload</v-icon>
+        </div>
       </v-img>
       </v-img>
     </div>
     </div>
   </main>
   </main>
 </template>
 </template>
 
 
-
 <script setup lang="ts">
 <script setup lang="ts">
 import {useImageFetch} from "~/composables/data/useImageFetch";
 import {useImageFetch} from "~/composables/data/useImageFetch";
-import {onUnmounted, watch, WatchStopHandle} from "@vue/runtime-core";
+import {onUnmounted, PropType, watch, WatchStopHandle} from "@vue/runtime-core";
 import ImageManager from "~/services/data/imageManager";
 import ImageManager from "~/services/data/imageManager";
 
 
 const props = defineProps({
 const props = defineProps({
-  id: {
-    type: Number,
+  /**
+   * Id de l'image (null si aucune)
+   */
+  imageId: {
+    type: Number as PropType<Number | null>,
     required: false,
     required: false,
     default: null
     default: null
   },
   },
+  /**
+   * Image par défaut
+   */
   defaultImage: {
   defaultImage: {
     type: String,
     type: String,
     required: false
     required: false
   },
   },
+  /**
+   * Hauteur de l'image à l'écran (en px)
+   */
   height: {
   height: {
     type: Number,
     type: Number,
     required: false
     required: false
   },
   },
+  /**
+   * Largeur de l'image à l'écran (en px)
+   */
   width: {
   width: {
     type: Number,
     type: Number,
     required: false
     required: false
   },
   },
-  field: {
+  overlayIcon: {
     type: String,
     type: String,
-    required: false
-  },
-  upload: {
-    type: Boolean,
     required: false,
     required: false,
-    default: false
-  },
-  ownerId:{
-    type: Number,
-    required: false
+    default: null
   }
   }
 })
 })
 
 
@@ -75,12 +80,15 @@ const { fetch } = useImageFetch()
 
 
 const defaultImagePath = props.defaultImage ?? ImageManager.defaultImage
 const defaultImagePath = props.defaultImage ?? ImageManager.defaultImage
 
 
-const { data: imageSrc, pending, refresh } = fetch(props.id ?? null, defaultImagePath, props.height, props.width) as any
+const { data: imageSrc, pending, refresh } = fetch(props.imageId ?? null, defaultImagePath, props.height, props.width) as any
+
+
+const emit = defineEmits(['overlay-clicked'])
 
 
 /**
 /**
  * Si l'id change, on recharge l'image
  * Si l'id change, on recharge l'image
  */
  */
-const unwatch: WatchStopHandle = watch(() => props.id, async () => {
+const unwatch: WatchStopHandle = watch(() => props.imageId, async () => {
   await refresh()
   await refresh()
 })
 })
 
 
@@ -98,7 +106,33 @@ onUnmounted(() => {
     position: relative;
     position: relative;
 
 
     img {
     img {
+      display: block;
       max-width: 100%;
       max-width: 100%;
     }
     }
+
+    .overlay {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      height: 100%;
+      width: 100%;
+      opacity: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      transition: .3s ease;
+    }
+    .overlay:hover {
+      opacity: 0.8;
+      background-color: rgb(var(--v-theme-neutral-strong));
+      cursor: pointer;
+    }
+
+    .overlay .v-icon {
+      color: rgb(var(--v-theme-on-neutral-strong));
+      font-size: 36px;
+    }
   }
   }
 </style>
 </style>

+ 264 - 111
components/Ui/Input/Image.vue

@@ -1,15 +1,27 @@
 <!--
 <!--
 Assistant de création d'image
 Assistant de création d'image
-https://norserium.github.io/vue-advanced-cropper/
+
+@see https://norserium.github.io/vue-advanced-cropper/
 -->
 -->
 <template>
 <template>
-    <LazyLayoutDialog :show="true">
+  <div class="input-image" >
+    <UiImage
+        :image-id="modelValue"
+        :default-image="defaultImage"
+        :width="width"
+        :height="height"
+        class="image"
+        overlay-icon="fas fa-upload"
+        @overlay-clicked="openModal()"
+    />
+
+    <LazyLayoutDialog :show="showModal">
       <template #dialogType>{{ $t('image_assistant') }}</template>
       <template #dialogType>{{ $t('image_assistant') }}</template>
       <template #dialogTitle>{{ $t('modif_picture') }}</template>
       <template #dialogTitle>{{ $t('modif_picture') }}</template>
       <template #dialogText>
       <template #dialogText>
         <div class="upload">
         <div class="upload">
           <v-row
           <v-row
-            v-if="fetchState.pending"
+            v-if="pending"
             class="fill-height ma-0 loading"
             class="fill-height ma-0 loading"
             align="center"
             align="center"
             justify="center"
             justify="center"
@@ -22,22 +34,28 @@ https://norserium.github.io/vue-advanced-cropper/
 
 
           <div v-else >
           <div v-else >
             <div class="upload__cropper-wrapper">
             <div class="upload__cropper-wrapper">
-              <cropper
+              <Cropper
                 ref="cropper"
                 ref="cropper"
                 class="upload__cropper"
                 class="upload__cropper"
                 check-orientation
                 check-orientation
-                :src="image.src"
-                :default-position="{left : coordinates.left, top : coordinates.top}"
-                :default-size="coordinates.width ? {width : coordinates.width, height : coordinates.height}: defaultSize"
-                @change="onChange"
+                :src="uploadedImage.src"
+                :default-position="defaultPosition"
+                :default-size="defaultSize"
+                @change="onCropperChange"
               />
               />
-              <div v-if="image.src" class="upload__reset-button" title="Reset Image" @click="reset()">
-                <v-icon>mdi-delete</v-icon>
+
+              <div
+                  v-if="uploadedImage.src"
+                  class="upload__reset-button"
+                  title="Reset Image"
+                  @click="reset()"
+              >
+                <v-icon>fas fa-trash</v-icon>
               </div>
               </div>
             </div>
             </div>
             <div class="upload__buttons-wrapper">
             <div class="upload__buttons-wrapper">
-              <button class="upload__button" @click="$refs.file.click()">
-                <input ref="file" type="file" accept="image/*" @change="uploadImage($event)" />
+              <button class="upload__button" @click="fileInput?.click()">
+                <input ref="fileInput" type="file" accept="image/*" @change="uploadImage($event)" />
                 {{$t('upload_image')}}
                 {{$t('upload_image')}}
               </button>
               </button>
             </div>
             </div>
@@ -46,190 +64,324 @@ https://norserium.github.io/vue-advanced-cropper/
         </div>
         </div>
       </template>
       </template>
       <template #dialogBtn>
       <template #dialogBtn>
-        <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="$emit('close')">
+        <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="cancel">
           {{ $t('cancel') }}
           {{ $t('cancel') }}
         </v-btn>
         </v-btn>
-        <v-btn class="mr-4 submitBtn theme-danger" @click="save">
+        <v-btn class="submitBtn theme-primary" @click="save" :disabled="pending">
           {{ $t('save') }}
           {{ $t('save') }}
         </v-btn>
         </v-btn>
       </template>
       </template>
     </LazyLayoutDialog>
     </LazyLayoutDialog>
-
+  </div>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 
 
-import {useNuxtApp} from "#app";
 import {ref, Ref} from "@vue/reactivity";
 import {ref, Ref} from "@vue/reactivity";
 import File from '~/models/Core/File'
 import File from '~/models/Core/File'
-import {useAp2iRequestService} from "~/composables/data/useAp2iRequestService";
-import UrlUtils from "~/services/utils/urlUtils";
-import {useImageFetch} from "~/composables/data/useImageFetch";
-import {onUnmounted, watch, WatchStopHandle} from "@vue/runtime-core";
+import {PropType, watch} from "@vue/runtime-core";
 import {useEntityManager} from "~/composables/data/useEntityManager";
 import {useEntityManager} from "~/composables/data/useEntityManager";
 import {AnyJson} from "~/types/data";
 import {AnyJson} from "~/types/data";
+import ApiResource from "~/models/ApiResource";
+import {useImageManager} from "~/composables/data/useImageManager";
+import { Cropper } from 'vue-advanced-cropper'
+import 'vue-advanced-cropper/dist/style.css';
 
 
 const props = defineProps({
 const props = defineProps({
-  imageId: {
+  /**
+   * Id de l'objet File, ou null
+   */
+  modelValue: {
+    type: Number as PropType<number | null>,
+    required: false,
+    default: null
+  },
+  /**
+   * Label du champ
+   * Si non défini, c'est le nom de propriété qui est utilisé
+   */
+  field: {
+    type: String,
+    required: true
+  },
+  /**
+   * Image par défaut en cas d'absence d'une image uploadée
+   */
+  defaultImage: {
+    type: String,
+    required: false
+  },
+  /**
+   * Hauteur de l'image à l'écran (en px)
+   */
+  height: {
     type: Number,
     type: Number,
     required: false
     required: false
   },
   },
-  ownerId: {
+  /**
+   * Largeur de l'image à l'écran (en px)
+   */
+  width: {
     type: Number,
     type: Number,
     required: false
     required: false
   },
   },
-  field: {
-    type: String,
-    required: true
+  /**
+   * TODO: completer
+   */
+  ownerId: {
+    type: Number,
+    required: false
   }
   }
 })
 })
 
 
-const { emit } = useNuxtApp()
-
 const { em } = useEntityManager()
 const { em } = useEntityManager()
+const { imageManager } = useImageManager()
 
 
-const file = new File()
+const emit = defineEmits(['update', 'reload', 'reset', 'close'])
 
 
+/**
+ * Références à des composants
+ */
+const fileInput: Ref<null | any> = ref(null)
 const cropper: Ref<any> = ref(null)
 const cropper: Ref<any> = ref(null)
 
 
-const image: Ref<AnyJson> = ref({
+/**
+ * L'objet File contenant les informations de l'image
+ */
+const file: Ref<ApiResource | null> = ref(null)
+
+/**
+ * L'image elle-même
+ */
+const image: Ref<ArrayBuffer | string | null> = ref(null)
+
+/**
+ * L'objet File ou l'image sont en cours de chargement
+ */
+const pending: Ref<boolean> = ref(false)
+
+/**
+ * Affiche la modale d'upload / modification de l'image
+ */
+const showModal = ref(false)
+
+/**
+ * Données d'une nouvelle image uploadée par l'utilisateur
+ */
+const uploadedImage: Ref<AnyJson> = ref({
   id: null,
   id: null,
   src: null,
   src: null,
   file: null,
   file: null,
   name: null
   name: null
 })
 })
 
 
-const coordinates: Ref<AnyJson> = ref({})
+/**
+ * Coordonnées du cropper
+ */
+const coordinates: Ref<{ left?: number, top?: number, height?: number, width?: number }> = ref({})
 
 
-const defaultSize = ({ imageSize, visibleArea }: any) => {
-  return {
-    width: (visibleArea || imageSize).width,
-    height: (visibleArea || imageSize).height,
-  };
+/**
+ * @see https://advanced-cropper.github.io/vue-advanced-cropper/components/cropper.html#defaultposition
+ */
+const defaultPosition = () => {
+  return { left : coordinates.value.left, top : coordinates.value.top }
 }
 }
 
 
-// Si l'id est renseigné, on récupère l'Item File afin d'avoir les informations de config, le nom, etc.
-if (props.imageId && props.imageId > 0) {
-  const { apiRequestService } = useAp2iRequestService()
-  const result: any = await apiRequestService.get(UrlUtils.join('api/files', '' + props.imageId))
-
-  const config = JSON.parse(result.data.config)
-  coordinates.value.left = config.x
-  coordinates.value.top = config.y
-  coordinates.value.height = config.height
-  coordinates.value.width = config.width
-  image.value.name = result.data.name
-  image.value.id = result.data.id
+/**
+ * @see https://advanced-cropper.github.io/vue-advanced-cropper/components/cropper.html#defaultsize
+ */
+const defaultSize = (params: any): { width: number, height: number } | null => {
+  if (!params) {
+    return null
+  }
+  const { imageSize, visibleArea } = params
+
+  return {
+    width: coordinates.value.width ?? (visibleArea || imageSize).width,
+    height: coordinates.value.height ?? (visibleArea || imageSize).height
+  }
 }
 }
 
 
-//On récupère l'image...
-const { fetch } = useImageFetch()
-const { data: imageLoaded, pending } = fetch(props.imageId ?? null)
+const openModal = () => {
+  if (!file.value) {
+    return
+  }
 
 
-const unwatch: WatchStopHandle = watch(
-  imageLoaded,
-(newValue, oldValue) => {
-    if (newValue === oldValue || typeof newValue === 'undefined') {
-      return
-    }
-    image.value.src = newValue
+  if (file.value.config) {
+    const config = JSON.parse(file.value.config)
+    coordinates.value.left = config.x
+    coordinates.value.top = config.y
+    coordinates.value.height = config.height
+    coordinates.value.width = config.width
   }
   }
-)
+
+  uploadedImage.value.name = file.value.name
+  uploadedImage.value.id = file.value.id
+  uploadedImage.value.src = image.value
+
+  showModal.value = true
+
+  console.log(defaultSize.value)
+}
+
+
 
 
 /**
 /**
- * Quand l'utilisateur choisit une image sur sa machine
+ * Charge le File et l'image correspondante.
+ * Si le File possède une configuration, l'applique à l'image.
+ *
+ * @param id
+ */
+const loadFile = async (id: number | string) => {
+  pending.value = true
+  file.value = await em.fetch(File, id as number)
+  image.value = await imageManager.get(id as number)
+
+  pending.value = false
+}
+
+if (props.modelValue !== null) {
+  loadFile(props.modelValue)
+}
+
+/**
+ * Upload une image depuis le poste client
  * @param event
  * @param event
  */
  */
 const uploadImage = (event:any) => {
 const uploadImage = (event:any) => {
   const { files } = event.target
   const { files } = event.target
-  if (files && files[0]) {
-    reset()
-    image.value.name = files[0].name
-    image.value.src = URL.createObjectURL(files[0])
-    image.value.file = files[0]
+
+  if (!files || !files[0]) {
+    return
   }
   }
+
+  reset()
+  uploadedImage.value.name = files[0].name
+  uploadedImage.value.src = URL.createObjectURL(files[0])
+  uploadedImage.value.file = files[0]
 }
 }
 
 
 /**
 /**
  * Lorsque le cropper change de position / taille, on met à jour les coordonnées
  * Lorsque le cropper change de position / taille, on met à jour les coordonnées
  * @param config
  * @param config
  */
  */
-const onChange = ({ coordinates: config } : any) => {
+const onCropperChange = ({ coordinates: config } : any) => {
   coordinates.value = config;
   coordinates.value = config;
 }
 }
 
 
 /**
 /**
- * Lorsque l'on sauvegarde l'image
+ * Réinitialise l'image sélectionnée
  */
  */
-// TODO: Voir si tout ou partie de cette fonction peut passer dans le useImageFetch, imageManager ou imageUtils
-const save = async () => {
-  file.config = JSON.stringify({
+const reset = () => {
+  uploadedImage.value.src = null
+  uploadedImage.value.file = null
+  uploadedImage.value.name = null
+  uploadedImage.value.id = null
+  URL.revokeObjectURL(uploadedImage.value.src)
+}
+
+/**
+ * Annule l'upload et les modifications, et ferme la modale
+ */
+const cancel = () => {
+  reset()
+  showModal.value = false
+  emit('close')
+}
+
+/**
+ * Enregistre une nouvelle image
+ */
+const saveNewImage = async () => {
+  if (!file.value) {
+    throw new Error('No File object defined')
+  }
+
+  // On créé l'objet File à sauvegarder
+  file.value.name = uploadedImage.value.name
+  file.value.imgFieldName = props.field
+
+  // Construit la nouvelle config du fichier
+  file.value!!.config = JSON.stringify({
     x: coordinates.value.left,
     x: coordinates.value.left,
     y: coordinates.value.top,
     y: coordinates.value.top,
     height: coordinates.value.height,
     height: coordinates.value.height,
     width: coordinates.value.width
     width: coordinates.value.width
   })
   })
 
 
-  if (image.value.id > 0) {
-    // Mise à jour d'une image existante : on bouge simplement le cropper
+  // TODO: utiliser des enums?
+  file.value.visibility = 'EVERYBODY'
+  file.value.folder = 'IMAGES'
+  file.value.status = 'READY'
 
 
-    file.id = image.value.id as number
+  if (props.ownerId) {
+    // TODO: revoir
+    file.value.ownerId = props.ownerId
+  }
 
 
-    await em.persist(File, file) // TODO: à revoir
+  // TODO: A revoir, on doit pouvoir persister l'image aussi
+  const returnedFile = await em.persist(File, file.value)
 
 
-    // On émet un évent afin de mettre à jour le formulaire de départ
-    emit('reload')
+  //On émet un évent afin de mettre à jour le formulaire de départ
+  emit('update', returnedFile.data['@id'])
+}
 
 
-  } else {
-    // Création d'une nouvelle image
-    if (image.value.file) {
-
-      // On créé l'objet File à sauvegarder
-      file.name = image.value.name
-      file.imgFieldName = props.field
-      file.visibility = 'EVERYBODY'
-      file.folder = 'IMAGES'
-      file.status = 'READY'
-
-      if (props.ownerId) {
-        file.ownerId = props.ownerId
-      }
+/**
+ * Met à jour l'image existante
+ */
+const saveExistingImage = async () => {
+  if (!file.value) {
+    throw new Error('No File object defined')
+  }
 
 
-      const returnedFile = await em.persist(File, file) // TODO: à revoir, il faudra pouvoir passer `image.value.file` avec la requête
+  file.value.id = uploadedImage.value.id
 
 
-      //On émet un évent afin de mettre à jour le formulaire de départ
-      emit('update', returnedFile.data['@id'])
+  // Construit la nouvelle config du fichier
+  file.value!!.config = JSON.stringify({
+    x: coordinates.value.left,
+    y: coordinates.value.top,
+    height: coordinates.value.height,
+    width: coordinates.value.width
+  })
 
 
-    } else {
-      // On reset l'image : on a appuyé sur "poubelle" puis on enregistre
-      emit('reset')
-    }
-  }
+  await em.persist(File, file.value) // TODO: à revoir
+
+  // On émet un évent afin de mettre à jour le formulaire de départ
+  emit('reload')
 }
 }
 
 
 /**
 /**
- * On choisit de supprimer l'image présente
+ * Lorsque l'on sauvegarde l'image
  */
  */
-const reset = () => {
-  image.value.src = null
-  image.value.file = null
-  image.value.name = null
-  image.value.id = null
-  URL.revokeObjectURL(image.value.src)
+// TODO: Voir si tout ou partie de cette fonction peut passer dans le useImageFetch, imageManager ou imageUtils
+const save = async () => {
+  if (!file.value) {
+    throw new Error('No File object defined')
+  }
+
+  if (uploadedImage.value.src && uploadedImage.value.src !== image.value) {
+    // Une nouvelle image a été uploadée
+    await saveNewImage()
+  } else if (uploadedImage.value.id) {
+    // L'image existante a été modifiée
+    await saveExistingImage()
+  } else {
+    // On a reset l'image
+    emit('reset')
+  }
 }
 }
 
 
 /**
 /**
  * Lorsqu'on démonte le component on supprime le watcher et on revoke l'objet URL
  * Lorsqu'on démonte le component on supprime le watcher et on revoke l'objet URL
  */
  */
 onUnmounted(() => {
 onUnmounted(() => {
-  unwatch()
-    if (image.value.src) {
-      URL.revokeObjectURL(image.value.src)
-    }
+  if (uploadedImage.value && uploadedImage.value.src) {
+    URL.revokeObjectURL(uploadedImage.value.src)
+  }
 })
 })
 </script>
 </script>
 
 
-<style lang="scss">
+<style scoped lang="scss">
   .vue-advanced-cropper__stretcher{
   .vue-advanced-cropper__stretcher{
     height: auto !important;
     height: auto !important;
     width: auto !important;
     width: auto !important;
@@ -237,14 +389,15 @@ onUnmounted(() => {
   .loading{
   .loading{
     height: 300px;
     height: 300px;
   }
   }
+
   .upload {
   .upload {
     user-select: none;
     user-select: none;
     padding: 20px;
     padding: 20px;
     display: block;
     display: block;
     &__cropper {
     &__cropper {
        border: solid 1px rgb(var(--v-theme-on-neutral-strong));;
        border: solid 1px rgb(var(--v-theme-on-neutral-strong));;
-       min-height: 500px;
-       max-height: 500px;
+       min-height: 300px;
+       max-height: 300px;
      }
      }
     &__cropper-wrapper {
     &__cropper-wrapper {
        position: relative;
        position: relative;

+ 2 - 1
lang/fr.json

@@ -210,7 +210,8 @@
   "newSubDomain": "Nouveau sous domaine",
   "newSubDomain": "Nouveau sous domaine",
   "yourSubdomains": "Vos sous-domaines",
   "yourSubdomains": "Vos sous-domaines",
   "timezone": "Fuseau horaire",
   "timezone": "Fuseau horaire",
-  "qrCode": "QrCode pour la licence",
+  "qrCode": "QrCode",
+  "qrCodeForLicence": "QrCode pour la licence",
   "studentsAreAdherents": "Les élèves sont également adhérents de l'association",
   "studentsAreAdherents": "Les élèves sont également adhérents de l'association",
   "showAdherentList": "Afficher la liste des adhérents et leurs coordonnées",
   "showAdherentList": "Afficher la liste des adhérents et leurs coordonnées",
   "endCourseDate": "Date de fin des cours ",
   "endCourseDate": "Date de fin des cours ",

+ 1 - 0
package.json

@@ -46,6 +46,7 @@
     "sass": "^1.59.3",
     "sass": "^1.59.3",
     "uuid": "^9.0.0",
     "uuid": "^9.0.0",
     "vite-plugin-vuetify": "^1.0.2",
     "vite-plugin-vuetify": "^1.0.2",
+    "vue-advanced-cropper": "^2.8.8",
     "vue-tel-input-vuetify": "^1.5.3",
     "vue-tel-input-vuetify": "^1.5.3",
     "vue-the-mask": "^0.11.1",
     "vue-the-mask": "^0.11.1",
     "vuetify": "3.3.5",
     "vuetify": "3.3.5",

+ 1 - 1
pages/organization/index.vue

@@ -43,7 +43,7 @@ Contient toutes les informations sur l'organization courante
                     </UiHelp>
                     </UiHelp>
                   </div>
                   </div>
                   <UiImage
                   <UiImage
-                    :id="getIdFromUri(organization.logo)"
+                    :imageId="getIdFromUri(organization.logo)"
                     :upload="true"
                     :upload="true"
                     :width="200"
                     :width="200"
                     field="logo"
                     field="logo"

+ 5 - 0
plugins/vue-advanced-cropper.ts

@@ -0,0 +1,5 @@
+import { Cropper } from 'vue-advanced-cropper'
+import 'vue-advanced-cropper/dist/style.css';
+
+export default defineNuxtPlugin((nuxtApp) => {
+})

+ 24 - 0
yarn.lock

@@ -2858,6 +2858,11 @@ citty@^0.1.1:
   resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.1.tgz#2d722479b9a95453269165184dca6b2c5d808784"
   resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.1.tgz#2d722479b9a95453269165184dca6b2c5d808784"
   integrity sha512-fL/EEp9TyXlNkgYFQYNqtMJhnAk2tAq8lCST7O5LPn1NrzWPsOKE5wafR7J+8W87oxqolpxNli+w7khq5WP7tg==
   integrity sha512-fL/EEp9TyXlNkgYFQYNqtMJhnAk2tAq8lCST7O5LPn1NrzWPsOKE5wafR7J+8W87oxqolpxNli+w7khq5WP7tg==
 
 
+classnames@^2.2.6:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
+  integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
+
 clean-regexp@^1.0.0:
 clean-regexp@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7"
   resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7"
@@ -3312,6 +3317,11 @@ deasync@^0.1.15:
     bindings "^1.5.0"
     bindings "^1.5.0"
     node-addon-api "^1.7.1"
     node-addon-api "^1.7.1"
 
 
+debounce@^1.2.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
+  integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
+
 debug@2.6.9, debug@^2.6.8:
 debug@2.6.9, debug@^2.6.8:
   version "2.6.9"
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -3608,6 +3618,11 @@ eastasianwidth@^0.2.0:
   resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
   resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
   integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
   integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
 
 
+easy-bem@^1.0.2:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/easy-bem/-/easy-bem-1.1.1.tgz#1bfcc10425498090bcfddc0f9c000aba91399e03"
+  integrity sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A==
+
 editorconfig@^0.15.3:
 editorconfig@^0.15.3:
   version "0.15.3"
   version "0.15.3"
   resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5"
   resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5"
@@ -9220,6 +9235,15 @@ vscode-uri@^3.0.2:
   resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8"
   resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8"
   integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==
   integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==
 
 
+vue-advanced-cropper@^2.8.8:
+  version "2.8.8"
+  resolved "https://registry.yarnpkg.com/vue-advanced-cropper/-/vue-advanced-cropper-2.8.8.tgz#af0e8324312be5a1a92ce9fd3aff8264d28a5b33"
+  integrity sha512-yDM7Jb/gnxcs//JdbOogBUoHr1bhCQSto7/ohgETKAe4wvRpmqIkKSppMm1huVQr+GP1YoVlX/fkjKxvYzwwDQ==
+  dependencies:
+    classnames "^2.2.6"
+    debounce "^1.2.0"
+    easy-bem "^1.0.2"
+
 vue-bundle-renderer@^1.0.3:
 vue-bundle-renderer@^1.0.3:
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/vue-bundle-renderer/-/vue-bundle-renderer-1.0.3.tgz#422438aa4a024e2833e87a5a2d0b97c6c12fb2d8"
   resolved "https://registry.yarnpkg.com/vue-bundle-renderer/-/vue-bundle-renderer-1.0.3.tgz#422438aa4a024e2833e87a5a2d0b97c6c12fb2d8"