This commit is contained in:
2026-04-27 15:24:23 -05:00
parent 32a00b3706
commit c4acdb2a66
28 changed files with 3575 additions and 358 deletions

View File

@@ -0,0 +1,130 @@
import React, { useEffect, useState } from 'react';
import { FiCornerUpRight, FiCalendar, FiSlash } from 'react-icons/fi';
import Modal from './Modal';
import LoadingOverlay from './LoadingOverlay';
import Forwarding from './Forwarding';
import OutOfOffice from './OutOfOffice';
import BlockedSenders from './BlockedSenders';
import { mailboxesAPI } from '../services/api';
const TABS = [
{ id: 'fwd', label: 'Forwarding', icon: FiCornerUpRight },
{ id: 'ooo', label: 'Out of Office', icon: FiCalendar },
{ id: 'block', label: 'Blocklist', icon: FiSlash },
];
const MailboxSettingsModal = ({ open, email, initialTab = 'fwd', onClose, onToast }) => {
const [activeTab, setActiveTab] = useState(initialTab);
const [rule, setRule] = useState(null);
const [blocklist, setBlocklist] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => { setActiveTab(initialTab); }, [initialTab, email]);
// Load both /rules and /blocklist in parallel when the modal opens.
useEffect(() => {
if (!open || !email) return;
let cancelled = false;
(async () => {
setLoading(true);
try {
const [r, b] = await Promise.all([
mailboxesAPI.getRules(email).catch(() => ({
email_address: email, ooo_active: false, ooo_message: '', forwards: [],
})),
mailboxesAPI.getBlocklist(email).catch(() => ({
email_address: email, blocked_patterns: [],
})),
]);
if (cancelled) return;
setRule(r || { email_address: email, ooo_active: false, ooo_message: '', forwards: [] });
setBlocklist(b || { email_address: email, blocked_patterns: [] });
} catch (err) {
if (!cancelled) onToast?.(`Failed to load settings: ${err.message}`, 'error');
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => { cancelled = true; };
}, [open, email, onToast]);
// Merge updates with existing rule and persist.
const saveRule = async (updates) => {
const merged = {
ooo_active: rule?.ooo_active ?? false,
ooo_message: rule?.ooo_message ?? '',
forwards: rule?.forwards ?? [],
...updates,
};
try {
const saved = await mailboxesAPI.putRules(email, merged);
setRule({ email_address: email, ...merged, ...saved });
onToast?.('Rule saved', 'success');
} catch (err) {
onToast?.(`Failed to save: ${err.message}`, 'error');
throw err;
}
};
const saveBlocklist = async (patterns) => {
try {
const saved = await mailboxesAPI.putBlocklist(email, patterns);
setBlocklist({ email_address: email, blocked_patterns: patterns, ...saved });
onToast?.('Block list saved', 'success');
} catch (err) {
onToast?.(`Failed to save: ${err.message}`, 'error');
throw err;
}
};
return (
<Modal
open={open}
onClose={onClose}
title={email || 'Mailbox settings'}
subtitle="Forwarding, auto-reply and blocklist"
size="md"
>
<div className="relative min-h-[400px]">
{/* Tabs */}
<div className="border-b border-gray-200 mb-6">
<div className="flex gap-1">
{TABS.map((t) => {
const Icon = t.icon;
const isActive = activeTab === t.id;
return (
<button
key={t.id}
onClick={() => setActiveTab(t.id)}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
isActive
? 'border-b-2 border-primary-600 text-primary-700 -mb-px'
: 'border-b-2 border-transparent text-gray-600 hover:text-gray-900 hover:border-gray-300'
}`}
>
<Icon className="w-4 h-4" />
{t.label}
</button>
);
})}
</div>
</div>
{/* Content */}
{loading || !rule || !blocklist ? (
<div className="py-16 flex items-center justify-center">
<LoadingOverlay message="Loading settings..." />
</div>
) : (
<>
{activeTab === 'fwd' && <Forwarding rule={rule} onSave={saveRule} />}
{activeTab === 'ooo' && <OutOfOffice rule={rule} onSave={saveRule} />}
{activeTab === 'block' && <BlockedSenders blocklist={blocklist} onSave={saveBlocklist} />}
</>
)}
</div>
</Modal>
);
};
export default MailboxSettingsModal;