16 KiB

SettingsPage

Overview

The SettingsPage provides a centralized interface for configuring all system-wide settings including organization branding, theme colors, email (SMTP), and feature toggles. It uses a tabbed interface with separate sections for each settings category.

Route: /app/settings Component: admin/src/pages/SettingsPage.tsx (420 lines) Auth Required: Yes (SUPER_ADMIN role recommended for production) Layout: AppLayout

Screenshot

[Screenshot: Settings page with 4 tabs (Organization, Theme Colors, Email, Feature Toggles). Currently showing Email tab with sections for Sender configuration, Active SMTP Provider toggle (MailHog vs Production), connection details, and test buttons. At bottom is a large "Save Settings" button.]

Features

  • Tabbed interface — 4 organized sections:
    • Organization (branding, logo, footer)
    • Theme Colors (admin + public themes with live preview)
    • Email (SMTP configuration with dual providers)
    • Feature Toggles (enable/disable modules)
  • SMTP provider switching — Toggle between MailHog (dev) and Production
  • Live theme preview — Color swatches + gradient preview
  • SMTP testing — Test connection + send test email
  • Form persistence — Settings loaded from Zustand store
  • Optimistic updates — Immediate UI feedback on save
  • ColorPicker integration — Visual color selection with hex output
  • Segmented control — Large toggle for SMTP provider switching

User Workflow

Updating Organization Settings

  1. Navigate to /app/settings
  2. Verify "Organization" tab is selected (default)
  3. Modify fields:
    • Organization Name
    • Short Name (max 10 chars, shown in collapsed sidebar)
    • Logo URL
    • Favicon URL
    • Footer Text
    • Login Subtitle
  4. Click "Save Settings" button at bottom
  5. Success message: "Settings saved successfully"
  6. Changes apply immediately (refresh not required)

Customizing Theme Colors

  1. Click "Theme Colors" tab
  2. Modify Admin Theme colors:
    • Primary Color (ColorPicker)
    • Background Color (ColorPicker)
  3. Modify Public Theme colors:
    • Primary Color
    • Background Color
    • Container Color
    • Header Gradient (CSS gradient string)
  4. View live preview swatches below form
  5. Click "Save Settings"
  6. Theme updates apply on next page load

Configuring SMTP Email

  1. Click "Email" tab
  2. Set Sender info:
    • From Name (e.g., "Changemaker Lite")
    • From Address (e.g., "noreply@cmlite.org")
  3. Switch SMTP Provider:
    • Click MailHog or Production segment
    • Confirmation: "Switched to [provider] SMTP"
  4. Configure Production SMTP:
    • SMTP Host (e.g., smtp.protonmail.ch)
    • SMTP Port (587 for STARTTLS, 465 for SSL)
    • SMTP User
    • SMTP Password
  5. Enable Test Mode (optional):
    • Toggle "Enable Test Mode" switch
    • Set Test Recipient email
    • All emails redirect to test recipient
  6. Click "Save Settings"
  7. Test configuration:
    • Click "Test Connection" → Verify "Connection successful"
    • Click "Send Test Email" → Check inbox for test message

Testing SMTP Configuration

  1. Navigate to Email tab
  2. Ensure production credentials are saved
  3. Switch to "Production" provider
  4. Click "Test Connection" button
  5. Wait for result (success/error alert)
  6. If successful, click "Send Test Email"
  7. Check email inbox for test message
  8. If failed, review error message and fix credentials

Enabling/Disabling Features

  1. Click "Feature Toggles" tab
  2. Toggle switches:
    • Enable Influence (campaigns, responses, reps)
    • Enable Map (locations, cuts, shifts, canvassing)
    • Enable Newsletter (Listmonk integration)
    • Enable Landing Pages (page builder)
  3. Info alert: "Disabling a module hides it from navigation but does not delete data"
  4. Click "Save Settings"
  5. Navigation menu updates to hide/show disabled modules

Component Breakdown

Ant Design Components Used

  • Typography.Text — Labels, descriptions
  • Tabs — Main navigation (4 tabs)
  • Form — All settings wrapped in single form instance
  • Form.Item — Individual fields with labels + extra descriptions
  • Input — Text fields (org name, logo URL, SMTP host, etc.)
  • Input.Password — SMTP password field (masked)
  • InputNumber — SMTP port (numeric, min 0, max 65535)
  • Switch — Boolean toggles (test mode, feature flags)
  • ColorPicker — Color selection with hex preview
  • Segmented — SMTP provider toggle (large button style)
  • Tag — Active provider indicator (green)
  • Alert — Info messages, connection/send test results
  • Divider — Section separators
  • Space — Button grouping
  • Button — Test actions + save button
  • Spin — Loading indicator during initial settings fetch

Tab Structure

