Initial commit: PC Builder AdonisJS project

This commit is contained in:
Kevin
2026-06-28 10:41:51 +02:00
commit 648d0af538
144 changed files with 18570 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
<script setup lang="ts">
defineProps<{
authError?: UIAuthError
}>()
</script>
<template>
<div
v-if="authError"
class="bg-red-100 border border-red-400 text-red-700 text-center text-sm rounded relative px-4 py-1 mt-1 w-full flex items-center justify-center space-x-2"
>
<div class="h-4 w-4 shrink-0">
<svg
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 554.2 554.199"
xml:space="preserve"
fill="currentColor"
>
<g>
<path
d="M538.5,386.199L356.5,70.8c-16.4-28.4-46.7-45.9-79.501-45.9c-32.8,0-63.1,17.5-79.5,45.9L12.3,391.6
c-16.4,28.4-16.4,63.4,0,91.8C28.7,511.8,59,529.3,91.8,529.3H462.2c0.101,0,0.2,0,0.2,0c50.7,0,91.8-41.101,91.8-91.8
C554.2,418.5,548.4,400.8,538.5,386.199z M316.3,416.899c0,21.7-16.7,38.3-39.2,38.3s-39.2-16.6-39.2-38.3V416
c0-21.601,16.7-38.301,39.2-38.301S316.3,394.3,316.3,416V416.899z M317.2,158.7L297.8,328.1c-1.3,12.2-9.4,19.8-20.7,19.8
s-19.4-7.7-20.7-19.8L237,158.6c-1.3-13.1,5.801-23,18-23H299.1C311.3,135.7,318.5,145.6,317.2,158.7z"
/>
</g>
</svg>
</div>
<span class="block sm:inline whitespace-nowrap">{{ authError.message }}</span>
</div>
</template>
<style scoped></style>
+156
View File
@@ -0,0 +1,156 @@
<script setup lang="ts">
import { computed, defineEmits, defineProps, onMounted, ref, watch } from 'vue'
import { Component } from '~/pages/types/UITypes'
interface ComponentsByType {
type: string
components: Component[]
}
const props = defineProps<{
availableComponents: Component[]
selectedComponents: number[]
initialSelectedComponents?: Component[]
}>()
const emit = defineEmits(['update:components'])
const componentTypeOrder = [
'Processor',
'Motherboard',
'Graphic Card',
'Memory',
'HDD',
'SSD',
'Cooler',
'Power Supply',
'Case',
'WiFi-Card',
'Fan',
'Other',
]
// Fusionner les composants disponibles et initialement s├®lectionn├®s
const allComponents = computed(() => {
const mergedComponents = [...props.availableComponents]
if (props.initialSelectedComponents) {
props.initialSelectedComponents.forEach((component) => {
if (!mergedComponents.find((c) => c.id === component.id)) {
mergedComponents.push(component)
}
})
}
return mergedComponents
})
// Trier les composants par type
const componentsByType = computed(() => {
const groupedComponents: Record<string, Component[]> = {}
allComponents.value.forEach((component) => {
const type = component.type.name
if (!groupedComponents[type]) {
groupedComponents[type] = []
}
groupedComponents[type].push(component)
})
const sortedComponentsByType: ComponentsByType[] = componentTypeOrder.map((type) => ({
type,
components: groupedComponents[type] || [],
}))
return sortedComponentsByType.filter((group) => group.components.length > 0)
})
// Initialisation de selectedComponents
let selectedComponents = ref<number[]>(
props.initialSelectedComponents?.map((c) => c.id) || props.selectedComponents || []
)
// Synchroniser les props avec le composant
watch(
() => props.selectedComponents,
(newVal) => {
selectedComponents.value = newVal
}
)
onMounted(() => {
if (props.initialSelectedComponents) {
emit(
'update:components',
props.initialSelectedComponents.map((c) => c.id)
)
}
})
function updateComponents(event: Event, type: string) {
const selectedComponentId = Number((event.target as HTMLSelectElement).value)
const updatedComponents = [...selectedComponents.value]
const alreadyInListComponentIndex = updatedComponents.findIndex((id) => {
return selectedComponentId === id
})
if (alreadyInListComponentIndex !== -1) {
updatedComponents.splice(alreadyInListComponentIndex, 1)
} else {
const typeIndex = componentsByType.value.findIndex((item) => item.type === type)
const existingComponentIndex = updatedComponents.findIndex((id) => {
const component = componentsByType.value[typeIndex].components.find((c) => c.id === id)
return component && component.type.unique
})
if (existingComponentIndex !== -1) updatedComponents.splice(existingComponentIndex, 1)
if (selectedComponentId != 0) updatedComponents.push(Number(selectedComponentId))
}
selectedComponents.value = updatedComponents
emit('update:components', updatedComponents)
}
</script>
<template>
<div v-for="{ type, components } in componentsByType" :key="type">
<label :for="`select-${type}`" class="block text-gray-700 text-sm font-bold mb-1 mt-2">
{{ type }}
</label>
<div v-if="components[0]?.type.unique">
<!-- Utilisation d'un select pour les composants uniques -->
<select
@change="updateComponents($event, type)"
:id="`select-${type}`"
class="w-full mt-2 border-gray-200 focus:border-indigo-600 focus:ring-indigo-500 px-3 py-2 focus:ring-1 border rounded text-gray-700 bg-white"
>
<option value="" selected>Select a component</option>
<option
v-for="component in components"
:key="component.id"
:value="component.id"
:selected="selectedComponents.includes(component.id)"
>
{{ component.name }}
</option>
</select>
</div>
<div v-else>
<!-- Utilisation de cases á cocher pour les composants non uniques avec grille responsive -->
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div v-for="component in components" :key="component.id" class="flex items-center mt-1">
<input
type="checkbox"
@click="updateComponents($event, type)"
:id="`checkbox-${component.id}`"
:value="component.id"
v-model="selectedComponents"
class="mr-2"
:checked="selectedComponents.includes(component.id)"
/>
<label :for="`checkbox-${component.id}`" class="text-sm text-gray-700">
{{ component.name }}
</label>
</div>
</div>
</div>
</div>
</template>
+182
View File
@@ -0,0 +1,182 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import TableComponent from '~/pages/widgets/TableComponent.vue'
import Component from '#models/component'
import ComponentType from '#models/component_type'
import { usePage } from '@inertiajs/vue3'
const { props } = usePage<{ components: Component[] }>()
const { components } = props
const usageDropdownOpen = ref(false)
const typeDropdownOpen = ref(false)
function toggleDropdown(dropdown: any) {
if (dropdown === 'usage') {
usageDropdownOpen.value = !usageDropdownOpen.value
typeDropdownOpen.value = false
} else if (dropdown === 'type') {
typeDropdownOpen.value = !typeDropdownOpen.value
usageDropdownOpen.value = false
}
}
const componentTypes = computed(() => {
const types = new Map<number, ComponentType>()
components.forEach((component) => {
if (!types.has(component.type.id)) {
types.set(component.type.id, component.type)
}
})
return Array.from(types.values())
})
// D├®finir le type de l'objet filters
interface Filters {
showUsed: boolean
showUnused: boolean
types: { [key: number]: boolean }
}
const filters = reactive<Filters>({
showUsed: false,
showUnused: true,
types: componentTypes.value.reduce(
(acc, type) => {
acc[type.id] = true
return acc
},
{} as { [key: number]: boolean }
),
})
const filteredComponents = computed(() => {
return components.filter((component) => {
const isUsed = filters.showUsed && component.computerId !== null
const isUnused = filters.showUnused && component.computerId === null
const isTypeSelected = filters.types[component.type.id]
return (isUsed || isUnused) && isTypeSelected
})
})
</script>
<template>
<div>
<!-- Dropdown pour les filtres -->
<div class="relative">
<h4 class="text-xl underline">Filters</h4>
<div class="flex gap-2">
<!-- Usage Dropdown -->
<div id="usage-selector" class="relative" style="width: fit-content">
<button
@click="toggleDropdown('usage')"
class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-blue-700 transition text-center inline-flex items-center"
type="button"
>
Usage
<svg
class="w-4 h-4 ml-2"
aria-hidden="true"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
></path>
</svg>
</button>
<!-- Dropdown menu -->
<div
v-show="usageDropdownOpen"
class="absolute right-0 z-20 py-2 mt-2 bg-white rounded-md shadow-xl"
>
<ul class="space-y-2 text-sm px-4 py-2">
<li class="flex items-center">
<input
id="unused"
type="checkbox"
v-model="filters.showUnused"
class="w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
/>
<label
for="unused"
class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-100"
>
Unused
</label>
</li>
<li class="flex items-center">
<input
id="used"
type="checkbox"
v-model="filters.showUsed"
class="w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
/>
<label for="used" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-100">
Used
</label>
</li>
</ul>
</div>
</div>
<!-- Type Dropdown -->
<div id="type-selector" class="relative" style="width: fit-content">
<button
@click="toggleDropdown('type')"
class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-blue-700 transition text-center inline-flex items-center"
type="button"
>
Types
<svg
class="w-4 h-4 ml-2"
aria-hidden="true"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
></path>
</svg>
</button>
<!-- Dropdown menu -->
<div
v-show="typeDropdownOpen"
class="absolute right-0 z-20 py-2 mt-2 bg-white rounded-md shadow-xl"
>
<ul class="space-y-2 text-sm px-4 py-2">
<li class="flex items-center" v-for="type in componentTypes" :key="type.id">
<input
:id="`type-${type.id}`"
type="checkbox"
v-model="filters.types[type.id]"
class="w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
/>
<label
:for="`type-${type.id}`"
class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-100"
>
{{ type.name }}
</label>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Tableau des composants filtr®s -->
<TableComponent :components="filteredComponents" />
</div>
</template>
+57
View File
@@ -0,0 +1,57 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3'
import { useAdminMode } from '~/composables/isAdminMode'
const props = defineProps({
computerId: Number,
imageSrc: String,
imageAlt: String,
title: String,
description: String,
cost: Number,
price: Number,
})
const { isAdminActive } = useAdminMode()
function goToShow() {
router.get(`/computers/${props.computerId}`)
}
</script>
<template>
<div
class="rounded-lg bg-white shadow-secondary-1 dark:bg-surface-dark h-full flex flex-col hover:cursor-pointer"
@click="goToShow"
>
<img class="w-full h-2/3 object-cover" :src="imageSrc" :alt="imageAlt" />
<div class="flex flex-col justify-between grow px-6 py-2">
<div class="text-xl font-bold text-gray-900 line-clamp-4">
{{ props.title }}
</div>
<div class="flex justify-end mt-auto">
<span
v-if="isAdminActive"
class="inline-block px-3 py-1 mb-2 mr-2 text-sm font-semibold text-gray-700 bg-gray-200 rounded-full"
>
{{ props.cost?.toFixed(2) }}
</span>
<span
class="inline-block px-3 py-1 mb-2 mr-2 text-sm font-semibold text-gray-700 bg-gray-200 rounded-full"
>
{{ props.price?.toFixed(2) }}
</span>
</div>
</div>
</div>
</template>
<style scoped>
.line-clamp-4 {
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
@@ -0,0 +1,136 @@
<script setup lang="ts">
import { computed, onBeforeMount } from 'vue'
import {
cooler,
cpu,
defaultIcon,
fan,
graphicCard,
hardDrive,
memory,
motherboard,
pcCase,
powerSupply,
ssd,
wiFiCard,
} from '~/assets/icons'
import Component from '#models/component'
import { Link } from '@inertiajs/vue3'
const props = defineProps<{
components: Component[]
isAdminActive: boolean
}>()
const components = props.components
const originHeader = { 'X-Inertia-Origin': 'computer' }
let cost = 0
onBeforeMount(() => {
components.forEach((component: Component) => {
cost += parseFloat(String(component.price))
})
})
const componentTypeOrder = [
'Processor',
'Motherboard',
'Graphic Card',
'Memory',
'SSD',
'Storage',
'Cooler',
'Power Supply',
'Case',
'WiFi-Card',
'Fan',
'Other',
]
// Trier les composants selon l'ordre sp├®cifi├®
const sortedComponents = computed(() => {
return components.slice().sort((a, b) => {
return componentTypeOrder.indexOf(a.type.name) - componentTypeOrder.indexOf(b.type.name)
})
})
function getIconPath(typeName: string): string {
const typeToIconMap: Record<string, string> = {
'Motherboard': motherboard,
'Processor': cpu,
'Memory': memory,
'HDD': hardDrive,
'SSD': ssd,
'Cooler': cooler,
'Graphic Card': graphicCard,
'Power Supply': powerSupply,
'Fan': fan,
'WiFi-Card': wiFiCard,
'Case': pcCase,
}
return typeToIconMap[typeName] || defaultIcon // Utilisez une ic├┤ne par d├®faut si n├®cessaire
}
</script>
<template>
<div class="w-full">
<table class="w-full mb-4 text-sm">
<thead>
<tr
class="px-4 py-2 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-400 bg-gray-50"
>
<td v-if="isAdminActive" colspan="5"></td>
<td v-else colspan="4"></td>
</tr>
</thead>
<tbody>
<tr
v-for="(component, index) in sortedComponents"
:key="component.id"
:class="{ 'bg-gray-200': index % 2 !== 0 }"
>
<td class="px-4 py-2 border-b border-gray-400">
<img
:src="getIconPath(component.type.name)"
:alt="`${component.type.name} Icon`"
class="w-6 h-6"
/>
</td>
<td class="px-4 py-2 border-b border-gray-400">
{{ component.type.name }}
</td>
<td
class="px-4 py-2 border-b border-gray-400"
:class="[isAdminActive ? '' : 'text-right']"
>
{{ component.name }}
</td>
<td v-if="isAdminActive" class="px-4 py-2 border-b border-gray-400 text-right">
Ôé¼{{ component.price }}
</td>
<td class="px-4 py-2 border-b border-gray-400 flex justify-center items-center">
<Link
:href="`/components/${component.id}/edit`"
method="get"
as="button"
class="px-3 py-1 bg-indigo-600 text-white rounded hover:bg-blue-700 transition text-sm"
:headers="originHeader"
>
Edit
</Link>
</td>
</tr>
</tbody>
<tfoot v-if="isAdminActive">
<tr class="bg-gray-800 text-white">
<td class="px-4 py-2 border-b border-gray-400 whitespace-nowrap" colspan="3">Total</td>
<td class="px-4 py-2 border-b border-gray-400 whitespace-nowrap text-right">
Ôé¼{{ cost.toFixed(2) }}
</td>
</tr>
</tfoot>
</table>
</div>
</template>
<style scoped></style>
+123
View File
@@ -0,0 +1,123 @@
<script setup lang="ts">
import { noImagePlaceholder } from '~/assets/images'
import { usePage } from '@inertiajs/vue3'
import Computer from '#models/computer'
import ComputerCard from '~/pages/widgets/ComputerCard.vue'
import { computed, reactive, ref } from 'vue'
const { computers } = usePage<{ computers: Computer[] }>().props
const stateDropdownOpen = ref(false)
function toggleDropdown() {
stateDropdownOpen.value = !stateDropdownOpen.value
}
const filters = reactive({
onGoing: true,
finished: true,
sold: false,
})
const filteredComputers = computed(() => {
return computers.filter((computer) => {
const isOnGoing = filters.onGoing && computer.state.name == 'ON_GOING'
const isFinished = filters.finished && computer.state.name == 'FINISHED'
const isSold = filters.sold && computer.state.name == 'SOLD'
return isOnGoing || isFinished || isSold
})
})
</script>
<template>
<div>
<!-- Dropdown pour les filtres -->
<div class="relative">
<h4 class="text-xl underline">Filters</h4>
<div class="flex gap-2">
<!-- Usage Dropdown -->
<div id="usage-selector" class="relative" style="width: fit-content">
<button
@click="toggleDropdown()"
class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-blue-700 transition text-center inline-flex items-center"
type="button"
>
Usage
<svg
class="w-4 h-4 ml-2"
aria-hidden="true"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
></path>
</svg>
</button>
<!-- Dropdown menu -->
<div
v-show="stateDropdownOpen"
class="absolute right-0 z-20 py-2 mt-2 bg-white rounded-md shadow-xl"
>
<ul class="space-y-2 text-sm px-4 py-2">
<li class="flex items-center">
<input
id="unused"
type="checkbox"
v-model="filters.onGoing"
class="w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
/>
<label
for="unused"
class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-100"
>
On Going
</label>
</li>
<li class="flex items-center">
<input
id="used"
type="checkbox"
v-model="filters.finished"
class="w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
/>
<label for="used" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-100">
Finished
</label>
</li>
<li class="flex items-center">
<input
id="used"
type="checkbox"
v-model="filters.sold"
class="w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
/>
<label for="used" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-100">
Sold
</label>
</li>
</ul>
</div>
</div>
</div>
<div class="mt-4 mb-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4">
<ComputerCard
v-for="computer in filteredComputers"
:key="computer.id"
:computerId="computer.id"
:image-src="computer.pictures.find((p) => p.order === 1)?.link || noImagePlaceholder"
image-alt="Computer Image"
:title="computer.title"
:cost="computer.cost"
:price="computer.sellPrice"
/>
</div>
</div>
</div>
</template>
<style scoped></style>
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { defineEmits, defineProps } from 'vue'
defineProps<{ show: boolean }>()
const emit = defineEmits(['confirm', 'cancel'])
const confirm = () => {
emit('confirm')
}
const cancel = () => {
emit('cancel')
}
</script>
<template>
<div v-if="show" class="fixed inset-0 flex items-center justify-center z-50">
<div class="fixed inset-0 bg-black opacity-50"></div>
<div class="bg-white rounded-lg p-6 z-10">
<h2 class="text-lg font-semibold mb-4">Confirmation</h2>
<p class="mb-4">Are you sure to delete this item ?</p>
<div class="flex justify-end space-x-2">
<button @click="confirm" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-700">
Ok
</button>
<button
@click="cancel"
class="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
>
Cancel
</button>
</div>
</div>
</div>
</template>
<style scoped>
/* Styles pour le modal */
</style>
+299
View File
@@ -0,0 +1,299 @@
<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 { NewImage, ServerImage } from '../../../app/types/UItypes'
import { Image } 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) => img.id === image.id)
if (index !== -1) {
serverImages.value.splice(index, 1)
}
} else {
const index = newImages.value.findIndex((img) => 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>
+128
View File
@@ -0,0 +1,128 @@
<script setup lang="ts">
import {
cooler,
cpu,
defaultIcon,
fan,
graphicCard,
hardDrive,
memory,
motherboard,
pcCase,
powerSupply,
ssd,
trashIcon,
wiFiCard,
} from '~/assets/icons'
import { router } from '@inertiajs/vue3'
import { useAdminMode } from '~/composables/isAdminMode'
import Component from '#models/component'
const props = defineProps<{
components: Component[]
}>()
const { isAdminActive } = useAdminMode()
function getIconPath(typeName: string): string {
const typeToIconMap: Record<string, string> = {
'Motherboard': motherboard,
'Processor': cpu,
'Memory': memory,
'HDD': hardDrive,
'SSD': ssd,
'Cooler': cooler,
'Graphic Card': graphicCard,
'Power Supply': powerSupply,
'Fan': fan,
'WiFi-Card': wiFiCard,
'Case': pcCase,
}
return typeToIconMap[typeName] || defaultIcon // Utilisez une ic├┤ne par d├®faut si n├®cessaire
}
function goToDetail(id: number) {
router.get(`/components/${id}`)
}
function deleteComponent(id: number) {
router.delete(`/components/${id}`)
}
</script>
<template>
<div class="flex flex-col mt-8">
<div class="py-2 -my-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div
class="inline-block min-w-full overflow-hidden align-middle border-b border-gray-200 shadow sm:rounded-lg"
>
<table class="min-w-full">
<thead>
<tr>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50"
>
Type
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50"
>
Name
</th>
<th
v-if="isAdminActive"
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50"
>
Price
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50"
>
Purchase Date
</th>
<th
class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50"
>
Action
</th>
</tr>
</thead>
<tbody class="bg-white">
<tr
v-for="component in components"
:key="component.id"
@click.stop.prevent="goToDetail(component.id)"
class="cursor-pointer hover:bg-indigo-600/50"
:class="[component.computerId !== null ? 'used bg-gray-400' : 'bg-white']"
>
<td class="px-6 py-4 border-b border-gray-200 whitespace-nowrap">
<img
:src="getIconPath(component.type.name)"
:alt="`${component.type.name} Icon`"
style="width: 32px; height: 32px"
/>
</td>
<td class="px-6 py-4 border-b border-gray-200 whitespace-nowrap">
{{ component.name }}
</td>
<td v-if="isAdminActive" class="px-6 py-4 border-b border-gray-200 whitespace-nowrap">
Ôé¼{{ component.price }}
</td>
<td class="px-6 py-4 border-b border-gray-200 whitespace-nowrap">
{{ component.purchaseDate }}
</td>
<td class="px-6 py-4 border-b border-gray-200 whitespace-nowrap">
<button
v-delete-confirm="() => deleteComponent(component.id)"
class="px-4 py-2 bg-gray-300 text-white rounded hover:bg-red-500 transition inline-flex items-center justify-center"
>
<img :src="trashIcon" alt="Delete" class="h-5 w-5" />
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<style scoped></style>
+14
View File
@@ -0,0 +1,14 @@
<script setup lang="ts">
defineProps<{
message: string
}>()
</script>
<template>
<div class="bg-red-100 border border-red-400 text-red-700 text-xs rounded relative px-1">
<strong class="me-0.5">!</strong>
<span class="block sm:inline whitespace-nowrap">{{ message }}</span>
</div>
</template>
<style scoped></style>