/* ===== Settings — fully functional, persisted via setProfile / tweaks ===== */
/* ---- Backup helpers: export/import ALL persisted app data ---- */
function exportAppData() {
var payload = { app: 'sectionly', version: 2, exportedAt: new Date().toISOString(), data: window.Store.state() };
var blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'sectionly-backup-' + new Date().toISOString().slice(0, 10) + '.json';
document.body.appendChild(a); a.click(); a.remove();
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
}
function importAppData(file, done) {
var r = new FileReader();
r.onload = function () {
try {
var parsed = JSON.parse(String(r.result));
var data = parsed && parsed.data ? parsed.data : parsed;
if (!data || typeof data !== 'object') { done(false, 0); return; }
window.Store.replaceAll(data);
var n = (data.userSections ? data.userSections.length : 0);
done(true, n);
} catch (e) { done(false, 0); }
};
r.readAsText(file);
}
function Switch({ on, onClick }) {
return ;
}
function SettingsView({ nav, route, profile, setProfile, notify, t, setTweak, favs }) {
const tabs = [
{ id: 'profile', label: 'Profile', icon: 'user' },
{ id: 'account', label: 'Account', icon: 'gear' },
{ id: 'appearance', label: 'Appearance', icon: 'palette' },
{ id: 'notifications', label: 'Notifications', icon: 'bell' },
{ id: 'billing', label: 'Billing & Plan', icon: 'card' },
{ id: 'security', label: 'Security', icon: 'shield' }
];
const [tab, setTab] = useState(route.tab || 'profile');
return (
Account
Settings
Manage your profile, preferences and workspace.
{tabs.map(tb =>
setTab(tb.id)}>
{tb.label}
)}
{ setTab('security'); }}>
Sign out
{tab === 'profile' &&
}
{tab === 'account' &&
}
{tab === 'appearance' &&
}
{tab === 'notifications' &&
}
{tab === 'billing' &&
}
{tab === 'security' &&
}
);
}
/* ---------- Profile ---------- */
function ProfileTab({ profile, setProfile, notify }) {
const [form, setForm] = useState(profile);
const fileRef = useRef(null);
const dirty = JSON.stringify(form) !== JSON.stringify(profile);
const set = (k, v) => setForm(f => Object.assign({}, f, { [k]: v }));
const onPick = (e) => {
const file = e.target.files[0]; if (!file) return;
if (!/^image\//.test(file.type)) { notify('Please choose an image', true); return; }
const r = new FileReader();
r.onload = () => set('avatar', String(r.result));
r.readAsDataURL(file);
};
return (
<>
Profile photo
This appears in the sidebar and on your shared sections.
{form.avatar ?
: initials(form.name)}
fileRef.current.click()}> Upload new
{form.avatar && set('avatar', '')}>Remove }
Personal information
Update your name and how others see you.
Bio
{ setProfile(form); notify('Profile saved'); }}>
Save changes
{dirty ? setForm(profile)}>Discard
: All changes saved }
>
);
}
/* ---------- Account ---------- */
function AccountTab({ profile, setProfile, notify }) {
const [lang, setLang] = useState(profile.lang || 'English (US)');
const [tz, setTz] = useState(profile.tz || '(GMT+6) Dhaka');
const importRef = useRef(null);
const customCount = window.SectionStore.list().length;
const editedCount = window.SectionStore.modifiedCount();
const backupSummary = customCount + ' custom section' + (customCount === 1 ? '' : 's')
+ (editedCount ? ' \u00b7 ' + editedCount + ' edited built-in' + (editedCount === 1 ? '' : 's') : '')
+ ' \u00b7 favorites, profile & preferences';
return (
<>
Regional
Set your language and timezone.
Language
setLang(e.target.value)}>
{['English (US)', 'English (UK)', 'বাংলা', 'Deutsch', 'Español', 'Français', '日本語'].map(l => {l} )}
Timezone
setTz(e.target.value)}>
{['(GMT+6) Dhaka', '(GMT+0) London', '(GMT-5) New York', '(GMT-8) Los Angeles', '(GMT+1) Berlin', '(GMT+9) Tokyo'].map(z => {z} )}
Connected accounts
Sign in faster and sync your work.
{[['GitHub', 'globe', true], ['Google', 'globe', true], ['Figma', 'palette', false]].map(([n, ic, conn]) =>
{n} {conn ? 'Connected' : 'Not connected'}
notify(conn ? n + ' disconnected' : n + ' connected')}>{conn ? 'Disconnect' : 'Connect'}
)}
Backup & restore
Your sections, favorites, profile and preferences are {window.storageLabel()}. Export a backup to keep them safe or move them to another browser or device.
Export backup {backupSummary}
{ exportAppData(); notify('Backup downloaded'); }}> Export
Restore from backup Import a .json backup file. This replaces matching data, then reloads.
{ const f = e.target.files[0]; e.target.value = ''; if (!f) return;
importAppData(f, (ok, n) => { if (ok) { notify('Restored ' + n + ' items \u2014 reloading\u2026'); setTimeout(() => location.reload(), 900); } else { notify('That file isn\u2019t a valid backup', true); } }); }} />
importRef.current.click()}> Import
{ setProfile({ lang, tz }); notify('Account settings saved'); }}> Save changes
>
);
}
/* ---------- Appearance (wired to live tweaks) ---------- */
function AppearanceTab({ t, setTweak, notify }) {
const accents = ['#1a7fff', '#6366f1', '#10b981', '#f97316', '#ec4899', '#8b5cf6'];
return (
<>
Theme
Switch between light and dark — applies instantly.
setTweak('theme', 'light')}> Light
setTweak('theme', 'dark')}> Dark
Accent color
Used across buttons, links and highlights.
{accents.map(c =>
setTweak('accent', c)} />)}
Density
Control spacing in the section grid.
{['compact', 'regular', 'comfy'].map(d =>
setTweak('density', d)} style={{ textTransform: 'capitalize' }}>{d} )}
Typography
Pick a font pairing for the interface.
{[['modern', 'Modern'], ['geometric', 'Geometric'], ['playful', 'Playful']].map(([v, l]) =>
setTweak('fontPair', v)}>{l} )}
>
);
}
/* ---------- Notifications ---------- */
function NotificationsTab({ profile, setProfile, notify }) {
const n = profile.notif || {};
const toggle = (k) => setProfile(p => Object.assign({}, p, { notif: Object.assign({}, p.notif, { [k]: !p.notif[k] }) }));
const rows = [
['product', 'Product updates', 'New sections, features and improvements.'],
['weekly', 'Weekly digest', 'A summary of fresh sections every Monday.'],
['mentions', 'Comments & mentions', 'When someone replies to or mentions you.'],
['marketing', 'Tips & offers', 'Occasional product tips and promotions.']
];
return (
Email notifications
Choose what lands in your inbox. Saved automatically.
{rows.map(([k, title, desc]) =>
{title} {desc}
{ toggle(k); notify('Preference updated'); }} />
)}
);
}
/* ---------- Billing ---------- */
function BillingTab({ favs, nav, notify }) {
const customCount = window.SectionStore.list().length;
const used = Math.min(40, customCount + (favs ? favs.length : 0));
return (
<>
Current plan
You're on the Pro trial.
Pro — Trial 9 days remaining · renews at $19/mo
notify('Redirecting to checkout…')}>Upgrade now
Favorites & custom sections
{used} of 40 used on the free tier
Payment method
Used for your subscription after the trial.
Visa ending 4242 Expires 08 / 27
notify('Update payment method')}>Update
Billing history
Your recent invoices.
{[['Jun 1, 2026', '$19.00', 'Pro monthly'], ['May 1, 2026', '$19.00', 'Pro monthly'], ['Apr 1, 2026', '$0.00', 'Trial started']].map(([d, amt, desc], i) =>
{desc} {d}
{amt}
notify('Invoice downloaded')}>
)}
>
);
}
/* ---------- Security ---------- */
function SecurityTab({ profile, setProfile, notify }) {
const [pw, setPw] = useState({ cur: '', next: '', conf: '' });
const canSave = pw.cur && pw.next.length >= 8 && pw.next === pw.conf;
return (
<>
Password
Use at least 8 characters.
Current password setPw(p => ({ ...p, cur: e.target.value }))} placeholder="••••••••" />
{pw.next && pw.next.length < 8 &&
Password must be at least 8 characters.
}
{pw.conf && pw.next !== pw.conf &&
Passwords don't match.
}
{ setPw({ cur: '', next: '', conf: '' }); notify('Password updated'); }}>Update password
Two-factor authentication
Add an extra layer of security at sign-in.
Authenticator app {profile.twoFactor ? 'Enabled' : 'Currently disabled'}
{ setProfile({ twoFactor: !profile.twoFactor }); notify(profile.twoFactor ? '2FA disabled' : '2FA enabled'); }} />
Active sessions
Devices currently signed in.
{[['MacBook Pro · Dhaka', 'This device · active now', true], ['iPhone 15 · Dhaka', 'Last active 2h ago', false]].map(([d, meta, cur], i) =>
{d} {meta}
{cur ?
Current
:
notify('Session revoked')}>Revoke }
)}
Delete account
Permanently remove your account and all custom sections. This can't be undone.
notify('Account deletion requires email confirmation', true)}> Delete my account
>
);
}
Object.assign(window, { SettingsView });