'use client'; import { useEffect, useState, useRef, useCallback } from 'react'; import { getRecommendation, sendFeedback, getVapidPublicKey, subscribePush } 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; style?: React.CSSProperties; }) { return (
{children}
); } export default function TipPage() { const [tip, setTip] = useState(null); const [state, setState] = useState('loading'); 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); } else { const t = setTimeout(() => setVisible(true), 30); return () => clearTimeout(t); } }, [state]); const loadTip = useCallback(async () => { setVisible(false); setState('loading'); try { const rec = await getRecommendation(); if (!rec) { setState('empty'); return; } setTip(rec.tip); setState('tip'); } catch (err: any) { console.error('[tip] loadTip error', err?.status, err?.message); setState('empty'); } }, []); 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') => { if (!tip) return; const isNavigating = ['done', 'dismiss', 'snooze'].includes(action); if (isNavigating) { setVisible(false); setState('done'); } else { setState('tip'); } await sendFeedback(tip.id, { action }); if (isNavigating) setTimeout(() => loadTip(), 700); }; const onPointerDown = () => { if (state !== 'tip') return; setPressed(true); holdTimer.current = setTimeout(() => { setState('actions'); setVisible(true); setPressed(false); }, 600); }; const onPointerUp = () => { setPressed(false); if (holdTimer.current) { clearTimeout(holdTimer.current); holdTimer.current = null; } }; return ( <>
{/* Ambient glow — breathes while loading */}
{/* Loading label */} {(state === 'loading' || state === 'done') && (

reading you…

)} {/* Tip */} {(state === 'tip' || state === 'actions') && tip && (

{tip.content}

hold to act

{pushState === 'idle' && ( )}
)} {/* Empty */} {state === 'empty' && (

All clear.

)} {/* Action sheet */} {state === 'actions' && ( <>
{ setState('tip'); }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', animation: 'none', }} />
{tip && (

{tip.content}

)} react('done')} primary /> react('helpful')} /> react('not_helpful')} /> react('snooze')} /> react('dismiss')} />
)}
); } function ActionButton({ label, onClick, primary }: { label: string; onClick: () => void; primary?: boolean }) { return ( ); }