diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 0000000..b62be4b --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,48 @@ +name: Version Bump + +on: + push: + branches: + - '**' + +jobs: + bump: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install deps + run: npm ci + - name: Determine bump type + id: bump + run: | + MSG=$(git log -1 --pretty=%B) + echo "Commit message: $MSG" + if echo "$MSG" | grep -qi "bump:major"; then + echo "type=major" >> $GITHUB_OUTPUT + elif echo "$MSG" | grep -qi "bump:minor"; then + echo "type=minor" >> $GITHUB_OUTPUT + else + echo "type=patch" >> $GITHUB_OUTPUT + fi + - name: Bump version + run: | + npm version ${{ steps.bump.outputs.type }} -m "chore: version bump to %s" + node scripts/sync-version.js + - name: Commit version files + run: | + git config user.name "github-actions" + git config user.email "github-actions@github.com" + git add package.json package-lock.json src/lib/version.ts + git commit -m "chore: sync version metadata" || echo "No changes to commit" + - name: Push changes + run: | + git push --follow-tags diff --git a/README.md b/README.md index 3133a09..3eba617 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,48 @@ -# Prayer App +# Haver — Prayer App -A Progressive Web App (PWA) for journaling prayers and praises, organized by date, with journaling functionality and email reminders. +A minimalist PWA for journaling prayers and tracking answered prayers. Built with Next.js, Firebase, and Tailwind. + +Current Version: 0.2.0 + +## What's New (0.2.0) +- Redesign: unified dark theme and gradients +- Icons: lucide-react across headings and nav +- Cleanup: removed legacy email routes and pages +- UX: Pray For suggestions in compose tab ## Features +- Firebase auth (login) +- Compose daily prayer with date and optional "Pray For" +- Suggested "Pray For" names via dropdown (type to add new) +- Mark prayers as answered with details +- Calendar view of entries +- Reflections list with search and filters +- Answered prayers gallery +- Reminder banner + notification time settings +- PWA installable; dark theme by default -- User authentication with Firebase (login and signup) -- Add prayers and praises with date organization -- Journal entries for each prayer -- "Pray For" field to specify a person for each prayer -- Email field auto-populated with user's email for reminders -- Archive prayers instead of deleting them -- View archived prayers with dedicated tab -- Search prayers by content, type, or journal (dedicated Search tab) -- Pagination for large numbers of entries -- Share prayers on social media, email, and text -- Prominent share buttons with visual feedback -- Admin panel for SMTP configuration and user management -- Email notifications (requires configuration) -- Reminder frequency options (daily, weekly, monthly, or never) -- Option to include active prayers summary in reminder emails -- Beautiful HTML-formatted emails with prayer cards and visual styling -- PWA for offline access and installation -- Dark mode theme -- Bottom navigation tabs for easy switching (Add Entry, Active, Archived, Search) -- Engaging loading screen with animations and inspirational content (minimum 5-second display) -- Enhanced bottom navigation with gradients, shadows, and improved touch targets - -## API Endpoints - -- `GET/POST /api/send-email` - Send a single email reminder -- `GET/POST /api/send-scheduled-emails` - Send scheduled email reminders to all users +## Versioning Policy +- Uses semantic versioning: major.minor +- Minor: UI/UX tweaks, non-breaking features +- Major: structural changes or breaking updates ## Setup +1. Create a Firebase project (Authentication + Firestore) +2. Add your Firebase config to [src/lib/firebase.ts](src/lib/firebase.ts) +3. Start the dev server -1. Create a Firebase project at https://console.firebase.google.com/ -2. Enable Authentication with Email/Password provider -3. Enable Firestore Database -4. Get your Firebase config and replace the placeholder in `src/lib/firebase.ts` +## Notes +- Email sending routes have been removed in the redesign. +- Email verification remains available via Firebase. -Users can now register accounts directly in the app! +## Firestore Rules +Deploy [firestore.rules](firestore.rules) to secure user data. -## Admin Setup - -Admin access is restricted to specific email addresses. Admins can manage the admin email list directly in the app through the Admin panel. - -**To add/remove admin emails:** -1. Log in as an existing admin -2. Click the "Admin" button -3. Scroll to "Admin Email Management" section -4. Add new admin emails or remove existing ones -5. Changes are saved automatically to Firestore - -Admin users can access the admin panel by clicking the "Admin" button in the header. From there, they can configure: - -- SMTP settings for email sending -- Admin email management -- Other server configurations - -The admin panel saves settings to Firestore for persistent configuration. - -## Firestore Security Rules - -To enable SMTP configuration saving and other admin features, you need to deploy Firestore security rules: - -1. Install Firebase CLI if you haven't already: - ```bash - npm install -g firebase-tools - ``` - -2. Login to Firebase: - ```bash - firebase login - ``` - -3. Initialize or connect to your Firebase project: - ```bash - firebase use --add - ``` - Select your prayer-app project. - -4. Deploy the security rules using the provided script: - ```bash - # On Linux/Mac - ./deploy-firestore-rules.sh - - # On Windows - deploy-firestore-rules.bat - ``` - - Or deploy manually: - ```bash - firebase deploy --only firestore:rules - ``` - -The `firestore.rules` file in this repository allows authenticated users to read/write configuration data needed for the admin panel. - -## SMTP Configuration - -After deploying Firestore rules, you can configure SMTP settings through the admin panel: - -1. Log in as an admin user -2. Click the "Admin" button -3. Fill in your SMTP provider details (Gmail, Outlook, etc.) -4. Save the configuration - -**Gmail Setup:** -- Host: `smtp.gmail.com` -- Port: `587` (or `465` for SSL) -- Username: Your Gmail address -- Password: App password (not your regular password) -- From: Your Gmail address -- Use SSL/TLS: Checked for port 465, unchecked for port 587 - -**Note:** For Gmail, you'll need to generate an "App Password" in your Google Account settings. +## Development +- `npm run dev` — start dev server +- `npm run build` — production build +- `npm run lint` — lint code +- `npm test` — run tests ## Troubleshooting @@ -133,76 +63,21 @@ After deploying Firestore rules, you can configure SMTP settings through the adm - Run `npm run lint` for code linting - Run `npm test` for running tests -## User Guides - -### Adding Prayers - -1. Click the "Add" tab -2. Fill in the date, type (prayer/praise), text, and optional journal -3. Set reminder frequency -4. Click "Add Prayer" - -### Managing Prayers - -- **Active**: View current prayers -- **Archived**: View archived prayers -- **Search**: Search by content -- **Community**: View shared prayers - -### Profile Customization - -- Click "Profile" to update avatar, theme, and categories - -### Admin Features - -- Configure SMTP for emails -- Manage admin users -- View analytics +## Usage +- Compose: write your prayer, pick a date, and optionally select or type a "Pray For" name. +- Praying: mark prayers as prayed today. +- Calendar: click a day to view/edit its entry. +- Reflections: search and filter past prayers. +- Answered: review answered prayers with details. +- About: set reminder time, test notifications, and read "What's New". ## Getting Started -## Email Configuration - -### Option 1: EmailJS (Free, Recommended) - -EmailJS provides free email sending without server-side SMTP configuration: - -1. **Sign up at [emailjs.com](https://www.emailjs.com/)** -2. **Create an Email Service** (Gmail, Outlook, etc.) -3. **Create an Email Template** with these variables: - ``` - Subject: New {{prayer_type}} added - {{reminder_frequency}} reminders enabled - - Hi {{to_name}}, - - A new prayer has been added to your Prayer App: - - Date: {{prayer_date}} - Type: {{prayer_type}} - Text: {{prayer_text}} - Journal: {{prayer_journal}} - Praying for: {{praying_for}} - - Reminder Frequency: {{reminder_frequency}} - - Best regards, - Prayer App - ``` -4. **Get your credentials**: - - Service ID - - Template ID - - Public Key - -5. **Add to `.env.local`**: - ``` - NEXT_PUBLIC_EMAILJS_SERVICE_ID=your_service_id - NEXT_PUBLIC_EMAILJS_TEMPLATE_ID=your_template_id - NEXT_PUBLIC_EMAILJS_PUBLIC_KEY=your_public_key - ``` - -### Option 2: SMTP (Advanced) - -For custom SMTP servers, configure settings in the admin panel after deploying Firestore rules. +## Build & Deploy +```bash +npm run build +``` +Deploy to Vercel or your preferred platform. ## Getting Started @@ -244,103 +119,10 @@ npm run build Deploy to Vercel or any platform supporting Next.js PWAs. -## Docker Deployment - -The app includes Docker support for containerized deployment with automated email scheduling. - -### Build and Push Docker Image - -1. **Using the automated script (Windows):** - ```powershell - .\build-and-push-docker.ps1 - ``` - This script will: - - Build the Docker image - - Prompt for your GitHub Personal Access Token - - Login to GitHub Container Registry - - Push the image - -2. **Manual build and push:** - ```bash - # Build the image - docker build -t ghcr.io/nmemmert/prayer-app:latest . - - # Login to GitHub Container Registry - echo $GITHUB_TOKEN | docker login ghcr.io -u nmemmert --password-stdin - - # Push the image - docker push ghcr.io/nmemmert/prayer-app:latest - ``` - -### Run with Docker Compose - -```bash -docker-compose up -d -``` - -The container includes: -- Automated email scheduling (runs every 5 minutes) -- Health checks -- Production-optimized Next.js build -- Cron jobs for scheduled tasks - -### Environment Variables - -Set these environment variables for the container: - -```bash -# Firebase Admin SDK (required for server-side operations) -FIREBASE_PROJECT_ID=your_project_id -FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..." -FIREBASE_CLIENT_EMAIL=your_service_account_email - -# EmailJS (for client-side emails) -NEXT_PUBLIC_EMAILJS_SERVICE_ID=your_service_id -NEXT_PUBLIC_EMAILJS_TEMPLATE_ID=your_template_id -NEXT_PUBLIC_EMAILJS_PUBLIC_KEY=your_public_key -``` +## Docker (optional) +Basic containerization is available. Remove or adapt scripts as needed. ## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. - -## Docker Support - -This application is fully containerized and ready for deployment. - -### Building Locally - -```bash -# Build the Docker image -docker build -t prayer-app . - -# Run the container -docker run -p 3000:3000 prayer-app +- [Next.js Documentation](https://nextjs.org/docs) +- [Tailwind CSS](https://tailwindcss.com/docs) ``` - -### Using Docker Compose - -```bash -# Start the application -docker-compose up -d - -# Stop the application -docker-compose down -``` - -### GitHub Container Registry - -The application includes GitHub Actions CI/CD that automatically builds and pushes Docker images to GitHub Container Registry on every push to the main branch. - -Images are available at: `ghcr.io/[your-username]/prayer-app:latest` diff --git a/icon.jpeg b/icon.jpeg new file mode 100644 index 0000000..5931411 Binary files /dev/null and b/icon.jpeg differ diff --git a/package.json b/package.json index 737851d..5ed2b86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prayer", - "version": "0.1.0", + "version": "0.2.0", "private": true, "type": "module", "scripts": { @@ -9,7 +9,9 @@ "start": "next start", "lint": "eslint", "test": "jest", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "version:minor": "npm version minor && node scripts/sync-version.js", + "version:major": "npm version major && node scripts/sync-version.js" }, "dependencies": { "@dataconnect/generated": "file:src/dataconnect-generated", diff --git a/public/sw.js b/public/sw.js index 096f09a..4a50bcd 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1 +1 @@ -if(!self.define){let e,s={};const a=(a,n)=>(a=new URL(a+".js",n).href,s[a]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=a,e.onload=s,document.head.appendChild(e)}else e=a,importScripts(a),s()}).then(()=>{let e=s[a];if(!e)throw new Error(`Module ${a} didn’t register its module`);return e}));self.define=(n,i)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let c={};const r=e=>a(e,t),o={module:{uri:t},exports:c,require:r};s[t]=Promise.all(n.map(e=>o[e]||r(e))).then(e=>(i(...e),c))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/Q7AkDBdJDGBRANlWHHBOE/_buildManifest.js",revision:"44608b7d53e1eefcd54452c125d6300e"},{url:"/_next/static/Q7AkDBdJDGBRANlWHHBOE/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/172-71074a5b97c03327.js",revision:"71074a5b97c03327"},{url:"/_next/static/chunks/462-1ebca3834d195b35.js",revision:"1ebca3834d195b35"},{url:"/_next/static/chunks/497-22b66697988d7f92.js",revision:"22b66697988d7f92"},{url:"/_next/static/chunks/4bd1b696-b7529f9b306ab05d.js",revision:"b7529f9b306ab05d"},{url:"/_next/static/chunks/512-8d2b9de6fda25175.js",revision:"8d2b9de6fda25175"},{url:"/_next/static/chunks/7508b87c-97d7ae290120d64a.js",revision:"97d7ae290120d64a"},{url:"/_next/static/chunks/995.a093d11da39e25d3.js",revision:"a093d11da39e25d3"},{url:"/_next/static/chunks/app/_global-error/page-2af6c391bfa86449.js",revision:"2af6c391bfa86449"},{url:"/_next/static/chunks/app/_not-found/page-f75e7ecfbcc8dd77.js",revision:"f75e7ecfbcc8dd77"},{url:"/_next/static/chunks/app/admin/page-be774d9a59b52f3a.js",revision:"be774d9a59b52f3a"},{url:"/_next/static/chunks/app/api/debug-smtp/route-2af6c391bfa86449.js",revision:"2af6c391bfa86449"},{url:"/_next/static/chunks/app/api/send-email/route-2af6c391bfa86449.js",revision:"2af6c391bfa86449"},{url:"/_next/static/chunks/app/api/send-scheduled-emails/route-2af6c391bfa86449.js",revision:"2af6c391bfa86449"},{url:"/_next/static/chunks/app/layout-7f3802a6a2a60f7d.js",revision:"7f3802a6a2a60f7d"},{url:"/_next/static/chunks/app/page-4907ce715213b04e.js",revision:"4907ce715213b04e"},{url:"/_next/static/chunks/app/profile/page-a3a0e0cd20fcb9d6.js",revision:"a3a0e0cd20fcb9d6"},{url:"/_next/static/chunks/app/testimonies/page-ed6616cfcac7de9d.js",revision:"ed6616cfcac7de9d"},{url:"/_next/static/chunks/dd8162e8-580391f39948d3f7.js",revision:"580391f39948d3f7"},{url:"/_next/static/chunks/framework-292291387d6b2e39.js",revision:"292291387d6b2e39"},{url:"/_next/static/chunks/main-5e55aa9a8969fd08.js",revision:"5e55aa9a8969fd08"},{url:"/_next/static/chunks/main-app-1640b70ccc992192.js",revision:"1640b70ccc992192"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-2af6c391bfa86449.js",revision:"2af6c391bfa86449"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-2af6c391bfa86449.js",revision:"2af6c391bfa86449"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-647436fbb68222b8.js",revision:"647436fbb68222b8"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-2af6c391bfa86449.js",revision:"2af6c391bfa86449"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-2af6c391bfa86449.js",revision:"2af6c391bfa86449"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-6b6a51bcb4cc4744.js",revision:"6b6a51bcb4cc4744"},{url:"/_next/static/css/bff524108e499dd4.css",revision:"bff524108e499dd4"},{url:"/_next/static/media/4cf2300e9c8272f7-s.p.woff2",revision:"18bae71b1e1b2bb25321090a3b563103"},{url:"/_next/static/media/747892c23ea88013-s.woff2",revision:"a0761690ccf4441ace5cec893b82d4ab"},{url:"/_next/static/media/8d697b304b401681-s.woff2",revision:"cc728f6c0adb04da0dfcb0fc436a8ae5"},{url:"/_next/static/media/93f479601ee12b01-s.p.woff2",revision:"da83d5f06d825c5ae65b7cca706cb312"},{url:"/_next/static/media/9610d9e46709d722-s.woff2",revision:"7b7c0ef93df188a852344fc272fc096b"},{url:"/_next/static/media/ba015fad6dcf6784-s.woff2",revision:"8ea4f719af3312a055caf09f34c89a77"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/firebase-messaging-sw.js",revision:"396ffcf00a63da03dfc03d78d4815494"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/icon-192x192.png",revision:"bbbd7efc0e31088ae0a3aeede7608534"},{url:"/icon-512x512.png",revision:"400538d76556a3a8ee4fd3dfda7bc092"},{url:"/locales/en/common.json",revision:"41332abbb11f0b33b3eb308a5448f8d6"},{url:"/manifest.json",revision:"6dbf565091414e98afd54d79577bb36d"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:a,state:n})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}); +if(!self.define){let e,s={};const a=(a,n)=>(a=new URL(a+".js",n).href,s[a]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=a,e.onload=s,document.head.appendChild(e)}else e=a,importScripts(a),s()}).then(()=>{let e=s[a];if(!e)throw new Error(`Module ${a} didn’t register its module`);return e}));self.define=(n,i)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let c={};const o=e=>a(e,t),r={module:{uri:t},exports:c,require:o};s[t]=Promise.all(n.map(e=>r[e]||o(e))).then(e=>(i(...e),c))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/RTXxDDjYPkCiNJ6w2qWiq/_buildManifest.js",revision:"f4d58a725936c94635b35b3f85e2a011"},{url:"/_next/static/RTXxDDjYPkCiNJ6w2qWiq/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/4bd1b696-b7529f9b306ab05d.js",revision:"b7529f9b306ab05d"},{url:"/_next/static/chunks/512-8d2b9de6fda25175.js",revision:"8d2b9de6fda25175"},{url:"/_next/static/chunks/7508b87c-dfcb7bb31c18c36f.js",revision:"dfcb7bb31c18c36f"},{url:"/_next/static/chunks/884-27d75bba95dbe213.js",revision:"27d75bba95dbe213"},{url:"/_next/static/chunks/995.a093d11da39e25d3.js",revision:"a093d11da39e25d3"},{url:"/_next/static/chunks/app/_global-error/page-48c825fda4ae9c10.js",revision:"48c825fda4ae9c10"},{url:"/_next/static/chunks/app/_not-found/page-f75e7ecfbcc8dd77.js",revision:"f75e7ecfbcc8dd77"},{url:"/_next/static/chunks/app/layout-5ea58a1aa81454ad.js",revision:"5ea58a1aa81454ad"},{url:"/_next/static/chunks/app/page-2a6751ff1202edf3.js",revision:"2a6751ff1202edf3"},{url:"/_next/static/chunks/dd8162e8-580391f39948d3f7.js",revision:"580391f39948d3f7"},{url:"/_next/static/chunks/framework-292291387d6b2e39.js",revision:"292291387d6b2e39"},{url:"/_next/static/chunks/main-5e55aa9a8969fd08.js",revision:"5e55aa9a8969fd08"},{url:"/_next/static/chunks/main-app-1640b70ccc992192.js",revision:"1640b70ccc992192"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-48c825fda4ae9c10.js",revision:"48c825fda4ae9c10"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-48c825fda4ae9c10.js",revision:"48c825fda4ae9c10"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-647436fbb68222b8.js",revision:"647436fbb68222b8"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-48c825fda4ae9c10.js",revision:"48c825fda4ae9c10"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-48c825fda4ae9c10.js",revision:"48c825fda4ae9c10"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-6240b410a738ea5b.js",revision:"6240b410a738ea5b"},{url:"/_next/static/css/c0d6f2e4d70567da.css",revision:"c0d6f2e4d70567da"},{url:"/_next/static/media/4cf2300e9c8272f7-s.p.woff2",revision:"18bae71b1e1b2bb25321090a3b563103"},{url:"/_next/static/media/747892c23ea88013-s.woff2",revision:"a0761690ccf4441ace5cec893b82d4ab"},{url:"/_next/static/media/8d697b304b401681-s.woff2",revision:"cc728f6c0adb04da0dfcb0fc436a8ae5"},{url:"/_next/static/media/93f479601ee12b01-s.p.woff2",revision:"da83d5f06d825c5ae65b7cca706cb312"},{url:"/_next/static/media/9610d9e46709d722-s.woff2",revision:"7b7c0ef93df188a852344fc272fc096b"},{url:"/_next/static/media/ba015fad6dcf6784-s.woff2",revision:"8ea4f719af3312a055caf09f34c89a77"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/firebase-messaging-sw.js",revision:"396ffcf00a63da03dfc03d78d4815494"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/icon-192x192.png",revision:"bbbd7efc0e31088ae0a3aeede7608534"},{url:"/icon-512x512.png",revision:"400538d76556a3a8ee4fd3dfda7bc092"},{url:"/locales/en/common.json",revision:"41332abbb11f0b33b3eb308a5448f8d6"},{url:"/manifest.json",revision:"6dbf565091414e98afd54d79577bb36d"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:a,state:n})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}); diff --git a/scripts/sync-version.js b/scripts/sync-version.js new file mode 100644 index 0000000..390f1e0 --- /dev/null +++ b/scripts/sync-version.js @@ -0,0 +1,14 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +const pkgPath = path.join(__dirname, '..', 'package.json'); +const versionPath = path.join(__dirname, '..', 'src', 'lib', 'version.ts'); + +const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); +const version = pkg.version; + +const content = `export const APP_VERSION = '${version}';\n\nexport const WHATS_NEW_BRIEF: string[] = [\n 'Redesign: unified dark theme and gradients',\n 'Icons: lucide-react across headings and nav',\n 'Cleanup: removed legacy email routes and pages',\n 'UX: Pray For suggestions in compose tab'\n];\n`; + +fs.writeFileSync(versionPath, content, 'utf8'); +console.log('Synced version to src/lib/version.ts:', version); diff --git a/src/app/page.tsx b/src/app/page.tsx index 56f97c6..5710baf 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -123,14 +123,14 @@ export default function Home() { setPrayers(prev => prev.map(p => (p.id === editingPrayer.id ? { ...p, ...prayerData } : p)) ); - toast.success('Prayer updated! 🙏'); + toast.success('Prayer updated!'); setEditingPrayer(null); } else { // Add new prayer const docRef = await addDoc(collection(db, 'prayers'), prayerData); const newPrayer: Prayer = { id: docRef.id, ...prayerData }; setPrayers(prev => [...prev, newPrayer]); - toast.success('Prayer saved! 🙏'); + toast.success('Prayer saved!'); } // Reset form @@ -176,7 +176,7 @@ export default function Home() { setPrayers(prev => prev.map(p => (p.id === prayerId ? { ...p, lastPrayedFor: today } : p)) ); - toast.success('Prayed! 🙏'); + toast.success('Prayed!'); } catch (error) { console.error('Error marking prayed:', error); toast.error('Failed to update'); @@ -200,6 +200,7 @@ export default function Home() { + Haver icon

