Files
pc_builder/inertia/pages/widgets/ComponentEditor.vue
T
Kevin L'Hereec 94f89be605 chore: harden config, strengthen frontend typing, fix UTF-8 mojibake
Security config (3.1):
- config/session.ts: cookie.secure now follows app.inProduction instead of
  being hardcoded to false
- config/shield.ts: enable CSP with baseline directives compatible with
  Vite dev server and Inertia (self + @viteDevUrl/@viteHmrUrl)

Frontend typing (3.2):
- Replace 'any' props with concrete types (Computer, Component, ComponentType,
  ComputerState) across computers/*.vue and components/*.vue, using the
  existing inertia/pages/types/UITypes.ts definitions
- Fix UITypes.ts: export ComponentType/ComputerState, switch from interface
  to type (interfaces are never assignable to Inertia's ComponentProps index
  signature), drop the stale componentTypeId/componentStateId fields that
  didn't match the actual Lucid schema (real field is typeId, no component
  state exists)
- Add an asPageProp() boundary cast in the two controllers: Lucid's
  serialize() returns a loosely typed ModelObject that isn't structurally
  assignable to the concrete page prop shapes (and the inertia/** tree is
  excluded from the backend tsconfig, so real types can't be imported
  directly across that boundary)

UTF-8 mojibake fixes (3.3):
- Repair broken accented characters (├®/├á/├┤ -> é/à/ô, Ôé¼ -> €) left over
  from a bad encoding during the GWT->Inertia migration, across
  ComponentEditor.vue, ComputerComponentsTable.vue, SimpleGallery.vue,
  TableComponent.vue, isAdminMode.ts and the custom directives

Misc cleanup (3.5):
- computer.ts: replace console.warn with the app logger (includes
  computerId) when .cost is accessed without preloading components
2026-07-23 11:10:37 +02:00

157 lines
4.7 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import type { 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>