← back to Patty
components/settings/SettingsTab.tsx
414 lines
'use client';
import { useState, useEffect } from 'react';
import {
Save, RefreshCw, Building2, Mail, Sparkles, Database,
Globe, FileText, Clock, Shield, Download, Megaphone,
Users, Target, Settings, Share2,
} from 'lucide-react';
import SocialCredentialsSection from './SocialCredentialsSection';
/* ─── Types ──────────────────────────────────────────────────────────────── */
interface OrgProfile {
name: string;
website: string;
email: string;
mission: string;
}
interface PetitionDefaults {
default_signature_goal: number;
default_distribution: string;
auto_compliance_check: boolean;
}
interface CampaignSettings {
default_send_time: string;
email_footer: string;
unsubscribe_text: string;
}
interface AIConfig {
model: string;
creativity_level: number;
default_tone: string;
}
interface DataStats {
total_petitions: number;
total_signatures: number;
total_subscribers: number;
total_campaigns: number;
last_backup: string;
}
interface SettingsData {
org_profile: OrgProfile;
petition_defaults: PetitionDefaults;
campaign_settings: CampaignSettings;
ai_config: AIConfig;
data_stats: DataStats;
}
/* ─── Defaults ───────────────────────────────────────────────────────────── */
const DEFAULTS: SettingsData = {
org_profile: {
name: 'Student Debt Crisis Center',
website: 'https://studentdebtcrisis.org',
email: 'info@studentdebtcrisis.org',
mission: 'Empowering borrowers through organized petition campaigns and advocacy for systemic student debt reform.',
},
petition_defaults: {
default_signature_goal: 10000,
default_distribution: 'email',
auto_compliance_check: true,
},
campaign_settings: {
default_send_time: '09:00',
email_footer: 'Student Debt Crisis Center | studentdebtcrisis.org',
unsubscribe_text: 'Click here to unsubscribe from future petition updates.',
},
ai_config: {
model: 'Gemini Flash',
creativity_level: 60,
default_tone: 'passionate',
},
data_stats: {
total_petitions: 0,
total_signatures: 0,
total_subscribers: 0,
total_campaigns: 0,
last_backup: new Date().toISOString(),
},
};
/* ─── Main Component ─────────────────────────────────────────────────────── */
export default function SettingsTab() {
const [settings, setSettings] = useState<SettingsData>(DEFAULTS);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [saveMessage, setSaveMessage] = useState('');
useEffect(() => {
fetchSettings();
}, []);
async function fetchSettings() {
try {
const res = await fetch('/api/settings');
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
const s = data.settings || {};
setSettings({
org_profile: { ...DEFAULTS.org_profile, ...(s.org_profile || {}) },
petition_defaults: { ...DEFAULTS.petition_defaults, ...(s.petition_defaults || {}) },
campaign_settings: { ...DEFAULTS.campaign_settings, ...(s.campaign_settings || {}) },
ai_config: { ...DEFAULTS.ai_config, ...(s.ai_config || {}) },
data_stats: { ...DEFAULTS.data_stats, ...(s.data_stats || {}) },
});
} catch (err) {
console.error('Failed to fetch settings:', err);
} finally {
setLoading(false);
}
}
async function handleSave() {
setSaving(true);
setSaveMessage('');
try {
const res = await fetch('/api/settings', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
org_profile: settings.org_profile,
petition_defaults: settings.petition_defaults,
campaign_settings: settings.campaign_settings,
ai_config: settings.ai_config,
}),
});
if (!res.ok) throw new Error('Failed to save');
setSaveMessage('Settings saved successfully');
setTimeout(() => setSaveMessage(''), 3000);
} catch (err) {
setSaveMessage('Failed to save settings');
} finally {
setSaving(false);
}
}
function updateOrg(field: string, value: string) {
setSettings((prev) => ({
...prev,
org_profile: { ...prev.org_profile, [field]: value },
}));
}
function updatePetition(field: string, value: string | number | boolean) {
setSettings((prev) => ({
...prev,
petition_defaults: { ...prev.petition_defaults, [field]: value },
}));
}
function updateCampaign(field: string, value: string) {
setSettings((prev) => ({
...prev,
campaign_settings: { ...prev.campaign_settings, [field]: value },
}));
}
function updateAI(field: string, value: string | number) {
setSettings((prev) => ({
...prev,
ai_config: { ...prev.ai_config, [field]: value },
}));
}
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<RefreshCw size={20} className="animate-spin" style={{ color: 'var(--color-text-muted)' }} />
</div>
);
}
return (
<div className="p-5 space-y-5 max-w-4xl">
{/* Save bar */}
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>Settings</h2>
<div className="flex items-center gap-3">
{saveMessage && (
<span className="text-sm" style={{ color: saveMessage.includes('success') ? '#34d399' : '#f87171' }}>
{saveMessage}
</span>
)}
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
{saving ? (
<><RefreshCw size={14} className="animate-spin" /><span>Saving...</span></>
) : (
<><Save size={14} /><span>Save Changes</span></>
)}
</button>
</div>
</div>
{/* ── Organization Profile ─────────────────────────────────────────── */}
<div className="card p-5">
<div className="flex items-center gap-2 mb-4">
<Building2 size={16} style={{ color: '#7c3aed' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>Organization Profile</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Organization Name</label>
<input className="input w-full" value={settings.org_profile.name} onChange={(e) => updateOrg('name', e.target.value)} />
</div>
<div>
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Website URL</label>
<div className="relative">
<Globe size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2" style={{ color: 'var(--color-text-muted)' }} />
<input className="input w-full pl-8" value={settings.org_profile.website} onChange={(e) => updateOrg('website', e.target.value)} />
</div>
</div>
<div className="col-span-2">
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Contact Email</label>
<div className="relative">
<Mail size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2" style={{ color: 'var(--color-text-muted)' }} />
<input className="input w-full pl-8" value={settings.org_profile.email} onChange={(e) => updateOrg('email', e.target.value)} />
</div>
</div>
</div>
<div className="mt-4">
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Mission Statement</label>
<textarea className="input w-full" rows={3} value={settings.org_profile.mission} onChange={(e) => updateOrg('mission', e.target.value)} />
</div>
</div>
{/* ── Petition Defaults ────────────────────────────────────────────── */}
<div className="card p-5">
<div className="flex items-center gap-2 mb-4">
<Megaphone size={16} style={{ color: '#f59e0b' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>Petition Defaults</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Default Signature Goal</label>
<div className="relative">
<Target size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2" style={{ color: 'var(--color-text-muted)' }} />
<input
type="number"
className="input w-full pl-8"
value={settings.petition_defaults.default_signature_goal}
onChange={(e) => updatePetition('default_signature_goal', parseInt(e.target.value) || 0)}
/>
</div>
</div>
<div>
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Default Distribution</label>
<select
className="input w-full"
value={settings.petition_defaults.default_distribution}
onChange={(e) => updatePetition('default_distribution', e.target.value)}
>
<option value="email">Email</option>
<option value="social">Social Media</option>
<option value="both">Both</option>
</select>
</div>
</div>
<div className="mt-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={settings.petition_defaults.auto_compliance_check}
onChange={(e) => updatePetition('auto_compliance_check', e.target.checked)}
/>
<span className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>
<Shield size={12} className="inline mr-1" style={{ verticalAlign: 'middle', color: '#10b981' }} />
Auto-compliance check on new petitions
</span>
</label>
</div>
</div>
{/* ── Campaign Settings ────────────────────────────────────────────── */}
<div className="card p-5">
<div className="flex items-center gap-2 mb-4">
<Mail size={16} style={{ color: '#06b6d4' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>Campaign Settings</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Default Send Time</label>
<input
type="time"
className="input w-full"
value={settings.campaign_settings.default_send_time}
onChange={(e) => updateCampaign('default_send_time', e.target.value)}
/>
</div>
<div>
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Email Footer</label>
<input className="input w-full" value={settings.campaign_settings.email_footer} onChange={(e) => updateCampaign('email_footer', e.target.value)} />
</div>
</div>
<div className="mt-4">
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Unsubscribe Text</label>
<textarea
className="input w-full"
rows={2}
value={settings.campaign_settings.unsubscribe_text}
onChange={(e) => updateCampaign('unsubscribe_text', e.target.value)}
/>
</div>
</div>
{/* ── AI Configuration ─────────────────────────────────────────────── */}
<div className="card p-5">
<div className="flex items-center gap-2 mb-4">
<Sparkles size={16} style={{ color: '#f59e0b' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>AI Configuration</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>AI Model</label>
<input className="input w-full" value={settings.ai_config.model} readOnly style={{ opacity: 0.7, cursor: 'not-allowed' }} />
</div>
<div>
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Default Tone</label>
<select className="input w-full" value={settings.ai_config.default_tone} onChange={(e) => updateAI('default_tone', e.target.value)}>
<option value="formal">Formal</option>
<option value="passionate">Passionate</option>
<option value="urgent">Urgent</option>
<option value="conversational">Conversational</option>
</select>
</div>
</div>
<div className="mt-4">
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>
Creativity Level: {settings.ai_config.creativity_level}%
</label>
<input
type="range"
min={0}
max={100}
value={settings.ai_config.creativity_level}
onChange={(e) => updateAI('creativity_level', parseInt(e.target.value))}
className="w-full"
style={{ accentColor: 'var(--color-primary)' }}
/>
<div className="flex justify-between text-xs mt-1" style={{ color: 'var(--color-text-muted)' }}>
<span>Conservative</span>
<span>Creative</span>
</div>
</div>
</div>
{/* ── Data Stats ───────────────────────────────────────────────────── */}
<div className="card p-5">
<div className="flex items-center gap-2 mb-4">
<Database size={16} style={{ color: '#10b981' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>Data Management</h3>
</div>
<div className="grid grid-cols-4 gap-3 mb-4">
{[
{ label: 'Petitions', value: settings.data_stats.total_petitions, icon: Megaphone },
{ label: 'Signatures', value: settings.data_stats.total_signatures, icon: FileText },
{ label: 'Subscribers', value: settings.data_stats.total_subscribers, icon: Users },
{ label: 'Campaigns', value: settings.data_stats.total_campaigns, icon: Mail },
].map((stat) => (
<div
key={stat.label}
className="rounded-lg p-3 text-center"
style={{ backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)' }}
>
<stat.icon size={16} className="mx-auto mb-1" style={{ color: 'var(--color-text-muted)' }} />
<p className="text-lg font-bold" style={{ color: 'var(--color-text)' }}>{stat.value}</p>
<p className="text-xs" style={{ color: 'var(--color-text-muted)' }}>{stat.label}</p>
</div>
))}
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-xs" style={{ color: 'var(--color-text-muted)' }}>
<Clock size={12} />
<span>
Last backup: {new Date(settings.data_stats.last_backup).toLocaleString('en-US', {
timeZone: 'America/Los_Angeles', month: 'short', day: 'numeric',
hour: 'numeric', minute: '2-digit', hour12: true,
})} PT
</span>
</div>
<button className="btn btn-ghost btn-sm">
<Download size={13} />
<span>Export All Data</span>
</button>
</div>
</div>
{/* ── Social Platform Credentials ───────────────────────────────────── */}
<div className="card p-5">
<div className="flex items-center gap-2 mb-1">
<Share2 size={16} style={{ color: '#8b5cf6' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>Social Platform Credentials</h3>
</div>
<p className="text-xs mb-4" style={{ color: 'var(--color-text-muted)' }}>
Configure API credentials for each platform. Credentials are saved to the agent servers and take effect immediately on save.
</p>
<SocialCredentialsSection />
</div>
</div>
);
}