Initial commit: PC Builder AdonisJS project
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
>
|
||||
×
|
||||
</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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user