feat: implement product draft functionality for create and edit pages
This commit is contained in:
parent
0126321a6e
commit
841767d5b1
@ -29,7 +29,9 @@ public function update(ProductVariantRequest $request, Product $product, Product
|
|||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->variantService->update($variant, $request->validated()),
|
fn () => $this->variantService->update($variant, $request->validated()),
|
||||||
'Varian berhasil diperbarui.',
|
'Varian berhasil diperbarui.',
|
||||||
'admin.master.products.index'
|
'admin.master.products.index',
|
||||||
|
'admin.master.products.variants.edit',
|
||||||
|
['product' => $product, 'variant' => $variant]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
68
resources/js/hooks/use-product-draft.ts
Normal file
68
resources/js/hooks/use-product-draft.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
saveProductDraft,
|
||||||
|
clearProductDraft,
|
||||||
|
type ProductDraftData,
|
||||||
|
} from '@/lib/product-draft';
|
||||||
|
|
||||||
|
type DraftType = 'create' | 'edit';
|
||||||
|
|
||||||
|
export function useProductDraftSave(
|
||||||
|
type: DraftType,
|
||||||
|
data: ProductDraftData,
|
||||||
|
userId?: number,
|
||||||
|
productId?: number,
|
||||||
|
delay = 500,
|
||||||
|
) {
|
||||||
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const dataRef = useRef(data);
|
||||||
|
dataRef.current = data;
|
||||||
|
|
||||||
|
const flush = useCallback(() => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
timeoutRef.current = null;
|
||||||
|
}
|
||||||
|
const ok = saveProductDraft(type, dataRef.current, userId, productId);
|
||||||
|
return ok;
|
||||||
|
}, [type, userId, productId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
saveProductDraft(type, dataRef.current, userId, productId);
|
||||||
|
timeoutRef.current = null;
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [data, type, userId, productId, delay]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
saveProductDraft(type, dataRef.current, userId, productId);
|
||||||
|
};
|
||||||
|
}, [type, userId, productId]);
|
||||||
|
|
||||||
|
return { flush };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProductDraftClear(
|
||||||
|
type: DraftType,
|
||||||
|
userId?: number,
|
||||||
|
productId?: number,
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearProductDraft(type, userId, productId);
|
||||||
|
};
|
||||||
|
}, [type, userId, productId]);
|
||||||
|
}
|
||||||
73
resources/js/lib/product-draft.ts
Normal file
73
resources/js/lib/product-draft.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
const DRAFT_PREFIX = 'product-draft';
|
||||||
|
|
||||||
|
export type ProductDraftData = {
|
||||||
|
productName: string;
|
||||||
|
status: string;
|
||||||
|
description: string;
|
||||||
|
categoryIds: number[];
|
||||||
|
useSamePrice: boolean;
|
||||||
|
sharedPrices: Array<{ type: string; price: number }>;
|
||||||
|
variants: Array<{
|
||||||
|
id?: number | null;
|
||||||
|
name: string;
|
||||||
|
stock: number;
|
||||||
|
reject_stock: number;
|
||||||
|
retail_stock: number;
|
||||||
|
photo: string | null;
|
||||||
|
prices: Array<{ type: string; price: number }>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getKey(
|
||||||
|
type: 'create' | 'edit',
|
||||||
|
userId?: number,
|
||||||
|
productId?: number,
|
||||||
|
): string {
|
||||||
|
if (type === 'edit' && productId) {
|
||||||
|
return `${DRAFT_PREFIX}-edit-${userId ?? 'anon'}-${productId}`;
|
||||||
|
}
|
||||||
|
return `${DRAFT_PREFIX}-create-${userId ?? 'anon'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveProductDraft(
|
||||||
|
type: 'create' | 'edit',
|
||||||
|
data: ProductDraftData,
|
||||||
|
userId?: number,
|
||||||
|
productId?: number,
|
||||||
|
): boolean {
|
||||||
|
try {
|
||||||
|
const key = getKey(type, userId, productId);
|
||||||
|
localStorage.setItem(key, JSON.stringify(data));
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadProductDraft(
|
||||||
|
type: 'create' | 'edit',
|
||||||
|
userId?: number,
|
||||||
|
productId?: number,
|
||||||
|
): ProductDraftData | null {
|
||||||
|
try {
|
||||||
|
const key = getKey(type, userId, productId);
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
if (!raw) return null;
|
||||||
|
return JSON.parse(raw) as ProductDraftData;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearProductDraft(
|
||||||
|
type: 'create' | 'edit',
|
||||||
|
userId?: number,
|
||||||
|
productId?: number,
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
const key = getKey(type, userId, productId);
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,8 +8,11 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { loadProductDraft, clearProductDraft } from '@/lib/product-draft';
|
||||||
|
import { useProductDraftSave } from '@/hooks/use-product-draft';
|
||||||
|
import { getTemporaryUrl } from '@/lib/upload';
|
||||||
import { index as productIndex, store } from '@/routes/admin/master/products';
|
import { index as productIndex, store } from '@/routes/admin/master/products';
|
||||||
import { Form, Head } from '@inertiajs/react';
|
import { Form, Head, router, usePage } from '@inertiajs/react';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Copy,
|
Copy,
|
||||||
@ -18,7 +21,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
type Category = {
|
type Category = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -51,30 +54,72 @@ type VariantState = {
|
|||||||
reject_stock: number;
|
reject_stock: number;
|
||||||
retail_stock: number;
|
retail_stock: number;
|
||||||
photo: string | null;
|
photo: string | null;
|
||||||
|
photoUrl: string | null;
|
||||||
uploading: boolean;
|
uploading: boolean;
|
||||||
prices: Array<{ type: string; price: number }>;
|
prices: Array<{ type: string; price: number }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProductCreate({ categories }: Props) {
|
export default function ProductCreate({ categories }: Props) {
|
||||||
const [categoryIds, setCategoryIds] = useState<number[]>([]);
|
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
||||||
const [useSamePrice, setUseSamePrice] = useState(true);
|
const userId = auth.user?.id;
|
||||||
const [sharedPrices, setSharedPrices] =
|
|
||||||
useState<Array<{ type: string; price: number }>>(createEmptyPrices());
|
const draft = loadProductDraft('create', userId);
|
||||||
const [variants, setVariants] = useState<VariantState[]>([
|
|
||||||
{
|
const [productName, setProductName] = useState(draft?.productName ?? '');
|
||||||
name: '',
|
const [status, setStatus] = useState(draft?.status ?? 'active');
|
||||||
stock: 0,
|
const [description, setDescription] = useState(draft?.description ?? '');
|
||||||
reject_stock: 0,
|
const [categoryIds, setCategoryIds] = useState<number[]>(
|
||||||
retail_stock: 0,
|
draft?.categoryIds ?? [],
|
||||||
photo: null,
|
);
|
||||||
uploading: false,
|
const [useSamePrice, setUseSamePrice] = useState(
|
||||||
prices: createEmptyPrices(),
|
draft?.useSamePrice ?? true,
|
||||||
},
|
);
|
||||||
]);
|
const [sharedPrices, setSharedPrices] = useState<
|
||||||
|
Array<{ type: string; price: number }>
|
||||||
|
>(draft?.sharedPrices ?? createEmptyPrices());
|
||||||
|
const [variants, setVariants] = useState<VariantState[]>(() => {
|
||||||
|
if (draft?.variants && draft.variants.length > 0) {
|
||||||
|
return draft.variants.map((v) => ({
|
||||||
|
...v,
|
||||||
|
photoUrl: v.photo ? getTemporaryUrl(v.photo) : null,
|
||||||
|
uploading: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
stock: 0,
|
||||||
|
reject_stock: 0,
|
||||||
|
retail_stock: 0,
|
||||||
|
photo: null,
|
||||||
|
photoUrl: null,
|
||||||
|
uploading: false,
|
||||||
|
prices: createEmptyPrices(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
const variantsRef = useRef(variants);
|
const variantsRef = useRef(variants);
|
||||||
variantsRef.current = variants;
|
variantsRef.current = variants;
|
||||||
|
|
||||||
|
const draftData = {
|
||||||
|
productName,
|
||||||
|
status,
|
||||||
|
description,
|
||||||
|
categoryIds,
|
||||||
|
useSamePrice,
|
||||||
|
sharedPrices,
|
||||||
|
variants: variants.map(({ uploading, photoUrl, ...v }) => v),
|
||||||
|
};
|
||||||
|
|
||||||
|
useProductDraftSave('create', draftData, userId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return router.on('success', () => {
|
||||||
|
clearProductDraft('create', userId);
|
||||||
|
});
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
const addVariant = useCallback(() => {
|
const addVariant = useCallback(() => {
|
||||||
setVariants((prev) => [
|
setVariants((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@ -84,6 +129,7 @@ export default function ProductCreate({ categories }: Props) {
|
|||||||
reject_stock: 0,
|
reject_stock: 0,
|
||||||
retail_stock: 0,
|
retail_stock: 0,
|
||||||
photo: null,
|
photo: null,
|
||||||
|
photoUrl: null,
|
||||||
uploading: false,
|
uploading: false,
|
||||||
prices: createEmptyPrices(),
|
prices: createEmptyPrices(),
|
||||||
},
|
},
|
||||||
@ -185,6 +231,9 @@ export default function ProductCreate({ categories }: Props) {
|
|||||||
|
|
||||||
function getPayload() {
|
function getPayload() {
|
||||||
return {
|
return {
|
||||||
|
name: productName,
|
||||||
|
status,
|
||||||
|
description,
|
||||||
category_ids: categoryIds,
|
category_ids: categoryIds,
|
||||||
use_same_price: useSamePrice,
|
use_same_price: useSamePrice,
|
||||||
shared_prices: useSamePrice
|
shared_prices: useSamePrice
|
||||||
@ -251,6 +300,12 @@ export default function ProductCreate({ categories }: Props) {
|
|||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
|
value={productName}
|
||||||
|
onChange={(e) =>
|
||||||
|
setProductName(
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
placeholder="Masukkan nama produk"
|
placeholder="Masukkan nama produk"
|
||||||
/>
|
/>
|
||||||
<InputError message={errors.name} />
|
<InputError message={errors.name} />
|
||||||
@ -264,7 +319,8 @@ export default function ProductCreate({ categories }: Props) {
|
|||||||
</Label>
|
</Label>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
name="status"
|
name="status"
|
||||||
defaultValue="active"
|
value={status}
|
||||||
|
onValueChange={setStatus}
|
||||||
className="flex gap-4"
|
className="flex gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@ -365,6 +421,12 @@ export default function ProductCreate({ categories }: Props) {
|
|||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDescription(
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
placeholder="Masukkan deskripsi produk"
|
placeholder="Masukkan deskripsi produk"
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
@ -682,13 +744,27 @@ export default function ProductCreate({ categories }: Props) {
|
|||||||
value={
|
value={
|
||||||
variant.photo
|
variant.photo
|
||||||
}
|
}
|
||||||
onChange={(photo) =>
|
existingUrl={
|
||||||
|
variant.photoUrl
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
photo,
|
||||||
|
) => {
|
||||||
updateVariant(
|
updateVariant(
|
||||||
variantIndex,
|
variantIndex,
|
||||||
'photo',
|
'photo',
|
||||||
photo,
|
photo,
|
||||||
)
|
);
|
||||||
}
|
updateVariant(
|
||||||
|
variantIndex,
|
||||||
|
'photoUrl',
|
||||||
|
photo
|
||||||
|
? getTemporaryUrl(
|
||||||
|
photo,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}}
|
||||||
folder="product-variant"
|
folder="product-variant"
|
||||||
onUploadingChange={(
|
onUploadingChange={(
|
||||||
uploading,
|
uploading,
|
||||||
|
|||||||
@ -8,8 +8,11 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { loadProductDraft, clearProductDraft } from '@/lib/product-draft';
|
||||||
|
import { useProductDraftSave } from '@/hooks/use-product-draft';
|
||||||
|
import { getTemporaryUrl } from '@/lib/upload';
|
||||||
import { index as productIndex, update } from '@/routes/admin/master/products';
|
import { index as productIndex, update } from '@/routes/admin/master/products';
|
||||||
import { Form, Head } from '@inertiajs/react';
|
import { Form, Head, router, usePage } from '@inertiajs/react';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Copy,
|
Copy,
|
||||||
@ -18,7 +21,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
type Category = {
|
type Category = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -87,7 +90,20 @@ type VariantState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ProductEdit({ product, categories }: Props) {
|
export default function ProductEdit({ product, categories }: Props) {
|
||||||
const initialVariants: VariantState[] = product.product_variants.map(
|
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
||||||
|
const userId = auth.user?.id;
|
||||||
|
|
||||||
|
const draft = loadProductDraft('edit', userId, product.id);
|
||||||
|
|
||||||
|
const [productName, setProductName] = useState(
|
||||||
|
draft?.productName ?? product.name,
|
||||||
|
);
|
||||||
|
const [status, setStatus] = useState(draft?.status ?? product.status);
|
||||||
|
const [description, setDescription] = useState(
|
||||||
|
draft?.description ?? product.description ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const serverVariants: VariantState[] = product.product_variants.map(
|
||||||
(v) => ({
|
(v) => ({
|
||||||
id: v.id,
|
id: v.id,
|
||||||
name: v.name,
|
name: v.name,
|
||||||
@ -101,27 +117,50 @@ export default function ProductEdit({ product, categories }: Props) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const allSamePrice =
|
|
||||||
initialVariants.length > 1
|
|
||||||
? initialVariants.every((v) =>
|
|
||||||
arePricesEqual(v.prices, initialVariants[0].prices),
|
|
||||||
)
|
|
||||||
: true;
|
|
||||||
|
|
||||||
const [categoryIds, setCategoryIds] = useState<number[]>(
|
const [categoryIds, setCategoryIds] = useState<number[]>(
|
||||||
product.category_ids,
|
draft?.categoryIds ?? product.category_ids,
|
||||||
|
);
|
||||||
|
const [useSamePrice, setUseSamePrice] = useState(
|
||||||
|
draft?.useSamePrice ??
|
||||||
|
(serverVariants.length > 1
|
||||||
|
? serverVariants.every((v) =>
|
||||||
|
arePricesEqual(v.prices, serverVariants[0].prices),
|
||||||
|
)
|
||||||
|
: true),
|
||||||
);
|
);
|
||||||
const [useSamePrice, setUseSamePrice] = useState(allSamePrice);
|
|
||||||
const [sharedPrices, setSharedPrices] = useState<
|
const [sharedPrices, setSharedPrices] = useState<
|
||||||
Array<{ type: string; price: number }>
|
Array<{ type: string; price: number }>
|
||||||
>(
|
>(
|
||||||
initialVariants.length > 0
|
draft?.sharedPrices ??
|
||||||
? initialVariants[0].prices
|
(serverVariants.length > 0
|
||||||
: createEmptyPrices(),
|
? serverVariants[0].prices
|
||||||
|
: createEmptyPrices()),
|
||||||
);
|
);
|
||||||
const [variants, setVariants] = useState<VariantState[]>(
|
const [variants, setVariants] = useState<VariantState[]>(() => {
|
||||||
initialVariants.length > 0
|
if (draft?.variants && draft.variants.length > 0) {
|
||||||
? initialVariants
|
const serverVariantMap = new Map(
|
||||||
|
serverVariants.map((sv) => [sv.id, sv]),
|
||||||
|
);
|
||||||
|
return draft.variants.map((v) => {
|
||||||
|
const serverMatch =
|
||||||
|
v.id != null ? serverVariantMap.get(v.id) : undefined;
|
||||||
|
return {
|
||||||
|
id: v.id ?? null,
|
||||||
|
name: v.name,
|
||||||
|
stock: v.stock,
|
||||||
|
reject_stock: v.reject_stock,
|
||||||
|
retail_stock: v.retail_stock,
|
||||||
|
photo: v.photo,
|
||||||
|
photoUrl:
|
||||||
|
serverMatch?.photoUrl ??
|
||||||
|
(v.photo ? getTemporaryUrl(v.photo) : null),
|
||||||
|
uploading: false,
|
||||||
|
prices: v.prices,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return serverVariants.length > 0
|
||||||
|
? serverVariants
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: null,
|
id: null,
|
||||||
@ -134,12 +173,30 @@ export default function ProductEdit({ product, categories }: Props) {
|
|||||||
uploading: false,
|
uploading: false,
|
||||||
prices: createEmptyPrices(),
|
prices: createEmptyPrices(),
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
);
|
});
|
||||||
|
|
||||||
const variantsRef = useRef(variants);
|
const variantsRef = useRef(variants);
|
||||||
variantsRef.current = variants;
|
variantsRef.current = variants;
|
||||||
|
|
||||||
|
const draftData = {
|
||||||
|
productName,
|
||||||
|
status,
|
||||||
|
description,
|
||||||
|
categoryIds,
|
||||||
|
useSamePrice,
|
||||||
|
sharedPrices,
|
||||||
|
variants: variants.map(({ uploading, photoUrl, ...v }) => v),
|
||||||
|
};
|
||||||
|
|
||||||
|
useProductDraftSave('edit', draftData, userId, product.id);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return router.on('success', () => {
|
||||||
|
clearProductDraft('edit', userId, product.id);
|
||||||
|
});
|
||||||
|
}, [userId, product.id]);
|
||||||
|
|
||||||
const addVariant = useCallback(() => {
|
const addVariant = useCallback(() => {
|
||||||
setVariants((prev) => [
|
setVariants((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@ -252,6 +309,9 @@ export default function ProductEdit({ product, categories }: Props) {
|
|||||||
|
|
||||||
function getPayload() {
|
function getPayload() {
|
||||||
return {
|
return {
|
||||||
|
name: productName,
|
||||||
|
status,
|
||||||
|
description,
|
||||||
category_ids: categoryIds,
|
category_ids: categoryIds,
|
||||||
use_same_price: useSamePrice,
|
use_same_price: useSamePrice,
|
||||||
shared_prices: useSamePrice
|
shared_prices: useSamePrice
|
||||||
@ -320,7 +380,12 @@ export default function ProductEdit({ product, categories }: Props) {
|
|||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
defaultValue={product.name}
|
value={productName}
|
||||||
|
onChange={(e) =>
|
||||||
|
setProductName(
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
placeholder="Masukkan nama produk"
|
placeholder="Masukkan nama produk"
|
||||||
/>
|
/>
|
||||||
<InputError message={errors.name} />
|
<InputError message={errors.name} />
|
||||||
@ -334,7 +399,8 @@ export default function ProductEdit({ product, categories }: Props) {
|
|||||||
</Label>
|
</Label>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
name="status"
|
name="status"
|
||||||
defaultValue={product.status}
|
value={status}
|
||||||
|
onValueChange={setStatus}
|
||||||
className="flex gap-4"
|
className="flex gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@ -435,8 +501,11 @@ export default function ProductEdit({ product, categories }: Props) {
|
|||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
defaultValue={
|
value={description}
|
||||||
product.description ?? ''
|
onChange={(e) =>
|
||||||
|
setDescription(
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
placeholder="Masukkan deskripsi produk"
|
placeholder="Masukkan deskripsi produk"
|
||||||
rows={3}
|
rows={3}
|
||||||
@ -755,13 +824,24 @@ export default function ProductEdit({ product, categories }: Props) {
|
|||||||
value={
|
value={
|
||||||
variant.photo
|
variant.photo
|
||||||
}
|
}
|
||||||
onChange={(photo) =>
|
onChange={(
|
||||||
|
photo,
|
||||||
|
) => {
|
||||||
updateVariant(
|
updateVariant(
|
||||||
variantIndex,
|
variantIndex,
|
||||||
'photo',
|
'photo',
|
||||||
photo,
|
photo,
|
||||||
)
|
);
|
||||||
}
|
updateVariant(
|
||||||
|
variantIndex,
|
||||||
|
'photoUrl',
|
||||||
|
photo
|
||||||
|
? getTemporaryUrl(
|
||||||
|
photo,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}}
|
||||||
folder="product-variant"
|
folder="product-variant"
|
||||||
existingUrl={
|
existingUrl={
|
||||||
variant.photoUrl
|
variant.photoUrl
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user