const items = [
  {
    key: 'organization',
    label: 'Organization',
    icon: <SettingOutlined />,
    children: (/* Organization form fields */)
  },
  {
    key: 'theme',
    label: 'Theme Colors',
    children: (/* Theme form fields */)
  },
  {
    key: 'email',
    label: 'Email',
    children: (/* Email form fields */)
  },
  {
    key: 'features',
    label: 'Feature Toggles',
    children: (/* Feature toggle switches */)
  },
];

return (
  <Form form={form} layout="vertical">
    <Tabs items={items} />
    <Button type="primary" icon={<SaveOutlined />} onClick={handleSave}>
      Save Settings
    </Button>
  </Form>
);

Color Swatch Preview

function Swatch({ label, color }: { label: string; color: string }) {
  return (
    <div style={{ textAlign: 'center' }}>
      <div
        style={{
          width: 48,
          height: 48,
          borderRadius: 8,
          background: color,
          border: '2px solid rgba(255,255,255,0.2)',
          marginBottom: 4,
        }}
      />
      <Text style={{ fontSize: 11 }}>{label}</Text>
    </div>
  );
}

State Management

Zustand Store Used

  • settings.store — Centralized settings state
    • settings — Current settings object
    • loading — Loading state
    • fetchAdminSettings() — Load settings from API
    • updateSettings(partial) — Update and persist settings
import { useSettingsStore } from '@/stores/settings.store';

const { settings, loading, fetchAdminSettings, updateSettings } = useSettingsStore();

useEffect(() => {
  fetchAdminSettings();
}, [fetchAdminSettings]);

Local State

const [form] = Form.useForm();
const [testingConnection, setTestingConnection] = useState(false);
const [connectionResult, setConnectionResult] = useState<SmtpTestResult | null>(null);
const [sendingTest, setSendingTest] = useState(false);
const [sendResult, setSendResult] = useState<SmtpSendTestResult | null>(null);

Form Initialization

useEffect(() => {
  if (settings) {
    form.setFieldsValue(settings);
  }
}, [settings, form]);

When settings load from store, form automatically populates with current values.

API Integration

Endpoints Used

Method Endpoint Purpose
GET /api/settings Load settings (via store)
PUT /api/settings Update settings
POST /api/settings/email/test-connection Test SMTP connection
POST /api/settings/email/test-send Send test email

Save Settings

const handleSave = async () => {
  try {
    const values = form.getFieldsValue();

    // Convert ColorPicker values to hex strings
    const colorFields = [
      'adminColorPrimary',
      'adminColorBgBase',
      'publicColorPrimary',
      'publicColorBgBase',
      'publicColorBgContainer',
    ] as const;

    for (const field of colorFields) {
      const val = values[field];
      if (val && typeof val === 'object' && 'toHexString' in val) {
        values[field] = val.toHexString();
      }
    }

    await updateSettings(values);
    setConnectionResult(null);
    setSendResult(null);
    message.success('Settings saved successfully');
  } catch {
    message.error('Failed to save settings');
  }
};

Request Payload:

{
  "organizationName": "Changemaker Lite",
  "organizationShortName": "CML",
  "organizationLogoUrl": "https://example.com/logo.png",
  "smtpHost": "smtp.protonmail.ch",
  "smtpPort": 587,
  "smtpUser": "user@example.com",
  "smtpPass": "***",
  "smtpActiveProvider": "production",
  "adminColorPrimary": "#1890ff",
  "publicColorPrimary": "#3498db",
  "enableInfluence": true,
  "enableMap": true
}

Test SMTP Connection

const handleTestConnection = async () => {
  setTestingConnection(true);
  setConnectionResult(null);
  try {
    const { data } = await api.post<SmtpTestResult>('/settings/email/test-connection');
    setConnectionResult(data);
  } catch {
    setConnectionResult({ success: false, message: 'Request failed' });
  } finally {
    setTestingConnection(false);
  }
};

Response (Success):

{
  "success": true,
  "message": "Connection successful"
}

Response (Failure):

{
  "success": false,
  "message": "Connection failed: Authentication failed"
}

Send Test Email

const handleSendTest = async () => {
  setSendingTest(true);
  setSendResult(null);
  try {
    const to = form.getFieldValue('testEmailRecipient');
    const { data } = await api.post<SmtpSendTestResult>('/settings/email/test-send', { to });
    setSendResult(data);
  } catch {
    setSendResult({ success: false, testMode: false, recipient: '' });
  } finally {
    setSendingTest(false);
  }
};

Request:

{
  "to": "admin@example.com"
}

Response (Success):

{
  "success": true,
  "testMode": false,
  "recipient": "admin@example.com",
  "messageId": "<abc123@smtp.protonmail.ch>"
}

Toggle SMTP Provider

const handleProviderToggle = async (value: string | number) => {
  const provider = value as 'mailhog' | 'production';
  try {
    await updateSettings({ smtpActiveProvider: provider });
    form.setFieldsValue({ smtpActiveProvider: provider });
    message.success(`Switched to ${provider === 'mailhog' ? 'MailHog' : 'Production'} SMTP`);
    setConnectionResult(null);
    setSendResult(null);
  } catch {
    message.error('Failed to switch SMTP provider');
  }
};

