final notif changes
This commit is contained in:
248
src/App.css
248
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 -- */
|
||||
|
||||
@@ -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>
|
||||
|
||||
274
src/components/ClockTimePicker.tsx
Normal file
274
src/components/ClockTimePicker.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
106
src/pages/TermsOfServicePage.tsx
Normal file
106
src/pages/TermsOfServicePage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user