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
+98
View File
@@ -0,0 +1,98 @@
<script setup lang="ts">
import { Form, Link } from '@adonisjs/inertia/vue'
import { useForm } from '@inertiajs/vue3'
import EmptyLayout from '~/pages/layout/emptyLayout.vue'
defineOptions({ layout: EmptyLayout })
const form = useForm({
fullName: '',
email: '',
password: '',
passwordConfirmation: '',
})
</script>
<template>
<div class="flex items-center justify-center min-h-screen px-4 py-8 bg-gradient-to-br from-gray-100 to-gray-200">
<div class="w-full max-w-md p-6 sm:p-8 bg-white rounded-lg shadow-lg">
<div class="flex flex-col items-center justify-center mb-8">
<h1 class="text-2xl sm:text-3xl font-bold text-gray-800">Create Account</h1>
<p class="text-sm text-gray-600 mt-2">Join PC Creator today</p>
</div>
<Form route="account.signup" #default="{ processing, errors }">
<div class="space-y-4 sm:space-y-6">
<div>
<label class="block">
<span class="text-sm font-medium text-gray-700 mb-2 block">Full Name</span>
<input
v-model="form.fullName"
class="w-full px-3 sm:px-4 py-2 sm:py-3 border-2 border-gray-300 rounded-lg focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500/40 transition text-base"
name="fullName"
type="text"
placeholder="Enter your full name"
/>
</label>
</div>
<div>
<label class="block">
<span class="text-sm font-medium text-gray-700 mb-2 block">Email</span>
<input
v-model="form.email"
class="w-full px-3 sm:px-4 py-2 sm:py-3 border-2 border-gray-300 rounded-lg focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500/40 transition text-base"
name="email"
type="email"
placeholder="Enter your email"
/>
</label>
</div>
<div>
<label class="block">
<span class="text-sm font-medium text-gray-700 mb-2 block">Password</span>
<input
v-model="form.password"
class="w-full px-3 sm:px-4 py-2 sm:py-3 border-2 border-gray-300 rounded-lg focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500/40 transition text-base"
name="password"
type="password"
placeholder="Enter your password"
/>
</label>
</div>
<div>
<label class="block">
<span class="text-sm font-medium text-gray-700 mb-2 block">Confirm Password</span>
<input
v-model="form.passwordConfirmation"
class="w-full px-3 sm:px-4 py-2 sm:py-3 border-2 border-gray-300 rounded-lg focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500/40 transition text-base"
name="passwordConfirmation"
type="password"
placeholder="Confirm your password"
/>
</label>
</div>
<button
type="submit"
:disabled="processing"
class="w-full px-3 sm:px-4 py-2 sm:py-3 mt-6 sm:mt-8 text-base font-semibold text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500/40 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{{ processing ? 'Creating account...' : 'Sign Up' }}
</button>
<div class="text-center mt-4">
<p class="text-sm text-gray-600">
Already have an account?
<Link route="auth.show_login" class="text-indigo-600 hover:text-indigo-700 font-semibold">
Login
</Link>
</p>
</div>
</div>
</Form>
</div>
</div>
</template>
+124
View File
@@ -0,0 +1,124 @@
<script lang="ts" setup>
import { Link, useForm } from '@inertiajs/vue3'
import { computed } from 'vue'
import EmptyLayout from '~/pages/layout/emptyLayout.vue'
defineOptions({ layout: EmptyLayout })
const props = defineProps<{ errors?: any[] | any }>()
const vineErrors = computed(() => {
if (Array.isArray(props.errors)) {
return props.errors as any[]
}
return []
})
const authError = computed(() => {
if (!Array.isArray(props.errors) && props.errors) {
return props.errors as any
}
return null
})
const form = useForm({
email: '',
password: '',
})
function getErrorMessage(field: string): string | null {
const error = vineErrors.value.find((error) => error.field === field)
return error ? error.message : null
}
console.log(authError)
console.log(vineErrors)
console.log(props)
</script>
<template>
<div class="flex items-center justify-center min-h-screen px-4 py-8 bg-gradient-to-br from-gray-100 to-gray-200">
<div class="w-full max-w-md p-6 sm:p-8 bg-white rounded-lg shadow-lg">
<div class="flex flex-col items-center justify-center mb-8">
<div class="mb-4">
<svg
class="w-12 h-12 sm:w-14 sm:h-14 text-indigo-600"
fill="currentColor"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 494.000000 555.000000"
xmlns="http://www.w3.org/2000/svg"
>
<g transform="translate(0.000000,555.000000) scale(0.100000,-0.100000)">
<path
d="M3090 3400 l0 -1950 -1190 0 -1190 0 0 234 c0 190 3 236 14 245 23 19 164 30 606 46 228 9 485 20 570 25 134 9 895 40 965 40 23 0 25 -4 25 -44 l0 -44 -82 -6 c-46 -3 -146 -8 -223 -11 -77 -3 -239 -12 -360 -20 -121 -8 -335 -19 -475 -25 -387 -15 -650 -32 -650 -41 0 -12 196 -11 515 1 257 10 1267 32 1271 27 7 -9 12 -74 6 -84 -7 -11 -190 -21 -777 -43 -437 -16 -748 -30 -777 -36 -16 -3 -28 -10 -28 -15 0 -13 837 -11 1237 2 360 12 343 15 343 -53 0 -36 -1 -37 -42 -42 -24 -3 -182 -8 -353 -11 -501 -10 -923 -31 -907 -47 4 -4 315 -7 690 -6 l682 3 -1 1445 c-1 795 -3 1488 -3 1540 -1 52 -2 249 -4 437 l-2 343 -23 5 c-23 6 -39 -1 -417 -200 -107 -56 -242 -127 -300 -157 -151 -80 -311 -164 -415 -218 -49 -26 -133 -70 -185 -97 -99 -53 -146 -77 -450 -238 -378 -199 -494 -265 -467 -265 3 0 134 64 293 143 159 79 381 188 494 243 113 54 440 216 727 360 494 248 613 303 613 285 0 -7 -59 -49 -135 -96 -66 -42 -219 -120 -875 -449 -377 -188 -741 -375 -810 -413 -69 -39 -150 -83 -180 -99 -32 -16 -48 -29 -39 -31 8 -2 57 17 107 42 51 25 94 45 96 45 2 0 95 44 207 99 346 167 404 190 404 162 0 -20 -55 -51 -385 -219 -304 -155 -360 -187 -360 -202 0 -14 22 -12 92 6 33 8 62 13 64 11 8 -8 -8 -37 -21 -37 -17 0 -95 -58 -95 -71 0 -14 52 -2 117 27 28 13 55 24 60 24 4 0 96 40 203 89 107 49 198 87 203 84 4 -2 7 -14 7 -27 0 -19 -16 -31 -97 -69 -67 -32 -87 -45 -64 -42 18 3 84 5 146 5 l112 0 17 45 c21 53 56 85 149 131 136 69 42 42 -156 -45 -62 -27 -67 -27 -67 7 0 23 8 31 63 56 34 17 148 72 254 123 106 51 194 93 198 93 3 0 5 -7 5 -16 0 -8 5 -24 11 -35 15 -28 39 -7 39 35 0 17 4 36 8 42 4 6 54 32 112 59 58 26 152 70 210 97 217 104 220 104 220 53 0 -42 -4 -45 -177 -121 -73 -32 -131 -59 -129 -61 2 -2 46 6 97 17 128 29 306 29 402 1 38 -11 75 -25 83 -32 11 -9 14 -43 14 -150 l0 -138 -27 -18 c-77 -52 -150 -70 -353 -84 l-155 -10 -45 -49 c-80 -86 -204 -129 -390 -136 l-125 -4 -17 -35 c-24 -45 -70 -79 -138 -101 -58 -18 -85 -34 -58 -34 9 0 18 -4 20 -9 4 -11 271 68 523 154 36 12 97 29 150 41 63 14 -125 -59 -495 -192 -173 -61 -71 -56 125 6 56 18 222 69 410 125 44 13 135 42 203 65 68 22 127 38 133 35 9 -6 4 -254 -7 -302 -2 -12 -13 -27 -23 -32 -17 -9 -19 -2 -31 94 -6 58 -16 105 -21 105 -5 0 -9 -58 -9 -128 0 -121 -1 -130 -23 -150 -34 -32 -47 -28 -47 16 -1 57 -11 196 -15 208 -4 14 -134 59 -143 49 -4 -3 -5 -92 -3 -196 3 -191 -1 -219 -34 -219 -9 0 -14 49 -19 183 -4 106 -11 186 -18 192 -7 7 -9 -49 -5 -192 3 -157 2 -203 -8 -203 -16 0 -25 88 -25 241 0 68 -4 120 -10 124 -12 7 -12 10 -10 -212 1 -143 -1 -173 -13 -173 -19 0 -27 69 -27 233 0 78 -4 127 -10 127 -6 0 -10 -42 -10 -105 0 -58 -2 -105 -5 -105 -2 0 -36 17 -75 37 -117 61 -220 50 -305 -33 -25 -24 -48 -44 -50 -44 -3 0 -5 29 -5 65 0 78 -11 87 -112 89 l-68 1 0 30 c0 17 -4 36 -9 43 -5 8 -48 22 -97 32 -49 10 -163 34 -254 53 -173 37 -338 63 -346 54 -5 -5 8 -8 246 -67 291 -71 324 -80 376 -95 l51 -15 5 -348 c3 -191 2 -351 -2 -355 -8 -9 -114 -1 -487 40 l-253 27 -30 -29 c-22 -21 -31 -25 -34 -14 -2 8 -7 199 -10 424 -5 396 -5 411 14 440 16 24 20 47 20 112 l0 81 -54 46 c-29 25 -56 46 -60 46 -3 0 -6 -609 -6 -1354 0 -1225 2 -1354 16 -1360 9 -3 37 -6 62 -6 59 0 65 -4 87 -63 10 -26 22 -50 26 -53 5 -3 9 -14 9 -25 0 -16 -7 -19 -55 -19 -59 0 -125 -21 -125 -40 0 -9 435 -11 1693 -8 1641 3 1694 4 1722 22 l29 19 -22 18 -22 18 40 54 c40 54 61 67 108 67 l22 0 -2 1718 -3 1717 -51 26 c-27 14 -208 102 -402 194 -193 92 -417 203 -498 246 -81 44 -153 79 -160 79 -12 0 -14 -307 -14 -1950z m338 1624 c106 -52 196 -94 200 -94 17 0 353 -174 381 -198 l32 -27 3 -1140 c3 -1319 1 -1584 -16 -1567 -22 22 -44 800 -48 1687 -1 363 -5 738 -9 834 l-6 174 -125 70 c-69 38 -174 94 -235 125 -181 91 -379 202 -400 224 l-20 20 25 -7 c14 -4 112 -50 218 -101z m-788 -93 c0 -12 -288 -151 -313 -151 -19 0 -19 2 -7 14 13 13 232 126 270 139 24 9 50 8 50 -2z"
/>
</g>
</svg>
</div>
<h1 class="text-2xl sm:text-3xl font-bold text-gray-800">PC Creator</h1>
</div>
<form class="space-y-4 sm:space-y-6">
<div>
<label class="block">
<span class="text-sm font-medium text-gray-700 mb-2 block">Email</span>
<input
v-model="form.email"
class="w-full px-3 sm:px-4 py-2 sm:py-3 border-2 border-gray-300 rounded-lg focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500/40 transition text-base"
name="email"
type="email"
placeholder="Enter your email"
/>
</label>
</div>
<div>
<label class="block">
<span class="text-sm font-medium text-gray-700 mb-2 block">Password</span>
<input
v-model="form.password"
class="w-full px-3 sm:px-4 py-2 sm:py-3 border-2 border-gray-300 rounded-lg focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500/40 transition text-base"
name="password"
type="password"
placeholder="Enter your password"
/>
</label>
</div>
<div class="flex items-center justify-between pt-2">
<label class="flex items-center cursor-pointer">
<input
class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-2 focus:ring-indigo-500/40"
type="checkbox"
/>
<span class="ml-2 text-sm text-gray-600">Remember me</span>
</label>
</div>
<Link
:data="form"
as="button"
class="w-full px-3 sm:px-4 py-2 sm:py-3 mt-6 sm:mt-8 text-base font-semibold text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500/40 transition"
href="/auth/login"
method="post"
type="button"
>
Login
</Link>
<Link
as="button"
class="w-full px-3 sm:px-4 py-2 sm:py-3 mt-3 sm:mt-4 text-base font-semibold text-indigo-600 bg-gray-100 rounded-lg hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500/40 transition"
href="/account/signup"
type="button"
>
Create an account
</Link>
</form>
</div>
</div>
</template>
<style scoped>
/* No inline styles - using Tailwind classes only */
</style>
+98
View File
@@ -0,0 +1,98 @@
<script setup lang="ts">
import { Form, Link } from '@adonisjs/inertia/vue'
import { useForm } from '@inertiajs/vue3'
import EmptyLayout from '~/pages/layout/emptyLayout.vue'
defineOptions({ layout: EmptyLayout })
const form = useForm({
fullName: '',
email: '',
password: '',
passwordConfirmation: '',
})
</script>
<template>
<div class="flex items-center justify-center min-h-screen px-4 py-8 bg-gradient-to-br from-gray-100 to-gray-200">
<div class="w-full max-w-md p-6 sm:p-8 bg-white rounded-lg shadow-lg">
<div class="flex flex-col items-center justify-center mb-8">
<h1 class="text-2xl sm:text-3xl font-bold text-gray-800">Create Account</h1>
<p class="text-sm text-gray-600 mt-2">Join PC Creator today</p>
</div>
<Form route="account.signup" #default="{ processing, errors }">
<div class="space-y-4 sm:space-y-6">
<div>
<label class="block">
<span class="text-sm font-medium text-gray-700 mb-2 block">Full Name</span>
<input
v-model="form.fullName"
class="w-full px-3 sm:px-4 py-2 sm:py-3 border-2 border-gray-300 rounded-lg focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500/40 transition text-base"
name="fullName"
type="text"
placeholder="Enter your full name"
/>
</label>
</div>
<div>
<label class="block">
<span class="text-sm font-medium text-gray-700 mb-2 block">Email</span>
<input
v-model="form.email"
class="w-full px-3 sm:px-4 py-2 sm:py-3 border-2 border-gray-300 rounded-lg focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500/40 transition text-base"
name="email"
type="email"
placeholder="Enter your email"
/>
</label>
</div>
<div>
<label class="block">
<span class="text-sm font-medium text-gray-700 mb-2 block">Password</span>
<input
v-model="form.password"
class="w-full px-3 sm:px-4 py-2 sm:py-3 border-2 border-gray-300 rounded-lg focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500/40 transition text-base"
name="password"
type="password"
placeholder="Enter your password"
/>
</label>
</div>
<div>
<label class="block">
<span class="text-sm font-medium text-gray-700 mb-2 block">Confirm Password</span>
<input
v-model="form.passwordConfirmation"
class="w-full px-3 sm:px-4 py-2 sm:py-3 border-2 border-gray-300 rounded-lg focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500/40 transition text-base"
name="passwordConfirmation"
type="password"
placeholder="Confirm your password"
/>
</label>
</div>
<button
type="submit"
:disabled="processing"
class="w-full px-3 sm:px-4 py-2 sm:py-3 mt-6 sm:mt-8 text-base font-semibold text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500/40 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{{ processing ? 'Creating account...' : 'Sign Up' }}
</button>
<div class="text-center mt-4">
<p class="text-sm text-gray-600">
Already have an account?
<Link route="auth.show_login" class="text-indigo-600 hover:text-indigo-700 font-semibold">
Login
</Link>
</p>
</div>
</div>
</Form>
</div>
</div>
</template>
+116
View File
@@ -0,0 +1,116 @@
<script setup lang="ts">
import { computed, reactive } from 'vue'
import { Link, useForm } from '@inertiajs/vue3'
const props = defineProps<{ types: any[]; errors?: any[] }>()
const errors = computed(() => props.errors || [])
const form = reactive({
name: '',
price: 0,
type_id: null,
purchase_date: new Date().toISOString().split('T')[0],
quantity: 1,
})
function getErrorMessage(field: string): string | null {
const error = errors.value.find((error) => error.field === field)
return error ? error.message : null
}
function formatDateForMySQL(date: string) {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
form.purchase_date = `${year}-${month}-${day}`
}
function submitForm() {
const inertiaForm = useForm(form)
inertiaForm.post('/components')
}
</script>
<template>
<div class="max-w-2xl mx-auto p-8 bg-white shadow-lg rounded-lg">
<h1 class="text-2xl font-semibold mb-8 text-gray-800">Create New Component</h1>
<form @submit.prevent="submitForm" class="space-y-6">
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="name">Name</label>
<input
name="name"
v-model="form.name"
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="price">Price</label>
<input
name="price"
v-model.number="form.price"
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"
min="0"
/>
</div>
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="type">Type</label>
<select
name="type_id"
v-model.number="form.type_id"
id="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 disabled :value="null">Choose a type</option>
<option v-for="type in types" :key="type.id" :value="type.id">
{{ type.name }}
</option>
</select>
</div>
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="purchaseDate"
>Purchase Date</label
>
<input
name="purchase_date"
v-model="form.purchase_date"
class="w-full mt-2 border-gray-200 focus:border-indigo-600 focus:ring focus:ring-indigo-500/40 border rounded text-gray-700 px-3 py-2"
type="date"
/>
</div>
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="quantity">Quantity</label>
<input
name="quantity"
v-model.number="form.quantity"
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"
min="1"
/>
</div>
<div class="flex gap-4">
<button
type="submit"
class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700 transition"
>
Create
</button>
<Link
href="/components"
class="px-4 py-2 bg-gray-300 text-gray-800 rounded hover:bg-gray-400 transition"
>
Cancel
</Link>
</div>
</form>
</div>
</template>
<style scoped></style>
+101
View File
@@ -0,0 +1,101 @@
<script setup lang="ts">
import { computed, reactive } from 'vue'
import { Link, usePage } from '@inertiajs/vue3'
const { props } = usePage<{
component: any
types: any[]
origin: string
errors?: any[]
}>()
const { component, types, origin } = props
const errors = computed(() => props.errors || [])
const form = reactive({
id: component.id,
name: component.name,
price: component.price,
type_id: component.type.id,
purchase_date: component.purchaseDate,
origin: origin,
})
function getErrorMessage(field: string): string | null {
const error = errors.value.find((error) => error.field === field)
return error ? error.message : null
}
function formatDateForMySQL(date: string) {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
form.purchase_date = `${year}-${month}-${day}`
}
</script>
<template>
<div class="max-w-2xl mx-auto p-8 bg-white shadow-lg rounded-lg">
<h1 class="text-2xl font-semibold mb-8 text-gray-800">Edit Component</h1>
<form class="space-y-6">
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="name">Name</label>
<input
name="name"
v-model="form.name"
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="price">Price</label>
<input
name="price"
v-model="form.price"
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>
<label class="block text-gray-700 text-sm font-bold mb-2" for="type">Type</label>
<select
v-model="form.type_id"
id="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 disabled :value="null">Choose a type</option>
<option v-for="type in types" :key="type.id" :value="type.id">
{{ type.name }}
</option>
</select>
</div>
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="purchaseDate"
>Purchase Date</label
>
<input
name="purchase_date"
v-model="form.purchase_date"
class="w-full mt-2 border-gray-200 focus:border-indigo-600 focus:ring focus:ring-indigo-500/40 border rounded text-gray-700 px-3 py-2"
type="date"
/>
</div>
<Link
:href="`/components/${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 text-center"
>
Save
</Link>
</form>
</div>
</template>
<style scoped></style>
+43
View File
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3'
defineProps<{ components: any[] }>()
</script>
<template>
<div>
<div class="flex justify-between items-center">
<h3 class="text-3xl font-semibold text-gray-700">Components</h3>
<Link
href="/components/create"
type="button"
class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-blue-700 transition"
>
Add
</Link>
</div>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<template v-if="components.length > 0">
<div
v-for="component in components"
:key="component.id"
class="p-4 border rounded-lg shadow hover:shadow-lg transition"
>
<h4 class="font-semibold text-lg text-gray-800">{{ component.name }}</h4>
<p class="text-gray-600">Type: {{ component.type?.name }}</p>
<p class="text-indigo-600 font-semibold">Price: ${{ component.price }}</p>
<Link
:href="`/components/${component.id}`"
class="mt-2 inline-block text-indigo-600 hover:text-indigo-700"
>
View Details
</Link>
</div>
</template>
<template v-else>
<p class="text-gray-500">No components yet. Create one to get started!</p>
</template>
</div>
</div>
</template>
+53
View File
@@ -0,0 +1,53 @@
<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3'
const { props } = usePage<{ component: any }>()
const { component } = props
const originHeader = { 'X-Inertia-Origin': 'components' }
</script>
<template>
<div class="max-w-2xl mx-auto p-8 bg-white shadow-lg rounded-lg">
<h1 class="text-2xl font-semibold mb-8 text-gray-800">Component Details</h1>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="name">Name</label>
<div class="px-3 py-2 border rounded text-gray-700" id="name">
{{ component.name }}
</div>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="price">Price</label>
<div class="px-3 py-2 border rounded text-gray-700" id="price">${{ component.price }}</div>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="type">Type</label>
<div class="px-3 py-2 border rounded text-gray-700" id="type">
{{ component.type.name }}
</div>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="purchaseDate"
>Purchase Date</label
>
<div class="px-3 py-2 border rounded text-gray-700" id="purchaseDate">
{{ component.purchaseDate }}
</div>
</div>
<Link
:href="`/components/${component.id}/edit`"
type="button"
as="button"
class="w-full md:w-auto px-4 py-2 bg-indigo-600 text-white rounded hover:bg-blue-700 transition text-center"
:headers="originHeader"
>
Edit
</Link>
</div>
</template>
<style scoped></style>
+171
View File
@@ -0,0 +1,171 @@
<script setup lang="ts">
import { computed, reactive } from 'vue'
import { Link, usePage } from '@inertiajs/vue3'
const { props } = usePage<{
components: any[]
states: any[]
errors: any[]
}>()
const { components, states } = props
const errors = computed(() => props.errors || [])
const componentTypeOrder = [
'Processor',
'Motherboard',
'Graphic Card',
'Memory',
'SSD',
'Storage',
'Cooler',
'Power Supply',
'Case',
'WiFi-Card',
'Fan',
'Other',
]
const componentsByType = computed(() => {
const groups = new Map<string, any[]>()
components.forEach((component) => {
if (!groups.has(component.type.name)) {
groups.set(component.type.name, [])
}
groups.get(component.type.name)!.push(component)
})
return Array.from(groups)
.map(([type, components]) => ({ type, components }))
.sort((a, b) => componentTypeOrder.indexOf(a.type) - componentTypeOrder.indexOf(b.type))
})
const form = reactive({
title: '',
sellPrice: 0,
gpuScore: 0,
cpuScore: 0,
stateId: 1,
globalScore: 0,
componentsId: [] as number[],
})
function getErrorMessage(field: string): string | null {
const error = errors.value.find((error) => error.field === field)
return error ? error.message : null
}
</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">Create New Computer</h1>
<form class="space-y-6">
<div class="mt-2">
<label class="block text-gray-700 text-sm font-bold mb-2" for="title">Title</label>
<input
name="title"
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 class="mt-2 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
name="gpuScore"
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
name="cpuScore"
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
name="globalScore"
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="mt-2">
<label class="block text-gray-700 text-sm font-bold mb-2" for="components"
>Components</label
>
<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 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"
:id="`checkbox-${component.id}`"
:value="component.id"
v-model="form.componentsId"
class="mr-2"
/>
<label :for="`checkbox-${component.id}`" class="text-sm text-gray-700">
{{ component.name }}
</label>
</div>
</div>
</div>
</div>
<div class="mt-2 grid grid-cols-3 gap-4">
<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 />
<div class="mt-2">
<label class="block text-gray-700 text-sm font-bold mb-2" for="sellPrice"
>Sell Price</label
>
<input
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>
<Link
href="/computers"
type="button"
as="button"
method="post"
:data="form"
class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-blue-700 transition"
>
Create
</Link>
</form>
</div>
</template>
<style scoped></style>
+127
View File
@@ -0,0 +1,127 @@
<script setup lang="ts">
import { computed, reactive } from 'vue'
import { Link, router, usePage } from '@inertiajs/vue3'
const { props } = usePage<{
computer: any
availableComponents: any[]
states: any[]
errors?: any[]
}>()
const { computer, availableComponents, states } = props
const errors = computed(() => props.errors || [])
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: any) => c.id),
})
function updateComponents(componentsId: number[]) {
form.componentsId = componentsId
}
function deleteComputer(id: number) {
router.delete(`/computers/${id}`)
}
function getErrorMessage(field: string): string | null {
const error = errors.value.find((error) => error.field === field)
return error ? error.message : null
}
</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>
+128
View File
@@ -0,0 +1,128 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3'
defineProps<{ computers: any[] }>()
</script>
<template>
<div>
<div class="flex justify-between items-center">
<h3 class="text-3xl font-semibold text-gray-700">Computers</h3>
<Link
href="/computers/create"
type="button"
as="button"
class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-blue-700 transition"
>
Add
</Link>
</div>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<template v-if="computers.length > 0">
<div
v-for="computer in computers"
:key="computer.id"
class="p-4 border rounded-lg shadow hover:shadow-lg transition"
>
<h4 class="font-semibold text-lg text-gray-800">{{ computer.title }}</h4>
<p class="text-gray-600">State: {{ computer.state?.name }}</p>
<p class="text-indigo-600 font-semibold">Price: ${{ computer.sellPrice }}</p>
<p class="text-gray-500 text-sm">{{ computer.components.length }} components</p>
<Link
:href="`/computers/${computer.id}`"
class="mt-2 inline-block text-indigo-600 hover:text-indigo-700"
>
View Details
</Link>
</div>
</template>
<template v-else>
<p class="text-gray-500">No computers yet. Create one to get started!</p>
</template>
</div>
</div>
</template>
<style scoped>
.computers-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.computers-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
.computer-card {
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
overflow: hidden;
display: flex;
flex-direction: column;
}
.card-image {
width: 100%;
height: 200px;
background-color: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.no-image {
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
}
.card-content {
padding: 1rem;
flex: 1;
display: flex;
flex-direction: column;
}
.card-content h3 {
margin: 0 0 0.5rem 0;
font-size: 1.125rem;
}
.card-content p {
color: #6b7280;
margin: 0 0 1rem 0;
flex: 1;
}
.card-meta {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
font-size: 0.875rem;
color: #6b7280;
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
}
</style>
+87
View File
@@ -0,0 +1,87 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3'
const { props } = defineProps<{ computer: any }>()
</script>
<template>
<div class="max-w-4xl mx-auto p-4 md:p-8 bg-white shadow-lg rounded-lg">
<h1 class="text-xl md:text-2xl font-semibold mb-4 md:mb-8 text-gray-800">
{{ computer.title }}
</h1>
<div class="mt-2">
<label class="block text-gray-700 text-sm font-bold mb-2">Components</label>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead class="bg-gray-200">
<tr>
<th class="border px-4 py-2 text-left">Name</th>
<th class="border px-4 py-2 text-left">Type</th>
<th class="border px-4 py-2 text-right">Price</th>
</tr>
</thead>
<tbody>
<tr
v-for="component in computer.components"
:key="component.id"
class="hover:bg-gray-50"
>
<td class="border px-4 py-2">{{ component.name }}</td>
<td class="border px-4 py-2">{{ component.type?.name }}</td>
<td class="border px-4 py-2 text-right">${{ component.price }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="mt-6 flex flex-wrap gap-4">
<div class="flex-1 min-w-[100px]">
<label class="block text-gray-700 text-sm font-bold mb-2">GPU Score</label>
<p class="w-full border-gray-200 px-3 py-2 border rounded text-gray-700">
{{ computer.gpuScore }}
</p>
</div>
<div class="flex-1 min-w-[100px]">
<label class="block text-gray-700 text-sm font-bold mb-2">CPU Score</label>
<p class="w-full border-gray-200 px-3 py-2 border rounded text-gray-700">
{{ computer.cpuScore }}
</p>
</div>
<div class="flex-1 min-w-[100px]">
<label class="block text-gray-700 text-sm font-bold mb-2">Global Score</label>
<p class="w-full border-gray-200 px-3 py-2 border rounded text-gray-700">
{{ computer.globalScore }}
</p>
</div>
</div>
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="pt-2">
<label class="block text-gray-700 text-sm font-bold mb-2">State</label>
<p class="w-full mt-2 border-gray-200 px-3 py-2.5 border rounded text-gray-700">
{{ computer.state.name }}
</p>
</div>
<div></div>
<div class="mt-2">
<label class="block text-gray-700 text-sm font-bold mb-2">Sell Price</label>
<p class="w-full mt-2 border-gray-200 px-3 py-2 border rounded text-gray-700">
${{ computer.sellPrice.toFixed(2) }}
</p>
</div>
</div>
<Link
:href="`/computers/${computer.id}/edit`"
type="button"
as="button"
class="w-full md:w-auto px-4 py-2 bg-indigo-600 text-white rounded hover:bg-blue-700 transition"
>
Edit
</Link>
</div>
</template>
<style scoped></style>
+14
View File
@@ -0,0 +1,14 @@
<script setup lang="ts"></script>
<template>
<div class="flex items-center justify-center min-h-screen bg-gray-100">
<div class="text-center">
<h1 class="text-6xl font-bold text-indigo-600 mb-4">404</h1>
<h2 class="text-3xl font-semibold text-gray-800 mb-2">Page not found</h2>
<p class="text-gray-600 mb-6">This page does not exist.</p>
<a href="/" class="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
Go Home
</a>
</div>
</div>
</template>
+16
View File
@@ -0,0 +1,16 @@
<script setup lang="ts">
defineProps<{ error?: any }>()
</script>
<template>
<div class="flex items-center justify-center min-h-screen bg-gray-100">
<div class="text-center">
<h1 class="text-6xl font-bold text-red-600 mb-4">500</h1>
<h2 class="text-3xl font-semibold text-gray-800 mb-2">Server Error</h2>
<p class="text-gray-600 mb-6">{{ error?.message || 'Something went wrong. Please try again later.' }}</p>
<a href="/" class="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
Go Home
</a>
</div>
</div>
</template>
+55
View File
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Link, usePage } from '@inertiajs/vue3'
const page = usePage()
// Redirect to computers if user is authenticated
const isAuthenticated = computed(() => (page.props as any).auth?.user)
</script>
<template>
<div v-if="isAuthenticated" class="container mx-auto px-6 py-8">
<div class="bg-white rounded-lg shadow-lg p-8">
<h1 class="text-3xl font-bold text-gray-800 mb-4">Welcome to PC Creator</h1>
<p class="text-gray-600 mb-6">
Manage your computer components and builds efficiently.
</p>
<div class="flex gap-4">
<Link
href="/computers"
class="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
View My Computers
</Link>
<Link
href="/components"
class="px-6 py-3 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300"
>
Manage Components
</Link>
</div>
</div>
</div>
<div v-else class="flex items-center justify-center min-h-screen bg-gradient-to-br from-gray-100 to-gray-200">
<div class="text-center">
<h1 class="text-4xl font-bold text-gray-800 mb-4">PC Creator</h1>
<p class="text-xl text-gray-600 mb-8">Build and manage your perfect computer setup</p>
<div class="flex gap-4 justify-center">
<Link
href="/auth/login"
class="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Login
</Link>
<Link
href="/account/signup"
class="px-6 py-3 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300"
>
Sign Up
</Link>
</div>
</div>
</div>
</template>
+20
View File
@@ -0,0 +1,20 @@
<script setup lang="ts">
import Sidebar from './sidebar.vue'
import Header from './header.vue'
</script>
<template>
<div class="flex h-screen bg-gray-200 font-roboto">
<Sidebar />
<div class="flex-1 flex flex-col overflow-hidden">
<Header />
<main class="flex-1 overflow-x-hidden overflow-y-auto bg-gray-200">
<div class="container mx-auto px-6 py-8">
<slot />
</div>
</main>
</div>
</div>
</template>
+5
View File
@@ -0,0 +1,5 @@
<template>
<div>
<slot />
</div>
</template>
+99
View File
@@ -0,0 +1,99 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useSidebar } from '~/composables/useSidebar'
import { Link, usePage } from '@inertiajs/vue3'
import { userProfileIcon } from '~/assets/icons'
import { useAdminMode } from '~/composables/isAdminMode'
const dropdownOpen = ref(false)
const { isOpen } = useSidebar()
const { isAdminActive } = useAdminMode()
function toggleActive() {
isAdminActive.value = !isAdminActive.value
}
const page = usePage()
const user = computed(() => {
const userData = (page.props as any).auth?.user || (page.props as any).user
return userData || { name: 'Guest', email: 'guest@example.com' }
})
</script>
<template>
<header class="flex items-center justify-between px-6 py-4 bg-white border-b-4 border-indigo-600">
<div class="flex items-center">
<button class="text-gray-500 focus:outline-none lg:hidden" @click="isOpen = true">
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4 6H20M4 12H20M4 18H11"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<div class="flex items-center">
<div class="relative">
<button
class="relative z-10 block w-8 h-8 overflow-hidden rounded-full shadow focus:outline-none"
@click="dropdownOpen = !dropdownOpen"
>
<img class="object-cover w-full h-full" :src="userProfileIcon" alt="Your avatar" />
</button>
<div
v-show="dropdownOpen"
class="fixed inset-0 z-10 w-full h-full"
@click="dropdownOpen = false"
/>
<transition
enter-active-class="transition duration-150 ease-out transform"
enter-from-class="scale-95 opacity-0"
enter-to-class="scale-100 opacity-100"
leave-active-class="transition duration-150 ease-in transform"
leave-from-class="scale-100 opacity-100"
leave-to-class="scale-95 opacity-0"
>
<div
v-show="dropdownOpen"
class="absolute right-0 z-20 w-48 py-2 mt-2 bg-white rounded-md shadow-xl"
>
<div class="flex items-center justify-between px-4 py-2 text-sm text-indigo-600">
<strong>{{ user.name }}</strong>
</div>
<hr />
<div
class="flex items-center justify-between px-4 py-2 text-sm text-gray-700 hover:bg-indigo-600 hover:text-white cursor-pointer"
@click="toggleActive"
>
<span>{{ isAdminActive ? 'Admin' : 'Classic' }}</span>
<button
:class="{ 'bg-indigo-800': isAdminActive, 'bg-gray-200': !isAdminActive }"
class="w-10 h-5 rounded-full focus:outline-none relative"
>
<span
:class="{ 'translate-x-5': isAdminActive, 'translate-x-0': !isAdminActive }"
class="block w-4 h-4 bg-white rounded-full shadow transform transition duration-300"
></span>
</button>
</div>
<Link
href="/auth/logout"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-indigo-600 hover:text-white"
>
Logout
</Link>
</div>
</transition>
</div>
</div>
</header>
</template>
+99
View File
@@ -0,0 +1,99 @@
<script lang="ts" setup>
import { ref, useAttrs } from 'vue'
import { useSidebar } from '~/composables/useSidebar'
import { Link } from '@inertiajs/vue3'
import { siteIcon } from '~/assets/icons'
const { isOpen } = useSidebar()
const activeClass = ref('bg-gray-600/25 text-gray-100 border-gray-100')
const inactiveClass = ref(
'border-gray-900 text-gray-500 hover:bg-gray-600/25 hover:text-gray-100'
)
const route = useAttrs()
console.log(route)
</script>
<template>
<div class="flex">
<!-- Backdrop -->
<div
:class="isOpen ? 'block' : 'hidden'"
class="fixed inset-0 z-20 transition-opacity bg-black opacity-50 lg:hidden"
@click="isOpen = false"
/>
<!-- End Backdrop -->
<div
:class="isOpen ? 'translate-x-0 ease-out' : '-translate-x-full ease-in'"
class="fixed inset-y-0 left-0 z-30 w-64 overflow-y-auto transition duration-300 transform bg-gray-900 lg:translate-x-0 lg:static lg:inset-0"
>
<div class="flex items-center justify-center mt-8">
<div class="flex items-center">
<img :src="siteIcon" style="width: 64px; height: 64px; fill: aliceblue" />
<span class="mx-2 text-2xl font-semibold text-white">PC CREATOR</span>
</div>
</div>
<nav class="mt-10">
<Link
:class="[$page.url.startsWith('/computers') ? activeClass : inactiveClass]"
class="flex items-center px-6 py-2 mt-4 duration-200 border-l-4"
href="/computers"
>
<svg
class="h-5 w-5"
fill="currentColor"
viewBox="0 0 512.000000 512.000000"
xmlns="http://www.w3.org/2000/svg"
>
<g stroke="none" transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)">
<path
d="M660 5089 c-116 -59 -216 -180 -257 -314 -17 -57 -18 -120 -18 -1140 0 -1014 1 -1082 18 -1112 31 -58 53 -63 287 -63 l210 0 0 1090 0 1090 1660 0 1660 0 0 -1090 0 -1090 210 0 c234 0 256 5 287 63 17 30 18 98 18 1112 0 1020 -1 1083 -18 1140 -41 134 -141 255 -257 314 l-63 31 -1837 0 -1837 0 -63 -31z m1948 -157 c17 -22 15 -62 -5 -84 -34 -38 -103 -11 -103 42 0 61 70 89 108 42z"
/>
<path
d="M2055 3549 c-38 -5 -92 -15 -119 -22 l-49 -13 386 -334 c430 -372 439 -382 438 -489 0 -33 -7 -76 -16 -98 -11 -25 -100 -122 -272 -295 -290 -290 -308 -303 -419 -296 -46 3 -72 11 -102 31 -23 15 -188 197 -369 406 -180 209 -331 380 -334 380 -16 2 -40 -178 -40 -289 2 -395 212 -734 566 -912 167 -85 319 -113 545 -104 125 6 166 4 214 -9 72 -19 145 -55 198 -96 21 -17 261 -304 533 -637 271 -334 510 -620 530 -636 143 -112 323 -132 487 -55 87 42 351 304 395 394 67 133 70 263 10 389 -56 119 8 61 -852 760 -425 345 -497 411 -535 486 -48 97 -54 135 -55 370 -1 197 -4 234 -22 302 -52 185 -134 330 -262 462 -168 174 -374 275 -616 305 -101 12 -139 12 -240 0z m2116 -2684 c102 -35 169 -130 169 -239 -1 -177 -161 -295 -332 -244 -48 14 -123 78 -147 127 -72 141 5 315 159 362 42 12 104 10 151 -6z"
/>
<path
d="M380 2259 c-19 -12 -112 -115 -208 -231 l-173 -210 3 -161 c3 -183 12 -212 80 -278 74 -71 41 -69 917 -69 l786 1 -92 38 c-371 154 -651 476 -749 859 l-18 72 -256 0 c-240 0 -257 -2 -290 -21z m92 -606 c79 -79 25 -213 -87 -213 -127 0 -174 164 -66 234 41 26 115 16 153 -21z"
/>
<path
d="M3465 2258 c15 -51 63 -95 383 -355 l335 -273 316 0 c354 0 361 -1 361 -64 0 -63 -9 -65 -274 -68 l-237 -3 114 -92 114 -93 165 0 c190 0 236 11 296 69 68 66 77 95 80 278 l3 162 -178 215 c-137 165 -187 218 -216 230 -33 14 -116 16 -653 16 l-615 0 6 -22z"
/>
</g>
</svg>
<span class="mx-4">Computers</span>
</Link>
<Link
:class="[$page.url.startsWith('/components') ? activeClass : inactiveClass]"
class="flex items-center px-6 py-2 mt-4 duration-200 border-l-4"
href="/components"
>
<svg
class="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 3C3.89543 3 3 3.89543 3 5V7C3 8.10457 3.89543 9 5 9H7C8.10457 9 9 8.10457 9 7V5C9 3.89543 8.10457 3 7 3H5Z"
/>
<path
d="M5 11C3.89543 11 3 11.8954 3 13V15C3 16.1046 3.89543 17 5 17H7C8.10457 17 9 16.1046 9 15V13C9 11.8954 8.10457 11 7 11H5Z"
/>
<path
d="M11 5C11 3.89543 11.8954 3 13 3H15C16.1046 3 17 3.89543 17 5V7C17 8.10457 16.1046 9 15 9H13C11.8954 9 11 8.10457 11 7V5Z"
/>
<path
d="M11 13C11 11.8954 11.8954 11 13 11H15C16.1046 11 17 11.8954 17 13V15C17 16.1046 16.1046 17 15 17H13C11.8954 17 11 16.1046 11 15V13Z"
/>
</svg>
<span class="mx-4">Components</span>
</Link>
</nav>
</div>
</div>
</template>
+77
View File
@@ -0,0 +1,77 @@
import { PageProps } from '@adonisjs/inertia/types'
import { InertiaAppProps } from '@inertiajs/vue3/types/app'
interface ComponentType {
id: number
name: string
unique: boolean
}
interface ComputerState {
id: number
name: string
}
export type Computer = {
id: number
title: string
sellPrice: number
gpuScore: number
cpuScore: number
globalScore: number
state: ComputerState
cost: number
pictures: { link: string; order: number }[]
components: Component[]
}
export type Component = {
id: number
name: string
price: string
componentTypeId: number
computerId: number | null
userId: number
componentStateId: number
purchaseDate: string
createdAt: string
updatedAt: string
type: ComponentType
}
interface ServerImage {
id: number
link: string
order: number
}
interface NewImage {
file: File
order: number
}
export type UIVineValidationError = {
message: string
rule: string
field: string
meta?: any
}
export type UIAuthError = {
status: number
code: string
message: string
}
interface SharedProps extends PageProps {
auth: {
user: {
id: number
name: string
email: string
}
}
}
export type PageWithSharedProps = InertiaAppProps<SharedProps>
export type Image = ServerImage | NewImage
+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>