Skip to content

Commit 18b185c

Browse files
committed
Implement product creation
1 parent fd42932 commit 18b185c

File tree

6 files changed

+177
-8
lines changed

6 files changed

+177
-8
lines changed

app/Http/Requests/ProductRequest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ public function rules()
2525
{
2626
return [
2727
'title' => ['required', 'max:2000'],
28-
'image' => ['required', 'image'],
29-
'price' => ['required', 'decimal'],
30-
'description' => ['string']
28+
'image' => ['nullable', 'image'],
29+
'price' => ['required', 'numeric'],
30+
'description' => ['nullable', 'string']
3131
];
3232
}
3333
}

app/Models/Product.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class Product extends Model
1414
use HasSlug;
1515
use SoftDeletes;
1616

17+
protected $fillable = ['title', 'description', 'price'];
18+
1719
/**
1820
* Get the options for generating the slug.
1921
*/
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<template>
2+
<div>
3+
<label class="sr-only">{{ label }}</label>
4+
<template v-if="type === 'textarea'">
5+
<textarea :name="name"
6+
:required="required"
7+
:value="props.modelValue"
8+
@input="emit('update:modelValue', $event.target.value)"
9+
class="block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
10+
:placeholder="label"></textarea>
11+
</template>
12+
<template v-else>
13+
<input :type="type"
14+
:name="name"
15+
:required="required"
16+
:value="props.modelValue"
17+
@input="emit('update:modelValue', $event.target.value)"
18+
class="block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
19+
:placeholder="label"/>
20+
</template>
21+
</div>
22+
</template>
23+
24+
<script setup>
25+
26+
const props = defineProps({
27+
modelValue: String,
28+
label: String,
29+
type: {
30+
type: String,
31+
default: 'text'
32+
},
33+
name: String,
34+
required: Boolean
35+
})
36+
37+
const emit = defineEmits(['update:modelValue'])
38+
39+
</script>
40+
41+
<style scoped>
42+
43+
</style>

