From 9aa535ace8b3fae466705b9fa06e1aa154f00d0c Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:02:30 +0200 Subject: [PATCH] feat: add EmailConfigPage with SMTP form, registration toggle, and test email --- ui/src/pages/vendor/EmailConfigPage.tsx | 296 ++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 ui/src/pages/vendor/EmailConfigPage.tsx diff --git a/ui/src/pages/vendor/EmailConfigPage.tsx b/ui/src/pages/vendor/EmailConfigPage.tsx new file mode 100644 index 0000000..f1fae4d --- /dev/null +++ b/ui/src/pages/vendor/EmailConfigPage.tsx @@ -0,0 +1,296 @@ +import { useState } from 'react'; +import { + Alert, + Button, + Card, + FormField, + Input, + Spinner, + useToast, +} from '@cameleer/design-system'; +import { Send, Trash2, Save, Power } from 'lucide-react'; +import { + useEmailConnector, + useSaveEmailConnector, + useDeleteEmailConnector, + useTestEmailConnector, + useToggleRegistration, +} from '../../api/email-connector-hooks'; +import styles from '../../styles/platform.module.css'; + +export function EmailConfigPage() { + const { toast } = useToast(); + const { data: connector, isLoading, isError } = useEmailConnector(); + const saveMutation = useSaveEmailConnector(); + const deleteMutation = useDeleteEmailConnector(); + const testMutation = useTestEmailConnector(); + const toggleMutation = useToggleRegistration(); + + const [editing, setEditing] = useState(false); + const [host, setHost] = useState(''); + const [port, setPort] = useState('587'); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [fromEmail, setFromEmail] = useState(''); + const [testTo, setTestTo] = useState(''); + const [confirmDelete, setConfirmDelete] = useState(false); + + const isConfigured = connector != null; + const showForm = !isConfigured || editing; + + function startEditing() { + if (connector) { + setHost(connector.host); + setPort(String(connector.port)); + setUsername(connector.username); + setPassword(''); + setFromEmail(connector.fromEmail); + } + setEditing(true); + } + + async function handleSave() { + if (!host || !username || !password || !fromEmail) { + toast({ title: 'All fields are required', variant: 'error' }); + return; + } + try { + await saveMutation.mutateAsync({ + host, + port: parseInt(port, 10) || 587, + username, + password, + fromEmail, + }); + toast({ title: 'Email connector saved', variant: 'success' }); + setEditing(false); + setPassword(''); + } catch (err) { + toast({ title: 'Failed to save', description: String(err), variant: 'error' }); + } + } + + async function handleDelete() { + try { + await deleteMutation.mutateAsync(); + toast({ title: 'Email connector removed', variant: 'success' }); + setConfirmDelete(false); + setEditing(false); + } catch (err) { + toast({ title: 'Failed to delete', description: String(err), variant: 'error' }); + } + } + + async function handleTest() { + if (!testTo) { + toast({ title: 'Enter a recipient email address', variant: 'error' }); + return; + } + try { + const result = await testMutation.mutateAsync(testTo); + if (result.status === 'sent') { + toast({ title: 'Test email sent', description: result.message, variant: 'success' }); + } else { + toast({ title: 'Test failed', description: result.message, variant: 'error' }); + } + } catch (err) { + toast({ title: 'Test failed', description: String(err), variant: 'error' }); + } + } + + async function handleToggleRegistration() { + if (!connector) return; + const newValue = !connector.registrationEnabled; + try { + await toggleMutation.mutateAsync(newValue); + toast({ + title: newValue ? 'Registration enabled' : 'Registration disabled', + variant: 'success', + }); + } catch (err) { + toast({ title: 'Failed to toggle registration', description: String(err), variant: 'error' }); + } + } + + if (isLoading) { + return ( +
+ {connector.registrationEnabled + ? 'New users can register with their email address and verify via a code sent to their inbox.' + : 'Registration is disabled. Only admin-invited users can sign in.'} +
+