add img preview + fix navbar

This commit is contained in:
Matthieu
2025-10-16 10:26:36 +02:00
parent 8eada12438
commit 4ccc19505f
10 changed files with 479 additions and 344 deletions

View File

@@ -59,132 +59,120 @@
Squelettes de machine
</NuxtLink>
</li>
<li
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'pieces-mobile' }"
@mouseenter="setDropdown('pieces-mobile')"
@mouseleave="scheduleDropdownClose('pieces-mobile')"
@focusin="setDropdown('pieces-mobile')"
@focusout="scheduleDropdownClose('pieces-mobile')"
>
<div
tabindex="0"
role="button"
class="rounded-md px-2 py-1 transition-colors cursor-pointer"
@click="toggleDropdown('pieces-mobile')"
@keydown.enter.prevent="toggleDropdown('pieces-mobile')"
@keydown.space.prevent="toggleDropdown('pieces-mobile')"
<li class="mt-1 border-t border-base-200 pt-2">
<button
type="button"
class="flex w-full items-center justify-between rounded-md px-2 py-1 text-left transition-colors"
:class="
isActive('/piece-category') || isActive('/pieces-catalog')
? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
@click="toggleDropdown('pieces-mobile')"
@keydown.enter.prevent="toggleDropdown('pieces-mobile')"
@keydown.space.prevent="toggleDropdown('pieces-mobile')"
:aria-expanded="openDropdown === 'pieces-mobile'"
>
Pièces
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu menu-sm bg-base-100 rounded-box p-2 shadow space-y-1"
>
<li>
<NuxtLink
to="/pieces-catalog"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/pieces-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue des pièces
</NuxtLink>
</li>
<li>
<NuxtLink
to="/piece-category"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/piece-category')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catégorie de pièce
</NuxtLink>
</li>
</ul>
<span>Pièces</span>
<IconLucideChevronRight
class="h-4 w-4 transition-transform"
:class="openDropdown === 'pieces-mobile' ? 'rotate-90' : ''"
aria-hidden="true"
/>
</button>
<Transition name="nav-dropdown-mobile">
<ul
v-if="openDropdown === 'pieces-mobile'"
class="mt-2 space-y-1 rounded-md border border-base-200 bg-base-100 p-2 shadow-sm overflow-hidden"
>
<li>
<NuxtLink
to="/pieces-catalog"
class="rounded-md px-2 py-1 transition-colors block"
:class="
isActive('/pieces-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue des pièces
</NuxtLink>
</li>
<li>
<NuxtLink
to="/piece-category"
class="rounded-md px-2 py-1 transition-colors block"
:class="
isActive('/piece-category')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catégorie de pièce
</NuxtLink>
</li>
</ul>
</Transition>
</li>
<li
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'component-mobile' }"
@mouseenter="setDropdown('component-mobile')"
@mouseleave="scheduleDropdownClose('component-mobile')"
@focusin="setDropdown('component-mobile')"
@focusout="scheduleDropdownClose('component-mobile')"
>
<div
tabindex="0"
role="button"
class="rounded-md px-2 py-1 transition-colors cursor-pointer"
@click="toggleDropdown('component-mobile')"
@keydown.enter.prevent="toggleDropdown('component-mobile')"
@keydown.space.prevent="toggleDropdown('component-mobile')"
<li class="mt-1 border-t border-base-200 pt-2">
<button
type="button"
class="flex w-full items-center justify-between rounded-md px-2 py-1 text-left transition-colors"
:class="
isActive('/component-category') ||
isActive('/component-catalog')
isActive('/component-category') || isActive('/component-catalog')
? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
@click="toggleDropdown('component-mobile')"
@keydown.enter.prevent="toggleDropdown('component-mobile')"
@keydown.space.prevent="toggleDropdown('component-mobile')"
:aria-expanded="openDropdown === 'component-mobile'"
>
Composant
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu menu-sm bg-base-100 rounded-box p-2 shadow space-y-1"
>
<li>
<NuxtLink
to="/component-catalog"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/component-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue des composants
</NuxtLink>
</li>
<li>
<NuxtLink
to="/component-category"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/component-category')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catégorie de composant
</NuxtLink>
</li>
</ul>
<span>Composant</span>
<IconLucideChevronRight
class="h-4 w-4 transition-transform"
:class="openDropdown === 'component-mobile' ? 'rotate-90' : ''"
aria-hidden="true"
/>
</button>
<Transition name="nav-dropdown-mobile">
<ul
v-if="openDropdown === 'component-mobile'"
class="mt-2 space-y-1 rounded-md border border-base-200 bg-base-100 p-2 shadow-sm overflow-hidden"
>
<li>
<NuxtLink
to="/component-catalog"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/component-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue des composants
</NuxtLink>
</li>
<li>
<NuxtLink
to="/component-category"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/component-category')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catégorie de composant
</NuxtLink>
</li>
</ul>
</Transition>
</li>
<li
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'resources-mobile' }"
@mouseenter="setDropdown('resources-mobile')"
@mouseleave="scheduleDropdownClose('resources-mobile')"
@focusin="setDropdown('resources-mobile')"
@focusout="scheduleDropdownClose('resources-mobile')"
>
<div
tabindex="0"
role="button"
class="rounded-md px-2 py-1 transition-colors cursor-pointer"
@click="toggleDropdown('resources-mobile')"
@keydown.enter.prevent="toggleDropdown('resources-mobile')"
@keydown.space.prevent="toggleDropdown('resources-mobile')"
<li class="mt-1 border-t border-base-200 pt-2">
<button
type="button"
class="flex w-full items-center justify-between rounded-md px-2 py-1 text-left transition-colors"
:class="
isActive('/sites') ||
isActive('/documents') ||
@@ -192,53 +180,64 @@
? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
@click="toggleDropdown('resources-mobile')"
@keydown.enter.prevent="toggleDropdown('resources-mobile')"
@keydown.space.prevent="toggleDropdown('resources-mobile')"
:aria-expanded="openDropdown === 'resources-mobile'"
>
Ressources liées
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu menu-sm bg-base-100 rounded-box p-2 shadow space-y-1"
>
<li>
<NuxtLink
to="/sites"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/sites')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Sites
</NuxtLink>
</li>
<li>
<NuxtLink
to="/documents"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/documents')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Documents
</NuxtLink>
</li>
<li>
<NuxtLink
to="/constructeurs"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/constructeurs')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Constructeurs
</NuxtLink>
</li>
</ul>
<span>Ressources liées</span>
<IconLucideChevronRight
class="h-4 w-4 transition-transform"
:class="openDropdown === 'resources-mobile' ? 'rotate-90' : ''"
aria-hidden="true"
/>
</button>
<Transition name="nav-dropdown-mobile">
<ul
v-if="openDropdown === 'resources-mobile'"
class="mt-2 space-y-1 rounded-md border border-base-200 bg-base-100 p-2 shadow-sm overflow-hidden"
>
<li>
<NuxtLink
to="/sites"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/sites')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Sites
</NuxtLink>
</li>
<li>
<NuxtLink
to="/documents"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/documents')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Documents
</NuxtLink>
</li>
<li>
<NuxtLink
to="/constructeurs"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/constructeurs')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Constructeurs
</NuxtLink>
</li>
</ul>
</Transition>
</li>
</ul>
</div>
@@ -297,131 +296,138 @@
</NuxtLink>
</li>
<li
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'pieces-desktop' }"
class="relative"
@mouseenter="setDropdown('pieces-desktop')"
@mouseleave="scheduleDropdownClose('pieces-desktop')"
@focusin="setDropdown('pieces-desktop')"
@focusout="scheduleDropdownClose('pieces-desktop')"
>
<div
tabindex="0"
role="button"
class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer"
@click="toggleDropdown('pieces-desktop')"
@keydown.enter.prevent="toggleDropdown('pieces-desktop')"
@keydown.space.prevent="toggleDropdown('pieces-desktop')"
<button
type="button"
class="inline-flex items-center gap-1 rounded-md px-3 py-2 transition-colors"
:class="
isActive('/piece-category') || isActive('/pieces-catalog')
? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
@click="toggleDropdown('pieces-desktop')"
@keydown.enter.prevent="toggleDropdown('pieces-desktop')"
@keydown.space.prevent="toggleDropdown('pieces-desktop')"
:aria-expanded="openDropdown === 'pieces-desktop'"
>
Pièces
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-60"
>
<li>
<NuxtLink
to="/piece-category"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/piece-category')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catégorie de pièce
</NuxtLink>
</li>
<li>
<NuxtLink
to="/pieces-catalog"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/pieces-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue des pièces
</NuxtLink>
</li>
</ul>
<IconLucideChevronRight
class="h-4 w-4 transition-transform"
:class="openDropdown === 'pieces-desktop' ? 'rotate-90' : ''"
aria-hidden="true"
/>
</button>
<Transition name="nav-dropdown-desktop">
<ul
v-if="openDropdown === 'pieces-desktop'"
class="absolute left-0 top-full mt-2 w-60 rounded-lg border border-base-200 bg-base-100 p-2 shadow-lg z-50"
>
<li>
<NuxtLink
to="/piece-category"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/piece-category')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catégorie de pièce
</NuxtLink>
</li>
<li>
<NuxtLink
to="/pieces-catalog"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/pieces-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue des pièces
</NuxtLink>
</li>
</ul>
</Transition>
</li>
<li
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'component-desktop' }"
class="relative"
@mouseenter="setDropdown('component-desktop')"
@mouseleave="scheduleDropdownClose('component-desktop')"
@focusin="setDropdown('component-desktop')"
@focusout="scheduleDropdownClose('component-desktop')"
>
<div
tabindex="0"
role="button"
class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer"
@click="toggleDropdown('component-desktop')"
@keydown.enter.prevent="toggleDropdown('component-desktop')"
@keydown.space.prevent="toggleDropdown('component-desktop')"
<button
type="button"
class="inline-flex items-center gap-1 rounded-md px-3 py-2 transition-colors"
:class="
isActive('/component-category') ||
isActive('/component-catalog')
? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
@click="toggleDropdown('component-desktop')"
@keydown.enter.prevent="toggleDropdown('component-desktop')"
@keydown.space.prevent="toggleDropdown('component-desktop')"
:aria-expanded="openDropdown === 'component-desktop'"
>
Composant
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-64"
>
<li>
<NuxtLink
to="/component-category"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/component-category')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catégorie de composant
</NuxtLink>
</li>
<li>
<NuxtLink
to="/component-catalog"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/component-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue des composants
</NuxtLink>
</li>
</ul>
<IconLucideChevronRight
class="h-4 w-4 transition-transform"
:class="openDropdown === 'component-desktop' ? 'rotate-90' : ''"
aria-hidden="true"
/>
</button>
<Transition name="nav-dropdown-desktop">
<ul
v-if="openDropdown === 'component-desktop'"
class="absolute left-0 top-full mt-2 w-64 rounded-lg border border-base-200 bg-base-100 p-2 shadow-lg z-50"
>
<li>
<NuxtLink
to="/component-category"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/component-category')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catégorie de composant
</NuxtLink>
</li>
<li>
<NuxtLink
to="/component-catalog"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/component-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue des composants
</NuxtLink>
</li>
</ul>
</Transition>
</li>
<li
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'resources-desktop' }"
class="relative"
@mouseenter="setDropdown('resources-desktop')"
@mouseleave="scheduleDropdownClose('resources-desktop')"
@focusin="setDropdown('resources-desktop')"
@focusout="scheduleDropdownClose('resources-desktop')"
>
<div
tabindex="0"
role="button"
class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer"
@click="toggleDropdown('resources-desktop')"
@keydown.enter.prevent="toggleDropdown('resources-desktop')"
@keydown.space.prevent="toggleDropdown('resources-desktop')"
<button
type="button"
class="inline-flex items-center gap-1 rounded-md px-3 py-2 transition-colors"
:class="
isActive('/sites') ||
isActive('/documents') ||
@@ -429,53 +435,64 @@
? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
@click="toggleDropdown('resources-desktop')"
@keydown.enter.prevent="toggleDropdown('resources-desktop')"
@keydown.space.prevent="toggleDropdown('resources-desktop')"
:aria-expanded="openDropdown === 'resources-desktop'"
>
Ressources liées
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-60"
>
<li>
<NuxtLink
to="/sites"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/sites')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Sites
</NuxtLink>
</li>
<li>
<NuxtLink
to="/documents"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/documents')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Documents
</NuxtLink>
</li>
<li>
<NuxtLink
to="/constructeurs"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/constructeurs')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Constructeurs
</NuxtLink>
</li>
</ul>
<IconLucideChevronRight
class="h-4 w-4 transition-transform"
:class="openDropdown === 'resources-desktop' ? 'rotate-90' : ''"
aria-hidden="true"
/>
</button>
<Transition name="nav-dropdown-desktop">
<ul
v-if="openDropdown === 'resources-desktop'"
class="absolute left-0 top-full mt-2 w-60 rounded-lg border border-base-200 bg-base-100 p-2 shadow-lg z-50"
>
<li>
<NuxtLink
to="/sites"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/sites')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Sites
</NuxtLink>
</li>
<li>
<NuxtLink
to="/documents"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/documents')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Documents
</NuxtLink>
</li>
<li>
<NuxtLink
to="/constructeurs"
class="block rounded-md px-2 py-1 transition-colors"
:class="
isActive('/constructeurs')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Constructeurs
</NuxtLink>
</li>
</ul>
</Transition>
</li>
</ul>
</div>
@@ -567,7 +584,7 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue";
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { useRoute, navigateTo, useRuntimeConfig } from "#imports";
import { useProfileSession } from "~/composables/useProfileSession";
import IconLucideMenu from "~icons/lucide/menu";
@@ -656,6 +673,13 @@ const toggleDropdown = (name) => {
setDropdown(name);
};
watch(
() => route.fullPath,
() => {
closeDropdownNow();
},
);
const activeProfileLabel = computed(() => {
if (!activeProfile.value) {
return "Profil inconnu";
@@ -685,3 +709,35 @@ onUnmounted(() => {
}
});
</script>
<style scoped>
.nav-dropdown-desktop-enter-active,
.nav-dropdown-desktop-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.nav-dropdown-desktop-enter-from,
.nav-dropdown-desktop-leave-to {
opacity: 0;
transform: translateY(0.25rem);
}
.nav-dropdown-desktop-enter-to,
.nav-dropdown-desktop-leave-from {
opacity: 1;
transform: translateY(0);
}
.nav-dropdown-mobile-enter-active,
.nav-dropdown-mobile-leave-active {
transition: max-height 0.2s ease, opacity 0.2s ease;
}
.nav-dropdown-mobile-enter-from,
.nav-dropdown-mobile-leave-to {
max-height: 0;
opacity: 0;
}
.nav-dropdown-mobile-enter-to,
.nav-dropdown-mobile-leave-from {
max-height: 12rem;
opacity: 1;
}
</style>

View File

@@ -214,13 +214,21 @@
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
>
<div class="flex items-center gap-3 text-sm">
<span class="text-xl" :class="documentIcon(document).colorClass">
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center">
<img
v-if="isImageDocument(document) && document.path"
:src="document.path"
class="h-full w-full object-cover"
:alt="`Aperçu de ${document.name}`"
>
<component
v-else
:is="documentIcon(document).component"
class="h-6 w-6"
:class="documentIcon(document).colorClass"
aria-hidden="true"
/>
</span>
</div>
<div>
<div class="font-medium">
{{ document.name }}
@@ -309,7 +317,7 @@ import DocumentUpload from './DocumentUpload.vue'
import ConstructeurSelect from './ConstructeurSelect.vue'
import { useDocuments } from '~/composables/useDocuments'
import { getFileIcon } from '~/utils/fileIcons'
import { canPreviewDocument } from '~/utils/documentPreview'
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
import IconLucideChevronRight from '~icons/lucide/chevron-right'
import { useCustomFields } from '~/composables/useCustomFields'

View File

@@ -37,12 +37,21 @@
<ul v-if="selectedFiles.length" class="mt-4 w-full space-y-2 text-left">
<li v-for="file in selectedFiles" :key="file.name" class="flex items-center justify-between text-sm">
<div class="flex items-center gap-3">
<component
:is="getIcon(file).component"
class="h-6 w-6"
:class="getIcon(file).colorClass"
aria-hidden="true"
/>
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-300 bg-base-200/70 flex items-center justify-center">
<img
v-if="isImageFile(file)"
:src="getFilePreview(file)"
class="h-full w-full object-cover"
:alt="`Aperçu de ${file.name}`"
>
<component
v-else
:is="getIcon(file).component"
class="h-6 w-6"
:class="getIcon(file).colorClass"
aria-hidden="true"
/>
</div>
<div class="flex flex-col">
<span class="font-medium">{{ file.name }}</span>
<span class="text-xs text-gray-500">{{ formatSize(file.size) }} {{ file.type || 'Type inconnu' }}</span>
@@ -58,7 +67,7 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onBeforeUnmount } from 'vue'
import { useToast } from '~/composables/useToast'
import { getFileIcon } from '~/utils/fileIcons'
import IconLucideCloudUpload from '~icons/lucide/cloud-upload'
@@ -96,6 +105,30 @@ const dragActive = ref(false)
const fileInput = ref(null)
const internalFiles = ref([])
const { showError } = useToast()
const previewUrls = new Map()
const isImageFile = (file) => (file?.type || '').startsWith('image/')
const getFilePreview = (file) => {
if (!isImageFile(file)) { return null }
if (!previewUrls.has(file)) {
previewUrls.set(file, URL.createObjectURL(file))
}
return previewUrls.get(file)
}
const cleanupRemovedPreviews = (previousFiles = [], nextFiles = []) => {
const nextSet = new Set(nextFiles)
previousFiles.forEach((file) => {
if (!nextSet.has(file)) {
const url = previewUrls.get(file)
if (url) {
URL.revokeObjectURL(url)
previewUrls.delete(file)
}
}
})
}
const selectedFiles = computed(() => internalFiles.value)
@@ -103,6 +136,7 @@ watch(
() => props.modelValue,
(newValue) => {
if (Array.isArray(newValue)) {
cleanupRemovedPreviews(internalFiles.value, newValue)
internalFiles.value = [...newValue]
}
},
@@ -114,6 +148,7 @@ const triggerFileDialog = () => {
}
const emitFiles = (files) => {
cleanupRemovedPreviews(internalFiles.value, files)
internalFiles.value = files
emit('update:modelValue', files)
emit('files-added', files)
@@ -180,4 +215,11 @@ const formatSize = (size) => {
const getIcon = (file) => {
return getFileIcon({ name: file.name, mime: file.type })
}
onBeforeUnmount(() => {
previewUrls.forEach((url) => {
URL.revokeObjectURL(url)
})
previewUrls.clear()
})
</script>

View File

@@ -276,13 +276,21 @@
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
>
<div class="flex items-center gap-3 text-sm">
<span class="text-xl" :class="documentIcon(document).colorClass">
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center">
<img
v-if="isImageDocument(document) && document.path"
:src="document.path"
class="h-full w-full object-cover"
:alt="`Aperçu de ${document.name}`"
>
<component
v-else
:is="documentIcon(document).component"
class="h-6 w-6"
:class="documentIcon(document).colorClass"
aria-hidden="true"
/>
</span>
</div>
<div>
<div class="font-medium">
{{ document.name }}
@@ -340,7 +348,7 @@ import { useCustomFields } from "~/composables/useCustomFields";
import { useToast } from "~/composables/useToast";
import { useDocuments } from "~/composables/useDocuments";
import { getFileIcon } from "~/utils/fileIcons";
import { canPreviewDocument } from "~/utils/documentPreview";
import { canPreviewDocument, isImageDocument } from "~/utils/documentPreview";
import DocumentUpload from "~/components/DocumentUpload.vue";
import DocumentPreviewModal from "~/components/DocumentPreviewModal.vue";
import IconLucidePackage from "~icons/lucide/package";

View File

@@ -53,9 +53,21 @@
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
>
<div class="flex items-center gap-3 text-sm">
<span class="text-xl" :class="documentIcon(document).colorClass">
<component :is="documentIcon(document).component" class="h-6 w-6" aria-hidden="true" />
</span>
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center">
<img
v-if="isImageDocument(document) && document.path"
:src="document.path"
class="h-full w-full object-cover"
:alt="`Aperçu de ${document.name}`"
>
<component
v-else
:is="documentIcon(document).component"
class="h-6 w-6"
:class="documentIcon(document).colorClass"
aria-hidden="true"
/>
</div>
<div>
<div class="font-medium">
{{ document.name }}
@@ -103,6 +115,7 @@
<script setup>
import { computed, toRefs } from 'vue'
import { isImageDocument } from '~/utils/documentPreview'
import DocumentUpload from '~/components/DocumentUpload.vue'
import SiteContactFormFields from '~/components/sites/SiteContactFormFields.vue'

View File

@@ -300,13 +300,21 @@
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
>
<div class="flex items-center gap-3 text-sm">
<span class="text-xl" :class="documentIcon(document).colorClass">
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center">
<img
v-if="isImageDocument(document) && document.path"
:src="document.path"
class="h-full w-full object-cover"
:alt="`Aperçu de ${document.name}`"
>
<component
v-else
:is="documentIcon(document).component"
class="h-6 w-6"
:class="documentIcon(document).colorClass"
aria-hidden="true"
/>
</span>
</div>
<div>
<div class="font-medium">
{{ document.name }}
@@ -379,7 +387,7 @@ import { formatStructurePreview } from '~/shared/modelUtils'
import type { ComponentModelStructure } from '~/shared/types/inventory'
import type { ModelType } from '~/services/modelTypes'
import { getFileIcon } from '~/utils/fileIcons'
import { canPreviewDocument } from '~/utils/documentPreview'
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
interface ComponentCatalogType extends ModelType {
structure: ComponentModelStructure | null

View File

@@ -265,13 +265,21 @@
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
>
<div class="flex items-center gap-3 text-sm">
<span class="text-xl" :class="documentIcon(document).colorClass">
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center">
<img
v-if="isImageDocument(document) && document.path"
:src="document.path"
class="h-full w-full object-cover"
:alt="`Aperçu de ${document.name}`"
>
<component
v-else
:is="documentIcon(document).component"
class="h-6 w-6"
:class="documentIcon(document).colorClass"
aria-hidden="true"
/>
</span>
</div>
<div>
<div class="font-medium">{{ document.name }}</div>
<div class="text-xs text-gray-500">
@@ -517,7 +525,7 @@ import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
import { getFileIcon } from '~/utils/fileIcons'
import { sanitizeDefinitionOverrides } from '~/shared/modelUtils'
import { canPreviewDocument } from '~/utils/documentPreview'
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
import ComponentHierarchy from '~/components/ComponentHierarchy.vue'
import DocumentUpload from '~/components/DocumentUpload.vue'
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'

View File

@@ -840,18 +840,6 @@ const formatAssignmentList = (assignments) => {
.join(', ')
}
const selectedComponentIds = computed(() => {
const ids = []
Object.values(componentRequirementSelections).forEach((entries) => {
;(entries || []).forEach((entry) => {
if (entry?.composantId) {
ids.push(entry.composantId)
}
})
})
return ids
})
const selectedPieceIds = computed(() => {
const ids = []
Object.values(pieceRequirementSelections).forEach((entries) => {
@@ -866,21 +854,15 @@ const selectedPieceIds = computed(() => {
const getComponentOptions = (requirement, currentEntry) => {
const requirementTypeId = requirement?.typeComposantId || requirement?.typeComposant?.id || null
const usedIds = new Set(
selectedComponentIds.value.filter((id) => id && (!currentEntry || id !== currentEntry.composantId)),
)
return componentInventory.value.filter((component) => {
if (!component?.id) {
return false
}
if (requirementTypeId && component.typeComposantId !== requirementTypeId) {
return false
return currentEntry?.composantId === component.id
}
if (!component.id) {
return false
}
if (currentEntry?.composantId === component.id) {
return true
}
return !usedIds.has(component.id)
return true
})
}

View File

@@ -270,13 +270,21 @@
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
>
<div class="flex items-center gap-3 text-sm">
<span class="text-xl" :class="documentIcon(document).colorClass">
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center">
<img
v-if="isImageDocument(document) && document.path"
:src="document.path"
class="h-full w-full object-cover"
:alt="`Aperçu de ${document.name}`"
>
<component
v-else
:is="documentIcon(document).component"
class="h-6 w-6"
:class="documentIcon(document).colorClass"
aria-hidden="true"
/>
</span>
</div>
<div>
<div class="font-medium">
{{ document.name }}
@@ -346,7 +354,7 @@ import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
import { getFileIcon } from '~/utils/fileIcons'
import { canPreviewDocument } from '~/utils/documentPreview'
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
import { formatPieceStructurePreview } from '~/shared/modelUtils'
import type { PieceModelStructure } from '~/shared/types/inventory'
import type { ModelType } from '~/services/modelTypes'

View File

@@ -17,6 +17,8 @@ export const getPreviewType = (document) => {
export const canPreviewDocument = (document = {}) => !!getPreviewType(document)
export const isImageDocument = (document = {}) => getPreviewType(document) === 'image'
export const describeDocument = (document) => {
if (!document) { return '' }
const name = document.filename || document.name || ''