wrap inputs in real forms, so enter submits instead of doing nothing

24 panel and screen files had their fields sitting in a <div> instead of a
<form> — typing a value and pressing Enter did nothing, and the button was
the only way in. Fixed the sites that are a real form, a single field with
an obvious action, or a multi-field editor; left search/filter boxes and a
few genuinely ambiguous sites alone (see TODO.md for the full breakdown).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 10:51:58 +00:00
co-authored by Claude Opus 5
parent 319afe2c5d
commit 994de45296
24 changed files with 510 additions and 408 deletions
@@ -1,3 +1,4 @@
import type { FormEvent } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router';
import { useQuery, useQueryClient } from '@tanstack/react-query';
@@ -344,7 +345,8 @@ export const CapabilityList = ({
queryFn: () => client.get<CapabilitySummary[]>(endpoint),
});
const handleCreate = async () => {
const handleCreate = async (ev?: FormEvent) => {
ev?.preventDefault();
const name = newName.trim();
if (!name) return;
try {
@@ -382,16 +384,15 @@ export const CapabilityList = ({
)}
</div>
{creating && (
<div className="shrink-0 px-3 py-2 border-b border-duck-dark/10 bg-duck-teal/5 flex items-center gap-1.5">
<form
onSubmit={handleCreate}
className="shrink-0 px-3 py-2 border-b border-duck-dark/10 bg-duck-teal/5 flex items-center gap-1.5"
>
<input
ref={newNameRef}
value={newName}
onChange={(ev) => setNewName(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
handleCreate();
}
if (ev.key === 'Escape') {
setCreating(false);
setNewName('');
@@ -401,13 +402,14 @@ export const CapabilityList = ({
className="flex-1 min-w-0 rounded border border-duck-dark/20 bg-background px-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
/>
<button
onClick={handleCreate}
type="submit"
disabled={!newName.trim()}
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors disabled:opacity-30"
>
<Check className="h-3.5 w-3.5 text-duck-teal" />
</button>
<button
type="button"
onClick={() => {
setCreating(false);
setNewName('');
@@ -416,7 +418,7 @@ export const CapabilityList = ({
>
<X className="h-3.5 w-3.5 text-duck-dark/50" />
</button>
</div>
</form>
)}
{externalSearch === undefined && (
<div className="shrink-0 px-3 py-2 border-b border-duck-dark/10">
@@ -1,3 +1,4 @@
import type { FormEvent } from 'react';
import { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -61,7 +62,8 @@ export const ApiKeys = () => {
const refresh = () => queryClient.invalidateQueries({ queryKey: API_KEYS_QUERY_KEY });
const create = async () => {
const create = async (ev?: FormEvent) => {
ev?.preventDefault();
const trimmed = name.trim();
if (!trimmed || isCreating) return;
setIsCreating(true);
@@ -130,22 +132,21 @@ export const ApiKeys = () => {
</div>
)}
<div className="flex items-end gap-2">
<form onSubmit={create} className="flex items-end gap-2">
<Label className="grid flex-1 gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">New key</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20"
value={name}
onChange={(ev) => setName(ev.target.value)}
onKeyDown={(ev) => ev.key === 'Enter' && create()}
placeholder="iPhone, Music app, laptop CLI…"
/>
</Label>
<Button onClick={create} disabled={!name.trim() || isCreating} className="h-11">
<Button type="submit" disabled={!name.trim() || isCreating} className="h-11">
<Plus className="mr-1.5 h-4 w-4" />
Create
</Button>
</div>
</form>
{keys.length === 0 ? (
<div className="rounded-lg border border-dashed border-duck-dark/15 dark:border-foreground/15 p-6 text-center text-sm text-duck-dark/40 dark:text-foreground/40">
@@ -1,3 +1,4 @@
import type { FormEvent } from 'react';
import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { Input } from '@/components/ui/input';
@@ -31,7 +32,8 @@ export const ApifyConfig = () => {
.finally(() => setIsLoading(false));
}, []);
const handleSave = async () => {
const handleSave = async (ev?: FormEvent) => {
ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -60,7 +62,7 @@ export const ApifyConfig = () => {
</div>
)}
<div className="grid gap-5">
<form onSubmit={handleSave} className="grid gap-5">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">API Token</span>
<Input
@@ -84,14 +86,13 @@ export const ApifyConfig = () => {
</Label>
<Button
type="button"
onClick={handleSave}
type="submit"
disabled={isSaving || !apiToken.trim()}
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
{isSaving ? 'Saving...' : 'Save'}
</Button>
</div>
</form>
</div>
);
};
@@ -1,3 +1,4 @@
import type { FormEvent } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { toast } from 'sonner';
import { Copy, Trash2, Ban, Plus, Smartphone } from 'lucide-react';
@@ -59,7 +60,8 @@ export const DavAppPasswords = () => {
useEffect(load, [load]);
const create = async () => {
const create = async (ev?: FormEvent) => {
ev?.preventDefault();
const trimmed = label.trim();
if (!trimmed || isCreating) return;
setIsCreating(true);
@@ -142,22 +144,21 @@ export const DavAppPasswords = () => {
</div>
)}
<div className="flex items-end gap-2">
<form onSubmit={create} className="flex items-end gap-2">
<Label className="grid flex-1 gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">New device</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20"
value={label}
onChange={(ev) => setLabel(ev.target.value)}
onKeyDown={(ev) => ev.key === 'Enter' && create()}
placeholder="Pixel 9, iPad, Thunderbird…"
/>
</Label>
<Button onClick={create} disabled={!label.trim() || isCreating} className="h-11">
<Button type="submit" disabled={!label.trim() || isCreating} className="h-11">
<Plus className="mr-1.5 h-4 w-4" />
Create
</Button>
</div>
</form>
{passwords.length === 0 ? (
<div className="rounded-lg border border-dashed border-duck-dark/15 dark:border-foreground/15 p-6 text-center text-sm text-duck-dark/40 dark:text-foreground/40">
@@ -1,3 +1,4 @@
import type { FormEvent } from 'react';
import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { ChevronDown } from 'lucide-react';
@@ -266,7 +267,8 @@ export const GoogleOAuthConfig = () => {
if (!isLoading && clientId && clientSecret) verify();
}, [isLoading]);
const handleSave = async () => {
const handleSave = async (ev?: FormEvent) => {
ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -290,7 +292,7 @@ export const GoogleOAuthConfig = () => {
<SetupGuide redirectUri={redirectUri} />
<div className="border-t border-duck-dark/10 dark:border-foreground/10 pt-5 grid gap-5">
<form onSubmit={handleSave} className="border-t border-duck-dark/10 dark:border-foreground/10 pt-5 grid gap-5">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Client ID</span>
<Input
@@ -327,14 +329,13 @@ export const GoogleOAuthConfig = () => {
</Label>
<Button
type="button"
onClick={handleSave}
type="submit"
disabled={isSaving || !clientId.trim() || !clientSecret.trim()}
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
{isSaving ? 'Saving...' : 'Save'}
</Button>
</div>
</form>
</div>
);
};
@@ -226,6 +226,23 @@ export const AIHarnessesSection = () => {
}
};
const submitLocalUrl = (ev?: React.FormEvent) => {
ev?.preventDefault();
if (probeState.step === 'url' && localUrl.trim()) handleProbe();
};
const submitAuth = (ev?: React.FormEvent) => {
ev?.preventDefault();
if (probeState.step !== 'auth') return;
const ready = probeState.probe.authType === 'basic' ? authUsername && authPassword : authApiKey;
if (ready) handleAuthSubmit(probeState.probe);
};
const submitApiKey = (ev: React.FormEvent, providerId: string) => {
ev.preventDefault();
if (keyInputs[providerId]) saveApiKey(providerId);
};
const removeLocalProvider = async (id: string) => {
await client.delete(`/server-settings/chat-providers/local-providers/${id}`);
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'] });
@@ -291,57 +308,60 @@ export const AIHarnessesSection = () => {
{addingLocal ? (
<div className="flex flex-col gap-2 max-w-sm">
{/* Step 1: Name + URL inputs stacked */}
<Input
type="text"
className="h-8 text-xs"
placeholder="Name (e.g. My Ollama)"
value={localName}
onChange={(ev) => setLocalName(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Escape') resetLocalForm();
}}
disabled={probeState.step !== 'url'}
autoFocus
/>
<Input
type="url"
className="h-8 text-xs"
placeholder="http://localhost:11434"
value={localUrl}
onChange={(ev) => setLocalUrl(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Escape') resetLocalForm();
if (ev.key === 'Enter' && localUrl.trim() && probeState.step === 'url') handleProbe();
}}
disabled={probeState.step !== 'url'}
/>
<div className="flex items-center gap-2">
{probeState.step === 'url' && (
<Button
<form onSubmit={submitLocalUrl} className="contents">
<Input
type="text"
className="h-8 text-xs"
placeholder="Name (e.g. My Ollama)"
value={localName}
onChange={(ev) => setLocalName(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Escape') resetLocalForm();
}}
disabled={probeState.step !== 'url'}
autoFocus
/>
<Input
type="url"
className="h-8 text-xs"
placeholder="http://localhost:11434"
value={localUrl}
onChange={(ev) => setLocalUrl(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Escape') resetLocalForm();
}}
disabled={probeState.step !== 'url'}
/>
<div className="flex items-center gap-2">
{probeState.step === 'url' && (
<Button
type="submit"
size="sm"
className="h-7 text-xs bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40"
disabled={!localUrl.trim()}
>
Connect
</Button>
)}
{(probeState.step === 'probing' || probeState.step === 'saving') && (
<Loader2 className="h-4 w-4 animate-spin text-duck-teal shrink-0" />
)}
<button
type="button"
size="sm"
className="h-7 text-xs bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40"
disabled={!localUrl.trim()}
onClick={() => handleProbe()}
onClick={resetLocalForm}
className="text-duck-dark/40 hover:text-duck-dark/70 dark:text-foreground/40 dark:hover:text-foreground/70 cursor-pointer transition-colors text-xs"
>
Connect
</Button>
)}
{(probeState.step === 'probing' || probeState.step === 'saving') && (
<Loader2 className="h-4 w-4 animate-spin text-duck-teal shrink-0" />
)}
<button
type="button"
onClick={resetLocalForm}
className="text-duck-dark/40 hover:text-duck-dark/70 dark:text-foreground/40 dark:hover:text-foreground/70 cursor-pointer transition-colors text-xs"
>
Cancel
</button>
</div>
Cancel
</button>
</div>
</form>
{/* Step 2: Auth form (if needed) */}
{probeState.step === 'auth' && (
<div className="flex flex-col gap-2 rounded-md border border-duck-dark/10 dark:border-foreground/10 p-3">
<form
onSubmit={submitAuth}
className="flex flex-col gap-2 rounded-md border border-duck-dark/10 dark:border-foreground/10 p-3"
>
<span className="text-duck-dark/70 dark:text-foreground/70 font-medium">
{probeState.probe.name} requires authentication
</span>
@@ -361,17 +381,12 @@ export const AIHarnessesSection = () => {
placeholder="Password"
value={authPassword}
onChange={(ev) => setAuthPassword(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Enter' && authUsername && authPassword)
handleAuthSubmit(probeState.probe);
}}
/>
<Button
type="button"
type="submit"
size="sm"
className="h-7 text-xs w-fit bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40"
disabled={!authUsername || !authPassword}
onClick={() => handleAuthSubmit(probeState.probe)}
>
Authenticate
</Button>
@@ -384,23 +399,19 @@ export const AIHarnessesSection = () => {
placeholder="API Key"
value={authApiKey}
onChange={(ev) => setAuthApiKey(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Enter' && authApiKey) handleAuthSubmit(probeState.probe);
}}
autoFocus
/>
<Button
type="button"
type="submit"
size="sm"
className="h-7 text-xs w-fit bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40"
disabled={!authApiKey}
onClick={() => handleAuthSubmit(probeState.probe)}
>
Authenticate
</Button>
</>
)}
</div>
</form>
)}
</div>
) : (
@@ -460,7 +471,10 @@ export const AIHarnessesSection = () => {
</button>
</div>
{editingProvider === provider.key && (
<div className="flex items-center gap-2 pl-4">
<form
onSubmit={(ev) => submitApiKey(ev, provider.providerId)}
className="flex items-center gap-2 pl-4"
>
<Input
type="password"
className="h-7 text-xs flex-1"
@@ -470,15 +484,13 @@ export const AIHarnessesSection = () => {
setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value }))
}
onKeyDown={(ev) => {
if (ev.key === 'Enter' && keyInputs[provider.providerId]) saveApiKey(provider.providerId);
if (ev.key === 'Escape') setEditingProvider(null);
}}
autoFocus
/>
<button
type="button"
type="submit"
disabled={keyInputs[provider.providerId] === undefined || savingKey === provider.providerId}
onClick={() => saveApiKey(provider.providerId)}
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
title="Save key"
>
@@ -491,7 +503,7 @@ export const AIHarnessesSection = () => {
>
Cancel
</button>
</div>
</form>
)}
</div>
))}
@@ -501,7 +513,11 @@ export const AIHarnessesSection = () => {
if (!provider || connectedProviders.some((cp) => cp.providerId === provider.providerId))
return null;
return (
<div key={provider.providerId} className="flex items-center gap-2">
<form
key={provider.providerId}
onSubmit={(ev) => submitApiKey(ev, provider.providerId)}
className="flex items-center gap-2"
>
<label className="w-36 text-duck-dark/70 dark:text-foreground/70 shrink-0 truncate font-medium text-[11px]">
{provider.key}
</label>
@@ -514,15 +530,13 @@ export const AIHarnessesSection = () => {
setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value }))
}
onKeyDown={(ev) => {
if (ev.key === 'Enter' && keyInputs[provider.providerId]) saveApiKey(provider.providerId);
if (ev.key === 'Escape') setEditingProvider(null);
}}
autoFocus
/>
<button
type="button"
type="submit"
disabled={keyInputs[provider.providerId] === undefined || savingKey === provider.providerId}
onClick={() => saveApiKey(provider.providerId)}
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
title="Save key"
>
@@ -535,7 +549,7 @@ export const AIHarnessesSection = () => {
>
Cancel
</button>
</div>
</form>
);
})()}
{unconnectedProviders.length > 0 && (
@@ -1,3 +1,4 @@
import type { FormEvent } from 'react';
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -51,7 +52,8 @@ export const OCRSection = () => {
fetchModels(config.url);
}, [config]);
const handleSave = async () => {
const handleSave = async (ev?: FormEvent) => {
ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -92,7 +94,7 @@ export const OCRSection = () => {
if (isLoading) return <p className="text-sm text-duck-dark/40">Loading...</p>;
return (
<div className="grid gap-4">
<form onSubmit={handleSave} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Vision Model Server URL</span>
<Input
@@ -143,8 +145,7 @@ export const OCRSection = () => {
<div className="flex items-center gap-2">
<Button
type="button"
onClick={handleSave}
type="submit"
disabled={isSaving}
className="flex-1 h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
@@ -160,6 +161,6 @@ export const OCRSection = () => {
{isTesting ? 'Testing...' : 'Test'}
</Button>
</div>
</div>
</form>
);
};
@@ -1,3 +1,4 @@
import type { FormEvent } from 'react';
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -84,7 +85,8 @@ export const SMTPSection = () => {
...(provider === 'resend' ? { apiKey } : { host, port: parseInt(port) || 587, username, password, secure }),
});
const handleSave = async () => {
const handleSave = async (ev?: FormEvent) => {
ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -118,7 +120,8 @@ export const SMTPSection = () => {
}
};
const handleTest = async () => {
const handleTest = async (ev?: FormEvent) => {
ev?.preventDefault();
if (isTesting || !testEmail) return;
setIsTesting(true);
try {
@@ -151,150 +154,147 @@ export const SMTPSection = () => {
return (
<div className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Provider</span>
<Select value={provider} onValueChange={(v) => handleProviderChange(v as Provider)}>
<SelectTrigger className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground">
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[600]">
<SelectItem value="mailhog">MailHog (local testing)</SelectItem>
<SelectItem value="resend">Resend</SelectItem>
<SelectItem value="smtp">Custom SMTP</SelectItem>
</SelectContent>
</Select>
</Label>
{provider === 'resend' && (
<form onSubmit={handleSave} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">API Key</span>
<Input
type="password"
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground placeholder:text-duck-dark/40"
placeholder="re_..."
value={apiKey}
onChange={(ev) => setApiKey(ev.target.value)}
/>
<span className="text-duck-dark/70 dark:text-foreground/70">Provider</span>
<Select value={provider} onValueChange={(v) => handleProviderChange(v as Provider)}>
<SelectTrigger className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground">
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[600]">
<SelectItem value="mailhog">MailHog (local testing)</SelectItem>
<SelectItem value="resend">Resend</SelectItem>
<SelectItem value="smtp">Custom SMTP</SelectItem>
</SelectContent>
</Select>
</Label>
)}
{showSmtpFields && (
<>
<div className="grid grid-cols-[1fr_auto] gap-2">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Host</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground placeholder:text-duck-dark/40"
placeholder="smtp.example.com"
value={host}
onChange={(ev) => setHost(ev.target.value)}
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Port</span>
<Input
type="number"
className="h-11 w-24 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground"
value={port}
onChange={(ev) => setPort(ev.target.value)}
/>
</Label>
</div>
{provider === 'smtp' && (
<>
<div className="grid grid-cols-2 gap-2">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Username</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground placeholder:text-duck-dark/40"
placeholder="user@example.com"
value={username}
onChange={(ev) => setUsername(ev.target.value)}
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Password</span>
<Input
type="password"
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground"
value={password}
onChange={(ev) => setPassword(ev.target.value)}
/>
</Label>
</div>
<Label className="flex items-center gap-3">
<Switch checked={secure} onCheckedChange={setSecure} />
<span className="text-duck-dark/70 dark:text-foreground/70">TLS / SSL</span>
{provider === 'resend' && (
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">API Key</span>
<Input
type="password"
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground placeholder:text-duck-dark/40"
placeholder="re_..."
value={apiKey}
onChange={(ev) => setApiKey(ev.target.value)}
/>
</Label>
)}
{showSmtpFields && (
<>
<div className="grid grid-cols-[1fr_auto] gap-2">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Host</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground placeholder:text-duck-dark/40"
placeholder="smtp.example.com"
value={host}
onChange={(ev) => setHost(ev.target.value)}
/>
</Label>
</>
)}
</>
)}
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Port</span>
<Input
type="number"
className="h-11 w-24 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground"
value={port}
onChange={(ev) => setPort(ev.target.value)}
/>
</Label>
</div>
{provider === 'smtp' && (
<>
<div className="grid grid-cols-2 gap-2">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Username</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground placeholder:text-duck-dark/40"
placeholder="user@example.com"
value={username}
onChange={(ev) => setUsername(ev.target.value)}
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Password</span>
<Input
type="password"
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground"
value={password}
onChange={(ev) => setPassword(ev.target.value)}
/>
</Label>
</div>
<Label className="flex items-center gap-3">
<Switch checked={secure} onCheckedChange={setSecure} />
<span className="text-duck-dark/70 dark:text-foreground/70">TLS / SSL</span>
</Label>
</>
)}
</>
)}
<div className="grid grid-cols-2 gap-2">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">From Name</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground placeholder:text-duck-dark/40"
value={fromName}
onChange={(ev) => setFromName(ev.target.value)}
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">From Email</span>
<Input
type="email"
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground placeholder:text-duck-dark/40"
value={fromEmail}
onChange={(ev) => setFromEmail(ev.target.value)}
/>
</Label>
</div>
<div className="grid grid-cols-2 gap-2">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">From Name</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground placeholder:text-duck-dark/40"
value={fromName}
onChange={(ev) => setFromName(ev.target.value)}
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">From Email</span>
<Input
type="email"
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground placeholder:text-duck-dark/40"
value={fromEmail}
onChange={(ev) => setFromEmail(ev.target.value)}
/>
</Label>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
onClick={handleSave}
disabled={isSaving}
className="flex-1 h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
{isSaving ? 'Saving...' : 'Save'}
</Button>
<Button
type="button"
variant="outline"
onClick={handleTestConnection}
disabled={isTestingConnection}
className="h-11 cursor-pointer disabled:opacity-50"
>
{isTestingConnection ? 'Testing...' : 'Test'}
</Button>
</div>
<div className="flex items-center gap-2">
<Button
type="submit"
disabled={isSaving}
className="flex-1 h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
{isSaving ? 'Saving...' : 'Save'}
</Button>
<Button
type="button"
variant="outline"
onClick={handleTestConnection}
disabled={isTestingConnection}
className="h-11 cursor-pointer disabled:opacity-50"
>
{isTestingConnection ? 'Testing...' : 'Test'}
</Button>
</div>
</form>
<div className="border-t border-duck-dark/10 dark:border-foreground/10 pt-4 mt-1">
<span className="text-sm font-medium text-duck-dark dark:text-foreground">Send Test Email</span>
<div className="flex items-center gap-2 mt-2">
<form onSubmit={handleTest} className="flex items-center gap-2 mt-2">
<Input
type="email"
className="h-9 bg-background/60 border-duck-dark/20 text-duck-dark dark:text-foreground placeholder:text-duck-dark/40 flex-1"
placeholder="recipient@example.com"
value={testEmail}
onChange={(ev) => setTestEmail(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Enter') handleTest();
}}
/>
<Button
type="button"
type="submit"
variant="outline"
size="sm"
onClick={handleTest}
disabled={isTesting || !testEmail}
className="h-9 cursor-pointer disabled:opacity-50"
>
{isTesting ? 'Sending...' : 'Send'}
</Button>
</div>
</form>
</div>
</div>
);
@@ -1,3 +1,4 @@
import type { FormEvent } from 'react';
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -28,7 +29,8 @@ export const STTSection = () => {
setUrl(config.url);
}, [config]);
const handleSave = async () => {
const handleSave = async (ev?: FormEvent) => {
ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -69,7 +71,7 @@ export const STTSection = () => {
if (isLoading) return <p className="text-sm text-duck-dark/40">Loading...</p>;
return (
<div className="grid gap-4">
<form onSubmit={handleSave} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Whisper Server URL</span>
<Input
@@ -82,8 +84,7 @@ export const STTSection = () => {
<div className="flex items-center gap-2">
<Button
type="button"
onClick={handleSave}
type="submit"
disabled={isSaving}
className="flex-1 h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
@@ -99,6 +100,6 @@ export const STTSection = () => {
{isTesting ? 'Testing...' : 'Test'}
</Button>
</div>
</div>
</form>
);
};
@@ -1,3 +1,4 @@
import type { FormEvent } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -102,7 +103,8 @@ export const TTSSection = () => {
...(apiKey ? { apiKey } : {}),
});
const handleSave = async () => {
const handleSave = async (ev?: FormEvent) => {
ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -152,7 +154,7 @@ export const TTSSection = () => {
if (isLoading) return <p className="text-sm text-duck-dark/40">Loading...</p>;
return (
<div className="grid gap-4">
<form onSubmit={handleSave} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Provider</span>
<Select value={provider} onValueChange={(v) => handleProviderChange(v as Provider)}>
@@ -255,8 +257,7 @@ export const TTSSection = () => {
<div className="flex items-center gap-2">
<Button
type="button"
onClick={handleSave}
type="submit"
disabled={isSaving}
className="flex-1 h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
@@ -272,6 +273,6 @@ export const TTSSection = () => {
{isTesting ? 'Testing...' : 'Test'}
</Button>
</div>
</div>
</form>
);
};