feat: add app icon branding to all remaining page headers

- CalendarView header now displays icon
- ReflectionsList header now displays icon
- AnsweredPrayers header now displays icon
- ThingsToPrayFor header now displays icon

Completes visual identity unification across all main screens
This commit is contained in:
nmemmert committed 2026-01-19 09:10:39 -05:00
1 parent 74686cece1
commit aa3a35bc4a
17 files changed
+234 -330

No files matched your search

+48
View File
@@ -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
+52 -270
View File
@@ -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`
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

+4 -2
View File
@@ -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",
+1 -1
View File
@@ -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.cleanupOutdatedCaLine truncated
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-assLine truncated
+14
View File
@@ -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);
+5 -3
View File
@@ -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() {
</div>
</div>
<img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" />
<h1 className="text-2xl font-bold text-white mb-2 animate-pulse">Haver</h1>
<p className="text-gray-400 text-lg mb-4">Preparing your sacred space...</p>
@@ -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}
+18 -6
View File
@@ -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) {
<div className="max-w-md mx-auto">
{/* Header */}
<div className="text-center mb-8">
<img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" />
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent mb-2">
<span className="inline-flex items-center gap-2 justify-center">
<Info className="w-7 h-7 text-blue-400" />
@@ -264,7 +266,7 @@ export default function AboutPage({ userEmail }: AboutPageProps) {
{/* Platform Support Info */}
{platform === 'ios' && (
<div className="bg-blue-900 border border-blue-700 text-blue-200 px-3 py-2 rounded-lg text-xs">
<p className="font-medium mb-1">📱 iOS: In-App Reminders</p>
<p className="font-medium mb-1 inline-flex items-center gap-1.5"><Smartphone className="w-4 h-4" /> iOS: In-App Reminders</p>
<p>iOS Safari doesn't support push notifications. Enable reminders to get a message when you open the app.</p>
</div>
)}
@@ -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
<span className="inline-flex items-center gap-1.5"><BellRing className="w-4 h-4" /> Test Notification</span>
</button>
)}
</div>
@@ -331,11 +333,21 @@ export default function AboutPage({ userEmail }: AboutPageProps) {
<div className="border-b border-slate-700 pb-6">
<h2 className="text-lg font-semibold text-white mb-4">App Info</h2>
<div className="space-y-2 text-sm text-slate-400">
<p>Haver v1.0</p>
<p>Haver v{APP_VERSION}</p>
<p>A simple, private space for your daily prayers and reflections.</p>
</div>
</div>
{/* What's New Section */}
<div className="border-b border-slate-700 pb-6">
<h2 className="text-lg font-semibold text-white mb-4">What's New</h2>
<ul className="list-disc list-inside text-sm text-slate-300 space-y-1">
{WHATS_NEW_BRIEF.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
{/* Logout and Delete Section */}
<div className="space-y-3">
<button
+3 -2
View File
@@ -17,7 +17,7 @@ interface AnsweredPrayersProps {
onEdit?: (prayer: Prayer) => void;
}
import { CheckCircle2 } from 'lucide-react';
import { CheckCircle2, Sparkles } from 'lucide-react';
export default function AnsweredPrayers({ prayers, onEdit }: AnsweredPrayersProps) {
const answeredPrayers = prayers.filter(p => p.answered);
@@ -32,6 +32,7 @@ export default function AnsweredPrayers({ prayers, onEdit }: AnsweredPrayersProp
<div className="max-w-md mx-auto">
<div className="text-center mb-8">
<img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" />
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent mb-2">
<span className="inline-flex items-center gap-2 justify-center">
<CheckCircle2 className="w-7 h-7 text-blue-400" />
@@ -76,7 +77,7 @@ export default function AnsweredPrayers({ prayers, onEdit }: AnsweredPrayersProp
</div>
) : (
<div className="text-center py-12">
<div className="text-6xl mb-4 animate-bounce">🙏</div>
<Sparkles className="w-12 h-12 mb-4 text-blue-400 animate-pulse mx-auto" />
<p className="text-white font-semibold mb-2">No answered prayers yet</p>
<p className="text-sm text-slate-400">
Mark prayers as answered in the Compose tab to see them here
+1 -1
View File
@@ -113,7 +113,7 @@ export default function BottomNav({ activeTab, onTabChange }: BottomNavProps) {
<nav className="hidden md:fixed md:top-0 md:left-0 md:right-0 md:bg-slate-800/80 md:backdrop-blur-md md:border-b md:border-slate-700 md:z-50 md:flex md:items-center md:justify-between md:px-6 md:py-3 md:shadow-sm">
<div className="flex items-center gap-8">
<div className="flex items-center gap-2">
<HandHeart className="w-5 h-5 text-blue-400" />
<img src="/icon-192x192.png" alt="Haver icon" className="w-6 h-6 rounded" />
<h1 className="text-2xl font-bold text-white">Haver</h1>
</div>
<div className="flex gap-1">
+1 -2
View File
@@ -66,8 +66,7 @@ export default function CalendarView({ prayers, onDayClick }: CalendarViewProps)
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent mb-2">
<div className="text-center mb-8"> <img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" /> <h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent mb-2">
<span className="inline-flex items-center gap-2 justify-center">
<CalendarIcon className="w-7 h-7 text-blue-400" />
Prayer Journal
+31 -8
View File
@@ -2,7 +2,7 @@
import React, { useState } from 'react';
import toast from 'react-hot-toast';
import { PenSquare } from 'lucide-react';
import { PenSquare, Calendar as CalendarIcon, HandHeart, CheckCircle2, Sparkles, Save, Loader2 } from 'lucide-react';
interface DailyEntryProps {
date: string;
@@ -11,6 +11,7 @@ interface DailyEntryProps {
onTextChange: (text: string) => void;
prayFor: string;
onPrayForChange: (prayFor: string) => void;
prayForOptions?: string[];
answered?: boolean;
onAnsweredChange?: (answered: boolean) => void;
answerDetails?: string;
@@ -27,6 +28,7 @@ export default function DailyEntry({
onTextChange,
prayFor,
onPrayForChange,
prayForOptions = [],
answered = false,
onAnsweredChange,
answerDetails = '',
@@ -50,6 +52,7 @@ export default function DailyEntry({
<div className="max-w-2xl mx-auto">
{/* Header */}
<div className="text-center mb-12">
<img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" />
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent mb-2">
<span className="inline-flex items-center gap-2 justify-center">
<PenSquare className="w-7 h-7 text-blue-400" />
@@ -70,7 +73,7 @@ export default function DailyEntry({
<form onSubmit={onSubmit} className="space-y-6">
{/* Date Input */}
<div>
<label className="block text-sm font-semibold text-slate-300 mb-3">📅 Date</label>
<label className="block text-sm font-semibold text-slate-300 mb-3 inline-flex items-center gap-1.5"><CalendarIcon className="w-4 h-4" /> Date</label>
<input
type="date"
value={date}
@@ -81,20 +84,28 @@ export default function DailyEntry({
{/* Pray For */}
<div>
<label className="block text-sm font-semibold text-slate-300 mb-3">🙏 Praying For (Optional)</label>
<label className="block text-sm font-semibold text-slate-300 mb-3 inline-flex items-center gap-1.5"><HandHeart className="w-4 h-4" /> Praying For (Optional)</label>
<input
type="text"
value={prayFor}
onChange={(e) => onPrayForChange(e.target.value)}
placeholder="Person, situation, or concern..."
list="prayfor-options"
className="w-full px-4 py-3 rounded-xl bg-slate-800/60 backdrop-blur-xl border border-slate-700/50 text-white placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
/>
{prayForOptions.length > 0 && (
<datalist id="prayfor-options">
{prayForOptions.map((opt) => (
<option key={opt} value={opt} />
))}
</datalist>
)}
</div>
{/* Prayer Text */}
<div>
<div className="flex items-center justify-between mb-3">
<label className="block text-sm font-semibold text-slate-300">💬 Your Prayer</label>
<label className="block text-sm font-semibold text-slate-300 inline-flex items-center gap-1.5"><PenSquare className="w-4 h-4" /> Your Prayer</label>
<span className="text-xs text-slate-500">{wordCount} words</span>
</div>
<textarea
@@ -116,15 +127,15 @@ export default function DailyEntry({
onChange={(e) => onAnsweredChange?.(e.target.checked)}
className="w-5 h-5 rounded border-slate-600 bg-slate-700 text-green-500 focus:ring-green-500 cursor-pointer"
/>
<span className="font-semibold text-slate-300">✅ Mark as answered</span>
<span className="font-semibold text-slate-300 inline-flex items-center gap-1.5"><CheckCircle2 className="w-4 h-4" /> Mark as answered</span>
</label>
</div>
{/* Answer Details */}
{answered && (
<div className="bg-green-900/20 backdrop-blur-xl rounded-xl p-4 border border-green-500/30 animate-in fade-in-50 duration-300">
<label htmlFor="answerDetails" className="block text-sm font-semibold text-green-400 mb-3">
✨ How was it answered?
<label htmlFor="answerDetails" className="block text-sm font-semibold text-green-400 mb-3 inline-flex items-center gap-1.5">
<Sparkles className="w-4 h-4" /> How was it answered?
</label>
<textarea
id="answerDetails"
@@ -143,7 +154,19 @@ export default function DailyEntry({
disabled={loading}
className="w-full bg-gradient-to-r from-blue-600 via-blue-500 to-purple-600 hover:from-blue-700 hover:via-blue-600 hover:to-purple-700 disabled:from-slate-600 disabled:to-slate-600 text-white font-bold py-4 px-6 rounded-xl transition-all duration-200 disabled:cursor-not-allowed shadow-lg hover:shadow-2xl transform hover:scale-105 border border-blue-400/30 disabled:border-slate-400/30"
>
{loading ? '⏳ Saving...' : isEditing ? '📝 Update Prayer' : '💾 Save Prayer'}
{loading ? (
<span className="inline-flex items-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" /> Saving...
</span>
) : isEditing ? (
<span className="inline-flex items-center gap-2">
<PenSquare className="w-5 h-5" /> Update Prayer
</span>
) : (
<span className="inline-flex items-center gap-2">
<Save className="w-5 h-5" /> Save Prayer
</span>
)}
</button>
</form>
</div>
+22 -20
View File
@@ -2,6 +2,7 @@
import { useState, useEffect } from 'react';
import Clock from './Clock';
import { HandHeart, CheckCircle2, PenSquare, BookOpen, Sparkles, Flame, BarChart3, MessageSquare } from 'lucide-react';
interface Prayer {
id: string;
@@ -130,6 +131,7 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
{/* Header with Personalized Greeting */}
<div className="max-w-md mx-auto mb-12 text-center">
<img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" />
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent mb-3">
{getGreeting()}, {getFirstName()}!
</h1>
@@ -139,8 +141,8 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
You have <span className="font-bold text-blue-400">{activePrayerCount}</span> {activePrayerCount === 1 ? 'thing' : 'things'} that need prayer today.
</p>
{prayedTodayCount > 0 && (
<p className="mt-1">
You've prayed for <span className="font-bold text-green-400">{prayedTodayCount}</span> of them! 🙏
<p className="mt-1 inline-flex items-center gap-2">
<HandHeart className="w-5 h-5 text-green-400" /> You've prayed for <span className="font-bold text-green-400">{prayedTodayCount}</span> of them!
</p>
)}
</div>
@@ -163,7 +165,7 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
<div className="text-blue-200 text-sm font-semibold mb-2">Today's Prayers</div>
<div className="text-5xl font-bold text-white">{todayCount}</div>
</div>
<div className="text-4xl opacity-30">📿</div>
<HandHeart className="w-10 h-10 text-white opacity-20" />
</div>
</div>
@@ -185,14 +187,14 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
{/* Answered Prayers */}
<div className="bg-slate-800/60 backdrop-blur-xl rounded-xl p-4 border border-slate-700/50 hover:border-slate-600 transition-all">
<div className="text-slate-400 text-xs font-semibold mb-2">✅ Answered</div>
<div className="text-slate-400 text-xs font-semibold mb-2 inline-flex items-center gap-1.5"><CheckCircle2 className="w-4 h-4" /> Answered</div>
<div className="text-3xl font-bold text-green-400">{answeredPrayers}</div>
<div className="text-xs text-slate-500 mt-2">{answeredPercentage}% answered</div>
</div>
{/* Streak */}
<div className="bg-slate-800/60 backdrop-blur-xl rounded-xl p-4 border border-slate-700/50 hover:border-slate-600 transition-all">
<div className="text-slate-400 text-xs font-semibold mb-2">🔥 Streak</div>
<div className="text-slate-400 text-xs font-semibold mb-2 inline-flex items-center gap-1.5"><Flame className="w-4 h-4" /> Streak</div>
<div className="text-3xl font-bold text-orange-400">{streak}</div>
<div className="text-xs text-slate-500 mt-2">consecutive days</div>
</div>
@@ -202,14 +204,14 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
<div className="grid grid-cols-2 gap-4 mb-8">
{/* Avg Prayer Length */}
<div className="bg-purple-900/20 backdrop-blur-xl rounded-xl p-4 border border-purple-500/30">
<div className="text-purple-300 text-xs font-semibold mb-2">📊 Avg Length</div>
<div className="text-purple-300 text-xs font-semibold mb-2 inline-flex items-center gap-1.5"><BarChart3 className="w-4 h-4" /> Avg Length</div>
<div className="text-2xl font-bold text-purple-200">{avgPrayerLength}</div>
<div className="text-xs text-purple-400 mt-2">characters</div>
</div>
{/* Most Prayed For */}
<div className="bg-pink-900/20 backdrop-blur-xl rounded-xl p-4 border border-pink-500/30">
<div className="text-pink-300 text-xs font-semibold mb-2">💝 Most Prayed For</div>
<div className="text-pink-300 text-xs font-semibold mb-2 inline-flex items-center gap-1.5"><HandHeart className="w-4 h-4" /> Most Prayed For</div>
{mostPrayedFor ? (
<>
<div className="text-sm font-bold text-pink-200 line-clamp-1">{mostPrayedFor[0]}</div>
@@ -231,7 +233,7 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
onClick={onStartPraying}
className="w-full bg-gradient-to-r from-blue-600 via-blue-500 to-purple-600 hover:from-blue-700 hover:via-blue-600 hover:to-purple-700 text-white font-bold py-4 px-6 rounded-xl transition-all shadow-lg hover:shadow-2xl transform hover:scale-105 border border-blue-400/30 flex items-center justify-center gap-2"
>
<span className="text-xl">✍️</span>
<PenSquare className="w-5 h-5" />
<span>Start New Prayer</span>
</button>
</div>
@@ -243,7 +245,7 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
return (
<div className="max-w-md mx-auto mb-12">
<h2 className="text-lg font-bold text-amber-400 mb-4 flex items-center gap-2">
<span>🙏</span> Things to Pray For
<HandHeart className="w-5 h-5" /> Things to Pray For
</h2>
<div className="bg-amber-900/20 backdrop-blur-xl rounded-xl p-5 border border-amber-500/30 hover:border-amber-400/50 transition-all">
<div className="text-3xl font-bold text-amber-300 mb-2">{pendingPrayers.length}</div>
@@ -269,7 +271,7 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
{recentPrayers.length > 0 && (
<div className="max-w-md mx-auto mb-12">
<h2 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
<span>📖</span> Recent Prayers
<BookOpen className="w-5 h-5" /> Recent Prayers
</h2>
<div className="space-y-3">
{recentPrayers.map((prayer, idx) => (
@@ -279,11 +281,11 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
className="w-full text-left group bg-slate-800/60 backdrop-blur-xl rounded-xl p-4 shadow-md border border-slate-700/50 hover:border-blue-500/50 transition-all hover:shadow-xl hover:bg-slate-800/80 transform hover:translate-x-1"
>
<div className="flex items-start gap-3">
<div className="text-2xl">{prayer.answered ? '✅' : '📝'}</div>
<div className="text-2xl">{prayer.answered ? <CheckCircle2 className="w-6 h-6 text-green-400" /> : <PenSquare className="w-6 h-6 text-blue-400" />}</div>
<div className="flex-1 min-w-0">
{prayer.prayFor && (
<div className="text-xs font-bold text-blue-400 mb-1 uppercase tracking-wider">
🙏 {prayer.prayFor}
<div className="text-xs font-bold text-blue-400 mb-1 uppercase tracking-wider inline-flex items-center gap-1.5">
<HandHeart className="w-3.5 h-3.5" /> {prayer.prayFor}
</div>
)}
<p className="text-sm text-slate-300 line-clamp-2 group-hover:text-slate-200">{prayer.text}</p>
@@ -303,7 +305,7 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
{recentAnswered.length > 0 && (
<div className="max-w-md mx-auto mb-12">
<h2 className="text-lg font-bold text-green-400 mb-4 flex items-center gap-2">
<span>✨</span> Recently Answered
<Sparkles className="w-5 h-5" /> Recently Answered
</h2>
<div className="space-y-3">
{recentAnswered.map((prayer) => (
@@ -312,17 +314,17 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
className="bg-green-900/20 backdrop-blur-xl rounded-xl p-4 border border-green-500/30 hover:border-green-400/50 transition-all"
>
<div className="flex items-start gap-3">
<div className="text-2xl">✅</div>
<CheckCircle2 className="w-6 h-6 text-green-400" />
<div className="flex-1 min-w-0">
{prayer.prayFor && (
<div className="text-xs font-bold text-green-300 mb-1 uppercase tracking-wider">
🙏 {prayer.prayFor}
<div className="text-xs font-bold text-green-300 mb-1 uppercase tracking-wider inline-flex items-center gap-1.5">
<HandHeart className="w-3.5 h-3.5" /> {prayer.prayFor}
</div>
)}
<p className="text-sm text-slate-300 line-clamp-2">{prayer.text}</p>
{prayer.answerDetails && (
<p className="text-xs text-green-400 mt-2 italic">
💬 "{prayer.answerDetails.substring(0, 60)}..."
<p className="text-xs text-green-400 mt-2 italic inline-flex items-center gap-1.5">
<MessageSquare className="w-4 h-4" /> "{prayer.answerDetails.substring(0, 60)}..."
</p>
)}
<div className="text-xs text-slate-500 mt-2">
@@ -339,7 +341,7 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
{/* Empty State */}
{prayers.length === 0 && (
<div className="max-w-md mx-auto text-center py-12">
<div className="text-6xl mb-4 animate-bounce">🙏</div>
<Sparkles className="w-12 h-12 mb-4 text-blue-400 animate-pulse mx-auto" />
<p className="text-white font-semibold mb-2">Start Your Prayer Journey</p>
<p className="text-slate-400 mb-6">
Create your first prayer entry and begin tracking your spiritual journey.
+10 -9
View File
@@ -1,7 +1,7 @@
'use client';
import React, { useState, useMemo } from 'react';
import { BookOpen } from 'lucide-react';
import { BookOpen, Calendar as CalendarIcon, CheckCircle2, Hourglass, XCircle, HandHeart } from 'lucide-react';
interface Prayer {
id: string;
@@ -80,6 +80,7 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
<div className="max-w-2xl mx-auto">
{/* Header */}
<div className="text-center mb-8">
<img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" />
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent mb-2">
<span className="inline-flex items-center gap-2 justify-center">
<BookOpen className="w-7 h-7 text-blue-400" />
@@ -111,7 +112,7 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
}`}
>
📅 Newest
<span className="inline-flex items-center gap-1.5"><CalendarIcon className="w-4 h-4" /> Newest</span>
</button>
<button
onClick={() => setSortOrder('oldest')}
@@ -121,7 +122,7 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
}`}
>
📅 Oldest
<span className="inline-flex items-center gap-1.5"><CalendarIcon className="w-4 h-4" /> Oldest</span>
</button>
</div>
@@ -145,7 +146,7 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
}`}
>
✅ Answered
<span className="inline-flex items-center gap-1.5"><CheckCircle2 className="w-4 h-4" /> Answered</span>
</button>
<button
onClick={() => setFilterAnswered('unanswered')}
@@ -155,7 +156,7 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
}`}
>
⏳ Waiting
<span className="inline-flex items-center gap-1.5"><Hourglass className="w-4 h-4" /> Waiting</span>
</button>
</div>
</div>
@@ -165,7 +166,7 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
{filtered.length === 0 ? (
<div className="text-center py-12">
<p className="text-slate-400 text-lg">
{searchTerm || filterAnswered !== 'all' ? '❌ No prayers match your filters.' : '📝 No prayers yet. Start praying!'}
{searchTerm || filterAnswered !== 'all' ? 'No prayers match your filters.' : 'No prayers yet. Start praying!'}
</p>
{searchTerm && (
<button
@@ -206,12 +207,12 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
>
<div className="flex items-start gap-3">
{prayer.answered && (
<span className="text-2xl">✅</span>
<CheckCircle2 className="w-6 h-6 text-green-400" />
)}
<div className="flex-1 min-w-0">
{prayer.prayFor && (
<p className="text-sm font-medium text-blue-400 mb-2">
🙏 {prayer.prayFor}
<p className="text-sm font-medium text-blue-400 mb-2 inline-flex items-center gap-1.5">
<HandHeart className="w-4 h-4" /> {prayer.prayFor}
</p>
)}
<p className="text-slate-200 line-clamp-3">
+3 -2
View File
@@ -1,6 +1,7 @@
'use client';
import React, { useState, useEffect } from 'react';
import { BellRing, X } from 'lucide-react';
interface ReminderBannerProps {
userEmail?: string;
@@ -58,7 +59,7 @@ export default function ReminderBanner({ userEmail }: ReminderBannerProps) {
<div className="fixed top-0 left-0 right-0 z-40 p-4">
<div className="max-w-md mx-auto bg-gradient-to-r from-blue-600 to-blue-500 rounded-lg shadow-lg p-4 animate-in slide-in-from-top">
<div className="flex items-start gap-3">
<div className="text-2xl mt-1">🙏</div>
<BellRing className="w-6 h-6 text-white mt-1" />
<div className="flex-1">
<h3 className="font-semibold text-white">Time to Pray</h3>
<p className="text-blue-100 text-sm mt-1">
@@ -69,7 +70,7 @@ export default function ReminderBanner({ userEmail }: ReminderBannerProps) {
onClick={() => setShowBanner(false)}
className="text-blue-200 hover:text-white transition-colors mt-1"
>
✕
<X className="w-5 h-5" />
</button>
</div>
<div className="mt-3 flex gap-2">
+13 -4
View File
@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { HandHeart, Sparkles } from 'lucide-react';
import { HandHeart, Sparkles, CheckCircle2, Target } from 'lucide-react';
interface Prayer {
id: string;
@@ -48,6 +48,7 @@ export default function ThingsToPrayFor({ prayers, onEdit, onMarkPrayed }: Thing
<div className="max-w-2xl mx-auto">
{/* Header */}
<div className="text-center mb-12">
<img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" />
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent mb-2">
<span className="inline-flex items-center gap-2 justify-center">
<HandHeart className="w-7 h-7 text-blue-400" />
@@ -73,8 +74,8 @@ export default function ThingsToPrayFor({ prayers, onEdit, onMarkPrayed }: Thing
<div key={topic}>
<div className="flex items-center gap-2 mb-4">
<div className="h-1 flex-1 bg-gradient-to-r from-blue-500/50 to-transparent rounded"></div>
<h2 className="text-xl font-bold text-blue-400 px-4">
🎯 {topic}
<h2 className="text-xl font-bold text-blue-400 px-4 inline-flex items-center gap-2">
<Target className="w-5 h-5" /> {topic}
</h2>
<div className="h-1 flex-1 bg-gradient-to-l from-blue-500/50 to-transparent rounded"></div>
</div>
@@ -114,7 +115,15 @@ export default function ThingsToPrayFor({ prayers, onEdit, onMarkPrayed }: Thing
: 'bg-blue-900/40 text-blue-300 border border-blue-500/30 hover:bg-blue-900/60 hover:border-blue-400/50'
}`}
>
{prayedToday ? '✓ Prayed Today' : '🙏 Mark Prayed'}
{prayedToday ? (
<span className="inline-flex items-center gap-1.5">
<CheckCircle2 className="w-4 h-4" /> Prayed Today
</span>
) : (
<span className="inline-flex items-center gap-1.5">
<HandHeart className="w-4 h-4" /> Mark Prayed
</span>
)}
</button>
<button
onClick={() => onEdit(prayer)}
+8
View File
@@ -0,0 +1,8 @@
export const APP_VERSION = '0.2.0';
export const WHATS_NEW_BRIEF: string[] = [
'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'
];