This commit is contained in:
2026-02-15 01:51:34 -06:00
parent ba8c6e9050
commit c29ec8e210
6 changed files with 183 additions and 18 deletions

View File

@@ -83,6 +83,9 @@ export default function SessionRunnerPage() {
setCurrentStep(sessionData.current_step);
setCurrentStepIndex(sessionData.session.current_step_index);
setStatus(sessionData.session.status === 'paused' ? 'paused' : 'active');
if (sessionData.session.status !== 'paused') {
setIsTimerRunning(true);
}
// Mark previous steps as completed
const completed = new Set<number>();
@@ -117,7 +120,7 @@ export default function SessionRunnerPage() {
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [isTimerRunning, timerSeconds]);
}, [isTimerRunning]);
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);

View File

@@ -5,6 +5,7 @@ import api from '@/lib/api';
import { VolumeIcon, VolumeOffIcon, SparklesIcon } from '@/components/ui/Icons';
import { playStepComplete } from '@/lib/sounds';
import { hapticTap } from '@/lib/haptics';
import PushNotificationToggle from '@/components/notifications/PushNotificationToggle';
interface Preferences {
sound_enabled: boolean;
@@ -13,6 +14,13 @@ interface Preferences {
celebration_style: string;
}
interface NotifSettings {
discord_webhook: string;
discord_enabled: boolean;
ntfy_topic: string;
ntfy_enabled: boolean;
}
export default function SettingsPage() {
const [prefs, setPrefs] = useState<Preferences>({
sound_enabled: false,
@@ -20,29 +28,57 @@ export default function SettingsPage() {
show_launch_screen: true,
celebration_style: 'standard',
});
const [notif, setNotif] = useState<NotifSettings>({
discord_webhook: '',
discord_enabled: false,
ntfy_topic: '',
ntfy_enabled: false,
});
const [isLoading, setIsLoading] = useState(true);
const [saved, setSaved] = useState(false);
useEffect(() => {
api.preferences.get()
.then((data: Preferences) => setPrefs(data))
Promise.all([
api.preferences.get().then((data: Preferences) => setPrefs(data)),
api.notifications.getSettings().then((data) => setNotif({
discord_webhook: data.discord_webhook,
discord_enabled: data.discord_enabled,
ntfy_topic: data.ntfy_topic,
ntfy_enabled: data.ntfy_enabled,
})),
])
.catch(() => {})
.finally(() => setIsLoading(false));
}, []);
const flashSaved = () => {
setSaved(true);
setTimeout(() => setSaved(false), 1500);
};
const updatePref = async (key: keyof Preferences, value: boolean | string) => {
const updated = { ...prefs, [key]: value };
setPrefs(updated);
try {
await api.preferences.update({ [key]: value });
setSaved(true);
setTimeout(() => setSaved(false), 1500);
flashSaved();
} catch {
// revert on failure
setPrefs(prefs);
}
};
const updateNotif = async (updates: Partial<NotifSettings>) => {
const prev = { ...notif };
const updated = { ...notif, ...updates };
setNotif(updated);
try {
await api.notifications.updateSettings(updates);
flashSaved();
} catch {
setNotif(prev);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[50vh]">
@@ -136,6 +172,75 @@ export default function SettingsPage() {
</div>
</div>
{/* Notifications */}
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">Notifications</h2>
<div className="bg-white rounded-xl shadow-sm divide-y divide-gray-100">
{/* Push Notifications */}
<PushNotificationToggle />
{/* ntfy */}
<div className="p-4 space-y-3">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">ntfy</p>
<p className="text-sm text-gray-500">Push notifications via ntfy.sh</p>
</div>
<button
onClick={() => updateNotif({ ntfy_enabled: !notif.ntfy_enabled })}
className={`w-12 h-7 rounded-full transition-colors ${
notif.ntfy_enabled ? 'bg-indigo-500' : 'bg-gray-300'
}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
notif.ntfy_enabled ? 'translate-x-5' : ''
}`} />
</button>
</div>
{notif.ntfy_enabled && (
<input
type="text"
placeholder="Your ntfy topic ID"
value={notif.ntfy_topic}
onChange={(e) => setNotif({ ...notif, ntfy_topic: e.target.value })}
onBlur={() => updateNotif({ ntfy_topic: notif.ntfy_topic })}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 placeholder-gray-400"
/>
)}
</div>
{/* Discord */}
<div className="p-4 space-y-3">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Discord</p>
<p className="text-sm text-gray-500">Send notifications to a Discord channel</p>
</div>
<button
onClick={() => updateNotif({ discord_enabled: !notif.discord_enabled })}
className={`w-12 h-7 rounded-full transition-colors ${
notif.discord_enabled ? 'bg-indigo-500' : 'bg-gray-300'
}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
notif.discord_enabled ? 'translate-x-5' : ''
}`} />
</button>
</div>
{notif.discord_enabled && (
<input
type="url"
placeholder="Discord webhook URL"
value={notif.discord_webhook}
onChange={(e) => setNotif({ ...notif, discord_webhook: e.target.value })}
onBlur={() => updateNotif({ discord_webhook: notif.discord_webhook })}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 placeholder-gray-400"
/>
)}
</div>
</div>
</div>
{/* Celebration Style */}
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">Celebration Style</h2>

View File

@@ -81,23 +81,21 @@ export default function PushNotificationToggle() {
if (!supported) return null;
return (
<div className="flex items-center justify-between bg-white rounded-xl p-4 shadow-sm">
<div className="flex items-center justify-between p-4">
<div>
<h3 className="font-semibold text-gray-900 text-sm">Push Notifications</h3>
<p className="text-xs text-gray-500">Get reminders on this device</p>
<p className="font-medium text-gray-900">Push notifications</p>
<p className="text-sm text-gray-500">Get reminders on this device</p>
</div>
<button
onClick={toggle}
disabled={loading}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
enabled ? 'bg-indigo-600' : 'bg-gray-300'
className={`w-12 h-7 rounded-full transition-colors ${
enabled ? 'bg-indigo-500' : 'bg-gray-300'
} ${loading ? 'opacity-50' : ''}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
enabled ? 'translate-x-6' : 'translate-x-1'
}`}
/>
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
enabled ? 'translate-x-5' : ''
}`} />
</button>
</div>
);

View File

@@ -646,6 +646,28 @@ export const api = {
body: JSON.stringify({ endpoint }),
});
},
getSettings: async () => {
return request<{
discord_webhook: string;
discord_enabled: boolean;
ntfy_topic: string;
ntfy_enabled: boolean;
web_push_enabled: boolean;
}>('/api/notifications/settings', { method: 'GET' });
},
updateSettings: async (data: {
discord_webhook?: string;
discord_enabled?: boolean;
ntfy_topic?: string;
ntfy_enabled?: boolean;
}) => {
return request<{ updated: boolean }>('/api/notifications/settings', {
method: 'PUT',
body: JSON.stringify(data),
});
},
},
// Medications