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
+33 -19

No files matched your search

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