changes shopping
This commit is contained in:
1 parent
62ab5b2608
commit
f4bc93bc8a
17 files changed
+2290
-1025
No files matched your search
@@ -0,0 +1,224 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useRef, useState, useEffect } from "react";
|
||||
import { Upload, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { resizeImageToDataURL } from "@/lib/image/resize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface ProcessedImage {
|
||||
id: string;
|
||||
originalName: string;
|
||||
previewUrl: string;
|
||||
dataUrl: string;
|
||||
mimeType: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface ItemImageCaptureProps {
|
||||
maxImages?: number;
|
||||
className?: string;
|
||||
onImagesChange?: (images: ProcessedImage[]) => void;
|
||||
}
|
||||
|
||||
export function ItemImageCapture({
|
||||
maxImages = 2,
|
||||
className,
|
||||
onImagesChange,
|
||||
}: ItemImageCaptureProps) {
|
||||
const generateId = useCallback(() => {
|
||||
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return `img-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
}, []);
|
||||
|
||||
const [images, setImages] = useState<ProcessedImage[]>([]);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const previewUrlsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const updateImages = useCallback(
|
||||
(next: ProcessedImage[]) => {
|
||||
setImages(next);
|
||||
onImagesChange?.(next);
|
||||
},
|
||||
[onImagesChange],
|
||||
);
|
||||
|
||||
const handleFiles = useCallback(
|
||||
async (files: FileList | File[]) => {
|
||||
const fileArray = Array.from(files).slice(0, maxImages - images.length);
|
||||
if (fileArray.length === 0) return;
|
||||
|
||||
const processed: ProcessedImage[] = [];
|
||||
|
||||
for (const file of fileArray) {
|
||||
if (!file.type.startsWith("image/")) continue;
|
||||
try {
|
||||
const { dataUrl, width, height } = await resizeImageToDataURL(file, {
|
||||
mimeType: file.type === "image/png" ? "image/png" : "image/jpeg",
|
||||
});
|
||||
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
previewUrlsRef.current.add(previewUrl);
|
||||
|
||||
processed.push({
|
||||
id: generateId(),
|
||||
originalName: file.name,
|
||||
previewUrl,
|
||||
dataUrl,
|
||||
mimeType: file.type === "image/png" ? "image/png" : "image/jpeg",
|
||||
width,
|
||||
height,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Falha ao processar imagem", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (processed.length > 0) {
|
||||
updateImages([...images, ...processed].slice(0, maxImages));
|
||||
}
|
||||
},
|
||||
[generateId, images, maxImages, updateImages],
|
||||
);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
async (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer.files?.length) {
|
||||
await handleFiles(event.dataTransfer.files);
|
||||
}
|
||||
},
|
||||
[handleFiles],
|
||||
);
|
||||
|
||||
const onPaste = useCallback(
|
||||
async (event: React.ClipboardEvent<HTMLDivElement>) => {
|
||||
const items = event.clipboardData?.items;
|
||||
if (!items) return;
|
||||
const files: File[] = [];
|
||||
for (const item of items) {
|
||||
if (item.type.startsWith("image/")) {
|
||||
const file = item.getAsFile();
|
||||
if (file) files.push(file);
|
||||
}
|
||||
}
|
||||
if (files.length) {
|
||||
await handleFiles(files);
|
||||
}
|
||||
},
|
||||
[handleFiles],
|
||||
);
|
||||
|
||||
const removeImage = useCallback(
|
||||
(id: string) => {
|
||||
const nextImage = images.find((image) => image.id === id);
|
||||
if (nextImage) {
|
||||
URL.revokeObjectURL(nextImage.previewUrl);
|
||||
previewUrlsRef.current.delete(nextImage.previewUrl);
|
||||
}
|
||||
const next = images.filter((image) => image.id !== id);
|
||||
updateImages(next);
|
||||
},
|
||||
[images, updateImages],
|
||||
);
|
||||
|
||||
const canAddMore = images.length < maxImages;
|
||||
|
||||
const dropLabel = useMemo(() => {
|
||||
if (images.length === 0) {
|
||||
return "Arraste, cole (Ctrl+V) ou escolha imagens";
|
||||
}
|
||||
if (canAddMore) {
|
||||
return "Adicione mais imagens ou cole via Ctrl+V";
|
||||
}
|
||||
return "Limite máximo de imagens atingido";
|
||||
}, [images.length, canAddMore]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
previewUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
|
||||
previewUrlsRef.current.clear();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border-2 border-dashed border-muted-foreground/40 bg-muted/40 p-6 text-center transition-colors",
|
||||
canAddMore ? "hover:border-primary" : "opacity-70",
|
||||
)}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onPaste={onPaste}
|
||||
role="presentation"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Upload className="h-8 w-8 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">{dropLabel}</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
disabled={!canAddMore}
|
||||
>
|
||||
Selecionar arquivo
|
||||
</Button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={async (event) => {
|
||||
if (event.target.files?.length) {
|
||||
await handleFiles(event.target.files);
|
||||
event.target.value = "";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{images.length > 0 && (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{images.map((image) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="relative overflow-hidden rounded-md border bg-background shadow-sm"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2 rounded-full bg-background/80 p-1 text-muted-foreground transition hover:bg-destructive hover:text-destructive-foreground"
|
||||
onClick={() => removeImage(image.id)}
|
||||
aria-label="Remover imagem"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
<img
|
||||
src={image.previewUrl}
|
||||
alt={image.originalName}
|
||||
className="h-48 w-full object-cover"
|
||||
/>
|
||||
<div className="border-t p-3 text-xs text-muted-foreground">
|
||||
<p className="font-medium">{image.originalName}</p>
|
||||
<p>
|
||||
{image.width}×{image.height}px ·{" "}
|
||||
{Math.round((image.dataUrl.length * 3) / 4 / 1024)}KB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user