Files
Kevin c4cad4f875
Build & Deploy / build (push) Successful in 32s
Build & Deploy / docker (push) Successful in 1m42s
fix: TS errors, serialize calls, unused imports, pre-commit hook
2026-07-07 20:49:52 +02:00

299 lines
7.7 KiB
Vue

<template>
<div class="relative w-full" :id="props.galleryId">
<div v-if="editMode" class="grid grid-cols-3 gap-4 w-full min-h-10">
<div
v-for="image in combinedImages"
:key="getKeyForImage(image)"
class="relative cursor-move group"
draggable="true"
@dragstart="(event) => dragStart(event, combinedImages.indexOf(image))"
@dragover.prevent
@dragenter.stop.prevent="handleDragEnter(combinedImages.indexOf(image))"
@drop.stop.prevent="drop"
>
<img :src="getImageSrc(image)" class="w-full h-66 object-cover rounded-lg" />
<button
v-if="editMode"
class="absolute top-2 right-2 bg-white text-gray-800 w-6 h-6 flex items-center justify-center p-0 rounded-full hover:bg-gray-300 transition-opacity opacity-0 group-hover:opacity-100"
@click.stop.prevent="removeImage(image)"
>
<img :src="crossIcon" alt="Delete" class="h-4 w-4" />
</button>
</div>
</div>
<div v-else>
<Carousel v-bind="adjustedSettings" v-if="combinedImages.length != 0" ref="carousel">
<Slide v-for="image in combinedImages" :key="getKeyForImage(image)">
<div class="carousel__item">
<img :src="getImageSrc(image)" alt="" class="carousel-image" />
</div>
</Slide>
<template #addons>
<Navigation />
<Pagination />
</template>
</Carousel>
</div>
<button
v-if="editMode"
@click.stop.prevent="showModal = true"
class="py-2 px-4 bg-indigo-600 text-white font-semibold rounded hover:bg-blue-700 absolute bottom-0 right-0 z-50"
>
+
</button>
<div
v-if="showModal"
class="fixed inset-0 bg-black/50 flex justify-center items-center z-50"
>
<div class="bg-white p-6 rounded shadow-lg relative w-1/4">
<button
@click.prevent="resetModal"
class="absolute top-2 right-2 text-gray-600 hover:text-gray-900"
>
&times;
</button>
<h3 class="text-lg font-semibold mb-4">Add a New Image</h3>
<div class="flex items-center">
<input type="file" ref="fileInput" @change="handleFileUpload" class="hidden" />
<button
@click.prevent="triggerFileInput"
class="bg-blue-500 text-white rounded hover:bg-blue-700 h-10 px-4"
>
Browse
</button>
<div
v-if="fileName"
class="ml-4 p-2 border border-gray-300 rounded flex items-center h-10 w-full overflow-hidden whitespace-nowrap text-ellipsis"
>
{{ formatFileName(fileName) }}
</div>
</div>
<button
@click.prevent="addImage"
class="w-full mt-4 bg-blue-500 text-white p-2 rounded hover:bg-blue-700"
>
Add Image
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { crossIcon } from '~/assets/icons'
import { Navigation, Pagination } from 'vue3-carousel'
import type { Image, NewImage, ServerImage } from '~/pages/types/UITypes'
const props = defineProps<{
galleryId: string
serverImages: ServerImage[]
editMode?: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const fileInput = ref<null | HTMLInputElement>(null)
const serverImages = ref<ServerImage[]>([...props.serverImages])
const newImages = ref<NewImage[]>([])
const showModal = ref(false)
const newImageFile = ref<File | null>(null)
const dragIndex = ref<number | null>(null)
const fileName = ref('')
const editMode = props.editMode ?? false
const combinedImages = computed(() => [...serverImages.value, ...newImages.value])
watch(
[serverImages, newImages],
() => {
reassignOrder()
emit('update:modelValue', combinedImages.value)
},
{ deep: true }
)
function handleFileUpload(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0]
if (file) {
fileName.value = file.name
newImageFile.value = file
}
}
function addImage() {
if (newImageFile.value) {
newImages.value.push({
file: newImageFile.value,
order: combinedImages.value.length + 1,
})
resetModal()
}
}
function removeImage(image: Image) {
if ('id' in image) {
const index = serverImages.value.findIndex((img: ServerImage) => img.id === image.id)
if (index !== -1) {
serverImages.value.splice(index, 1)
}
} else {
const index = newImages.value.findIndex((img: NewImage) => img.file === image.file)
if (index !== -1) {
newImages.value.splice(index, 1)
}
}
}
function resetModal() {
newImageFile.value = null
fileName.value = ''
showModal.value = false
}
function triggerFileInput() {
fileInput.value?.click()
}
function dragStart(event: DragEvent, index: number) {
dragIndex.value = index
event.dataTransfer!.effectAllowed = 'move'
}
function handleDragEnter(index: number) {
if (dragIndex.value !== null && index !== dragIndex.value) {
const sourceArray =
dragIndex.value < serverImages.value.length ? serverImages.value : newImages.value
const draggedItem = sourceArray.splice(dragIndex.value, 1)[0]
sourceArray.splice(index, 0, draggedItem as any) // Utilisation de 'as any' pour r├®soudre le probl├¿me de typage
dragIndex.value = index
}
}
function drop() {
dragIndex.value = null
reassignOrder()
}
function reassignOrder() {
combinedImages.value.forEach((image, index) => {
image.order = index + 1
})
}
function getImageSrc(image: Image): string {
return 'link' in image ? image.link : URL.createObjectURL(image.file)
}
function getKeyForImage(image: Image): string {
return 'id' in image ? `server-${image.id}` : `new-${image.file.name}`
}
function formatFileName(fileName: string): string {
const maxLength = 39 // Ajustez cette valeur selon vos besoins
const extension = fileName.split('.').pop()
const nameWithoutExtension = fileName.slice(0, -(extension!.length + 1))
if (fileName.length <= maxLength) {
return fileName
}
const truncatedName = nameWithoutExtension.slice(-maxLength + extension!.length + 3)
return `...${truncatedName}.${extension}`
}
// Carousel settings
// Carousel settings
const settings = {
itemsToShow: 2.5,
snapAlign: 'center',
wrapAround: true,
transition: 500,
}
const adjustedSettings = computed(() => {
const imageCount = combinedImages.value.length
if (imageCount === 1) {
return { ...settings, itemsToShow: 1, wrapAround: false }
} else {
return settings
}
})
</script>
<style scoped>
.carousel__item {
min-height: 310px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.carousel-image {
height: 310px;
object-fit: cover;
object-position: center;
}
.carousel__slide {
padding: 5px;
}
.carousel__viewport {
perspective: 2000px;
}
.carousel__track {
transform-style: preserve-3d;
}
.carousel__slide--sliding {
transition: 0.5s;
}
.carousel__slide {
opacity: 0.9;
transform: rotateY(-20deg) scale(0.9);
}
.carousel__slide--active ~ .carousel__slide {
transform: rotateY(20deg) scale(0.9);
}
.carousel__slide--active {
opacity: 1;
transform: rotateY(0) scale(1.1);
}
.carousel-nav-button {
background-color: var(--color-gray-50);
border-radius: var(--radius-full);
padding: 10px;
box-shadow: var(--shadow-sm);
transition: background-color var(--duration-base) var(--ease-in-out);
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.carousel-nav-button:hover {
background-color: var(--color-gray-100);
}
.carousel-nav-button svg {
width: 24px;
height: 24px;
}
.carousel-nav-button-prev {
left: -30px; /* Adjust as needed */
}
.carousel-nav-button-next {
right: -30px; /* Adjust as needed */
}
</style>