backend/src/store/actions.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,14 @@ export function logout({commit}) {
2929
export function getProducts({commit, state}, {url = null, search, per_page, sort_field, sort_direction}) {
3030
commit('setProducts', [true])
3131
url = url || '/products'
32+
const params = {
33+
per_page: state.products.limit,
34+
}
3235
return axiosClient.get(url, {
33-
params: {search, per_page, sort_field, sort_direction}
36+
params: {
37+
...params,
38+
search, per_page, sort_field, sort_direction
39+
}
3440
})
3541
.then((response) => {
3642
commit('setProducts', [false, response.data])
@@ -39,3 +45,7 @@ export function getProducts({commit, state}, {url = null, search, per_page, sort
3945
commit('setProducts', [false])
4046
})
4147
}
48+
49+
export function createProduct({commit}, product) {
50+
return axiosClient.post('/products', product)
51+
}

backend/src/views/AddNewProduct.vue

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<!-- This example requires Tailwind CSS v2.0+ -->
2+
<template>
3+
<TransitionRoot as="template" :show="show">
4+
<Dialog as="div" class="relative z-10" @close="show = false">
5+
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100"
6+
leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
7+
<div class="fixed inset-0 bg-black bg-opacity-70 transition-opacity"/>
8+
</TransitionChild>
9+
10+
<div class="fixed z-10 inset-0 overflow-y-auto">
11+
<div class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0">
12+
<TransitionChild as="template" enter="ease-out duration-300"
13+
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
14+
enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200"
15+
leave-from="opacity-100 translate-y-0 sm:scale-100"
16+
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
17+
<DialogPanel
18+
class="relative bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg sm:w-full">
19+
<header class="py-3 px-4 flex justify-between items-center">
20+
<DialogTitle as="h3" class="text-lg leading-6 font-medium text-gray-900"> Create new Product
21+
</DialogTitle>
22+
<button
23+
@click="closeModal()"
24+
class="w-8 h-8 flex items-center justify-center rounded-full transition-colors cursor-pointer hover:bg-[rgba(0,0,0,0.2)]"
25+
>
26+
<svg
27+
xmlns="http://www.w3.org/2000/svg"
28+
class="h-6 w-6"
29+
fill="none"
30+
viewBox="0 0 24 24"
31+
stroke="currentColor"
32+
>
33+
<path
34+
stroke-linecap="round"
35+
stroke-linejoin="round"
36+
stroke-width="2"
37+
d="M6 18L18 6M6 6l12 12"
38+
/>
39+
</svg>
40+
</button>
41+
</header>
42+
<form @submit.prevent="onSubmit">
43+
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
44+
<CustomInput class="mb-2" v-model="product.title" label="Product Title"/>
45+
<CustomInput type="textarea" class="mb-2" v-model="product.description" label="Description"/>
46+
<CustomInput type="number" class="mb-2" v-model="product.price" label="Price"/>
47+
<CustomInput class="mb-2" v-model="product.title" label="Product Title"/>
48+
</div>
49+
<footer class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
50+
<button type="submit"
51+
class="py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ml-3">
52+
Submit
53+
</button>
54+
<button type="button"
55+
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
56+
@click="show = false" ref="cancelButtonRef">Cancel
57+
</button>
58+
</footer>
59+
</form>
60+
</DialogPanel>
61+
</TransitionChild>
62+
</div>
63+
</div>
64+
</Dialog>
65+
</TransitionRoot>
66+
</template>
67+
68+
<script setup>
69+
import {computed, ref} from 'vue'
70+
import {Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot} from '@headlessui/vue'
71+
import {ExclamationIcon} from '@heroicons/vue/outline'
72+
import CustomInput from "../components/core/CustomInput.vue";
73+
import store from "../store/index.js";
74+
75+
const product = ref({
76+
title: null,
77+
image: null,
78+
description: null,
79+
price: null
80+
})
81+
82+
const props = defineProps({
83+
modelValue: Boolean,
84+
})
85+
86+
const emit = defineEmits(['update:modelValue'])
87+
88+
const show = computed({
89+
get: () => props.modelValue,
90+
set: (value) => emit('update:modelValue', value)
91+
})
92+
93+
function closeModal() {
94+
show.value = false
95+
}
96+
97+
function onSubmit() {
98+
store.dispatch('createProduct', product.value)
99+
.then(response => {
100+
debugger;
101+
if (response.status === 201) {
102+
// TODO show notification
103+
store.dispatch('getProducts')
104+
}
105+
})
106+
}
107+
</script>

backend/src/views/Products.vue

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
<template>
22
<div class="flex items-center justify-between mb-3">
33
<h1 class="text-3xl font-semibold">Products</h1>
4-
<button type="submit"
5-
class="flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
4+
<button type="button"
5+
@click="showAddNewModal()"
6+
class="py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
67
>
78
Add new Product
89
</button>
910
</div>
1011
<div class="bg-white p-4 rounded-lg shadow">
11-
{{ search }}
12-
1312
<div class="flex justify-between border-b-2 pb-3">
1413
<div class="flex items-center">
1514
<span class="whitespace-nowrap mr-3">Per Page</span>
@@ -111,6 +110,7 @@
111110
</nav>
112111
</div>
113112
</div>
113+
<AddNewProduct v-model="showProductModal"/>
114114
</template>
115115

116116
<script setup>
@@ -119,13 +119,16 @@ import store from "../store";
119119
import Spinner from "../components/core/Spinner.vue";
120120
import {PRODUCTS_PER_PAGE} from "../constants";
121121
import TableHeaderCell from "../components/core/Table/TableHeaderCell.vue";
122+
import AddNewProduct from "./AddNewProduct.vue";
122123
123124
const perPage = ref(PRODUCTS_PER_PAGE);
124125
const search = ref('');
125126
const products = computed(() => store.state.products);
126127
const sortField = ref('updated_at');
127128
const sortDirection = ref('desc')
128129
130+
const showProductModal = ref(false);
131+
129132
onMounted(() => {
130133
getProducts();
131134
})
@@ -163,6 +166,10 @@ function sortProducts(field) {
163166
164167
getProducts()
165168
}
169+
170+
function showAddNewModal() {
171+
showProductModal.value = true
172+
}
166173
</script>
167174

168175
<style scoped>

0 commit comments

Comments
 (0)