Add streaks, today view, custom plan editing/chapter ranges, and ntfy reminders
Build and Push Docker Image / build-and-push (push) Failing after 21s
Build and Push Docker Image / build-and-push (push) Failing after 21s
Reading plans now track a cross-plan activity log for streaks, surface a combined "today's reading" view on both Home and the Reading Plans page, and let custom plans be edited after creation (including narrowing a book to a specific chapter range). The reader auto-marks a plan's day read after a dwell period, active plans that fall behind their pace get a "catch me up" banner, and a new Settings section wires up daily reading reminders via ntfy.sh, checked once a minute by a small server-side scheduler. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
600b3d3ab9
commit
1dd6aef6a5
10 files changed
+711
-141
No files matched your search
@@ -7,6 +7,7 @@ import {
|
||||
disableMfa,
|
||||
changePassword,
|
||||
updateProfile,
|
||||
updateReminderSettings,
|
||||
} from '../syncService.js';
|
||||
|
||||
export default function SettingsPage() {
|
||||
@@ -28,6 +29,15 @@ export default function SettingsPage() {
|
||||
const [changePasswordBusy, setChangePasswordBusy] = useState(false);
|
||||
const [changePasswordError, setChangePasswordError] = useState('');
|
||||
const [changePasswordSaved, setChangePasswordSaved] = useState(false);
|
||||
const [reminderForm, setReminderForm] = useState({
|
||||
reminderEnabled: authUser?.reminderEnabled ?? false,
|
||||
ntfyUrl: authUser?.ntfyUrl ?? '',
|
||||
ntfyTopic: authUser?.ntfyTopic ?? '',
|
||||
reminderTime: authUser?.reminderTime ?? '07:00',
|
||||
});
|
||||
const [reminderBusy, setReminderBusy] = useState(false);
|
||||
const [reminderError, setReminderError] = useState('');
|
||||
const [reminderSaved, setReminderSaved] = useState(false);
|
||||
|
||||
const startSetup = async () => {
|
||||
setMfaSetupError('');
|
||||
@@ -107,6 +117,21 @@ export default function SettingsPage() {
|
||||
window.setTimeout(() => setPodcastNameSaved(false), 2500);
|
||||
};
|
||||
|
||||
const submitReminderSettings = async (e) => {
|
||||
e.preventDefault();
|
||||
setReminderError('');
|
||||
setReminderBusy(true);
|
||||
const result = await updateReminderSettings(reminderForm);
|
||||
setReminderBusy(false);
|
||||
if (!result.ok) {
|
||||
setReminderError(result.error ?? 'Failed to save reminder settings.');
|
||||
return;
|
||||
}
|
||||
setAuthUser((u) => ({ ...u, ...result.data }));
|
||||
setReminderSaved(true);
|
||||
window.setTimeout(() => setReminderSaved(false), 2500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 text-slate-900">
|
||||
<header className="border-b border-slate-200 bg-slate-900 text-white shadow-sm">
|
||||
@@ -217,6 +242,71 @@ export default function SettingsPage() {
|
||||
{podcastNameSaved && <p className="text-sm text-emerald-600">Saved.</p>}
|
||||
</section>
|
||||
|
||||
<section className="rounded-3xl border border-slate-200 bg-white p-8 shadow-panel space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-900">Reading reminders</h2>
|
||||
<p className="text-sm text-slate-500">
|
||||
Get a daily nudge via{' '}
|
||||
<a href="https://ntfy.sh" target="_blank" rel="noreferrer" className="text-sky-600 underline">ntfy.sh</a>
|
||||
{' '}— a free push-notification service. Install the ntfy app (or open{' '}
|
||||
<span className="font-mono text-xs">{(reminderForm.ntfyUrl || 'https://ntfy.sh').replace(/\/+$/, '')}/{reminderForm.ntfyTopic || '<topic>'}</span>
|
||||
{' '}in a browser) and subscribe to the topic below to receive it.
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={submitReminderSettings} className="space-y-3">
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-slate-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={reminderForm.reminderEnabled}
|
||||
onChange={(e) => setReminderForm((f) => ({ ...f, reminderEnabled: e.target.checked }))}
|
||||
className="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500"
|
||||
/>
|
||||
Enable daily reading reminder
|
||||
</label>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<label className="block max-w-xs text-sm font-medium text-slate-700">
|
||||
Topic
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. nate-bible-reading-8f2"
|
||||
value={reminderForm.ntfyTopic}
|
||||
onChange={(e) => setReminderForm((f) => ({ ...f, ntfyTopic: e.target.value }))}
|
||||
className="mt-1 block w-full rounded-xl border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
/>
|
||||
<span className="mt-1 block text-xs text-slate-400">Pick something unique — anyone who knows it can subscribe.</span>
|
||||
</label>
|
||||
<label className="block max-w-xs text-sm font-medium text-slate-700">
|
||||
Reminder time
|
||||
<input
|
||||
type="time"
|
||||
value={reminderForm.reminderTime}
|
||||
onChange={(e) => setReminderForm((f) => ({ ...f, reminderTime: e.target.value }))}
|
||||
className="mt-1 block w-full rounded-xl border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className="block max-w-xs text-sm font-medium text-slate-700">
|
||||
ntfy server <span className="text-slate-400">(optional — defaults to ntfy.sh)</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://ntfy.sh"
|
||||
value={reminderForm.ntfyUrl}
|
||||
onChange={(e) => setReminderForm((f) => ({ ...f, ntfyUrl: e.target.value }))}
|
||||
className="mt-1 block w-full rounded-xl border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
/>
|
||||
</label>
|
||||
{reminderError && <p className="text-sm text-rose-600">{reminderError}</p>}
|
||||
{reminderSaved && <p className="text-sm text-emerald-600">Saved.</p>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={reminderBusy}
|
||||
className="rounded-md bg-sky-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-sky-400 disabled:cursor-not-allowed disabled:bg-slate-300"
|
||||
>
|
||||
{reminderBusy ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className="rounded-3xl border border-slate-200 bg-white p-8 shadow-panel space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-900">Two-factor authentication</h2>
|
||||
|
||||
Reference in new issue
Block a user