feat: aprimora upload/anexos e regras de atendimento no portal
This commit is contained in:
parent
7e8023ed87
commit
c90e99820f
8 changed files with 218 additions and 74 deletions
|
|
@ -5,7 +5,7 @@ import { api } from "@/convex/_generated/api";
|
|||
import { useCallback, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Upload } from "lucide-react";
|
||||
import { Upload, Check, X, AlertCircle } from "lucide-react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
type Uploaded = { storageId: string; name: string; size?: number; type?: string; previewUrl?: string };
|
||||
|
|
@ -26,7 +26,9 @@ export function Dropzone({
|
|||
const generateUrl = useAction(api.files.generateUploadUrl);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [drag, setDrag] = useState(false);
|
||||
const [items, setItems] = useState<Array<{ id: string; name: string; progress: number; status: "idle" | "uploading" | "done" | "error" }>>([]);
|
||||
const [items, setItems] = useState<
|
||||
Array<{ id: string; name: string; progress: number; status: "uploading" | "done" | "error" }>
|
||||
>([]);
|
||||
|
||||
const startUpload = useCallback(async (files: FileList | File[]) => {
|
||||
const list = Array.from(files).slice(0, maxFiles);
|
||||
|
|
@ -55,29 +57,25 @@ export function Dropzone({
|
|||
const res = JSON.parse(xhr.responseText);
|
||||
if (res?.storageId) {
|
||||
uploaded.push({ storageId: res.storageId, name: file.name, size: file.size, type: file.type, previewUrl: localPreview });
|
||||
setItems((prev) => prev.map((it) => (it.id === id ? { ...it, progress: 100, status: "done" } : it)));
|
||||
setTimeout(() => {
|
||||
setItems((prev) => prev.filter((it) => it.id !== id));
|
||||
}, 600);
|
||||
setItems((prev) =>
|
||||
prev.map((it) => (it.id === id ? { ...it, progress: 100, status: "done" } : it))
|
||||
);
|
||||
} else {
|
||||
setItems((prev) => prev.map((it) => (it.id === id ? { ...it, status: "error" } : it)));
|
||||
setTimeout(() => {
|
||||
setItems((prev) => prev.filter((it) => it.id !== id));
|
||||
}, 1200);
|
||||
setItems((prev) =>
|
||||
prev.map((it) => (it.id === id ? { ...it, status: "error" } : it))
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
setItems((prev) => prev.map((it) => (it.id === id ? { ...it, status: "error" } : it)));
|
||||
setTimeout(() => {
|
||||
setItems((prev) => prev.filter((it) => it.id !== id));
|
||||
}, 1200);
|
||||
setItems((prev) =>
|
||||
prev.map((it) => (it.id === id ? { ...it, status: "error" } : it))
|
||||
);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
setItems((prev) => prev.map((it) => (it.id === id ? { ...it, status: "error" } : it)));
|
||||
setTimeout(() => {
|
||||
setItems((prev) => prev.filter((it) => it.id !== id));
|
||||
}, 1200);
|
||||
setItems((prev) =>
|
||||
prev.map((it) => (it.id === id ? { ...it, status: "error" } : it))
|
||||
);
|
||||
resolve();
|
||||
};
|
||||
xhr.send(file);
|
||||
|
|
@ -130,15 +128,51 @@ export function Dropzone({
|
|||
</div>
|
||||
{items.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{items.map((it) => (
|
||||
<div key={it.id} className="flex items-center justify-between gap-3 rounded-md border p-2 text-sm">
|
||||
<span className="truncate">{it.name}</span>
|
||||
<div className="flex min-w-[140px] items-center gap-2">
|
||||
<Progress value={it.progress} className="h-1.5 w-24" />
|
||||
<span className="w-10 text-right text-xs text-neutral-500">{it.progress}%</span>
|
||||
{items.map((it) => {
|
||||
const isUploading = it.status === "uploading";
|
||||
const isDone = it.status === "done";
|
||||
const isError = it.status === "error";
|
||||
return (
|
||||
<div key={it.id} className="flex items-center justify-between gap-3 rounded-md border p-2 text-sm">
|
||||
<div className="flex flex-1 items-center gap-3 overflow-hidden">
|
||||
<span className="truncate">{it.name}</span>
|
||||
{isError ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs text-rose-600">
|
||||
<AlertCircle className="size-3.5" /> Falhou
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isUploading ? (
|
||||
<>
|
||||
<Progress value={it.progress} className="h-1.5 w-24" />
|
||||
<span className="w-10 text-right text-xs text-neutral-500">{it.progress}%</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
isDone ? "bg-emerald-50 text-emerald-700" : "bg-rose-50 text-rose-700"
|
||||
)}
|
||||
>
|
||||
{isDone ? <Check className="size-3.5" /> : <AlertCircle className="size-3.5" />}
|
||||
{isDone ? "Pronto" : "Erro"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex size-7 items-center justify-center rounded-full border border-slate-200 bg-white text-neutral-600 transition hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-300"
|
||||
aria-label="Remover item"
|
||||
onClick={() => setItems((prev) => prev.filter((item) => item.id !== it.id))}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue