/* settings.jsx — раздел «Настройки»: категории-фильтры + базовые опции. */ function SettingsScreen({ theme, onToggleTheme }) { const settings = Store.useSettings(); const cats = settings.categories || []; const [adding, setAdding] = React.useState(false); const setDensity = (d) => Store.patchSettings({ density: d }); const setDefault = (k) => Store.patchSettings({ defaultCategory: k }); const toggle = (k) => Store.patchSettings({ [k]: !settings[k] }); return (
{/* --------------------------------------------------- КАТЕГОРИИ */}
{cats.map((c) => )}
{adding ? ( setAdding(false)} /> ) : ( )}
{/* --------------------------------------------------- ВНЕШНИЙ ВИД */}
{ if (v !== theme) onToggleTheme(); }} options={[{ v: "light", label: "Светлая", icon: "sun" }, { v: "dark", label: "Тёмная", icon: "moon" }]} /> toggle("showSource")} />
{/* --------------------------------------------------- ИИ */}
toggle("autoReminders")} />
{/* --------------------------------------------------- БЕЗОПАСНОСТЬ */}
включён
{/* --------------------------------------------------- АККАУНТ */} {/* --------------------------------------------------- АДМИН */} {/* --------------------------------------------------- ДАННЫЕ */}
); } /* ----------------------------------------------------------- аккаунт: логин/пароль */ function AccountSection() { const [me, setMe] = React.useState(null); const [username, setUsername] = React.useState(""); const [password, setPassword] = React.useState(""); const [busy, setBusy] = React.useState(false); const [msg, setMsg] = React.useState(null); // {ok, text} React.useEffect(() => { api.getMe().then((u) => { setMe(u); setUsername(u.username || ""); }).catch(() => {}); }, []); const save = async () => { setMsg(null); setBusy(true); try { const r = await api.setCredentials({ username: username.trim(), password }); setMe((m) => ({ ...m, username: (r.user && r.user.username) || username.trim(), has_password: true })); setPassword(""); setMsg({ ok: true, text: "Сохранено. Теперь можно входить по логину и паролю." }); } catch (e) { const code = e && e.status; setMsg({ ok: false, text: code === 409 ? "Этот логин уже занят." : code === 422 ? "Логин 3–64 символа (буквы/цифры/._-), пароль от 6 символов." : "Не удалось сохранить." }); } finally { setBusy(false); } }; return (
{me && me.username && ( {me.username} )}
setUsername(e.target.value)} placeholder="логин" autoCapitalize="none" className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-2)] px-3.5 h-11 text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition" /> setPassword(e.target.value)} placeholder={me && me.has_password ? "новый пароль" : "пароль"} className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-2)] px-3.5 h-11 text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition" />
{msg &&

{msg.text}

}
); } /* ----------------------------------------------------------- админ-панель */ function AdminSection() { const [me, setMe] = React.useState(null); const [data, setData] = React.useState(null); // {users, pending} const [tgId, setTgId] = React.useState(""); const [note, setNote] = React.useState(""); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(""); const load = React.useCallback(async () => { try { setData(await api.adminListUsers()); } catch (e) {} }, []); React.useEffect(() => { api.getMe().then((u) => { setMe(u); if (u.is_admin) load(); }).catch(() => {}); }, [load]); if (!me || !me.is_admin) return null; const invite = async () => { setErr(""); const id = parseInt(String(tgId).replace(/\D/g, ""), 10); if (!id) { setErr("Введи числовой Telegram ID."); return; } setBusy(true); try { await api.adminInvite({ telegram_id: id, note: note.trim() }); setTgId(""); setNote(""); await load(); } catch (e) { setErr("Не удалось пригласить."); } finally { setBusy(false); } }; const revoke = async (id) => { await api.adminRevoke(id); load(); }; return (
setTgId(e.target.value)} inputMode="numeric" placeholder="Telegram ID (напр. 123456789)" className="w-full rounded-lg border border-[var(--border)] bg-[var(--surface-2)] px-3 h-10 text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition mono" /> setNote(e.target.value)} placeholder="имя/заметка (необязательно)" className="w-full rounded-lg border border-[var(--border)] bg-[var(--surface-2)] px-3 h-10 text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition" />
{err &&

{err}

}

Свой Telegram ID можно узнать у @userinfobot.

{data && (
{data.users.map((u) => (
{(u.name || "?").slice(0, 1).toUpperCase()}
{u.name || "Без имени"} {u.is_admin && · админ}{!u.is_allowed && · заблокирован}
ID {u.telegram_id}{u.username ? " · @" + u.username : ""}
{!u.is_admin && ( )}
))} {data.pending.length > 0 && ( <>

Приглашены (ещё не заходили)

{data.pending.map((p) => (
ID {p.telegram_id}
{p.note &&
{p.note}
}
))} )}
)}
); } /* ----------------------------------------------------------- экспорт */ function ExportButton() { const [busy, setBusy] = React.useState(false); const go = async () => { setBusy(true); const data = await api.exportData(); setBusy(false); const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "bloknot-backup-" + new Date().toISOString().slice(0, 10) + ".json"; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); }; return ( ); } /* ----------------------------------------------------------- строка категории */ function CategoryRow({ cat, canDelete, draft, onDone }) { const [editing, setEditing] = React.useState(!!draft); const [label, setLabel] = React.useState(draft ? "" : cat.label); const [color, setColor] = React.useState(draft ? "amber" : cat.color); const [confirmDel, setConfirmDel] = React.useState(false); const [busy, setBusy] = React.useState(false); const save = async () => { if (!label.trim()) return; setBusy(true); if (draft) await Store.addCategory(label.trim(), color); else await Store.editCategory(cat.key, { label: label.trim(), color }); setBusy(false); setEditing(false); onDone && onDone(); }; const del = async () => { setBusy(true); await Store.removeCategory(cat.key); setBusy(false); }; if (editing) { return (
setLabel(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") save(); if (e.key === "Escape") { draft ? onDone() : setEditing(false); } }} placeholder="Название категории" className="flex-1 rounded-lg border border-[var(--border)] bg-[var(--surface-2)] px-3 h-9 text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition" />
{Store.COLORS.map((cl) => (
); } return (
{cat.label} {cat.system && встроенная}
{canDelete && (confirmDel ? (
) : ( ))}
); } /* ----------------------------------------------------------- примитивы настроек */ function Section({ title, desc, children }) { return (

{title}

{desc &&

{desc}

} {!desc &&
} {children}
); } function Row({ label, desc, children }) { return (
{label}
{desc &&
{desc}
}
{children}
); } function Toggle({ on, onClick }) { return ); })} ); } Object.assign(window, { SettingsScreen });