dress/resources/js/pages/admin/master/product/create.tsx

423 lines
22 KiB
TypeScript

import { Head, Link } from '@inertiajs/react';
import { useForm } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import productRoutes from '@/routes/product';
import React, { useRef, useState } from 'react';
import { toast } from 'sonner';
import { SimpleEditor } from '@/components/tiptap-templates/simple/simple-editor'
import { Category } from '@/types/category';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxChips,
ComboboxChip,
ComboboxChipsInput,
ComboboxValue,
useComboboxAnchor,
} from "@/components/ui/combobox"
import { NumericFormat } from 'react-number-format';
import { ImagePlus, X, Upload } from 'lucide-react';
export default function ProductCreate({ categories }: { categories: Category[] }) {
const { data, setData, post, processing, errors } = useForm<{
name: string;
description: string;
category_ids: number[];
prices: {
purchase: string;
distributor: string;
agent: string;
reseller: string;
retail: string;
};
thumbnail: File | null;
images: File[];
}>({
name: '',
description: '',
category_ids: [] as number[],
prices: {
purchase: '',
distributor: '',
agent: '',
reseller: '',
retail: '',
},
thumbnail: null,
images: [],
});
const anchor = useComboboxAnchor();
const thumbnailInputRef = useRef<HTMLInputElement>(null);
const imagesInputRef = useRef<HTMLInputElement>(null);
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null);
const [imagePreviews, setImagePreviews] = useState<{ file: File; preview: string }[]>([]);
const handleThumbnailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setData('thumbnail', file);
const reader = new FileReader();
reader.onloadend = () => setThumbnailPreview(reader.result as string);
reader.readAsDataURL(file);
};
const removeThumbnail = () => {
setData('thumbnail', null);
setThumbnailPreview(null);
if (thumbnailInputRef.current) thumbnailInputRef.current.value = '';
};
const handleImagesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files ?? []);
if (!files.length) return;
const newPreviews = files.map(file => ({
file,
preview: URL.createObjectURL(file),
}));
setImagePreviews(prev => [...prev, ...newPreviews]);
setData('images', [...data.images, ...files]);
if (imagesInputRef.current) imagesInputRef.current.value = '';
};
const removeImage = (index: number) => {
const updated = imagePreviews.filter((_, i) => i !== index);
setImagePreviews(updated);
setData('images', updated.map(p => p.file));
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
post(productRoutes.store().url, {
forceFormData: true,
onSuccess: (response: any) => {
toast.success(response.props.flash.success);
},
});
};
const filteredCategories = categories.filter((c) => !data.category_ids.includes(c.id));
return (
<div className="flex flex-col gap-6 p-6">
<Head title="Tambah Produk" />
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Tambah Produk</h1>
</div>
<Link href={productRoutes.index().url}>
<Button variant='outline'>
Kembali
</Button>
</Link>
</div>
<form onSubmit={onSubmit} className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
<CardHeader className="border-b">
<CardTitle>Informasi Produk</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<Field>
<Label htmlFor="name" required>Nama</Label>
<Input
id="name"
name="name"
value={data.name}
onChange={e => setData('name', e.target.value)}
autoComplete='off'
placeholder='Contoh: Gamis Wanita'
maxLength={100}
/>
{errors.name && <p className="text-xs text-red-500">{errors.name}</p>}
</Field>
<Field>
<Label htmlFor="category_ids" required>Kategori</Label>
<Combobox
multiple
autoHighlight
items={filteredCategories}
value={categories.filter(c => data.category_ids.includes(c.id))}
onValueChange={(selected) => {
setData(
"category_ids",
selected.map((item: Category) => item.id)
)
}}
>
<ComboboxChips ref={anchor} className="w-full">
<ComboboxValue>
{(values: Category[]) => (
<>
{values.map((value) => (
<ComboboxChip key={value.id}>
{value.name}
</ComboboxChip>
))}
<ComboboxChipsInput placeholder='Pilih Kategori' />
</>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxContent anchor={anchor}>
<ComboboxEmpty>Data tidak ditemukan.</ComboboxEmpty>
<ComboboxList>
{(item: Category) => (
<ComboboxItem key={item.id} value={item}>
{item.name}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
{errors.category_ids && <p className="text-xs text-red-500 mt-1">{errors.category_ids}</p>}
</Field>
<Field>
<Label htmlFor="description">Deskripsi</Label>
<SimpleEditor
value={data.description}
onChange={(val) => setData('description', val)}
/>
{errors.description && <p className="text-xs text-red-500 mt-1">{errors.description}</p>}
</Field>
</CardContent>
</Card>
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
<CardHeader className="border-b">
<CardTitle>Harga Produk</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Field>
<Label htmlFor="price_purchase" required>Harga Beli</Label>
<NumericFormat
id="price_purchase"
customInput={Input}
thousandSeparator="."
decimalSeparator=","
prefix="Rp "
value={data.prices.purchase}
onValueChange={(values) => {
setData('prices', {
...data.prices,
purchase: values.value
})
}}
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.purchase'] && <p className="text-xs text-red-500">{errors['prices.purchase']}</p>}
</Field>
<Field>
<Label htmlFor="price_distributor" required>Harga Distributor</Label>
<NumericFormat
id="price_distributor"
customInput={Input}
thousandSeparator="."
decimalSeparator=","
prefix="Rp "
value={data.prices.distributor}
onValueChange={(values) => {
setData('prices', {
...data.prices,
distributor: values.value
})
}}
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.distributor'] && <p className="text-xs text-red-500">{errors['prices.distributor']}</p>}
</Field>
<Field>
<Label htmlFor="price_agent" required>Harga Agen</Label>
<NumericFormat
id="price_agent"
customInput={Input}
thousandSeparator="."
decimalSeparator=","
prefix="Rp "
value={data.prices.agent}
onValueChange={(values) => {
setData('prices', {
...data.prices,
agent: values.value
})
}}
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.agent'] && <p className="text-xs text-red-500">{errors['prices.agent']}</p>}
</Field>
<Field>
<Label htmlFor="price_reseller" required>Harga Reseller</Label>
<NumericFormat
id="price_reseller"
customInput={Input}
thousandSeparator="."
decimalSeparator=","
prefix="Rp "
value={data.prices.reseller}
onValueChange={(values) => {
setData('prices', {
...data.prices,
reseller: values.value
})
}}
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.reseller'] && <p className="text-xs text-red-500">{errors['prices.reseller']}</p>}
</Field>
<Field className="md:col-span-2">
<Label htmlFor="price_retail" required>Harga Retail</Label>
<NumericFormat
id="price_retail"
customInput={Input}
thousandSeparator="."
decimalSeparator=","
prefix="Rp "
value={data.prices.retail}
onValueChange={(values) => {
setData('prices', {
...data.prices,
retail: values.value
})
}}
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.retail'] && <p className="text-xs text-red-500">{errors['prices.retail']}</p>}
</Field>
</div>
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
<CardHeader className="border-b">
<CardTitle>Thumbnail</CardTitle>
</CardHeader>
<CardContent>
{thumbnailPreview ? (
<div className="relative inline-block">
<img
src={thumbnailPreview}
alt="Thumbnail preview"
className="object-cover rounded-xl border shadow"
/>
<button
type="button"
onClick={removeThumbnail}
className="absolute -top-2 -right-2 bg-red-500 hover:bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center shadow-md transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
) : (
<button
type="button"
onClick={() => thumbnailInputRef.current?.click()}
className="flex flex-col items-center justify-center w-full h-40 border-2 border-dashed border-border rounded-xl hover:border-primary hover:bg-primary/5 transition-all cursor-pointer group"
>
<ImagePlus className="w-10 h-10 text-muted-foreground group-hover:text-primary transition-colors mb-2" />
<span className="text-sm text-muted-foreground group-hover:text-primary transition-colors font-medium">Klik untuk upload thumbnail</span>
<span className="text-xs text-muted-foreground mt-1">PNG, JPG, WEBP maks. 5MB</span>
</button>
)}
<input
ref={thumbnailInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleThumbnailChange}
/>
{errors.thumbnail && <p className="text-xs text-red-500 mt-2">{errors.thumbnail}</p>}
</CardContent>
</Card>
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm">
<CardHeader className="border-b">
<CardTitle>Galeri Gambar</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
{imagePreviews.map((item, index) => (
<div key={index} className="relative group aspect-square">
<img
src={item.preview}
alt={`Gambar ${index + 1}`}
className="object-cover rounded-lg border shadow-sm"
/>
<button
type="button"
onClick={() => removeImage(index)}
className="absolute -top-2 -right-2 bg-red-500 hover:bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center shadow-md transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
))}
<button
type="button"
onClick={() => imagesInputRef.current?.click()}
className="aspect-square flex flex-col items-center justify-center border-2 border-dashed border-border rounded-lg hover:border-primary hover:bg-primary/5 transition-all cursor-pointer group"
>
<Upload className="w-6 h-6 text-muted-foreground group-hover:text-primary transition-colors mb-1" />
<span className="text-xs text-muted-foreground group-hover:text-primary transition-colors">Tambah</span>
</button>
</div>
<input
ref={imagesInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={handleImagesChange}
/>
{errors['images'] && <p className="text-xs text-red-500">{errors['images']}</p>}
</CardContent>
</Card>
<div className="flex flex-col gap-4 p-4 rounded-xl bg-primary/5 border border-primary/10 shadow-sm">
<Button type="submit" className="w-full" disabled={processing}>
{processing ? 'Menyimpan...' : 'Simpan'}
</Button>
</div>
</div>
</div>
</form>
</div>
);
}
ProductCreate.layout = {
breadcrumbs: [
{
title: 'Master',
},
],
};