Why clear test results?

Test results are provider-specific. Switching providers invalidates previous test results.

ColorPicker Integration

Converting Color Values

Ant Design ColorPicker returns an object with toHexString() method:

// ColorPicker value
const colorValue = {
  toHexString: () => '#1890ff',
  // ... other methods
};

// Convert before saving
for (const field of colorFields) {
  const val = values[field];
  if (val && typeof val === 'object' && 'toHexString' in val) {
    values[field] = val.toHexString();
  }
}

Theme Preview

{settings && (
  <div style={{ marginTop: 24 }}>
    <Text strong style={{ fontSize: 15 }}>Preview</Text>
    <Divider style={{ margin: '12px 0' }} />
    <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
      <Swatch label="Admin Primary" color={settings.adminColorPrimary} />
      <Swatch label="Admin BG" color={settings.adminColorBgBase} />
      <Swatch label="Public Primary" color={settings.publicColorPrimary} />
      <Swatch label="Public BG" color={settings.publicColorBgBase} />
      <Swatch label="Public Container" color={settings.publicColorBgContainer} />
    </div>
    <div
      style={{
        marginTop: 12,
        padding: '12px 24px',
        background: settings.publicHeaderGradient,
        borderRadius: 8,
        color: '#fff',
        fontWeight: 600,
      }}
    >
      Header Gradient Preview
    </div>
  </div>
)}

Performance Considerations

Single Form Instance

All settings use one form instance:

const [form] = Form.useForm();

<Form form={form} layout="vertical">
  <Tabs items={items} />
  <Button onClick={handleSave}>Save Settings</Button>
</Form>

Benefits:

  • Single save operation — One API call saves all modified fields
  • Consistent validation — All fields validated together
  • Simplified state — No need to track which tab has changes

Optimistic Provider Switching

Provider toggle updates immediately without waiting for API:

await updateSettings({ smtpActiveProvider: provider });
form.setFieldsValue({ smtpActiveProvider: provider });  // Update form immediately
message.success(`Switched to ${provider}`);

Why optimistic?

  • Instant feedback — User sees immediate response
  • Better UX — No loading delay for simple toggle
  • Safe operation — Provider toggle is low-risk (can always switch back)

Troubleshooting

SMTP Test Connection Failing

Problem: Click "Test Connection" → Error: "Connection failed: Authentication failed"

Diagnosis:

Check SMTP credentials:

{
  smtpHost: "smtp.protonmail.ch",
  smtpPort: 587,
  smtpUser: "user@protonmail.com",
  smtpPass: "***"
}

Common Issues:

  1. Wrong port:

    • Use 587 for STARTTLS
    • Use 465 for SSL/TLS
    • Port 25 often blocked by ISPs
  2. App-specific password required:

    • Gmail requires app-specific passwords (not account password)
    • ProtonMail requires ProtonMail Bridge for SMTP
  3. Wrong provider selected:

    • Ensure "Production" is selected before testing production credentials

Solution:

  1. Verify credentials with email provider documentation
  2. Switch to "Production" provider
  3. Save settings before testing
  4. Check firewall rules (port 587/465 outbound)

Theme Colors Not Applying

Problem: Change colors, save settings, but theme doesn't update.

Diagnosis:

Check if page reload is required:

// Theme updates apply on NEXT page load, not immediately
await updateSettings({ adminColorPrimary: '#ff0000' });
// Current page still shows old color

Solution:

Refresh page after saving theme changes:

const handleSave = async () => {
  await updateSettings(values);
  message.success('Settings saved. Refreshing page...');
  setTimeout(() => window.location.reload(), 1000);
};

Feature Toggle Not Hiding Module

Problem: Disable "Enable Influence" toggle, save, but Influence menu items still visible.

Diagnosis:

Check AppLayout navigation logic:

// AppLayout should check settings.enableInfluence
{settings.enableInfluence && (
  <SubMenu key="influence" title="Influence">
    {/* Influence menu items */}
  </SubMenu>
)}

Solution:

Ensure AppLayout reads settings from store and conditionally renders menu items.


Test Email Not Sending

Problem: Click "Send Test Email" → Success message, but no email in inbox.

Diagnosis:

  1. Check active provider:

    settings.smtpActiveProvider === 'mailhog' // MailHog (dev)
    settings.smtpActiveProvider === 'production' // Real SMTP
    
  2. Check test mode:

    settings.emailTestMode === true // All emails redirect to testEmailRecipient
    
  3. Check spam folder

  4. Check MailHog web UI (http://localhost:8025) if MailHog is active

Solution:

  • Switch to "Production" provider
  • Disable test mode if you want emails to go to actual recipients
  • Save settings before sending test email