diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a633f70..2c2a900 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -22,7 +22,8 @@ "Bash(npx --yes lighthouse --version)", "Bash(curl:*)", "Bash(npx lighthouse:*)", - "Bash(echo \"exit:$?\")" + "Bash(echo \"exit:$?\")", + "Bash(python -c \"from config import get_settings; s = get_settings\\(\\); print\\('SA JSON set:', bool\\(s.firebase_service_account_json\\)\\)\")" ] } } diff --git a/index.html b/index.html index a6437a1..76dfeaf 100644 --- a/index.html +++ b/index.html @@ -156,7 +156,7 @@
About ยท - Privacy Policy + Privacy Policy
diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js deleted file mode 100644 index 3e85f9a..0000000 --- a/public/firebase-messaging-sw.js +++ /dev/null @@ -1,40 +0,0 @@ -// Firebase Cloud Messaging service worker -// Config values are injected at build time by the Vite plugin (see vite.config.ts) -importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js') -importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js') - -firebase.initializeApp({ - apiKey: '__VITE_FIREBASE_API_KEY__', - authDomain: '__VITE_FIREBASE_AUTH_DOMAIN__', - projectId: '__VITE_FIREBASE_PROJECT_ID__', - messagingSenderId: '__VITE_FIREBASE_MESSAGING_SENDER_ID__', - appId: '__VITE_FIREBASE_APP_ID__', -}) - -const messaging = firebase.messaging() - -// Handle background push messages (browser/PWA is closed or in background) -messaging.onBackgroundMessage((payload) => { - const title = payload.notification?.title || 'Grateful Journal ๐ฑ' - const body = payload.notification?.body || "You haven't written today yet. Take a moment to reflect." - - self.registration.showNotification(title, { - body, - icon: '/web-app-manifest-192x192.png', - badge: '/favicon-96x96.png', - tag: 'gj-daily-reminder', - }) -}) - -self.addEventListener('notificationclick', (e) => { - e.notification.close() - e.waitUntil( - self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => { - if (clients.length > 0) { - clients[0].focus() - return clients[0].navigate('/') - } - return self.clients.openWindow('/') - }) - ) -}) diff --git a/public/sw.js b/public/sw.js index 385b4a6..7f78d91 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,3 +1,29 @@ +// Firebase Messaging โ handles background push notifications +importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js') +importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js') + +firebase.initializeApp({ + apiKey: '__VITE_FIREBASE_API_KEY__', + authDomain: '__VITE_FIREBASE_AUTH_DOMAIN__', + projectId: '__VITE_FIREBASE_PROJECT_ID__', + messagingSenderId: '__VITE_FIREBASE_MESSAGING_SENDER_ID__', + appId: '__VITE_FIREBASE_APP_ID__', +}) + +const messaging = firebase.messaging() + +messaging.onBackgroundMessage((payload) => { + const title = payload.notification?.title || 'Grateful Journal ๐ฑ' + const body = payload.notification?.body || "You haven't written today yet. Take a moment to reflect." + self.registration.showNotification(title, { + body, + icon: '/web-app-manifest-192x192.png', + badge: '/favicon-96x96.png', + tag: 'gj-daily-reminder', + }) +}) + +// Cache management const CACHE = 'gj-__BUILD_TIME__' self.addEventListener('install', (e) => { @@ -23,9 +49,8 @@ self.addEventListener('notificationclick', (e) => { e.waitUntil( self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => { if (clients.length > 0) { - const client = clients[0] - client.focus() - client.navigate('/') + clients[0].focus() + clients[0].navigate('/') } else { self.clients.openWindow('/') } @@ -34,12 +59,7 @@ self.addEventListener('notificationclick', (e) => { }) self.addEventListener('fetch', (e) => { - // Only cache GET requests for same-origin non-API resources - if ( - e.request.method !== 'GET' || - e.request.url.includes('/api/') - ) return - + if (e.request.method !== 'GET' || e.request.url.includes('/api/')) return e.respondWith( caches.match(e.request).then((cached) => cached || fetch(e.request)) ) diff --git a/src/App.css b/src/App.css index 90d8176..f2a3756 100644 --- a/src/App.css +++ b/src/App.css @@ -2750,6 +2750,254 @@ background: #333; } +/* โโ Reminder Modal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ + +.reminder-modal-overlay { + background: rgba(0, 0, 0, 0.85); + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.reminder-modal { + background: #ffffff; + max-width: 340px; + width: 100%; + border-radius: 24px; + max-height: none; + overflow: visible; + padding: 1.25rem 1.25rem 1.5rem; +} + +[data-theme="dark"] .reminder-modal-overlay { + background: rgba(0, 0, 0, 0.88); +} + +[data-theme="dark"] .reminder-modal { + background: #1c1c1e; +} + +/* โโ Clock Time Picker โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ + +.clock-picker { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.6rem; + padding: 0.25rem 0; +} + +.clock-picker__display { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.clock-picker__seg { + background: var(--color-surface-alt, #f3f4f6); + border: none; + border-radius: 10px; + font-size: 2.25rem; + font-weight: 700; + font-family: inherit; + color: var(--color-text, #111827); + width: 3rem; + height: 3.5rem; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.15s, color 0.15s; + line-height: 1; + padding: 0; +} + +.clock-picker__seg--active { + background: var(--color-primary, #22c55e); + color: #fff; +} + +.clock-picker__colon { + font-size: 2rem; + font-weight: 700; + color: var(--color-text, #111827); + line-height: 1; + padding: 0 0.1rem; + user-select: none; +} + +.clock-picker__ampm { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-left: 0.5rem; +} + +.clock-picker__ampm-btn { + background: var(--color-surface-alt, #f3f4f6); + border: 1.5px solid transparent; + border-radius: 8px; + font-size: 0.7rem; + font-weight: 700; + font-family: inherit; + color: var(--color-text-muted, #6b7280); + padding: 0.25rem 0.5rem; + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; + letter-spacing: 0.03em; +} + +.clock-picker__ampm-btn--active { + background: var(--color-primary, #22c55e); + color: #fff; + border-color: var(--color-primary, #22c55e); +} + +.clock-picker__face { + width: 180px; + height: 180px; + display: block; + border-radius: 50%; + overflow: visible; +} + +.clock-picker__bg { + fill: var(--color-surface-alt, #f3f4f6); +} + +.clock-picker__sector { + fill: rgba(34, 197, 94, 0.12); +} + +.clock-picker__hand { + stroke: var(--color-primary, #22c55e); + stroke-width: 2.5; + stroke-linecap: round; +} + +.clock-picker__center-dot { + fill: var(--color-primary, #22c55e); +} + +.clock-picker__hand-tip { + fill: var(--color-primary, #22c55e); +} + +.clock-picker__num { + font-family: inherit; + font-size: 13px; + font-weight: 500; + fill: var(--color-text, #111827); + user-select: none; + pointer-events: none; +} + +.clock-picker__num--selected { + fill: #fff; + font-weight: 700; +} + +.clock-picker__tick { + stroke: var(--color-text-muted, #9ca3af); + stroke-width: 1.5; + opacity: 0.4; +} + +.clock-picker__modes { + display: flex; + gap: 0.5rem; +} + +.clock-picker__mode-btn { + background: none; + border: 1.5px solid var(--color-border, #e5e7eb); + border-radius: 20px; + font-size: 0.7rem; + font-weight: 600; + font-family: inherit; + color: var(--color-text-muted, #6b7280); + padding: 0.25rem 0.75rem; + cursor: pointer; + transition: all 0.15s; +} + +.clock-picker__mode-btn--active { + background: var(--color-primary, #22c55e); + border-color: var(--color-primary, #22c55e); + color: #fff; +} + +/* Dark mode overrides */ +[data-theme="dark"] .clock-picker__seg { + background: #2a2a2a; + color: #f9fafb; +} + +[data-theme="dark"] .clock-picker__seg--active { + background: var(--color-primary, #4ade80); + color: #111; +} + +[data-theme="dark"] .clock-picker__colon { + color: #f9fafb; +} + +[data-theme="dark"] .clock-picker__ampm-btn { + background: #2a2a2a; + color: #9ca3af; +} + +[data-theme="dark"] .clock-picker__ampm-btn--active { + background: var(--color-primary, #4ade80); + color: #111; + border-color: var(--color-primary, #4ade80); +} + +[data-theme="dark"] .clock-picker__bg { + fill: #2a2a2a; +} + +[data-theme="dark"] .clock-picker__sector { + fill: rgba(74, 222, 128, 0.12); +} + +[data-theme="dark"] .clock-picker__hand { + stroke: #4ade80; +} + +[data-theme="dark"] .clock-picker__center-dot { + fill: #4ade80; +} + +[data-theme="dark"] .clock-picker__hand-tip { + fill: #4ade80; +} + +[data-theme="dark"] .clock-picker__num { + fill: #d1d5db; +} + +[data-theme="dark"] .clock-picker__num--selected { + fill: #111; +} + +[data-theme="dark"] .clock-picker__tick { + stroke: #4b5563; +} + +[data-theme="dark"] .clock-picker__mode-btn { + border-color: #333; + color: #9ca3af; +} + +[data-theme="dark"] .clock-picker__mode-btn--active { + background: #4ade80; + border-color: #4ade80; + color: #111; +} + +/* โโ End Clock Time Picker โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ + /* -- Login page โ light mode only, no dark theme overrides -- */ /* -- Google sign-in btn -- */ diff --git a/src/App.tsx b/src/App.tsx index 641f330..704eed9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ const SettingsPage = lazy(() => import('./pages/SettingsPage')) const LoginPage = lazy(() => import('./pages/LoginPage')) const PrivacyPage = lazy(() => import('./pages/PrivacyPage')) const AboutPage = lazy(() => import('./pages/AboutPage')) +const TermsOfServicePage = lazy(() => import('./pages/TermsOfServicePage')) function App() { return ( @@ -44,6 +45,7 @@ function App() { />- You'll get a notification at this time each day if you haven't written yet. - {' '}The reminder stays even if you log out and back in. -
- - - setReminderPickedTime(e.target.value)} + onChange={setReminderPickedTime} disabled={reminderSaving} - autoFocus /> {reminderError && ( @@ -966,17 +958,7 @@ export default function SettingsPage() { )} -- Works best when the app is installed on your home screen. -
- -