Skip to content

Commit dc68108

Browse files
committed
Implement image ordering
1 parent 1ff6c21 commit dc68108

File tree

7 files changed

+126
-21
lines changed

7 files changed

+126
-21
lines changed

app/Http/Controllers/Api/ProductController.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ public function store(ProductRequest $request)
5050

5151
/** @var \Illuminate\Http\UploadedFile[] $images */
5252
$images = $data['images'] ?? [];
53+
$imagePositions = $data['image_positions'] ?? [];
5354

5455
$product = Product::create($data);
5556

56-
$this->saveImages($images, $product);
57+
$this->saveImages($images, $imagePositions, $product);
5758

5859
return new ProductResource($product);
5960
}
@@ -84,8 +85,9 @@ public function update(ProductRequest $request, Product $product)
8485
/** @var \Illuminate\Http\UploadedFile[] $images */
8586
$images = $data['images'] ?? [];
8687
$deletedImages = $data['deleted_images'] ?? [];
88+
$imagePositions = $data['image_positions'] ?? [];
8789

88-
$this->saveImages($images, $product);
90+
$this->saveImages($images, $imagePositions, $product);
8991
if (count($deletedImages) > 0) {
9092
$this->deleteImages($deletedImages, $product);
9193
}
@@ -116,9 +118,15 @@ public function destroy(Product $product)
116118
* @throws \Exception
117119
* @author Zura Sekhniashvili <[email protected]>
118120
*/
119-
private function saveImages($images, Product $product)
121+
private function saveImages($images, $positions, Product $product)
120122
{
121-
foreach ($images as $i => $image) {
123+
foreach ($positions as $id => $position) {
124+
ProductImage::query()
125+
->where('id', $id)
126+
->update(['position' => $position]);
127+
}
128+
129+
foreach ($images as $id => $image) {
122130
$path = 'images/' . Str::random();
123131
if (!Storage::exists($path)) {
124132
Storage::makeDirectory($path, 0755, true);
@@ -135,7 +143,7 @@ private function saveImages($images, Product $product)
135143
'url' => URL::to(Storage::url($relativePath)),
136144
'mime' => $image->getClientMimeType(),
137145
'size' => $image->getSize(),
138-
'position' => $i + 1
146+
'position' => $positions[$id]
139147
]);
140148
}
141149
}

app/Http/Requests/ProductRequest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public function rules()
2727
'title' => ['required', 'max:2000'],
2828
'images.*' => ['nullable', 'image'],
2929
'deleted_images.*' => ['nullable', 'int'],
30+
'image_positions.*' => ['nullable', 'int'],
3031
'price' => ['required', 'numeric'],
3132
'quantity' => ['nullable', 'numeric'],
3233
'description' => ['nullable', 'string'],

backend/package-lock.json

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"@ckeditor/ckeditor5-vue": "^5.1.0",
1313
"axios": "^0.27.2",
1414
"chart.js": "^3.9.1",
15+
"sortablejs": "^1.15.0",
16+
"sortablejs-vue3": "^1.2.9",
1517
"uuid": "^9.0.0",
1618
"vue": "^3.2.25",
1719
"vue-chartjs": "^4.1.2",

backend/src/components/ImagePreview.vue

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
<template>
22
<div class="flex flex-wrap gap-1">
3-
<div v-for="image of imageUrls"
4-
class="relative w-[120px] h-[120px] rounded border border-dashed flex items-center justify-center hover:border-purple-500 overflow-hidden">
5-
<img :src="image.url" class="max-w-full max-h-full" :class="image.deleted ? 'opacity-50' : ''">
6-
<small v-if="image.deleted" class="absolute left-0 bottom-0 right-0 py-1 px-2 bg-black w-100 text-white justify-between items-center flex">
7-
To be deleted
8-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 cursor-pointer" @click="revertImage(image)">
9-
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
10-
</svg>
11-
</small>
12-
<span class="absolute top-1 right-1 cursor-pointer" @click="removeImage(image)">
3+
<Sortable
4+
:list="imageUrls"
5+
item-key="id"
6+
class="flex gap-1 flex-wrap"
7+
@end="onImageDragEnd"
8+
>
9+
<template #item="{element: image, index}">
10+
<div
11+
class="relative w-[120px] h-[120px] rounded border border-dashed flex items-center justify-center hover:border-purple-500 overflow-hidden">
12+
<img :src="image.url" class="max-w-full max-h-full" :class="image.deleted ? 'opacity-50' : ''">
13+
<small v-if="image.deleted"
14+
class="absolute left-0 bottom-0 right-0 py-1 px-2 bg-black w-100 text-white justify-between items-center flex">
15+
To be deleted
16+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
17+
stroke="currentColor" class="w-4 h-4 cursor-pointer" @click="revertImage(image)">
18+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3"/>
19+
</svg>
20+
</small>
21+
<span class="absolute top-1 right-1 cursor-pointer" @click="removeImage(image)">
1322
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
1423
<path
1524
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"/>
1625
</svg>
1726
</span>
18-
</div>
27+
</div>
28+
</template>
29+
</Sortable>
1930
<div
2031
class="relative w-[120px] h-[120px] rounded border border-dashed flex items-center justify-center hover:border-purple-500 overflow-hidden">
2132
<span>
@@ -29,6 +40,7 @@
2940

