Skip to content

Commit 3aac6aa

Browse files
committed
feat: add image preview functionality to inventory form
1 parent 5f537ed commit 3aac6aa

2 files changed

Lines changed: 139 additions & 25 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { ref } from 'vue';
2+
3+
export function useImagePreviews(imageCount: number = 3) {
4+
const imagePreviews = ref<(string | null)[]>(Array(imageCount).fill(null));
5+
6+
const handleImageSelect = (event: Event, index: number): void => {
7+
const input = event.target as HTMLInputElement;
8+
const file = input.files?.[0];
9+
10+
if (file) {
11+
const reader = new FileReader();
12+
reader.onload = (e) => {
13+
imagePreviews.value[index] = e.target?.result as string;
14+
};
15+
reader.readAsDataURL(file);
16+
}
17+
};
18+
19+
const removeImage = (index: number, inputId: string): void => {
20+
imagePreviews.value[index] = null;
21+
const input = document.getElementById(inputId) as HTMLInputElement;
22+
if (input) {
23+
input.value = '';
24+
}
25+
};
26+
27+
const setInitialPreviews = (initialImages: (string | null)[]): void => {
28+
imagePreviews.value = [...initialImages];
29+
};
30+
31+
const clearAllPreviews = (): void => {
32+
imagePreviews.value = Array(imageCount).fill(null);
33+
};
34+
35+
const hasAnyImage = (): boolean => {
36+
return imagePreviews.value.some(preview => preview !== null);
37+
};
38+
39+
return {
40+
imagePreviews,
41+
handleImageSelect,
42+
removeImage,
43+
setInitialPreviews,
44+
clearAllPreviews,
45+
hasAnyImage,
46+
};
47+
}

resources/js/pages/admin/Inventory/Create.vue

