/* 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 (
);
}
/* ----------------------------------------------------------- админ-панель */
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 (
{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 (
);
}
function Toggle({ on, onClick }) {
return ;
}
function Segmented({ value, onChange, options }) {
return (
{options.map((o) => {
const active = value === o.v;
return (
);
})}
);
}
Object.assign(window, { SettingsScreen });