feat(web): exibe erros ao enfileirar e melhora acessibilidade e contraste

- Mostra no formulário o erro devolvido pela API ao enfileirar (antes só ia ao console) e desabilita o botão durante o envio.
- Adiciona aria-label no campo de URL, role=alert no erro e role=progressbar na barra.
- Clareia a cor destructive (contraste do texto de erro sobre o fundo escuro ~2:1 para ~4,5:1).
- Corrige o título da página (era 'web').
This commit is contained in:
evandro committed 2026-10-10 03:56:37 -03:00
1 parent 6fbc1222b4
commit bc9a43d6de
6 files changed
+32 -18

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>web</title> <title>Ephemeral Downloader</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1 -9
View File
@@ -35,14 +35,6 @@ function DownloadList() {
function Main() { function Main() {
const { queueDownload } = useSession(); const { queueDownload } = useSession();
const handleDownload = async (url: string) => {
try {
await queueDownload(url);
} catch (error) {
console.error("Failed to queue", error);
}
};
return ( return (
<Layout> <Layout>
<div className="grid gap-8 lg:grid-cols-12"> <div className="grid gap-8 lg:grid-cols-12">
@@ -56,7 +48,7 @@ function Main() {
Paste a link to start. Files are downloaded to the server first. Paste a link to start. Files are downloaded to the server first.
</p> </p>
</div> </div>
<DownloadForm onDownload={handleDownload} /> <DownloadForm onDownload={queueDownload} />
</section> </section>
<Alert variant="destructive"> <Alert variant="destructive">
+20 -5
View File
@@ -9,14 +9,15 @@ const formSchema = z.object({
}); });
interface DownloadFormProps { interface DownloadFormProps {
onDownload: (url: string) => void; onDownload: (url: string) => Promise<void>;
} }
export function DownloadForm({ onDownload }: DownloadFormProps) { export function DownloadForm({ onDownload }: DownloadFormProps) {
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(null); setError(null);
@@ -27,8 +28,15 @@ export function DownloadForm({ onDownload }: DownloadFormProps) {
return; return;
} }
onDownload(url); setSubmitting(true);
try {
await onDownload(url);
setUrl(""); setUrl("");
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to queue download");
} finally {
setSubmitting(false);
}
}; };
return ( return (
@@ -37,14 +45,21 @@ export function DownloadForm({ onDownload }: DownloadFormProps) {
<div className="flex-1 space-y-1"> <div className="flex-1 space-y-1">
<Input <Input
type="url" type="url"
aria-label="File URL"
aria-invalid={error ? true : undefined}
aria-describedby={error ? "url-error" : undefined}
placeholder="https://example.com/file.zip" placeholder="https://example.com/file.zip"
value={url} value={url}
onChange={(e) => setUrl(e.target.value)} onChange={(e) => setUrl(e.target.value)}
className={error ? "border-destructive focus-visible:ring-destructive" : ""} className={error ? "border-destructive focus-visible:ring-destructive" : ""}
/> />
{error && <p className="text-xs text-destructive">{error}</p>} {error && (
<p id="url-error" role="alert" className="text-xs text-destructive">
{error}
</p>
)}
</div> </div>
<Button type="submit"> <Button type="submit" disabled={submitting}>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
Download Download
</Button> </Button>
+4
View File
@@ -14,6 +14,10 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
return ( return (
<div <div
ref={ref} ref={ref}
role="progressbar"
aria-valuemin={0}
aria-valuemax={max}
aria-valuenow={percent}
className={cn( className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-secondary", "relative h-2 w-full overflow-hidden rounded-full bg-secondary",
className className
+5 -2
View File
@@ -90,7 +90,7 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
}, [sessionId]); }, [sessionId]);
const queueDownload = async (url: string) => { const queueDownload = async (url: string) => {
if (!sessionId) return; if (!sessionId) throw new Error("Session not ready, try again in a moment");
try { try {
const res = await fetch(`${API_URL}/api/queue`, { const res = await fetch(`${API_URL}/api/queue`, {
method: "POST", method: "POST",
@@ -101,7 +101,10 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
body: JSON.stringify({ url }), body: JSON.stringify({ url }),
}); });
if (!res.ok) throw new Error("Failed to queue download"); if (!res.ok) {
const body = (await res.json().catch(() => null)) as { error?: string } | null;
throw new Error(body?.error || "Failed to queue download");
}
await refreshTasks(); // Immediate refresh await refreshTasks(); // Immediate refresh
+1 -1
View File
@@ -45,7 +45,7 @@
--accent: 217.2 32.6% 17.5%; --accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%; --accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 72% 54%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%; --border: 217.2 32.6% 17.5%;