Compare commits

...

2 Commits

Author SHA1 Message Date
816476ed02 added liquid glass theme 2026-04-14 15:26:13 +05:30
6e906436cc update policies 2026-04-14 15:13:15 +05:30
7 changed files with 345 additions and 15 deletions

View File

@@ -1580,6 +1580,12 @@
.settings-theme-dot-dark { .settings-theme-dot-dark {
background: #1a1a1a; background: #1a1a1a;
} }
.settings-theme-dot-glass {
background: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(209,250,229,0.7) 50%, rgba(167,243,208,0.5) 100%);
border: 2px solid rgba(255, 255, 255, 0.8);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.settings-theme-dot:hover:not(:disabled) { .settings-theme-dot:hover:not(:disabled) {
transform: scale(1.15); transform: scale(1.15);
} }
@@ -2997,6 +3003,279 @@
color: #9ca3af; color: #9ca3af;
} }
/* ============================
LIQUID GLASS THEME
============================ */
/* -- Pages must be transparent so body background shows through -- */
[data-theme="liquid-glass"] .home-page,
[data-theme="liquid-glass"] .history-page,
[data-theme="liquid-glass"] .settings-page {
background: transparent;
}
/* -- Glass surface applied to all card/surface elements -- */
[data-theme="liquid-glass"] .journal-card,
[data-theme="liquid-glass"] .calendar-card,
[data-theme="liquid-glass"] .entry-card,
[data-theme="liquid-glass"] .entry-modal,
[data-theme="liquid-glass"] .confirm-modal,
[data-theme="liquid-glass"] .settings-profile,
[data-theme="liquid-glass"] .settings-card,
[data-theme="liquid-glass"] .settings-tutorial-btn,
[data-theme="liquid-glass"] .settings-clear-btn,
[data-theme="liquid-glass"] .settings-signout-btn,
[data-theme="liquid-glass"] .bottom-nav,
[data-theme="liquid-glass"] .lp__form {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: var(--glass-border);
box-shadow: var(--glass-shadow);
}
/* -- Unified shadow (no individual overrides needed) -- */
[data-theme="liquid-glass"] .journal-card,
[data-theme="liquid-glass"] .calendar-card,
[data-theme="liquid-glass"] .entry-card,
[data-theme="liquid-glass"] .settings-profile,
[data-theme="liquid-glass"] .settings-card {
box-shadow: var(--glass-shadow);
}
/* -- Bottom nav glass -- */
[data-theme="liquid-glass"] .bottom-nav {
box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.5), 0 -8px 32px rgba(0, 0, 0, 0.1);
}
/* -- Text colors — dark & crisp for readability on glass -- */
[data-theme="liquid-glass"] .journal-prompt,
[data-theme="liquid-glass"] .settings-header-text h1,
[data-theme="liquid-glass"] .history-header-text h1,
[data-theme="liquid-glass"] .settings-profile-name,
[data-theme="liquid-glass"] .settings-item-title,
[data-theme="liquid-glass"] .calendar-month,
[data-theme="liquid-glass"] .entry-title {
color: #0f172a;
}
[data-theme="liquid-glass"] .journal-date {
color: #16a34a;
}
[data-theme="liquid-glass"] .settings-subtitle,
[data-theme="liquid-glass"] .history-subtitle,
[data-theme="liquid-glass"] .settings-item-subtitle,
[data-theme="liquid-glass"] .settings-section-title,
[data-theme="liquid-glass"] .entry-preview,
[data-theme="liquid-glass"] .entry-date,
[data-theme="liquid-glass"] .entry-time,
[data-theme="liquid-glass"] .recent-entries-title,
[data-theme="liquid-glass"] .calendar-weekday {
color: #334155;
}
[data-theme="liquid-glass"] .journal-title-input,
[data-theme="liquid-glass"] .journal-entry-textarea {
color: #1e293b;
}
[data-theme="liquid-glass"] .journal-title-input::placeholder,
[data-theme="liquid-glass"] .journal-entry-textarea::placeholder {
color: rgba(30, 41, 59, 0.45);
}
[data-theme="liquid-glass"] .journal-title-input {
border-bottom-color: rgba(255, 255, 255, 0.4);
}
[data-theme="liquid-glass"] .journal-title-input:focus {
border-bottom-color: #16a34a;
}
/* -- Settings buttons text -- */
[data-theme="liquid-glass"] .settings-tutorial-btn {
color: #0f172a;
}
[data-theme="liquid-glass"] .settings-clear-btn {
color: #b91c1c;
}
[data-theme="liquid-glass"] .settings-signout-btn {
color: #475569;
}
/* -- Settings buttons hover -- */
[data-theme="liquid-glass"] .settings-tutorial-btn:hover {
background: rgba(255, 255, 255, 0.35);
}
[data-theme="liquid-glass"] .settings-clear-btn:hover {
background: rgba(254, 202, 202, 0.35);
}
[data-theme="liquid-glass"] .settings-signout-btn:hover {
background: rgba(255, 255, 255, 0.3);
color: #1e293b;
}
/* -- Settings item hover -- */
[data-theme="liquid-glass"] .settings-item-button:hover {
background: rgba(255, 255, 255, 0.28);
}
/* -- Settings divider -- */
[data-theme="liquid-glass"] .settings-divider {
background: rgba(255, 255, 255, 0.45);
}
/* -- Settings theme dot -- */
[data-theme="liquid-glass"] .settings-theme-dot {
border-color: rgba(0, 0, 0, 0.12);
}
[data-theme="liquid-glass"] .settings-theme-dot-active {
border-color: #16a34a;
box-shadow: 0 0 0 2px #16a34a;
}
/* -- Settings icon backgrounds -- */
[data-theme="liquid-glass"] .settings-item-icon-green {
background: rgba(34, 197, 94, 0.2);
color: #15803d;
}
[data-theme="liquid-glass"] .settings-item-icon-gray {
background: rgba(100, 116, 139, 0.18);
color: #475569;
}
[data-theme="liquid-glass"] .settings-item-icon-orange {
background: rgba(251, 146, 60, 0.2);
color: #c2410c;
}
[data-theme="liquid-glass"] .settings-item-icon-blue {
background: rgba(59, 130, 246, 0.18);
color: #1d4ed8;
}
[data-theme="liquid-glass"] .settings-item-icon-purple {
background: rgba(139, 92, 246, 0.18);
color: #6d28d9;
}
/* -- Settings misc -- */
[data-theme="liquid-glass"] .settings-toggle-slider {
background: rgba(0, 0, 0, 0.2);
}
[data-theme="liquid-glass"] .settings-toggle input:checked + .settings-toggle-slider {
background: #16a34a;
}
[data-theme="liquid-glass"] .settings-item-arrow {
color: #334155;
}
[data-theme="liquid-glass"] .settings-enc {
color: rgba(15, 23, 42, 0.45);
}
[data-theme="liquid-glass"] .settings-edit-btn {
background: rgba(34, 197, 94, 0.2);
color: #15803d;
}
/* -- Calendar -- */
[data-theme="liquid-glass"] .calendar-day {
color: #334155;
}
[data-theme="liquid-glass"] .calendar-day:not(.calendar-day-empty):hover {
background: rgba(255, 255, 255, 0.4);
color: #0f172a;
}
[data-theme="liquid-glass"] .calendar-day-has-entry {
background: rgba(34, 197, 94, 0.22);
color: #15803d;
}
[data-theme="liquid-glass"] .calendar-day-today {
background: #16a34a;
color: #fff;
}
[data-theme="liquid-glass"] .calendar-nav-btn:hover {
background: rgba(255, 255, 255, 0.35);
color: #0f172a;
}
/* -- Entry card -- */
[data-theme="liquid-glass"] .entry-card {
border-left-color: rgba(22, 163, 74, 0.5);
}
[data-theme="liquid-glass"] .entry-card:hover {
background: rgba(255, 255, 255, 0.28);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.14), 0 1px 0 rgba(255, 255, 255, 0.7) inset;
}
/* -- Entry modal -- */
[data-theme="liquid-glass"] .entry-modal {
border-top-color: #16a34a;
}
[data-theme="liquid-glass"] .entry-modal-title {
color: #0f172a;
}
[data-theme="liquid-glass"] .entry-modal-content {
color: #1e293b;
}
[data-theme="liquid-glass"] .entry-modal-date,
[data-theme="liquid-glass"] .entry-modal-time {
color: #475569;
}
[data-theme="liquid-glass"] .entry-modal-badge {
background: rgba(34, 197, 94, 0.2);
color: #15803d;
}
[data-theme="liquid-glass"] .entry-modal-close {
background: rgba(255, 255, 255, 0.35);
color: #475569;
}
[data-theme="liquid-glass"] .entry-modal-close:hover {
background: rgba(255, 255, 255, 0.5);
color: #0f172a;
}
/* -- Confirm/modal overlays -- */
[data-theme="liquid-glass"] .confirm-modal-overlay,
[data-theme="liquid-glass"] .entry-modal-overlay {
background: rgba(15, 23, 42, 0.2);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
/* -- Bottom nav -- */
[data-theme="liquid-glass"] .bottom-nav-btn {
color: #475569;
}
[data-theme="liquid-glass"] .bottom-nav-btn:hover {
color: #15803d;
background: rgba(255, 255, 255, 0.3);
}
[data-theme="liquid-glass"] .bottom-nav-btn-active {
background: #16a34a;
color: #fff;
}
/* -- Write button -- */
[data-theme="liquid-glass"] .journal-write-btn {
background: #16a34a;
box-shadow: 0 4px 16px rgba(22, 163, 74, 0.35);
}
[data-theme="liquid-glass"] .journal-write-btn:hover:not(:disabled) {
background: #15803d;
box-shadow: 0 6px 24px rgba(22, 163, 74, 0.45);
}
/* -- Desktop sidebar nav glass -- */
@media (min-width: 860px) {
[data-theme="liquid-glass"] .bottom-nav {
background: var(--glass-bg);
border-right-color: rgba(255, 255, 255, 0.4);
}
[data-theme="liquid-glass"] .bottom-nav-brand {
border-bottom-color: rgba(255, 255, 255, 0.4);
}
}
[data-theme="dark"] .clock-picker__mode-btn--active { [data-theme="dark"] .clock-picker__mode-btn--active {
background: #4ade80; background: #4ade80;
border-color: #4ade80; border-color: #4ade80;

View File

@@ -96,3 +96,28 @@ button:focus-visible {
[data-theme="dark"] body { [data-theme="dark"] body {
background: #0a0a0a; background: #0a0a0a;
} }
/* ── Liquid Glass theme root overrides ───────────────────── */
[data-theme="liquid-glass"] {
--glass-bg: rgba(255, 255, 255, 0.18);
--glass-blur: blur(28px) saturate(200%) brightness(1.05);
--glass-border: 1px solid rgba(255, 255, 255, 0.55);
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 1px 0 rgba(255, 255, 255, 0.7) inset;
--color-primary: #16a34a;
--color-primary-hover: #15803d;
--color-bg-soft: transparent;
--color-surface: var(--glass-bg);
--color-accent-light: rgba(220, 252, 231, 0.4);
--color-text: #0f172a;
--color-text-muted: #334155;
--color-border: rgba(255, 255, 255, 0.4);
color: var(--color-text);
background-color: transparent;
caret-color: #16a34a;
}
/* Same bg as light theme when no custom image is set */
[data-theme="liquid-glass"] body:not(.gj-has-bg) {
background: #eef6ee;
}

View File

@@ -4,6 +4,10 @@ import './index.css'
import App from './App.tsx' import App from './App.tsx'
import { listenForegroundMessages } from './hooks/useReminder' import { listenForegroundMessages } from './hooks/useReminder'
// Apply saved theme immediately to avoid flash
const savedTheme = localStorage.getItem('gj-theme') || 'light'
document.documentElement.setAttribute('data-theme', savedTheme)
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', () => { window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js') navigator.serviceWorker.register('/sw.js')

View File

@@ -30,7 +30,7 @@ export default function AboutPage() {
<h2>Features</h2> <h2>Features</h2>
<ul> <ul>
<li><strong>End-to-end encrypted</strong> — your entries are encrypted before leaving your device. We cannot read them.</li> <li><strong>End-to-end encrypted entries</strong> — your journal content is encrypted before leaving your device. We cannot read it.</li>
<li><strong>No ads, no tracking</strong> — we don't sell your data or show you ads.</li> <li><strong>No ads, no tracking</strong> — we don't sell your data or show you ads.</li>
<li><strong>Works offline</strong> installable as a PWA on Android, iOS, and desktop.</li> <li><strong>Works offline</strong> installable as a PWA on Android, iOS, and desktop.</li>
<li><strong>Daily prompts</strong> gentle nudges to keep your practice consistent.</li> <li><strong>Daily prompts</strong> gentle nudges to keep your practice consistent.</li>
@@ -48,7 +48,10 @@ export default function AboutPage() {
<h2>Privacy first</h2> <h2>Privacy first</h2>
<p> <p>
We built Grateful Journal because we believe your inner thoughts deserve a private space. We built Grateful Journal because we believe your inner thoughts deserve a private space.
Read our full <Link to="/privacy">Privacy Policy</Link> to understand exactly how your data is protected. Your journal entries are end-to-end encrypted — only you can read them. App preferences
such as your display name, profile photo, and background images are stored as plain account
settings and are not encrypted. Read our full <Link to="/privacy">Privacy Policy</Link> for
a complete breakdown of what is and isn't encrypted.
</p> </p>
</main> </main>

View File

@@ -4,10 +4,10 @@ import { usePageMeta } from '../hooks/usePageMeta'
export default function PrivacyPage() { export default function PrivacyPage() {
usePageMeta({ usePageMeta({
title: 'Privacy Policy — Grateful Journal', title: 'Privacy Policy — Grateful Journal',
description: 'Grateful Journal\'s privacy policy. Your journal entries are end-to-end encrypted — we cannot read them. No ads, no tracking, no data selling.', description: 'Grateful Journal\'s privacy policy. Your journal entries are end-to-end encrypted — we cannot read them. App preferences are stored unencrypted. No ads, no tracking.',
canonical: 'https://gratefuljournal.online/privacy', canonical: 'https://gratefuljournal.online/privacy',
ogTitle: 'Privacy Policy — Grateful Journal', ogTitle: 'Privacy Policy — Grateful Journal',
ogDescription: 'Your journal entries are end-to-end encrypted and private. We cannot read them, we don\'t sell your data, and we use no advertising cookies.', ogDescription: 'Your journal entries are end-to-end encrypted and private. App preferences like background images are stored unencrypted. No ads, no tracking, no data selling.',
}) })
return ( return (
<div className="static-page"> <div className="static-page">
@@ -17,7 +17,7 @@ export default function PrivacyPage() {
<main className="static-page__content"> <main className="static-page__content">
<h1>Privacy Policy</h1> <h1>Privacy Policy</h1>
<p className="static-page__updated">Last updated: April 8, 2026</p> <p className="static-page__updated">Last updated: April 14, 2026</p>
<p> <p>
Grateful Journal is built on a simple promise: your journal entries are yours alone. Grateful Journal is built on a simple promise: your journal entries are yours alone.
@@ -28,13 +28,21 @@ export default function PrivacyPage() {
<ul> <ul>
<li><strong>Account info</strong> your name and email address via Google Sign-In, used solely to identify your account.</li> <li><strong>Account info</strong> your name and email address via Google Sign-In, used solely to identify your account.</li>
<li><strong>Journal entries</strong> stored encrypted in our database. We do not have access to the content of your entries.</li> <li><strong>Journal entries</strong> stored encrypted in our database. We do not have access to the content of your entries.</li>
<li><strong>App preferences</strong> your display name, profile photo, background images, and theme are stored unencrypted as account settings. See the Encryption section below for the full breakdown.</li>
<li><strong>Usage data</strong> no analytics, no tracking pixels, no third-party advertising SDKs.</li> <li><strong>Usage data</strong> no analytics, no tracking pixels, no third-party advertising SDKs.</li>
</ul> </ul>
<h2>Encryption</h2> <h2>Encryption</h2>
<p> <p>
Your journal entries are end-to-end encrypted. They are encrypted on your device before being sent to our servers. Encryption is applied selectively based on the sensitivity of each type of data:
We store only the encrypted ciphertext decryption happens locally in your browser using your account key. </p>
<ul>
<li><strong>Journal entries end-to-end encrypted.</strong> Entries are encrypted on your device using XSalsa20-Poly1305 before being sent to our servers. We store only ciphertext. Decryption happens locally in your browser using a key derived from your account. We cannot read your entries.</li>
<li><strong>App preferences not encrypted.</strong> Your display name, profile photo, background images, and theme setting are stored as plain data. These are appearance and account settings, not personal journal content. They are accessible to us at the database level.</li>
</ul>
<p>
If you upload a personal photo as a background image, be aware that it is stored unencrypted on our servers.
For maximum privacy, use abstract or non-personal images as backgrounds.
</p> </p>
<h2>Data sharing</h2> <h2>Data sharing</h2>

View File

@@ -54,8 +54,8 @@ export default function SettingsPage() {
const { user, userId, mongoUser, signOut, loading, refreshMongoUser } = useAuth() const { user, userId, mongoUser, signOut, loading, refreshMongoUser } = useAuth()
// const [passcodeEnabled, setPasscodeEnabled] = useState(false) // Passcode lock — disabled for now // const [passcodeEnabled, setPasscodeEnabled] = useState(false) // Passcode lock — disabled for now
// const [faceIdEnabled, setFaceIdEnabled] = useState(false) // Face ID — disabled for now // const [faceIdEnabled, setFaceIdEnabled] = useState(false) // Face ID — disabled for now
const [theme, setTheme] = useState<'light' | 'dark'>(() => { const [theme, setTheme] = useState<'light' | 'dark' | 'liquid-glass'>(() => {
return (localStorage.getItem('gj-theme') as 'light' | 'dark') || 'light' return (localStorage.getItem('gj-theme') as 'light' | 'dark' | 'liquid-glass') || 'light'
}) })
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
@@ -229,7 +229,7 @@ export default function SettingsPage() {
} }
// Apply theme to DOM // Apply theme to DOM
const applyTheme = useCallback((t: 'light' | 'dark') => { const applyTheme = useCallback((t: 'light' | 'dark' | 'liquid-glass') => {
document.documentElement.setAttribute('data-theme', t) document.documentElement.setAttribute('data-theme', t)
localStorage.setItem('gj-theme', t) localStorage.setItem('gj-theme', t)
}, []) }, [])
@@ -239,10 +239,11 @@ export default function SettingsPage() {
applyTheme(theme) applyTheme(theme)
}, [theme, applyTheme]) }, [theme, applyTheme])
const handleThemeChange = (newTheme: 'light' | 'dark') => { const handleThemeChange = (newTheme: 'light' | 'dark' | 'liquid-glass') => {
setTheme(newTheme) setTheme(newTheme)
applyTheme(newTheme) applyTheme(newTheme)
setMessage({ type: 'success', text: `Switched to ${newTheme === 'light' ? 'Light' : 'Dark'} theme` }) const label = newTheme === 'light' ? 'Light' : newTheme === 'dark' ? 'Dark' : 'Liquid Glass'
setMessage({ type: 'success', text: `Switched to ${label} theme` })
setTimeout(() => setMessage(null), 2000) setTimeout(() => setMessage(null), 2000)
} }
@@ -560,7 +561,9 @@ export default function SettingsPage() {
</div> </div>
<div className="settings-item-content"> <div className="settings-item-content">
<h4 className="settings-item-title">Theme</h4> <h4 className="settings-item-title">Theme</h4>
<p className="settings-item-subtitle">Currently: {theme === 'light' ? 'Light' : 'Dark'}</p> <p className="settings-item-subtitle">
Currently: {theme === 'light' ? 'Light' : theme === 'dark' ? 'Dark' : 'Liquid Glass'}
</p>
</div> </div>
<div className="settings-theme-colors"> <div className="settings-theme-colors">
<button <button
@@ -575,6 +578,12 @@ export default function SettingsPage() {
className={`settings-theme-dot settings-theme-dot-dark${theme === 'dark' ? ' settings-theme-dot-active' : ''}`} className={`settings-theme-dot settings-theme-dot-dark${theme === 'dark' ? ' settings-theme-dot-active' : ''}`}
title="Dark theme" title="Dark theme"
></button> ></button>
<button
type="button"
onClick={() => handleThemeChange('liquid-glass')}
className={`settings-theme-dot settings-theme-dot-glass${theme === 'liquid-glass' ? ' settings-theme-dot-active' : ''}`}
title="Liquid Glass theme"
></button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -39,8 +39,10 @@ export default function TermsOfServicePage() {
<h2>3. Your Content</h2> <h2>3. Your Content</h2>
<p> <p>
You own all journal entries and content you create. We do not claim any ownership over your 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 content. Your journal entries are end-to-end encrypted and inaccessible to us. App preferences
responsible for the content you store in the app. such as your display name, profile photo, and background images are stored as plain account
settings and are accessible to us at the database level. You are solely responsible for the
content you store in the app, including any images you upload as backgrounds.
</p> </p>
<h2>4. Prohibited Conduct</h2> <h2>4. Prohibited Conduct</h2>