Compare commits
4 Commits
816476ed02
...
85477e5499
| Author | SHA1 | Date | |
|---|---|---|---|
| 85477e5499 | |||
| 7f06fa347a | |||
| 11940678f7 | |||
| bf7245d6d1 |
115
about.html
Normal file
115
about.html
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" style="background-color:#eef6ee">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Grateful Journal" />
|
||||||
|
<meta name="theme-color" content="#16a34a" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- SEO -->
|
||||||
|
<title>About Grateful Journal | Private, Encrypted Gratitude Journaling</title>
|
||||||
|
<meta name="description" content="Learn about Grateful Journal — a free, end-to-end encrypted daily gratitude journal. No ads, no tracking, no social feed. Just you and your thoughts." />
|
||||||
|
<meta name="keywords" content="about grateful journal, private gratitude journal, encrypted journal app, gratitude journaling, mindfulness app" />
|
||||||
|
<meta name="robots" content="index, follow, max-snippet:160, max-image-preview:large" />
|
||||||
|
<link rel="canonical" href="https://gratefuljournal.online/about" />
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content="en_US" />
|
||||||
|
<meta property="og:url" content="https://gratefuljournal.online/about" />
|
||||||
|
<meta property="og:title" content="About Grateful Journal | Private, Encrypted Gratitude Journaling" />
|
||||||
|
<meta property="og:description" content="A free, private gratitude journal with end-to-end encryption. Learn how we built a distraction-free space for your daily reflection practice." />
|
||||||
|
<meta property="og:image" content="https://gratefuljournal.online/web-app-manifest-512x512.png" />
|
||||||
|
<meta property="og:image:width" content="512" />
|
||||||
|
<meta property="og:image:height" content="512" />
|
||||||
|
<meta property="og:image:alt" content="Grateful Journal logo - a green sprout" />
|
||||||
|
<meta property="og:site_name" content="Grateful Journal" />
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="About Grateful Journal | Private, Encrypted Gratitude Journaling" />
|
||||||
|
<meta name="twitter:description" content="A free, private gratitude journal with end-to-end encryption. No ads, no tracking, no social feed." />
|
||||||
|
<meta name="twitter:image" content="https://gratefuljournal.online/web-app-manifest-512x512.png" />
|
||||||
|
<meta name="twitter:image:alt" content="Grateful Journal logo - a green sprout" />
|
||||||
|
|
||||||
|
<!-- JSON-LD: WebPage -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "AboutPage",
|
||||||
|
"name": "About Grateful Journal",
|
||||||
|
"url": "https://gratefuljournal.online/about",
|
||||||
|
"description": "Learn about Grateful Journal — a free, end-to-end encrypted daily gratitude journal. No ads, no tracking, no social feed.",
|
||||||
|
"isPartOf": {
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "Grateful Journal",
|
||||||
|
"url": "https://gratefuljournal.online/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- JSON-LD: Organization -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Grateful Journal",
|
||||||
|
"url": "https://gratefuljournal.online/",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "https://gratefuljournal.online/web-app-manifest-512x512.png",
|
||||||
|
"width": 512,
|
||||||
|
"height": 512
|
||||||
|
},
|
||||||
|
"description": "A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts.",
|
||||||
|
"sameAs": []
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<noscript>
|
||||||
|
<main style="font-family:sans-serif;max-width:680px;margin:4rem auto;padding:1rem 1.5rem;color:#1a1a1a;line-height:1.7">
|
||||||
|
<nav style="margin-bottom:2rem"><a href="/" style="color:#15803d">← Grateful Journal</a></nav>
|
||||||
|
|
||||||
|
<h1 style="color:#15803d">About Grateful Journal</h1>
|
||||||
|
<p style="font-size:1.1rem">A private space for gratitude and reflection. No feeds. No noise. Just you and your thoughts.</p>
|
||||||
|
|
||||||
|
<h2>What is it?</h2>
|
||||||
|
<p>Grateful Journal is a free, end-to-end encrypted daily journal focused on gratitude. You write a few things you're grateful for each day, and over time you build a private record of the good in your life — visible only to you.</p>
|
||||||
|
|
||||||
|
<h2>Features</h2>
|
||||||
|
<ul>
|
||||||
|
<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>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>History view</strong> — browse past entries and reflect on how far you've come.</li>
|
||||||
|
<li><strong>Free to use</strong> — no subscription, no paywall.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Why gratitude?</h2>
|
||||||
|
<p>Research consistently shows that a regular gratitude practice improves mood, reduces stress, and builds resilience. Grateful Journal gives you the simplest possible tool to build that habit — without distractions or social pressure.</p>
|
||||||
|
|
||||||
|
<h2>Privacy first</h2>
|
||||||
|
<p>We built Grateful Journal because we believe your inner thoughts deserve a private space. 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 <a href="/privacy">Privacy Policy</a> for a complete breakdown of what is and isn't encrypted.</p>
|
||||||
|
|
||||||
|
<nav style="margin-top:2rem">
|
||||||
|
<a href="/">← Back to Grateful Journal</a> ·
|
||||||
|
<a href="/privacy">Privacy Policy</a>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
</noscript>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
71
index.html
71
index.html
@@ -17,7 +17,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- SEO -->
|
<!-- SEO -->
|
||||||
<title>Grateful Journal — Your Private Gratitude Journal</title>
|
<title>Private Gratitude Journal App | Grateful Journal</title>
|
||||||
<meta name="description" content="A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts. Grow your gratitude one moment at a time." />
|
<meta name="description" content="A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts. Grow your gratitude one moment at a time." />
|
||||||
<meta name="keywords" content="gratitude journal, private journal, encrypted journal, daily gratitude, mindfulness, reflection" />
|
<meta name="keywords" content="gratitude journal, private journal, encrypted journal, daily gratitude, mindfulness, reflection" />
|
||||||
<meta name="robots" content="index, follow, max-snippet:160, max-image-preview:large" />
|
<meta name="robots" content="index, follow, max-snippet:160, max-image-preview:large" />
|
||||||
@@ -27,20 +27,20 @@
|
|||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:locale" content="en_US" />
|
<meta property="og:locale" content="en_US" />
|
||||||
<meta property="og:url" content="https://gratefuljournal.online/" />
|
<meta property="og:url" content="https://gratefuljournal.online/" />
|
||||||
<meta property="og:title" content="Grateful Journal — Your Private Gratitude Journal" />
|
<meta property="og:title" content="Private Gratitude Journal App | Grateful Journal" />
|
||||||
<meta property="og:description" content="A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts." />
|
<meta property="og:description" content="A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts." />
|
||||||
<meta property="og:image" content="https://gratefuljournal.online/web-app-manifest-512x512.png" />
|
<meta property="og:image" content="https://gratefuljournal.online/web-app-manifest-512x512.png" />
|
||||||
<meta property="og:image:width" content="512" />
|
<meta property="og:image:width" content="512" />
|
||||||
<meta property="og:image:height" content="512" />
|
<meta property="og:image:height" content="512" />
|
||||||
<meta property="og:image:alt" content="Grateful Journal logo — a green sprout" />
|
<meta property="og:image:alt" content="Grateful Journal logo - a green sprout" />
|
||||||
<meta property="og:site_name" content="Grateful Journal" />
|
<meta property="og:site_name" content="Grateful Journal" />
|
||||||
|
|
||||||
<!-- Twitter Card -->
|
<!-- Twitter Card -->
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="Grateful Journal — Your Private Gratitude Journal" />
|
<meta name="twitter:title" content="Private Gratitude Journal App | Grateful Journal" />
|
||||||
<meta name="twitter:description" content="A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts." />
|
<meta name="twitter:description" content="A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts." />
|
||||||
<meta name="twitter:image" content="https://gratefuljournal.online/web-app-manifest-512x512.png" />
|
<meta name="twitter:image" content="https://gratefuljournal.online/web-app-manifest-512x512.png" />
|
||||||
<meta name="twitter:image:alt" content="Grateful Journal logo — a green sprout" />
|
<meta name="twitter:image:alt" content="Grateful Journal logo - a green sprout" />
|
||||||
|
|
||||||
<!-- JSON-LD: WebSite -->
|
<!-- JSON-LD: WebSite -->
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
@@ -140,24 +140,55 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<noscript>
|
<noscript>
|
||||||
<main style="font-family:sans-serif;max-width:640px;margin:4rem auto;padding:1rem;color:#1a1a1a">
|
<main style="font-family:sans-serif;max-width:680px;margin:4rem auto;padding:1rem 1.5rem;color:#1a1a1a;line-height:1.7">
|
||||||
<h1>Grateful Journal — Your Private Gratitude Journal</h1>
|
<h1 style="color:#15803d">Grateful Journal - Your Private Gratitude Journal</h1>
|
||||||
<p>A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts. Grow your gratitude one moment at a time.</p>
|
<p style="font-size:1.1rem">A free, private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts. Grow your gratitude one moment at a time.</p>
|
||||||
<h2>Features</h2>
|
|
||||||
|
<h2>What is Grateful Journal?</h2>
|
||||||
|
<p>Grateful Journal is a daily gratitude journaling app built for people who value privacy. You write a few things you're grateful for each day, and over time you build a private record of the good in your life — visible only to you. No social pressure, no algorithms, no distractions.</p>
|
||||||
|
|
||||||
|
<h2>Key Features</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>End-to-end encrypted journal entries — only you can read them</li>
|
<li><strong>End-to-end encrypted entries</strong> — your journal content is encrypted on your device before it reaches our servers. We cannot read it.</li>
|
||||||
<li>Daily gratitude prompts to keep you consistent</li>
|
<li><strong>No ads, no tracking</strong> — we do not sell your data, show ads, or use tracking pixels of any kind.</li>
|
||||||
<li>No ads, no tracking, no social feed</li>
|
<li><strong>Works offline</strong> — installable as a Progressive Web App (PWA) on Android, iOS, and desktop. Write even without an internet connection.</li>
|
||||||
<li>Works offline as a Progressive Web App (PWA)</li>
|
<li><strong>Daily gratitude prompts</strong> — gentle nudges to keep your reflection practice consistent.</li>
|
||||||
<li>Free to use</li>
|
<li><strong>History view</strong> — browse past entries and see how far you've come.</li>
|
||||||
|
<li><strong>Completely free</strong> — no subscription, no paywall, no hidden fees.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>How it works</h2>
|
|
||||||
<p>Sign in with Google, write a few things you're grateful for each day, and watch your mindset shift over time. Your entries are encrypted before they leave your device.</p>
|
<h2>Why a Private Gratitude Journal?</h2>
|
||||||
<p><a href="https://gratefuljournal.online/">Get started — it's free</a></p>
|
<p>Research consistently shows that a regular gratitude practice improves mood, reduces stress, and builds resilience. But most journaling apps either sell your data or make your entries visible in social feeds. Grateful Journal gives you the simplest possible tool to build the gratitude habit — with your privacy as a non-negotiable foundation.</p>
|
||||||
<p>
|
|
||||||
|
<h2>How Encryption Works</h2>
|
||||||
|
<p>Your journal entries are encrypted using XSalsa20-Poly1305 before leaving your device. The encryption key is derived from your account and never sent to our servers. We store only ciphertext — even a database breach would expose nothing readable. App preferences like your display name and theme are stored as plain settings, not journal content.</p>
|
||||||
|
|
||||||
|
<h2>Who Is It For?</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Privacy-conscious users who want a digital journal without surveillance</li>
|
||||||
|
<li>People building a daily gratitude or mindfulness practice</li>
|
||||||
|
<li>Anyone who wants a distraction-free space for daily reflection</li>
|
||||||
|
<li>Users looking for a free, encrypted alternative to Day One or Notion</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Frequently Asked Questions</h2>
|
||||||
|
<dl>
|
||||||
|
<dt><strong>Is Grateful Journal free?</strong></dt>
|
||||||
|
<dd>Yes, completely free. No subscription, no paywall.</dd>
|
||||||
|
<dt><strong>Are my entries private?</strong></dt>
|
||||||
|
<dd>Yes. Entries are end-to-end encrypted. Even we cannot read them.</dd>
|
||||||
|
<dt><strong>Does it work offline?</strong></dt>
|
||||||
|
<dd>Yes. Install it as a PWA on Android, iOS, or desktop for offline access.</dd>
|
||||||
|
<dt><strong>Do you sell data or show ads?</strong></dt>
|
||||||
|
<dd>No. We do not sell data, show ads, or use any tracking.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<p><a href="https://gratefuljournal.online/" style="color:#15803d;font-weight:bold">Get started — it's free</a></p>
|
||||||
|
<nav>
|
||||||
<a href="/about">About</a> ·
|
<a href="/about">About</a> ·
|
||||||
<a href="/privacypolicy">Privacy Policy</a>
|
<a href="/privacy">Privacy Policy</a> ·
|
||||||
</p>
|
<a href="/termsofservice">Terms of Service</a>
|
||||||
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
</noscript>
|
</noscript>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
|||||||
@@ -59,12 +59,26 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Known SPA routes — serve index.html
|
# Homepage
|
||||||
location = / {
|
location = / {
|
||||||
try_files /index.html =404;
|
try_files /index.html =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ ^/(write|history|settings|privacy|about)(/|$) {
|
# Pre-rendered public pages — each gets its own HTML with correct meta tags
|
||||||
|
location ~ ^/about(/|$) {
|
||||||
|
try_files /about.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/privacy(/|$) {
|
||||||
|
try_files /privacy.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/termsofservice(/|$) {
|
||||||
|
try_files /termsofservice.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Protected SPA routes — serve index.html (React handles auth redirect)
|
||||||
|
location ~ ^/(write|history|settings)(/|$) {
|
||||||
try_files /index.html =404;
|
try_files /index.html =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
103
privacy.html
Normal file
103
privacy.html
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" style="background-color:#eef6ee">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Grateful Journal" />
|
||||||
|
<meta name="theme-color" content="#16a34a" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- SEO -->
|
||||||
|
<title>Privacy Policy | Grateful Journal</title>
|
||||||
|
<meta name="description" content="Grateful Journal's privacy policy. Your journal entries are end-to-end encrypted — we cannot read them. No ads, no tracking, no data selling." />
|
||||||
|
<meta name="keywords" content="grateful journal privacy policy, encrypted journal, private journal app, data privacy" />
|
||||||
|
<meta name="robots" content="index, follow, max-snippet:160, max-image-preview:large" />
|
||||||
|
<link rel="canonical" href="https://gratefuljournal.online/privacy" />
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content="en_US" />
|
||||||
|
<meta property="og:url" content="https://gratefuljournal.online/privacy" />
|
||||||
|
<meta property="og:title" content="Privacy Policy | Grateful Journal" />
|
||||||
|
<meta property="og:description" content="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." />
|
||||||
|
<meta property="og:image" content="https://gratefuljournal.online/web-app-manifest-512x512.png" />
|
||||||
|
<meta property="og:image:width" content="512" />
|
||||||
|
<meta property="og:image:height" content="512" />
|
||||||
|
<meta property="og:image:alt" content="Grateful Journal logo - a green sprout" />
|
||||||
|
<meta property="og:site_name" content="Grateful Journal" />
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="Privacy Policy | Grateful Journal" />
|
||||||
|
<meta name="twitter:description" content="Your journal entries are end-to-end encrypted. No ads, no tracking, no data selling." />
|
||||||
|
<meta name="twitter:image" content="https://gratefuljournal.online/web-app-manifest-512x512.png" />
|
||||||
|
<meta name="twitter:image:alt" content="Grateful Journal logo - a green sprout" />
|
||||||
|
|
||||||
|
<!-- JSON-LD: WebPage -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebPage",
|
||||||
|
"name": "Privacy Policy",
|
||||||
|
"url": "https://gratefuljournal.online/privacy",
|
||||||
|
"description": "Grateful Journal's privacy policy. Your journal entries are end-to-end encrypted — we cannot read them.",
|
||||||
|
"isPartOf": {
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "Grateful Journal",
|
||||||
|
"url": "https://gratefuljournal.online/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<noscript>
|
||||||
|
<main style="font-family:sans-serif;max-width:680px;margin:4rem auto;padding:1rem 1.5rem;color:#1a1a1a;line-height:1.7">
|
||||||
|
<nav style="margin-bottom:2rem"><a href="/" style="color:#15803d">← Grateful Journal</a></nav>
|
||||||
|
|
||||||
|
<h1 style="color:#15803d">Privacy Policy</h1>
|
||||||
|
<p><em>Last updated: April 14, 2026</em></p>
|
||||||
|
|
||||||
|
<p>Grateful Journal is built on a simple promise: your journal entries are yours alone. We designed the app so that we cannot read your entries even if we wanted to.</p>
|
||||||
|
|
||||||
|
<h2>What we collect</h2>
|
||||||
|
<ul>
|
||||||
|
<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>App preferences</strong> — your display name, profile photo, background images, and theme are stored unencrypted as account settings.</li>
|
||||||
|
<li><strong>Usage data</strong> — no analytics, no tracking pixels, no third-party advertising SDKs.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Encryption</h2>
|
||||||
|
<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. 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.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Data sharing</h2>
|
||||||
|
<p>We do not sell, share, or rent your personal data to any third party. We use Firebase (Google) for authentication only.</p>
|
||||||
|
|
||||||
|
<h2>Data deletion</h2>
|
||||||
|
<p>You can delete your account and all associated data at any time from the Settings page. Deletion is permanent and irreversible.</p>
|
||||||
|
|
||||||
|
<h2>Cookies</h2>
|
||||||
|
<p>We use a single session cookie to keep you signed in. No advertising or tracking cookies are used.</p>
|
||||||
|
|
||||||
|
<nav style="margin-top:2rem">
|
||||||
|
<a href="/">← Back to Grateful Journal</a> ·
|
||||||
|
<a href="/about">About</a>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
</noscript>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,19 +2,25 @@
|
|||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://gratefuljournal.online/</loc>
|
<loc>https://gratefuljournal.online/</loc>
|
||||||
<lastmod>2026-04-13</lastmod>
|
<lastmod>2026-04-16</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://gratefuljournal.online/about</loc>
|
<loc>https://gratefuljournal.online/about</loc>
|
||||||
<lastmod>2026-04-13</lastmod>
|
<lastmod>2026-04-16</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://gratefuljournal.online/privacy</loc>
|
<loc>https://gratefuljournal.online/privacy</loc>
|
||||||
<lastmod>2026-04-13</lastmod>
|
<lastmod>2026-04-16</lastmod>
|
||||||
|
<changefreq>yearly</changefreq>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://gratefuljournal.online/termsofservice</loc>
|
||||||
|
<lastmod>2026-04-16</lastmod>
|
||||||
<changefreq>yearly</changefreq>
|
<changefreq>yearly</changefreq>
|
||||||
<priority>0.4</priority>
|
<priority>0.4</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|||||||
168
src/App.css
168
src/App.css
@@ -19,6 +19,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
background: #eef6ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .page-loader {
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-loader--transparent {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1133,6 +1141,26 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry-edit-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.625rem;
|
||||||
|
height: 1.625rem;
|
||||||
|
border-radius: 7px;
|
||||||
|
border: 1px solid #dbeafe;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #3b82f6;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.18s ease, border-color 0.18s ease;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.entry-edit-btn:hover {
|
||||||
|
background: #dbeafe;
|
||||||
|
border-color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
.entry-delete-btn {
|
.entry-delete-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1914,6 +1942,115 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Edit entry modal */
|
||||||
|
.entry-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-modal-edit {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #3b82f6;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.entry-modal-edit:hover {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-modal {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-title-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
border: 1.5px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #111827;
|
||||||
|
background: #f9fafb;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.edit-entry-title-input:focus {
|
||||||
|
border-color: #6ee7b7;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-content-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
border: 1.5px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-family: "Sniglet", system-ui;
|
||||||
|
color: #374151;
|
||||||
|
background: #f9fafb;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: vertical;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.edit-entry-content-input:focus {
|
||||||
|
border-color: #6ee7b7;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.edit-entry-content-input:disabled,
|
||||||
|
.edit-entry-title-input:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-save {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 10rem;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
font-family: inherit;
|
||||||
|
background: #10b981;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.edit-entry-save:hover:not(:disabled) {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
|
.edit-entry-save:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Responsive: tablet+ (≥ 768px) ---- */
|
/* ---- Responsive: tablet+ (≥ 768px) ---- */
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.entry-modal-overlay {
|
.entry-modal-overlay {
|
||||||
@@ -2690,6 +2827,25 @@
|
|||||||
color: #f87171;
|
color: #f87171;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .entry-edit-btn {
|
||||||
|
background: rgba(59, 130, 246, 0.08);
|
||||||
|
border-color: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .entry-edit-btn:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.18);
|
||||||
|
border-color: rgba(59, 130, 246, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .entry-modal-edit {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .entry-modal-edit:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .entry-delete-btn {
|
[data-theme="dark"] .entry-delete-btn {
|
||||||
background: rgba(239, 68, 68, 0.08);
|
background: rgba(239, 68, 68, 0.08);
|
||||||
border-color: rgba(239, 68, 68, 0.2);
|
border-color: rgba(239, 68, 68, 0.2);
|
||||||
@@ -2700,6 +2856,18 @@
|
|||||||
border-color: rgba(239, 68, 68, 0.35);
|
border-color: rgba(239, 68, 68, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .edit-entry-title-input,
|
||||||
|
[data-theme="dark"] .edit-entry-content-input {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #2d2d2d;
|
||||||
|
color: #e8f5e8;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .edit-entry-title-input:focus,
|
||||||
|
[data-theme="dark"] .edit-entry-content-input:focus {
|
||||||
|
border-color: #4ade80;
|
||||||
|
background: #151515;
|
||||||
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .delete-confirm-modal {
|
[data-theme="dark"] .delete-confirm-modal {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export function PageLoader() {
|
export function PageLoader({ transparent }: { transparent?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="page-loader" role="status" aria-label="Loading">
|
<div className={`page-loader${transparent ? ' page-loader--transparent' : ''}`} role="status" aria-label="Loading">
|
||||||
<svg
|
<svg
|
||||||
className="page-loader__tree"
|
className="page-loader__tree"
|
||||||
viewBox="0 0 60 90"
|
viewBox="0 0 60 90"
|
||||||
|
|||||||
@@ -1,23 +1,45 @@
|
|||||||
import { type ReactNode } from 'react'
|
import { type ReactNode, Suspense, useState, useEffect } from 'react'
|
||||||
import { Navigate, useLocation } from 'react-router-dom'
|
import { Navigate, useLocation } from 'react-router-dom'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { PageLoader } from './PageLoader'
|
import { PageLoader } from './PageLoader'
|
||||||
|
|
||||||
type Props = {
|
// Mounts only once Suspense has resolved (chunk is ready).
|
||||||
children: ReactNode
|
// Signals the parent to hide the loader and reveal content.
|
||||||
|
function ContentReady({ onReady }: { onReady: () => void }) {
|
||||||
|
useEffect(() => {
|
||||||
|
onReady()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Props = { children: ReactNode }
|
||||||
|
|
||||||
export function ProtectedRoute({ children }: Props) {
|
export function ProtectedRoute({ children }: Props) {
|
||||||
const { user, loading } = useAuth()
|
const { user, loading } = useAuth()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
if (loading) {
|
// On page refresh: loading starts true → contentReady=false → loader shows throughout.
|
||||||
return <PageLoader />
|
// On in-app navigation: loading is already false → contentReady=true → no loader shown.
|
||||||
}
|
const [contentReady, setContentReady] = useState(() => !loading)
|
||||||
|
|
||||||
if (!user) {
|
if (!loading && !user) {
|
||||||
return <Navigate to="/" state={{ from: location }} replace />
|
return <Navigate to="/" state={{ from: location }} replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>
|
const showLoader = loading || !contentReady
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showLoader && <PageLoader />}
|
||||||
|
{!loading && user && (
|
||||||
|
<div style={{ display: contentReady ? 'contents' : 'none' }}>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<ContentReady onReady={() => setContentReady(true)} />
|
||||||
|
{children}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { usePageMeta } from '../hooks/usePageMeta'
|
|||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
usePageMeta({
|
usePageMeta({
|
||||||
title: 'About — Grateful Journal',
|
title: 'About Grateful Journal | Private, Encrypted Gratitude Journaling',
|
||||||
description: 'Learn about Grateful Journal — a free, end-to-end encrypted daily gratitude journal. No ads, no tracking, no social feed. Just you and your thoughts.',
|
description: 'Learn about Grateful Journal — a free, end-to-end encrypted daily gratitude journal. No ads, no tracking, no social feed. Just you and your thoughts.',
|
||||||
canonical: 'https://gratefuljournal.online/about',
|
canonical: 'https://gratefuljournal.online/about',
|
||||||
ogTitle: 'About Grateful Journal',
|
ogTitle: 'About Grateful Journal | Private, Encrypted Gratitude Journaling',
|
||||||
ogDescription: 'A free, private gratitude journal with end-to-end encryption. Learn how we built a distraction-free space for your daily reflection practice.',
|
ogDescription: 'A free, private gratitude journal with end-to-end encryption. Learn how we built a distraction-free space for your daily reflection practice.',
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { getUserEntries, deleteEntry, type JournalEntry } from '../lib/api'
|
import { getUserEntries, deleteEntry, updateEntry, type JournalEntry } from '../lib/api'
|
||||||
import { decryptEntry } from '../lib/crypto'
|
import { decryptEntry, encryptEntry } from '../lib/crypto'
|
||||||
import { formatIST, getISTDateComponents } from '../lib/timezone'
|
import { formatIST, getISTDateComponents } from '../lib/timezone'
|
||||||
import BottomNav from '../components/BottomNav'
|
import BottomNav from '../components/BottomNav'
|
||||||
import { useOnboardingTour, hasPendingTourStep, clearPendingTourStep } from '../hooks/useOnboardingTour'
|
import { useOnboardingTour, hasPendingTourStep, clearPendingTourStep } from '../hooks/useOnboardingTour'
|
||||||
@@ -22,6 +22,10 @@ export default function HistoryPage() {
|
|||||||
const [selectedEntry, setSelectedEntry] = useState<DecryptedEntry | null>(null)
|
const [selectedEntry, setSelectedEntry] = useState<DecryptedEntry | null>(null)
|
||||||
const [entryToDelete, setEntryToDelete] = useState<DecryptedEntry | null>(null)
|
const [entryToDelete, setEntryToDelete] = useState<DecryptedEntry | null>(null)
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
const [entryToEdit, setEntryToEdit] = useState<DecryptedEntry | null>(null)
|
||||||
|
const [editTitle, setEditTitle] = useState('')
|
||||||
|
const [editContent, setEditContent] = useState('')
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
const { continueTourOnHistory } = useOnboardingTour()
|
const { continueTourOnHistory } = useOnboardingTour()
|
||||||
|
|
||||||
@@ -178,6 +182,58 @@ export default function HistoryPage() {
|
|||||||
setSelectedDate(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day))
|
setSelectedDate(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isEntryFromToday = (createdAt: string): boolean => {
|
||||||
|
const nowIST = new Date(new Date().getTime() + 5.5 * 60 * 60 * 1000)
|
||||||
|
const components = getISTDateComponents(createdAt)
|
||||||
|
return (
|
||||||
|
components.year === nowIST.getUTCFullYear() &&
|
||||||
|
components.month === nowIST.getUTCMonth() &&
|
||||||
|
components.date === nowIST.getUTCDate()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditModal = (entry: DecryptedEntry) => {
|
||||||
|
setEntryToEdit(entry)
|
||||||
|
setEditTitle(entry.decryptedTitle || '')
|
||||||
|
setEditContent(entry.decryptedContent || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditSave = async () => {
|
||||||
|
if (!entryToEdit || !user || !userId || !secretKey) return
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
const token = await user.getIdToken()
|
||||||
|
const combined = `${editTitle.trim()}\n\n${editContent.trim()}`
|
||||||
|
const { ciphertext, nonce } = await encryptEntry(combined, secretKey)
|
||||||
|
|
||||||
|
await updateEntry(userId, entryToEdit.id, {
|
||||||
|
title: undefined,
|
||||||
|
content: undefined,
|
||||||
|
encryption: {
|
||||||
|
encrypted: true,
|
||||||
|
ciphertext,
|
||||||
|
nonce,
|
||||||
|
algorithm: 'XSalsa20-Poly1305',
|
||||||
|
},
|
||||||
|
}, token)
|
||||||
|
|
||||||
|
const updatedEntry: DecryptedEntry = {
|
||||||
|
...entryToEdit,
|
||||||
|
encryption: { encrypted: true, ciphertext, nonce, algorithm: 'XSalsa20-Poly1305' },
|
||||||
|
decryptedTitle: editTitle.trim(),
|
||||||
|
decryptedContent: editContent.trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
setEntries((prev) => prev.map((e) => e.id === entryToEdit.id ? updatedEntry : e))
|
||||||
|
if (selectedEntry?.id === entryToEdit.id) setSelectedEntry(updatedEntry)
|
||||||
|
setEntryToEdit(null)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update entry:', error)
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDeleteConfirm = async () => {
|
const handleDeleteConfirm = async () => {
|
||||||
if (!entryToDelete || !user || !userId) return
|
if (!entryToDelete || !user || !userId) return
|
||||||
setDeleting(true)
|
setDeleting(true)
|
||||||
@@ -270,7 +326,7 @@ export default function HistoryPage() {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{loadingEntries ? (
|
{loadingEntries ? (
|
||||||
<PageLoader />
|
<PageLoader transparent />
|
||||||
) : (
|
) : (
|
||||||
<div className="entries-list">
|
<div className="entries-list">
|
||||||
{selectedDateEntries.length === 0 ? (
|
{selectedDateEntries.length === 0 ? (
|
||||||
@@ -291,6 +347,19 @@ export default function HistoryPage() {
|
|||||||
<span className="entry-date">{formatDate(entry.createdAt)}</span>
|
<span className="entry-date">{formatDate(entry.createdAt)}</span>
|
||||||
<div className="entry-header-right">
|
<div className="entry-header-right">
|
||||||
<span className="entry-time">{formatTime(entry.createdAt)}</span>
|
<span className="entry-time">{formatTime(entry.createdAt)}</span>
|
||||||
|
{isEntryFromToday(entry.createdAt) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="entry-edit-btn"
|
||||||
|
title="Edit entry"
|
||||||
|
onClick={(e) => { e.stopPropagation(); openEditModal(entry) }}
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="entry-delete-btn"
|
className="entry-delete-btn"
|
||||||
@@ -332,17 +401,32 @@ export default function HistoryPage() {
|
|||||||
<span className="entry-modal-date">{formatDate(selectedEntry.createdAt)}</span>
|
<span className="entry-modal-date">{formatDate(selectedEntry.createdAt)}</span>
|
||||||
<span className="entry-modal-time">{formatTime(selectedEntry.createdAt)}</span>
|
<span className="entry-modal-time">{formatTime(selectedEntry.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="entry-modal-actions">
|
||||||
type="button"
|
{isEntryFromToday(selectedEntry.createdAt) && (
|
||||||
className="entry-modal-close"
|
<button
|
||||||
onClick={() => setSelectedEntry(null)}
|
type="button"
|
||||||
title="Close"
|
className="entry-modal-edit"
|
||||||
>
|
onClick={() => { setSelectedEntry(null); openEditModal(selectedEntry) }}
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
title="Edit entry"
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
>
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
</svg>
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
</button>
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="entry-modal-close"
|
||||||
|
onClick={() => setSelectedEntry(null)}
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="entry-modal-title">
|
<h2 className="entry-modal-title">
|
||||||
@@ -381,6 +465,71 @@ export default function HistoryPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Edit Entry Modal */}
|
||||||
|
{entryToEdit && (
|
||||||
|
<div
|
||||||
|
className="entry-modal-overlay"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget && !saving) setEntryToEdit(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="entry-modal edit-entry-modal">
|
||||||
|
<div className="entry-modal-header">
|
||||||
|
<span className="entry-modal-date">Edit Entry</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="entry-modal-close"
|
||||||
|
onClick={() => setEntryToEdit(null)}
|
||||||
|
disabled={saving}
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="edit-entry-fields">
|
||||||
|
<input
|
||||||
|
className="edit-entry-title-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Title"
|
||||||
|
value={editTitle}
|
||||||
|
onChange={(e) => setEditTitle(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
|
maxLength={200}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className="edit-entry-content-input"
|
||||||
|
placeholder="What are you grateful for today?"
|
||||||
|
value={editContent}
|
||||||
|
onChange={(e) => setEditContent(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
|
rows={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="edit-entry-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="delete-confirm-cancel"
|
||||||
|
onClick={() => setEntryToEdit(null)}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="edit-entry-save"
|
||||||
|
onClick={handleEditSave}
|
||||||
|
disabled={saving || (!editTitle.trim() && !editContent.trim())}
|
||||||
|
>
|
||||||
|
{saving ? 'Saving…' : 'Save'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
{entryToDelete && (
|
{entryToDelete && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { usePageMeta } from '../hooks/usePageMeta'
|
|||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
usePageMeta({
|
usePageMeta({
|
||||||
title: 'Grateful Journal — Your Private Gratitude Journal',
|
title: 'Private Gratitude Journal App | Grateful Journal',
|
||||||
description: 'A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts. Grow your gratitude one moment at a time.',
|
description: 'A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts. Grow your gratitude one moment at a time.',
|
||||||
canonical: 'https://gratefuljournal.online/',
|
canonical: 'https://gratefuljournal.online/',
|
||||||
})
|
})
|
||||||
@@ -34,7 +34,10 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading || signingIn) {
|
// Keep showing the loader until the navigate effect fires.
|
||||||
|
// Without the `user` check here, the login form flashes for one frame
|
||||||
|
// between loading→false and the useEffect redirect.
|
||||||
|
if (loading || signingIn || user) {
|
||||||
return <PageLoader />
|
return <PageLoader />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,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. App preferences are stored unencrypted. No ads, no tracking.',
|
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.',
|
||||||
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. App preferences like background images are stored unencrypted. No ads, no tracking, no data selling.',
|
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 (
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { usePageMeta } from '../hooks/usePageMeta'
|
|||||||
|
|
||||||
export default function TermsOfServicePage() {
|
export default function TermsOfServicePage() {
|
||||||
usePageMeta({
|
usePageMeta({
|
||||||
title: 'Terms of Service — Grateful Journal',
|
title: 'Terms of Service | Grateful Journal',
|
||||||
description: 'Terms of Service for Grateful Journal. Read about the rules and guidelines for using our app.',
|
description: 'Terms of Service for Grateful Journal — a free, private gratitude journal app. Read about the rules and guidelines for using the service.',
|
||||||
canonical: 'https://gratefuljournal.online/termsofservice',
|
canonical: 'https://gratefuljournal.online/termsofservice',
|
||||||
ogTitle: 'Terms of Service — Grateful Journal',
|
ogTitle: 'Terms of Service | Grateful Journal',
|
||||||
ogDescription: 'Terms of Service for Grateful Journal. Read about the rules and guidelines for using our app.',
|
ogDescription: 'Terms of Service for Grateful Journal — a free, private gratitude journal app. Read about the rules and guidelines for using the service.',
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<div className="static-page">
|
<div className="static-page">
|
||||||
|
|||||||
108
termsofservice.html
Normal file
108
termsofservice.html
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" style="background-color:#eef6ee">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Grateful Journal" />
|
||||||
|
<meta name="theme-color" content="#16a34a" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- SEO -->
|
||||||
|
<title>Terms of Service | Grateful Journal</title>
|
||||||
|
<meta name="description" content="Terms of Service for Grateful Journal — a free, private gratitude journal app. Read about the rules and guidelines for using the service." />
|
||||||
|
<meta name="keywords" content="grateful journal terms of service, gratitude journal app terms, journal app conditions" />
|
||||||
|
<meta name="robots" content="index, follow, max-snippet:160, max-image-preview:large" />
|
||||||
|
<link rel="canonical" href="https://gratefuljournal.online/termsofservice" />
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content="en_US" />
|
||||||
|
<meta property="og:url" content="https://gratefuljournal.online/termsofservice" />
|
||||||
|
<meta property="og:title" content="Terms of Service | Grateful Journal" />
|
||||||
|
<meta property="og:description" content="Terms of Service for Grateful Journal — a free, private gratitude journal app. Read about the rules and guidelines for using the service." />
|
||||||
|
<meta property="og:image" content="https://gratefuljournal.online/web-app-manifest-512x512.png" />
|
||||||
|
<meta property="og:image:width" content="512" />
|
||||||
|
<meta property="og:image:height" content="512" />
|
||||||
|
<meta property="og:image:alt" content="Grateful Journal logo - a green sprout" />
|
||||||
|
<meta property="og:site_name" content="Grateful Journal" />
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="Terms of Service | Grateful Journal" />
|
||||||
|
<meta name="twitter:description" content="Terms of Service for Grateful Journal — a free, private gratitude journal app." />
|
||||||
|
<meta name="twitter:image" content="https://gratefuljournal.online/web-app-manifest-512x512.png" />
|
||||||
|
<meta name="twitter:image:alt" content="Grateful Journal logo - a green sprout" />
|
||||||
|
|
||||||
|
<!-- JSON-LD: WebPage -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebPage",
|
||||||
|
"name": "Terms of Service",
|
||||||
|
"url": "https://gratefuljournal.online/termsofservice",
|
||||||
|
"description": "Terms of Service for Grateful Journal — a free, private gratitude journal app.",
|
||||||
|
"isPartOf": {
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "Grateful Journal",
|
||||||
|
"url": "https://gratefuljournal.online/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<noscript>
|
||||||
|
<main style="font-family:sans-serif;max-width:680px;margin:4rem auto;padding:1rem 1.5rem;color:#1a1a1a;line-height:1.7">
|
||||||
|
<nav style="margin-bottom:2rem"><a href="/" style="color:#15803d">← Grateful Journal</a></nav>
|
||||||
|
|
||||||
|
<h1 style="color:#15803d">Terms of Service</h1>
|
||||||
|
<p><em>Last updated: April 14, 2026</em></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. 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. Your journal 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 use the service for any unlawful purpose, attempt to gain unauthorized access to the service, or abuse the service in a way that impairs its operation for other users.</p>
|
||||||
|
|
||||||
|
<h2>5. Service Availability</h2>
|
||||||
|
<p>We strive to keep Grateful Journal available at all times but do not guarantee uninterrupted access. 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.</p>
|
||||||
|
|
||||||
|
<h2>7. Disclaimer of Warranties</h2>
|
||||||
|
<p>Grateful Journal is provided "as is" without warranties of any kind. 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, or consequential damages arising from your use of the service.</p>
|
||||||
|
|
||||||
|
<h2>9. Changes to These Terms</h2>
|
||||||
|
<p>We may update these Terms of Service from time to time. Continued use of the service after changes constitutes acceptance of the updated terms.</p>
|
||||||
|
|
||||||
|
<nav style="margin-top:2rem">
|
||||||
|
<a href="/">← Back to Grateful Journal</a> ·
|
||||||
|
<a href="/privacy">Privacy Policy</a> ·
|
||||||
|
<a href="/about">About</a>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
</noscript>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineConfig, loadEnv } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path, { resolve } from 'path'
|
||||||
|
|
||||||
function injectFirebaseConfig(content: string, env: Record<string, string>): string {
|
function injectFirebaseConfig(content: string, env: Record<string, string>): string {
|
||||||
return content
|
return content
|
||||||
@@ -60,6 +60,12 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
chunkSizeWarningLimit: 1000,
|
chunkSizeWarningLimit: 1000,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: resolve(__dirname, 'index.html'),
|
||||||
|
about: resolve(__dirname, 'about.html'),
|
||||||
|
privacy: resolve(__dirname, 'privacy.html'),
|
||||||
|
termsofservice: resolve(__dirname, 'termsofservice.html'),
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
manualChunks(id) {
|
manualChunks(id) {
|
||||||
if (id.includes('node_modules/firebase')) {
|
if (id.includes('node_modules/firebase')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user