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:
1 parent
6fbc1222b4
commit
bc9a43d6de
6 files changed
+33
-19
No files matched your search
+1
-1
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
setUrl("");
|
try {
|
||||||
|
await onDownload(url);
|
||||||
|
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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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%;
|
||||||
|
|||||||
Reference in new issue
Block a user