diff --git a/apps/web/src/app/config/page.tsx b/apps/web/src/app/config/page.tsx new file mode 100644 index 0000000..fa147ee --- /dev/null +++ b/apps/web/src/app/config/page.tsx @@ -0,0 +1,119 @@ +'use client'; + +import { useEffect, useState, useCallback } from 'react'; +import { getVapidPublicKey, subscribePush } from '@/lib/api'; + +type PushState = 'idle' | 'subscribed' | 'denied'; + +export default function ConfigPage() { + const [pushState, setPushState] = useState('idle'); + + useEffect(() => { + if (typeof Notification !== 'undefined') { + if (Notification.permission === 'granted') setPushState('subscribed'); + else if (Notification.permission === 'denied') setPushState('denied'); + } + }, []); + + const requestPush = useCallback(async () => { + if (!('serviceWorker' in navigator) || !('PushManager' in window)) return; + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { setPushState('denied'); return; } + try { + const reg = await navigator.serviceWorker.register('/sw.js'); + const vapidKey = await getVapidPublicKey(); + const sub = await reg.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: vapidKey, + }); + await subscribePush(sub.toJSON()); + setPushState('subscribed'); + } catch { setPushState('denied'); } + }, []); + + return ( +
+
+ + ← back + +

+ Settings +

+
+ + {/* Notifications */} +
+

+ Notifications +

+
+
+
Push notifications
+
+ {pushState === 'subscribed' ? 'Enabled' : pushState === 'denied' ? 'Blocked by browser' : 'Get notified when a tip is ready'} +
+
+ {pushState === 'idle' && ( + + )} + {pushState === 'subscribed' && ( + + )} +
+
+ + {/* Integrations */} +
+

+ Integrations +

+ +
+
Connected apps
+
+ Manage Todoist and other sources +
+
+ +
+
+
+ ); +} diff --git a/apps/web/src/app/tip/page.tsx b/apps/web/src/app/tip/page.tsx index 68e2a7b..8f82390 100644 --- a/apps/web/src/app/tip/page.tsx +++ b/apps/web/src/app/tip/page.tsx @@ -1,12 +1,11 @@ 'use client'; import { useEffect, useState, useRef, useCallback } from 'react'; -import { getRecommendation, sendFeedback, getVapidPublicKey, subscribePush } from '@/lib/api'; +import { getRecommendation, sendFeedback } from '@/lib/api'; import type { Tip } from '@oo/shared-types'; type State = 'loading' | 'tip' | 'empty' | 'actions' | 'done'; -// Fade wrapper — children fade in when `visible`, fade out when not function Fade({ visible, children, style }: { visible: boolean; children: React.ReactNode; @@ -30,9 +29,7 @@ export default function TipPage() { const [visible, setVisible] = useState(false); const holdTimer = useRef | null>(null); const [pressed, setPressed] = useState(false); - const [pushState, setPushState] = useState<'idle' | 'subscribed' | 'denied'>('idle'); - // Fade in after state change settles useEffect(() => { if (state === 'loading' || state === 'done') { setVisible(false); @@ -61,42 +58,12 @@ export default function TipPage() { useEffect(() => { loadTip(); }, [loadTip]); - // Check existing push permission on mount - useEffect(() => { - if (typeof Notification !== 'undefined' && Notification.permission === 'granted') { - setPushState('subscribed'); - } else if (typeof Notification !== 'undefined' && Notification.permission === 'denied') { - setPushState('denied'); - } - }, []); - - const requestPush = useCallback(async () => { - if (!('serviceWorker' in navigator) || !('PushManager' in window)) return; - const permission = await Notification.requestPermission(); - if (permission !== 'granted') { setPushState('denied'); return; } - try { - const reg = await navigator.serviceWorker.register('/sw.js'); - const vapidKey = await getVapidPublicKey(); - const sub = await reg.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: vapidKey, - }); - await subscribePush(sub.toJSON()); - setPushState('subscribed'); - } catch { setPushState('denied'); } - }, []); - - const react = async (action: 'done' | 'dismiss' | 'snooze' | 'helpful' | 'not_helpful') => { + const react = async (action: 'done' | 'dismiss' | 'snooze') => { if (!tip) return; - const isNavigating = ['done', 'dismiss', 'snooze'].includes(action); - if (isNavigating) { - setVisible(false); - setState('done'); - } else { - setState('tip'); - } + setVisible(false); + setState('done'); await sendFeedback(tip.id, { action }); - if (isNavigating) setTimeout(() => loadTip(), 700); + setTimeout(() => loadTip(), 700); }; const onPointerDown = () => { @@ -119,7 +86,6 @@ export default function TipPage() { return ( <> -