3041
<script setup>
3142
// Imports
43+
import {Sortable} from "sortablejs-vue3";
3244
import {v4 as uuidv4} from 'uuid';
3345
import {onMounted, ref, watch} from "vue";
3446
@@ -39,10 +51,11 @@ import {onMounted, ref, watch} from "vue";
3951
const files = ref([])
4052
const imageUrls = ref([])
4153
const deletedImages = ref([])
54+
const imagePositions = ref([])
4255
4356
// Props & Emit
4457
const props = defineProps(['modelValue', 'deletedImages', 'images'])
45-
const emit = defineEmits(['update:modelValue', 'update:deletedImages'])
58+
const emit = defineEmits(['update:modelValue', 'update:deletedImages', 'update:imagePositions'])
4659
4760
// Computed
4861
@@ -51,16 +64,23 @@ function onFileChange($event) {
5164
const chosenFiles = [...$event.target.files];
5265
files.value = [...files.value, ...chosenFiles];
5366
$event.target.value = ''
67+
const allPromises = [];
5468
for (let file of chosenFiles) {
5569
file.id = uuidv4()
56-
readFile(file)
70+
const promise = readFile(file)
71+
allPromises.push(promise)
72+
promise
5773
.then(url => {
5874
imageUrls.value.push({
5975
url,
6076
id: file.id
6177
})
6278
})
6379
}
80+
Promise.all(allPromises)
81+
.then(() => {
82+
updateImagePositions()
83+
})
6484
emit('update:modelValue', files.value)
6585
}
6686
@@ -87,9 +107,11 @@ function removeImage(image) {
87107
88108
emit('update:modelValue', files.value)
89109
}
110+
111+
updateImagePositions();
90112
}
91113
92-
function revertImage(image){
114+
function revertImage(image) {
93115
if (image.isProp) {
94116
deletedImages.value = deletedImages.value.filter(id => id !== image.id)
95117
image.deleted = false;
@@ -98,6 +120,33 @@ function revertImage(image){
98120
}
99121
}
100122
123+
function onImageDragEnd(ev) {
124+
console.log(ev)
125+
126+
const {newIndex, oldIndex} = ev;
127+
128+
const [tmp] = imageUrls.value.splice(oldIndex, 1)
129+
imageUrls.value.splice(newIndex, 0, tmp)
130+
131+
updateImagePositions()
132+
}
133+
134+
function updateImagePositions() {
135+
/**
136+
* [
137+
* [1, 1],
138+
* [4, 2],
139+
* [5, 3],
140+
* ]
141+
*/
142+
imagePositions.value = Object.fromEntries(
143+
imageUrls.value.filter(im => !im.deleted)
144+
.map((im, ind) => [im.id, ind + 1])
145+
)
146+
147+
emit('update:imagePositions', imagePositions.value)
148+
}
149+
101150
// Hooks
102151
watch('props.images', () => {
103152
console.log(props.images)
@@ -108,6 +157,8 @@ watch('props.images', () => {
108157
isProp: true
109158
}))
110159
]
160+
161+
updateImagePositions()
111162
}, {immediate: true, deep: true})
112163
onMounted(() => {
113164
emit('update:modelValue', [])

backend/src/store/actions.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,13 @@ export function updateProduct({commit}, product) {
100100
const form = new FormData();
101101
form.append('id', product.id);
102102
form.append('title', product.title);
103-
product.images.forEach(im => form.append('images[]', im))
103+
product.images.forEach(im => form.append(`images[${im.id}]`, im))
104104
if (product.deleted_images) {
105105
product.deleted_images.forEach(id => form.append('deleted_images[]', id))
106106
}
107+
for (let id in product.image_positions) {
108+
form.append(`image_positions[${id}]`, product.image_positions[id])
109+
}
107110
form.append('description', product.description || '');
108111
form.append('published', product.published ? 1 : 0);
109112
form.append('price', product.price);

backend/src/views/Products/ProductForm.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
<div class="col-span-1 px-4 pt-5 pb-4">
2121
<image-preview v-model="product.images"
2222
:images="product.images"
23-
v-model:deleted-images="product.deleted_images" />
23+
v-model:deleted-images="product.deleted_images"
24+
v-model:image-positions="product.image_positions"/>
2425
</div>
2526
</div>
2627
<footer class="bg-gray-50 rounded-b-lg px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
@@ -61,6 +62,7 @@ const product = ref({
6162
title: null,
6263
images: [],
6364
deleted_images: [],
65+
image_positions: {},
6466
description: '',
6567
price: null,
6668
quantity: null,

0 commit comments

Comments
 (0)