Fix edit product images and warranty toggle
This commit is contained in:
@ -7,6 +7,7 @@ import { useLanguage } from "@/lib/i18n-context";
|
|||||||
import { getBackendErrorMessage } from "@/lib/error-message";
|
import { getBackendErrorMessage } from "@/lib/error-message";
|
||||||
import { assertValidAddProductPayload } from "@/lib/product-request-validation";
|
import { assertValidAddProductPayload } from "@/lib/product-request-validation";
|
||||||
import { assertUploadFileSize } from "@/lib/upload-limits";
|
import { assertUploadFileSize } from "@/lib/upload-limits";
|
||||||
|
import { resolveBackendImageUrlFromValue } from "@/lib/image-url";
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -108,6 +109,7 @@ interface EditState {
|
|||||||
fileId: string;
|
fileId: string;
|
||||||
};
|
};
|
||||||
warrantyInformation: {
|
warrantyInformation: {
|
||||||
|
enabled: boolean;
|
||||||
type: string;
|
type: string;
|
||||||
duration: string;
|
duration: string;
|
||||||
durationType: string;
|
durationType: string;
|
||||||
@ -351,6 +353,9 @@ function newModel(index: number): EditModel {
|
|||||||
|
|
||||||
function apiToEditState(data: ApiProduct): EditState {
|
function apiToEditState(data: ApiProduct): EditState {
|
||||||
const rawModels = Array.isArray(data?.productModels) ? data.productModels : [];
|
const rawModels = Array.isArray(data?.productModels) ? data.productModels : [];
|
||||||
|
const warrantyType = toStr(data?.warrantyInformation?.type);
|
||||||
|
const warrantyDuration = toStr(data?.warrantyInformation?.duration);
|
||||||
|
const hasWarranty = Boolean(warrantyType.trim() || warrantyDuration.trim());
|
||||||
|
|
||||||
const models: EditModel[] = rawModels.length > 0
|
const models: EditModel[] = rawModels.length > 0
|
||||||
? rawModels.map((m: ApiModel, i: number) => ({
|
? rawModels.map((m: ApiModel, i: number) => ({
|
||||||
@ -452,8 +457,9 @@ function apiToEditState(data: ApiProduct): EditState {
|
|||||||
fileId: toStr(data?.complianceInformation?.fileId ?? data?.complianceInformation?.file),
|
fileId: toStr(data?.complianceInformation?.fileId ?? data?.complianceInformation?.file),
|
||||||
},
|
},
|
||||||
warrantyInformation: {
|
warrantyInformation: {
|
||||||
type: toStr(data?.warrantyInformation?.type),
|
enabled: hasWarranty,
|
||||||
duration: toStr(data?.warrantyInformation?.duration),
|
type: warrantyType,
|
||||||
|
duration: warrantyDuration,
|
||||||
durationType: toStr(data?.warrantyInformation?.durationType) || "MONTH",
|
durationType: toStr(data?.warrantyInformation?.durationType) || "MONTH",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -491,10 +497,24 @@ function ImageSlotUpload({
|
|||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [previewUrl, setPreviewUrl] = useState("");
|
||||||
|
const [displayUrl, setDisplayUrl] = useState("");
|
||||||
|
const fallbackUrl = fileId && !fileId.startsWith("http")
|
||||||
|
? resolveBackendImageUrlFromValue(fileId, false)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisplayUrl(previewUrl || resolveBackendImageUrlFromValue(fileId, true));
|
||||||
|
}, [fileId, previewUrl]);
|
||||||
|
|
||||||
async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
const objectUrl = URL.createObjectURL(file);
|
||||||
|
setPreviewUrl((current) => {
|
||||||
|
if (current) URL.revokeObjectURL(current);
|
||||||
|
return objectUrl;
|
||||||
|
});
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@ -502,6 +522,8 @@ function ImageSlotUpload({
|
|||||||
onUploaded(fileId);
|
onUploaded(fileId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Upload gagal");
|
setError(err instanceof Error ? err.message : "Upload gagal");
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
setPreviewUrl("");
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
if (inputRef.current) inputRef.current.value = "";
|
if (inputRef.current) inputRef.current.value = "";
|
||||||
@ -510,10 +532,25 @@ function ImageSlotUpload({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 p-3 rounded-xl bg-surface-container-low border border-outline-variant/10">
|
<div className="flex items-center gap-3 p-3 rounded-xl bg-surface-container-low border border-outline-variant/10">
|
||||||
<div className="w-11 h-11 rounded-lg bg-surface-container flex items-center justify-center flex-shrink-0">
|
<div className="w-11 h-11 rounded-lg bg-surface-container flex items-center justify-center flex-shrink-0 overflow-hidden">
|
||||||
|
{displayUrl ? (
|
||||||
|
<img
|
||||||
|
src={displayUrl}
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onError={() => {
|
||||||
|
if (fallbackUrl && displayUrl !== fallbackUrl) {
|
||||||
|
setDisplayUrl(fallbackUrl);
|
||||||
|
} else {
|
||||||
|
setDisplayUrl("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<span className="material-symbols-outlined text-xl" style={{ fontVariationSettings: fileId ? "'FILL' 1" : "'FILL' 0", color: fileId ? "var(--md-sys-color-primary)" : undefined }}>
|
<span className="material-symbols-outlined text-xl" style={{ fontVariationSettings: fileId ? "'FILL' 1" : "'FILL' 0", color: fileId ? "var(--md-sys-color-primary)" : undefined }}>
|
||||||
{fileId ? "image" : "add_photo_alternate"}
|
{fileId ? "image" : "add_photo_alternate"}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-[10px] font-black uppercase tracking-widest text-outline">{label}</p>
|
<p className="text-[10px] font-black uppercase tracking-widest text-outline">{label}</p>
|
||||||
@ -542,12 +579,23 @@ function ModelImageUpload({ value, onUploaded }: { value: string; onUploaded: (i
|
|||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [previewUrl, setPreviewUrl] = useState("");
|
const [previewUrl, setPreviewUrl] = useState("");
|
||||||
|
const [displayUrl, setDisplayUrl] = useState("");
|
||||||
|
const fallbackUrl = value && !value.startsWith("http")
|
||||||
|
? resolveBackendImageUrlFromValue(value, false)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisplayUrl(previewUrl || resolveBackendImageUrlFromValue(value, true));
|
||||||
|
}, [previewUrl, value]);
|
||||||
|
|
||||||
async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const objectUrl = URL.createObjectURL(file);
|
const objectUrl = URL.createObjectURL(file);
|
||||||
setPreviewUrl(objectUrl);
|
setPreviewUrl((current) => {
|
||||||
|
if (current) URL.revokeObjectURL(current);
|
||||||
|
return objectUrl;
|
||||||
|
});
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@ -555,6 +603,7 @@ function ModelImageUpload({ value, onUploaded }: { value: string; onUploaded: (i
|
|||||||
onUploaded(fileId);
|
onUploaded(fileId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Upload gagal");
|
setError(err instanceof Error ? err.message : "Upload gagal");
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
setPreviewUrl("");
|
setPreviewUrl("");
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
@ -572,8 +621,19 @@ function ModelImageUpload({ value, onUploaded }: { value: string; onUploaded: (i
|
|||||||
{hasImage ? (
|
{hasImage ? (
|
||||||
<>
|
<>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
{previewUrl ? (
|
{displayUrl ? (
|
||||||
<img src={previewUrl} alt="Model preview" className="w-full h-full object-cover" />
|
<img
|
||||||
|
src={displayUrl}
|
||||||
|
alt="Model preview"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onError={() => {
|
||||||
|
if (fallbackUrl && displayUrl !== fallbackUrl) {
|
||||||
|
setDisplayUrl(fallbackUrl);
|
||||||
|
} else {
|
||||||
|
setDisplayUrl("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<span className="material-symbols-outlined text-4xl text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>image</span>
|
<span className="material-symbols-outlined text-4xl text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>image</span>
|
||||||
@ -1343,7 +1403,17 @@ function EditProductPageInner() {
|
|||||||
productInformations: form.productInformations.filter((i) => i.paramName && i.paramValue),
|
productInformations: form.productInformations.filter((i) => i.paramName && i.paramValue),
|
||||||
categoryInformations: form.categoryInformations.filter((i) => i.paramName && i.paramValue),
|
categoryInformations: form.categoryInformations.filter((i) => i.paramName && i.paramValue),
|
||||||
complianceInformation: { ...form.complianceInformation },
|
complianceInformation: { ...form.complianceInformation },
|
||||||
warrantyInformation: { ...form.warrantyInformation, duration: toNum(form.warrantyInformation.duration) },
|
warrantyInformation: form.warrantyInformation.enabled
|
||||||
|
? {
|
||||||
|
type: form.warrantyInformation.type,
|
||||||
|
duration: toNum(form.warrantyInformation.duration),
|
||||||
|
durationType: form.warrantyInformation.durationType,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: null,
|
||||||
|
duration: null,
|
||||||
|
durationType: "MONTH",
|
||||||
|
},
|
||||||
state: resolvedState,
|
state: resolvedState,
|
||||||
};
|
};
|
||||||
return base;
|
return base;
|
||||||
@ -1844,23 +1914,44 @@ function EditProductPageInner() {
|
|||||||
|
|
||||||
{/* Warranty */}
|
{/* Warranty */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<h3 className="text-sm font-black text-on-surface">{e.warranty}</h3>
|
<h3 className="text-sm font-black text-on-surface">{e.warranty}</h3>
|
||||||
|
<label className="inline-flex items-center gap-3 text-xs font-black uppercase tracking-[0.16em] text-on-surface-variant">
|
||||||
|
<span>{form.warrantyInformation.enabled ? "On" : "Off"}</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={form.warrantyInformation.enabled}
|
||||||
|
onChange={(ev) =>
|
||||||
|
update({
|
||||||
|
warrantyInformation: {
|
||||||
|
...form.warrantyInformation,
|
||||||
|
enabled: ev.target.checked,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="peer sr-only"
|
||||||
|
/>
|
||||||
|
<span className="relative h-7 w-12 rounded-full bg-surface-container-high transition-colors peer-checked:bg-primary after:absolute after:left-1 after:top-1 after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm after:transition-transform peer-checked:after:translate-x-5" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={form.warrantyInformation.enabled ? "space-y-4" : "space-y-4 opacity-50"}>
|
||||||
<input value={form.warrantyInformation.type}
|
<input value={form.warrantyInformation.type}
|
||||||
onChange={(ev) => update({ warrantyInformation: { ...form.warrantyInformation, type: ev.target.value } })}
|
onChange={(ev) => update({ warrantyInformation: { ...form.warrantyInformation, type: ev.target.value } })}
|
||||||
placeholder={e.warrantyType} className={inputCls} />
|
placeholder={e.warrantyType} disabled={!form.warrantyInformation.enabled} className={inputCls} />
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<input value={form.warrantyInformation.duration}
|
<input value={form.warrantyInformation.duration}
|
||||||
onChange={(ev) => update({ warrantyInformation: { ...form.warrantyInformation, duration: ev.target.value } })}
|
onChange={(ev) => update({ warrantyInformation: { ...form.warrantyInformation, duration: ev.target.value } })}
|
||||||
placeholder={e.warrantyDuration} type="number" min="0" className={inputCls} />
|
placeholder={e.warrantyDuration} type="number" min="0" disabled={!form.warrantyInformation.enabled} className={inputCls} />
|
||||||
<select value={form.warrantyInformation.durationType}
|
<select value={form.warrantyInformation.durationType}
|
||||||
onChange={(ev) => update({ warrantyInformation: { ...form.warrantyInformation, durationType: ev.target.value } })}
|
onChange={(ev) => update({ warrantyInformation: { ...form.warrantyInformation, durationType: ev.target.value } })}
|
||||||
className={inputCls}>
|
disabled={!form.warrantyInformation.enabled} className={inputCls}>
|
||||||
<option value="DAY">DAY</option>
|
<option value="DAY">DAY</option>
|
||||||
<option value="MONTH">MONTH</option>
|
<option value="MONTH">MONTH</option>
|
||||||
<option value="YEAR">YEAR</option>
|
<option value="YEAR">YEAR</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-surface-container" />
|
<div className="border-t border-surface-container" />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user