Haver

Preparing your sacred space...

@@ -247,6 +248,7 @@ export default function Home() { onTextChange={setText} prayFor={prayFor} onPrayForChange={setPrayFor} + prayForOptions={Array.from(new Set(prayers.map(p => p.prayFor).filter(Boolean))) as string[]} answered={answered} onAnsweredChange={setAnswered} answerDetails={answerDetails} diff --git a/src/components/AboutPage.tsx b/src/components/AboutPage.tsx index 520cbf8..a98c576 100644 --- a/src/components/AboutPage.tsx +++ b/src/components/AboutPage.tsx @@ -2,7 +2,8 @@ import React, { useState } from 'react'; import { useAuth } from '@/context/AuthContext'; -import { Info } from 'lucide-react'; +import { Info, Smartphone, BellRing } from 'lucide-react'; +import { APP_VERSION, WHATS_NEW_BRIEF } from '@/lib/version'; interface AboutPageProps { userEmail?: string; @@ -102,7 +103,7 @@ export default function AboutPage({ userEmail }: AboutPageProps) { const testNotificationNow = async () => { if (supportsWebPush && Notification.permission === 'granted') { - new Notification('🙏 Test Prayer Reminder', { + new Notification('Test Prayer Reminder', { body: 'This is a test notification. Your reminders are working!', icon: '/icon-192x192.png', badge: '/icon-192x192.png', @@ -139,7 +140,7 @@ export default function AboutPage({ userEmail }: AboutPageProps) { if (timeUntilNotification > 0) { setTimeout(() => { if ('Notification' in window && Notification.permission === 'granted' && notificationsEnabled) { - new Notification('🙏 Time to Pray', { + new Notification('Time to Pray', { body: 'Take a moment to connect with God and pray.', icon: '/icon-192x192.png', badge: '/icon-192x192.png', @@ -168,6 +169,7 @@ export default function AboutPage({ userEmail }: AboutPageProps) {
{/* Header */}
+ Haver icon

@@ -264,7 +266,7 @@ export default function AboutPage({ userEmail }: AboutPageProps) { {/* Platform Support Info */} {platform === 'ios' && (
-

📱 iOS: In-App Reminders

+

iOS: In-App Reminders

iOS Safari doesn't support push notifications. Enable reminders to get a message when you open the app.

)} @@ -319,7 +321,7 @@ export default function AboutPage({ userEmail }: AboutPageProps) { onClick={testNotificationNow} className="w-full px-3 py-2 bg-slate-700 hover:bg-slate-600 text-slate-200 text-sm font-medium rounded-lg transition-colors border border-slate-600" > - 📬 Test Notification + Test Notification )}

@@ -331,11 +333,21 @@ export default function AboutPage({ userEmail }: AboutPageProps) {

App Info

-

Haver v1.0

+

Haver v{APP_VERSION}

A simple, private space for your daily prayers and reflections.

+ {/* What's New Section */} +
+

What's New

+ +
+ {/* Logout and Delete Section */}