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:
@@ -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">
|
||||
|
||||
+6
-5
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-5
@@ -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">
|
||||
|
||||
+6
-5
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+82
-68
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user