final notif changes

This commit is contained in:
2026-04-14 11:10:44 +05:30
parent a1ac8e7933
commit 19dcd73b29
11 changed files with 685 additions and 94 deletions

View File

@@ -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 -- */

View File

@@ -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() {
/>
<Route path="/privacy" element={<PrivacyPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/termsofservice" element={<TermsOfServicePage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>

View File

@@ -0,0 +1,274 @@
import { useState, useRef, useCallback, useEffect } from 'react'
interface Props {
value: string // "HH:MM" 24-hour format
onChange: (value: string) => void
disabled?: boolean
}
const SIZE = 240
const CENTER = SIZE / 2
const CLOCK_RADIUS = 108
const NUM_RADIUS = 82
const HAND_RADIUS = 74
const TIP_RADIUS = 16
function polarToXY(angleDeg: number, radius: number) {
const rad = ((angleDeg - 90) * Math.PI) / 180
return {
x: CENTER + radius * Math.cos(rad),
y: CENTER + radius * Math.sin(rad),
}
}
function parseValue(v: string): { h: number; m: number } {
const [h, m] = v.split(':').map(Number)
return { h: isNaN(h) ? 8 : h, m: isNaN(m) ? 0 : m }
}
export default function ClockTimePicker({ value, onChange, disabled }: Props) {
const { h: initH, m: initM } = parseValue(value)
const [mode, setMode] = useState<'hours' | 'minutes'>('hours')
const [hour24, setHour24] = useState(initH)
const [minute, setMinute] = useState(initM)
const svgRef = useRef<SVGSVGElement>(null)
const isDragging = useRef(false)
// Keep mutable refs for use inside native event listeners
const modeRef = useRef(mode)
const isPMRef = useRef(initH >= 12)
const hour24Ref = useRef(initH)
const minuteRef = useRef(initM)
// Keep refs in sync with state
useEffect(() => { modeRef.current = mode }, [mode])
useEffect(() => { isPMRef.current = hour24 >= 12 }, [hour24])
useEffect(() => { hour24Ref.current = hour24 }, [hour24])
useEffect(() => { minuteRef.current = minute }, [minute])
// Sync when value prop changes externally
useEffect(() => {
const { h, m } = parseValue(value)
setHour24(h)
setMinute(m)
}, [value])
const isPM = hour24 >= 12
const hour12 = hour24 === 0 ? 12 : hour24 > 12 ? hour24 - 12 : hour24
const emit = useCallback(
(h24: number, m: number) => {
onChange(`${h24.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`)
},
[onChange]
)
const handleAmPm = (pm: boolean) => {
if (disabled) return
let newH = hour24
if (pm && hour24 < 12) newH = hour24 + 12
else if (!pm && hour24 >= 12) newH = hour24 - 12
setHour24(newH)
emit(newH, minute)
}
const applyAngle = useCallback(
(angle: number, currentMode: 'hours' | 'minutes') => {
if (currentMode === 'hours') {
const h12 = Math.round(angle / 30) % 12 || 12
const pm = isPMRef.current
const newH24 = pm ? (h12 === 12 ? 12 : h12 + 12) : (h12 === 12 ? 0 : h12)
setHour24(newH24)
emit(newH24, minuteRef.current)
} else {
const m = Math.round(angle / 6) % 60
setMinute(m)
emit(hour24Ref.current, m)
}
},
[emit]
)
const getSVGAngle = (clientX: number, clientY: number): number => {
if (!svgRef.current) return 0
const rect = svgRef.current.getBoundingClientRect()
const scale = rect.width / SIZE
const x = clientX - rect.left - CENTER * scale
const y = clientY - rect.top - CENTER * scale
return ((Math.atan2(y, x) * 180) / Math.PI + 90 + 360) % 360
}
// Mouse handlers (mouse events don't need passive:false)
const handleMouseDown = (e: React.MouseEvent<SVGSVGElement>) => {
if (disabled) return
isDragging.current = true
applyAngle(getSVGAngle(e.clientX, e.clientY), modeRef.current)
}
const handleMouseMove = (e: React.MouseEvent<SVGSVGElement>) => {
if (!isDragging.current || disabled) return
applyAngle(getSVGAngle(e.clientX, e.clientY), modeRef.current)
}
const handleMouseUp = (e: React.MouseEvent<SVGSVGElement>) => {
if (!isDragging.current) return
isDragging.current = false
applyAngle(getSVGAngle(e.clientX, e.clientY), modeRef.current)
if (modeRef.current === 'hours') setTimeout(() => setMode('minutes'), 120)
}
const handleMouseLeave = () => { isDragging.current = false }
// Attach non-passive touch listeners imperatively to avoid the passive warning
useEffect(() => {
const svg = svgRef.current
if (!svg) return
const onTouchStart = (e: TouchEvent) => {
if (disabled) return
e.preventDefault()
isDragging.current = true
const t = e.touches[0]
applyAngle(getSVGAngle(t.clientX, t.clientY), modeRef.current)
}
const onTouchMove = (e: TouchEvent) => {
if (!isDragging.current || disabled) return
e.preventDefault()
const t = e.touches[0]
applyAngle(getSVGAngle(t.clientX, t.clientY), modeRef.current)
}
const onTouchEnd = (e: TouchEvent) => {
if (!isDragging.current) return
e.preventDefault()
isDragging.current = false
const t = e.changedTouches[0]
applyAngle(getSVGAngle(t.clientX, t.clientY), modeRef.current)
if (modeRef.current === 'hours') setTimeout(() => setMode('minutes'), 120)
}
svg.addEventListener('touchstart', onTouchStart, { passive: false })
svg.addEventListener('touchmove', onTouchMove, { passive: false })
svg.addEventListener('touchend', onTouchEnd, { passive: false })
return () => {
svg.removeEventListener('touchstart', onTouchStart)
svg.removeEventListener('touchmove', onTouchMove)
svg.removeEventListener('touchend', onTouchEnd)
}
}, [applyAngle, disabled])
const handAngle = mode === 'hours' ? (hour12 / 12) * 360 : (minute / 60) * 360
const handTip = polarToXY(handAngle, HAND_RADIUS)
const displayH = hour12.toString()
const displayM = minute.toString().padStart(2, '0')
const selectedNum = mode === 'hours' ? hour12 : minute
const hourPositions = Array.from({ length: 12 }, (_, i) => {
const h = i + 1
return { h, ...polarToXY((h / 12) * 360, NUM_RADIUS) }
})
const minutePositions = Array.from({ length: 12 }, (_, i) => {
const m = i * 5
return { m, ...polarToXY((m / 60) * 360, NUM_RADIUS) }
})
return (
<div className="clock-picker">
{/* Time display */}
<div className="clock-picker__display">
<button
type="button"
className={`clock-picker__seg${mode === 'hours' ? ' clock-picker__seg--active' : ''}`}
onClick={() => !disabled && setMode('hours')}
>
{displayH}
</button>
<span className="clock-picker__colon">:</span>
<button
type="button"
className={`clock-picker__seg${mode === 'minutes' ? ' clock-picker__seg--active' : ''}`}
onClick={() => !disabled && setMode('minutes')}
>
{displayM}
</button>
<div className="clock-picker__ampm">
<button
type="button"
className={`clock-picker__ampm-btn${!isPM ? ' clock-picker__ampm-btn--active' : ''}`}
onClick={() => handleAmPm(false)}
disabled={disabled}
>AM</button>
<button
type="button"
className={`clock-picker__ampm-btn${isPM ? ' clock-picker__ampm-btn--active' : ''}`}
onClick={() => handleAmPm(true)}
disabled={disabled}
>PM</button>
</div>
</div>
{/* Clock face */}
<svg
ref={svgRef}
viewBox={`0 0 ${SIZE} ${SIZE}`}
className="clock-picker__face"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
style={{ cursor: disabled ? 'default' : 'pointer', touchAction: 'none', userSelect: 'none' }}
>
<circle cx={CENTER} cy={CENTER} r={CLOCK_RADIUS} className="clock-picker__bg" />
{/* Shaded sector */}
{(() => {
const start = polarToXY(0, HAND_RADIUS)
const end = polarToXY(handAngle, HAND_RADIUS)
const large = handAngle > 180 ? 1 : 0
return (
<path
d={`M ${CENTER} ${CENTER} L ${start.x} ${start.y} A ${HAND_RADIUS} ${HAND_RADIUS} 0 ${large} 1 ${end.x} ${end.y} Z`}
className="clock-picker__sector"
/>
)
})()}
<line x1={CENTER} y1={CENTER} x2={handTip.x} y2={handTip.y} className="clock-picker__hand" />
<circle cx={CENTER} cy={CENTER} r={4} className="clock-picker__center-dot" />
<circle cx={handTip.x} cy={handTip.y} r={TIP_RADIUS} className="clock-picker__hand-tip" />
{mode === 'hours' && hourPositions.map(({ h, x, y }) => (
<text key={h} x={x} y={y} textAnchor="middle" dominantBaseline="central"
className={`clock-picker__num${h === selectedNum ? ' clock-picker__num--selected' : ''}`}
>{h}</text>
))}
{mode === 'minutes' && minutePositions.map(({ m, x, y }) => (
<text key={m} x={x} y={y} textAnchor="middle" dominantBaseline="central"
className={`clock-picker__num${m === selectedNum ? ' clock-picker__num--selected' : ''}`}
>{m.toString().padStart(2, '0')}</text>
))}
{mode === 'minutes' && Array.from({ length: 60 }, (_, i) => {
if (i % 5 === 0) return null
const angle = (i / 60) * 360
const inner = polarToXY(angle, CLOCK_RADIUS - 10)
const outer = polarToXY(angle, CLOCK_RADIUS - 4)
return <line key={i} x1={inner.x} y1={inner.y} x2={outer.x} y2={outer.y} className="clock-picker__tick" />
})}
</svg>
{/* Mode pills */}
<div className="clock-picker__modes">
<button type="button"
className={`clock-picker__mode-btn${mode === 'hours' ? ' clock-picker__mode-btn--active' : ''}`}
onClick={() => !disabled && setMode('hours')}
>Hours</button>
<button type="button"
className={`clock-picker__mode-btn${mode === 'minutes' ? ' clock-picker__mode-btn--active' : ''}`}
onClick={() => !disabled && setMode('minutes')}
>Minutes</button>
</div>
</div>
)
}

View File

@@ -26,14 +26,13 @@ export function isReminderEnabled(): boolean {
return localStorage.getItem(REMINDER_ENABLED_KEY) === 'true'
}
/** Get FCM token using the dedicated firebase-messaging SW. */
/** Get FCM token using the existing sw.js (which includes Firebase messaging). */
async function getFcmToken(): Promise<string | null> {
const messaging = await messagingPromise
if (!messaging) return null
const swReg = await navigator.serviceWorker.register('/firebase-messaging-sw.js', { scope: '/' })
await navigator.serviceWorker.ready
// Use the already-registered sw.js — no second SW needed
const swReg = await navigator.serviceWorker.ready
return getToken(messaging, { vapidKey: VAPID_KEY, serviceWorkerRegistration: swReg })
}

View File

@@ -12,6 +12,7 @@ import {
getSavedReminderTime, isReminderEnabled,
enableReminder, disableReminder, reenableReminder,
} from '../hooks/useReminder'
import ClockTimePicker from '../components/ClockTimePicker'
const MAX_PHOTO_SIZE = 200 // px — resize to 200x200
const MAX_BG_HISTORY = 3
@@ -933,25 +934,16 @@ export default function SettingsPage() {
{/* Daily Reminder Modal */}
{showReminderModal && (
<div className="confirm-modal-overlay" onClick={() => !reminderSaving && setShowReminderModal(false)}>
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
<div style={{ fontSize: '2rem', textAlign: 'center', marginBottom: '0.5rem' }}>🔔</div>
<h3 className="confirm-modal-title">
<div className="confirm-modal-overlay reminder-modal-overlay" onClick={() => !reminderSaving && setShowReminderModal(false)}>
<div className="confirm-modal reminder-modal" onClick={(e) => e.stopPropagation()}>
<div style={{ fontSize: '1.75rem', textAlign: 'center', marginBottom: '0.25rem' }}>🔔</div>
<h3 className="edit-modal-title" style={{ marginBottom: '0.5rem' }}>
{reminderTime ? 'Edit Reminder' : 'Set Daily Reminder'}
</h3>
<p className="confirm-modal-desc">
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.
</p>
<label className="confirm-modal-label">Reminder time</label>
<input
type="time"
className="confirm-modal-input"
<ClockTimePicker
value={reminderPickedTime}
onChange={(e) => setReminderPickedTime(e.target.value)}
onChange={setReminderPickedTime}
disabled={reminderSaving}
autoFocus
/>
{reminderError && (
@@ -966,17 +958,7 @@ export default function SettingsPage() {
</p>
)}
<p style={{
fontSize: '0.75rem',
color: 'var(--color-text-muted)',
marginTop: '0.75rem',
textAlign: 'center',
lineHeight: 1.5,
}}>
Works best when the app is installed on your home screen.
</p>
<div className="confirm-modal-actions" style={{ marginTop: '1rem' }}>
<div className="confirm-modal-actions" style={{ marginTop: '0.75rem' }}>
<button
type="button"
className="confirm-modal-cancel"

View File

@@ -0,0 +1,106 @@
import { Link } from 'react-router-dom'
import { usePageMeta } from '../hooks/usePageMeta'
export default function TermsOfServicePage() {
usePageMeta({
title: 'Terms of Service — Grateful Journal',
description: 'Terms of Service for Grateful Journal. Read about the rules and guidelines for using our app.',
canonical: 'https://gratefuljournal.online/termsofservice',
ogTitle: 'Terms of Service — Grateful Journal',
ogDescription: 'Terms of Service for Grateful Journal. Read about the rules and guidelines for using our app.',
})
return (
<div className="static-page">
<header className="static-page__header">
<Link to="/" className="static-page__logo">🌱 Grateful Journal</Link>
</header>
<main className="static-page__content">
<h1>Terms of Service</h1>
<p className="static-page__updated">Last updated: April 14, 2026</p>
<p>
By using Grateful Journal, you agree to these Terms of Service. Please read them carefully.
</p>
<h2>1. Use of the Service</h2>
<p>
Grateful Journal is a personal journaling app. You may use it for your own personal,
non-commercial journaling purposes. You must be at least 13 years old to use the service.
</p>
<h2>2. Your Account</h2>
<p>
You are responsible for maintaining the security of your account. We use Google Sign-In for
authentication. You agree to provide accurate information and to keep your account credentials
confidential. Notify us immediately if you suspect unauthorized access to your account.
</p>
<h2>3. Your Content</h2>
<p>
You own all journal entries and content you create. We do not claim any ownership over your
content. Your entries are end-to-end encrypted and inaccessible to us. You are solely
responsible for the content you store in the app.
</p>
<h2>4. Prohibited Conduct</h2>
<p>You agree not to:</p>
<ul>
<li>Use the service for any unlawful purpose or in violation of any applicable laws.</li>
<li>Attempt to gain unauthorized access to any part of the service or its infrastructure.</li>
<li>Reverse-engineer, decompile, or otherwise attempt to extract the source code of the app.</li>
<li>Use the service to distribute malware or harmful code.</li>
<li>Abuse or overload the service in a way that impairs its operation for other users.</li>
</ul>
<h2>5. Service Availability</h2>
<p>
We strive to keep Grateful Journal available at all times, but we do not guarantee
uninterrupted access. We may perform maintenance, updates, or changes that temporarily
affect availability. We are not liable for any downtime or data loss.
</p>
<h2>6. Account Termination</h2>
<p>
You may delete your account at any time from the Settings page. Deletion permanently removes
your account and all associated data. We reserve the right to suspend or terminate accounts
that violate these terms.
</p>
<h2>7. Disclaimer of Warranties</h2>
<p>
Grateful Journal is provided "as is" without warranties of any kind, express or implied.
We do not warrant that the service will be error-free, secure, or continuously available.
Use of the service is at your own risk.
</p>
<h2>8. Limitation of Liability</h2>
<p>
To the maximum extent permitted by law, Grateful Journal and its creators shall not be
liable for any indirect, incidental, special, or consequential damages arising from your
use of the service, including loss of data.
</p>
<h2>9. Changes to These Terms</h2>
<p>
We may update these Terms of Service from time to time. We will indicate the date of the
last update at the top of this page. Continued use of the service after changes constitutes
acceptance of the updated terms.
</p>
<h2>10. Contact</h2>
<p>
Questions about these terms? Reach us at the contact details on our <Link to="/about">About page</Link>.
</p>
</main>
<footer className="static-page__footer">
<Link to="/"> Back to Grateful Journal</Link>
<span>·</span>
<Link to="/privacy">Privacy Policy</Link>
<span>·</span>
<Link to="/about">About</Link>
</footer>
</div>
)
}