Lines changed: 92 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { Button } from '@/components/ui/button';
1111
import Select from '@/components/Select.vue';
1212
import Textarea from '@/components/ui/textarea/Textarea.vue';
1313
import { Spinner } from '@/components/ui/spinner';
14+
import { X, Upload } from 'lucide-vue-next';
15+
import { useImagePreviews } from '@/composables/useImagePreviews';
1416
1517
const breadcrumbs: BreadcrumbItem[] = [
1618
{
@@ -33,6 +35,8 @@ const categories = [
3335
'Paint and Finishes',
3436
'Chemicals'
3537
];
38+
39+
const { imagePreviews, handleImageSelect, removeImage } = useImagePreviews(3);
3640
</script>
3741

3842
<template>
@@ -41,7 +45,7 @@ const categories = [
4145

4246
<AppLayout :breadcrumbs="breadcrumbs">
4347
<div class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4">
44-
<div class="flex justify-between items-center">
48+
<div class="flex justify-between items-center mb-4">
4549
<div>
4650
<h1 class="text-2xl font-bold">Add New Inventory Item</h1>
4751
<p class="text-sm text-muted-foreground">Fill in the details to add a new item to the inventory.</p>
@@ -66,21 +70,21 @@ const categories = [
6670
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
6771
<!-- Item Name -->
6872
<div class="space-y-2">
69-
<Label for="item_name">Item Name</Label>
73+
<Label for="item_name">Item Name<span class="text-red-500">*</span></Label>
7074
<Input id="item_name" name="item_name" placeholder="Enter item name" required />
7175
<InputError :message="errors.item_name" />
7276
</div>
7377

7478
<!-- Brand Name -->
7579
<div class="space-y-2">
76-
<Label for="brand_name">Brand Name</Label>
80+
<Label for="brand_name">Brand Name<span class="text-red-500">*</span></Label>
7781
<Input id="brand_name" name="brand_name" placeholder="Enter brand name" />
7882
<InputError :message="errors.brand_name" />
7983
</div>
8084

8185
<!-- Category -->
8286
<div class="space-y-2">
83-
<Label for="category">Category</Label>
87+
<Label for="category">Category<span class="text-red-500">*</span></Label>
8488
<Select name="category" :options="categories" placeholder="Select a category" required />
8589
<InputError :message="errors.category" />
8690
</div>
@@ -94,23 +98,27 @@ const categories = [
9498

9599
<!-- Unit Price -->
96100
<div class="space-y-2">
97-
<Label for="unit_price">Unit Price</Label>
101+
<Label for="unit_price">Unit Price<span class="text-red-500">*</span></Label>
98102
<Input id="unit_price" name="unit_price" type="number" step="0.01" placeholder="0.00"
103+
class="[&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
99104
required />
100105
<InputError :message="errors.unit_price" />
101106
</div>
102107

103108
<!-- Quantity -->
104109
<div class="space-y-2">
105-
<Label for="quantity">Quantity</Label>
106-
<Input id="quantity" name="quantity" type="number" placeholder="0" required />
110+
<Label for="quantity">Quantity<span class="text-red-500">*</span></Label>
111+
<Input id="quantity" name="quantity" type="number" placeholder="0"
112+
class="[&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
113+
required />
107114
<InputError :message="errors.quantity" />
108115
</div>
109116

110117
<!-- Restock Threshold -->
111118
<div class="space-y-2">
112-
<Label for="restock_threshold">Restock Threshold</Label>
113-
<Input id="restock_threshold" name="restock_threshold" type="number" placeholder="10" />
119+
<Label for="restock_threshold">Restock Threshold<span class="text-red-500">*</span></Label>
120+
<Input id="restock_threshold" name="restock_threshold" type="number" placeholder="10"
121+
class="[&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" />
114122
<InputError :message="errors.restock_threshold" />
115123
</div>
116124
</div>
@@ -122,22 +130,81 @@ const categories = [
122130
<InputError :message="errors.description" />
123131
</div>
124132

125-
<!-- Images -->
126-
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
127-
<div class="space-y-2">
128-
<Label for="item_image_1">Image 1</Label>
129-
<Input id="item_image_1" name="item_image_1" type="file" accept="image/*" />
130-
<InputError :message="errors.item_image_1" />
131-
</div>
132-
<div class="space-y-2">
133-
<Label for="item_image_2">Image 2</Label>
134-
<Input id="item_image_2" name="item_image_2" type="file" accept="image/*" />
135-
<InputError :message="errors.item_image_2" />
136-
</div>
137-
<div class="space-y-2">
138-
<Label for="item_image_3">Image 3</Label>
139-
<Input id="item_image_3" name="item_image_3" type="file" accept="image/*" />
140-
<InputError :message="errors.item_image_3" />
133+
<!-- Images with Preview -->
134+
<div class="space-y-2">
135+
<Label>Product Images</Label>
136+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
137+
<!-- Image 1 -->
138+
<div class="space-y-2">
139+
<div class="relative group">
140+
<div v-if="imagePreviews[0]"
141+
class="relative aspect-square rounded-lg border-2 border-border overflow-hidden bg-muted">
142+
<img :src="imagePreviews[0]" alt="Preview 1" class="w-full h-full object-cover" />
143+
<button type="button" @click="removeImage(0, 'item_image_1')"
144+
class="absolute top-2 right-2 p-1.5 bg-destructive text-destructive-foreground rounded-full shadow-lg hover:bg-destructive/90 transition-colors"
145+
aria-label="Remove image 1">
146+
<X :size="16" />
147+
</button>
148+
</div>
149+
<label v-else for="item_image_1"
150+
class="flex flex-col items-center justify-center aspect-square rounded-lg border-2 border-dashed border-border hover:border-primary hover:bg-accent cursor-pointer transition-colors">
151+
<Upload :size="32" class="text-muted-foreground mb-2" />
152+
<span class="text-sm text-muted-foreground">Upload Image 1</span>
153+
<span class="text-xs text-muted-foreground mt-1">Click to browse</span>
154+
</label>
155+
<Input id="item_image_1" name="item_image_1" type="file" accept="image/*" class="hidden"
156+
@change="(e: Event) => handleImageSelect(e, 0)" />
157+
</div>
158+
<InputError :message="errors.item_image_1" />
159+
</div>
160+
161+
<!-- Image 2 -->
162+
<div class="space-y-2">
163+
<div class="relative group">
164+
<div v-if="imagePreviews[1]"
165+
class="relative aspect-square rounded-lg border-2 border-border overflow-hidden bg-muted">
166+
<img :src="imagePreviews[1]" alt="Preview 2" class="w-full h-full object-cover" />
167+
<button type="button" @click="removeImage(1, 'item_image_2')"
168+
class="absolute top-2 right-2 p-1.5 bg-destructive text-destructive-foreground rounded-full shadow-lg hover:bg-destructive/90 transition-colors"
169+
aria-label="Remove image 2">
170+
<X :size="16" />
171+
</button>
172+
</div>
173+
<label v-else for="item_image_2"
174+
class="flex flex-col items-center justify-center aspect-square rounded-lg border-2 border-dashed border-border hover:border-primary hover:bg-accent cursor-pointer transition-colors">
175+
<Upload :size="32" class="text-muted-foreground mb-2" />
176+
<span class="text-sm text-muted-foreground">Upload Image 2 (optional)</span>
177+
<span class="text-xs text-muted-foreground mt-1">Click to browse</span>
178+
</label>
179+
<Input id="item_image_2" name="item_image_2" type="file" accept="image/*" class="hidden"
180+
@change="(e: Event) => handleImageSelect(e, 1)" />
181+
</div>
182+
<InputError :message="errors.item_image_2" />
183+
</div>
184+
185+
<!-- Image 3 -->
186+
<div class="space-y-2">
187+
<div class="relative group">
188+
<div v-if="imagePreviews[2]"
189+
class="relative aspect-square rounded-lg border-2 border-border overflow-hidden bg-muted">
190+
<img :src="imagePreviews[2]" alt="Preview 3" class="w-full h-full object-cover" />
191+
<button type="button" @click="removeImage(2, 'item_image_3')"
192+
class="absolute top-2 right-2 p-1.5 bg-destructive text-destructive-foreground rounded-full shadow-lg hover:bg-destructive/90 transition-colors"
193+
aria-label="Remove image 3">
194+
<X :size="16" />
195+
</button>
196+
</div>
197+
<label v-else for="item_image_3"
198+
class="flex flex-col items-center justify-center aspect-square rounded-lg border-2 border-dashed border-border hover:border-primary hover:bg-accent cursor-pointer transition-colors">
199+
<Upload :size="32" class="text-muted-foreground mb-2" />
200+
<span class="text-sm text-muted-foreground">Upload Image 3 (optional)</span>
201+
<span class="text-xs text-muted-foreground mt-1">Click to browse</span>
202+
</label>
203+
<Input id="item_image_3" name="item_image_3" type="file" accept="image/*" class="hidden"
204+
@change="(e: Event) => handleImageSelect(e, 2)" />
205+
</div>
206+
<InputError :message="errors.item_image_3" />
207+
</div>
141208
</div>
142209
</div>
143210

0 commit comments

Comments
 (0)