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
120 lines
4.1 KiB
Vue
120 lines
4.1 KiB
Vue
<script setup lang="ts">
|
|
import { reactive } from 'vue'
|
|
import { Link } from '@adonisjs/inertia/vue'
|
|
import { router, usePage } from '@inertiajs/vue3'
|
|
import type { Component, Computer, ComputerState } from '~/pages/types/UITypes'
|
|
|
|
const { props } = usePage<{
|
|
computer: Computer
|
|
availableComponents: Component[]
|
|
states: ComputerState[]
|
|
errors?: Record<string, string>
|
|
}>()
|
|
const { computer, states } = props
|
|
|
|
const form = reactive({
|
|
id: computer.id,
|
|
title: computer.title,
|
|
sellPrice: computer.sellPrice,
|
|
gpuScore: computer.gpuScore,
|
|
cpuScore: computer.cpuScore,
|
|
globalScore: computer.globalScore,
|
|
stateId: computer.state.id,
|
|
componentsId: computer.components.map((c) => c.id),
|
|
})
|
|
|
|
function deleteComputer(id: number) {
|
|
router.delete(`/computers/${id}`)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="max-w-4xl mx-auto p-8 bg-white shadow-lg rounded-lg">
|
|
<h1 class="text-2xl font-semibold mb-8 text-gray-800">Edit Computer</h1>
|
|
<form class="space-y-6">
|
|
<div>
|
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="title">Title</label>
|
|
<input
|
|
v-model="form.title"
|
|
class="w-full mt-2 border-gray-200 focus:border-indigo-600 focus:ring focus:ring-indigo-500/40 px-3 py-2 border rounded text-gray-700"
|
|
type="text"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="sellPrice">Sell Price</label>
|
|
<input
|
|
name="sell_price"
|
|
v-model="form.sellPrice"
|
|
class="w-full mt-2 border-gray-200 focus:border-indigo-600 focus:ring focus:ring-indigo-500/40 px-3 py-2 border rounded text-gray-700"
|
|
type="number"
|
|
step="0.01"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="gpuScore">GPU Score</label>
|
|
<input
|
|
v-model="form.gpuScore"
|
|
class="w-full border-gray-200 focus:border-indigo-600 focus:ring focus:ring-indigo-500/40 px-3 py-2 border rounded text-gray-700"
|
|
type="number"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="cpuScore">CPU Score</label>
|
|
<input
|
|
v-model="form.cpuScore"
|
|
class="w-full border-gray-200 focus:border-indigo-600 focus:ring focus:ring-indigo-500/40 px-3 py-2 border rounded text-gray-700"
|
|
type="number"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="globalScore"
|
|
>Global Score</label
|
|
>
|
|
<input
|
|
v-model="form.globalScore"
|
|
class="w-full border-gray-200 focus:border-indigo-600 focus:ring focus:ring-indigo-500/40 px-3 py-2 border rounded text-gray-700"
|
|
type="number"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pt-2">
|
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="components">State</label>
|
|
<select
|
|
v-model="form.stateId"
|
|
class="w-full mt-2 border-gray-200 focus:border-indigo-600 focus:ring-indigo-500 px-3 py-2.5 focus:ring-1 border rounded text-gray-700 bg-white"
|
|
>
|
|
<option disabled value="">Please select one</option>
|
|
<option v-for="state in states" :key="state.id" :value="state.id">
|
|
{{ state.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div
|
|
class="flex flex-col md:flex-row items-stretch justify-between space-y-2 md:space-y-0 md:space-x-2"
|
|
>
|
|
<Link
|
|
:href="`/computers/${form.id}`"
|
|
method="put"
|
|
:data="form"
|
|
as="button"
|
|
class="w-full md:w-auto px-4 py-2 bg-indigo-600 text-white rounded hover:bg-blue-700 transition flex justify-center"
|
|
>
|
|
Save
|
|
</Link>
|
|
<button
|
|
@click="deleteComputer(computer.id)"
|
|
class="w-full md:w-auto px-4 py-2 bg-gray-300 text-white rounded hover:bg-red-500 transition flex items-center justify-center"
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped></style>
|