Initial commit: Complete NodeJS-native setup

- Migrated from Python pre-commit to NodeJS-native solution
- Reorganized documentation structure
- Set up Husky + lint-staged for efficient pre-commit hooks
- Fixed Dockerfile healthcheck issue
- Added comprehensive documentation index
This commit is contained in:
William Valentin
2025-09-06 01:42:48 -07:00
commit e48adbcb00
159 changed files with 24405 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
# EditorConfig helps maintain consistent coding styles
# See https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.{js,jsx,ts,tsx,json,css,scss,html,vue}]
indent_style = space
indent_size = 2
[*.{md,markdown}]
trim_trailing_whitespace = false
[*.{py}]
indent_size = 4
[*.{go}]
indent_style = tab
[Makefile]
indent_style = tab
[*.{yml,yaml}]
indent_size = 2
[*.{sh,bash}]
indent_size = 2
[Dockerfile*]
indent_size = 2
+25
View File
@@ -0,0 +1,25 @@
# Demo Environment Configuration for RxMinder
# This demonstrates the template-based approach
# Application Configuration
APP_NAME=my-rxminder
INGRESS_HOST=rxminder.demo.local
# Docker Image Configuration
DOCKER_IMAGE=my-registry.com/rxminder:demo
# Database Configuration (no base64 encoding needed!)
COUCHDB_USER=admin
COUCHDB_PASSWORD=super-secure-demo-password-123
# Storage Configuration (NEW!)
STORAGE_CLASS=fast-ssd
STORAGE_SIZE=10Gi
# Frontend Configuration
VITE_COUCHDB_URL=http://localhost:5984
VITE_COUCHDB_USER=admin
VITE_COUCHDB_PASSWORD=super-secure-demo-password-123
# Application Settings
APP_BASE_URL=http://rxminder.demo.local
+88
View File
@@ -0,0 +1,88 @@
# Environment Configuration Template
# Copy this file to .env and fill in your actual values
# DO NOT commit .env to version control
# Application Name (used in Kubernetes labels and branding)
APP_NAME=rxminder
# Docker Image Configuration
# Examples:
# - Local registry: localhost:5000/rxminder:latest
# - Docker Hub: rxminder/rxminder:v1.0.0
# - GitHub Container Registry: ghcr.io/username/rxminder:latest
# - AWS ECR: 123456789012.dkr.ecr.us-west-2.amazonaws.com/rxminder:latest
DOCKER_IMAGE=gitea-http.taildb3494.ts.net/will/meds:latest
# CouchDB Configuration
COUCHDB_USER=admin
COUCHDB_PASSWORD=change-this-secure-password
VITE_COUCHDB_URL=http://localhost:5984
VITE_COUCHDB_USER=admin
VITE_COUCHDB_PASSWORD=change-this-secure-password
# Application Configuration
# Base URL for your application (used in email links)
# Development: http://localhost:5173
# Production: https://your-domain.com
APP_BASE_URL=http://localhost:5173
# Kubernetes Ingress Configuration
# Host for Kubernetes ingress (used in deployment)
# Examples: app.rxminder.192.168.1.100.nip.io, rxminder.yourdomain.com
INGRESS_HOST=app.rxminder.192.168.1.100.nip.io
# Kubernetes Storage Configuration
# Storage class for PersistentVolumeClaims
# Common options: longhorn, local-path, standard, gp2, fast-ssd
STORAGE_CLASS=longhorn
# Storage size for CouchDB data
# Examples: 1Gi, 5Gi, 10Gi, 100Gi
STORAGE_SIZE=5Gi
# Mailgun Email Configuration
MAILGUN_API_KEY=your-mailgun-api-key-here
MAILGUN_DOMAIN=your-domain.com
MAILGUN_FROM_EMAIL=noreply@your-domain.com
# Production-specific settings
NODE_ENV=development
# Optional: External CouchDB for production
# VITE_COUCHDB_URL=https://your-couchdb-instance.com:5984
# VITE_COUCHDB_USER=production-user
# VITE_COUCHDB_PASSWORD=super-secure-production-password
# OAuth Configuration (Optional - for production OAuth)
VITE_GOOGLE_CLIENT_ID=your_google_client_id_here
VITE_GITHUB_CLIENT_ID=your_github_client_id_here
# ============================================================================
# CONTAINER REGISTRY CONFIGURATION
# ============================================================================
# Container registry for Docker images
# Examples:
# - GitHub Container Registry: ghcr.io
# - GitLab Container Registry: registry.gitlab.com
# - Gitea Container Registry: gitea.yourdomain.com
# - Docker Hub: docker.io (or leave empty)
CONTAINER_REGISTRY=ghcr.io
# Repository name for container images
# Format: username/repository-name or organization/repository-name
CONTAINER_REPOSITORY=yourusername/rxminder
# Gitea-specific settings
# Repository name in Gitea (alternative to CONTAINER_REPOSITORY)
GITEA_REPOSITORY=yourusername/rxminder
# ============================================================================
# CI/CD CONFIGURATION
# ============================================================================
# Deployment webhook URL for notifications (optional)
DEPLOYMENT_WEBHOOK_URL=
# Image cleanup settings
CLEANUP_OLD_IMAGES=true
+38
View File
@@ -0,0 +1,38 @@
# Production environment configuration
# Application Name (used in Kubernetes labels and branding)
APP_NAME=rxminder
# Docker Image Configuration
# Use a specific tag for production (not :latest)
DOCKER_IMAGE=gitea-http.taildb3494.ts.net/will/meds:v1.2.0
# CouchDB Configuration
COUCHDB_USER=admin
COUCHDB_PASSWORD=change-this-secure-password
VITE_COUCHDB_URL=http://localhost:5984
VITE_COUCHDB_USER=admin
VITE_COUCHDB_PASSWORD=change-this-secure-password
# Application configuration
APP_BASE_URL=https://your-production-domain.com
# Kubernetes Ingress Configuration
INGRESS_HOST=meds.your-production-domain.com
# Kubernetes Storage Configuration
# Production storage class (adjust for your cluster)
STORAGE_CLASS=fast-ssd
# Production storage size (larger for production data)
STORAGE_SIZE=20Gi
# Mailgun configuration for production
MAILGUN_API_KEY=your-production-mailgun-api-key-here
MAILGUN_DOMAIN=your-production-domain.com
MAILGUN_FROM_EMAIL=noreply@your-production-domain.com
# Production-specific settings
NODE_ENV=production
# OAuth Configuration (Optional)
VITE_GOOGLE_CLIENT_ID=your_google_client_id_here
VITE_GITHUB_CLIENT_ID=your_github_client_id_here
+236
View File
@@ -0,0 +1,236 @@
# Gitea Actions Configuration for RxMinder
## Environment Variables
### Required Secrets (Configure in Gitea Repository Settings)
```
GITEA_TOKEN # Gitea access token for registry access
VITE_COUCHDB_PASSWORD # CouchDB password (sensitive)
DEPLOYMENT_WEBHOOK_URL # Optional: webhook for deployment notifications
```
### Repository Variables (Configure in Gitea Repository Settings)
```
VITE_COUCHDB_URL # Default: http://localhost:5984
VITE_COUCHDB_USER # Default: admin
APP_BASE_URL # Default: http://localhost:8080
VITE_GOOGLE_CLIENT_ID # Optional: Google OAuth client ID
VITE_GITHUB_CLIENT_ID # Optional: GitHub OAuth client ID
GITEA_REGISTRY # Container registry URL (e.g., gitea.yourdomain.com)
GITEA_REPOSITORY # Repository name (e.g., username/rxminder)
```
### Environment Variables (.env file)
The scripts will automatically load configuration from your `.env` file. Copy `.env.example` to `.env` and customize:
```bash
# Copy example and customize
cp .env.example .env
# Key variables for container registry:
CONTAINER_REGISTRY=gitea.yourdomain.com
CONTAINER_REPOSITORY=username/rxminder
GITEA_REGISTRY=gitea.yourdomain.com # Alternative to CONTAINER_REGISTRY
GITEA_REPOSITORY=username/rxminder # Alternative to CONTAINER_REPOSITORY
```
## Gitea Actions Features
### Workflows
- **Build & Test**: Multi-platform Docker builds with buildx
- **Security Scanning**: Trivy vulnerability scanning
- **Deployment**: Automated deployment to production
- **Cleanup**: Registry and image cleanup
### Multi-Platform Support
- linux/amd64 (Intel/AMD)
- linux/arm64 (ARM64/Apple Silicon)
### Caching Strategy
- Registry-based caching for faster builds
- Layer caching between builds
- Dependency caching for Node.js/Bun
## Setup Instructions
### 1. Gitea Server Requirements
```bash
# Minimum Gitea version
Gitea >= 1.20.0 with Actions enabled
# Required Gitea features
- Gitea Actions enabled
- Container Registry enabled
- Runners configured
```
### 2. Configure Gitea Runner
```yaml
# .gitea/runners/config.yml (on runner machine)
name: 'rxminder-runner'
labels:
- 'ubuntu-latest'
- 'self-hosted'
capabilities:
- docker
- buildx
```
### 3. Repository Configuration
```bash
# 1. Go to Repository Settings → Actions → Secrets
# Add required secrets and variables
# 2. Go to Repository Settings → Packages
# Enable container registry
# 3. Configure runner labels in workflow files if needed
```
### 4. Local Testing
```bash
# Test Gitea Actions locally with act
# Install: https://github.com/nektos/act
# Test the workflow
act -P ubuntu-latest=catthehacker/ubuntu:act-latest
# Test specific job
act -P ubuntu-latest=catthehacker/ubuntu:act-latest -j build
```
## Deployment Targets
### Docker Compose (Default)
```bash
# Deploys using docker-compose.yml
# Suitable for single-server deployments
./scripts/gitea-deploy.sh production
```
### Kubernetes
```bash
# Deploys to Kubernetes cluster
# Requires kubectl configured
./scripts/gitea-deploy.sh kubernetes
```
### Staging Environment
```bash
# Deploys to staging with different configs
./scripts/gitea-deploy.sh staging
```
## Monitoring & Notifications
### Health Checks
- Frontend: `http://localhost:8080/health`
- CouchDB: `http://localhost:5984/_up`
### Deployment Notifications
Configure `DEPLOYMENT_WEBHOOK_URL` to receive notifications:
```json
{
"text": "✅ RxMinder deployed to production",
"environment": "production",
"image": "gitea.example.com/user/rxminder:abc123"
}
```
## Troubleshooting
### Common Issues
1. **Build Fails - Buildx Not Available**
```bash
# Ensure Docker Buildx is installed on runner
docker buildx version
```
2. **Registry Push Fails**
```bash
# Check GITEA_TOKEN has package write permissions
# Verify registry URL is correct
```
3. **Deployment Fails**
```bash
# Check environment variables are set
# Verify server has Docker/Kubernetes access
```
### Debug Commands
```bash
# Check workflow logs in Gitea UI
# Repository → Actions → [Workflow Run]
# Test deployment script locally
./scripts/gitea-deploy.sh production --debug
# Check service status
docker-compose -f docker/docker-compose.yaml ps
docker-compose -f docker/docker-compose.yaml logs
```
## Security Considerations
### Image Scanning
- Trivy vulnerability scanning in CI
- Base image security updates
- Dependency audit checks
### Secrets Management
- Use Gitea secrets for sensitive data
- Rotate access tokens regularly
- Limit token permissions
### Registry Security
- Private registry recommended
- Image signing (optional)
- Regular image cleanup
## Performance Optimization
### Build Optimization
- Multi-stage Dockerfile
- Layer caching
- Minimal base images
### Deployment Optimization
- Health checks
- Rolling updates
- Resource limits
## Migration from GitHub Actions
If migrating from GitHub Actions:
1. **Copy workflow structure** (already compatible)
2. **Update variable references**: `github.` → `gitea.`
3. **Configure secrets** in Gitea repository settings
4. **Test locally** with act before pushing
5. **Update registry URLs** if different
+45
View File
@@ -0,0 +1,45 @@
# Gitea Actions CI/CD Docker Compose Override
# This file provides CI-specific configurations for Gitea Actions
version: '3.8'
services:
# Frontend service with CI optimizations
frontend:
build:
context: .
target: builder
cache_from:
- ${REGISTRY:-gitea.example.com}/${IMAGE_NAME:-rxminder}:buildcache
args:
# Use build args from CI environment
- VITE_COUCHDB_URL=${VITE_COUCHDB_URL:-http://couchdb:5984}
- VITE_COUCHDB_USER=${VITE_COUCHDB_USER:-admin}
- VITE_COUCHDB_PASSWORD=${VITE_COUCHDB_PASSWORD:-change-this-secure-password}
- APP_BASE_URL=${APP_BASE_URL:-http://localhost:8080}
- VITE_GOOGLE_CLIENT_ID=${VITE_GOOGLE_CLIENT_ID:-}
- VITE_GITHUB_CLIENT_ID=${VITE_GITHUB_CLIENT_ID:-}
- NODE_ENV=production
environment:
- CI=true
labels:
- 'gitea.ci=true'
- 'gitea.project=rxminder'
# Test database for CI
couchdb-test:
image: couchdb:3.3.2
environment:
- COUCHDB_USER=admin
- COUCHDB_PASSWORD=test-secure-password
ports:
- '5985:5984'
volumes:
- couchdb_test_data:/opt/couchdb/data
labels:
- 'gitea.ci=true'
- 'gitea.service=test-database'
volumes:
couchdb_test_data:
driver: local
+156
View File
@@ -0,0 +1,156 @@
# Gitea-specific Docker Bake file for advanced multi-platform builds
# Usage: docker buildx bake -f gitea-bake.hcl
variable "GITEA_REGISTRY" {
default = notequal("", GITEA_REGISTRY) ? GITEA_REGISTRY : "ghcr.io"
}
variable "GITEA_REPOSITORY" {
default = notequal("", GITEA_REPOSITORY) ? GITEA_REPOSITORY : "user/rxminder"
}
variable "TAG" {
default = "latest"
}
variable "GITEA_SHA" {
default = "dev"
}
variable "VITE_COUCHDB_URL" {
default = "http://localhost:5984"
}
variable "VITE_COUCHDB_USER" {
default = "admin"
}
variable "VITE_COUCHDB_PASSWORD" {
default = "change-this-secure-password"
}
variable "APP_BASE_URL" {
default = "http://localhost:8080"
}
variable "VITE_GOOGLE_CLIENT_ID" {
default = ""
}
variable "VITE_GITHUB_CLIENT_ID" {
default = ""
}
group "default" {
targets = ["app"]
}
group "ci" {
targets = ["app-ci"]
}
target "app" {
dockerfile = "Dockerfile"
context = "."
platforms = [
"linux/amd64",
"linux/arm64"
]
tags = [
"${GITEA_REGISTRY}/${GITEA_REPOSITORY}:${TAG}",
"${GITEA_REGISTRY}/${GITEA_REPOSITORY}:latest"
]
args = {
VITE_COUCHDB_URL = "${VITE_COUCHDB_URL}"
VITE_COUCHDB_USER = "${VITE_COUCHDB_USER}"
VITE_COUCHDB_PASSWORD = "${VITE_COUCHDB_PASSWORD}"
APP_BASE_URL = "${APP_BASE_URL}"
VITE_GOOGLE_CLIENT_ID = "${VITE_GOOGLE_CLIENT_ID}"
VITE_GITHUB_CLIENT_ID = "${VITE_GITHUB_CLIENT_ID}"
NODE_ENV = "production"
}
# Gitea registry caching
cache-from = [
"type=registry,ref=${GITEA_REGISTRY}/${GITEA_REPOSITORY}:buildcache"
]
cache-to = [
"type=registry,ref=${GITEA_REGISTRY}/${GITEA_REPOSITORY}:buildcache,mode=max"
]
}
# CI-specific target with commit SHA tagging
target "app-ci" {
inherits = ["app"]
tags = [
"${GITEA_REGISTRY}/${GITEA_REPOSITORY}:${GITEA_SHA}",
"${GITEA_REGISTRY}/${GITEA_REPOSITORY}:latest"
]
# Enhanced CI-specific features
attest = [
"type=provenance,mode=max",
"type=sbom"
]
# CI registry push
output = ["type=registry"]
}
# Development target for local builds
target "dev" {
inherits = ["app"]
platforms = ["linux/amd64"]
tags = ["rxminder:dev"]
# Local caching only
cache-from = ["type=registry,ref=${GITEA_REGISTRY}/${GITEA_REPOSITORY}:buildcache"]
cache-to = ["type=registry,ref=${GITEA_REGISTRY}/${GITEA_REPOSITORY}:buildcache"]
# Load locally instead of push
output = ["type=docker"]
}
# Production target with full attestations
target "prod" {
inherits = ["app-ci"]
# Production-specific tags
tags = [
"${GITEA_REGISTRY}/${GITEA_REPOSITORY}:prod-${TAG}",
"${GITEA_REGISTRY}/${GITEA_REPOSITORY}:production"
]
# Full security attestations for production
attest = [
"type=provenance,mode=max",
"type=sbom"
]
}
# Staging target
target "staging" {
inherits = ["app"]
platforms = ["linux/amd64"] # Single platform for staging
tags = [
"${GITEA_REGISTRY}/${GITEA_REPOSITORY}:staging-${TAG}",
"${GITEA_REGISTRY}/${GITEA_REPOSITORY}:staging"
]
# Staging-specific build args
args = {
VITE_COUCHDB_URL = "${VITE_COUCHDB_URL}"
VITE_COUCHDB_USER = "${VITE_COUCHDB_USER}"
VITE_COUCHDB_PASSWORD = "${VITE_COUCHDB_PASSWORD}"
APP_BASE_URL = "http://staging.localhost:8080"
VITE_GOOGLE_CLIENT_ID = "${VITE_GOOGLE_CLIENT_ID}"
VITE_GITHUB_CLIENT_ID = "${VITE_GITHUB_CLIENT_ID}"
NODE_ENV = "staging"
}
output = ["type=registry"]
}
+178
View File
@@ -0,0 +1,178 @@
name: Build and Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
# Use environment variables for registry configuration
REGISTRY: ${{ vars.GITEA_REGISTRY || secrets.GITEA_REGISTRY || 'ghcr.io' }}
IMAGE_NAME: ${{ gitea.repository }}
jobs:
build:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
if: gitea.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./docker
platforms: linux/amd64,linux/arm64
push: ${{ gitea.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VITE_COUCHDB_URL=${{ vars.VITE_COUCHDB_URL || 'http://localhost:5984' }}
VITE_COUCHDB_USER=${{ vars.VITE_COUCHDB_USER || 'admin' }}
VITE_COUCHDB_PASSWORD=${{ secrets.VITE_COUCHDB_PASSWORD || 'change-this-secure-password' }}
APP_BASE_URL=${{ vars.APP_BASE_URL || 'http://localhost:8080' }}
VITE_GOOGLE_CLIENT_ID=${{ vars.VITE_GOOGLE_CLIENT_ID || '' }}
VITE_GITHUB_CLIENT_ID=${{ vars.VITE_GITHUB_CLIENT_ID || '' }}
NODE_ENV=production
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
- name: Build with Bake (Alternative)
if: false # Set to true to use bake instead
uses: docker/bake-action@v4
with:
workdir: ./docker
files: docker-bake.hcl
targets: prod
push: ${{ gitea.event_name != 'pull_request' }}
test:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run linting
run: bun run lint
- name: Run type checking
run: bun run type-check
- name: Run tests
run: bun run test
- name: Run integration tests
run: bun run test:integration
security:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
if: gitea.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run security audit
run: |
# Install and run security audit tools
npm audit --audit-level moderate || true
- name: Scan Docker image for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ gitea.sha }}'
format: 'table'
exit-code: '0'
deploy:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
needs: [build, test]
if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push'
environment: production
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Deploy to production
run: |
echo "Deploying to production server..."
# Example deployment script
# You would typically SSH to your server and update the containers
# Install kubectl if deploying to Kubernetes
# curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
# chmod +x kubectl && sudo mv kubectl /usr/local/bin/
# Or deploy via docker-compose
# ssh user@server "cd /app && docker-compose pull && docker-compose up -d"
echo "Deployment placeholder - configure your deployment method"
- name: Notify deployment status
if: always()
run: |
if [ "${{ job.status }}" == "success" ]; then
echo "✅ Deployment successful"
# Send success notification (webhook, email, etc.)
else
echo "❌ Deployment failed"
# Send failure notification
fi
cleanup:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
needs: [build, test, deploy]
if: always() && gitea.ref == 'refs/heads/main'
steps:
- name: Cleanup old images
run: |
echo "Cleaning up old container images..."
# Add cleanup logic for old images in registry
# This helps manage storage costs
echo "Cleanup placeholder - implement registry cleanup"
+90
View File
@@ -0,0 +1,90 @@
# Bug Report
## 🐛 Bug Description
A clear and concise description of the bug.
## 🔄 Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
## ✅ Expected Behavior
A clear and concise description of what you expected to happen.
## ❌ Actual Behavior
A clear and concise description of what actually happened.
## 📱 Environment
**Desktop:**
- OS: [e.g. Windows 10, macOS 12.0, Ubuntu 20.04]
- Browser: [e.g. Chrome 96, Firefox 95, Safari 15]
- Version: [e.g. 22]
**Mobile:**
- Device: [e.g. iPhone 13, Samsung Galaxy S21]
- OS: [e.g. iOS 15.1, Android 12]
- Browser: [e.g. Safari, Chrome]
**Application:**
- Version: [e.g. 1.2.0]
- Environment: [e.g. Local Development, Production]
- Authentication Method: [e.g. Email/Password, Google OAuth]
## 📸 Screenshots
If applicable, add screenshots to help explain your problem.
## 📝 Additional Context
Add any other context about the problem here.
### Error Messages
```
Paste any error messages here
```
### Console Logs
```
Paste relevant console logs here
```
### Network Requests
If the issue involves API calls, include relevant network request/response information.
## 🔧 Troubleshooting Attempted
- [ ] Cleared browser cache
- [ ] Tried incognito/private browsing
- [ ] Checked browser console for errors
- [ ] Verified internet connection
- [ ] Tried different browser
- [ ] Logged out and back in
## 🏥 Medical Context (if applicable)
- [ ] This affects medication reminders
- [ ] This affects dose tracking
- [ ] This could impact patient safety
- [ ] This involves sensitive health data
**Priority Level:** [Low / Medium / High / Critical]
## 📋 Checklist
- [ ] I have searched existing issues to ensure this is not a duplicate
- [ ] I have provided clear steps to reproduce
- [ ] I have included environment details
- [ ] I have added relevant screenshots/logs
- [ ] I have marked appropriate priority level
+194
View File
@@ -0,0 +1,194 @@
name: 🐛 Bug Report
description: Report a bug or issue with the application
title: '[BUG] '
labels: ['bug', 'needs-triage']
assignees: []
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to report a bug! Please fill out the information below to help us reproduce and fix the issue quickly.
- type: checkboxes
id: pre-check
attributes:
label: Pre-submission Checklist
description: Please verify these items before submitting
options:
- label: I have searched existing issues to ensure this is not a duplicate
required: true
- label: I have provided clear steps to reproduce the issue
required: true
- label: I have tested this in the latest version
required: true
- type: textarea
id: description
attributes:
label: Bug Description
description: A clear and concise description of what the bug is
placeholder: Describe the bug...
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to Reproduce
description: Clear steps to reproduce the behavior
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What you expected to happen
placeholder: I expected...
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
description: What actually happened
placeholder: Instead...
validations:
required: true
- type: dropdown
id: severity
attributes:
label: Severity
description: How severe is this issue?
options:
- Low - Minor inconvenience
- Medium - Noticeable issue that doesn't block usage
- High - Significant issue that impacts functionality
- Critical - Application is unusable or data loss
validations:
required: true
- type: dropdown
id: environment
attributes:
label: Environment
description: Where did this occur?
options:
- Local Development
- Docker Environment
- Production
- Staging
validations:
required: true
- type: input
id: os
attributes:
label: Operating System
description: Your operating system
placeholder: e.g., Windows 11, macOS 13.0, Ubuntu 22.04
validations:
required: true
- type: input
id: browser
attributes:
label: Browser
description: Browser and version
placeholder: e.g., Chrome 119, Firefox 118, Safari 17
validations:
required: true
- type: input
id: version
attributes:
label: Application Version
description: Version of the application
placeholder: e.g., 1.2.0
validations:
required: false
- type: dropdown
id: auth-method
attributes:
label: Authentication Method
description: How were you authenticated when this occurred?
options:
- Not authenticated
- Email/Password
- Google OAuth
- GitHub OAuth
- Admin Account
validations:
required: false
- type: textarea
id: error-messages
attributes:
label: Error Messages
description: Any error messages you received
placeholder: Paste error messages here...
render: text
validations:
required: false
- type: textarea
id: console-logs
attributes:
label: Console Logs
description: Relevant browser console logs
placeholder: Paste console logs here...
render: text
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Add screenshots to help explain the problem
placeholder: Drag and drop screenshots here...
validations:
required: false
- type: checkboxes
id: troubleshooting
attributes:
label: Troubleshooting Attempted
description: What troubleshooting steps have you tried?
options:
- label: Cleared browser cache
- label: Tried incognito/private browsing
- label: Checked browser console for errors
- label: Verified internet connection
- label: Tried different browser
- label: Logged out and back in
- label: Restarted the application
- type: checkboxes
id: medical-impact
attributes:
label: Medical Context
description: Does this issue affect medication management? (Check all that apply)
options:
- label: Affects medication reminders
- label: Affects dose tracking
- label: Could impact patient safety
- label: Involves sensitive health data
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Any other context about the problem
placeholder: Add any other context here...
validations:
required: false
+120
View File
@@ -0,0 +1,120 @@
name: 📚 Documentation Issue
description: Report problems with documentation or suggest improvements
title: '[DOCS] '
labels: ['documentation', 'needs-triage']
assignees: []
body:
- type: markdown
attributes:
value: |
Help us improve our documentation! Report issues or suggest enhancements to make it more helpful.
- type: dropdown
id: doc-type
attributes:
label: Documentation Type
description: What type of documentation needs attention?
options:
- README.md
- API Documentation
- Security Guide
- Deployment Guide
- Contributing Guide
- Code Comments
- User Guide
- Setup Instructions
- Other
validations:
required: true
- type: dropdown
id: issue-type
attributes:
label: Issue Type
description: What kind of documentation issue is this?
options:
- Missing information
- Incorrect information
- Unclear instructions
- Outdated content
- Broken links
- Formatting issues
- Spelling/grammar errors
- Enhancement suggestion
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Describe the documentation issue or improvement
placeholder: The documentation issue is...
validations:
required: true
- type: input
id: location
attributes:
label: Document Location
description: Which file or section needs attention?
placeholder: e.g., README.md line 45, docs/API.md section "Authentication"
validations:
required: true
- type: textarea
id: current-content
attributes:
label: Current Content (if applicable)
description: Quote the current text that needs to be changed
placeholder: Current text...
validations:
required: false
- type: textarea
id: suggested-content
attributes:
label: Suggested Content
description: Provide the corrected or improved content
placeholder: Suggested replacement...
validations:
required: false
- type: dropdown
id: audience
attributes:
label: Target Audience
description: Who is the primary audience for this documentation?
options:
- End users
- Developers
- System administrators
- Healthcare providers
- Contributors
- All audiences
validations:
required: true
- type: checkboxes
id: impact
attributes:
label: Impact
description: What areas does this documentation issue affect?
options:
- label: New user onboarding
- label: Development setup
- label: Deployment process
- label: Security configuration
- label: API usage
- label: Troubleshooting
- label: Contributing process
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Any other relevant information
placeholder: Additional context...
validations:
required: false
+173
View File
@@ -0,0 +1,173 @@
# Feature Request
## 🚀 Feature Description
A clear and concise description of the feature you'd like to see implemented.
## 💡 Problem Statement
What problem does this feature solve? What user need does it address?
## 🎯 Proposed Solution
Describe your proposed solution in detail. How should this feature work?
## 🏥 Medical Use Case
Explain how this feature would improve medication management or patient outcomes.
## 📱 User Experience
Describe the user journey and interface for this feature.
### **User Interface Mockups**
If applicable, add mockups, wireframes, or descriptions of the UI.
### **User Flow**
1. User navigates to...
2. User clicks/taps...
3. System displays...
4. User completes...
## 🔧 Technical Considerations
### **Implementation Complexity**
- [ ] Simple (few hours)
- [ ] Medium (few days)
- [ ] Complex (few weeks)
- [ ] Major (significant architecture changes)
### **Affected Components**
- [ ] Frontend UI
- [ ] Authentication system
- [ ] Database schema
- [ ] Email notifications
- [ ] Mobile responsiveness
- [ ] API endpoints
- [ ] Third-party integrations
### **Dependencies**
List any external libraries, services, or system changes needed.
## 🎨 Design Requirements
### **Visual Design**
- [ ] Follows existing design system
- [ ] Requires new design patterns
- [ ] Needs accessibility considerations
- [ ] Mobile-first approach needed
### **Responsive Behavior**
Describe how this feature should work on different screen sizes.
## 📊 Success Metrics
How will we measure the success of this feature?
- [ ] User engagement metrics
- [ ] Medication adherence improvement
- [ ] User satisfaction scores
- [ ] Performance metrics
- [ ] Error rate reduction
## 🔒 Security & Privacy
### **Data Handling**
- [ ] Handles sensitive health data
- [ ] Requires data encryption
- [ ] Needs audit logging
- [ ] Affects user privacy
### **Compliance**
- [ ] GDPR considerations
- [ ] HIPAA considerations (if applicable)
- [ ] Data retention policies
- [ ] User consent requirements
## 🌍 Accessibility
- [ ] Screen reader compatible
- [ ] Keyboard navigation support
- [ ] Color contrast compliant
- [ ] Mobile accessibility
- [ ] Language localization needed
## 🔄 Alternative Solutions
What other approaches have you considered? Why is this the preferred solution?
## 📚 Additional Context
### **Similar Features**
Are there similar features in other applications that work well?
### **User Research**
Any user feedback, surveys, or research supporting this feature?
### **Priority Justification**
Why should this feature be prioritized?
## 🎯 Acceptance Criteria
Define specific, testable criteria for when this feature is complete:
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] Criterion 3
## 📋 Implementation Plan (Optional)
If you have ideas for implementation:
### **Phase 1: Foundation**
- [ ] Database changes
- [ ] API endpoints
- [ ] Basic UI components
### **Phase 2: Core Feature**
- [ ] Main functionality
- [ ] User interface
- [ ] Basic testing
### **Phase 3: Polish**
- [ ] Advanced features
- [ ] Performance optimization
- [ ] Comprehensive testing
## 🏷️ Labels
Please suggest appropriate labels:
- [ ] enhancement
- [ ] ui/ux
- [ ] backend
- [ ] frontend
- [ ] security
- [ ] accessibility
- [ ] documentation
- [ ] high-priority
- [ ] good-first-issue
## 📋 Checklist
- [ ] I have searched existing issues to ensure this is not a duplicate
- [ ] I have clearly described the problem and solution
- [ ] I have considered the user experience impact
- [ ] I have thought about technical implementation
- [ ] I have considered security and privacy implications
- [ ] I have defined success criteria
+218
View File
@@ -0,0 +1,218 @@
name: ✨ Feature Request
description: Suggest a new feature or enhancement
title: '[FEATURE] '
labels: ['enhancement', 'needs-triage']
assignees: []
body:
- type: markdown
attributes:
value: |
Thank you for suggesting a new feature! Please provide detailed information to help us understand and evaluate your request.
- type: checkboxes
id: pre-check
attributes:
label: Pre-submission Checklist
description: Please verify these items before submitting
options:
- label: I have searched existing issues to ensure this is not a duplicate
required: true
- label: I have clearly described the problem and solution
required: true
- label: I have considered the user experience impact
required: true
- type: textarea
id: feature-description
attributes:
label: Feature Description
description: A clear and concise description of the feature you'd like to see
placeholder: Describe the feature...
validations:
required: true
- type: textarea
id: problem-statement
attributes:
label: Problem Statement
description: What problem does this feature solve? What user need does it address?
placeholder: This feature would solve...
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: Proposed Solution
description: Describe your proposed solution in detail
placeholder: The feature should work by...
validations:
required: true
- type: textarea
id: medical-use-case
attributes:
label: Medical Use Case
description: How would this improve medication management or patient outcomes?
placeholder: This would help patients by...
validations:
required: true
- type: dropdown
id: user-type
attributes:
label: Primary User Type
description: Who would primarily benefit from this feature?
options:
- Patients managing their own medications
- Caregivers managing medications for others
- Healthcare providers
- System administrators
- All users
validations:
required: true
- type: dropdown
id: priority
attributes:
label: Priority Level
description: How important is this feature?
options:
- Low - Nice to have enhancement
- Medium - Would improve user experience
- High - Important for better outcomes
- Critical - Essential for core functionality
validations:
required: true
- type: dropdown
id: complexity
attributes:
label: Implementation Complexity
description: How complex do you think this feature would be to implement?
options:
- Simple (few hours)
- Medium (few days)
- Complex (few weeks)
- Major (significant architecture changes)
- Unknown
validations:
required: false
- type: checkboxes
id: affected-components
attributes:
label: Affected Components
description: Which parts of the system would this feature affect?
options:
- label: Frontend UI
- label: Authentication system
- label: Database schema
- label: Email notifications
- label: Mobile responsiveness
- label: API endpoints
- label: Third-party integrations
- label: Admin interface
- type: textarea
id: user-flow
attributes:
label: User Experience Flow
description: Describe the user journey for this feature
placeholder: |
1. User navigates to...
2. User clicks/taps...
3. System displays...
4. User completes...
validations:
required: true
- type: textarea
id: success-metrics
attributes:
label: Success Metrics
description: How will we measure the success of this feature?
placeholder: |
- User engagement metrics
- Medication adherence improvement
- User satisfaction scores
validations:
required: false
- type: checkboxes
id: design-requirements
attributes:
label: Design Requirements
description: What design considerations are important?
options:
- label: Follows existing design system
- label: Requires new design patterns
- label: Needs accessibility considerations
- label: Mobile-first approach needed
- label: Dark/light theme support
- type: checkboxes
id: security-privacy
attributes:
label: Security & Privacy Considerations
description: Does this feature involve sensitive data or security concerns?
options:
- label: Handles sensitive health data
- label: Requires data encryption
- label: Needs audit logging
- label: Affects user privacy
- label: Requires user consent
- label: GDPR compliance needed
- label: HIPAA considerations
- type: checkboxes
id: accessibility
attributes:
label: Accessibility Requirements
description: What accessibility features are needed?
options:
- label: Screen reader compatible
- label: Keyboard navigation support
- label: Color contrast compliant
- label: Mobile accessibility
- label: Language localization needed
- type: textarea
id: alternatives
attributes:
label: Alternative Solutions
description: What other approaches have you considered?
placeholder: I also considered...
validations:
required: false
- type: textarea
id: acceptance-criteria
attributes:
label: Acceptance Criteria
description: Define specific, testable criteria for when this feature is complete
placeholder: |
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] Criterion 3
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Any other context, mockups, or references
placeholder: Additional information...
validations:
required: false
- type: textarea
id: similar-features
attributes:
label: Similar Features
description: Are there similar features in other applications that work well?
placeholder: Similar implementations I've seen...
validations:
required: false
+179
View File
@@ -0,0 +1,179 @@
# Pull Request
## 📝 Description
Brief description of changes made in this pull request.
## 🔗 Related Issues
Fixes #(issue_number)
Closes #(issue_number)
Related to #(issue_number)
## 🎯 Type of Change
- [ ] 🐛 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] 📚 Documentation update
- [ ] 🔧 Code refactoring (no functional changes)
- [ ] ⚡ Performance improvement
- [ ] 🧪 Test coverage improvement
- [ ] 🔒 Security enhancement
- [ ] 🎨 UI/UX improvement
## 🧪 Testing
Describe the tests you ran to verify your changes.
### **Test Environment**
- [ ] Local development
- [ ] Docker environment
- [ ] Production-like environment
### **Test Cases**
- [ ] Unit tests pass
- [ ] Integration tests pass
- [ ] Manual testing completed
- [ ] Cross-browser testing (if UI changes)
- [ ] Mobile testing (if responsive changes)
- [ ] Accessibility testing (if UI changes)
### **New Tests Added**
- [ ] Unit tests for new functionality
- [ ] Integration tests for API changes
- [ ] End-to-end tests for user flows
## 📱 Screenshots (if applicable)
Include screenshots for UI changes.
### **Before**
[Add screenshot of current state]
### **After**
[Add screenshot of new state]
### **Mobile View**
[Add mobile screenshots if applicable]
## 🔒 Security Considerations
- [ ] No sensitive data exposed in logs
- [ ] Authentication/authorization properly implemented
- [ ] Input validation in place
- [ ] SQL injection prevention (if applicable)
- [ ] XSS prevention (if applicable)
- [ ] CSRF protection maintained
## 📊 Performance Impact
- [ ] No performance degradation
- [ ] Performance improvements measured
- [ ] Database queries optimized
- [ ] Bundle size impact acceptable
- [ ] Memory usage acceptable
## 🔄 Breaking Changes
If this is a breaking change, describe:
1. What breaks
2. Migration path for users
3. Version bump requirements
## 📚 Documentation
- [ ] Code is self-documenting with clear naming
- [ ] Complex logic has comments
- [ ] API documentation updated (if applicable)
- [ ] README updated (if applicable)
- [ ] User documentation updated (if applicable)
## 📋 Checklist
### **Code Quality**
- [ ] Code follows project style guidelines
- [ ] Self-review of code completed
- [ ] Code is commented where necessary
- [ ] No console.log statements left in code
- [ ] Error handling implemented properly
### **Testing & Validation**
- [ ] All tests pass locally
- [ ] TypeScript compilation succeeds
- [ ] ESLint passes without errors
- [ ] Build succeeds without warnings
- [ ] Manual testing completed
### **Review Preparation**
- [ ] Pull request title is descriptive
- [ ] Pull request description is complete
- [ ] Commits are atomic and well-described
- [ ] No merge conflicts
- [ ] Base branch is correct
### **Deployment Readiness**
- [ ] Environment variables documented (if new)
- [ ] Database migrations included (if needed)
- [ ] Docker configuration updated (if needed)
- [ ] Deployment scripts updated (if needed)
## 🎯 Review Focus Areas
Please pay special attention to:
- [ ] Security implications
- [ ] Performance impact
- [ ] Error handling
- [ ] User experience
- [ ] Code maintainability
- [ ] Test coverage
## 📝 Additional Notes
Any additional information for reviewers:
### **Design Decisions**
Explain any significant design or architecture decisions made.
### **Trade-offs**
Describe any trade-offs made and why they were necessary.
### **Future Work**
List any follow-up work that should be done in future PRs.
## 🚀 Deployment Notes
Special considerations for deployment:
- [ ] Requires environment variable changes
- [ ] Requires database migration
- [ ] Requires cache clearing
- [ ] Requires dependency updates
- [ ] No special deployment requirements
---
**Reviewer Checklist:**
- [ ] Code review completed
- [ ] Tests reviewed and verified
- [ ] Documentation reviewed
- [ ] Security review completed (if applicable)
- [ ] Performance review completed (if applicable)
- [ ] Breaking changes noted and approved
- [ ] Deployment considerations reviewed
+110
View File
@@ -0,0 +1,110 @@
name: Build and Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./docker
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VITE_COUCHDB_URL=${{ vars.VITE_COUCHDB_URL || 'http://localhost:5984' }}
VITE_COUCHDB_USER=${{ vars.VITE_COUCHDB_USER || 'admin' }}
VITE_COUCHDB_PASSWORD=${{ secrets.VITE_COUCHDB_PASSWORD || 'change-this-secure-password' }}
APP_BASE_URL=${{ vars.APP_BASE_URL || 'http://localhost:8080' }}
VITE_GOOGLE_CLIENT_ID=${{ vars.VITE_GOOGLE_CLIENT_ID || '' }}
VITE_GITHUB_CLIENT_ID=${{ vars.VITE_GITHUB_CLIENT_ID || '' }}
NODE_ENV=production
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build with Bake (Alternative)
if: false # Set to true to use bake instead
uses: docker/bake-action@v4
with:
workdir: ./docker
files: docker-bake.hcl
targets: prod
push: ${{ github.event_name != 'pull_request' }}
test:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run linting
run: bun run lint
- name: Run type checking
run: bun run type-check
- name: Run tests
run: bun run test
deploy:
runs-on: ubuntu-latest
needs: [build, test]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- name: Deploy to production
run: |
echo "Deploy to production server"
# Add your deployment commands here
# Example: SSH to server and pull the new image
+38
View File
@@ -0,0 +1,38 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment files (contain sensitive data)
.env
.env.local
# .env.production - committed with placeholder values for deployment
.env.staging
# Database data
couchdb-data/
# Playwright artifacts
test-results/
playwright-report/
playwright/.cache/
+8
View File
@@ -0,0 +1,8 @@
# Optional: Add commit message linting here if needed
# For now, just ensure commit message is not empty
if [ -z "$(cat $1 | head -1)" ]; then
echo "Error: Commit message cannot be empty"
exit 1
fi
echo "✅ Commit message looks good!"
+4
View File
@@ -0,0 +1,4 @@
# Run lint-staged for file-specific checks
bunx lint-staged
echo "✅ Pre-commit checks passed!"
+18
View File
@@ -0,0 +1,18 @@
{
"default": true,
"MD001": true,
"MD003": {
"style": "atx"
},
"MD007": {
"indent": 2
},
"MD013": {
"line_length": 120
},
"MD024": {
"allow_different_nesting": true
},
"MD033": false,
"MD041": false
}
+45
View File
@@ -0,0 +1,45 @@
# Prettier ignore file
# See https://prettier.io/docs/en/ignore.html
# Dependencies
node_modules/
dist/
build/
# Generated files
*.min.js
*.min.css
# Lock files
package-lock.json
yarn.lock
pnpm-lock.yaml
bun.lockb
# Logs
*.log
# Coverage directory used by tools like istanbul
coverage/
# Docker
Dockerfile*
.dockerignore
# Git
.git/
# IDE
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db
# CouchDB data
couchdb-data/
# Test outputs
test-results/
playwright-report/
+41
View File
@@ -0,0 +1,41 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf",
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"overrides": [
{
"files": "*.md",
"options": {
"printWidth": 120,
"proseWrap": "preserve"
}
},
{
"files": "*.json",
"options": {
"printWidth": 120
}
},
{
"files": "*.yml",
"options": {
"tabWidth": 2
}
},
{
"files": "*.yaml",
"options": {
"tabWidth": 2
}
}
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"rules": [
{
"id": "@secretlint/secretlint-rule-preset-recommend"
}
],
"allowMessageIds": [],
"disabledMessages": [],
"reporterOptions": {
"formatter": "table"
}
}
+939
View File
@@ -0,0 +1,939 @@
import React, {
useState,
useEffect,
useMemo,
useCallback,
useRef,
} from 'react';
import { generateSchedule, generateReminderSchedule } from './utils/schedule';
import {
Medication,
Dose,
DoseStatus,
HistoricalDose,
User,
UserSettings,
TakenDoses,
CustomReminder,
ScheduleItem,
DailyStat,
MedicationStat,
UserRole,
} from './types';
// Component imports - organized by feature
import {
AddMedicationModal,
EditMedicationModal,
ManageMedicationsModal,
DoseCard,
} from './components/medication';
import {
AuthPage,
AvatarDropdown,
ChangePasswordModal,
} from './components/auth';
import { AdminInterface } from './components/admin';
import {
AccountModal,
AddReminderModal,
EditReminderModal,
HistoryModal,
ManageRemindersModal,
OnboardingModal,
StatsModal,
} from './components/modals';
import { BarChart, ReminderCard, ThemeSwitcher } from './components/ui';
// Icon and utility imports
import {
PillIcon,
PlusIcon,
MenuIcon,
HistoryIcon,
SunIcon,
SunsetIcon,
MoonIcon,
SearchIcon,
SettingsIcon,
BellIcon,
BarChartIcon,
} from './components/icons/Icons';
import { useUser } from './contexts/UserContext';
import { dbService } from './services/couchdb.factory';
import { databaseSeeder } from './services/database.seeder';
const Header: React.FC<{
onAdd: () => void;
onManage: () => void;
onManageReminders: () => void;
onHistory: () => void;
onStats: () => void;
onAccount: () => void;
onAdmin: () => void;
onChangePassword: () => void;
user: User;
onLogout: () => void;
}> = ({
onAdd,
onManage,
onManageReminders,
onHistory,
onStats,
onAccount,
onAdmin,
onChangePassword,
user,
onLogout,
}) => (
<header className='bg-white dark:bg-slate-800 shadow-md sticky top-0 z-20 border-b border-slate-200 dark:border-slate-700'>
<div className='container mx-auto px-4 py-3 flex justify-between items-center'>
<div className='flex items-center space-x-3'>
<div className='bg-indigo-600 p-2 rounded-lg'>
<PillIcon className='w-6 h-6 text-white' />
</div>
<h1 className='text-xl md:text-2xl font-bold text-slate-800 dark:text-slate-100'>
Medication Reminder
</h1>
</div>
<div className='flex items-center space-x-2'>
<button
onClick={onStats}
className='hidden sm:flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-lg hover:bg-slate-200 transition-colors dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600'
>
<BarChartIcon className='w-4 h-4' aria-hidden='true' />
<span>Stats</span>
</button>
<button
onClick={onHistory}
className='hidden sm:flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-lg hover:bg-slate-200 transition-colors dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600'
>
<HistoryIcon className='w-4 h-4' aria-hidden='true' />
<span>History</span>
</button>
<button
onClick={onManage}
className='hidden sm:flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-lg hover:bg-slate-200 transition-colors dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600'
>
<MenuIcon className='w-4 h-4' aria-hidden='true' />
<span>Meds</span>
</button>
<button
onClick={onManageReminders}
className='flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-lg hover:bg-slate-200 transition-colors dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600'
>
<BellIcon className='w-4 h-4' aria-hidden='true' />
<span className='hidden sm:inline'>Reminders</span>
</button>
<button
onClick={onAdd}
className='flex items-center space-x-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-slate-900'
>
<PlusIcon className='w-4 h-4' aria-hidden='true' />
<span className='hidden sm:inline'>Add Med</span>
</button>
<div className='border-l border-slate-200 dark:border-slate-600 h-8 mx-2'></div>
<ThemeSwitcher />
<button
onClick={onAccount}
className='flex items-center justify-center w-10 h-10 rounded-lg bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors'
aria-label='Account settings'
>
<SettingsIcon className='w-5 h-5 text-slate-700 dark:text-slate-200' />
</button>
<div className='border-l border-slate-200 dark:border-slate-600 h-8 ml-2'></div>
<AvatarDropdown
user={user}
onLogout={onLogout}
onAdmin={onAdmin}
onChangePassword={onChangePassword}
/>
</div>
</div>
</header>
);
const EmptyState: React.FC<{ onAdd: () => void }> = ({ onAdd }) => (
<div className='text-center py-20 px-4'>
<div className='mx-auto bg-slate-200 dark:bg-slate-700 rounded-full h-16 w-16 flex items-center justify-center animate-float'>
<PillIcon className='w-8 h-8 text-slate-500 dark:text-slate-400' />
</div>
<h3 className='mt-4 text-lg font-semibold text-slate-800 dark:text-slate-100'>
No Medications Scheduled
</h3>
<p className='mt-1 text-slate-500 dark:text-slate-400'>
Get started by adding your first medication.
</p>
<div className='mt-6'>
<button
type='button'
onClick={onAdd}
className='inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-slate-900'
>
<PlusIcon className='-ml-1 mr-2 h-5 w-5' />
Add Medication
</button>
</div>
</div>
);
const groupDetails: {
[key: string]: {
icon: React.FC<React.SVGProps<SVGSVGElement>>;
iconClass: string;
};
} = {
Morning: { icon: SunIcon, iconClass: 'text-amber-500' },
Afternoon: { icon: SunIcon, iconClass: 'text-sky-500' },
Evening: {
icon: SunsetIcon,
iconClass: 'text-indigo-500 dark:text-indigo-400',
},
Night: { icon: MoonIcon, iconClass: 'text-slate-500 dark:text-slate-400' },
};
const MedicationScheduleApp: React.FC<{ user: User }> = ({ user }) => {
const { logout, updateUser } = useUser();
const [medications, setMedications] = useState<Medication[]>([]);
const [customReminders, setCustomReminders] = useState<CustomReminder[]>([]);
const [takenDosesDoc, setTakenDosesDoc] = useState<TakenDoses | null>(null);
const [settings, setSettings] = useState<UserSettings | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [currentTime, setCurrentTime] = useState(() => new Date());
const [isAddModalOpen, setAddModalOpen] = useState(false);
const [isManageModalOpen, setManageModalOpen] = useState(false);
const [isHistoryModalOpen, setHistoryModalOpen] = useState(false);
const [isAccountModalOpen, setAccountModalOpen] = useState(false);
const [isStatsModalOpen, setStatsModalOpen] = useState(false);
const [editingMedication, setEditingMedication] = useState<Medication | null>(
null
);
const [searchQuery, setSearchQuery] = useState('');
const [isOnboardingOpen, setOnboardingOpen] = useState(false);
const [isAdminInterfaceOpen, setAdminInterfaceOpen] = useState(false);
const [isChangePasswordOpen, setChangePasswordOpen] = useState(false);
const [isManageRemindersOpen, setManageRemindersOpen] = useState(false);
const [isAddReminderOpen, setAddReminderOpen] = useState(false);
const [editingReminder, setEditingReminder] = useState<CustomReminder | null>(
null
);
const [snoozedDoses, setSnoozedDoses] = useState<Record<string, string>>({});
const notificationTimers = useRef<Record<string, number>>({});
const takenDoses = useMemo(() => takenDosesDoc?.doses ?? {}, [takenDosesDoc]);
useEffect(() => {
// Don't try to fetch data if user._id is not available
if (!user._id) {
console.warn('Skipping data fetch: user._id is not available');
return;
}
const fetchData = async () => {
try {
setIsLoading(true);
setError(null);
console.log('Fetching data for user:', user._id);
const [medsData, remindersData, takenDosesData, settingsData] =
await Promise.all([
dbService.getMedications(user._id),
dbService.getCustomReminders(user._id),
dbService.getTakenDoses(user._id),
dbService.getSettings(user._id),
]);
console.log('Data fetched successfully:', {
medications: medsData.length,
reminders: remindersData.length,
hasTakenDoses: !!takenDosesData,
hasSettings: !!settingsData,
});
setMedications(medsData);
setCustomReminders(remindersData);
setTakenDosesDoc(takenDosesData);
setSettings(settingsData);
if (!settingsData.hasCompletedOnboarding) {
setOnboardingOpen(true);
}
} catch (e) {
setError('Failed to load your data. Please try again.');
console.error('Error loading user data:', e);
console.error('User object:', user);
} finally {
setIsLoading(false);
}
};
// Add a small delay to ensure user state is fully settled
const timeoutId = setTimeout(() => {
fetchData();
}, 100);
return () => clearTimeout(timeoutId);
}, [user._id]);
useEffect(() => {
if (
settings?.notificationsEnabled &&
'Notification' in window &&
Notification.permission === 'default'
) {
Notification.requestPermission();
}
}, [settings?.notificationsEnabled]);
useEffect(() => {
const timer = setInterval(() => setCurrentTime(new Date()), 60000);
return () => clearInterval(timer);
}, []);
const unifiedSchedule = useMemo(() => {
const medSchedule = generateSchedule(medications, currentTime);
const reminderSchedule = generateReminderSchedule(
customReminders,
currentTime
);
const combined = [...medSchedule, ...reminderSchedule] as ScheduleItem[];
return combined.sort(
(a, b) => a.scheduledTime.getTime() - b.scheduledTime.getTime()
);
}, [medications, customReminders, currentTime]);
const handleAddMedication = async (med: Omit<Medication, '_id' | '_rev'>) => {
const newMed = await dbService.addMedication(user._id, med);
setMedications(prev => [...prev, newMed]);
setAddModalOpen(false);
};
const handleDeleteMedication = async (medToDelete: Medication) => {
await dbService.deleteMedication(user._id, medToDelete);
setMedications(meds => meds.filter(med => med._id !== medToDelete._id));
};
const handleUpdateMedication = async (updatedMed: Medication) => {
const savedMed = await dbService.updateMedication(user._id, updatedMed);
setMedications(meds =>
meds.map(m => (m._id === savedMed._id ? savedMed : m))
);
setEditingMedication(null);
};
const handleAddReminder = async (
reminder: Omit<CustomReminder, '_id' | '_rev'>
) => {
const newReminder = await dbService.addCustomReminder(user._id, reminder);
setCustomReminders(prev => [...prev, newReminder]);
setAddReminderOpen(false);
};
const handleUpdateReminder = async (updatedReminder: CustomReminder) => {
const savedReminder = await dbService.updateCustomReminder(
user._id,
updatedReminder
);
setCustomReminders(reminders =>
reminders.map(r => (r._id === savedReminder._id ? savedReminder : r))
);
setEditingReminder(null);
};
const handleDeleteReminder = async (reminderToDelete: CustomReminder) => {
await dbService.deleteCustomReminder(user._id, reminderToDelete);
setCustomReminders(reminders =>
reminders.filter(r => r._id !== reminderToDelete._id)
);
};
const handleOpenEditModal = (med: Medication) => {
setEditingMedication(med);
setManageModalOpen(false);
};
const handleOpenEditReminderModal = (reminder: CustomReminder) => {
setEditingReminder(reminder);
setManageRemindersOpen(false);
};
const handleToggleDose = useCallback(
async (doseId: string) => {
if (!takenDosesDoc) return;
const newDoses = { ...takenDosesDoc.doses };
if (newDoses[doseId]) {
delete newDoses[doseId];
} else {
newDoses[doseId] = new Date().toISOString();
}
const updatedDoc = await dbService.updateTakenDoses(user._id, {
...takenDosesDoc,
doses: newDoses,
});
setTakenDosesDoc(updatedDoc);
},
[takenDosesDoc, user._id]
);
const handleSnoozeDose = useCallback((doseId: string) => {
const SNOOZE_DURATION = 5 * 60 * 1000; // 5 minutes
const snoozedUntil = new Date(Date.now() + SNOOZE_DURATION).toISOString();
setSnoozedDoses(prev => ({ ...prev, [doseId]: snoozedUntil }));
// Clear existing timer and set a new one
if (notificationTimers.current[doseId]) {
clearTimeout(notificationTimers.current[doseId]);
delete notificationTimers.current[doseId];
}
}, []);
const getDoseStatus = useCallback(
(dose: Dose, doseTime: Date, now: Date): DoseStatus => {
if (takenDoses[dose.id]) return DoseStatus.TAKEN;
if (snoozedDoses[dose.id] && new Date(snoozedDoses[dose.id]) > now)
return DoseStatus.SNOOZED;
if (doseTime.getTime() < now.getTime()) return DoseStatus.MISSED;
return DoseStatus.UPCOMING;
},
[takenDoses, snoozedDoses]
);
const scheduleWithStatus = useMemo(() => {
return unifiedSchedule
.map(item => {
if ('medicationId' in item) {
// It's a Dose
const medication = medications.find(m => m._id === item.medicationId);
if (!medication) return null;
return {
...item,
type: 'dose' as const,
medication,
status: getDoseStatus(item, item.scheduledTime, currentTime),
takenAt: takenDoses[item.id],
snoozedUntil: snoozedDoses[item.id]
? new Date(snoozedDoses[item.id])
: undefined,
};
} else {
// It's a Custom Reminder
return {
...item,
type: 'reminder' as const,
};
}
})
.filter((d): d is NonNullable<typeof d> => d !== null);
}, [
unifiedSchedule,
medications,
getDoseStatus,
currentTime,
takenDoses,
snoozedDoses,
]);
useEffect(() => {
if (
!settings?.notificationsEnabled ||
!('Notification' in window) ||
Notification.permission !== 'granted'
) {
return;
}
const now = new Date();
const activeTimers = notificationTimers.current;
scheduleWithStatus.forEach(item => {
const itemId = item.id;
if (activeTimers[itemId]) return; // Timer already set
let timeToNotification = -1;
let notificationBody = '';
let notificationTitle = '';
if (item.type === 'dose' && item.status === DoseStatus.UPCOMING) {
timeToNotification = item.scheduledTime.getTime() - now.getTime();
notificationTitle = 'Time for your medication!';
notificationBody = `${item.medication.name} (${item.medication.dosage})`;
} else if (
item.type === 'dose' &&
item.status === DoseStatus.SNOOZED &&
item.snoozedUntil
) {
timeToNotification = item.snoozedUntil.getTime() - now.getTime();
notificationTitle = 'Snoozed Medication Reminder';
notificationBody = `${item.medication.name} (${item.medication.dosage})`;
} else if (item.type === 'reminder' && item.scheduledTime > now) {
timeToNotification = item.scheduledTime.getTime() - now.getTime();
notificationTitle = 'Reminder';
notificationBody = item.title;
}
if (timeToNotification > 0) {
activeTimers[itemId] = setTimeout(() => {
new Notification(notificationTitle, {
body: notificationBody,
tag: itemId,
});
if (item.type === 'dose' && item.status === DoseStatus.SNOOZED) {
setSnoozedDoses(prev => {
const newSnoozed = { ...prev };
delete newSnoozed[itemId];
return newSnoozed;
});
}
delete activeTimers[itemId];
}, timeToNotification) as unknown as number;
}
});
return () => Object.values(activeTimers).forEach(clearTimeout);
}, [scheduleWithStatus, settings?.notificationsEnabled]);
const filteredSchedule = useMemo(
() =>
scheduleWithStatus.filter(item => {
if (item.type === 'reminder') {
return item.title.toLowerCase().includes(searchQuery.toLowerCase());
}
return item.medication.name
.toLowerCase()
.includes(searchQuery.toLowerCase());
}),
[scheduleWithStatus, searchQuery]
);
const groupedSchedule = useMemo(() => {
const groups: { [key: string]: typeof filteredSchedule } = {
Morning: [],
Afternoon: [],
Evening: [],
Night: [],
};
filteredSchedule.forEach(item => {
const hour = item.scheduledTime.getHours();
if (hour >= 5 && hour < 12) groups['Morning'].push(item);
else if (hour >= 12 && hour < 17) groups['Afternoon'].push(item);
else if (hour >= 17 && hour < 21) groups['Evening'].push(item);
else groups['Night'].push(item);
});
return groups;
}, [filteredSchedule]);
const medicationHistory = useMemo(() => {
const history: { date: string; doses: HistoricalDose[] }[] = [];
const today = new Date();
today.setHours(0, 0, 0, 0);
const now = new Date();
for (let i = 0; i < 7; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const daySchedule = generateSchedule(medications, date);
if (daySchedule.length === 0 || date.getTime() > now.getTime()) continue;
const dosesForDay: HistoricalDose[] = daySchedule
.map(dose => {
const medication = medications.find(m => m._id === dose.medicationId);
return medication
? {
id: dose.id,
medication,
scheduledTime: dose.scheduledTime,
status: getDoseStatus(dose, dose.scheduledTime, now),
takenAt: takenDoses[dose.id],
}
: null;
})
.filter((d): d is NonNullable<typeof d> => d !== null);
if (dosesForDay.length > 0)
history.push({
date: date.toISOString().split('T')[0],
doses: dosesForDay,
});
}
return history;
}, [medications, takenDoses, getDoseStatus]);
const { dailyStats, medicationStats } = useMemo(() => {
const today = new Date();
today.setHours(23, 59, 59, 999);
const now = new Date();
const daily: DailyStat[] = [];
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setHours(0, 0, 0, 0);
date.setDate(date.getDate() - i);
const daySchedule = generateSchedule(medications, date);
const pastDoses = daySchedule.filter(d => d.scheduledTime < now);
if (pastDoses.length === 0) {
daily.push({ date: date.toISOString().split('T')[0], adherence: 100 });
continue;
}
let takenCount = 0;
pastDoses.forEach(dose => {
if (takenDoses[dose.id]) {
takenCount++;
}
});
const adherence = (takenCount / pastDoses.length) * 100;
daily.push({
date: date.toISOString().split('T')[0],
adherence: Math.round(adherence),
});
}
const statsByMedId: Record<
string,
{
taken: number;
missed: number;
upcoming: number;
medication: Medication;
lastTakenAt?: string;
}
> = {};
medications.forEach(med => {
statsByMedId[med._id] = {
taken: 0,
missed: 0,
upcoming: 0,
medication: med,
};
});
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
medications.forEach(med => {
for (let i = 0; i < 7; i++) {
const date = new Date();
date.setDate(date.getDate() - i);
const daySchedule = generateSchedule([med], date);
daySchedule.forEach(dose => {
const stat = statsByMedId[dose.medicationId];
if (stat) {
const status = getDoseStatus(dose, dose.scheduledTime, now);
if (status === DoseStatus.TAKEN) {
stat.taken++;
const takenAt = takenDoses[dose.id];
if (
takenAt &&
(!stat.lastTakenAt ||
new Date(takenAt) > new Date(stat.lastTakenAt))
) {
stat.lastTakenAt = takenAt;
}
} else if (status === DoseStatus.MISSED) stat.missed++;
else if (status === DoseStatus.UPCOMING) stat.upcoming++;
}
});
}
});
const medication: MedicationStat[] = Object.values(statsByMedId)
.map(stat => {
const totalPast = stat.taken + stat.missed;
const adherence =
totalPast > 0 ? Math.round((stat.taken / totalPast) * 100) : 100;
return { ...stat, adherence };
})
.sort((a, b) => a.medication.name.localeCompare(b.medication.name));
return { dailyStats: daily, medicationStats: medication };
}, [medications, takenDoses, getDoseStatus]);
const handleUpdateSettings = async (newSettings: UserSettings) => {
const updatedSettings = await dbService.updateSettings(
user._id,
newSettings
);
setSettings(updatedSettings);
};
const handleDeleteAllData = async () => {
if (
window.confirm(
'Are you sure you want to delete all your medication data? This action cannot be undone.'
)
) {
await dbService.deleteAllUserData(user._id);
setMedications([]);
setCustomReminders([]);
const updatedTakenDoses = await dbService.getTakenDoses(user._id);
setTakenDosesDoc(updatedTakenDoses);
setAccountModalOpen(false);
}
};
const handleCompleteOnboarding = async () => {
if (settings) {
try {
const updatedSettings = await dbService.updateSettings(user._id, {
...settings,
hasCompletedOnboarding: true,
});
setSettings(updatedSettings);
setOnboardingOpen(false);
} catch (error) {
console.error('Failed to update onboarding status', error);
setOnboardingOpen(false);
}
}
};
if (isLoading) {
return (
<div className='min-h-screen flex items-center justify-center'>
<PillIcon className='w-12 h-12 text-indigo-500 animate-spin' />
</div>
);
}
if (error) {
return (
<div className='min-h-screen flex items-center justify-center text-red-500'>
{error}
</div>
);
}
return (
<div className='min-h-screen text-slate-800 dark:text-slate-200'>
<Header
onAdd={() => setAddModalOpen(true)}
onManage={() => setManageModalOpen(true)}
onManageReminders={() => setManageRemindersOpen(true)}
onHistory={() => setHistoryModalOpen(true)}
onStats={() => setStatsModalOpen(true)}
onAccount={() => setAccountModalOpen(true)}
onAdmin={() => setAdminInterfaceOpen(true)}
onChangePassword={() => setChangePasswordOpen(true)}
user={user}
onLogout={logout}
/>
<main className='container mx-auto p-4 md:p-6'>
<div className='flex items-center justify-between mb-6'>
<h2 className='text-2xl font-bold dark:text-slate-100'>
Today's Schedule
</h2>
<time className='font-medium text-slate-500 dark:text-slate-400'>
{currentTime.toLocaleDateString(undefined, {
weekday: 'long',
month: 'long',
day: 'numeric',
})}
</time>
</div>
{medications.length > 0 || customReminders.length > 0 ? (
<>
<div className='relative mb-6'>
<div className='pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3'>
<SearchIcon
className='h-5 w-5 text-slate-400'
aria-hidden='true'
/>
</div>
<input
type='search'
name='search'
id='search'
className='block w-full pl-10 pr-4 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
placeholder='Search schedule...'
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
</div>
{filteredSchedule.length > 0 ? (
<div className='space-y-8'>
{Object.entries(groupedSchedule).map(([groupName, items]) => {
const scheduleItems = items as typeof filteredSchedule;
if (scheduleItems.length === 0) return null;
const Icon = groupDetails[groupName]?.icon;
return (
<section key={groupName}>
<div className='flex items-center space-x-3 mb-3 pb-2 border-b border-slate-200 dark:border-slate-700'>
{Icon && (
<Icon
className={`w-6 h-6 ${groupDetails[groupName].iconClass}`}
aria-hidden='true'
/>
)}
<h3 className='text-lg font-semibold text-slate-600 dark:text-slate-300'>
{groupName}
</h3>
</div>
<ul className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'>
{scheduleItems.map(item =>
item.type === 'dose' ? (
<DoseCard
key={item.id}
dose={item}
medication={item.medication}
status={item.status}
onToggleDose={handleToggleDose}
onSnooze={handleSnoozeDose}
snoozedUntil={item.snoozedUntil}
/>
) : (
<ReminderCard key={item.id} reminder={item} />
)
)}
</ul>
</section>
);
})}
</div>
) : (
<div className='text-center py-10 px-4'>
<SearchIcon className='mx-auto h-12 w-12 text-slate-400' />
<h3 className='mt-2 text-sm font-semibold text-slate-900 dark:text-slate-100'>
No items found
</h3>
<p className='mt-1 text-sm text-slate-500 dark:text-slate-400'>
Your search for "{searchQuery}" did not match any items
scheduled for today.
</p>
</div>
)}
</>
) : (
<EmptyState onAdd={() => setAddModalOpen(true)} />
)}
</main>
<AddMedicationModal
isOpen={isAddModalOpen}
onClose={() => setAddModalOpen(false)}
onAdd={handleAddMedication}
/>
<ManageMedicationsModal
isOpen={isManageModalOpen}
onClose={() => setManageModalOpen(false)}
medications={medications}
onDelete={handleDeleteMedication}
onEdit={handleOpenEditModal}
/>
<EditMedicationModal
isOpen={editingMedication !== null}
onClose={() => setEditingMedication(null)}
medication={editingMedication}
onUpdate={handleUpdateMedication}
/>
<HistoryModal
isOpen={isHistoryModalOpen}
onClose={() => setHistoryModalOpen(false)}
history={medicationHistory}
/>
<StatsModal
isOpen={isStatsModalOpen}
onClose={() => setStatsModalOpen(false)}
dailyStats={dailyStats}
medicationStats={medicationStats}
/>
{settings && (
<AccountModal
isOpen={isAccountModalOpen}
onClose={() => setAccountModalOpen(false)}
user={user}
settings={settings}
onUpdateUser={updateUser}
onUpdateSettings={handleUpdateSettings}
onDeleteAllData={handleDeleteAllData}
/>
)}
<OnboardingModal
isOpen={isOnboardingOpen}
onComplete={handleCompleteOnboarding}
/>
<ManageRemindersModal
isOpen={isManageRemindersOpen}
onClose={() => setManageRemindersOpen(false)}
reminders={customReminders}
onAdd={() => {
setManageRemindersOpen(false);
setAddReminderOpen(true);
}}
onEdit={handleOpenEditReminderModal}
onDelete={handleDeleteReminder}
/>
<AddReminderModal
isOpen={isAddReminderOpen}
onClose={() => setAddReminderOpen(false)}
onAdd={handleAddReminder}
/>
<EditReminderModal
isOpen={editingReminder !== null}
onClose={() => setEditingReminder(null)}
reminder={editingReminder}
onUpdate={handleUpdateReminder}
/>
{/* Admin Interface - Only shown when opened */}
{isAdminInterfaceOpen && (
<AdminInterface onClose={() => setAdminInterfaceOpen(false)} />
)}
{/* Password Change Modal - Only shown when opened */}
{isChangePasswordOpen && (
<ChangePasswordModal
onClose={() => setChangePasswordOpen(false)}
onSuccess={() => {
alert('Password changed successfully!');
setChangePasswordOpen(false);
}}
/>
)}
</div>
);
};
const App: React.FC = () => {
const { user, isLoading } = useUser();
// Run database seeding on app startup
useEffect(() => {
const runSeeding = async () => {
try {
console.log('🌱 Initializing database seeding...');
await databaseSeeder.seedDatabase();
} catch (error) {
console.error(' Database seeding failed:', error);
}
};
runSeeding();
}, []);
if (isLoading) {
return (
<div className='min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900'>
<PillIcon className='w-12 h-12 text-indigo-500 animate-spin' />
</div>
);
}
if (!user) {
return <AuthPage />;
}
return <MedicationScheduleApp user={user} />;
};
export default App;
+299
View File
@@ -0,0 +1,299 @@
# Changelog
All notable changes to the Medication Reminder App will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2025-09-05
### Added
- **Complete Authentication System**
- Email/password authentication with bcrypt hashing
- OAuth integration (Google, GitHub)
- Email verification with Mailgun
- Password reset functionality
- Role-based access control (User, Admin)
- **Medication Management**
- Add, edit, delete medications
- Flexible scheduling (Daily, Multiple times, Custom intervals)
- Visual medication cards with custom icons
- Medication history tracking
- **Reminder System**
- Smart scheduling based on medication frequency
- Dose tracking (Taken, Missed, Upcoming)
- Custom reminders with personalized messages
- Adherence statistics and progress monitoring
- **Admin Interface**
- Complete user management dashboard
- View all users with status and role information
- Suspend/activate user accounts
- Delete users with protection mechanisms
- Change user passwords
- Role assignment capabilities
- **User Experience Features**
- Responsive design for mobile and desktop
- Dark/Light theme support
- Intuitive interface with modern design
- Onboarding flow for new users
- Avatar customization with image upload
- Settings management for preferences
- **Analytics Dashboard**
- Daily adherence statistics with visual charts
- Medication-specific analytics
- Progress tracking over time
- Export capabilities for healthcare providers
- **Infrastructure**
- Docker containerization with multi-stage builds
- CouchDB integration for data persistence
- Environment-based service factory pattern
- Production-ready nginx configuration
- Comprehensive health checks
- **Security Features**
- Secure password hashing with bcrypt
- JWT-like token system for sessions
- Email verification for account activation
- Input validation and sanitization
- Role-based authorization
- Secure credential management
- **Development Tools**
- TypeScript for type safety
- ESLint for code quality
- Automated setup and deployment scripts
- Comprehensive test suite
- Environment configuration management
- **Documentation**
- Complete README with setup instructions
- API documentation with examples
- Security guide and best practices
- Deployment guide for various platforms
- Troubleshooting documentation
### Technical Details
- **Frontend**: React 19 with TypeScript, Vite build system
- **Backend**: CouchDB with localStorage fallback
- **Email**: Mailgun integration for verification and password reset
- **Deployment**: Docker Compose with nginx for production
- **Testing**: Jest integration tests for authentication flows
- **Package Management**: Bun for fast dependency management
### Database Schema
- Users collection with authentication and profile data
- Medications collection with scheduling information
- Settings collection for user preferences
- Taken doses collection for adherence tracking
- Reminders collection for custom user reminders
### Security Implementations
- Password requirements with strength validation
- Account status management (Pending, Active, Suspended)
- Email verification workflow
- Secure token generation for password reset
- Admin privilege separation
- Data privacy controls
### Performance Features
- Lazy loading for large datasets
- Optimized Docker images with multi-stage builds
- Static file serving with nginx
- Database indexing for efficient queries
- Responsive design for all screen sizes
## [Unreleased]
### Planned Features
- Mobile app development (React Native)
- Push notifications for reminders
- Integration with health APIs (Apple Health, Google Fit)
- Medication interaction checking
- Prescription refill reminders
- Healthcare provider portal
- Advanced analytics with machine learning
- Multi-language support
- Backup and restore functionality
- API rate limiting improvements
### Under Consideration
- Voice commands for medication logging
- Barcode scanning for medication identification
- Family account management
- Telemedicine integration
- Insurance information management
- Side effect tracking
- Mood and symptom correlation
- Wearable device integration
## Development Milestones
### Phase 1: Core Functionality ✅
- [x] Basic medication tracking
- [x] User authentication
- [x] Reminder system
- [x] Data persistence
### Phase 2: Enhanced Features ✅
- [x] Admin interface
- [x] Email integration
- [x] Analytics dashboard
- [x] Security hardening
### Phase 3: Production Ready ✅
- [x] Docker deployment
- [x] Environment management
- [x] Documentation
- [x] Testing suite
### Phase 4: Advanced Features (In Progress)
- [ ] Mobile application
- [ ] Advanced analytics
- [ ] Healthcare integrations
- [ ] Multi-tenant support
## Breaking Changes
### Version 1.0.0
- Initial release - no breaking changes from previous versions
- Migration from localStorage-only to production CouchDB
- Environment variable restructuring for security
## Migration Guide
### From Development to Production
1. Copy `.env.example` to `.env`
2. Configure CouchDB credentials
3. Set up Mailgun for email features
4. Run `./deploy.sh production`
5. Seed database with admin user
### Database Migration
- Automatic migration from localStorage to CouchDB
- Data import tools available for existing installations
- Backup and restore procedures documented
## Security Updates
### Version 1.0.0
- Implemented bcrypt password hashing
- Added JWT-like session management
- Configured secure email verification
- Established role-based access control
- Implemented input validation and sanitization
## Performance Improvements
### Version 1.0.0
- Optimized Docker build process
- Implemented lazy loading for large datasets
- Added database indexing for efficient queries
- Configured nginx for optimal static file serving
- Optimized React component rendering
## Bug Fixes
### Version 1.0.0
- Fixed authentication state management
- Resolved timezone handling in reminders
- Corrected medication scheduling edge cases
- Fixed mobile responsive design issues
- Resolved Docker environment variable handling
## Contributors
### Core Team
- Lead Developer - Full-stack development and architecture
- UI/UX Designer - Interface design and user experience
- DevOps Engineer - Infrastructure and deployment
- Security Specialist - Security audit and hardening
### Community Contributors
- Documentation improvements
- Bug reports and testing
- Feature suggestions and feedback
- Translation contributions
## Acknowledgments
### Open Source Libraries
- React team for the excellent frontend framework
- CouchDB team for the robust database system
- Mailgun for reliable email delivery services
- Docker team for containerization technology
- TypeScript team for enhanced development experience
### Inspiration
- Healthcare professionals providing feedback
- Patients sharing medication management challenges
- Open source community best practices
- Modern web development standards
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Release Notes
### Version 1.0.0 - "Foundation Release"
This inaugural release establishes the core foundation of the Medication Reminder App with enterprise-grade features and security. The application provides a complete medication management solution with professional-grade authentication, administration tools, and analytics capabilities.
**Key Highlights:**
- Production-ready Docker deployment
- Comprehensive user and admin interfaces
- Secure authentication with email verification
- Real-time medication tracking and analytics
- Mobile-responsive design
- Extensive documentation and security guides
**Who Should Upgrade:**
- All users moving from development to production
- Healthcare organizations requiring medication tracking
- Individuals seeking comprehensive medication management
- Developers needing a complete authentication reference
**Migration Path:**
- Follow the deployment guide for new installations
- Use the migration tools for existing data
- Review security guide for production deployment
- Test thoroughly in staging environment before production
---
For technical support or questions about this release, please:
- Check the documentation in the `docs/` directory
- Open an issue on GitHub for bug reports
- Contact the development team for enterprise support
- Join our community Discord for general questions
+524
View File
@@ -0,0 +1,524 @@
# Contributing to Medication Reminder App
Thank you for your interest in contributing to the Medication Reminder App! This document provides guidelines and information for contributors.
## 🤝 How to Contribute
### Reporting Issues
Before creating an issue, please check if it already exists in our [issue tracker](https://github.com/your-username/rxminder/issues).
#### Bug Reports
- Use the bug report template
- Include steps to reproduce
- Provide system information
- Add screenshots if applicable
#### Feature Requests
- Use the feature request template
- Explain the problem you're trying to solve
- Describe your proposed solution
- Consider implementation complexity
### Development Process
#### 1. Fork and Clone
```bash
# Fork the repository on GitHub
# Clone your fork
git clone https://github.com/your-username/rxminder.git
cd meds
# Add upstream remote
git remote add upstream https://github.com/original-owner/meds.git
```
#### 2. Set Up Development Environment
```bash
# Run setup script
./setup.sh
# Or manual setup
bun install
cp .env.example .env
# Edit .env with your development values
docker compose -f docker/docker-compose.yaml up -d
```
#### 3. Create Feature Branch
```bash
# Update main branch
git checkout main
git pull upstream main
# Create feature branch
git checkout -b feature/your-feature-name
```
#### 4. Make Changes
- Follow coding standards (see below)
- Write tests for new functionality
- Update documentation as needed
- Ensure all tests pass
#### 5. Commit Changes
```bash
# Stage changes
git add .
# Commit with descriptive message
git commit -m "feat: add medication interaction checking
- Implement drug interaction API integration
- Add warning UI components
- Include interaction severity levels
- Update medication form validation"
```
#### 6. Push and Create Pull Request
```bash
# Push to your fork
git push origin feature/your-feature-name
# Create pull request on GitHub
# Use the pull request template
# Link related issues
```
## 📝 Coding Standards
### TypeScript/JavaScript
```typescript
// Use TypeScript for all new code
// Define interfaces for data structures
interface Medication {
_id: string;
name: string;
dosage: string;
frequency: Frequency;
}
// Use meaningful variable names
const medicationList = getMedications();
const isUserAuthenticated = checkAuthStatus();
// Add JSDoc comments for complex functions
/**
* Calculates medication adherence percentage
* @param takenDoses - Number of doses taken
* @param totalDoses - Total number of scheduled doses
* @returns Adherence percentage (0-100)
*/
function calculateAdherence(takenDoses: number, totalDoses: number): number {
return totalDoses > 0 ? (takenDoses / totalDoses) * 100 : 0;
}
```
### React Components
```tsx
// Use functional components with hooks
import React, { useState, useEffect } from 'react';
interface MedicationCardProps {
medication: Medication;
onEdit: (medication: Medication) => void;
onDelete: (id: string) => void;
}
export const MedicationCard: React.FC<MedicationCardProps> = ({ medication, onEdit, onDelete }) => {
const [isLoading, setIsLoading] = useState(false);
// Use descriptive event handlers
const handleEditClick = () => {
onEdit(medication);
};
const handleDeleteClick = async () => {
setIsLoading(true);
try {
await onDelete(medication._id);
} finally {
setIsLoading(false);
}
};
return <div className='medication-card'>{/* Component JSX */}</div>;
};
```
### CSS/Styling
```css
/* Use BEM methodology for CSS classes */
.medication-card {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
}
.medication-card__header {
display: flex;
justify-content: space-between;
align-items: center;
}
.medication-card__title {
font-size: 1.2rem;
font-weight: 600;
margin: 0;
}
.medication-card--highlighted {
border-color: var(--primary-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Use CSS custom properties for theming */
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
}
```
### File Organization
```
src/
├── components/ # Reusable UI components
│ ├── common/ # Generic components
│ ├── forms/ # Form-specific components
│ └── modals/ # Modal components
├── pages/ # Page-level components
├── hooks/ # Custom React hooks
├── services/ # API and business logic
├── utils/ # Utility functions
├── types/ # TypeScript type definitions
├── contexts/ # React context providers
└── assets/ # Static assets
```
## 🧪 Testing Guidelines
### Unit Tests
```typescript
// Test filename: ComponentName.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { MedicationCard } from './MedicationCard';
describe('MedicationCard', () => {
const mockMedication = {
_id: '1',
name: 'Aspirin',
dosage: '100mg',
frequency: Frequency.Daily
};
const mockOnEdit = jest.fn();
const mockOnDelete = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('renders medication information correctly', () => {
render(
<MedicationCard
medication={mockMedication}
onEdit={mockOnEdit}
onDelete={mockOnDelete}
/>
);
expect(screen.getByText('Aspirin')).toBeInTheDocument();
expect(screen.getByText('100mg')).toBeInTheDocument();
});
it('calls onEdit when edit button is clicked', () => {
render(
<MedicationCard
medication={mockMedication}
onEdit={mockOnEdit}
onDelete={mockOnDelete}
/>
);
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
expect(mockOnEdit).toHaveBeenCalledWith(mockMedication);
});
});
```
### Integration Tests
```typescript
// Test authentication flow
describe('Authentication Integration', () => {
beforeEach(() => {
// Clear localStorage and reset mocks
localStorage.clear();
jest.clearAllMocks();
});
it('allows user to register and login', async () => {
// Test registration
const registrationResult = await authService.register('test@example.com', 'Password123!', 'Test User');
expect(registrationResult.user.email).toBe('test@example.com');
expect(registrationResult.user.status).toBe(AccountStatus.PENDING);
// Test email verification
await authService.verifyEmail(registrationResult.verificationToken.token);
// Test login
const loginResult = await authService.login({
email: 'test@example.com',
password: 'Password123!',
});
expect(loginResult.user.status).toBe(AccountStatus.ACTIVE);
expect(loginResult.accessToken).toBeDefined();
});
});
```
### Running Tests
```bash
# Run all tests
bun test
# Run tests in watch mode
bun test --watch
# Run specific test file
bun test MedicationCard.test.tsx
# Run tests with coverage
bun test --coverage
# Run integration tests
bun run test:integration
# Run E2E tests with Playwright
bun run test:e2e
# Run E2E tests in UI mode
bun run test:e2e:ui
# Debug E2E tests
bun run test:e2e:debug
# Run all tests (unit + integration + e2e)
bun run test:all
```
### E2E Testing
E2E tests use Playwright and are located in `tests/e2e/`. When adding new features:
```typescript
// Use custom fixtures for authenticated testing
import { test } from './fixtures';
test('should perform user action', async ({ adminPage }) => {
// Test implementation with auto-logged-in admin
});
```
See [tests/e2e/README.md](tests/e2e/README.md) for detailed E2E testing guidelines.
## 📚 Documentation Standards
### Code Documentation
- Add JSDoc comments for all public functions
- Document complex algorithms and business logic
- Include examples for utility functions
- Keep comments up-to-date with code changes
### API Documentation
- Document all endpoints with examples
- Include request/response schemas
- Specify error codes and messages
- Provide authentication requirements
### User Documentation
- Write clear setup instructions
- Include troubleshooting guides
- Provide usage examples
- Keep screenshots current
## 🔍 Code Review Process
### Before Requesting Review
- [ ] All tests pass locally
- [ ] Code follows style guidelines
- [ ] Documentation is updated
- [ ] No console.log statements
- [ ] Secrets are not committed
- [ ] Performance impact considered
### Review Checklist
- [ ] Code is readable and well-structured
- [ ] Tests cover new functionality
- [ ] Security implications considered
- [ ] Accessibility requirements met
- [ ] Browser compatibility verified
- [ ] Mobile responsiveness checked
### Review Response
- Be open to feedback
- Ask questions for unclear comments
- Address all review comments
- Update documentation if needed
- Test suggested changes
## 🚀 Release Process
### Version Numbering
We follow [Semantic Versioning](https://semver.org/):
- **MAJOR**: Breaking changes
- **MINOR**: New features (backward compatible)
- **PATCH**: Bug fixes (backward compatible)
### Release Checklist
- [ ] Update CHANGELOG.md
- [ ] Update version in package.json
- [ ] Run full test suite
- [ ] Update documentation
- [ ] Create release notes
- [ ] Tag release in Git
- [ ] Deploy to staging
- [ ] Validate staging deployment
- [ ] Deploy to production
## 🎯 Development Priorities
### High Priority
- Bug fixes and security issues
- Performance improvements
- Accessibility enhancements
- Core functionality stability
### Medium Priority
- New features and enhancements
- Developer experience improvements
- Documentation updates
- Test coverage improvements
### Low Priority
- Code refactoring
- Minor UI improvements
- Non-critical feature requests
- Experimental features
## 🛠️ Development Tools
### Required Tools
- **Node.js 18+** or **Bun 1.0+**
- **Docker and Docker Compose**
- **Git**
- **Code Editor** (VS Code recommended)
### Recommended Extensions (VS Code)
- TypeScript and JavaScript Language Features
- ESLint
- Prettier
- Docker
- GitLens
- Thunder Client (for API testing)
### Useful Commands
```bash
# Development
bun run dev # Start development server
bun run build # Build for production
bun run preview # Preview production build
# Quality
bun run lint # Check code quality
bun run lint:fix # Fix linting issues
bun run type-check # TypeScript type checking
# Testing
bun test # Run tests
bun test:coverage # Run tests with coverage
bun test:watch # Run tests in watch mode
# Docker
docker compose -f docker/docker-compose.yaml up -d # Start services
docker compose -f docker/docker-compose.yaml logs # View logs
docker compose -f docker/docker-compose.yaml down # Stop services
```
## 🆘 Getting Help
### Documentation
- [README.md](README.md) - Project overview and setup
- [docs/API.md](docs/API.md) - API documentation
- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) - Deployment guide
- [docs/SECURITY.md](docs/SECURITY.md) - Security guidelines
### Community
- **GitHub Issues** - Bug reports and feature requests
- **GitHub Discussions** - General questions and ideas
- **Discord/Slack** - Real-time chat with contributors
### Support
- **Email** - development@your-domain.com
- **Professional Support** - Available for enterprise users
- **Consulting** - Custom development and deployment assistance
## 📄 License
By contributing to this project, you agree that your contributions will be licensed under the MIT License.
## 🙏 Recognition
Contributors will be:
- Listed in the CHANGELOG.md for their contributions
- Mentioned in release notes for significant features
- Added to the contributors section of README.md
- Eligible for contributor benefits and recognition
### Types of Contributions Recognized
- **Code contributions** - Features, bug fixes, improvements
- **Documentation** - Writing, editing, translating
- **Testing** - Bug reports, test writing, QA
- **Design** - UI/UX improvements, graphics, branding
- **Community** - Support, mentoring, evangelism
Thank you for contributing to the Medication Reminder App! Your efforts help improve healthcare outcomes for users worldwide. 🌟
+179
View File
@@ -0,0 +1,179 @@
# Medication Reminder App - License
MIT License
Copyright (c) 2025 Medication Reminder App Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Third-Party Licenses
This project includes or uses the following third-party libraries and services:
### Frontend Dependencies
#### React (MIT License)
- **Source**: https://github.com/facebook/react
- **License**: MIT
- **Purpose**: User interface framework
#### Lucide React (ISC License)
- **Source**: https://github.com/lucide-icons/lucide
- **License**: ISC
- **Purpose**: Icon library
#### React Hook Form (MIT License)
- **Source**: https://github.com/react-hook-form/react-hook-form
- **License**: MIT
- **Purpose**: Form management
#### Chart.js (MIT License)
- **Source**: https://github.com/chartjs/Chart.js
- **License**: MIT
- **Purpose**: Data visualization
### Build Tools
#### Vite (MIT License)
- **Source**: https://github.com/vitejs/vite
- **License**: MIT
- **Purpose**: Build tool and development server
#### TypeScript (Apache 2.0 License)
- **Source**: https://github.com/microsoft/TypeScript
- **License**: Apache 2.0
- **Purpose**: Type checking and compilation
#### Bun (MIT License)
- **Source**: https://github.com/oven-sh/bun
- **License**: MIT
- **Purpose**: JavaScript runtime and package manager
### Backend Services
#### CouchDB (Apache 2.0 License)
- **Source**: https://github.com/apache/couchdb
- **License**: Apache 2.0
- **Purpose**: Database server
#### Node.js (MIT License)
- **Source**: https://github.com/nodejs/node
- **License**: MIT
- **Purpose**: Server runtime (for production services)
### External Services
#### Mailgun
- **Service**: Email delivery
- **Terms**: https://www.mailgun.com/terms/
- **Privacy**: https://www.mailgun.com/privacy-policy/
#### Google OAuth 2.0
- **Service**: Authentication provider
- **Terms**: https://developers.google.com/terms/
- **Privacy**: https://policies.google.com/privacy
### Docker Images
#### Node.js (Official)
- **Source**: https://hub.docker.com/_/node
- **License**: MIT
- **Purpose**: Base image for application
#### Nginx (Official)
- **Source**: https://hub.docker.com/_/nginx
- **License**: 2-clause BSD
- **Purpose**: Web server for static files
#### CouchDB (Official)
- **Source**: https://hub.docker.com/_/couchdb
- **License**: Apache 2.0
- **Purpose**: Database server
## License Compliance
### MIT License Requirements
When redistributing this software:
1. Include the original copyright notice
2. Include the MIT license text
3. Include any third-party license notices
### Attribution Requirements
When using this software in your own projects:
1. Credit the original authors
2. Link to the original repository
3. Include license information in your documentation
### Commercial Use
This software is free for commercial use under the MIT license. However:
1. External services (Mailgun, Google OAuth) may have their own terms
2. Consider your data privacy obligations
3. Review compliance requirements for healthcare applications
## Warranty Disclaimer
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
This medication reminder application is intended for informational purposes
only and should not be used as a substitute for professional medical advice,
diagnosis, or treatment. Always seek the advice of your physician or other
qualified health provider with any questions you may have regarding a medical
condition.
## Data Privacy Notice
This software may process personal health information. When deploying:
1. **Review Privacy Laws**: Ensure compliance with GDPR, HIPAA, or local regulations
2. **Secure Deployment**: Use encryption, secure credentials, and access controls
3. **Data Handling**: Document data collection, processing, and retention practices
4. **User Consent**: Obtain appropriate consent for data processing
5. **Incident Response**: Have procedures for data breaches or security incidents
## Medical Disclaimer
This application is a reminder tool and is not intended to:
- Replace professional medical advice
- Diagnose medical conditions
- Recommend medication changes
- Provide medical treatment
Users should:
- Consult healthcare providers for medical decisions
- Follow prescribed medication regimens
- Report adverse effects to healthcare providers
- Seek immediate medical attention for emergencies
## Support and Contact
For license-related questions:
- **Email**: legal@your-domain.com
- **Issues**: https://github.com/your-username/rxminder/issues
- **Documentation**: https://github.com/your-username/rxminder/docs
For technical support:
- **Email**: support@your-domain.com
- **Documentation**: [README.md](README.md)
- **Contributing**: [CONTRIBUTING.md](CONTRIBUTING.md)
---
*Last updated: December 2024*
+757
View File
@@ -0,0 +1,757 @@
# 💊 RxMinder
A modern, secure web application for managing medication schedules and reminders. Built with React, TypeScript, CouchDB, and Docker for reliable medication tracking and adherence monitoring.
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![TypeScript](https://img.shields.io/badge/typescript-5.8.2-blue.svg)
![React](https://img.shields.io/badge/react-19.1.1-blue.svg)
![Docker](https://img.shields.io/badge/docker-ready-blue.svg)
## ✨ Features
### 🔐 **Authentication & Security**
- **Email/Password Authentication** with secure password hashing (bcrypt)
- **OAuth Integration** (Google, GitHub) for social login
- **Email Verification** for account activation
- **Password Reset** functionality with secure tokens
- **Admin Interface** for user management
- **Role-based Access Control** (User, Admin)
### 💊 **Medication Management**
- **Add/Edit/Delete Medications** with dosage and frequency
- **Flexible Scheduling** (Daily, Twice/Three times daily, Custom intervals)
- **Visual Medication Cards** with custom icons
- **Medication History** tracking
### ⏰ **Reminder System**
- **Smart Scheduling** based on medication frequency
- **Dose Tracking** (Taken, Missed, Upcoming)
- **Custom Reminders** with personalized messages
- **Adherence Statistics** and progress monitoring
### 📊 **Analytics & Insights**
- **Daily Adherence Statistics** with visual charts
- **Medication-specific Analytics** (taken vs missed doses)
- **Progress Tracking** over time
- **Export Capabilities** for healthcare providers
### 🎨 **User Experience**
- **Responsive Design** for mobile and desktop
- **Dark/Light Theme** support
- **Intuitive Interface** with modern design
- **Onboarding Flow** for new users
- **Avatar Customization** with image upload
## 🏗️ Architecture
### **Frontend Stack**
- **React 19** with TypeScript
- **Vite** for fast development and building
- **Modern CSS** with responsive design
- **Component-based Architecture**
### **Backend Services**
- **CouchDB** for document-based data storage
- **Mailgun** for email delivery (verification, password reset)
- **bcrypt** for secure password hashing
- **JWT-like** token system for authentication
### **Infrastructure**
- **Docker & Docker Compose** for containerization
- **Nginx** for production static file serving
- **Multi-stage Builds** for optimized images
- **Health Checks** for service monitoring
### **Development Tools**
- **TypeScript** for type safety and modern JavaScript features
- **ESLint** for code quality and consistent style
- **Prettier** for automated code formatting
- **Pre-commit hooks** for automated quality checks
- **Bun** for fast package management and development
- **Environment-based Configuration** for flexible deployments
## 🚀 Quick Start
### **Prerequisites**
- [Docker](https://docker.com) and Docker Compose
- [Bun](https://bun.sh) (for local development)
- [Git](https://git-scm.com)
### **1. Clone and Setup**
```bash
git clone <repository-url>
cd meds
./setup.sh
# Validate configuration (optional)
./validate-env.sh
```
### **2. Configure Environment**
```bash
# Copy the template and customize
cp .env.example .env
# Edit .env with your credentials
nano .env
```
### **3. Deploy**
```bash
# Quick deployment
./deploy.sh
# Or manual Docker Compose
docker compose up -d
```
### **4. Access the Application**
- **Frontend**: http://localhost:8080
- **CouchDB Admin**: http://localhost:5984/\_utils
- **Default Admin**: `admin@localhost` / `change-this-secure-password`
## 🔧 Development
### **Local Development**
```bash
# Install dependencies
bun install
# Start development server
bun run dev
# Run with real CouchDB (Docker)
docker compose up -d couchdb
VITE_COUCHDB_URL=http://localhost:5984 bun run dev
```
### **Code Quality**
This project includes comprehensive code quality tools and pre-commit hooks. See [`docs/development/CODE_QUALITY.md`](docs/development/CODE_QUALITY.md) for detailed documentation.
```bash
# Format code
bun run format
# Check formatting
bun run format:check
# Lint code
bun run lint
# Fix lint issues
bun run lint:fix
# Type checking
bun run type-check
# Run pre-commit checks
bun run pre-commit
# Setup pre-commit hooks (one-time)
./scripts/setup-pre-commit.sh
```
**Automatic Quality Checks**: Pre-commit hooks automatically format code, run linting, type checking, and security scans on every commit.
### **Testing**
```bash
# Run tests
bun run test
# Run specific test file
bun run test auth.integration.test.ts
```
## 🔐 Security & Configuration
### **Environment Variables**
#### **Required Variables**
```bash
# CouchDB Configuration
COUCHDB_USER=admin
COUCHDB_PASSWORD=your-secure-password
VITE_COUCHDB_URL=http://localhost:5984
VITE_COUCHDB_USER=admin
VITE_COUCHDB_PASSWORD=your-secure-password
```
#### **Optional Variables**
```bash
# Mailgun (for email features)
MAILGUN_API_KEY=your-mailgun-api-key
MAILGUN_DOMAIN=your-domain.com
MAILGUN_FROM_EMAIL=noreply@your-domain.com
# Production Settings
NODE_ENV=production
```
### **Security Best Practices**
1. **🔒 Never commit `.env` files** - Already in `.gitignore`
2. **🛡️ Use strong passwords** - Minimum 8 characters with mixed case, numbers, symbols
3. **🔄 Rotate credentials regularly** - Especially in production
4. **📧 Verify email configuration** - Test Mailgun setup before production
5. **🔍 Monitor logs** - Check Docker logs for security events
6. **🚪 Limit access** - Use firewall rules for production deployments
### **Credential Management Methods**
#### **Development**
```bash
# Method 1: .env file (recommended for local dev)
cp .env.example .env
# Edit with your values
# Method 2: Shell environment
export COUCHDB_PASSWORD="secure-password"
export MAILGUN_API_KEY="key-123..."
```
#### **Production**
```bash
# Method 1: Secure deployment script
./deploy.sh production
# Method 2: CI/CD with environment variables
# Set in GitHub Actions, GitLab CI, etc.
# Method 3: External secrets management
# AWS Secrets Manager, Azure Key Vault, etc.
```
#### **Docker Deployment**
```bash
# Using .env file
docker compose --env-file .env.production up -d
# Using environment variables
COUCHDB_PASSWORD="secure-password" docker compose up -d
```
## 📁 Project Structure
```
meds/
├── 📄 README.md # This documentation
├── package.json # Dependencies and scripts
├── ⚙️ vite.config.ts # Build configuration
├── 📝 tsconfig.json # TypeScript configuration
├── 🎨 index.html # Entry point
├── 🚀 deploy.sh # Secure deployment script
├── 🔧 setup.sh # Development setup script
├── 🌱 seed-production.js # Database seeding
├── 🧪 test-production.js # Production testing
├── 🔒 .env.example # Environment template
├── 📁 docker/ # Container configuration
│ ├── 🐳 Dockerfile # Multi-stage Docker build
│ ├── 🐳 docker-compose.yaml # Service orchestration
│ ├── 🌐 nginx.conf # Production web server config
│ └── 🚫 .dockerignore # Docker ignore patterns
├── 📁 components/ # React components
│ ├── 🔐 AuthPage.tsx # Login/register interface
│ ├── 👑 AdminInterface.tsx # Admin user management
│ ├── 💊 AddMedicationModal.tsx # Medication creation
│ ├── ⏰ ReminderCard.tsx # Reminder display
│ ├── 📊 StatsModal.tsx # Analytics dashboard
│ └── ... # Other UI components
├── 📁 services/ # Business logic & APIs
│ ├── 🗄️ couchdb.ts # Mock database service
│ ├── 🗄️ couchdb.production.ts # Real CouchDB service
│ ├── 🏭 couchdb.factory.ts # Service factory
│ ├── 📧 mailgun.service.ts # Email delivery
│ ├── 📧 mailgun.config.ts # Email configuration
│ ├── 🌱 database.seeder.ts # Data seeding
│ └── 📁 auth/ # Authentication services
│ ├── 🔐 auth.service.ts # Core auth logic
│ ├── ✉️ emailVerification.service.ts
│ └── 📁 __tests__/ # Test suites
├── 📁 contexts/ # React context providers
│ └── 👤 UserContext.tsx # User state management
├── 📁 hooks/ # Custom React hooks
│ ├── 💾 useLocalStorage.ts # Persistent storage
│ ├── ⚙️ useSettings.ts # User preferences
│ └── 🎨 useTheme.ts # Theme management
└── 📁 utils/ # Utility functions
└── ⏰ schedule.ts # Reminder scheduling
```
## 🎯 API Reference
### **Authentication Endpoints**
#### **Register User**
```typescript
authService.register(email: string, password: string, username?: string)
// Returns: { user: User, verificationToken: EmailVerificationToken }
```
#### **Login User**
```typescript
authService.login({ email: string, password: string });
// Returns: { user: User, accessToken: string, refreshToken: string }
```
#### **OAuth Login**
```typescript
authService.loginWithOAuth(provider: 'google' | 'github', userData: OAuthUserData)
// Returns: { user: User, accessToken: string, refreshToken: string }
```
#### **Change Password**
```typescript
authService.changePassword(userId: string, currentPassword: string, newPassword: string)
// Returns: { success: boolean, message: string }
```
### **Database Operations**
#### **User Management**
```typescript
dbService.saveUser(user: User): Promise<User>
dbService.findUserByEmail(email: string): Promise<User | null>
dbService.updateUser(userId: string, updates: Partial<User>): Promise<User>
dbService.deleteUser(userId: string): Promise<void>
```
#### **Medication Management**
```typescript
dbService.saveMedication(medication: Medication): Promise<Medication>
dbService.getMedications(userId: string): Promise<Medication[]>
dbService.updateMedication(medicationId: string, updates: Partial<Medication>): Promise<Medication>
dbService.deleteMedication(medicationId: string): Promise<void>
```
#### **Reminder & Dose Tracking**
```typescript
dbService.saveReminder(reminder: CustomReminder): Promise<CustomReminder>
dbService.getReminders(userId: string): Promise<CustomReminder[]>
dbService.saveTakenDose(dose: TakenDose): Promise<void>
dbService.getTakenDoses(userId: string, date?: string): Promise<TakenDoses>
```
## 🐳 Docker Reference
### **Build Images**
```bash
# Build all services
docker compose build
# Build specific service
docker compose build frontend
# Build with no cache
docker compose build --no-cache
```
### **Manage Services**
```bash
# Start all services
docker compose up -d
# Start specific service
docker compose up -d couchdb
# Stop all services
docker compose down
# View logs
docker compose logs
docker compose logs frontend
```
### **Database Management**
```bash
# Access CouchDB container
docker compose exec couchdb bash
# Backup database
docker compose exec couchdb curl -X GET http://admin:password@localhost:5984/users/_all_docs?include_docs=true
# Restore database
# Use CouchDB Fauxton interface or curl commands
```
## 🧪 Testing & Quality Assurance
### **Development Testing**
```bash
# Run all unit tests
bun run test
# Run tests in watch mode
bun run test:watch
# Run with coverage
bun run test:coverage
# Run integration tests
bun run test:integration
# Run E2E tests with Playwright
bun run test:e2e
# Run E2E tests in UI mode
bun run test:e2e:ui
# Debug E2E tests
bun run test:e2e:debug
# Run all tests (unit + integration + e2e)
bun run test:all
```
### **Testing Structure**
- **Unit Tests**: Jest-based tests for individual functions and components
- **Integration Tests**: Production environment validation and service testing
- **E2E Tests**: Playwright-based full user journey testing across browsers
- **Manual Tests**: Browser console debugging scripts
See [tests/README.md](tests/README.md) for detailed testing documentation.
### **Test Production Environment**
```bash
# Run comprehensive production tests
bun test-production.js
# Manual testing checklist
./deploy.sh # Deploy environment
# Visit http://localhost:8080
# Test user registration/login
# Test admin interface
# Test medication management
# Test password change
# Verify data persistence
```
### **Performance Testing**
```bash
# Check service health
docker compose ps
curl -f http://localhost:5984/_up
curl -f http://localhost:8080
# Monitor resource usage
docker stats
```
### **Security Testing**
```bash
# Check for vulnerable dependencies
bun audit
# Validate environment configuration
./deploy.sh --dry-run
# Test authentication flows
# - Registration with weak passwords
# - Login with wrong credentials
# - Access admin without proper role
```
## 🚀 Deployment Guide
### **Development Deployment**
```bash
# Quick local setup
./setup.sh
```
### **Production Deployment**
```bash
# Secure production deployment
./deploy.sh production
```
### **Cloud Deployment**
#### **AWS EC2**
```bash
# 1. Launch EC2 instance with Docker
# 2. Clone repository
git clone <repo-url>
cd meds
# 3. Configure environment
cp .env.example .env
# Edit .env with production values
# 4. Deploy
./deploy.sh production
```
#### **Google Cloud Run**
```bash
# Build and push image
gcloud builds submit --tag gcr.io/PROJECT-ID/meds-app
# Deploy with environment variables
gcloud run deploy meds-app \
--image gcr.io/PROJECT-ID/meds-app \
--set-env-vars COUCHDB_URL=your-couchdb-url \
--set-env-vars MAILGUN_API_KEY=your-key
```
#### **Kubernetes (Template-Based)**
```bash
# 1. Copy and configure environment
cp .env.example .env
# Edit .env with your secure credentials
# 2. Deploy with templates (recommended)
./scripts/k8s-deploy-template.sh deploy
# Alternative: Manual deployment
# Create secrets manually
kubectl create secret generic meds-secrets \
--from-literal=couchdb-password=secure-password \
--from-literal=mailgun-api-key=your-key
# Apply manifests
kubectl apply -f k8s/
```
## 🔍 Troubleshooting
### **Common Issues**
#### **Environment Variables Not Loading**
```bash
# Check .env file exists and is properly formatted
cat .env
# Verify Docker Compose uses env file
docker compose config
```
#### **CouchDB Connection Issues**
```bash
# Check CouchDB health
curl -u admin:password http://localhost:5984/_up
# Verify credentials
docker compose logs couchdb
# Reset database
docker compose down
docker volume rm meds_couchdb-data
docker compose up -d
```
#### **Frontend Build Failures**
```bash
# Clear node modules and reinstall
rm -rf node_modules bun.lockb
bun install
# Check for TypeScript errors
bun run type-check
# Build with verbose output
bun run build --verbose
```
#### **Email Not Sending**
```bash
# Verify Mailgun configuration
echo $MAILGUN_API_KEY
echo $MAILGUN_DOMAIN
# Check Mailgun service logs
docker compose logs frontend | grep -i mailgun
# Test Mailgun API directly
curl -s --user 'api:YOUR_API_KEY' \
https://api.mailgun.net/v3/YOUR_DOMAIN/messages \
-F from='test@YOUR_DOMAIN' \
-F to='you@example.com' \
-F subject='Test' \
-F text='Testing'
```
### **Performance Issues**
```bash
# Check resource usage
docker stats
# Optimize Docker images
docker system prune -a
# Monitor application performance
docker compose logs --tail=100 frontend
```
### **Debug Mode**
```bash
# Run with debug logging
DEBUG=* docker compose up
# Access container for debugging
docker compose exec frontend sh
docker compose exec couchdb bash
```
## 📚 Documentation
### **Complete Documentation Index**
For comprehensive documentation, visit **[`docs/README.md`](docs/README.md)** which includes:
#### 🏗️ Architecture & Design
- [Project Structure](docs/architecture/PROJECT_STRUCTURE.md) - Codebase organization
- [Template Approach](docs/architecture/TEMPLATE_APPROACH.md) - Design philosophy
#### 🚀 Setup & Configuration
- [Complete Template Configuration](docs/setup/COMPLETE_TEMPLATE_CONFIGURATION.md) - Full setup guide
- [Setup Complete](docs/setup/SETUP_COMPLETE.md) - Post-setup verification
#### 💻 Development
- [API Documentation](docs/development/API.md) - REST API endpoints
- [Code Quality](docs/development/CODE_QUALITY.md) - Quality standards & tools
- [Application Security](docs/development/APPLICATION_SECURITY.md) - App security practices
#### 🚢 Deployment
- [Deployment Guide](docs/deployment/DEPLOYMENT.md) - General deployment
- [Docker Configuration](docs/deployment/DOCKER_IMAGE_CONFIGURATION.md) - Docker setup
- [Gitea Setup](docs/deployment/GITEA_SETUP.md) - CI/CD configuration
#### 🔄 Migration Guides
- [NodeJS Pre-commit Migration](docs/migration/NODEJS_PRECOMMIT_MIGRATION.md) - Modern git hooks
- [Buildx Migration](docs/migration/BUILDX_MIGRATION.md) - Docker improvements
## 📚 Additional Resources
### **Documentation**
- [CouchDB Documentation](https://docs.couchdb.org/)
- [Mailgun API Reference](https://documentation.mailgun.com/)
- [Docker Compose Reference](https://docs.docker.com/compose/)
- [React Documentation](https://react.dev/)
### **Development Tools**
- [Bun Documentation](https://bun.sh/docs)
- [Vite Documentation](https://vitejs.dev/)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
### **Security Resources**
- [OWASP Security Guidelines](https://owasp.org/)
- [Docker Security Best Practices](https://docs.docker.com/engine/security/)
- [CouchDB Security](https://docs.couchdb.org/en/stable/intro/security.html)
## 🤝 Contributing
1. **Fork the repository**
2. **Create a feature branch**: `git checkout -b feature/amazing-feature`
3. **Commit changes**: `git commit -m 'Add amazing feature'`
4. **Push to branch**: `git push origin feature/amazing-feature`
5. **Open a Pull Request**
### **Development Workflow**
```bash
# Setup development environment
./setup.sh
# Make changes and test
bun run dev
bun run lint
bun run type-check
# Test in production environment
./deploy.sh
bun test-production.js
# Submit pull request
```
## Documentation
### **Project Documentation**
- **[Code Quality Guide](docs/CODE_QUALITY.md)** - Code formatting, linting, and pre-commit hooks setup
- **[Security Guide](docs/SECURITY.md)** - Security best practices and configuration
- **[Deployment Guide](docs/DEPLOYMENT.md)** - Production deployment instructions
- **[API Documentation](docs/API.md)** - Complete API reference
- **[Contributing Guide](CONTRIBUTING.md)** - Development guidelines and contribution process
- **[License](LICENSE)** - MIT license and third-party attributions
- **[Changelog](CHANGELOG.md)** - Version history and release notes
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
- **CouchDB Team** for the robust database system
- **Mailgun** for reliable email delivery
- **React Team** for the excellent frontend framework
- **Docker Team** for containerization technology
- **Bun Team** for the fast JavaScript runtime
---
**Built with ❤️ for better medication adherence and health outcomes.**
For support, please open an issue on GitHub or contact the development team.
+194
View File
@@ -0,0 +1,194 @@
# 🔐 Security Configuration Guide for RxMinder
This guide outlines the security configurations in RxMinder and how to properly secure your deployment.
> **📋 Related Documentation**: For application-level security practices (password requirements, authentication, etc.), see [`docs/development/APPLICATION_SECURITY.md`](docs/development/APPLICATION_SECURITY.md)
## ⚠️ Critical Security Updates
We use a template-based approach with environment variables for secure, user-friendly credential management.
## 🔑 Template-Based Configuration
### Kubernetes Deployment
**Files**: `k8s/*.yaml.template`
RxMinder uses Kubernetes template files that automatically substitute environment variables. No manual base64 encoding required!
**Template files:**
- `k8s/couchdb-secret.yaml.template` - Database credentials
- `k8s/ingress.yaml.template` - Ingress configuration
- `k8s/configmap.yaml.template` - Application configuration
- `k8s/frontend-deployment.yaml.template` - Frontend deployment
**Example secret template:**
```yaml
apiVersion: v1
kind: Secret
metadata:
name: couchdb-secret
labels:
app: ${APP_NAME:-rxminder}
type: Opaque
stringData:
username: ${COUCHDB_USER:-admin}
password: ${COUCHDB_PASSWORD:-change-this-secure-password}
```
### Environment Variables
**File**: `.env`
```env
# Application Name (used in Kubernetes labels)
APP_NAME=rxminder
# Database Credentials (automatically substituted in templates)
COUCHDB_USER=admin
COUCHDB_PASSWORD=your-very-secure-password
VITE_COUCHDB_USER=admin
VITE_COUCHDB_PASSWORD=your-very-secure-password
# Kubernetes Configuration
INGRESS_HOST=rxminder.yourdomain.com
```
## 🚀 Template-Based Deployment
### Quick Start
1. **Copy environment template:**
```bash
cp .env.example .env
```
2. **Update .env with your secure credentials:**
```bash
# Edit .env with your secure passwords and configuration
nano .env
```
3. **Deploy with templates:**
```bash
./scripts/k8s-deploy-template.sh deploy
```
The deployment script automatically:
- ✅ Loads environment variables from `.env`
- ✅ Substitutes variables in template files
- ✅ Applies resources in correct dependency order
- ✅ Runs database seeding
- ✅ Shows deployment status
## 🛡️ Security Best Practices
### 1. **Strong Passwords**
- Use passwords with at least 16 characters
- Include uppercase, lowercase, numbers, and symbols
- Use a password manager to generate unique passwords
### 2. **Environment-Specific Credentials**
- **Development**: Use different credentials than production
- **Staging**: Use different credentials than production
- **Production**: Use strong, unique credentials
### 3. **Credential Rotation**
- Rotate database credentials regularly
- Update Kubernetes secrets using `kubectl`
- Update Docker environment variables
### 4. **Secret Management**
- Never commit actual credentials to version control
- Use Kubernetes secrets for production deployments
- Consider external secret management (HashiCorp Vault, etc.)
## 🔄 Updating Credentials
### Kubernetes Environment
```bash
# Create new secret with secure credentials
kubectl create secret generic couchdb-secret \
--from-literal=username=your-secure-username \
--from-literal=password=your-very-secure-password \
--dry-run=client -o yaml | kubectl apply -f -
# Restart pods to pick up new credentials
kubectl rollout restart statefulset/couchdb
kubectl rollout restart deployment/frontend
```
### Docker Environment
````bash
### Docker Environment
```bash
# Update environment variables and restart containers
export COUCHDB_PASSWORD="your-very-secure-password"
export VITE_COUCHDB_PASSWORD="your-very-secure-password"
docker compose down && docker compose up -d
````
## 🔄 CI/CD Security
### GitHub Actions / Gitea Workflows
Set these secrets in your repository settings:
- `VITE_COUCHDB_PASSWORD`: Your production CouchDB password
- `GITEA_TOKEN` / `GITHUB_TOKEN`: For container registry authentication
**Important**: CI/CD workflows use secure fallback values but should use repository secrets for production builds.
### Test Environments
Test databases use secure passwords by default:
- CI containers: `test-secure-password`
- End-to-end tests: Use dedicated test credentials (acceptable for testing)
## ✅ Security Checklist
```
## ⚡ Quick Security Checklist
- [ ] Changed default admin password in `k8s/couchdb-secret.yaml`
- [ ] Updated `.env` file with secure credentials
- [ ] Used different passwords for each environment
- [ ] Credentials are not in version control (in `.gitignore`)
- [ ] Reviewed all scripts for hardcoded values
- [ ] Configured proper network policies (if using Kubernetes)
- [ ] Set up TLS/SSL for production deployments
## 🚨 Emergency Response
If credentials are compromised:
1. **Immediately** change passwords in all environments
2. Rotate Kubernetes secrets
3. Review access logs
4. Update any applications using the old credentials
5. Consider rotating container registry credentials if needed
## 📚 Additional Resources
- [CouchDB Security Best Practices](https://docs.couchdb.org/en/stable/intro/security.html)
- [Kubernetes Secrets Management](https://kubernetes.io/docs/concepts/configuration/secret/)
- [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/)
---
**Remember**: Security is an ongoing process, not a one-time setup. Regularly review and update your security configurations.
```
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+2611
View File
File diff suppressed because it is too large Load Diff
+98
View File
@@ -0,0 +1,98 @@
# 🧩 Component Architecture
## Component Organization
The components are organized by feature and responsibility for better maintainability:
```
components/
├── medication/ # Medication-related components
│ ├── AddMedicationModal.tsx
│ ├── EditMedicationModal.tsx
│ ├── ManageMedicationsModal.tsx
│ ├── DoseCard.tsx
│ └── index.ts
├── auth/ # Authentication components
│ ├── AuthPage.tsx
│ ├── AvatarDropdown.tsx
│ ├── ChangePasswordModal.tsx
│ └── index.ts
├── admin/ # Admin interface components
│ ├── AdminInterface.tsx
│ └── index.ts
├── modals/ # Generic modal components
│ ├── AccountModal.tsx
│ ├── AddReminderModal.tsx
│ ├── EditReminderModal.tsx
│ ├── HistoryModal.tsx
│ ├── ManageRemindersModal.tsx
│ ├── OnboardingModal.tsx
│ ├── StatsModal.tsx
│ └── index.ts
├── ui/ # Reusable UI components
│ ├── BarChart.tsx
│ ├── ReminderCard.tsx
│ ├── ThemeSwitcher.tsx
│ └── index.ts
└── icons/ # Icon components
└── Icons.tsx
```
## Import Structure
### Feature-Based Imports
```tsx
// Medication components
import { AddMedicationModal, DoseCard } from './components/medication';
// Authentication components
import { AuthPage, AvatarDropdown } from './components/auth';
// Modal components
import { AccountModal, StatsModal } from './components/modals';
// UI components
import { BarChart, ThemeSwitcher } from './components/ui';
```
## Component Categories
### 🏥 **Medication Components**
- **Purpose**: Medication management and dose tracking
- **Components**: AddMedicationModal, EditMedicationModal, ManageMedicationsModal, DoseCard
- **Responsibility**: CRUD operations for medications and dose status management
### 🔐 **Authentication Components**
- **Purpose**: User authentication and profile management
- **Components**: AuthPage, AvatarDropdown, ChangePasswordModal
- **Responsibility**: Login/register, user menus, credential management
### 👑 **Admin Components**
- **Purpose**: Administrative functionality
- **Components**: AdminInterface
- **Responsibility**: User management, system administration
### 🎛️ **Modal Components**
- **Purpose**: Overlay interfaces for specific actions
- **Components**: AccountModal, AddReminderModal, EditReminderModal, HistoryModal, ManageRemindersModal, OnboardingModal, StatsModal
- **Responsibility**: Focused user interactions in modal format
### 🎨 **UI Components**
- **Purpose**: Reusable interface elements
- **Components**: BarChart, ReminderCard, ThemeSwitcher
- **Responsibility**: Visual presentation and data display
## Benefits of This Organization
**Feature Cohesion** - Related components grouped together
**Easy Navigation** - Clear folder structure
**Reduced Import Complexity** - Index files for clean imports
**Better Maintainability** - Logical separation of concerns
**Scalability** - Easy to add new components in appropriate categories
**Testing** - Each feature can be tested independently
+361
View File
@@ -0,0 +1,361 @@
import React, { useState, useEffect } from 'react';
import { User, UserRole } from '../../types';
import { AccountStatus } from '../../services/auth/auth.constants';
import { dbService } from '../../services/couchdb.factory';
import { useUser } from '../../contexts/UserContext';
interface AdminInterfaceProps {
onClose: () => void;
}
const AdminInterface: React.FC<AdminInterfaceProps> = ({ onClose }) => {
const { user: currentUser } = useUser();
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [newPassword, setNewPassword] = useState('');
useEffect(() => {
loadUsers();
}, []);
const loadUsers = async () => {
try {
const allUsers = await dbService.getAllUsers();
setUsers(allUsers);
} catch (error) {
setError('Failed to load users');
console.error('Error loading users:', error);
} finally {
setLoading(false);
}
};
const handleSuspendUser = async (userId: string) => {
try {
await dbService.suspendUser(userId);
await loadUsers();
} catch (error) {
setError('Failed to suspend user');
console.error('Error suspending user:', error);
}
};
const handleActivateUser = async (userId: string) => {
try {
await dbService.activateUser(userId);
await loadUsers();
} catch (error) {
setError('Failed to activate user');
console.error('Error activating user:', error);
}
};
const handleDeleteUser = async (userId: string) => {
if (
!confirm(
'Are you sure you want to delete this user? This action cannot be undone.'
)
) {
return;
}
try {
await dbService.deleteUser(userId);
await loadUsers();
} catch (error) {
setError('Failed to delete user');
console.error('Error deleting user:', error);
}
};
const handleChangePassword = async (userId: string) => {
if (!newPassword || newPassword.length < 6) {
setError('Password must be at least 6 characters long');
return;
}
try {
await dbService.changeUserPassword(userId, newPassword);
setNewPassword('');
setSelectedUser(null);
setError('');
alert('Password changed successfully');
} catch (error) {
setError('Failed to change password');
console.error('Error changing password:', error);
}
};
const getStatusColor = (status?: AccountStatus) => {
switch (status) {
case AccountStatus.ACTIVE:
return 'text-green-600 bg-green-100';
case AccountStatus.SUSPENDED:
return 'text-red-600 bg-red-100';
case AccountStatus.PENDING:
return 'text-yellow-600 bg-yellow-100';
default:
return 'text-gray-600 bg-gray-100';
}
};
const getRoleColor = (role?: UserRole) => {
return role === UserRole.ADMIN
? 'text-purple-600 bg-purple-100'
: 'text-blue-600 bg-blue-100';
};
if (currentUser?.role !== UserRole.ADMIN) {
return (
<div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'>
<div className='bg-white dark:bg-slate-800 rounded-lg p-6 max-w-md'>
<h2 className='text-xl font-bold text-red-600 mb-4'>Access Denied</h2>
<p className='text-slate-600 dark:text-slate-300 mb-4'>
You don't have permission to access the admin interface.
</p>
<button
onClick={onClose}
className='w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md'
>
Close
</button>
</div>
</div>
);
}
return (
<div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4'>
<div className='bg-white dark:bg-slate-800 rounded-lg w-full max-w-6xl max-h-[90vh] overflow-hidden'>
<div className='p-6 border-b border-slate-200 dark:border-slate-600'>
<div className='flex justify-between items-center'>
<h2 className='text-2xl font-bold text-slate-800 dark:text-slate-100'>
Admin Interface
</h2>
<button
onClick={onClose}
className='text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
>
<svg
className='w-6 h-6'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</button>
</div>
</div>
<div className='p-6 overflow-y-auto max-h-[calc(90vh-120px)]'>
{error && (
<div className='bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4'>
{error}
</div>
)}
{loading ? (
<div className='text-center py-8'>
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600 mx-auto'></div>
<p className='mt-2 text-slate-600 dark:text-slate-300'>
Loading users...
</p>
</div>
) : (
<div className='space-y-4'>
<div className='flex justify-between items-center'>
<h3 className='text-lg font-semibold text-slate-800 dark:text-slate-100'>
User Management ({users.length} users)
</h3>
<button
onClick={loadUsers}
className='bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md text-sm'
>
Refresh
</button>
</div>
<div className='overflow-x-auto'>
<table className='min-w-full bg-white dark:bg-slate-700 rounded-lg overflow-hidden'>
<thead className='bg-slate-50 dark:bg-slate-600'>
<tr>
<th className='px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-300 uppercase tracking-wider'>
User
</th>
<th className='px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-300 uppercase tracking-wider'>
Email
</th>
<th className='px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-300 uppercase tracking-wider'>
Status
</th>
<th className='px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-300 uppercase tracking-wider'>
Role
</th>
<th className='px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-300 uppercase tracking-wider'>
Created
</th>
<th className='px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-300 uppercase tracking-wider'>
Actions
</th>
</tr>
</thead>
<tbody className='divide-y divide-slate-200 dark:divide-slate-600'>
{users.map(user => (
<tr
key={user._id}
className='hover:bg-slate-50 dark:hover:bg-slate-600'
>
<td className='px-4 py-4'>
<div className='flex items-center'>
{user.avatar ? (
<img
src={user.avatar}
alt={user.username}
className='w-8 h-8 rounded-full mr-3'
/>
) : (
<div className='w-8 h-8 bg-indigo-600 rounded-full flex items-center justify-center mr-3'>
<span className='text-white text-sm font-medium'>
{user.username.charAt(0).toUpperCase()}
</span>
</div>
)}
<div>
<div className='text-sm font-medium text-slate-900 dark:text-slate-100'>
{user.username}
</div>
<div className='text-sm text-slate-500 dark:text-slate-400'>
ID: {user._id.slice(-8)}
</div>
</div>
</div>
</td>
<td className='px-4 py-4 text-sm text-slate-900 dark:text-slate-100'>
{user.email}
{user.emailVerified && (
<span className='ml-2 text-green-600'>✓</span>
)}
</td>
<td className='px-4 py-4'>
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(user.status)}`}
>
{user.status || 'Unknown'}
</span>
</td>
<td className='px-4 py-4'>
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getRoleColor(user.role)}`}
>
{user.role || 'USER'}
</span>
</td>
<td className='px-4 py-4 text-sm text-slate-500 dark:text-slate-400'>
{user.createdAt
? new Date(user.createdAt).toLocaleDateString()
: 'Unknown'}
</td>
<td className='px-4 py-4'>
<div className='flex space-x-2'>
{user.status === AccountStatus.ACTIVE ? (
<button
onClick={() => handleSuspendUser(user._id)}
className='text-red-600 hover:text-red-800 text-xs'
disabled={user._id === currentUser?._id}
>
Suspend
</button>
) : (
<button
onClick={() => handleActivateUser(user._id)}
className='text-green-600 hover:text-green-800 text-xs'
>
Activate
</button>
)}
{user.password && (
<button
onClick={() => setSelectedUser(user)}
className='text-blue-600 hover:text-blue-800 text-xs'
>
Change Password
</button>
)}
<button
onClick={() => handleDeleteUser(user._id)}
className='text-red-600 hover:text-red-800 text-xs'
disabled={
user._id === currentUser?._id ||
user.role === UserRole.ADMIN
}
>
Delete
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
{/* Password Change Modal */}
{selectedUser && (
<div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-60'>
<div className='bg-white dark:bg-slate-800 rounded-lg p-6 max-w-md w-full mx-4'>
<h3 className='text-lg font-semibold text-slate-800 dark:text-slate-100 mb-4'>
Change Password for {selectedUser.username}
</h3>
<div className='space-y-4'>
<div>
<label className='block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2'>
New Password
</label>
<input
type='password'
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
className='w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:border-slate-600 dark:text-white'
placeholder='Enter new password (min 6 characters)'
/>
</div>
<div className='flex space-x-3'>
<button
onClick={() => handleChangePassword(selectedUser._id)}
className='flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md'
>
Change Password
</button>
<button
onClick={() => {
setSelectedUser(null);
setNewPassword('');
}}
className='flex-1 bg-slate-300 hover:bg-slate-400 text-slate-700 font-medium py-2 px-4 rounded-md'
>
Cancel
</button>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default AdminInterface;
+2
View File
@@ -0,0 +1,2 @@
// Admin Components
export { default as AdminInterface } from './AdminInterface';
+316
View File
@@ -0,0 +1,316 @@
import React, { useState, useEffect } from 'react';
import { useUser } from '../../contexts/UserContext';
import { authService } from '../../services/auth/auth.service';
import { PillIcon } from '../icons/Icons';
const AuthPage: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isSignUp, setIsSignUp] = useState(false);
const [error, setError] = useState('');
const { login, register, loginWithOAuth } = useUser();
// State for email verification result
const [verificationResult, setVerificationResult] = useState<
null | 'success' | 'error'
>(null);
// Extract token from URL and verify email
useEffect(() => {
const path = window.location.pathname;
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
if (path === '/verify-email' && token) {
authService
.verifyEmail(token)
.then(() => setVerificationResult('success'))
.catch(() => setVerificationResult('error'));
}
}, []);
// FIX: Made the function async and added await to handle promises from login.
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!email.trim()) {
setError('Email cannot be empty.');
return;
}
if (!password.trim()) {
setError('Password cannot be empty.');
return;
}
// Validate email format (allow localhost for admin)
const emailRegex = /^[^\s@]+@[^\s@]+$/; // Simplified to allow any domain including localhost
if (!emailRegex.test(email)) {
setError('Please enter a valid email address.');
return;
}
if (isSignUp) {
// Registration
if (password.length < 6) {
setError('Password must be at least 6 characters long.');
return;
}
if (password !== confirmPassword) {
setError('Passwords do not match.');
return;
}
const success = await register(email, password);
if (success) {
setError(
'Registration successful! Please check your email for verification (demo: verification not actually sent).'
);
setIsSignUp(false); // Switch back to login mode
setPassword('');
setConfirmPassword('');
} else {
setError('Registration failed. Email may already be in use.');
}
} else {
// Login
const success = await login(email, password);
if (!success) {
setError('Login failed. Please check your email and password.');
}
}
};
const handleOAuthLogin = async (provider: 'google' | 'github') => {
setError('');
try {
// Mock OAuth data - in a real app, this would come from the OAuth provider
const mockUserData = {
email: provider === 'google' ? 'user@gmail.com' : 'user@github.com',
username: `${provider}_user_${Date.now()}`,
avatar: `https://via.placeholder.com/150?text=${provider.toUpperCase()}`,
};
const success = await loginWithOAuth(provider, mockUserData);
if (!success) {
setError(`${provider} authentication failed. Please try again.`);
}
} catch (error) {
setError(`${provider} authentication failed. Please try again.`);
}
};
if (verificationResult) {
return (
<div className='min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900'>
<div className='text-center'>
{verificationResult === 'success' ? (
<p className='text-green-600'>
Email verified successfully! You can now sign in.
</p>
) : (
<p className='text-red-600'>
Email verification failed. Please try again.
</p>
)}
<button
onClick={() => {
setVerificationResult(null);
window.location.href = '/';
}}
className='mt-4 px-4 py-2 bg-indigo-600 text-white rounded'
>
Go to Sign In
</button>
</div>
</div>
);
}
return (
<div className='min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900 px-4'>
<div className='w-full max-w-sm'>
<div className='text-center mb-8'>
<div className='inline-block bg-indigo-600 p-3 rounded-xl mb-4'>
<PillIcon className='w-8 h-8 text-white' />
</div>
<h1 className='text-3xl font-bold text-slate-800 dark:text-slate-100'>
Medication Reminder
</h1>
<p className='text-slate-500 dark:text-slate-400 mt-1'>
Sign in with your email or create an account
</p>
</div>
<div className='bg-white dark:bg-slate-800 rounded-lg shadow-lg p-8'>
<div className='flex space-x-1 mb-6'>
<button
type='button'
onClick={() => {
setIsSignUp(false);
setError('');
setPassword('');
setConfirmPassword('');
}}
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-colors ${
!isSignUp
? 'bg-indigo-600 text-white'
: 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600'
}`}
>
Sign In
</button>
<button
type='button'
onClick={() => {
setIsSignUp(true);
setError('');
setPassword('');
setConfirmPassword('');
}}
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-colors ${
isSignUp
? 'bg-indigo-600 text-white'
: 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600'
}`}
>
Sign Up
</button>
</div>
<form onSubmit={handleSubmit} className='mb-6'>
<div className='space-y-4'>
<div>
<label
htmlFor='email'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Email Address
</label>
<input
type='email'
id='email'
value={email}
onChange={e => setEmail(e.target.value)}
required
autoFocus
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
placeholder='your@email.com'
/>
</div>
<div>
<label
htmlFor='password'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Password
</label>
<input
type='password'
id='password'
value={password}
onChange={e => setPassword(e.target.value)}
required
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
placeholder={
isSignUp
? 'Create a password (min 6 characters)'
: 'Enter your password'
}
/>
</div>
{isSignUp && (
<div>
<label
htmlFor='confirmPassword'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Confirm Password
</label>
<input
type='password'
id='confirmPassword'
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
required
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
placeholder='Confirm your password'
/>
</div>
)}
</div>
{error && (
<p
className={`text-sm mt-3 ${error.includes('successful') ? 'text-green-600' : 'text-red-500'}`}
>
{error}
</p>
)}
<button
type='submit'
className='w-full mt-6 bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'
>
{isSignUp ? 'Create Account' : 'Sign In'}
</button>
</form>
<div className='relative mb-6'>
<div className='absolute inset-0 flex items-center'>
<div className='w-full border-t border-slate-300 dark:border-slate-600' />
</div>
<div className='relative flex justify-center text-sm'>
<span className='px-2 bg-white dark:bg-slate-800 text-slate-500 dark:text-slate-400'>
Or create an account with
</span>
</div>
</div>
<div className='space-y-3'>
<button
onClick={() => handleOAuthLogin('google')}
className='w-full flex items-center justify-center px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-md shadow-sm text-sm font-medium text-slate-700 dark:text-slate-200 bg-white dark:bg-slate-700 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200'
>
<svg className='w-4 h-4 mr-2' viewBox='0 0 24 24'>
<path
fill='#4285F4'
d='M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z'
/>
<path
fill='#34A853'
d='M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z'
/>
<path
fill='#FBBC05'
d='M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z'
/>
<path
fill='#EA4335'
d='M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z'
/>
</svg>
Continue with Google
</button>
<button
onClick={() => handleOAuthLogin('github')}
className='w-full flex items-center justify-center px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-md shadow-sm text-sm font-medium text-slate-700 dark:text-slate-200 bg-white dark:bg-slate-700 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200'
>
<svg className='w-4 h-4 mr-2 fill-current' viewBox='0 0 24 24'>
<path d='M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z' />
</svg>
Continue with GitHub
</button>
</div>
</div>
</div>
</div>
);
};
export default AuthPage;
+112
View File
@@ -0,0 +1,112 @@
import React, { useState, useRef, useEffect } from 'react';
import { User, UserRole } from '../../types';
interface AvatarDropdownProps {
user: User;
onLogout: () => void;
onAdmin?: () => void;
onChangePassword?: () => void;
}
const getInitials = (name: string) => {
return name ? name.charAt(0).toUpperCase() : '?';
};
const AvatarDropdown: React.FC<AvatarDropdownProps> = ({
user,
onLogout,
onAdmin,
onChangePassword,
}) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className='relative' ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className='w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-lg font-bold text-slate-600 dark:text-slate-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-slate-900'
aria-label='User menu'
>
{user.avatar ? (
<img
src={user.avatar}
alt='User avatar'
className='w-full h-full rounded-full object-cover'
/>
) : (
<span>{getInitials(user.username)}</span>
)}
</button>
{isOpen && (
<div className='absolute right-0 mt-2 w-48 bg-white dark:bg-slate-800 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 py-1 z-30 border dark:border-slate-700'>
<div className='px-4 py-2 border-b border-slate-200 dark:border-slate-700'>
<p className='text-sm text-slate-500 dark:text-slate-400'>
Signed in as
</p>
<p className='text-sm font-medium text-slate-800 dark:text-slate-200 truncate'>
{user.username}
</p>
{user.role === UserRole.ADMIN && (
<p className='text-xs text-purple-600 dark:text-purple-400 font-medium'>
Administrator
</p>
)}
</div>
{/* Password Change Option - Only for password-based accounts */}
{user.password && onChangePassword && (
<button
onClick={() => {
onChangePassword();
setIsOpen(false);
}}
className='w-full text-left px-4 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700'
>
Change Password
</button>
)}
{/* Admin Interface - Only for admins */}
{user.role === UserRole.ADMIN && onAdmin && (
<button
onClick={() => {
onAdmin();
setIsOpen(false);
}}
className='w-full text-left px-4 py-2 text-sm text-purple-700 dark:text-purple-300 hover:bg-slate-100 dark:hover:bg-slate-700 font-medium'
>
Admin Interface
</button>
)}
<button
onClick={() => {
onLogout();
setIsOpen(false);
}}
className='w-full text-left px-4 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700'
>
Logout
</button>
</div>
)}
</div>
);
};
export default AvatarDropdown;
+192
View File
@@ -0,0 +1,192 @@
import React, { useState } from 'react';
import { authService } from '../../services/auth/auth.service';
import { useUser } from '../../contexts/UserContext';
interface ChangePasswordModalProps {
onClose: () => void;
onSuccess: () => void;
}
const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({
onClose,
onSuccess,
}) => {
const { user } = useUser();
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
// Validation
if (!currentPassword || !newPassword || !confirmPassword) {
setError('All fields are required');
setLoading(false);
return;
}
if (newPassword.length < 6) {
setError('New password must be at least 6 characters long');
setLoading(false);
return;
}
if (newPassword !== confirmPassword) {
setError('New passwords do not match');
setLoading(false);
return;
}
if (currentPassword === newPassword) {
setError('New password must be different from current password');
setLoading(false);
return;
}
try {
await authService.changePassword(user!._id, currentPassword, newPassword);
onSuccess();
onClose();
} catch (error: any) {
setError(error.message || 'Failed to change password');
} finally {
setLoading(false);
}
};
// Don't show for OAuth users
if (!user?.password) {
return (
<div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'>
<div className='bg-white dark:bg-slate-800 rounded-lg p-6 max-w-md'>
<h2 className='text-xl font-bold text-slate-800 dark:text-slate-100 mb-4'>
Password Change Not Available
</h2>
<p className='text-slate-600 dark:text-slate-300 mb-4'>
This account was created using OAuth (Google/GitHub). Password
changes are not available for OAuth accounts.
</p>
<button
onClick={onClose}
className='w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md'
>
Close
</button>
</div>
</div>
);
}
return (
<div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'>
<div className='bg-white dark:bg-slate-800 rounded-lg p-6 max-w-md w-full mx-4'>
<div className='flex justify-between items-center mb-6'>
<h2 className='text-xl font-bold text-slate-800 dark:text-slate-100'>
Change Password
</h2>
<button
onClick={onClose}
className='text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
>
<svg
className='w-6 h-6'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</button>
</div>
<form onSubmit={handleSubmit} className='space-y-4'>
<div>
<label
htmlFor='currentPassword'
className='block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1'
>
Current Password
</label>
<input
type='password'
id='currentPassword'
value={currentPassword}
onChange={e => setCurrentPassword(e.target.value)}
className='w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:border-slate-600 dark:text-white'
placeholder='Enter your current password'
/>
</div>
<div>
<label
htmlFor='newPassword'
className='block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1'
>
New Password
</label>
<input
type='password'
id='newPassword'
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
className='w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:border-slate-600 dark:text-white'
placeholder='Enter new password (min 6 characters)'
/>
</div>
<div>
<label
htmlFor='confirmPassword'
className='block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1'
>
Confirm New Password
</label>
<input
type='password'
id='confirmPassword'
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
className='w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:border-slate-600 dark:text-white'
placeholder='Confirm your new password'
/>
</div>
{error && (
<div className='bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded'>
{error}
</div>
)}
<div className='flex space-x-3 pt-4'>
<button
type='submit'
disabled={loading}
className='flex-1 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200'
>
{loading ? 'Changing...' : 'Change Password'}
</button>
<button
type='button'
onClick={onClose}
className='flex-1 bg-slate-300 hover:bg-slate-400 text-slate-700 font-medium py-2 px-4 rounded-md transition-colors duration-200'
>
Cancel
</button>
</div>
</form>
</div>
</div>
);
};
export default ChangePasswordModal;
+4
View File
@@ -0,0 +1,4 @@
// Authentication Components
export { default as AuthPage } from './AuthPage';
export { default as AvatarDropdown } from './AvatarDropdown';
export { default as ChangePasswordModal } from './ChangePasswordModal';
+546
View File
@@ -0,0 +1,546 @@
import React from 'react';
export const PillIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='m10.5 20.5 10-10a4.95 4.95 0 1 0-7-7l-10 10a4.95 4.95 0 1 0 7 7Z' />
<path d='m8.5 8.5 7 7' />
</svg>
);
export const ClockIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<circle cx='12' cy='12' r='10' />
<polyline points='12 6 12 12 16 14' />
</svg>
);
export const CheckCircleIcon: React.FC<
React.SVGProps<SVGSVGElement>
> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z' />
<path d='m9 12 2 2 4-4' />
</svg>
);
export const XCircleIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<circle cx='12' cy='12' r='10' />
<line x1='15' y1='9' x2='9' y2='15' />
<line x1='9' y1='9' x2='15' y2='15' />
</svg>
);
export const PlusIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<line x1='12' y1='5' x2='12' y2='19' />
<line x1='5' y1='12' x2='19' y2='12' />
</svg>
);
export const TrashIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<polyline points='3 6 5 6 21 6' />
<path d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2' />
<line x1='10' y1='11' x2='10' y2='17' />
<line x1='14' y1='11' x2='14' y2='17' />
</svg>
);
export const EditIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'></path>
<path d='M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z'></path>
</svg>
);
export const MenuIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<line x1='4' x2='20' y1='12' y2='12' />
<line x1='4' x2='20' y1='6' y2='6' />
<line x1='4' x2='20' y1='18' y2='18' />
</svg>
);
export const HistoryIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8' />
<path d='M3 3v5h5' />
<path d='M12 7v5l4 2' />
</svg>
);
export const InfoIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<circle cx='12' cy='12' r='10'></circle>
<line x1='12' y1='16' x2='12' y2='12'></line>
<line x1='12' y1='8' x2='12.01' y2='8'></line>
</svg>
);
export const SunIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<circle cx='12' cy='12' r='4'></circle>
<path d='M12 2v2'></path>
<path d='M12 20v2'></path>
<path d='m4.93 4.93 1.41 1.41'></path>
<path d='m17.66 17.66 1.41 1.41'></path>
<path d='M2 12h2'></path>
<path d='M20 12h2'></path>
<path d='m6.34 17.66-1.41 1.41'></path>
<path d='m19.07 4.93-1.41 1.41'></path>
</svg>
);
export const SunsetIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M12 10V2' />
<path d='m4.93 10.93 1.41 1.41' />
<path d='M2 18h2' />
<path d='M20 18h2' />
<path d='m19.07 10.93-1.41 1.41' />
<path d='M22 22H2' />
<path d='m16 6-4 4-4-4' />
<path d='M16 18a4 4 0 0 0-8 0' />
</svg>
);
export const MoonIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'></path>
</svg>
);
export const DesktopIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<rect width='20' height='14' x='2' y='3' rx='2' />
<line x1='8' x2='16' y1='21' y2='21' />
<line x1='12' x2='12' y1='17' y2='21' />
</svg>
);
export const SearchIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<circle cx='11' cy='11' r='8'></circle>
<line x1='21' y1='21' x2='16.65' y2='16.65'></line>
</svg>
);
export const CapsuleIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M8.5 16.5a5 5 0 1 0 0-9' />
<path d='M15.5 7.5a5 5 0 1 0 0 9' />
<line x1='8.5' y1='7.5' x2='15.5' y2='7.5' />
<line x1='8.5' y1='16.5' x2='15.5' y2='16.5' />
</svg>
);
export const SyringeIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='m18 2 4 4' />
<path d='m18 6 2.5-2.5' />
<path d='m13.5 7.5 7.5-7.5' />
<path d='M3 21l6-6' />
<path d='m3 11 8 8' />
<path d='m7 7 4 4' />
</svg>
);
export const BottleIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M9 11h6' />
<path d='M12 8v6' />
<path d='M20 10.5A6.5 6.5 0 0 0 7.5 4H7a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-.5A6.5 6.5 0 0 0 20 10.5Z' />
</svg>
);
export const TabletIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<circle cx='12' cy='12' r='10' />
<path d='m14.2 7.8-8.4 8.4' />
</svg>
);
export const SettingsIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 0 2l-.15.08a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1 0-2l.15-.08a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z' />
<circle cx='12' cy='12' r='3' />
</svg>
);
export const UserIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2' />
<circle cx='12' cy='7' r='4' />
</svg>
);
export const CameraIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z' />
<circle cx='12' cy='13' r='3' />
</svg>
);
export const BellIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9' />
<path d='M10.3 21a1.94 1.94 0 0 0 3.4 0' />
</svg>
);
export const ZzzIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M4 12h4l4 8 4-16h4' />
</svg>
);
export const WaterDropIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z' />
</svg>
);
export const CoffeeIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M10 2v2' />
<path d='M14 2v2' />
<path d='M16 8a4 4 0 0 1-4 4H8a4 4 0 0 1 0-8h8' />
<path d='M6 22V8h14v10a4 4 0 0 1-4 4H6' />
</svg>
);
export const BarChartIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<line x1='12' y1='20' x2='12' y2='10' />
<line x1='18' y1='20' x2='18' y2='4' />
<line x1='6' y1='20' x2='6' y2='16' />
</svg>
);
export const medicationIcons: {
[key: string]: React.FC<React.SVGProps<SVGSVGElement>>;
} = {
pill: PillIcon,
tablet: TabletIcon,
capsule: CapsuleIcon,
syringe: SyringeIcon,
bottle: BottleIcon,
};
export const getMedicationIcon = (
iconName?: string
): React.FC<React.SVGProps<SVGSVGElement>> => {
return (iconName && medicationIcons[iconName]) || PillIcon;
};
export const reminderIcons: {
[key: string]: React.FC<React.SVGProps<SVGSVGElement>>;
} = {
bell: BellIcon,
water: WaterDropIcon,
break: CoffeeIcon,
};
export const getReminderIcon = (
iconName?: string
): React.FC<React.SVGProps<SVGSVGElement>> => {
return (iconName && reminderIcons[iconName]) || BellIcon;
};
@@ -0,0 +1,269 @@
import React, { useState, useEffect, useRef } from 'react';
import { Medication, Frequency } from '../../types';
import { medicationIcons } from '../icons/Icons';
interface AddMedicationModalProps {
isOpen: boolean;
onClose: () => void;
onAdd: (medication: Omit<Medication, '_id' | '_rev'>) => Promise<void>;
}
const AddMedicationModal: React.FC<AddMedicationModalProps> = ({
isOpen,
onClose,
onAdd,
}) => {
const [name, setName] = useState('');
const [dosage, setDosage] = useState('');
const [frequency, setFrequency] = useState<Frequency>(Frequency.Daily);
const [hoursBetween, setHoursBetween] = useState(8);
const [startTime, setStartTime] = useState('09:00');
const [notes, setNotes] = useState('');
const [icon, setIcon] = useState('pill');
const [isSaving, setIsSaving] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
const nameInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen) {
setName('');
setDosage('');
setFrequency(Frequency.Daily);
setHoursBetween(8);
setStartTime('09:00');
setNotes('');
setIcon('pill');
setIsSaving(false);
setTimeout(() => nameInputRef.current?.focus(), 100);
}
}, [isOpen]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && !isSaving) onClose();
};
if (isOpen) {
window.addEventListener('keydown', handleKeyDown);
}
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose, isSaving]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name || !dosage || !startTime || isSaving) {
return;
}
setIsSaving(true);
try {
await onAdd({
name,
dosage,
frequency,
hoursBetween:
frequency === Frequency.EveryXHours ? hoursBetween : undefined,
startTime,
notes,
icon,
});
} catch (error) {
console.error('Failed to add medication', error);
alert('There was an error saving your medication. Please try again.');
setIsSaving(false);
}
};
if (!isOpen) return null;
return (
<div
className='fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 z-50 flex justify-center items-center p-4'
role='dialog'
aria-modal='true'
aria-labelledby='add-med-title'
>
<div
className='bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-md'
ref={modalRef}
>
<div className='p-6 border-b border-slate-200 dark:border-slate-700'>
<h3
id='add-med-title'
className='text-xl font-semibold text-slate-800 dark:text-slate-100'
>
Add New Medication
</h3>
</div>
<form onSubmit={handleSubmit}>
<div className='p-6 space-y-4 max-h-[70vh] overflow-y-auto'>
<div>
<label
htmlFor='name'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Medication Name
</label>
<input
type='text'
id='name'
value={name}
onChange={e => setName(e.target.value)}
required
ref={nameInputRef}
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
/>
</div>
<div>
<label
htmlFor='dosage'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Dosage (e.g., "1 tablet", "500mg")
</label>
<input
type='text'
id='dosage'
value={dosage}
onChange={e => setDosage(e.target.value)}
required
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
/>
</div>
<div>
<label
htmlFor='frequency'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Frequency
</label>
<select
id='frequency'
value={frequency}
onChange={e => setFrequency(e.target.value as Frequency)}
className='mt-1 block w-full px-3 py-2 border border-slate-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-slate-700 dark:border-slate-600 dark:text-white'
>
{Object.values(Frequency).map(f => (
<option key={f} value={f}>
{f}
</option>
))}
</select>
</div>
{frequency === Frequency.EveryXHours && (
<div>
<label
htmlFor='hoursBetween'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Hours Between Doses
</label>
<input
type='number'
id='hoursBetween'
value={hoursBetween}
onChange={e => setHoursBetween(parseInt(e.target.value, 10))}
min='1'
max='23'
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
/>
</div>
)}
<div>
<label
htmlFor='startTime'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
First Dose Time
</label>
<input
type='time'
id='startTime'
value={startTime}
onChange={e => setStartTime(e.target.value)}
required
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
/>
</div>
<div>
<label className='block text-sm font-medium text-slate-700 dark:text-slate-300'>
Icon
</label>
<div className='mt-2 flex flex-wrap gap-2'>
{Object.entries(medicationIcons).map(([key, IconComponent]) => (
<button
key={key}
type='button'
onClick={() => setIcon(key)}
className={`p-2 rounded-full transition-colors ${icon === key ? 'bg-indigo-600 text-white ring-2 ring-offset-2 ring-indigo-500 ring-offset-white dark:ring-offset-slate-800' : 'bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600'}`}
aria-label={`Select ${key} icon`}
>
<IconComponent className='w-6 h-6' />
</button>
))}
</div>
</div>
<div>
<label
htmlFor='notes'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Notes (optional)
</label>
<textarea
id='notes'
value={notes}
onChange={e => setNotes(e.target.value)}
rows={3}
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
placeholder='e.g., take with food'
></textarea>
</div>
</div>
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end space-x-3 rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
<button
type='button'
onClick={onClose}
disabled={isSaving}
className='px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-slate-200 dark:border-slate-600 dark:hover:bg-slate-600 dark:focus:ring-offset-slate-800 disabled:opacity-50'
>
Cancel
</button>
<button
type='submit'
disabled={isSaving}
className='px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-slate-800 disabled:opacity-50 disabled:cursor-not-allowed flex items-center'
>
{isSaving && <Spinner />}
{isSaving ? 'Adding...' : 'Add Medication'}
</button>
</div>
</form>
</div>
</div>
);
};
const Spinner = () => (
<svg
className='animate-spin -ml-1 mr-3 h-5 w-5 text-white'
xmlns='http://www.w3.org/2000/svg'
fill='none'
viewBox='0 0 24 24'
>
<circle
className='opacity-25'
cx='12'
cy='12'
r='10'
stroke='currentColor'
strokeWidth='4'
></circle>
<path
className='opacity-75'
fill='currentColor'
d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'
></path>
</svg>
);
export default AddMedicationModal;
+158
View File
@@ -0,0 +1,158 @@
import React from 'react';
import { Medication, Dose, DoseStatus } from '../../types';
import {
ClockIcon,
CheckCircleIcon,
XCircleIcon,
InfoIcon,
getMedicationIcon,
ZzzIcon,
} from '../icons/Icons';
interface DoseCardProps {
dose: Dose & { takenAt?: string };
medication: Medication;
status: DoseStatus;
onToggleDose: (doseId: string) => void;
onSnooze: (doseId: string) => void;
snoozedUntil?: Date;
}
const statusStyles = {
[DoseStatus.UPCOMING]: {
bg: 'bg-white dark:bg-slate-800',
icon: <ClockIcon className='w-6 h-6 text-slate-400 dark:text-slate-500' />,
text: 'text-slate-500 dark:text-slate-400',
button:
'border-indigo-600 text-indigo-600 hover:bg-indigo-600 hover:text-white dark:text-indigo-400 dark:border-indigo-400 dark:hover:bg-indigo-400 dark:hover:text-white',
buttonText: 'Take',
ring: 'hover:ring-indigo-300 dark:hover:ring-indigo-500',
},
[DoseStatus.TAKEN]: {
bg: 'bg-green-50 dark:bg-green-900/20',
icon: (
<CheckCircleIcon className='w-6 h-6 text-green-500 dark:text-green-400' />
),
text: 'text-green-700 dark:text-green-400',
button:
'border-green-500 text-green-500 hover:bg-green-500 hover:text-white dark:text-green-400 dark:border-green-400 dark:hover:bg-green-400 dark:hover:text-slate-900',
buttonText: 'Untake',
ring: '',
},
[DoseStatus.MISSED]: {
bg: 'bg-red-50 dark:bg-red-900/20',
icon: <XCircleIcon className='w-6 h-6 text-red-500 dark:text-red-400' />,
text: 'text-red-700 dark:text-red-400',
button:
'border-red-500 text-red-500 hover:bg-red-500 hover:text-white dark:text-red-400 dark:border-red-400 dark:hover:bg-red-400 dark:hover:text-slate-900',
buttonText: 'Take Now',
ring: '',
},
[DoseStatus.SNOOZED]: {
bg: 'bg-amber-50 dark:bg-amber-900/20',
icon: <ZzzIcon className='w-6 h-6 text-amber-500 dark:text-amber-400' />,
text: 'text-amber-700 dark:text-amber-400',
button:
'border-indigo-600 text-indigo-600 hover:bg-indigo-600 hover:text-white dark:text-indigo-400 dark:border-indigo-400 dark:hover:bg-indigo-400 dark:hover:text-white',
buttonText: 'Take',
ring: '',
},
};
const DoseCard: React.FC<DoseCardProps> = ({
dose,
medication,
status,
onToggleDose,
onSnooze,
snoozedUntil,
}) => {
const styles = statusStyles[status];
const timeString = dose.scheduledTime.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
const takenTimeString = dose.takenAt
? new Date(dose.takenAt).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})
: '';
const snoozedTimeString = snoozedUntil
? snoozedUntil.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})
: '';
const MedicationIcon = getMedicationIcon(medication.icon);
return (
<li
className={`shadow-md rounded-lg p-4 flex flex-col justify-between transition-all duration-300 ${styles.bg} ${styles.ring} ring-4 ring-transparent border border-slate-200 dark:border-slate-700`}
>
<div>
<div className='flex justify-between items-start'>
<div className='flex items-center space-x-3'>
<MedicationIcon className='w-7 h-7 text-indigo-500 dark:text-indigo-400 flex-shrink-0' />
<div>
<h4 className='font-bold text-lg text-slate-800 dark:text-slate-100'>
{medication.name}
</h4>
<p className='text-slate-600 dark:text-slate-300'>
{medication.dosage}
</p>
</div>
</div>
{styles.icon}
</div>
<div
className={`flex items-center space-x-2 mt-4 font-semibold text-lg ${styles.text}`}
>
<ClockIcon className='w-5 h-5' />
<span>{timeString}</span>
</div>
{status === DoseStatus.SNOOZED && (
<p className='text-sm text-amber-600 dark:text-amber-500 mt-1'>
Snoozed until {snoozedTimeString}
</p>
)}
{status === DoseStatus.TAKEN && (
<p className='text-sm text-green-600 dark:text-green-500 mt-1'>
Taken at {takenTimeString}
</p>
)}
{medication.notes && (
<div className='mt-3 p-2 bg-indigo-50 dark:bg-indigo-900/30 rounded-lg flex items-start space-x-2'>
<InfoIcon className='w-4 h-4 text-indigo-500 dark:text-indigo-400 mt-0.5 flex-shrink-0' />
<p className='text-sm text-indigo-800 dark:text-indigo-200'>
{medication.notes}
</p>
</div>
)}
</div>
<div className='mt-4 flex items-center space-x-2'>
{status === DoseStatus.UPCOMING && (
<button
onClick={() => onSnooze(dose.id)}
className='w-1/3 py-2 px-2 rounded-lg font-semibold border-2 transition-colors duration-200 border-slate-300 text-slate-500 hover:bg-slate-100 dark:border-slate-600 dark:text-slate-400 dark:hover:bg-slate-700'
aria-label={`Snooze ${medication.name} for 5 minutes`}
>
<ZzzIcon className='w-5 h-5 mx-auto' />
</button>
)}
<button
onClick={() => onToggleDose(dose.id)}
className={`w-full py-2 px-4 rounded-lg font-semibold border-2 transition-colors duration-200 ${styles.button}`}
aria-label={`${styles.buttonText} ${medication.name} at ${timeString}`}
>
{styles.buttonText}
</button>
</div>
</li>
);
};
export default DoseCard;
@@ -0,0 +1,274 @@
import React, { useState, useEffect, useRef } from 'react';
import { Medication, Frequency } from '../../types';
import { medicationIcons } from '../icons/Icons';
interface EditMedicationModalProps {
isOpen: boolean;
onClose: () => void;
medication: Medication | null;
onUpdate: (medication: Medication) => Promise<void>;
}
const EditMedicationModal: React.FC<EditMedicationModalProps> = ({
isOpen,
onClose,
medication,
onUpdate,
}) => {
const [name, setName] = useState('');
const [dosage, setDosage] = useState('');
const [frequency, setFrequency] = useState<Frequency>(Frequency.Daily);
const [hoursBetween, setHoursBetween] = useState(8);
const [startTime, setStartTime] = useState('09:00');
const [notes, setNotes] = useState('');
const [icon, setIcon] = useState('pill');
const [isSaving, setIsSaving] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
const nameInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (medication) {
setName(medication.name);
setDosage(medication.dosage);
setFrequency(medication.frequency);
setHoursBetween(medication.hoursBetween || 8);
setStartTime(medication.startTime);
setNotes(medication.notes || '');
setIcon(medication.icon || 'pill');
setIsSaving(false);
}
if (isOpen) {
setTimeout(() => nameInputRef.current?.focus(), 100);
}
}, [medication, isOpen]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && !isSaving) onClose();
};
if (isOpen) {
window.addEventListener('keydown', handleKeyDown);
}
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose, isSaving]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!medication || !name || !dosage || !startTime || isSaving) {
return;
}
setIsSaving(true);
try {
await onUpdate({
...medication,
name,
dosage,
frequency,
hoursBetween:
frequency === Frequency.EveryXHours ? hoursBetween : undefined,
startTime,
notes,
icon,
});
} catch (error) {
console.error('Failed to update medication', error);
alert('There was an error updating your medication. Please try again.');
setIsSaving(false);
}
};
if (!isOpen) return null;
return (
<div
className='fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 z-50 flex justify-center items-center p-4'
role='dialog'
aria-modal='true'
aria-labelledby='edit-med-title'
>
<div
className='bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-md'
ref={modalRef}
>
<div className='p-6 border-b border-slate-200 dark:border-slate-700'>
<h3
id='edit-med-title'
className='text-xl font-semibold text-slate-800 dark:text-slate-100'
>
Edit Medication
</h3>
</div>
<form onSubmit={handleSubmit}>
<div className='p-6 space-y-4 max-h-[70vh] overflow-y-auto'>
<div>
<label
htmlFor='edit-name'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Medication Name
</label>
<input
type='text'
id='edit-name'
value={name}
onChange={e => setName(e.target.value)}
required
ref={nameInputRef}
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
/>
</div>
<div>
<label
htmlFor='edit-dosage'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Dosage (e.g., "1 tablet", "500mg")
</label>
<input
type='text'
id='edit-dosage'
value={dosage}
onChange={e => setDosage(e.target.value)}
required
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
/>
</div>
<div>
<label
htmlFor='edit-frequency'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Frequency
</label>
<select
id='edit-frequency'
value={frequency}
onChange={e => setFrequency(e.target.value as Frequency)}
className='mt-1 block w-full px-3 py-2 border border-slate-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-slate-700 dark:border-slate-600 dark:text-white'
>
{Object.values(Frequency).map(f => (
<option key={f} value={f}>
{f}
</option>
))}
</select>
</div>
{frequency === Frequency.EveryXHours && (
<div>
<label
htmlFor='edit-hoursBetween'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Hours Between Doses
</label>
<input
type='number'
id='edit-hoursBetween'
value={hoursBetween}
onChange={e => setHoursBetween(parseInt(e.target.value, 10))}
min='1'
max='23'
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
/>
</div>
)}
<div>
<label
htmlFor='edit-startTime'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
First Dose Time
</label>
<input
type='time'
id='edit-startTime'
value={startTime}
onChange={e => setStartTime(e.target.value)}
required
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
/>
</div>
<div>
<label className='block text-sm font-medium text-slate-700 dark:text-slate-300'>
Icon
</label>
<div className='mt-2 flex flex-wrap gap-2'>
{Object.entries(medicationIcons).map(([key, IconComponent]) => (
<button
key={key}
type='button'
onClick={() => setIcon(key)}
className={`p-2 rounded-full transition-colors ${icon === key ? 'bg-indigo-600 text-white ring-2 ring-offset-2 ring-indigo-500 ring-offset-white dark:ring-offset-slate-800' : 'bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600'}`}
aria-label={`Select ${key} icon`}
>
<IconComponent className='w-6 h-6' />
</button>
))}
</div>
</div>
<div>
<label
htmlFor='edit-notes'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Notes (optional)
</label>
<textarea
id='edit-notes'
value={notes}
onChange={e => setNotes(e.target.value)}
rows={3}
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
placeholder='e.g., take with food'
></textarea>
</div>
</div>
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end space-x-3 rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
<button
type='button'
onClick={onClose}
disabled={isSaving}
className='px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-slate-200 dark:border-slate-600 dark:hover:bg-slate-600 dark:focus:ring-offset-slate-800 disabled:opacity-50'
>
Cancel
</button>
<button
type='submit'
disabled={isSaving}
className='px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-slate-800 disabled:opacity-50 disabled:cursor-not-allowed flex items-center'
>
{isSaving && <Spinner />}
{isSaving ? 'Saving...' : 'Save Changes'}
</button>
</div>
</form>
</div>
</div>
);
};
const Spinner = () => (
<svg
className='animate-spin -ml-1 mr-3 h-5 w-5 text-white'
xmlns='http://www.w3.org/2000/svg'
fill='none'
viewBox='0 0 24 24'
>
<circle
className='opacity-25'
cx='12'
cy='12'
r='10'
stroke='currentColor'
strokeWidth='4'
></circle>
<path
className='opacity-75'
fill='currentColor'
d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'
></path>
</svg>
);
export default EditMedicationModal;
@@ -0,0 +1,177 @@
import React, { useMemo, useEffect, useRef } from 'react';
import { Medication } from '../../types';
import { TrashIcon, EditIcon, getMedicationIcon } from '../icons/Icons';
interface ManageMedicationsModalProps {
isOpen: boolean;
onClose: () => void;
medications: Medication[];
// FIX: Changed onDelete to expect the full medication object to match the parent's handler.
onDelete: (medication: Medication) => void;
onEdit: (medication: Medication) => void;
}
const ManageMedicationsModal: React.FC<ManageMedicationsModalProps> = ({
isOpen,
onClose,
medications,
onDelete,
onEdit,
}) => {
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) {
setTimeout(() => closeButtonRef.current?.focus(), 100);
}
}, [isOpen]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose();
};
if (isOpen) {
window.addEventListener('keydown', handleKeyDown);
}
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
useEffect(() => {
if (!isOpen || !modalRef.current) return;
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[
focusableElements.length - 1
] as HTMLElement;
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
};
document.addEventListener('keydown', handleTabKey);
return () => document.removeEventListener('keydown', handleTabKey);
}, [isOpen]);
const sortedMedications = useMemo(
() => [...medications].sort((a, b) => a.name.localeCompare(b.name)),
[medications]
);
const handleDeleteConfirmation = (medication: Medication) => {
if (window.confirm(`Are you sure you want to delete ${medication.name}?`)) {
// FIX: Pass the whole medication object to the onDelete handler.
onDelete(medication);
}
};
if (!isOpen) return null;
return (
<div
className='fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 z-50 flex justify-center items-center p-4'
role='dialog'
aria-modal='true'
aria-labelledby='manage-med-title'
>
<div
className='bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-lg'
ref={modalRef}
>
<div className='p-6 border-b border-slate-200 dark:border-slate-700 flex justify-between items-center'>
<h3
id='manage-med-title'
className='text-xl font-semibold text-slate-800 dark:text-slate-100'
>
Manage Medications
</h3>
<button
onClick={onClose}
ref={closeButtonRef}
className='text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 text-3xl leading-none'
aria-label='Close'
>
&times;
</button>
</div>
<div className='p-6 max-h-[60vh] overflow-y-auto'>
{sortedMedications.length > 0 ? (
<ul className='space-y-3'>
{sortedMedications.map(med => {
const MedicationIcon = getMedicationIcon(med.icon);
return (
// FIX: The Medication type has `_id`, not `id`. Used for the key.
<li
key={med._id}
className='p-3 bg-slate-50 dark:bg-slate-700/50 rounded-lg flex justify-between items-center'
>
<div className='flex items-center space-x-3'>
<MedicationIcon className='w-6 h-6 text-indigo-500 dark:text-indigo-400 flex-shrink-0' />
<div>
<p className='font-semibold text-slate-800 dark:text-slate-100'>
{med.name}
</p>
<p className='text-sm text-slate-500 dark:text-slate-400'>
{med.dosage} &bull; {med.frequency}
</p>
{med.notes && (
<p className='text-xs text-slate-400 dark:text-slate-500 mt-1 italic'>
Note: "{med.notes}"
</p>
)}
</div>
</div>
<div className='flex items-center space-x-1'>
<button
onClick={() => onEdit(med)}
className='p-2 text-indigo-600 hover:bg-indigo-100 dark:text-indigo-400 dark:hover:bg-slate-700 rounded-full'
aria-label={`Edit ${med.name}`}
>
<EditIcon className='w-5 h-5' />
</button>
<button
onClick={() => handleDeleteConfirmation(med)}
className='p-2 text-red-500 hover:bg-red-100 dark:text-red-400 dark:hover:bg-slate-700 rounded-full'
aria-label={`Delete ${med.name}`}
>
<TrashIcon className='w-5 h-5' />
</button>
</div>
</li>
);
})}
</ul>
) : (
<p className='text-center text-slate-500 dark:text-slate-400 py-8'>
No medications have been added yet.
</p>
)}
</div>
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
<button
type='button'
onClick={onClose}
className='px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-slate-200 dark:border-slate-600 dark:hover:bg-slate-600 dark:focus:ring-offset-slate-800'
>
Close
</button>
</div>
</div>
</div>
);
};
export default ManageMedicationsModal;
+5
View File
@@ -0,0 +1,5 @@
// Medication Components
export { default as AddMedicationModal } from './AddMedicationModal';
export { default as EditMedicationModal } from './EditMedicationModal';
export { default as ManageMedicationsModal } from './ManageMedicationsModal';
export { default as DoseCard } from './DoseCard';
+301
View File
@@ -0,0 +1,301 @@
import React, { useState, useEffect, useRef } from 'react';
import { User, UserSettings } from '../../types';
import { CameraIcon, TrashIcon, UserIcon } from '../icons/Icons';
interface AccountModalProps {
isOpen: boolean;
onClose: () => void;
user: User;
settings: UserSettings;
onUpdateUser: (user: User) => Promise<void>;
onUpdateSettings: (settings: UserSettings) => Promise<void>;
onDeleteAllData: () => Promise<void>;
}
const AccountModal: React.FC<AccountModalProps> = ({
isOpen,
onClose,
user,
settings,
onUpdateUser,
onUpdateSettings,
onDeleteAllData,
}) => {
const [username, setUsername] = useState(user.username);
const [successMessage, setSuccessMessage] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen) {
setUsername(user.username);
setSuccessMessage('');
setTimeout(() => closeButtonRef.current?.focus(), 100);
}
}, [isOpen, user.username]);
const handleUsernameSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (username.trim() && username !== user.username) {
setIsSaving(true);
try {
await onUpdateUser({ ...user, username: username.trim() });
setSuccessMessage('Username updated successfully!');
setTimeout(() => setSuccessMessage(''), 3000);
} catch (error) {
alert('Failed to update username.');
} finally {
setIsSaving(false);
}
}
};
const handleToggleNotifications = (
e: React.ChangeEvent<HTMLInputElement>
) => {
onUpdateSettings({ ...settings, notificationsEnabled: e.target.checked });
};
const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = async () => {
setIsSaving(true);
try {
await onUpdateUser({ ...user, avatar: reader.result as string });
} finally {
setIsSaving(false);
}
};
reader.readAsDataURL(file);
}
};
const handleRemoveAvatar = async () => {
const { avatar, ...userWithoutAvatar } = user;
setIsSaving(true);
try {
await onUpdateUser(userWithoutAvatar);
} finally {
setIsSaving(false);
}
};
const handleDelete = async () => {
setIsDeleting(true);
try {
await onDeleteAllData();
} catch (error) {
alert('Failed to delete data.');
} finally {
setIsDeleting(false);
}
};
if (!isOpen) return null;
return (
<div
className='fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 z-50 flex justify-center items-center p-4'
role='dialog'
aria-modal='true'
aria-labelledby='account-title'
>
<div
className='bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-lg'
ref={modalRef}
>
<div className='p-6 border-b border-slate-200 dark:border-slate-700 flex justify-between items-center'>
<h3
id='account-title'
className='text-xl font-semibold text-slate-800 dark:text-slate-100'
>
Account Settings
</h3>
<button
onClick={onClose}
ref={closeButtonRef}
className='text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 text-3xl leading-none'
aria-label='Close'
>
&times;
</button>
</div>
<div className='p-6 space-y-6 max-h-[60vh] overflow-y-auto'>
<section>
<h4 className='text-lg font-medium text-slate-700 dark:text-slate-200 mb-3'>
Profile
</h4>
<div className='flex items-center space-x-4'>
<div className='relative'>
{user.avatar ? (
<img
src={user.avatar}
alt='User avatar'
className='w-20 h-20 rounded-full object-cover'
/>
) : (
<span className='w-20 h-20 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center'>
<UserIcon className='w-10 h-10 text-slate-500' />
</span>
)}
<button
onClick={() => fileInputRef.current?.click()}
className='absolute bottom-0 right-0 bg-white dark:bg-slate-600 rounded-full p-1.5 shadow-md border border-slate-200 dark:border-slate-500 hover:bg-slate-100 dark:hover:bg-slate-500'
aria-label='Change profile picture'
>
<CameraIcon className='w-4 h-4 text-slate-700 dark:text-slate-200' />
</button>
<input
type='file'
ref={fileInputRef}
onChange={handleAvatarChange}
accept='image/*'
className='hidden'
/>
</div>
<div className='flex flex-col'>
<button
onClick={() => fileInputRef.current?.click()}
className='px-3 py-1.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md shadow-sm hover:bg-slate-50 dark:bg-slate-700 dark:text-slate-200 dark:border-slate-600 dark:hover:bg-slate-600'
>
Change Picture
</button>
{user.avatar && (
<button
onClick={handleRemoveAvatar}
className='mt-2 flex items-center text-sm text-red-600 dark:text-red-500 hover:underline'
>
<TrashIcon className='w-3 h-3 mr-1' /> Remove
</button>
)}
</div>
</div>
<form onSubmit={handleUsernameSubmit} className='space-y-3 mt-4'>
<div>
<label
htmlFor='username'
className='block text-sm font-medium text-slate-600 dark:text-slate-300'
>
Username
</label>
<div className='mt-1 flex rounded-md shadow-sm'>
<input
type='text'
id='username'
value={username}
onChange={e => setUsername(e.target.value)}
className='flex-1 block w-full min-w-0 rounded-none rounded-l-md px-3 py-2 border border-slate-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
/>
<button
type='submit'
className='inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-r-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 dark:focus:ring-offset-slate-800'
disabled={
username === user.username || !username.trim() || isSaving
}
>
{isSaving ? 'Saving...' : 'Save'}
</button>
</div>
{successMessage && (
<p className='text-sm text-green-600 dark:text-green-500 mt-2'>
{successMessage}
</p>
)}
</div>
</form>
</section>
<section>
<h4 className='text-lg font-medium text-slate-700 dark:text-slate-200 mb-3'>
Preferences
</h4>
<div className='flex items-center justify-between p-3 bg-slate-50 dark:bg-slate-700/50 rounded-lg'>
<span className='font-medium text-slate-800 dark:text-slate-100'>
Enable Notifications
</span>
<label
htmlFor='notifications-toggle'
className='relative inline-flex items-center cursor-pointer'
>
<input
type='checkbox'
id='notifications-toggle'
className='sr-only peer'
checked={settings.notificationsEnabled}
onChange={handleToggleNotifications}
/>
<div className="w-11 h-6 bg-slate-200 dark:bg-slate-600 rounded-full peer peer-focus:ring-4 peer-focus:ring-indigo-300 dark:peer-focus:ring-indigo-800 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-slate-600 peer-checked:bg-indigo-600"></div>
</label>
</div>
</section>
<section>
<h4 className='text-lg font-medium text-red-600 dark:text-red-500 mb-3'>
Danger Zone
</h4>
<div className='p-4 border border-red-300 dark:border-red-500/50 rounded-lg'>
<div className='flex items-center justify-between'>
<div>
<p className='font-semibold text-slate-800 dark:text-slate-100'>
Delete All Data
</p>
<p className='text-sm text-slate-500 dark:text-slate-400'>
Permanently delete all your medications and history.
</p>
</div>
<button
onClick={handleDelete}
disabled={isDeleting}
className='px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 dark:focus:ring-offset-slate-800 disabled:opacity-50 disabled:cursor-not-allowed flex items-center'
>
{isDeleting && <Spinner />}
{isDeleting ? 'Deleting...' : 'Delete Data'}
</button>
</div>
</div>
</section>
</div>
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
<button
type='button'
onClick={onClose}
className='px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-slate-200 dark:border-slate-600 dark:hover:bg-slate-600 dark:focus:ring-offset-slate-800'
>
Close
</button>
</div>
</div>
</div>
);
};
const Spinner = () => (
<svg
className='animate-spin -ml-1 mr-3 h-5 w-5 text-white'
xmlns='http://www.w3.org/2000/svg'
fill='none'
viewBox='0 0 24 24'
>
<circle
className='opacity-25'
cx='12'
cy='12'
r='10'
stroke='currentColor'
strokeWidth='4'
></circle>
<path
className='opacity-75'
fill='currentColor'
d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'
></path>
</svg>
);
export default AccountModal;
+193
View File
@@ -0,0 +1,193 @@
import React, { useState, useEffect, useRef } from 'react';
import { CustomReminder } from '../../types';
import { reminderIcons } from '../icons/Icons';
interface AddReminderModalProps {
isOpen: boolean;
onClose: () => void;
onAdd: (reminder: Omit<CustomReminder, '_id' | '_rev'>) => Promise<void>;
}
const AddReminderModal: React.FC<AddReminderModalProps> = ({
isOpen,
onClose,
onAdd,
}) => {
const [title, setTitle] = useState('');
const [icon, setIcon] = useState('bell');
const [frequencyMinutes, setFrequencyMinutes] = useState(60);
const [startTime, setStartTime] = useState('09:00');
const [endTime, setEndTime] = useState('17:00');
const [isSaving, setIsSaving] = useState(false);
const titleInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen) {
setTitle('');
setIcon('bell');
setFrequencyMinutes(60);
setStartTime('09:00');
setEndTime('17:00');
setIsSaving(false);
setTimeout(() => titleInputRef.current?.focus(), 100);
}
}, [isOpen]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!title || isSaving) return;
setIsSaving(true);
try {
await onAdd({
title,
icon,
frequencyMinutes,
startTime,
endTime,
});
} catch (error) {
console.error('Failed to add reminder', error);
alert('There was an error saving your reminder. Please try again.');
} finally {
setIsSaving(false);
}
};
if (!isOpen) return null;
return (
<div
className='fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 z-50 flex justify-center items-center p-4'
role='dialog'
aria-modal='true'
aria-labelledby='add-rem-title'
>
<div className='bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-md'>
<div className='p-6 border-b border-slate-200 dark:border-slate-700'>
<h3
id='add-rem-title'
className='text-xl font-semibold text-slate-800 dark:text-slate-100'
>
Add New Reminder
</h3>
</div>
<form onSubmit={handleSubmit}>
<div className='p-6 space-y-4 max-h-[70vh] overflow-y-auto'>
<div>
<label
htmlFor='rem-title'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Title
</label>
<input
type='text'
id='rem-title'
value={title}
onChange={e => setTitle(e.target.value)}
required
ref={titleInputRef}
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
placeholder='e.g., Drink water'
/>
</div>
<div>
<label className='block text-sm font-medium text-slate-700 dark:text-slate-300'>
Icon
</label>
<div className='mt-2 flex flex-wrap gap-2'>
{Object.entries(reminderIcons).map(([key, IconComponent]) => (
<button
key={key}
type='button'
onClick={() => setIcon(key)}
className={`p-2 rounded-full transition-colors ${icon === key ? 'bg-indigo-600 text-white ring-2 ring-offset-2 ring-indigo-500 ring-offset-white dark:ring-offset-slate-800' : 'bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600'}`}
aria-label={`Select ${key} icon`}
>
<IconComponent className='w-6 h-6' />
</button>
))}
</div>
</div>
<div>
<label
htmlFor='rem-frequency'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Remind me every (minutes)
</label>
<input
type='number'
id='rem-frequency'
value={frequencyMinutes}
onChange={e =>
setFrequencyMinutes(parseInt(e.target.value, 10))
}
min='1'
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
/>
</div>
<div className='grid grid-cols-2 gap-4'>
<div>
<label
htmlFor='rem-startTime'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
From
</label>
<input
type='time'
id='rem-startTime'
value={startTime}
onChange={e => setStartTime(e.target.value)}
required
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
/>
</div>
<div>
<label
htmlFor='rem-endTime'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Until
</label>
<input
type='time'
id='rem-endTime'
value={endTime}
onChange={e => setEndTime(e.target.value)}
required
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
/>
</div>
</div>
</div>
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end space-x-3 rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
<button
type='button'
onClick={onClose}
disabled={isSaving}
className='px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md shadow-sm hover:bg-slate-50 disabled:opacity-50 dark:bg-slate-700 dark:text-slate-200 dark:border-slate-600 dark:hover:bg-slate-600'
>
Cancel
</button>
<button
type='submit'
disabled={isSaving}
className='px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center dark:focus:ring-offset-slate-800'
>
{isSaving ? 'Adding...' : 'Add Reminder'}
</button>
</div>
</form>
</div>
</div>
);
};
export default AddReminderModal;
+195
View File
@@ -0,0 +1,195 @@
import React, { useState, useEffect, useRef } from 'react';
import { CustomReminder } from '../../types';
import { reminderIcons } from '../icons/Icons';
interface EditReminderModalProps {
isOpen: boolean;
onClose: () => void;
reminder: CustomReminder | null;
onUpdate: (reminder: CustomReminder) => Promise<void>;
}
const EditReminderModal: React.FC<EditReminderModalProps> = ({
isOpen,
onClose,
reminder,
onUpdate,
}) => {
const [title, setTitle] = useState('');
const [icon, setIcon] = useState('bell');
const [frequencyMinutes, setFrequencyMinutes] = useState(60);
const [startTime, setStartTime] = useState('09:00');
const [endTime, setEndTime] = useState('17:00');
const [isSaving, setIsSaving] = useState(false);
const titleInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen && reminder) {
setTitle(reminder.title);
setIcon(reminder.icon);
setFrequencyMinutes(reminder.frequencyMinutes);
setStartTime(reminder.startTime);
setEndTime(reminder.endTime);
setIsSaving(false);
setTimeout(() => titleInputRef.current?.focus(), 100);
}
}, [isOpen, reminder]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!title || !reminder || isSaving) return;
setIsSaving(true);
try {
await onUpdate({
...reminder,
title,
icon,
frequencyMinutes,
startTime,
endTime,
});
} catch (error) {
console.error('Failed to update reminder', error);
alert('There was an error updating your reminder. Please try again.');
} finally {
setIsSaving(false);
}
};
if (!isOpen) return null;
return (
<div
className='fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 z-50 flex justify-center items-center p-4'
role='dialog'
aria-modal='true'
aria-labelledby='edit-rem-title'
>
<div className='bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-md'>
<div className='p-6 border-b border-slate-200 dark:border-slate-700'>
<h3
id='edit-rem-title'
className='text-xl font-semibold text-slate-800 dark:text-slate-100'
>
Edit Reminder
</h3>
</div>
<form onSubmit={handleSubmit}>
<div className='p-6 space-y-4 max-h-[70vh] overflow-y-auto'>
<div>
<label
htmlFor='rem-edit-title'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Title
</label>
<input
type='text'
id='rem-edit-title'
value={title}
onChange={e => setTitle(e.target.value)}
required
ref={titleInputRef}
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
/>
</div>
<div>
<label className='block text-sm font-medium text-slate-700 dark:text-slate-300'>
Icon
</label>
<div className='mt-2 flex flex-wrap gap-2'>
{Object.entries(reminderIcons).map(([key, IconComponent]) => (
<button
key={key}
type='button'
onClick={() => setIcon(key)}
className={`p-2 rounded-full transition-colors ${icon === key ? 'bg-indigo-600 text-white ring-2 ring-offset-2 ring-indigo-500 ring-offset-white dark:ring-offset-slate-800' : 'bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600'}`}
aria-label={`Select ${key} icon`}
>
<IconComponent className='w-6 h-6' />
</button>
))}
</div>
</div>
<div>
<label
htmlFor='rem-edit-frequency'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Remind me every (minutes)
</label>
<input
type='number'
id='rem-edit-frequency'
value={frequencyMinutes}
onChange={e =>
setFrequencyMinutes(parseInt(e.target.value, 10))
}
min='1'
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
/>
</div>
<div className='grid grid-cols-2 gap-4'>
<div>
<label
htmlFor='rem-edit-startTime'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
From
</label>
<input
type='time'
id='rem-edit-startTime'
value={startTime}
onChange={e => setStartTime(e.target.value)}
required
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
/>
</div>
<div>
<label
htmlFor='rem-edit-endTime'
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
>
Until
</label>
<input
type='time'
id='rem-edit-endTime'
value={endTime}
onChange={e => setEndTime(e.target.value)}
required
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
/>
</div>
</div>
</div>
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end space-x-3 rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
<button
type='button'
onClick={onClose}
disabled={isSaving}
className='px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md shadow-sm hover:bg-slate-50 disabled:opacity-50 dark:bg-slate-700 dark:text-slate-200 dark:border-slate-600 dark:hover:bg-slate-600'
>
Cancel
</button>
<button
type='submit'
disabled={isSaving}
className='px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center dark:focus:ring-offset-slate-800'
>
{isSaving ? 'Saving...' : 'Save Changes'}
</button>
</div>
</form>
</div>
</div>
);
};
export default EditReminderModal;
+206
View File
@@ -0,0 +1,206 @@
import React, { useEffect, useRef } from 'react';
import { HistoricalDose } from '../../types';
import {
PillIcon,
CheckCircleIcon,
XCircleIcon,
ClockIcon,
} from '../icons/Icons';
interface HistoryModalProps {
isOpen: boolean;
onClose: () => void;
history: { date: string; doses: HistoricalDose[] }[];
}
const getStatusIcon = (status: HistoricalDose['status']) => {
switch (status) {
case 'TAKEN':
return (
<CheckCircleIcon className='w-5 h-5 text-green-500 dark:text-green-400' />
);
case 'MISSED':
return <XCircleIcon className='w-5 h-5 text-red-500 dark:text-red-400' />;
default:
return (
<ClockIcon className='w-5 h-5 text-slate-400 dark:text-slate-500' />
);
}
};
const HistoryModal: React.FC<HistoryModalProps> = ({
isOpen,
onClose,
history,
}) => {
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) {
setTimeout(() => closeButtonRef.current?.focus(), 100);
}
}, [isOpen]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose();
};
if (isOpen) {
window.addEventListener('keydown', handleKeyDown);
}
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
useEffect(() => {
if (!isOpen || !modalRef.current) return;
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[
focusableElements.length - 1
] as HTMLElement;
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
};
document.addEventListener('keydown', handleTabKey);
return () => document.removeEventListener('keydown', handleTabKey);
}, [isOpen]);
if (!isOpen) return null;
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
return new Date(date.getTime() + userTimezoneOffset).toLocaleDateString(
undefined,
{
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}
);
};
return (
<div
className='fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 z-50 flex justify-center items-center p-4'
role='dialog'
aria-modal='true'
aria-labelledby='history-title'
>
<div
className='bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-2xl'
ref={modalRef}
>
<div className='p-6 border-b border-slate-200 dark:border-slate-700 flex justify-between items-center'>
<h3
id='history-title'
className='text-xl font-semibold text-slate-800 dark:text-slate-100'
>
Medication History
</h3>
<button
onClick={onClose}
ref={closeButtonRef}
className='text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 text-3xl leading-none'
aria-label='Close'
>
&times;
</button>
</div>
<div className='p-6 max-h-[60vh] overflow-y-auto space-y-6'>
{history.length > 0 ? (
history.map(({ date, doses }) => (
<section key={date}>
<h4 className='font-bold text-slate-700 dark:text-slate-300 mb-3'>
{formatDate(date)}
</h4>
<ul className='space-y-2'>
{doses.map(dose => (
<li
key={dose.id}
className='p-3 bg-slate-50 dark:bg-slate-700/50 rounded-lg flex justify-between items-center'
>
<div className='flex items-center space-x-3'>
<div className='flex-shrink-0'>
{getStatusIcon(dose.status)}
</div>
<div>
<p className='font-semibold text-slate-800 dark:text-slate-100'>
{dose.medication.name}
</p>
<p className='text-sm text-slate-500 dark:text-slate-400'>
{dose.medication.dosage}
</p>
</div>
</div>
<div className='text-right'>
<p className='font-medium text-slate-700 dark:text-slate-300'>
{dose.scheduledTime.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</p>
{dose.status === 'TAKEN' && dose.takenAt && (
<p className='text-xs text-green-600 dark:text-green-500'>
Taken at{' '}
{new Date(dose.takenAt).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</p>
)}
{dose.status === 'MISSED' && (
<p className='text-xs text-red-600 dark:text-red-500'>
Missed
</p>
)}
</div>
</li>
))}
</ul>
</section>
))
) : (
<div className='text-center py-10'>
<PillIcon className='w-12 h-12 mx-auto text-slate-300 dark:text-slate-600' />
<p className='mt-4 text-slate-500 dark:text-slate-400'>
No medication history found.
</p>
<p className='text-sm text-slate-400 dark:text-slate-500'>
History will appear here once you start tracking doses.
</p>
</div>
)}
</div>
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
<button
type='button'
onClick={onClose}
className='px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-slate-200 dark:border-slate-600 dark:hover:bg-slate-600 dark:focus:ring-offset-slate-800'
>
Close
</button>
</div>
</div>
</div>
);
};
export default HistoryModal;
+127
View File
@@ -0,0 +1,127 @@
import React from 'react';
import { CustomReminder } from '../../types';
import { TrashIcon, EditIcon, PlusIcon, getReminderIcon } from '../icons/Icons';
interface ManageRemindersModalProps {
isOpen: boolean;
onClose: () => void;
reminders: CustomReminder[];
onAdd: () => void;
onDelete: (reminder: CustomReminder) => void;
onEdit: (reminder: CustomReminder) => void;
}
const ManageRemindersModal: React.FC<ManageRemindersModalProps> = ({
isOpen,
onClose,
reminders,
onAdd,
onDelete,
onEdit,
}) => {
const handleDeleteConfirmation = (reminder: CustomReminder) => {
if (
window.confirm(
`Are you sure you want to delete the reminder "${reminder.title}"?`
)
) {
onDelete(reminder);
}
};
if (!isOpen) return null;
return (
<div
className='fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 z-50 flex justify-center items-center p-4'
role='dialog'
aria-modal='true'
aria-labelledby='manage-rem-title'
>
<div className='bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-lg'>
<div className='p-6 border-b border-slate-200 dark:border-slate-700 flex justify-between items-center'>
<h3
id='manage-rem-title'
className='text-xl font-semibold text-slate-800 dark:text-slate-100'
>
Manage Custom Reminders
</h3>
<button
onClick={onClose}
className='text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 text-3xl leading-none'
aria-label='Close'
>
&times;
</button>
</div>
<div className='p-6 max-h-[60vh] overflow-y-auto'>
{reminders.length > 0 ? (
<ul className='space-y-3'>
{reminders.map(rem => {
const ReminderIcon = getReminderIcon(rem.icon);
return (
<li
key={rem._id}
className='p-3 bg-slate-50 dark:bg-slate-700/50 rounded-lg flex justify-between items-center'
>
<div className='flex items-center space-x-3'>
<ReminderIcon className='w-6 h-6 text-sky-500 dark:text-sky-400 flex-shrink-0' />
<div>
<p className='font-semibold text-slate-800 dark:text-slate-100'>
{rem.title}
</p>
<p className='text-sm text-slate-500 dark:text-slate-400'>
Every {rem.frequencyMinutes} mins from {rem.startTime}{' '}
to {rem.endTime}
</p>
</div>
</div>
<div className='flex items-center space-x-1'>
<button
onClick={() => onEdit(rem)}
className='p-2 text-indigo-600 hover:bg-indigo-100 dark:text-indigo-400 dark:hover:bg-slate-700 rounded-full'
aria-label={`Edit ${rem.title}`}
>
<EditIcon className='w-5 h-5' />
</button>
<button
onClick={() => handleDeleteConfirmation(rem)}
className='p-2 text-red-500 hover:bg-red-100 dark:text-red-400 dark:hover:bg-slate-700 rounded-full'
aria-label={`Delete ${rem.title}`}
>
<TrashIcon className='w-5 h-5' />
</button>
</div>
</li>
);
})}
</ul>
) : (
<p className='text-center text-slate-500 dark:text-slate-400 py-8'>
No custom reminders have been added yet.
</p>
)}
</div>
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-between items-center rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
<button
type='button'
onClick={onAdd}
className='inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-slate-900'
>
<PlusIcon className='-ml-1 mr-2 h-5 w-5' />
Add New Reminder
</button>
<button
type='button'
onClick={onClose}
className='px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-slate-200 dark:border-slate-600 dark:hover:bg-slate-600 dark:focus:ring-offset-slate-800'
>
Close
</button>
</div>
</div>
</div>
);
};
export default ManageRemindersModal;
+81
View File
@@ -0,0 +1,81 @@
import React, { useState } from 'react';
import { PillIcon, PlusIcon, CheckCircleIcon } from './icons/Icons';
interface OnboardingModalProps {
isOpen: boolean;
onComplete: () => void;
}
const onboardingSteps = [
{
icon: PillIcon,
title: 'Welcome to Medication Reminder!',
description:
'This quick tour will show you how to get the most out of the app.',
},
{
icon: PlusIcon,
title: 'Add Your Medications',
description:
"Start by clicking the 'Add Medication' button. You can set the name, dosage, frequency, and a custom icon.",
},
{
icon: CheckCircleIcon,
title: 'Track Your Doses',
description:
"Your daily schedule will appear on the main screen. Simply tap 'Take' to record a dose and stay on track with your health.",
},
];
const OnboardingModal: React.FC<OnboardingModalProps> = ({
isOpen,
onComplete,
}) => {
const [step, setStep] = useState(0);
const currentStep = onboardingSteps[step];
const isLastStep = step === onboardingSteps.length - 1;
const handleNext = () => {
if (isLastStep) {
onComplete();
} else {
setStep(s => s + 1);
}
};
if (!isOpen) return null;
return (
<div className='fixed inset-0 bg-black bg-opacity-60 dark:bg-opacity-80 z-50 flex justify-center items-center p-4'>
<div className='bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-sm text-center p-8 m-4'>
<div className='mx-auto bg-indigo-100 dark:bg-indigo-900/50 rounded-full h-16 w-16 flex items-center justify-center mb-6'>
<currentStep.icon className='w-8 h-8 text-indigo-600 dark:text-indigo-400' />
</div>
<h2 className='text-2xl font-bold text-slate-800 dark:text-slate-100 mb-2'>
{currentStep.title}
</h2>
<p className='text-slate-600 dark:text-slate-300 mb-8'>
{currentStep.description}
</p>
<div className='flex justify-center items-center mb-8 space-x-2'>
{onboardingSteps.map((_, index) => (
<div
key={index}
className={`w-2.5 h-2.5 rounded-full transition-colors ${step === index ? 'bg-indigo-600' : 'bg-slate-300 dark:bg-slate-600'}`}
/>
))}
</div>
<button
onClick={handleNext}
className='w-full px-4 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-slate-800'
>
{isLastStep ? 'Get Started' : 'Next'}
</button>
</div>
</div>
);
};
export default OnboardingModal;
+244
View File
@@ -0,0 +1,244 @@
import React, { useEffect, useRef } from 'react';
import { DailyStat, MedicationStat } from '../../types';
import BarChart from '../ui/BarChart';
import { BarChartIcon, getMedicationIcon } from '../icons/Icons';
interface StatsModalProps {
isOpen: boolean;
onClose: () => void;
dailyStats: DailyStat[];
medicationStats: MedicationStat[];
}
const formatLastTaken = (isoString?: string) => {
if (!isoString)
return <span className='text-slate-400 dark:text-slate-500'>N/A</span>;
const date = new Date(isoString);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate() - 1
);
const timeString = date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
const datePart = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
);
if (datePart.getTime() === today.getTime()) {
return `Today at ${timeString}`;
}
if (datePart.getTime() === yesterday.getTime()) {
return `Yesterday at ${timeString}`;
}
return date.toLocaleDateString();
};
const StatsModal: React.FC<StatsModalProps> = ({
isOpen,
onClose,
dailyStats,
medicationStats,
}) => {
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) {
setTimeout(() => closeButtonRef.current?.focus(), 100);
}
}, [isOpen]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose();
};
if (isOpen) {
window.addEventListener('keydown', handleKeyDown);
}
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
const hasData = medicationStats.length > 0;
return (
<div
className='fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 z-50 flex justify-center items-center p-4'
role='dialog'
aria-modal='true'
aria-labelledby='stats-title'
>
<div
className='bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-3xl'
ref={modalRef}
>
<div className='p-6 border-b border-slate-200 dark:border-slate-700 flex justify-between items-center'>
<h3
id='stats-title'
className='text-xl font-semibold text-slate-800 dark:text-slate-100'
>
Medication Statistics
</h3>
<button
onClick={onClose}
ref={closeButtonRef}
className='text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 text-3xl leading-none'
aria-label='Close'
>
&times;
</button>
</div>
<div className='p-6 max-h-[70vh] overflow-y-auto space-y-8'>
{hasData ? (
<>
<section>
<h4 className='text-lg font-semibold text-slate-700 dark:text-slate-200 mb-4'>
Weekly Adherence
</h4>
<div className='p-4 bg-slate-50 dark:bg-slate-700/50 rounded-lg'>
<BarChart data={dailyStats} />
</div>
</section>
<section>
<h4 className='text-lg font-semibold text-slate-700 dark:text-slate-200 mb-4'>
Medication Breakdown
</h4>
<div className='overflow-x-auto'>
<table className='min-w-full divide-y divide-slate-200 dark:divide-slate-700'>
<thead className='bg-slate-50 dark:bg-slate-700/50'>
<tr>
<th
scope='col'
className='px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider'
>
Medication
</th>
<th
scope='col'
className='px-4 py-3 text-center text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider'
>
Taken
</th>
<th
scope='col'
className='px-4 py-3 text-center text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider'
>
Missed
</th>
<th
scope='col'
className='px-4 py-3 text-center text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider'
>
Upcoming
</th>
<th
scope='col'
className='px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider'
>
Last Taken
</th>
<th
scope='col'
className='px-4 py-3 text-right text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider'
>
Adherence
</th>
</tr>
</thead>
<tbody className='bg-white dark:bg-slate-800 divide-y divide-slate-200 dark:divide-slate-700'>
{medicationStats.map(
({
medication,
taken,
missed,
upcoming,
adherence,
lastTakenAt,
}) => {
const MedicationIcon = getMedicationIcon(
medication.icon
);
const adherenceColor =
adherence >= 90
? 'text-green-600 dark:text-green-400'
: adherence >= 70
? 'text-amber-600 dark:text-amber-400'
: 'text-red-600 dark:text-red-400';
return (
<tr key={medication._id}>
<td className='px-4 py-4 whitespace-nowrap'>
<div className='flex items-center space-x-3'>
<MedicationIcon className='w-6 h-6 text-indigo-500 dark:text-indigo-400 flex-shrink-0' />
<div>
<div className='text-sm font-semibold text-slate-900 dark:text-slate-100'>
{medication.name}
</div>
<div className='text-xs text-slate-500 dark:text-slate-400'>
{medication.dosage}
</div>
</div>
</div>
</td>
<td className='px-4 py-4 whitespace-nowrap text-center text-sm text-slate-500 dark:text-slate-400'>
{taken}
</td>
<td className='px-4 py-4 whitespace-nowrap text-center text-sm text-slate-500 dark:text-slate-400'>
{missed}
</td>
<td className='px-4 py-4 whitespace-nowrap text-center text-sm text-slate-500 dark:text-slate-400'>
{upcoming}
</td>
<td className='px-4 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400'>
{formatLastTaken(lastTakenAt)}
</td>
<td
className={`px-4 py-4 whitespace-nowrap text-right text-sm font-bold ${adherenceColor}`}
>
{adherence}%
</td>
</tr>
);
}
)}
</tbody>
</table>
</div>
</section>
</>
) : (
<div className='text-center py-10'>
<BarChartIcon className='w-12 h-12 mx-auto text-slate-300 dark:text-slate-600' />
<p className='mt-4 text-slate-500 dark:text-slate-400'>
Not enough data to display stats.
</p>
<p className='text-sm text-slate-400 dark:text-slate-500'>
Statistics will appear here once you start tracking your doses.
</p>
</div>
)}
</div>
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
<button
type='button'
onClick={onClose}
className='px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-slate-200 dark:border-slate-600 dark:hover:bg-slate-600 dark:focus:ring-offset-slate-800'
>
Close
</button>
</div>
</div>
</div>
);
};
export default StatsModal;
+8
View File
@@ -0,0 +1,8 @@
// Modal Components
export { default as AccountModal } from './AccountModal';
export { default as AddReminderModal } from './AddReminderModal';
export { default as EditReminderModal } from './EditReminderModal';
export { default as HistoryModal } from './HistoryModal';
export { default as ManageRemindersModal } from './ManageRemindersModal';
export { default as OnboardingModal } from './OnboardingModal';
export { default as StatsModal } from './StatsModal';
+112
View File
@@ -0,0 +1,112 @@
import React from 'react';
import { DailyStat } from '../../types';
interface BarChartProps {
data: DailyStat[];
}
const BarChart: React.FC<BarChartProps> = ({ data }) => {
const chartHeight = 150;
const barWidth = 30;
const barMargin = 15;
const chartWidth = data.length * (barWidth + barMargin);
const getDayLabel = (dateString: string) => {
const date = new Date(dateString);
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
const adjustedDate = new Date(date.getTime() + userTimezoneOffset);
return adjustedDate.toLocaleDateString('en-US', { weekday: 'short' });
};
return (
<div className='w-full overflow-x-auto pb-4'>
<svg
viewBox={`0 0 ${chartWidth} ${chartHeight + 40}`}
width='100%'
height='190'
aria-labelledby='chart-title'
role='img'
>
<title id='chart-title'>Weekly Medication Adherence Chart</title>
{/* Y-Axis Labels */}
<g className='text-xs fill-current text-slate-500 dark:text-slate-400'>
<text x='-5' y='15' textAnchor='end'>
100%
</text>
<text x='-5' y={chartHeight / 2 + 5} textAnchor='end'>
50%
</text>
<text x='-5' y={chartHeight + 5} textAnchor='end'>
0%
</text>
</g>
{/* Y-Axis Grid Lines */}
<line
x1='0'
y1='10'
x2={chartWidth}
y2='10'
className='stroke-current text-slate-200 dark:text-slate-600'
strokeDasharray='2,2'
/>
<line
x1='0'
y1={chartHeight / 2 + 2.5}
x2={chartWidth}
y2={chartHeight / 2 + 2.5}
className='stroke-current text-slate-200 dark:text-slate-600'
strokeDasharray='2,2'
/>
<line
x1='0'
y1={chartHeight}
x2={chartWidth}
y2={chartHeight}
className='stroke-current text-slate-300 dark:text-slate-500'
/>
{data.map((item, index) => {
const x = index * (barWidth + barMargin);
const barHeight = (item.adherence / 100) * (chartHeight - 10);
const y = chartHeight - barHeight;
const barColorClass =
item.adherence >= 90
? 'fill-current text-green-500 dark:text-green-400'
: item.adherence >= 70
? 'fill-current text-amber-500 dark:text-amber-400'
: 'fill-current text-red-500 dark:text-red-400';
return (
<g key={item.date}>
<rect
x={x}
y={y}
width={barWidth}
height={barHeight}
rx='4'
className={barColorClass}
>
<title>
{getDayLabel(item.date)}: {item.adherence}% adherence
</title>
</rect>
<text
x={x + barWidth / 2}
y={chartHeight + 20}
textAnchor='middle'
className='text-xs fill-current text-slate-600 dark:text-slate-300 font-medium'
>
{getDayLabel(item.date)}
</text>
</g>
);
})}
</svg>
</div>
);
};
export default BarChart;
+38
View File
@@ -0,0 +1,38 @@
import React from 'react';
import { ReminderInstance } from '../../types';
import { ClockIcon, getReminderIcon } from '../icons/Icons';
interface ReminderCardProps {
reminder: ReminderInstance;
}
const ReminderCard: React.FC<ReminderCardProps> = ({ reminder }) => {
const timeString = reminder.scheduledTime.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
const ReminderIcon = getReminderIcon(reminder.icon);
return (
<li className='shadow-md rounded-lg p-4 flex flex-col justify-between transition-all duration-300 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700'>
<div>
<div className='flex justify-between items-start'>
<div className='flex items-center space-x-3'>
<ReminderIcon className='w-7 h-7 text-sky-500 dark:text-sky-400 flex-shrink-0' />
<div>
<h4 className='font-bold text-lg text-slate-800 dark:text-slate-100'>
{reminder.title}
</h4>
</div>
</div>
</div>
<div className='flex items-center space-x-2 mt-4 font-semibold text-lg text-slate-500 dark:text-slate-400'>
<ClockIcon className='w-5 h-5' />
<span>{timeString}</span>
</div>
</div>
</li>
);
};
export default ReminderCard;
+74
View File
@@ -0,0 +1,74 @@
import React, { useState, useRef, useEffect } from 'react';
import { useTheme } from '../../hooks/useTheme';
import { SunIcon, MoonIcon, DesktopIcon } from '../icons/Icons';
type Theme = 'light' | 'dark' | 'system';
const themeOptions: {
value: Theme;
label: string;
icon: React.FC<React.ComponentProps<'svg'>>;
}[] = [
{ value: 'light', label: 'Light', icon: SunIcon },
{ value: 'dark', label: 'Dark', icon: MoonIcon },
{ value: 'system', label: 'System', icon: DesktopIcon },
];
const ThemeSwitcher: React.FC = () => {
const { theme, setTheme } = useTheme();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const currentTheme =
themeOptions.find(t => t.value === theme) || themeOptions[2];
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className='relative' ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className='flex items-center justify-center w-10 h-10 rounded-lg bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors'
aria-label={`Current theme: ${currentTheme.label}. Change theme.`}
>
<SunIcon className='w-5 h-5 text-slate-700 dark:hidden' />
<MoonIcon className='w-5 h-5 text-slate-200 hidden dark:block' />
</button>
{isOpen && (
<div className='absolute right-0 mt-2 w-36 bg-white dark:bg-slate-800 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 py-1 z-30 border dark:border-slate-700'>
{themeOptions.map(option => (
<button
key={option.value}
onClick={() => {
setTheme(option.value);
setIsOpen(false);
}}
className={`w-full text-left flex items-center space-x-2 px-3 py-2 text-sm ${
theme === option.value
? 'bg-indigo-600 text-white'
: 'text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
<option.icon className='w-4 h-4' />
<span>{option.label}</span>
</button>
))}
</div>
)}
</div>
);
};
export default ThemeSwitcher;
+4
View File
@@ -0,0 +1,4 @@
// UI Components
export { default as BarChart } from './BarChart';
export { default as ReminderCard } from './ReminderCard';
export { default as ThemeSwitcher } from './ThemeSwitcher';
+219
View File
@@ -0,0 +1,219 @@
import React, {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from 'react';
import { User } from '../types';
import { dbService } from '../services/couchdb.factory';
import { authService } from '../services/auth/auth.service';
const SESSION_KEY = 'medication_app_session';
interface UserContextType {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<boolean>;
register: (
email: string,
password: string,
username?: string
) => Promise<boolean>;
loginWithOAuth: (
provider: 'google' | 'github',
userData: any
) => Promise<boolean>;
changePassword: (
currentPassword: string,
newPassword: string
) => Promise<boolean>;
logout: () => void;
updateUser: (
updatedUser: Omit<User, '_rev'> & { _rev: string }
) => Promise<void>;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export const UserProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
try {
const sessionUser = localStorage.getItem(SESSION_KEY);
if (sessionUser) {
setUser(JSON.parse(sessionUser));
}
} catch {
// silent fail
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (user) {
localStorage.setItem(SESSION_KEY, JSON.stringify(user));
} else {
localStorage.removeItem(SESSION_KEY);
}
}, [user]);
const login = async (email: string, password: string): Promise<boolean> => {
try {
// Use auth service for password-based login
const result = await authService.login({ email, password });
console.log('Login result received:', result);
console.log('User from login:', result.user);
console.log('User _id:', result.user._id);
// Update last login time
const updatedUser = { ...result.user, lastLoginAt: new Date() };
await dbService.updateUser(updatedUser);
console.log('Updated user with last login:', updatedUser);
// Store access token for subsequent API calls.
localStorage.setItem('access_token', result.accessToken);
// Set the user from the login result
setUser(updatedUser);
console.log('User set in context');
return true;
} catch (error) {
console.error('Login error:', error);
return false;
}
};
const register = async (
email: string,
password: string,
username?: string
): Promise<boolean> => {
try {
const result = await authService.register(email, password, username);
// Don't auto-login after registration, require email verification
return true;
} catch (error) {
console.error('Registration error:', error);
return false;
}
};
const loginWithOAuth = async (
provider: 'google' | 'github',
userData: any
): Promise<boolean> => {
try {
const result = await authService.loginWithOAuth(provider, userData);
console.log('OAuth login result received:', result);
console.log('OAuth user:', result.user);
console.log('OAuth user _id:', result.user._id);
// Update last login time
const updatedUser = { ...result.user, lastLoginAt: new Date() };
await dbService.updateUser(updatedUser);
console.log('Updated OAuth user with last login:', updatedUser);
localStorage.setItem('access_token', result.accessToken);
setUser(updatedUser);
console.log('OAuth user set in context');
return true;
} catch (error) {
console.error('OAuth login error:', error);
return false;
}
};
const changePassword = async (
currentPassword: string,
newPassword: string
): Promise<boolean> => {
try {
if (!user) {
throw new Error('No user logged in');
}
await authService.changePassword(user._id, currentPassword, newPassword);
return true;
} catch (error) {
console.error('Password change error:', error);
return false;
}
};
const logout = () => {
setUser(null);
};
const updateUser = async (updatedUser: User) => {
try {
const savedUser = await dbService.updateUser(updatedUser);
setUser(savedUser);
} catch (error) {
console.error('Failed to update user', error);
// Optionally revert state or show error
}
};
if (isLoading) {
return (
<div className='min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900'>
<PillIcon className='w-12 h-12 text-indigo-500 animate-spin' />
</div>
);
}
return (
<UserContext.Provider
value={{
user,
isLoading: false,
login,
register,
loginWithOAuth,
changePassword,
logout,
updateUser,
}}
>
{children}
</UserContext.Provider>
);
};
export const useUser = (): UserContextType => {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
};
// Dummy icon for loading screen
const PillIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='m10.5 20.5 10-10a4.95 4.95 0 1 0-7-7l-10 10a4.95 4.95 0 1 0 7 7Z' />
<path d='m8.5 8.5 7 7' />
</svg>
);
+78
View File
@@ -0,0 +1,78 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build output
dist
build
# Environment files (security)
.env
.env.*
!.env.example
# Development files
.vscode
.idea
*.swp
*.swo
*~
# Version control
.git
.gitignore
.gitattributes
# Documentation
README.md
README_*.md
CHANGELOG.md
CONTRIBUTING.md
LICENSE
docs/
# Docker files (avoid recursion)
Dockerfile
docker-compose.yaml
.dockerignore
# Scripts and testing (not needed in container)
scripts/
tests/
coverage/
**/__tests__
**/*.test.*
**/*.spec.*
# Logs
logs
*.log
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
tmp/
temp/
.tmp
# CouchDB data
couchdb-data/
# Scripts (not needed in container)
setup.sh
deploy.sh
deploy-k8s.sh
validate-env.sh
validate-deployment.sh
# Kubernetes manifests
k8s/
+81
View File
@@ -0,0 +1,81 @@
# check=skip=SecretsUsedInArgOrEnv
# Build stage
FROM oven/bun:alpine AS builder
# Install system dependencies for native modules
RUN apk add --no-cache python3 make g++
# Create non-root user for security
RUN addgroup -g 1001 -S nodeuser && adduser -S nodeuser -u 1001 -G nodeuser
# Create and set permissions for the working directory
RUN mkdir -p /app && chown -R nodeuser:nodeuser /app
WORKDIR /app
USER nodeuser
# Copy package files first for better Docker layer caching
COPY --chown=nodeuser:nodeuser package.json ./
COPY --chown=nodeuser:nodeuser bun.lock ./
# Install dependencies
RUN bun install --frozen-lockfile
# Copy source code
COPY --chown=nodeuser:nodeuser . ./
# Build arguments for environment configuration
# CouchDB Configuration
ARG VITE_COUCHDB_URL=http://localhost:5984
ARG VITE_COUCHDB_USER=admin
ARG VITE_COUCHDB_PASSWORD=change-this-secure-password
# Application Configuration
ARG APP_BASE_URL=http://localhost:5173
# OAuth Configuration (Optional)
ARG VITE_GOOGLE_CLIENT_ID=""
ARG VITE_GITHUB_CLIENT_ID=""
# Build Environment
ARG NODE_ENV=production
# Set environment variables for build process
# These are embedded into the static build at compile time
ENV VITE_COUCHDB_URL=$VITE_COUCHDB_URL
ENV VITE_COUCHDB_USER=$VITE_COUCHDB_USER
ENV VITE_COUCHDB_PASSWORD=$VITE_COUCHDB_PASSWORD
ENV APP_BASE_URL=$APP_BASE_URL
ENV VITE_GOOGLE_CLIENT_ID=$VITE_GOOGLE_CLIENT_ID
ENV VITE_GITHUB_CLIENT_ID=$VITE_GITHUB_CLIENT_ID
ENV NODE_ENV=$NODE_ENV
# Build the application
RUN bun run build
# Production stage - serve with nginx
FROM nginx:alpine
# Install curl for health checks
RUN apk add --no-cache curl
# Copy built files from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY --from=builder /app/docker/nginx.conf /etc/nginx/conf.d/default.conf
# Set proper permissions for nginx
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
chown -R nginx:nginx /etc/nginx/conf.d
# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1
# Expose port 80
EXPOSE 80
# Start nginx (runs as nginx user by default in alpine)
CMD ["nginx", "-g", "daemon off;"]
+76
View File
@@ -0,0 +1,76 @@
# 🐳 Docker Configuration
This directory contains all Docker and containerization-related files for RxMinder.
## Files
- **`Dockerfile`** - Multi-stage Docker build configuration with buildx support
- **`docker-compose.yaml`** - Service orchestration with multi-platform support
- **`docker-bake.hcl`** - Advanced buildx configuration for multi-platform builds
- **`nginx.conf`** - Production web server configuration
- **`.dockerignore`** - Files and directories to exclude from Docker build context
## Docker Buildx Support
This project now supports Docker Buildx for multi-platform builds (AMD64 and ARM64).
### Quick Start with Buildx
```bash
# Setup buildx builder (run once)
../scripts/buildx-helper.sh setup
# Build for local platform only (faster for development)
../scripts/buildx-helper.sh build-local
# Build for multiple platforms
../scripts/buildx-helper.sh build-multi
# Build and push to registry
../scripts/buildx-helper.sh push docker.io/username latest
# Build using Docker Bake (advanced)
../scripts/buildx-helper.sh bake
```
### Manual Buildx Commands
```bash
# Create and use buildx builder
docker buildx create --name rxminder-builder --driver docker-container --bootstrap --use
# Build for multiple platforms
docker buildx build --platform linux/amd64,linux/arm64 -t rxminder:latest --load .
# Build with bake file
docker buildx bake -f docker-bake.hcl
```
## Traditional Usage
From the project root directory:
```bash
# Build and start services
docker compose -f docker/docker-compose.yaml up -d
# View logs
docker compose -f docker/docker-compose.yaml logs
# Stop services
docker compose -f docker/docker-compose.yaml down
```
## Build Process
The Dockerfile uses a multi-stage build:
1. **Builder stage**: Installs dependencies and builds the React app
2. **Production stage**: Serves the built app with nginx
## Services
- **frontend**: React application served by nginx
- **couchdb**: Database for medication and user data
Both services include health checks and proper security configurations.
+101
View File
@@ -0,0 +1,101 @@
# Docker Bake file for advanced multi-platform builds
# Usage: docker buildx bake -f docker-bake.hcl
variable "TAG" {
default = "latest"
}
variable "REGISTRY" {
default = ""
}
variable "VITE_COUCHDB_URL" {
default = "http://localhost:5984"
}
variable "VITE_COUCHDB_USER" {
default = "admin"
}
variable "VITE_COUCHDB_PASSWORD" {
default = "change-this-secure-password"
}
variable "APP_BASE_URL" {
default = "http://localhost:8080"
}
variable "VITE_GOOGLE_CLIENT_ID" {
default = ""
}
variable "VITE_GITHUB_CLIENT_ID" {
default = ""
}
group "default" {
targets = ["app"]
}
target "app" {
dockerfile = "Dockerfile"
context = "."
platforms = [
"linux/amd64",
"linux/arm64"
]
tags = [
"${REGISTRY}rxminder:${TAG}",
"${REGISTRY}rxminder:latest"
]
args = {
# CouchDB Configuration
VITE_COUCHDB_URL = "${VITE_COUCHDB_URL}"
VITE_COUCHDB_USER = "${VITE_COUCHDB_USER}"
VITE_COUCHDB_PASSWORD = "${VITE_COUCHDB_PASSWORD}"
# Application Configuration
APP_BASE_URL = "${APP_BASE_URL}"
# OAuth Configuration (Optional)
VITE_GOOGLE_CLIENT_ID = "${VITE_GOOGLE_CLIENT_ID}"
VITE_GITHUB_CLIENT_ID = "${VITE_GITHUB_CLIENT_ID}"
# Build environment
NODE_ENV = "production"
}
# Advanced buildx features
cache-from = [
"type=gha",
"type=registry,ref=${REGISTRY}rxminder:buildcache"
]
cache-to = [
"type=gha,mode=max",
"type=registry,ref=${REGISTRY}rxminder:buildcache,mode=max"
]
# Attestations for supply chain security
attest = [
"type=provenance,mode=max",
"type=sbom"
]
}
# Development target for faster local builds
target "dev" {
inherits = ["app"]
platforms = ["linux/amd64"]
tags = ["rxminder:dev"]
cache-from = ["type=gha"]
cache-to = ["type=gha,mode=max"]
}
# Production target with registry push
target "prod" {
inherits = ["app"]
output = ["type=registry"]
}
+65
View File
@@ -0,0 +1,65 @@
services:
# Frontend service
frontend:
build:
context: .
args:
# CouchDB Configuration
- VITE_COUCHDB_URL=${VITE_COUCHDB_URL:-http://couchdb:5984}
- VITE_COUCHDB_USER=${VITE_COUCHDB_USER:-admin}
- VITE_COUCHDB_PASSWORD=${VITE_COUCHDB_PASSWORD:-change-this-secure-password}
# Application Configuration
- APP_BASE_URL=${APP_BASE_URL:-http://localhost:8080}
# OAuth Configuration (Optional)
- VITE_GOOGLE_CLIENT_ID=${VITE_GOOGLE_CLIENT_ID:-}
- VITE_GITHUB_CLIENT_ID=${VITE_GITHUB_CLIENT_ID:-}
# Build Environment
- NODE_ENV=${NODE_ENV:-production}
# Enable buildx for multi-platform builds
platforms:
- linux/amd64
- linux/arm64
ports:
- '8080:80'
depends_on:
couchdb:
condition: service_healthy
restart: unless-stopped
# Health check for the frontend container
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost/']
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
labels:
- 'monitoring=true'
- 'service=frontend'
# CouchDB service
couchdb:
image: couchdb:3.3.2
volumes:
- ./couchdb-data:/opt/couchdb/data
environment:
- COUCHDB_USER=${COUCHDB_USER:-admin}
- COUCHDB_PASSWORD=${COUCHDB_PASSWORD:-change-this-secure-password}
ports:
- '5984:5984'
restart: unless-stopped
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:5984/_up']
interval: 30s
timeout: 10s
retries: 3
labels:
- 'monitoring=true'
- 'service=couchdb'
# Redis service (commented out as per requirements)
# redis:
# image: redis:alpine
# restart: unless-stopped
# labels:
# - "monitoring=true"
# - "service=redis"
+36
View File
@@ -0,0 +1,36 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Handle client-side routing
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
+85
View File
@@ -0,0 +1,85 @@
# Documentation Updates Summary
## Files Updated
### 📚 Main Documentation
- **`docs/CODE_QUALITY.md`** - Comprehensive code quality and pre-commit hooks documentation
- **`README.md`** - Updated development and code quality sections
- **`SETUP_COMPLETE.md`** - Updated quick reference guide
## Key Updates Made
### 1. Enhanced Code Quality Documentation (`docs/CODE_QUALITY.md`)
- ✅ Added detailed pre-commit hook descriptions
- ✅ Updated Python virtual environment paths for commands
- ✅ Added comprehensive troubleshooting section
- ✅ Enhanced IDE integration instructions with VS Code settings
- ✅ Added security tools documentation (detect-secrets)
- ✅ Updated manual command examples with correct paths
### 2. Main README Updates (`README.md`)
- ✅ Updated "Development Tools" section to include new formatting tools
- ✅ Enhanced "Code Quality" section with comprehensive commands
- ✅ Added reference to detailed code quality documentation
- ✅ Added Code Quality Guide to project documentation index
- ✅ Updated commands to reflect current npm scripts
### 3. Quick Reference Guide (`SETUP_COMPLETE.md`)
- ✅ Updated tool descriptions to be more comprehensive
- ✅ Added Python virtual environment information
- ✅ Updated command examples with correct paths
- ✅ Enhanced configuration file descriptions
## Current Setup Summary
### 🔧 Tools Configured
- **Pre-commit hooks** with 15+ quality checks
- **Prettier** for comprehensive code formatting
- **ESLint** with TypeScript and React rules
- **TypeScript** type checking
- **Security scanning** with detect-secrets
- **Docker linting** with Hadolint
- **Shell script linting** with ShellCheck
- **Markdown linting** for documentation quality
### 📁 Key Files
- `.pre-commit-config.yaml` - Comprehensive hook configuration
- `.prettierrc` - Formatting rules optimized for TypeScript/React
- `eslint.config.cjs` - Enhanced linting rules
- `.editorconfig` - Editor consistency
- `.secrets.baseline` - Security baseline
- `scripts/setup-pre-commit.sh` - Automated setup
- Python virtual environment (`.venv/`) - Isolated tool environment
### 🚀 Available Commands
```bash
# Code Quality
bun run format # Format all files
bun run format:check # Check formatting
bun run lint # Lint code
bun run lint:fix # Fix linting issues
bun run type-check # TypeScript checks
bun run pre-commit # Run lint-staged
# Pre-commit Hooks
/home/will/Code/meds/.venv/bin/pre-commit run --all-files
/home/will/Code/meds/.venv/bin/pre-commit autoupdate
```
## Next Steps
1. **Test the setup**: Run `bun run format` and `bun run lint:fix` to verify everything works
2. **Make a commit**: Test that pre-commit hooks run automatically
3. **Configure IDE**: Install recommended VS Code extensions for optimal experience
4. **Review docs**: Check `docs/CODE_QUALITY.md` for comprehensive setup details
---
**All documentation is now up-to-date with the current code quality setup! 🎉**
+81
View File
@@ -0,0 +1,81 @@
# 📚 Documenta#### 💻 Development
- **[API Documentation](development/API.md)** - REST API endpoints and usage
- **[Code Quality](development/CODE_QUALITY.md)** - Linting, formatting, and quality standards
- **[Application Security](development/APPLICATION_SECURITY.md)** - Application security practices
- **[Security Changes](development/SECURITY_CHANGES.md)** - Recent security updates and changesndex
Welcome to the RxMinder documentation! This guide will help you navigate through all available documentation organized by category.
## 🏗️ Architecture & Design
- **[Project Structure](architecture/PROJECT_STRUCTURE.md)** - Complete overview of the codebase organization
- **[Template Approach](architecture/TEMPLATE_APPROACH.md)** - Design philosophy and template methodology
## 🚀 Setup & Configuration
- **[Complete Template Configuration](setup/COMPLETE_TEMPLATE_CONFIGURATION.md)** - Full setup guide
- **[Setup Complete](setup/SETUP_COMPLETE.md)** - Post-setup verification checklist
## 💻 Development
- **[API Documentation](development/API.md)** - REST API endpoints and usage
- **[Code Quality](development/CODE_QUALITY.md)** - Linting, formatting, and quality standards
- **[Security](development/SECURITY.md)** - Security guidelines and best practices
- **[Security Changes](development/SECURITY_CHANGES.md)** - Recent security updates and changes
## 🚢 Deployment
- **[Deployment Guide](deployment/DEPLOYMENT.md)** - General deployment instructions
- **[Docker Configuration](deployment/DOCKER_IMAGE_CONFIGURATION.md)** - Docker setup and configuration
- **[Gitea Setup](deployment/GITEA_SETUP.md)** - Gitea CI/CD configuration
- **[Storage Configuration](deployment/STORAGE_CONFIGURATION.md)** - Database and storage setup
## 🔄 Migration Guides
- **[NodeJS Pre-commit Migration](migration/NODEJS_PRECOMMIT_MIGRATION.md)** - Migration from Python to NodeJS pre-commit hooks
- **[Buildx Migration](migration/BUILDX_MIGRATION.md)** - Docker Buildx migration guide
## 📝 Project Information
- **[README](../README.md)** - Main project overview and quick start
- **[Contributing](../CONTRIBUTING.md)** - How to contribute to the project
- **[Changelog](../CHANGELOG.md)** - Version history and changes
- **[License](../LICENSE)** - Project license information
## 📋 Documentation Meta
- **[Documentation Reorganization](REORGANIZATION_SUMMARY.md)** - How we restructured the docs
- **[Docs Update Summary](DOCS_UPDATE_SUMMARY.md)** - Legacy documentation summary
## 🔍 Quick Navigation
### For New Developers
1. Start with [README](../README.md)
2. Review [Project Structure](architecture/PROJECT_STRUCTURE.md)
3. Follow [Complete Template Configuration](setup/COMPLETE_TEMPLATE_CONFIGURATION.md)
4. Read [Code Quality](development/CODE_QUALITY.md) guidelines
### For Deployment
1. Read [Deployment Guide](deployment/DEPLOYMENT.md)
2. Configure [Docker](deployment/DOCKER_IMAGE_CONFIGURATION.md)
3. Set up [Storage](deployment/STORAGE_CONFIGURATION.md)
4. Review [Security](development/SECURITY.md) requirements
### For API Integration
1. Check [API Documentation](development/API.md)
2. Review [Security](development/SECURITY.md) requirements
### For Migration Tasks
1. [NodeJS Pre-commit Migration](migration/NODEJS_PRECOMMIT_MIGRATION.md) - For modernizing git hooks
2. [Buildx Migration](migration/BUILDX_MIGRATION.md) - For Docker build improvements
---
📋 **Last Updated:** September 6, 2025
🔄 **Documentation Version:** 2.0
📦 **Project Version:** 0.0.0
+136
View File
@@ -0,0 +1,136 @@
# 📚 Documentation Reorganization Summary
## Overview
Successfully reorganized the project documentation from scattered root-level files into a structured, categorized system for better navigation and maintenance.
## Changes Made
### 🗂️ New Structure Created
```
docs/
├── README.md # 📋 Main documentation index
├── DOCS_UPDATE_SUMMARY.md # 📝 Legacy docs summary
├── architecture/ # 🏗️ Design & Architecture
│ ├── PROJECT_STRUCTURE.md
│ └── TEMPLATE_APPROACH.md
├── setup/ # 🚀 Setup & Configuration
│ ├── COMPLETE_TEMPLATE_CONFIGURATION.md
│ └── SETUP_COMPLETE.md
├── development/ # 💻 Development Guides
│ ├── API.md
│ ├── CODE_QUALITY.md
│ ├── SECURITY.md
│ └── SECURITY_CHANGES.md
├── deployment/ # 🚢 Deployment Guides
│ ├── DEPLOYMENT.md
│ ├── DOCKER_IMAGE_CONFIGURATION.md
│ ├── GITEA_SETUP.md
│ └── STORAGE_CONFIGURATION.md
└── migration/ # 🔄 Migration Guides
├── BUILDX_MIGRATION.md
└── NODEJS_PRECOMMIT_MIGRATION.md
```
### 📁 Files Moved
#### From Root → `docs/architecture/`
- `PROJECT_STRUCTURE.md`
- `TEMPLATE_APPROACH.md`
#### From Root → `docs/setup/`
- `COMPLETE_TEMPLATE_CONFIGURATION.md`
- `SETUP_COMPLETE.md`
#### From `docs/``docs/development/`
- `API.md`
- `CODE_QUALITY.md`
- `SECURITY.md`
- `SECURITY_CHANGES.md` (from root)
#### From `docs/``docs/deployment/`
- `DEPLOYMENT.md`
- `DOCKER_IMAGE_CONFIGURATION.md` (from root)
- `GITEA_SETUP.md` (from root)
- `STORAGE_CONFIGURATION.md` (from root)
#### From Root → `docs/migration/`
- `BUILDX_MIGRATION.md`
- `NODEJS_PRECOMMIT_MIGRATION.md`
#### To `docs/` root
- `DOCS_UPDATE_SUMMARY.md` (from root)
### 📋 New Documentation Index
Created `docs/README.md` with:
- **Complete categorized index** of all documentation
- **Quick navigation paths** for different user types
- **Direct links** to all organized documents
- **Usage scenarios** (new developers, deployment, API integration, etc.)
### 🔗 Updated References
- Updated main `README.md` to include comprehensive documentation section
- Fixed broken link to `CODE_QUALITY.md` in main README
- Added structured documentation navigation
## Benefits
### 🎯 **Improved Organization**
- **Logical categorization** by purpose and audience
- **Easier navigation** with clear folder structure
- **Reduced root directory clutter**
### 👥 **Better User Experience**
- **Role-based navigation** (developers, ops, admins)
- **Quick-start paths** for different scenarios
- **Comprehensive index** for easy discovery
### 🔧 **Maintainability**
- **Centralized documentation management**
- **Clear ownership** by category
- **Easier updates** and maintenance
### 📈 **Scalability**
- **Room for growth** in each category
- **Consistent structure** for new docs
- **Template for future organization**
## Navigation Guide
### 🔰 For New Team Members
1. Start with main [`README.md`](../README.md)
2. Visit [`docs/README.md`](README.md) for complete index
3. Follow role-specific quick navigation paths
### 📝 For Contributors
1. Check [`docs/development/`](development/) for coding standards
2. Review [`docs/architecture/`](architecture/) for design context
3. Follow [`CONTRIBUTING.md`](../CONTRIBUTING.md) guidelines
### 🚀 For Deployment
1. Start with [`docs/deployment/DEPLOYMENT.md`](deployment/DEPLOYMENT.md)
2. Follow specific deployment guides in [`docs/deployment/`](deployment/)
3. Check [`docs/setup/`](setup/) for configuration help
---
**Documentation Structure Version:** 2.0
**Reorganized:** September 6, 2025
**Status:** ✅ Complete
+180
View File
@@ -0,0 +1,180 @@
# 📁 Project Structure
## Final Organized Structure
```
rxminder/
├── 📄 README.md # Main documentation
├── package.json # Dependencies and scripts
├── ⚙️ vite.config.ts # Build configuration
├── 📝 tsconfig.json # TypeScript configuration
├── 🎨 index.html # Entry point
├── 🔒 .env.example # Environment template
├── 📊 metadata.json # Project metadata
├── 🖼️ banner.jpeg # Project banner image
├── docker/ # Container configuration
│ ├── 🐳 Dockerfile # Multi-stage Docker build
│ ├── docker-compose.yaml # Service orchestration
│ ├── 🌐 nginx.conf # Production web server config
│ └── 🚫 .dockerignore # Docker ignore patterns
├── 📁 scripts/ # All deployment and utility scripts
│ ├── 🚀 deploy.sh # Production deployment
│ ├── ⚡ deploy-k8s.sh # Kubernetes deployment
│ ├── 🔧 setup.sh # Development setup
│ ├── 🌱 seed-production.js # Database seeding
│ ├── ✅ validate-env.sh # Environment validation
│ └── 🧪 validate-deployment.sh # Deployment testing
├── 📁 tests/ # Testing infrastructure
│ ├── 📝 README.md # Testing documentation
│ ├── ⚙️ setup.ts # Jest configuration
│ ├── 📁 integration/ # Integration tests
│ │ └── 🧪 production.test.js # Production validation
│ ├── 📁 manual/ # Manual testing scripts
│ │ ├── 🔧 admin-login-debug.js # Admin debugging
│ │ ├── 🔧 auth-db-debug.js # Auth debugging
│ │ └── 🔧 debug-email-validation.js # Email debugging
│ └── 📁 e2e/ # End-to-end tests with Playwright
│ ├── 📝 README.md # E2E testing documentation
│ ├── 🧪 fixtures.ts # Custom test fixtures
│ ├── 🧪 helpers.ts # Test utilities and data
│ ├── 🧪 auth.spec.ts # Authentication flow tests
│ ├── 🧪 medication.spec.ts # Medication management tests
│ ├── 🧪 admin.spec.ts # Admin interface tests
│ ├── 🧪 ui-navigation.spec.ts # UI and navigation tests
│ └── 🧪 reminders.spec.ts # Reminder system tests
├── 📁 components/ # React components (organized by feature)
│ ├── 📝 README.md # Component architecture docs
│ ├── 📁 medication/ # Medication-related components
│ │ ├── 💊 AddMedicationModal.tsx
│ │ ├── ✏️ EditMedicationModal.tsx
│ │ ├── 📋 ManageMedicationsModal.tsx
│ │ ├── 🏷️ DoseCard.tsx
│ │ └── 📦 index.ts # Feature exports
│ ├── 📁 auth/ # Authentication components
│ │ ├── 🔐 AuthPage.tsx # Login/register interface
│ │ ├── 👤 AvatarDropdown.tsx # User menu
│ │ ├── 🔑 ChangePasswordModal.tsx
│ │ └── 📦 index.ts # Feature exports
│ ├── 📁 admin/ # Admin interface components
│ │ ├── 👑 AdminInterface.tsx # User management
│ │ └── 📦 index.ts # Feature exports
│ ├── 📁 modals/ # Modal components
│ │ ├── ⚙️ AccountModal.tsx # User settings
│ │ ├── AddReminderModal.tsx # Add reminders
│ │ ├── ✏️ EditReminderModal.tsx
│ │ ├── 📚 HistoryModal.tsx # Medication history
│ │ ├── 📋 ManageRemindersModal.tsx
│ │ ├── 🎯 OnboardingModal.tsx # New user setup
│ │ ├── 📊 StatsModal.tsx # Analytics dashboard
│ │ └── 📦 index.ts # Feature exports
│ ├── 📁 ui/ # Reusable UI components
│ │ ├── 📊 BarChart.tsx # Data visualization
│ │ ├── 🔔 ReminderCard.tsx # Reminder display
│ │ ├── 🎨 ThemeSwitcher.tsx # Dark/light theme
│ │ └── 📦 index.ts # Feature exports
│ └── 📁 icons/ # Icon components
│ └── 🎨 Icons.tsx # All icon definitions
├── 📁 services/ # Business logic & APIs
│ ├── 🗄️ couchdb.ts # Mock database service
│ ├── 🗄️ couchdb.production.ts # Real CouchDB service
│ ├── 🏭 couchdb.factory.ts # Service factory
│ ├── 📧 email.ts # Email utilities
│ ├── 📧 mailgun.service.ts # Email delivery
│ ├── 📧 mailgun.config.ts # Email configuration
│ ├── 🌱 database.seeder.ts # Data seeding
│ ├── 🔐 oauth.ts # OAuth integration
│ └── 📁 auth/ # Authentication services
│ ├── 🔐 auth.service.ts # Core auth logic
│ ├── 🔐 auth.types.ts # Auth type definitions
│ ├── 🔐 auth.constants.ts # Auth constants
│ ├── 🔐 auth.error.ts # Error handling
│ ├── 🔐 auth.middleware.ts # Middleware
│ ├── ✉️ emailVerification.service.ts
│ ├── 📁 templates/ # Email templates
│ │ └── ✉️ verification.email.ts
│ └── 📁 __tests__/ # Unit tests
│ ├── 🧪 auth.integration.test.ts
│ └── 🧪 emailVerification.test.ts
├── 📁 contexts/ # React context providers
│ └── 👤 UserContext.tsx # User state management
├── 📁 hooks/ # Custom React hooks
│ ├── 💾 useLocalStorage.ts # Persistent storage
│ ├── ⚙️ useSettings.ts # User preferences
│ ├── 🎨 useTheme.ts # Theme management
│ └── 👤 useUserData.ts # User data management
├── 📁 utils/ # Utility functions
│ └── ⏰ schedule.ts # Reminder scheduling
├── 📁 docs/ # Project documentation
│ ├── 🔐 SECURITY.md # Security guidelines
│ ├── 🚀 DEPLOYMENT.md # Deployment instructions
│ └── 📖 API.md # API documentation
├── 📁 k8s/ # Kubernetes manifests
│ ├── 📝 README.md # K8s deployment guide
│ ├── 🗺️ configmap.yaml # Configuration
│ ├── 🔒 *-secret.yaml # Secrets
│ ├── 🚀 *-deployment.yaml # Deployments
│ ├── 🌐 *-service.yaml # Services
│ ├── 📊 hpa.yaml # Auto-scaling
│ ├── 🌐 ingress.yaml # Load balancing
│ └── 🔒 network-policy.yaml # Network security
└── 📁 .github/ # GitHub configuration
├── 📝 pull_request_template.md
└── 📁 ISSUE_TEMPLATE/
├── 🐛 bug_report.md
└── ✨ feature_request.md
```
## Key Organizational Principles
### ✅ **Feature-Based Organization**
- Components grouped by functionality (medication, auth, admin, etc.)
- Clear separation of concerns
- Easy to locate related files
### ✅ **Script Centralization**
- All deployment and utility scripts in `/scripts/`
- Consistent naming conventions
- Easy access via npm/bun scripts
### ✅ **Testing Structure**
- Unit tests alongside source code (`services/auth/__tests__/`)
- Integration tests in `/tests/integration/`
- E2E tests with Playwright in `/tests/e2e/`
- Manual debugging tools in `/tests/manual/`
- Comprehensive test documentation
- TypeScript support with temporary type declarations
### ✅ **Documentation Organization**
- Feature-specific READMEs in relevant folders
- Centralized docs in `/docs/` folder
- Clear architectural documentation
### ✅ **Configuration Management**
- Environment files at root level
- Build configurations easily accessible
- Docker and K8s configs clearly separated
## Benefits
🎯 **Maintainability** - Clear structure makes code easy to maintain
🔍 **Discoverability** - Logical organization helps find files quickly
🧪 **Testability** - Well-organized test structure
📦 **Deployability** - Scripts and configs clearly separated
👥 **Team Collaboration** - Consistent patterns across the project
📈 **Scalability** - Structure supports growth and new features
+159
View File
@@ -0,0 +1,159 @@
# 🎯 Template-Based Kubernetes Configuration
## Overview
We've implemented a **template-based approach** using environment variables instead of manual base64 encoding for Kubernetes secrets. This is much more user-friendly and secure.
## 🆚 Before vs After Comparison
### ❌ Before (Manual Base64 Encoding)
**Old approach required manual base64 encoding:**
```yaml
# k8s/couchdb-secret.yaml
apiVersion: v1
kind: Secret
data:
# User had to manually encode:
# echo -n "admin" | base64 -> YWRtaW4=
# echo -n "password" | base64 -> cGFzc3dvcmQ=
username: YWRtaW4=
password: cGFzc3dvcmQ=
```
**Problems:**
- 😣 Manual base64 encoding required
- 🔧 Error-prone (encoding mistakes)
- 📝 Hard to read/verify credentials
- 🔒 Credentials visible in YAML files
### ✅ After (Template-Based)
**New approach uses templates with automatic substitution:**
```yaml
# k8s/couchdb-secret.yaml.template
apiVersion: v1
kind: Secret
metadata:
name: couchdb-secret
labels:
app: ${APP_NAME}
type: Opaque
stringData:
# Kubernetes automatically base64 encodes stringData
username: ${COUCHDB_USER}
password: ${COUCHDB_PASSWORD}
```
**Benefits:**
- ✅ No manual base64 encoding needed
- ✅ Environment variables from `.env` file
- ✅ Human-readable configuration
- ✅ Automatic deployment script
- ✅ Customizable app names
## 🚀 How It Works
### 1. Configuration in `.env`
```bash
# .env (user-friendly configuration)
APP_NAME=my-rxminder
COUCHDB_USER=admin
COUCHDB_PASSWORD=super-secure-password-123
INGRESS_HOST=rxminder.mydomain.com
```
### 2. Template Substitution
```bash
# Automatic substitution with envsubst
envsubst < k8s/couchdb-secret.yaml.template
```
**Result:**
```yaml
apiVersion: v1
kind: Secret
metadata:
name: couchdb-secret
labels:
app: my-rxminder
type: Opaque
stringData:
username: admin
password: super-secure-password-123
```
### 3. Kubernetes Processing
- Kubernetes automatically base64 encodes `stringData` fields
- No manual encoding required
- More secure and reliable
## 🎛️ Deployment Options
### Option 1: Automated Script (Recommended)
```bash
# Copy and configure
cp .env.example .env
nano .env
# Deploy everything
./scripts/k8s-deploy-template.sh deploy
```
### Option 2: Manual Template Processing
```bash
# Set environment variables
export APP_NAME=my-rxminder
export COUCHDB_PASSWORD=secure-password
# Process templates
envsubst < k8s/couchdb-secret.yaml.template | kubectl apply -f -
envsubst < k8s/ingress.yaml.template | kubectl apply -f -
```
## 🔧 Template Files Created
1. **`k8s/couchdb-secret.yaml.template`** - Database credentials
2. **`k8s/ingress.yaml.template`** - Ingress with custom hostname
3. **`k8s/configmap.yaml.template`** - Application configuration
4. **`k8s/frontend-deployment.yaml.template`** - Frontend deployment
5. **`scripts/k8s-deploy-template.sh`** - Automated deployment script
## 🛡️ Security Benefits
- **No hardcoded credentials** in version control
- **Environment-specific configuration** via `.env` files
- **Automatic validation** of required variables
- **Kubernetes stringData** (auto base64 encoding)
- **Clear separation** of config and code
## 📝 User Experience Improvements
| Aspect | Before | After |
| -------------------- | ------------------------ | ---------------------- |
| **Setup Complexity** | High (manual base64) | Low (edit .env) |
| **Error Rate** | High (encoding mistakes) | Low (plain text) |
| **Readability** | Poor (base64 strings) | Excellent (plain text) |
| **Customization** | Manual file editing | Environment variables |
| **Deployment** | Multi-step manual | Single command |
## 🎯 Result
The template-based approach makes RxMinder deployment:
- **More user-friendly** - No technical encoding required
- **More secure** - Credentials externalized to `.env`
- **More maintainable** - Clear separation of config and manifests
- **More flexible** - Easy customization via environment variables
This is a **production-ready, enterprise-grade** configuration management approach that follows Kubernetes best practices.
+538
View File
@@ -0,0 +1,538 @@
# Deployment Guide
## 🚀 Complete Deployment Guide for Medication Reminder App
### **Prerequisites**
#### **System Requirements**
- Docker 20.10+ and Docker Compose 2.0+
- 2GB RAM minimum, 4GB recommended
- 10GB disk space for application and data
- Linux/macOS/Windows with WSL2
#### **Required Accounts**
- [Mailgun Account](https://mailgun.com) for email services
- Domain name for production deployment (optional)
- SSL certificate for HTTPS (recommended)
### **Environment Setup**
#### **1. Clone Repository**
```bash
git clone <repository-url>
cd meds
```
#### **2. Configure Environment**
```bash
# Copy template
cp .env.example .env
# Edit with your credentials
nano .env
```
**Required Variables:**
```bash
# Application Configuration
APP_BASE_URL=https://yourdomain.com
# CouchDB Configuration
COUCHDB_USER=admin
COUCHDB_PASSWORD=super-secure-password-123!
VITE_COUCHDB_URL=http://couchdb:5984
VITE_COUCHDB_USER=admin
VITE_COUCHDB_PASSWORD=super-secure-password-123!
# Mailgun Configuration
MAILGUN_API_KEY=key-1234567890abcdef1234567890abcdef
MAILGUN_DOMAIN=mg.yourdomain.com
MAILGUN_FROM_EMAIL=noreply@yourdomain.com
# Production Settings
NODE_ENV=production
```
### **Local Development Deployment**
#### **Quick Start**
```bash
# Automated setup
./setup.sh
# Manual setup
bun install
docker compose up -d
bun run seed-production.js
```
#### **Development URLs**
- Frontend: http://localhost:8080
- CouchDB: http://localhost:5984
- Admin Panel: http://localhost:5984/\_utils
### **Production Deployment**
#### **Method 1: Automated Script**
```bash
# Secure deployment with validation
./deploy.sh production
```
#### **Method 2: Manual Docker Compose**
```bash
# Build images
docker compose build --no-cache
# Start services
docker compose up -d
# Seed database
node seed-production.js
# Verify deployment
bun test-production.js
```
#### **Method 3: Docker Swarm**
```bash
# Initialize swarm
docker swarm init
# Deploy stack
docker stack deploy -c docker/docker-compose.yaml meds-stack
# Scale services
docker service scale meds-stack_frontend=3
```
### **Cloud Platform Deployments**
#### **AWS EC2 Deployment**
**1. Launch EC2 Instance**
```bash
# Amazon Linux 2 AMI
# Instance type: t3.medium or larger
# Security group: Allow ports 22, 80, 443, 8080
```
**2. Install Dependencies**
```bash
# Connect to instance
ssh -i your-key.pem ec2-user@your-instance-ip
# Install Docker
sudo yum update -y
sudo yum install -y docker
sudo service docker start
sudo usermod -a -G docker ec2-user
# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
```
**3. Deploy Application**
```bash
# Clone and configure
git clone <repository-url>
cd meds
cp .env.example .env
# Edit .env with production values
# Deploy
./deploy.sh production
```
#### **Google Cloud Platform Deployment**
**1. Cloud Run Deployment**
```bash
# Build and push image
gcloud builds submit --tag gcr.io/PROJECT-ID/meds-app
# Deploy service
gcloud run deploy meds-app \
--image gcr.io/PROJECT-ID/meds-app \
--platform managed \
--region us-central1 \
--set-env-vars COUCHDB_URL=your-couchdb-url \
--set-env-vars MAILGUN_API_KEY=your-key \
--allow-unauthenticated
```
**2. Compute Engine Deployment**
```bash
# Create instance
gcloud compute instances create meds-server \
--image-family debian-11 \
--image-project debian-cloud \
--machine-type e2-medium \
--tags http-server,https-server
# SSH and install
gcloud compute ssh meds-server
# Follow standard installation steps
```
#### **Digital Ocean Deployment**
**1. Droplet Setup**
```bash
# Create droplet with Docker pre-installed
# Or install Docker manually on Ubuntu droplet
# Connect and deploy
ssh root@your-droplet-ip
git clone <repository-url>
cd meds
./setup.sh
./deploy.sh production
```
**2. App Platform Deployment**
```bash
# Create app.yaml
version: 1
services:
- name: meds-app
source_dir: /
github:
repo: your-username/meds
branch: main
build_command: bun run build
environment_slug: node-js
instance_count: 1
instance_size_slug: basic-xxs
envs:
- key: COUCHDB_URL
value: ${COUCHDB_URL}
- key: MAILGUN_API_KEY
value: ${MAILGUN_API_KEY}
# Deploy
doctl apps create --spec app.yaml
```
### **Kubernetes Deployment**
#### **Method 1: Automated Deployment Script (Recommended)**
```bash
# Configure environment
cp .env.example .env
# Edit .env with your settings:
# INGRESS_HOST=app.meds.192.168.1.100.nip.io # For local cluster
# INGRESS_HOST=meds.yourdomain.com # For production
# Deploy with environment substitution
./deploy-k8s.sh
# Check deployment status
./deploy-k8s.sh --status
# Deploy with custom environment file
./deploy-k8s.sh --env .env.production
# Preview deployment (dry run)
./deploy-k8s.sh --dry-run
```
#### **Method 2: Manual Deployment**
#### **1. Create Namespace and Secrets**
```bash
# Create namespace
kubectl create namespace meds-app
# Create secrets
kubectl create secret generic meds-secrets \
--from-literal=couchdb-user=admin \
--from-literal=couchdb-password=secure-password \
--from-literal=mailgun-api-key=your-api-key \
--namespace meds-app
```
#### **2. Deploy Services**
```bash
# Apply Kubernetes manifests
kubectl apply -f k8s/ --namespace meds-app
# Check deployment status
kubectl get pods -n meds-app
kubectl get services -n meds-app
```
#### **3. Configure Ingress (Manual)**
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: meds-ingress
namespace: meds-app
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- meds.yourdomain.com
secretName: meds-tls
rules:
- host: meds.yourdomain.com # Update this to your domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: meds-frontend
port:
number: 80
```
### **SSL/HTTPS Configuration**
#### **Let's Encrypt with Nginx**
```bash
# Install certbot
sudo apt-get install certbot python3-certbot-nginx
# Get certificate
sudo certbot --nginx -d yourdomain.com
# Auto-renewal
sudo crontab -e
# Add: 0 12 * * * /usr/bin/certbot renew --quiet
```
#### **Cloudflare SSL**
```bash
# Update docker/nginx.conf for Cloudflare
# Set ssl_certificate and ssl_certificate_key
# Configure Cloudflare for Full (Strict) SSL
```
### **Database Backup and Recovery**
#### **CouchDB Backup**
```bash
# Create backup script
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backup/couchdb"
# Backup all databases
curl -X GET http://admin:password@localhost:5984/_all_dbs | \
jq -r '.[]' | while read db; do
curl -X GET "http://admin:password@localhost:5984/$db/_all_docs?include_docs=true" \
> "$BACKUP_DIR/${db}_${DATE}.json"
done
```
#### **Automated Backups**
```bash
# Add to crontab
0 2 * * * /opt/meds/backup-couchdb.sh
# Upload to cloud storage
aws s3 cp /backup/couchdb/ s3://your-backup-bucket/ --recursive
```
### **Monitoring and Logging**
#### **Health Checks**
```bash
# Application health
curl -f http://localhost:8080/health
# CouchDB health
curl -f http://admin:password@localhost:5984/_up
# Docker container health
docker compose ps
```
#### **Log Management**
```bash
# View logs
docker compose logs -f frontend
docker compose logs -f couchdb
# Log rotation
# Configure in docker/docker-compose.yaml:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
```
#### **Performance Monitoring**
```bash
# Resource usage
docker stats
# Application metrics
# Implement custom metrics endpoint
# Use Prometheus/Grafana for monitoring
```
### **Scaling and Load Balancing**
#### **Horizontal Scaling**
```bash
# Scale frontend containers
docker compose up -d --scale frontend=3
# Load balancer configuration
# Use nginx, HAProxy, or cloud load balancer
```
#### **Database Scaling**
```bash
# CouchDB clustering
# Configure multiple CouchDB nodes
# Set up replication between nodes
```
### **Security Hardening**
#### **Firewall Configuration**
```bash
# UFW (Ubuntu)
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 5984/tcp # CouchDB admin (internal only)
sudo ufw enable
```
#### **Container Security**
```bash
# Run security scan
docker scout cves meds-frontend:latest
# Update base images regularly
docker compose build --no-cache
```
### **Troubleshooting**
#### **Common Issues**
**1. Environment Variables Not Loading**
```bash
# Check file format
cat -A .env
# Verify Docker Compose config
docker compose config
```
**2. Database Connection Issues**
```bash
# Test CouchDB connection
curl -u admin:password http://localhost:5984/
# Check container logs
docker compose logs couchdb
```
**3. Email Not Sending**
```bash
# Verify Mailgun configuration
curl -s --user 'api:YOUR_API_KEY' \
https://api.mailgun.net/v3/YOUR_DOMAIN/messages \
-F from='test@YOUR_DOMAIN' \
-F to='you@example.com' \
-F subject='Test' \
-F text='Testing'
```
**4. Frontend Build Failures**
```bash
# Clear cache and rebuild
docker compose build --no-cache frontend
```
### **Maintenance**
#### **Regular Tasks**
- Update dependencies monthly
- Rotate credentials quarterly
- Backup database daily
- Monitor disk space weekly
- Review security logs daily
#### **Update Process**
```bash
# 1. Backup current deployment
./backup.sh
# 2. Pull latest changes
git pull origin main
# 3. Update dependencies
bun install
# 4. Rebuild and deploy
docker compose build --no-cache
docker compose up -d
# 5. Verify deployment
bun test-production.js
```
### **Support and Documentation**
#### **Getting Help**
- GitHub Issues: Create issue for bugs/features
- Documentation: Check README.md and docs/
- Community: Join our Discord/Slack channel
#### **Professional Support**
- Enterprise support available
- Custom deployment assistance
- Security auditing services
- Performance optimization consulting
@@ -0,0 +1,265 @@
# 🐳 Docker Image Configuration
## Overview
RxMinder now supports configurable Docker images via environment variables, enabling flexible deployment across different registries, environments, and versions.
## 🎯 Docker Image Variable
### **DOCKER_IMAGE**
The complete Docker image specification including registry, repository, and tag.
**Format:** `[registry/]repository:tag`
## 🌐 Registry Examples
### Public Registries
#### Docker Hub
```bash
# Official image on Docker Hub
DOCKER_IMAGE=rxminder/rxminder:latest
DOCKER_IMAGE=rxminder/rxminder:v1.2.0
DOCKER_IMAGE=rxminder/rxminder:stable
```
#### GitHub Container Registry (ghcr.io)
```bash
# GitHub Packages
DOCKER_IMAGE=ghcr.io/username/rxminder:latest
DOCKER_IMAGE=ghcr.io/organization/rxminder:v1.2.0
DOCKER_IMAGE=ghcr.io/username/rxminder:dev-branch
```
#### GitLab Container Registry
```bash
# GitLab Registry
DOCKER_IMAGE=registry.gitlab.com/username/rxminder:latest
DOCKER_IMAGE=registry.gitlab.com/group/rxminder:production
```
### Private/Self-Hosted Registries
#### Gitea Registry
```bash
# Current default (Gitea)
DOCKER_IMAGE=gitea-http.taildb3494.ts.net/will/meds:latest
DOCKER_IMAGE=gitea-http.taildb3494.ts.net/will/meds:v1.2.0
```
#### Harbor Registry
```bash
# Harbor enterprise registry
DOCKER_IMAGE=harbor.company.com/rxminder/rxminder:latest
DOCKER_IMAGE=harbor.company.com/rxminder/rxminder:production
```
#### Local Registry
```bash
# Local development registry
DOCKER_IMAGE=localhost:5000/rxminder:latest
DOCKER_IMAGE=registry.local:5000/rxminder:dev
```
### Cloud Provider Registries
#### AWS Elastic Container Registry (ECR)
```bash
# AWS ECR
DOCKER_IMAGE=123456789012.dkr.ecr.us-west-2.amazonaws.com/rxminder:latest
DOCKER_IMAGE=123456789012.dkr.ecr.us-west-2.amazonaws.com/rxminder:v1.2.0
```
#### Google Container Registry (GCR)
```bash
# Google Cloud Registry
DOCKER_IMAGE=gcr.io/project-id/rxminder:latest
DOCKER_IMAGE=us.gcr.io/project-id/rxminder:production
```
#### Azure Container Registry (ACR)
```bash
# Azure Container Registry
DOCKER_IMAGE=myregistry.azurecr.io/rxminder:latest
DOCKER_IMAGE=myregistry.azurecr.io/rxminder:stable
```
## 🏷️ Tagging Strategies
### Environment-Based Tagging
```bash
# Development
DOCKER_IMAGE=myregistry.com/rxminder:dev
DOCKER_IMAGE=myregistry.com/rxminder:develop-20250906
# Staging
DOCKER_IMAGE=myregistry.com/rxminder:staging
DOCKER_IMAGE=myregistry.com/rxminder:release-candidate
# Production
DOCKER_IMAGE=myregistry.com/rxminder:stable
DOCKER_IMAGE=myregistry.com/rxminder:v1.2.0
```
### Git-Based Tagging
```bash
# Branch-based
DOCKER_IMAGE=myregistry.com/rxminder:main
DOCKER_IMAGE=myregistry.com/rxminder:feature-auth
# Commit-based
DOCKER_IMAGE=myregistry.com/rxminder:sha-abc1234
DOCKER_IMAGE=myregistry.com/rxminder:pr-123
```
### Semantic Versioning
```bash
# Semantic versions
DOCKER_IMAGE=myregistry.com/rxminder:v1.0.0
DOCKER_IMAGE=myregistry.com/rxminder:v1.2.3-beta
DOCKER_IMAGE=myregistry.com/rxminder:v2.0.0-rc1
```
## 🎪 Environment-Specific Configurations
### Development (.env)
```bash
APP_NAME=rxminder-dev
DOCKER_IMAGE=localhost:5000/rxminder:dev
STORAGE_CLASS=local-path
STORAGE_SIZE=5Gi
INGRESS_HOST=rxminder-dev.local
```
### Staging (.env.staging)
```bash
APP_NAME=rxminder-staging
DOCKER_IMAGE=myregistry.com/rxminder:staging
STORAGE_CLASS=longhorn
STORAGE_SIZE=10Gi
INGRESS_HOST=staging.rxminder.company.com
```
### Production (.env.production)
```bash
APP_NAME=rxminder
DOCKER_IMAGE=myregistry.com/rxminder:v1.2.0 # Fixed version for stability
STORAGE_CLASS=fast-ssd
STORAGE_SIZE=50Gi
INGRESS_HOST=rxminder.company.com
```
## 🚀 CI/CD Integration
### GitHub Actions Example
```yaml
# .github/workflows/deploy.yml
- name: Deploy to Kubernetes
env:
DOCKER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
run: |
echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> .env
./scripts/k8s-deploy-template.sh deploy
```
### GitLab CI Example
```yaml
# .gitlab-ci.yml
deploy:
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
script:
- echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> .env
- ./scripts/k8s-deploy-template.sh deploy
```
## 🔒 Registry Authentication
### Docker Registry Secrets
```bash
# Create registry secret for private registries
kubectl create secret docker-registry regcred \
--docker-server=myregistry.com \
--docker-username=username \
--docker-password=password \
--docker-email=email@company.com
# Update deployment to use the secret
# (Add imagePullSecrets to deployment template if needed)
```
### Cloud Provider Authentication
```bash
# AWS ECR
aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-west-2.amazonaws.com
# Google GCR
gcloud auth configure-docker
# Azure ACR
az acr login --name myregistry
```
## 💡 Best Practices
### Production Recommendations
- ✅ **Use specific tags** (not `:latest`) for production
- ✅ **Pin to exact versions** for stability
- ✅ **Use semantic versioning** for releases
- ✅ **Separate registries** for different environments
- ✅ **Enable vulnerability scanning** on registries
### Development Workflow
- ✅ **Use `:dev` or `:latest`** for development
- ✅ **Branch-based tags** for feature development
- ✅ **Local registries** for fast iteration
- ✅ **Automated builds** on code changes
### Security Considerations
- ✅ **Private registries** for proprietary code
- ✅ **Registry authentication** properly configured
- ✅ **Image scanning** for vulnerabilities
- ✅ **Supply chain security** with signed images
## 🎭 Example Deployments
### Multi-Environment Setup
```bash
# Development
export DOCKER_IMAGE=localhost:5000/rxminder:dev
./scripts/k8s-deploy-template.sh deploy
# Staging
export DOCKER_IMAGE=registry.company.com/rxminder:staging
./scripts/k8s-deploy-template.sh deploy
# Production
export DOCKER_IMAGE=registry.company.com/rxminder:v1.2.0
./scripts/k8s-deploy-template.sh deploy
```
This flexible Docker image configuration makes RxMinder truly **portable** and **CI/CD-ready** across any container registry and deployment environment!
+242
View File
@@ -0,0 +1,242 @@
# 🦌 Gitea CI/CD Setup Complete!
Your RxMinder app now has comprehensive Gitea Actions CI/CD support! Here's what's been created:
## 📁 New Files Structure
```
.gitea/
├── workflows/
│ └── ci-cd.yml # Main CI/CD workflow
├── docker-compose.ci.yml # CI-specific compose override
├── gitea-bake.hcl # Gitea-optimized buildx config
└── README.md # Detailed Gitea configuration guide
scripts/
├── gitea-deploy.sh # Gitea-specific deployment script
└── gitea-helper.sh # Comprehensive Gitea operations helper
```
## 🚀 Quick Start
### 1. **Setup Environment Configuration**
```bash
# Copy the example environment file and customize
cp .env.example .env
# Edit .env with your registry and configuration:
CONTAINER_REGISTRY=gitea.yourdomain.com
CONTAINER_REPOSITORY=username/rxminder
GITEA_REGISTRY=gitea.yourdomain.com
GITEA_REPOSITORY=username/rxminder
```
### 2. **Setup Gitea Repository**
```bash
# Configure in Gitea Repository Settings → Actions
# Required Secrets:
GITEA_TOKEN # Personal access token with package write permissions
VITE_COUCHDB_PASSWORD # CouchDB password
DEPLOYMENT_WEBHOOK_URL # Optional: deployment notifications
# Repository Variables (optional - will use .env defaults):
GITEA_REGISTRY # Override registry from .env
VITE_COUCHDB_URL # http://localhost:5984
VITE_COUCHDB_USER # admin
APP_BASE_URL # http://localhost:8080
```
### 3. **Local Development with Gitea**
```bash
# Setup Gitea buildx builder
bun run gitea:setup
# Build for local development
bun run gitea:build-local
# Run tests
bun run gitea:test
# Check status
bun run gitea:status
```
### 4. **Production Deployment**
```bash
# Build and push to registry
export GITEA_TOKEN=your_token
export GITEA_REGISTRY=your-gitea.com
export GITEA_REPOSITORY=username/rxminder
bun run gitea:build-prod v1.0.0
# Deploy to production
bun run gitea:deploy production v1.0.0
```
## 🔧 Gitea Actions Features
### **Multi-Platform Builds**
- ✅ AMD64 (Intel/AMD processors)
- ✅ ARM64 (Apple Silicon, AWS Graviton)
- ✅ Optimized layer caching
- ✅ Registry-based build cache
### **Security & Quality**
- ✅ Trivy vulnerability scanning
- ✅ Supply chain attestations (SBOM, provenance)
- ✅ Dependency auditing
- ✅ Lint and type checking
### **Deployment Options**
- ✅ Docker Compose deployment
- ✅ Kubernetes deployment
- ✅ Staging environment support
- ✅ Health checks and monitoring
### **Automation**
- ✅ Automatic builds on push/PR
- ✅ Multi-environment deployments
- ✅ Image cleanup and maintenance
- ✅ Deployment notifications
## 📋 Available Commands
### **Gitea Helper Script**
```bash
./scripts/gitea-helper.sh setup # Setup buildx for Gitea
./scripts/gitea-helper.sh build-local # Local development build
./scripts/gitea-helper.sh build-multi # Multi-platform build
./scripts/gitea-helper.sh build-staging # Staging build
./scripts/gitea-helper.sh build-prod # Production build
./scripts/gitea-helper.sh test # Run all tests
./scripts/gitea-helper.sh deploy # Deploy to environment
./scripts/gitea-helper.sh status # Show CI/CD status
./scripts/gitea-helper.sh cleanup # Cleanup builders/images
```
### **Package.json Scripts**
```bash
bun run gitea:setup # Setup Gitea buildx
bun run gitea:build # Multi-platform build
bun run gitea:build-local # Local development
bun run gitea:build-staging # Staging build
bun run gitea:build-prod # Production build
bun run gitea:test # Run tests
bun run gitea:deploy # Deploy application
bun run gitea:status # Check status
bun run gitea:cleanup # Cleanup
```
## 🎯 Workflow Triggers
### **Automatic Triggers**
- **Push to main/develop**: Full build, test, and deploy
- **Pull Request**: Build, test, and security scan
- **Manual dispatch**: On-demand deployment
### **Environment-Specific**
- **Development**: Fast single-platform builds
- **Staging**: Full testing with staging configs
- **Production**: Multi-platform with attestations
## 🔒 Security Features
### **Image Security**
- Vulnerability scanning with Trivy
- Base image security updates
- Minimal attack surface
- Supply chain attestations
### **Secrets Management**
- Gitea-native secrets storage
- Environment-specific variables
- Token rotation support
- Secure registry authentication
## 📊 Monitoring & Notifications
### **Health Checks**
- Frontend application health
- Database connectivity
- Service dependency checks
- Container resource monitoring
### **Notifications**
- Deployment success/failure alerts
- Security scan results
- Build status updates
- Custom webhook integration
## 🚀 Next Steps
1. **Configure Gitea Repository**:
- Enable Actions in repository settings
- Add required secrets and variables
- Configure container registry
2. **Set up Gitea Runner**:
- Install and configure Gitea Actions runner
- Ensure Docker and buildx support
- Configure appropriate labels
3. **Test the Pipeline**:
```bash
# Push to trigger the workflow
git add .
git commit -m "Setup Gitea CI/CD"
git push origin main
```
4. **Customize for Your Environment**:
- Update registry URLs in `.gitea/gitea-bake.hcl`
- Modify deployment targets in `scripts/gitea-deploy.sh`
- Configure environment-specific variables
## 🔄 Migration Notes
- ✅ **Fully compatible** with existing Docker Buildx setup
- ✅ **No breaking changes** to development workflow
- ✅ **Parallel support** with GitHub Actions if needed
- ✅ **Easy rollback** - simply delete `.gitea/` directory
Your RxMinder app is now ready for professional-grade CI/CD with Gitea! 🎉
## 📞 Troubleshooting
### Common Issues:
1. **Build failures**: Check Gitea runner has Docker buildx
2. **Registry push errors**: Verify GITEA_TOKEN permissions
3. **Deployment issues**: Check environment variables and secrets
### Debug Commands:
```bash
# Check Gitea environment
./scripts/gitea-helper.sh status
# Test local build
./scripts/gitea-helper.sh build-local
# Verify registry login
docker login your-gitea.com
```
+226
View File
@@ -0,0 +1,226 @@
# 📦 Storage Configuration Examples
## Overview
RxMinder now supports configurable storage through environment variables, making it easy to adapt to different Kubernetes environments and storage requirements.
## 🗂️ Storage Configuration Variables
### **STORAGE_CLASS**
The Kubernetes StorageClass to use for persistent volumes.
**Common Options:**
- `longhorn` - Longhorn distributed storage (Raspberry Pi clusters)
- `local-path` - Local path provisioner (k3s default)
- `standard` - Cloud provider standard storage
- `fast-ssd` - High-performance SSD storage
- `gp2` - AWS General Purpose SSD
- `pd-standard` - Google Cloud Standard Persistent Disk
- `azure-disk` - Azure Standard Disk
### **STORAGE_SIZE**
The amount of storage to allocate for the CouchDB database.
**Sizing Guidelines:**
- `1Gi` - Minimal testing (not recommended for production)
- `5Gi` - Small deployment (default, good for development)
- `10Gi` - Medium deployment (suitable for small teams)
- `20Gi` - Large deployment (production use)
- `50Gi+` - Enterprise deployment (high-volume usage)
## 🎯 Environment-Specific Examples
### Development (.env)
```bash
# Development environment
APP_NAME=rxminder-dev
STORAGE_CLASS=local-path
STORAGE_SIZE=5Gi
INGRESS_HOST=rxminder-dev.local
```
### Staging (.env.staging)
```bash
# Staging environment
APP_NAME=rxminder-staging
STORAGE_CLASS=longhorn
STORAGE_SIZE=10Gi
INGRESS_HOST=staging.rxminder.company.com
```
### Production (.env.production)
```bash
# Production environment
APP_NAME=rxminder
STORAGE_CLASS=fast-ssd
STORAGE_SIZE=50Gi
INGRESS_HOST=rxminder.company.com
```
### Cloud Providers
#### AWS EKS
```bash
APP_NAME=rxminder
STORAGE_CLASS=gp2 # General Purpose SSD
STORAGE_SIZE=20Gi
INGRESS_HOST=rxminder.aws.company.com
```
#### Google GKE
```bash
APP_NAME=rxminder
STORAGE_CLASS=pd-standard # Standard Persistent Disk
STORAGE_SIZE=20Gi
INGRESS_HOST=rxminder.gcp.company.com
```
#### Azure AKS
```bash
APP_NAME=rxminder
STORAGE_CLASS=managed-premium # Premium SSD
STORAGE_SIZE=20Gi
INGRESS_HOST=rxminder.azure.company.com
```
## 🏗️ Generated Kubernetes Resources
### Before (Hardcoded)
```yaml
# Old approach - hardcoded values
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: couchdb-pvc
spec:
storageClassName: longhorn
resources:
requests:
storage: 1Gi
```
### After (Template-Based)
```yaml
# Template: k8s/couchdb-pvc.yaml.template
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ${APP_NAME}-couchdb-pvc
labels:
app: ${APP_NAME}
spec:
storageClassName: ${STORAGE_CLASS}
resources:
requests:
storage: ${STORAGE_SIZE}
```
### Deployed Result
```yaml
# After envsubst processing
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rxminder-couchdb-pvc
labels:
app: rxminder
spec:
storageClassName: fast-ssd
resources:
requests:
storage: 20Gi
```
## 🚀 Deployment Examples
### Quick Development Setup
```bash
# Development with local storage
export APP_NAME=rxminder-dev
export STORAGE_CLASS=local-path
export STORAGE_SIZE=5Gi
./scripts/k8s-deploy-template.sh deploy
```
### Production Deployment
```bash
# Copy production environment
cp .env.production .env
# Edit with your specific values
nano .env
# Deploy to production
./scripts/k8s-deploy-template.sh deploy
```
### Custom Configuration
```bash
# Override specific values
export STORAGE_CLASS=custom-storage
export STORAGE_SIZE=100Gi
./scripts/k8s-deploy-template.sh deploy
```
## 🔍 Storage Class Discovery
### Find Available Storage Classes
```bash
# List available storage classes in your cluster
kubectl get storageclass
# Get details about a specific storage class
kubectl describe storageclass longhorn
```
### Common Storage Class Names by Platform
| Platform | Common Storage Classes |
| -------------- | ------------------------------------------ |
| **k3s** | `local-path` (default) |
| **Longhorn** | `longhorn` |
| **AWS EKS** | `gp2`, `gp3`, `io1`, `io2` |
| **Google GKE** | `standard`, `ssd`, `pd-standard`, `pd-ssd` |
| **Azure AKS** | `default`, `managed-premium` |
| **Rancher** | `longhorn`, `local-path` |
## 💡 Benefits
### Flexibility
- ✅ **Environment-specific** storage configuration
- ✅ **Cloud-agnostic** deployment
- ✅ **Performance tuning** via storage class selection
- ✅ **Cost optimization** through appropriate sizing
### Maintainability
- ✅ **Single source of truth** via `.env` files
- ✅ **Easy scaling** by changing STORAGE_SIZE
- ✅ **Environment promotion** using different .env files
- ✅ **Disaster recovery** with consistent configurations
### Developer Experience
- ✅ **No hardcoded values** in manifests
- ✅ **Clear documentation** of requirements
- ✅ **Validation** of required variables
- ✅ **Automated deployment** with proper storage setup
This approach makes RxMinder truly **portable** across different Kubernetes environments while maintaining **production-grade** storage management!
+839
View File
@@ -0,0 +1,839 @@
# API Documentation
## 📚 API Reference for Medication Reminder App
### **Base URL**
- Development: `http://localhost:5173`
- Production: `http://localhost:8080`
### **Authentication**
All authenticated endpoints require a valid session token.
#### **Headers**
```http
Authorization: Bearer <token>
Content-Type: application/json
```
---
## 🔐 Authentication Endpoints
### **Register User**
Create a new user account with email verification.
**Endpoint:** `POST /auth/register`
**Request Body:**
```json
{
"email": "user@example.com",
"password": "SecurePassword123!",
"username": "JohnDoe"
}
```
**Response:**
```json
{
"user": {
"_id": "user-uuid",
"email": "user@example.com",
"username": "JohnDoe",
"status": "PENDING",
"emailVerified": false,
"role": "USER",
"createdAt": "2025-09-05T12:00:00Z"
},
"verificationToken": {
"token": "verification-token-uuid",
"expiresAt": "2025-09-05T13:00:00Z"
}
}
```
**Status Codes:**
- `201` - User created successfully
- `400` - Invalid input data
- `409` - Email already exists
---
### **Login User**
Authenticate user with email and password.
**Endpoint:** `POST /auth/login`
**Request Body:**
```json
{
"email": "user@example.com",
"password": "SecurePassword123!"
}
```
**Response:**
```json
{
"user": {
"_id": "user-uuid",
"email": "user@example.com",
"username": "JohnDoe",
"status": "ACTIVE",
"emailVerified": true,
"role": "USER"
},
"accessToken": "jwt-access-token",
"refreshToken": "jwt-refresh-token"
}
```
**Status Codes:**
- `200` - Login successful
- `401` - Invalid credentials
- `403` - Account not verified or suspended
---
### **OAuth Login**
Authenticate using OAuth providers (Google, GitHub).
**Endpoint:** `POST /auth/oauth`
**Request Body:**
```json
{
"provider": "google",
"userData": {
"email": "user@example.com",
"username": "John Doe",
"avatar": "https://example.com/avatar.jpg"
}
}
```
**Response:**
```json
{
"user": {
"_id": "user-uuid",
"email": "user@example.com",
"username": "John Doe",
"status": "ACTIVE",
"emailVerified": true,
"role": "USER",
"avatar": "https://example.com/avatar.jpg"
},
"accessToken": "jwt-access-token",
"refreshToken": "jwt-refresh-token"
}
```
---
### **Verify Email**
Activate user account using verification token.
**Endpoint:** `POST /auth/verify-email`
**Request Body:**
```json
{
"token": "verification-token-uuid"
}
```
**Response:**
```json
{
"user": {
"_id": "user-uuid",
"email": "user@example.com",
"username": "JohnDoe",
"status": "ACTIVE",
"emailVerified": true,
"role": "USER"
}
}
```
---
### **Change Password**
Change user password (requires current password).
**Endpoint:** `POST /auth/change-password`
**Request Body:**
```json
{
"userId": "user-uuid",
"currentPassword": "OldPassword123!",
"newPassword": "NewPassword456!"
}
```
**Response:**
```json
{
"success": true,
"message": "Password changed successfully"
}
```
---
### **Request Password Reset**
Request password reset email.
**Endpoint:** `POST /auth/request-password-reset`
**Request Body:**
```json
{
"email": "user@example.com"
}
```
**Response:**
```json
{
"success": true,
"message": "Password reset email sent"
}
```
---
### **Reset Password**
Reset password using reset token.
**Endpoint:** `POST /auth/reset-password`
**Request Body:**
```json
{
"token": "reset-token-uuid",
"newPassword": "NewPassword123!"
}
```
**Response:**
```json
{
"success": true,
"message": "Password reset successful"
}
```
---
## 💊 Medication Management
### **Add Medication**
Add a new medication to user's list.
**Endpoint:** `POST /medications`
**Request Body:**
```json
{
"name": "Aspirin",
"dosage": "100mg",
"frequency": "Daily",
"startTime": "08:00",
"notes": "Take with food",
"icon": "💊"
}
```
**Response:**
```json
{
"_id": "medication-uuid",
"name": "Aspirin",
"dosage": "100mg",
"frequency": "Daily",
"startTime": "08:00",
"notes": "Take with food",
"icon": "💊",
"userId": "user-uuid",
"createdAt": "2025-09-05T12:00:00Z"
}
```
---
### **Get Medications**
Retrieve user's medications.
**Endpoint:** `GET /medications`
**Query Parameters:**
- `active` (boolean) - Filter active medications only
**Response:**
```json
[
{
"_id": "medication-uuid",
"name": "Aspirin",
"dosage": "100mg",
"frequency": "Daily",
"startTime": "08:00",
"notes": "Take with food",
"icon": "💊"
}
]
```
---
### **Update Medication**
Update existing medication.
**Endpoint:** `PUT /medications/:id`
**Request Body:**
```json
{
"dosage": "200mg",
"notes": "Take with plenty of water"
}
```
**Response:**
```json
{
"_id": "medication-uuid",
"name": "Aspirin",
"dosage": "200mg",
"frequency": "Daily",
"startTime": "08:00",
"notes": "Take with plenty of water",
"icon": "💊"
}
```
---
### **Delete Medication**
Remove medication from user's list.
**Endpoint:** `DELETE /medications/:id`
**Response:**
```json
{
"success": true,
"message": "Medication deleted successfully"
}
```
---
## ⏰ Reminder Management
### **Add Custom Reminder**
Create a custom reminder.
**Endpoint:** `POST /reminders`
**Request Body:**
```json
{
"title": "Doctor Appointment",
"message": "Annual checkup with Dr. Smith",
"scheduledFor": "2025-09-15T14:00:00Z",
"recurrence": "yearly"
}
```
**Response:**
```json
{
"_id": "reminder-uuid",
"title": "Doctor Appointment",
"message": "Annual checkup with Dr. Smith",
"scheduledFor": "2025-09-15T14:00:00Z",
"recurrence": "yearly",
"userId": "user-uuid",
"isActive": true
}
```
---
### **Get Reminders**
Retrieve user's reminders.
**Endpoint:** `GET /reminders`
**Query Parameters:**
- `date` (string) - Filter by specific date (YYYY-MM-DD)
- `active` (boolean) - Filter active reminders only
**Response:**
```json
[
{
"_id": "reminder-uuid",
"title": "Doctor Appointment",
"message": "Annual checkup with Dr. Smith",
"scheduledFor": "2025-09-15T14:00:00Z",
"recurrence": "yearly",
"isActive": true
}
]
```
---
## 📊 Dose Tracking
### **Record Taken Dose**
Mark a dose as taken.
**Endpoint:** `POST /doses/taken`
**Request Body:**
```json
{
"medicationId": "medication-uuid",
"scheduledTime": "2025-09-05T08:00:00Z",
"takenAt": "2025-09-05T08:15:00Z",
"notes": "Took with breakfast"
}
```
**Response:**
```json
{
"success": true,
"dose": {
"id": "medication-uuid-2025-09-05",
"medicationId": "medication-uuid",
"scheduledTime": "2025-09-05T08:00:00Z",
"takenAt": "2025-09-05T08:15:00Z",
"status": "TAKEN",
"notes": "Took with breakfast"
}
}
```
---
### **Get Dose History**
Retrieve dose history for analytics.
**Endpoint:** `GET /doses`
**Query Parameters:**
- `medicationId` (string) - Filter by medication
- `startDate` (string) - Start date (YYYY-MM-DD)
- `endDate` (string) - End date (YYYY-MM-DD)
- `status` (string) - Filter by status (TAKEN, MISSED, UPCOMING)
**Response:**
```json
{
"doses": [
{
"id": "medication-uuid-2025-09-05",
"medicationId": "medication-uuid",
"scheduledTime": "2025-09-05T08:00:00Z",
"takenAt": "2025-09-05T08:15:00Z",
"status": "TAKEN"
}
],
"stats": {
"totalDoses": 30,
"takenDoses": 28,
"missedDoses": 2,
"adherenceRate": 93.3
}
}
```
---
## 👑 Admin Endpoints
### **Get All Users**
Retrieve all users (admin only).
**Endpoint:** `GET /admin/users`
**Query Parameters:**
- `status` (string) - Filter by status
- `role` (string) - Filter by role
- `page` (number) - Pagination page
- `limit` (number) - Items per page
**Response:**
```json
{
"users": [
{
"_id": "user-uuid",
"email": "user@example.com",
"username": "JohnDoe",
"status": "ACTIVE",
"role": "USER",
"emailVerified": true,
"createdAt": "2025-09-05T12:00:00Z",
"lastLoginAt": "2025-09-05T15:30:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"pages": 8
}
}
```
---
### **Update User Status**
Change user account status (admin only).
**Endpoint:** `PUT /admin/users/:id/status`
**Request Body:**
```json
{
"status": "SUSPENDED"
}
```
**Response:**
```json
{
"success": true,
"user": {
"_id": "user-uuid",
"status": "SUSPENDED"
}
}
```
---
### **Delete User**
Delete user account (admin only).
**Endpoint:** `DELETE /admin/users/:id`
**Response:**
```json
{
"success": true,
"message": "User deleted successfully"
}
```
---
## 📈 Analytics
### **User Statistics**
Get user's medication adherence statistics.
**Endpoint:** `GET /analytics/stats`
**Query Parameters:**
- `period` (string) - Time period (7d, 30d, 90d, 1y)
**Response:**
```json
{
"adherence": {
"overall": 92.5,
"trend": "improving",
"streak": 7
},
"medications": [
{
"medicationId": "medication-uuid",
"name": "Aspirin",
"taken": 28,
"missed": 2,
"adherence": 93.3
}
],
"dailyStats": [
{
"date": "2025-09-05",
"adherence": 100,
"totalDoses": 3,
"takenDoses": 3
}
]
}
```
---
## 🔧 User Settings
### **Get User Settings**
Retrieve user preferences.
**Endpoint:** `GET /settings`
**Response:**
```json
{
"notifications": {
"email": true,
"push": false,
"reminderSound": true
},
"preferences": {
"theme": "dark",
"timezone": "UTC-5",
"dateFormat": "MM/DD/YYYY"
},
"privacy": {
"shareStats": false,
"anonymousUsage": true
}
}
```
---
### **Update User Settings**
Update user preferences.
**Endpoint:** `PUT /settings`
**Request Body:**
```json
{
"notifications": {
"email": false,
"push": true
},
"preferences": {
"theme": "light"
}
}
```
**Response:**
```json
{
"success": true,
"settings": {
"notifications": {
"email": false,
"push": true,
"reminderSound": true
},
"preferences": {
"theme": "light",
"timezone": "UTC-5",
"dateFormat": "MM/DD/YYYY"
}
}
}
```
---
## 📁 File Upload
### **Upload Avatar**
Upload user avatar image.
**Endpoint:** `POST /upload/avatar`
**Request:** Multipart form data
- `avatar` (file) - Image file (JPEG, PNG, max 2MB)
**Response:**
```json
{
"success": true,
"avatarUrl": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
}
```
---
## 🔍 Search
### **Search Medications**
Search for medications by name.
**Endpoint:** `GET /search/medications`
**Query Parameters:**
- `q` (string) - Search query
- `limit` (number) - Max results
**Response:**
```json
{
"results": [
{
"name": "Aspirin",
"commonDosages": ["100mg", "325mg", "500mg"],
"category": "Pain Reliever"
}
]
}
```
---
## ❌ Error Responses
### **Error Format**
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": {
"email": "Invalid email format",
"password": "Password too weak"
}
}
}
```
### **Common Error Codes**
- `VALIDATION_ERROR` (400) - Invalid input data
- `UNAUTHORIZED` (401) - Authentication required
- `FORBIDDEN` (403) - Insufficient permissions
- `NOT_FOUND` (404) - Resource not found
- `CONFLICT` (409) - Resource already exists
- `RATE_LIMITED` (429) - Too many requests
- `INTERNAL_ERROR` (500) - Server error
---
## 📊 Rate Limiting
### **Limits**
- Authentication endpoints: 5 requests/minute
- General API: 100 requests/minute
- Upload endpoints: 10 requests/minute
### **Headers**
```http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1693929600
```
---
## 🔒 Security
### **Authentication**
- JWT tokens with 24-hour expiration
- Refresh tokens for automatic renewal
- Secure password hashing with bcrypt
### **Authorization**
- Role-based access control (USER, ADMIN)
- Resource-level permissions
- Account status validation
### **Data Protection**
- Input validation and sanitization
- SQL injection prevention
- XSS protection
- CORS configuration
---
## 📚 Additional Resources
- [Postman Collection](./postman-collection.json)
- [OpenAPI Specification](./openapi.yaml)
- [SDK Documentation](./sdk-docs.md)
- [Integration Examples](./examples/)
+162
View File
@@ -0,0 +1,162 @@
# Security Guide
## 🔐 Security Best Practices for Medication Reminder App
### **Password Security**
#### **Password Requirements**
- Minimum 8 characters
- Must contain uppercase and lowercase letters
- Must contain at least one number
- Must contain at least one special character
- Cannot be common passwords (password123, admin, etc.)
#### **Password Hashing**
- Uses bcrypt with salt rounds for secure password storage
- Passwords are never stored in plain text
- Password verification happens through secure hash comparison
### **Authentication Security**
#### **Session Management**
- JWT-like token system for user sessions
- Tokens have expiration times
- Secure token storage and transmission
- Automatic session cleanup on logout
#### **Email Verification**
- All new accounts require email verification
- Verification tokens are time-limited
- Prevents unauthorized account creation
- Uses cryptographically secure random tokens
#### **OAuth Security**
- Supports Google and GitHub OAuth
- Secure OAuth flow implementation
- No password storage for OAuth users
- Account linking prevention for security
### **Environment Security**
#### **Environment Variables**
- Never commit `.env` files to version control
- Use separate environment files for different deployments
- Rotate credentials regularly
- Use strong, unique passwords for each environment
#### **Docker Security**
- Non-root user for application execution
- Multi-stage builds to minimize attack surface
- Health checks for service monitoring
- Isolated network for services
### **Database Security**
#### **CouchDB Security**
- Admin authentication required
- Database-level access control
- SSL/TLS encryption for production
- Regular backup and security updates
#### **Data Protection**
- User data isolation by user ID
- Input validation and sanitization
- Protection against injection attacks
- Secure data deletion capabilities
### **Production Security Checklist**
#### **Before Deployment**
- [ ] Change default admin password
- [ ] Configure strong CouchDB credentials
- [ ] Set up Mailgun with proper API keys
- [ ] Enable SSL/TLS certificates
- [ ] Configure firewall rules
- [ ] Set up monitoring and logging
#### **Regular Security Tasks**
- [ ] Rotate credentials monthly
- [ ] Update dependencies regularly
- [ ] Monitor logs for suspicious activity
- [ ] Backup databases securely
- [ ] Review user access permissions
- [ ] Test disaster recovery procedures
### **Incident Response**
#### **Security Breach Protocol**
1. **Immediate Response**
- Disable affected accounts
- Change all credentials
- Review access logs
- Document the incident
2. **Investigation**
- Identify breach source
- Assess data exposure
- Notify affected users
- Implement fixes
3. **Recovery**
- Restore from secure backups
- Update security measures
- Monitor for further issues
- Conduct post-incident review
### **Compliance Considerations**
#### **Data Privacy**
- User data minimization
- Right to data deletion
- Transparent privacy policy
- Secure data export capabilities
#### **Healthcare Compliance**
- HIPAA considerations for health data
- Secure medication information handling
- Audit trail capabilities
- Data retention policies
### **Security Monitoring**
#### **Logging**
- Authentication attempts
- Failed login monitoring
- Admin actions tracking
- Database access logging
#### **Alerting**
- Multiple failed login attempts
- Admin privilege escalation
- Unusual data access patterns
- System health issues
### **Emergency Contacts**
#### **Security Issues**
- Development Team: security@your-domain.com
- System Administrator: admin@your-domain.com
- Emergency Response: +1-XXX-XXX-XXXX
#### **Third-party Services**
- Mailgun Support: support@mailgun.com
- CouchDB Security: security@apache.org
- Docker Security: security@docker.com
+246
View File
@@ -0,0 +1,246 @@
# Code Quality and Formatting Setup
This project includes comprehensive code quality tools and pre-commit hooks to maintain consistent code standards.
## Tools Configured
### Pre-commit Hooks
- **File formatting**: Trailing whitespace, end-of-file fixes, line ending normalization
- **Security**: Private key detection, secrets scanning with detect-secrets
- **Linting**: ESLint for TypeScript/JavaScript, Hadolint for Docker, ShellCheck for scripts
- **Type checking**: TypeScript compilation checks
- **Formatting**: Prettier for code formatting, Markdownlint for documentation
### Code Formatters
- **Prettier**: Handles JavaScript, TypeScript, JSON, YAML, Markdown, CSS, SCSS, HTML formatting
- **ESLint**: TypeScript/JavaScript linting with comprehensive rules and React hooks support
- **EditorConfig**: Consistent coding styles across editors
### Security Tools
- **detect-secrets**: Prevents secrets from being committed to the repository
- **Private key detection**: Automatically detects and blocks private keys
## Setup
Run the setup script to install all tools and configure pre-commit hooks:
```bash
./scripts/setup-pre-commit.sh
```
Alternatively, install manually:
```bash
# Install dependencies
bun install
# Install pre-commit in Python virtual environment
python -m venv .venv
source .venv/bin/activate # or .venv/Scripts/activate on Windows
pip install pre-commit detect-secrets
# Install pre-commit hooks
pre-commit install
# Create secrets baseline (if it doesn't exist)
detect-secrets scan --baseline .secrets.baseline
```
## Usage
### Automatic (Recommended)
Pre-commit hooks will automatically run on every commit, ensuring:
- Code is properly formatted
- Linting rules are followed
- Type checking passes
- No secrets are committed
### Manual Commands
```bash
# Format all files
bun run format
# Check formatting without fixing
bun run format:check
# Lint TypeScript/JavaScript files
bun run lint
# Lint with auto-fix
bun run lint:fix
# Type checking
bun run type-check
# Run pre-commit hook
bun run pre-commit
# Run all pre-commit hooks manually (using virtual environment)
/home/will/Code/meds/.venv/bin/pre-commit run --all-files
# Run specific hook
/home/will/Code/meds/.venv/bin/pre-commit run prettier --all-files
# Update pre-commit hook versions
/home/will/Code/meds/.venv/bin/pre-commit autoupdate
```
## Configuration Files
- `.pre-commit-config.yaml` - Pre-commit hooks configuration with comprehensive security and quality checks
- `.prettierrc` - Prettier formatting rules with TypeScript/React optimizations
- `.prettierignore` - Files to ignore for Prettier formatting
- `.editorconfig` - Editor configuration for consistent coding styles across IDEs
- `eslint.config.cjs` - ESLint linting rules with TypeScript and React hooks support
- `.markdownlint.json` - Markdown linting configuration for documentation quality
- `.secrets.baseline` - Baseline for detect-secrets security scanning
- `scripts/setup-pre-commit.sh` - Automated setup script for all tools
- `docs/CODE_QUALITY.md` - This documentation file
## Pre-commit Hook Details
The following hooks run automatically on every commit:
### File Quality Hooks
- `trailing-whitespace` - Removes trailing whitespace
- `end-of-file-fixer` - Ensures files end with newline
- `check-yaml` - Validates YAML syntax
- `check-json` - Validates JSON syntax
- `check-toml` - Validates TOML syntax
- `check-xml` - Validates XML syntax
- `check-merge-conflict` - Prevents merge conflict markers
- `check-added-large-files` - Prevents large files from being committed
- `check-case-conflict` - Prevents case conflicts on case-insensitive filesystems
- `check-symlinks` - Validates symlinks
- `mixed-line-ending` - Ensures consistent line endings (LF)
### Security Hooks
- `detect-private-key` - Prevents private keys from being committed
- `detect-secrets` - Scans for secrets using baseline comparison
### Code Quality Hooks
- `prettier` - Formats JavaScript, TypeScript, JSON, YAML, Markdown, CSS, SCSS, HTML
- `eslint` - Lints TypeScript/JavaScript with auto-fix
- `tsc` - TypeScript type checking
### Infrastructure Hooks
- `hadolint-docker` - Lints Dockerfile files
- `shellcheck` - Lints shell scripts
- `markdownlint` - Lints and formats Markdown files
## IDE Integration
### VS Code (Recommended)
Install these extensions for optimal integration:
- **Prettier - Code formatter** (`esbenp.prettier-vscode`)
- **ESLint** (`dbaeumer.vscode-eslint`)
- **EditorConfig for VS Code** (`editorconfig.editorconfig`)
### Settings for VS Code
Add these settings to your VS Code `settings.json`:
```json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
}
```
### Other IDEs
Most modern IDEs support EditorConfig, Prettier, and ESLint through plugins:
- **WebStorm/IntelliJ**: Built-in support for most tools
- **Vim/Neovim**: Use coc.nvim or native LSP with appropriate plugins
- **Sublime Text**: Install Package Control packages for each tool
## Customization
### Prettier
Edit `.prettierrc` to modify formatting rules.
### ESLint
Edit `eslint.config.cjs` to add or modify linting rules.
### Pre-commit
Edit `.pre-commit-config.yaml` to add, remove, or modify hooks.
## Troubleshooting
### Pre-commit hooks failing
```bash
# Skip hooks temporarily (not recommended)
git commit --no-verify -m "commit message"
# Fix issues and try again
bun run lint:fix
bun run format
git add .
git commit -m "commit message"
```
### Update hook versions
```bash
/home/will/Code/meds/.venv/bin/pre-commit autoupdate
```
### Clear pre-commit cache
```bash
/home/will/Code/meds/.venv/bin/pre-commit clean
```
### Secrets detection issues
```bash
# Update secrets baseline
/home/will/Code/meds/.venv/bin/detect-secrets scan --update .secrets.baseline
# Audit detected secrets
/home/will/Code/meds/.venv/bin/detect-secrets audit .secrets.baseline
```
### Python virtual environment issues
```bash
# Recreate virtual environment
rm -rf .venv
python -m venv .venv
source .venv/bin/activate
pip install pre-commit detect-secrets
/home/will/Code/meds/.venv/bin/pre-commit install
```
### ESLint configuration issues
If you encounter TypeScript project configuration errors:
```bash
# Ensure tsconfig.json is properly configured
bun run type-check
# Check ESLint configuration
npx eslint --print-config index.tsx
```
+148
View File
@@ -0,0 +1,148 @@
# 🔐 Security Changes Summary
## Overview
We have systematically removed all hardcoded credentials from the RxMinder application and replaced them with secure defaults and environment variables.
## ✅ Changes Made
### 1. Kubernetes Configuration
- **`k8s/couchdb-secret.yaml`**: Converted to template with secure base64-encoded defaults
- **`k8s/db-seed-job.yaml`**: Now uses environment variables from secrets instead of hardcoded credentials
### 2. Docker Configuration
- **`docker/Dockerfile`**: Updated default password arguments to secure values
- **`docker/docker-compose.yaml`**: All password environment variables use secure fallbacks
- **`docker/docker-bake.hcl`**: Updated variable defaults to secure passwords
### 3. Shell Scripts
Updated all deployment and build scripts with secure password fallbacks:
- `scripts/setup.sh`
- `scripts/deploy.sh`
- `scripts/validate-deployment.sh`
- `scripts/buildx-helper.sh`
- `scripts/gitea-deploy.sh`
- `scripts/gitea-helper.sh`
- `scripts/seed-production.js`
- `rename-app.sh`
### 4. CI/CD Workflows
- **`.github/workflows/build-deploy.yml`**: Updated fallback passwords to secure values
- **`.gitea/workflows/ci-cd.yml`**: Updated fallback passwords to secure values
- **`.gitea/docker-compose.ci.yml`**: Updated test database passwords
- **`.gitea/gitea-bake.hcl`**: Updated default password variables
### 5. Environment Files
- **`.env.example`**: Updated with secure default passwords and documentation
- **`.env.production`**: Updated with secure default passwords
- **`test.env`**: Updated test credentials to secure values
### 6. Documentation
- **`README.md`**: Updated default admin credentials documentation
- **`SECURITY.md`**: Created comprehensive security guide with checklists
- **`.gitea/README.md`**: Updated documentation
- **`GITEA_SETUP.md`**: Updated setup instructions
## 🛡️ Security Improvements
### Before
- Hardcoded `admin123!` and `password` throughout configuration files
- Weak default passwords in CI/CD systems
- No security documentation or guidelines
### After
- All passwords use environment variables or Kubernetes secrets
- Secure fallback passwords (`change-this-secure-password`)
- Comprehensive security documentation and checklists
- CI/CD systems use repository secrets with secure fallbacks
## 🔄 Required Actions
**CRITICAL**: Before production deployment, you must:
1. **Update Kubernetes Secrets**:
```bash
# Update k8s/couchdb-secret.yaml with your own secure base64-encoded credentials
echo -n "your-secure-password" | base64
```
2. **Update Environment Variables**:
```bash
# Update .env and .env.production with your secure passwords
COUCHDB_PASSWORD=your-very-secure-password
VITE_COUCHDB_PASSWORD=your-very-secure-password
```
3. **Configure CI/CD Secrets**:
- Set `VITE_COUCHDB_PASSWORD` in repository secrets
- Set `GITEA_TOKEN` / `GITHUB_TOKEN` for registry authentication
4. **Review Security Checklist**:
- Follow the checklist in `SECURITY.md`
- Use strong passwords (16+ characters, mixed case, numbers, symbols)
- Enable TLS/SSL for all external communications
## 📝 Files Modified
### Configuration Files (11)
- `k8s/couchdb-secret.yaml`
- `k8s/db-seed-job.yaml`
- `docker/Dockerfile`
- `docker/docker-compose.yaml`
- `docker/docker-bake.hcl`
- `.env.example`
- `.env.production`
- `test.env`
- `.github/workflows/build-deploy.yml`
- `.gitea/workflows/ci-cd.yml`
- `.gitea/docker-compose.ci.yml`
- `.gitea/gitea-bake.hcl`
### Scripts (8)
- `scripts/setup.sh`
- `scripts/deploy.sh`
- `scripts/validate-deployment.sh`
- `scripts/buildx-helper.sh`
- `scripts/gitea-deploy.sh`
- `scripts/gitea-helper.sh`
- `scripts/seed-production.js`
- `rename-app.sh`
### Documentation (5)
- `README.md`
- `SECURITY.md` (created)
- `SECURITY_CHANGES.md` (this file)
- `.gitea/README.md`
- `GITEA_SETUP.md`
## ✅ Verification
To verify no hardcoded credentials remain:
```bash
# Check for insecure passwords (should return only secure defaults)
grep -r "admin123\|password[^-]\|testpassword" --include="*.yaml" --include="*.yml" --include="*.sh" --include="*.env" --include="*.js" --include="*.hcl" .
# The only matches should be:
# - "change-this-secure-password" (secure fallback)
# - "test-secure-password" (secure test credentials)
# - Test files (acceptable for testing)
```
## 🎯 Result
RxMinder is now production-ready with secure credential management. All sensitive data is properly externalized to environment variables and Kubernetes secrets, with comprehensive documentation to guide secure deployment.
+119
View File
@@ -0,0 +1,119 @@
# Docker Buildx Migration Complete ✅
Your project has been successfully migrated to use Docker Buildx for multi-platform container builds!
## What's New
### 🚀 Multi-Platform Support
- **AMD64 (x86_64)**: Traditional Intel/AMD processors
- **ARM64 (aarch64)**: Apple Silicon, AWS Graviton, Raspberry Pi 4+
### 🛠️ New Tools & Scripts
#### **buildx-helper.sh** - Comprehensive buildx management
```bash
# Setup buildx builder (one-time setup)
./scripts/buildx-helper.sh setup
# Build for local platform only (faster development)
./scripts/buildx-helper.sh build-local
# Build for multiple platforms
./scripts/buildx-helper.sh build-multi
# Build and push to registry
./scripts/buildx-helper.sh push docker.io/username latest
# Build using Docker Bake (advanced)
./scripts/buildx-helper.sh bake
# Inspect builder capabilities
./scripts/buildx-helper.sh inspect
# Cleanup builder
./scripts/buildx-helper.sh cleanup
```
#### **Package.json Scripts**
```bash
# Quick access via npm/bun scripts
bun run docker:setup # Setup buildx
bun run docker:build # Multi-platform build
bun run docker:build-local # Local platform only
bun run docker:bake # Advanced bake build
bun run docker:inspect # Inspect builder
bun run docker:cleanup # Cleanup
```
### 📁 New Files Added
1. **`docker/docker-bake.hcl`** - Advanced buildx configuration
2. **`scripts/buildx-helper.sh`** - Buildx management script
3. **`.github/workflows/build-deploy.yml`** - CI/CD with buildx
### 🔧 Updated Files
1. **`docker/Dockerfile`** - Added NODE_ENV build arg
2. **`docker/docker-compose.yaml`** - Added multi-platform support
3. **`scripts/setup.sh`** - Updated to use buildx
4. **`scripts/validate-deployment.sh`** - Updated to use buildx
5. **`scripts/deploy.sh`** - Updated to use buildx
6. **`docker/README.md`** - Added buildx documentation
7. **`package.json`** - Added docker scripts
## Benefits
### 🎯 **Better Performance**
- Enhanced caching with BuildKit
- Parallel multi-platform builds
- Faster incremental builds
### 🌍 **Cross-Platform Compatibility**
- Deploy on ARM-based servers (AWS Graviton, Apple Silicon)
- Support for various architectures out of the box
- Future-proof for emerging platforms
### 🔒 **Enhanced Security**
- Supply chain attestations (SBOM, provenance)
- Secure multi-stage builds
- Container image signing support
### 🔄 **CI/CD Ready**
- GitHub Actions workflow included
- Registry caching optimized
- Automated multi-platform pushes
## Next Steps
1. **Test the setup**:
```bash
bun run docker:setup
bun run docker:build-local
```
2. **Configure registry** (optional):
```bash
./scripts/buildx-helper.sh push ghcr.io/yourusername latest
```
3. **Enable GitHub Actions** (optional):
- Push to GitHub to trigger the workflow
- Configure registry secrets if needed
## Migration Notes
- ✅ Backwards compatible with existing Docker commands
- ✅ Docker Compose still works as before
- ✅ All existing scripts updated to use buildx
- ✅ No breaking changes to development workflow
Your project now supports cutting-edge multi-platform container builds! 🎉
@@ -0,0 +1,117 @@
# NodeJS-Native Pre-commit Setup Migration
## Overview
Successfully migrated from Python's `pre-commit` framework to a 100% NodeJS-native solution using Husky and lint-staged.
## What Was Removed
- `.pre-commit-config.yaml` - Python pre-commit configuration
- `.secrets.baseline` - Python detect-secrets baseline
- Python `pre-commit` dependency requirement
- Python `detect-secrets` dependency requirement
## What Was Added
### Core Tools
- **Husky v9** - Modern Git hooks manager
- **lint-staged** - Run tools on staged files only (performance optimization)
### NodeJS Alternatives for Previous Python Tools
| Python Tool | NodeJS Alternative | Purpose |
| ------------------ | --------------------------- | -------------------------------------- |
| `pre-commit-hooks` | Built into Husky hook | File checks, trailing whitespace, etc. |
| `mirrors-prettier` | `prettier` (direct) | Code formatting |
| `eslint` (local) | `eslint` (direct) | JavaScript/TypeScript linting |
| `tsc` (local) | `typescript` (direct) | Type checking |
| `hadolint` | `dockerfilelint` | Dockerfile linting |
| `shellcheck-py` | Custom shell checks in hook | Shell script validation |
| `markdownlint-cli` | `markdownlint-cli2` | Markdown linting |
| `detect-secrets` | `@secretlint/node` | Secret detection |
## New Package.json Scripts
```json
{
"lint:markdown": "markdownlint-cli2 \"**/*.md\"",
"lint:markdown:fix": "markdownlint-cli2 --fix \"**/*.md\"",
"lint:docker": "dockerfilelint docker/Dockerfile",
"check:secrets": "secretlint \"**/*\"",
"check:editorconfig": "eclint check .",
"fix:editorconfig": "eclint fix ."
}
```
## Enhanced lint-staged Configuration
```json
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,yaml,yml,md,css,scss,html}": ["prettier --write"],
"*.md": ["markdownlint-cli2 --fix"],
"docker/Dockerfile": ["dockerfilelint"],
"*": ["eclint fix"]
}
}
```
## Husky Hooks
### `.husky/pre-commit`
- Runs lint-staged for efficient file-specific checks
- TypeScript type checking
- Large file detection (>500KB)
- Merge conflict marker detection
- Basic private key detection
### `.husky/commit-msg`
- Basic commit message validation
## Key Benefits
1. **No Python Dependencies** - Pure NodeJS ecosystem
2. **Better Performance** - lint-staged only processes changed files
3. **Simpler Setup** - No Python virtual environment needed
4. **Consistent Toolchain** - Everything uses npm/bun
5. **Modern Tooling** - Latest versions of all tools
6. **Easier CI/CD** - Same tools in development and CI
## Usage
### Setup
```bash
./scripts/setup-pre-commit.sh
```
### Manual Commands
```bash
bun run format # Format all files
bun run lint:fix # Fix linting issues
bun run lint:markdown:fix # Fix markdown issues
bun run check:secrets # Check for secrets
bun run type-check # TypeScript validation
```
### What Happens on Commit
1. **lint-staged** processes only changed files:
- ESLint auto-fix + Prettier for JS/TS files
- Prettier for JSON/YAML/MD/CSS files
- Markdownlint for Markdown files
- Dockerfilelint for Dockerfile
- EditorConfig fixes for all files
2. **TypeScript** type checking on entire project
3. **Security checks** for large files, merge conflicts, private keys
## Migration Complete ✅
The project now uses a modern, efficient, NodeJS-native pre-commit setup that provides the same (and better) functionality as the previous Python-based solution.
@@ -0,0 +1,319 @@
# 🎯 Complete Template-Based Configuration Summary
## Overview
RxMinder now supports **complete template-based configuration** with environment variables for all aspects of deployment, making it truly portable and customizable across any environment.
## 🔧 Configuration Variables
### Core Application
- **`APP_NAME`** - Application name used in all Kubernetes resources
- **`DOCKER_IMAGE`** - Container image to deploy
- **`INGRESS_HOST`** - External hostname for ingress
### Database Configuration
- **`COUCHDB_USER`** - Database username
- **`COUCHDB_PASSWORD`** - Database password (automatically base64 encoded)
### Storage Configuration
- **`STORAGE_CLASS`** - Kubernetes StorageClass for persistent volumes
- **`STORAGE_SIZE`** - Storage allocation for database
### Optional Configuration
- **`VITE_COUCHDB_URL`** - CouchDB URL for frontend
- **`APP_BASE_URL`** - Application base URL
## 📁 Template Files
All Kubernetes manifests are now template-based:
1. **`k8s/couchdb-secret.yaml.template`** - Database credentials (uses `stringData`)
2. **`k8s/couchdb-pvc.yaml.template`** - Persistent volume claim with configurable storage
3. **`k8s/couchdb-service.yaml.template`** - Database service with dynamic naming
4. **`k8s/couchdb-statefulset.yaml.template`** - Database deployment with storage config
5. **`k8s/configmap.yaml.template`** - Application configuration
6. **`k8s/frontend-deployment.yaml.template`** - Frontend with configurable image
7. **`k8s/frontend-service.yaml.template`** - Frontend service with dynamic naming
8. **`k8s/ingress.yaml.template`** - Ingress with configurable hostname
## 🎭 Environment Examples
### Development Environment
```bash
# .env
APP_NAME=rxminder-dev
DOCKER_IMAGE=localhost:5000/rxminder:dev
COUCHDB_USER=admin
COUCHDB_PASSWORD=dev-password-123
INGRESS_HOST=rxminder-dev.local
STORAGE_CLASS=local-path
STORAGE_SIZE=5Gi
```
### Staging Environment
```bash
# .env.staging
APP_NAME=rxminder-staging
DOCKER_IMAGE=registry.company.com/rxminder:staging
COUCHDB_USER=admin
COUCHDB_PASSWORD=staging-secure-password
INGRESS_HOST=staging.rxminder.company.com
STORAGE_CLASS=longhorn
STORAGE_SIZE=10Gi
```
### Production Environment
```bash
# .env.production
APP_NAME=rxminder
DOCKER_IMAGE=registry.company.com/rxminder:v1.2.0
COUCHDB_USER=admin
COUCHDB_PASSWORD=ultra-secure-production-password
INGRESS_HOST=rxminder.company.com
STORAGE_CLASS=fast-ssd
STORAGE_SIZE=50Gi
```
### Cloud Provider Examples
#### AWS EKS
```bash
APP_NAME=rxminder
DOCKER_IMAGE=123456789012.dkr.ecr.us-west-2.amazonaws.com/rxminder:v1.0.0
STORAGE_CLASS=gp3
STORAGE_SIZE=20Gi
INGRESS_HOST=rxminder.aws.company.com
```
#### Google GKE
```bash
APP_NAME=rxminder
DOCKER_IMAGE=gcr.io/project-id/rxminder:stable
STORAGE_CLASS=pd-ssd
STORAGE_SIZE=20Gi
INGRESS_HOST=rxminder.gcp.company.com
```
#### Azure AKS
```bash
APP_NAME=rxminder
DOCKER_IMAGE=myregistry.azurecr.io/rxminder:production
STORAGE_CLASS=managed-premium
STORAGE_SIZE=20Gi
INGRESS_HOST=rxminder.azure.company.com
```
## 🚀 Deployment Workflow
### Simple 3-Step Process
```bash
# 1. Configure environment
cp .env.example .env
nano .env # Edit with your values
# 2. Deploy with single command
./scripts/k8s-deploy-template.sh deploy
# 3. Check status
./scripts/k8s-deploy-template.sh status
```
### Advanced Deployment Options
```bash
# Deploy with specific environment file
ENV_FILE=.env.production ./scripts/k8s-deploy-template.sh deploy
# Override specific variables
export DOCKER_IMAGE=my-registry.com/rxminder:hotfix
./scripts/k8s-deploy-template.sh deploy
# Cleanup deployment
./scripts/k8s-deploy-template.sh delete
```
## 🎯 Generated Resources
### Before (Hardcoded)
```yaml
# Old approach - static values
metadata:
name: frontend
labels:
app: rxminder
spec:
containers:
- image: gitea-http.taildb3494.ts.net/will/meds:latest
volumeClaimTemplates:
- spec:
storageClassName: longhorn
resources:
requests:
storage: 1Gi
```
### After (Template-Based)
```yaml
# Template approach - dynamic values
metadata:
name: ${APP_NAME}-frontend
labels:
app: ${APP_NAME}
spec:
containers:
- image: ${DOCKER_IMAGE}
volumeClaimTemplates:
- spec:
storageClassName: ${STORAGE_CLASS}
resources:
requests:
storage: ${STORAGE_SIZE}
```
### Deployed Result
```yaml
# After envsubst processing
metadata:
name: rxminder-frontend
labels:
app: rxminder
spec:
containers:
- image: registry.company.com/rxminder:v1.0.0
volumeClaimTemplates:
- spec:
storageClassName: fast-ssd
resources:
requests:
storage: 20Gi
```
## 🔄 CI/CD Integration
### GitHub Actions
```yaml
name: Deploy to Kubernetes
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Production
env:
APP_NAME: rxminder
DOCKER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
COUCHDB_PASSWORD: ${{ secrets.COUCHDB_PASSWORD }}
INGRESS_HOST: rxminder.company.com
STORAGE_CLASS: fast-ssd
STORAGE_SIZE: 50Gi
run: |
./scripts/k8s-deploy-template.sh deploy
```
### GitLab CI
```yaml
deploy_production:
stage: deploy
variables:
APP_NAME: rxminder
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
STORAGE_CLASS: longhorn
STORAGE_SIZE: 20Gi
script:
- ./scripts/k8s-deploy-template.sh deploy
only:
- tags
```
## 💡 Benefits Achieved
### 🎯 Flexibility
- ✅ **Multi-environment** deployments with same codebase
- ✅ **Cloud-agnostic** configuration
- ✅ **Registry-agnostic** image deployment
- ✅ **Storage-flexible** for any Kubernetes cluster
### 🔒 Security
- ✅ **No hardcoded credentials** in version control
- ✅ **Environment-specific secrets** management
- ✅ **Automatic base64 encoding** via Kubernetes `stringData`
- ✅ **Credential validation** before deployment
### 🛠️ Developer Experience
- ✅ **Single command deployment** across all environments
- ✅ **Clear documentation** of all configuration options
- ✅ **Environment validation** with helpful error messages
- ✅ **Template debugging** with manual `envsubst` testing
### 🏢 Enterprise Ready
- ✅ **Production-grade** configuration management
- ✅ **CI/CD integration** ready
- ✅ **Multi-cluster** deployment support
- ✅ **Disaster recovery** friendly with consistent configs
## 🎪 Use Cases
### Multi-Tenant Deployment
```bash
# Tenant A
export APP_NAME=rxminder-tenant-a
export INGRESS_HOST=tenant-a.rxminder.company.com
./scripts/k8s-deploy-template.sh deploy
# Tenant B
export APP_NAME=rxminder-tenant-b
export INGRESS_HOST=tenant-b.rxminder.company.com
./scripts/k8s-deploy-template.sh deploy
```
### Blue-Green Deployment
```bash
# Blue environment
export APP_NAME=rxminder-blue
export DOCKER_IMAGE=registry.com/rxminder:v1.0.0
./scripts/k8s-deploy-template.sh deploy
# Green environment
export APP_NAME=rxminder-green
export DOCKER_IMAGE=registry.com/rxminder:v2.0.0
./scripts/k8s-deploy-template.sh deploy
```
### Development Branches
```bash
# Feature branch deployment
export APP_NAME=rxminder-feature-auth
export DOCKER_IMAGE=registry.com/rxminder:feature-auth
export INGRESS_HOST=auth-feature.rxminder.dev.company.com
./scripts/k8s-deploy-template.sh deploy
```
This **complete template-based approach** makes RxMinder the most **flexible**, **secure**, and **maintainable** medication reminder application for Kubernetes deployments!
+89
View File
@@ -0,0 +1,89 @@
# Pre-commit and Code Quality Quick Reference
## ✅ What's Been Set Up
Your project now has comprehensive code quality tools configured:
### 🔧 Tools Installed
- **Pre-commit hooks** - Automatically run on every commit
- **Prettier** - Code formatting for JS/TS/JSON/YAML/MD/CSS/SCSS/HTML
- **ESLint** - JavaScript/TypeScript linting with React hooks and comprehensive rules
- **TypeScript** - Type checking with strict configuration
- **Hadolint** - Docker linting
- **ShellCheck** - Shell script linting
- **Markdownlint** - Markdown formatting and quality checks
- **detect-secrets** - Security scanning to prevent secret commits
- **EditorConfig** - Consistent coding styles across editors
### 📁 Configuration Files Created
- `.pre-commit-config.yaml` - Comprehensive pre-commit hooks configuration
- `.prettierrc` - Prettier formatting rules optimized for TypeScript/React
- `.prettierignore` - Files excluded from formatting
- `.editorconfig` - Editor configuration for consistent styles
- `.markdownlint.json` - Markdown linting rules for documentation quality
- `.secrets.baseline` - Security baseline for secret detection
- `scripts/setup-pre-commit.sh` - Automated setup script
- `docs/CODE_QUALITY.md` - Comprehensive documentation
- Python virtual environment (`.venv/`) - Isolated Python tools environment
## 🚀 Quick Commands
```bash
# Format all files
bun run format
# Check formatting (no changes)
bun run format:check
# Lint TypeScript/JavaScript
bun run lint
# Lint with auto-fix
bun run lint:fix
# Type check
bun run type-check
# Run lint-staged (pre-commit formatting)
bun run pre-commit
# Run all pre-commit hooks manually
/home/will/Code/meds/.venv/bin/pre-commit run --all-files
# Update pre-commit hook versions
/home/will/Code/meds/.venv/bin/pre-commit autoupdate
# Update secrets baseline
/home/will/Code/meds/.venv/bin/detect-secrets scan --update .secrets.baseline
```
## 🔄 How It Works
1. **On Commit**: Pre-commit hooks automatically run to:
- Format code with Prettier
- Lint with ESLint
- Check TypeScript types
- Scan for secrets
- Lint Docker and shell files
- Check YAML/JSON syntax
2. **If Issues Found**: Commit is blocked until fixed
3. **Auto-fixes Applied**: Many issues are automatically corrected
## 🛠️ IDE Setup
### VS Code Extensions (Recommended)
- Prettier - Code formatter
- ESLint
- EditorConfig for VS Code
## 📖 Full Documentation
See `docs/CODE_QUALITY.md` for complete setup and customization guide.
---
**Your code will now be automatically formatted and checked on every commit! 🎉**
+70
View File
@@ -0,0 +1,70 @@
const tsParser = require('@typescript-eslint/parser');
const tsPlugin = require('@typescript-eslint/eslint-plugin');
const reactHooksPlugin = require('eslint-plugin-react-hooks');
module.exports = [
{
files: ['**/*.{js,jsx,ts,tsx}'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json',
ecmaFeatures: {
jsx: true,
},
},
globals: {
browser: true,
es2021: true,
node: true,
},
},
plugins: {
'@typescript-eslint': tsPlugin,
'react-hooks': reactHooksPlugin,
},
rules: {
// TypeScript ESLint recommended rules
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/no-empty-function': 'warn',
'@typescript-eslint/prefer-const': 'error',
'@typescript-eslint/no-var-requires': 'error',
// React Hooks rules
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
// General JavaScript/TypeScript rules
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'error',
'no-duplicate-imports': 'error',
'no-unused-expressions': 'error',
'prefer-template': 'error',
'prefer-const': 'error',
'no-var': 'error',
'object-shorthand': 'error',
'prefer-destructuring': ['error', { object: true, array: false }],
// Code style (handled by Prettier, but good to have)
'comma-dangle': ['error', 'es5'],
quotes: ['error', 'single', { avoidEscape: true }],
semi: ['error', 'always'],
},
},
{
files: ['**/*.test.{js,jsx,ts,tsx}', '**/*.spec.{js,jsx,ts,tsx}'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'no-console': 'off',
},
},
];
+29
View File
@@ -0,0 +1,29 @@
// FIX: This file was empty. Added a standard implementation for the useLocalStorage hook.
import { useState, useEffect, Dispatch, SetStateAction } from 'react';
function getStoredValue<T>(key: string, defaultValue: T): T {
if (typeof window === 'undefined') {
return defaultValue;
}
const saved = localStorage.getItem(key);
try {
return saved ? JSON.parse(saved) : defaultValue;
} catch (e) {
return defaultValue;
}
}
export function useLocalStorage<T>(
key: string,
defaultValue: T
): [T, Dispatch<SetStateAction<T>>] {
const [value, setValue] = useState<T>(() =>
getStoredValue(key, defaultValue)
);
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
+50
View File
@@ -0,0 +1,50 @@
import { useState, useEffect } from 'react';
const useSettings = () => {
const [settings, setSettings] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchSettings = async () => {
try {
const response = await fetch('/api/settings');
const data = await response.json();
setSettings(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchSettings();
}, []);
const updateSettings = async newSettings => {
try {
const response = await fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newSettings),
});
const data = await response.json();
setSettings(data);
return data;
} catch (err) {
setError(err);
throw err;
}
};
return {
settings,
loading,
error,
updateSettings,
};
};
export default useSettings;
+43
View File
@@ -0,0 +1,43 @@
import { useEffect, useMemo } from 'react';
import { useLocalStorage } from './useLocalStorage';
type Theme = 'light' | 'dark' | 'system';
export function useTheme() {
const [theme, setTheme] = useLocalStorage<Theme>('theme', 'system');
const systemTheme = useMemo(() => {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
return 'light';
}, []);
const applyTheme = () => {
const themeToApply = theme === 'system' ? systemTheme : theme;
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
root.classList.add(themeToApply);
};
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
if (theme === 'system') {
applyTheme();
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [theme]);
useEffect(() => {
applyTheme();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [theme, systemTheme]);
return { theme, setTheme };
}
+31
View File
@@ -0,0 +1,31 @@
import { useState, useEffect } from 'react';
const useUserData = () => {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUserData = async () => {
try {
const response = await fetch('/api/user/profile');
const data = await response.json();
setUserData(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchUserData();
}, []);
return {
userData,
loading,
error,
};
};
export default useUserData;
+51
View File
@@ -0,0 +1,51 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RxMinder</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-10px)' },
},
},
animation: {
float: 'float 3s ease-in-out infinite',
},
},
},
};
</script>
<script type="importmap">
{
"imports": {
"react": "https://aistudiocdn.com/react@^19.1.1",
"react/": "https://aistudiocdn.com/react@^19.1.1/",
"react-dom/": "https://aistudiocdn.com/react-dom@^19.1.1/"
}
}
</script>
<link rel="stylesheet" href="/index.css" />
</head>
<body class="bg-slate-50 dark:bg-slate-900 antialiased">
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { UserProvider } from './contexts/UserContext';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error('Could not find root element to mount to');
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<UserProvider>
<App />
</UserProvider>
</React.StrictMode>
);
+26
View File
@@ -0,0 +1,26 @@
{
"preset": "ts-jest",
"testEnvironment": "jsdom",
"setupFilesAfterEnv": ["<rootDir>/tests/setup.ts"],
"testMatch": [
"<rootDir>/services/**/__tests__/**/*.test.ts",
"<rootDir>/tests/**/*.test.ts",
"<rootDir>/tests/**/*.test.js"
],
"collectCoverageFrom": [
"services/**/*.ts",
"components/**/*.tsx",
"hooks/**/*.ts",
"utils/**/*.ts",
"!**/*.d.ts",
"!**/__tests__/**"
],
"coverageDirectory": "coverage",
"coverageReporters": ["text", "lcov", "html"],
"moduleNameMapping": {
"^@/(.*)$": "<rootDir>/$1"
},
"transform": {
"^.+\\.tsx?$": "ts-jest"
}
}
+185
View File
@@ -0,0 +1,185 @@
# Kubernetes Manifests for RxMinder
This directory contains Kubernetes manifests and templates for deploying RxMinder on a Kubernetes cluster.
## 🎯 Template-Based Deployment (Recommended)
RxMinder uses **template files** with environment variable substitution for secure, user-friendly deployment.
### **Template Files**
- `couchdb-secret.yaml.template` - Database credentials (uses `stringData` - no base64 encoding needed!)
- `ingress.yaml.template` - Ingress configuration with customizable hostname
- `configmap.yaml.template` - Application configuration
- `frontend-deployment.yaml.template` - Frontend deployment
### **Static Files**
- `couchdb-statefulset.yaml` - StatefulSet for CouchDB database
- `couchdb-service.yaml` - Service to expose CouchDB
- `couchdb-pvc.yaml` - PersistentVolumeClaim for CouchDB storage
- `db-seed-job.yaml` - Job to seed initial database data
- `frontend-service.yaml` - Service to expose frontend
- `hpa.yaml` - Horizontal Pod Autoscaler
- `network-policy.yaml` - Network security policies
## 🚀 Deployment Instructions
### **Option 1: Template-Based Deployment (Recommended)**
```bash
# 1. Copy and configure environment
cp .env.example .env
# 2. Edit .env with your settings
nano .env
# Set: APP_NAME, COUCHDB_PASSWORD, INGRESS_HOST, etc.
# 3. Deploy with templates
./scripts/k8s-deploy-template.sh deploy
# 4. Check status
./scripts/k8s-deploy-template.sh status
# 5. Cleanup (if needed)
./scripts/k8s-deploy-template.sh delete
```
**Benefits of template approach:**
- ✅ No manual base64 encoding required
- ✅ Secure credential management via `.env`
- ✅ Automatic dependency ordering
- ✅ Built-in validation and status checking
- ✅ Easy customization of app name and configuration
### **Option 2: Manual Deployment**
For advanced users who want manual control:
```bash
# Manual template processing (requires envsubst)
envsubst < couchdb-secret.yaml.template > /tmp/couchdb-secret.yaml
envsubst < ingress.yaml.template > /tmp/ingress.yaml
# Apply resources in order
kubectl apply -f /tmp/couchdb-secret.yaml
kubectl apply -f couchdb-pvc.yaml
kubectl apply -f couchdb-service.yaml
kubectl apply -f couchdb-statefulset.yaml
kubectl apply -f configmap.yaml.template
kubectl apply -f frontend-deployment.yaml.template
kubectl apply -f frontend-service.yaml
kubectl apply -f /tmp/ingress.yaml
kubectl apply -f network-policy.yaml
kubectl apply -f hpa.yaml
kubectl apply -f db-seed-job.yaml
```
### **Environment Configuration**
Create `.env` with these required variables:
```bash
# Application Configuration
APP_NAME=rxminder # Customize your app name
INGRESS_HOST=rxminder.yourdomain.com # Your external hostname
# Docker Image Configuration
DOCKER_IMAGE=myregistry.com/rxminder:v1.0.0 # Your container image
# Database Credentials
COUCHDB_USER=admin
COUCHDB_PASSWORD=super-secure-password-123
# Storage Configuration
STORAGE_CLASS=longhorn # Your cluster's storage class
STORAGE_SIZE=20Gi # Database storage allocation
# Optional: Advanced Configuration
VITE_COUCHDB_URL=http://localhost:5984
APP_BASE_URL=https://rxminder.yourdomain.com
```
### **Docker Image Options**
Configure the container image based on your registry:
| Registry Type | Example Image | Use Case |
| ----------------------------- | -------------------------------------------------------------- | ------------------ |
| **Docker Hub** | `rxminder/rxminder:v1.0.0` | Public releases |
| **GitHub Container Registry** | `ghcr.io/username/rxminder:latest` | GitHub integration |
| **AWS ECR** | `123456789012.dkr.ecr.us-west-2.amazonaws.com/rxminder:v1.0.0` | AWS deployments |
| **Google GCR** | `gcr.io/project-id/rxminder:stable` | Google Cloud |
| **Private Registry** | `registry.company.com/rxminder:production` | Enterprise |
| **Local Registry** | `localhost:5000/rxminder:dev` | Development |
### **Storage Class Options**
Choose the appropriate storage class for your environment:
| Platform | Recommended Storage Class | Notes |
| --------------------------- | ------------------------- | -------------------------------- |
| **Raspberry Pi + Longhorn** | `longhorn` | Distributed storage across nodes |
| **k3s** | `local-path` | Single-node local storage |
| **AWS EKS** | `gp3` or `gp2` | General Purpose SSD |
| **Google GKE** | `pd-ssd` | SSD Persistent Disk |
| **Azure AKS** | `managed-premium` | Premium SSD |
**Check available storage classes:**
```bash
kubectl get storageclass
```
```bash
# Kubernetes Ingress Configuration
INGRESS_HOST=app.meds.192.168.1.100.nip.io # Your cluster IP
# For production with custom domain
INGRESS_HOST=meds.yourdomain.com
```
## Credentials
The CouchDB credentials are stored in a Kubernetes secret. **IMPORTANT**: Update the credentials in `couchdb-secret.yaml` with your own secure values before deploying to production.
## Architecture
```
┌─────────────────────────────────────┐
│ Frontend Pod │
│ ┌─────────────────────────────────┐│
│ │ React Application ││
│ │ • Authentication Service ││ ← Embedded in frontend
│ │ • UI Components ││
│ │ • Medication Management ││
│ │ • Email Integration ││
│ └─────────────────────────────────┘│
└─────────────────────────────────────┘
↓ HTTP API
┌─────────────────────────────────────┐
│ CouchDB StatefulSet │
│ • User Data & Authentication │
│ • Medication Records │
│ • Persistent Storage │
└─────────────────────────────────────┘
```
**Key Features:**
- **Monolithic Frontend**: Single container with all functionality
- **Database**: CouchDB running as a StatefulSet with persistent storage
- **Storage**: Longhorn for persistent volume management
- **Networking**: Services configured for proper communication between components
## Raspberry Pi Compatibility
All manifests use multi-architecture images and are optimized for ARM architecture commonly used in Raspberry Pi clusters.
## Important Notes
- The PVC uses Longhorn storage class for persistent storage
- CouchDB runs as a StatefulSet for stable network identifiers
- Frontend is exposed via LoadBalancer service
- CouchDB is exposed via ClusterIP service (internal access only)
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: ${APP_NAME:-rxminder}-config
labels:
app: ${APP_NAME:-rxminder}
data:
NODE_ENV: 'production'
REACT_APP_API_URL: 'http://couchdb-service:5984'
LOG_LEVEL: 'info'
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: ${APP_NAME}-config
labels:
app: ${APP_NAME}
data:
NODE_ENV: "production"
REACT_APP_API_URL: "http://${APP_NAME}-couchdb-service:5984"
LOG_LEVEL: "info"
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ${APP_NAME}-couchdb-pvc
labels:
app: ${APP_NAME}
component: database
spec:
accessModes:
- ReadWriteOnce
storageClassName: ${STORAGE_CLASS}
resources:
requests:
storage: ${STORAGE_SIZE}
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ${APP_NAME}-couchdb-pvc
labels:
app: ${APP_NAME}
component: database
spec:
accessModes:
- ReadWriteOnce
storageClassName: ${STORAGE_CLASS}
resources:
requests:
storage: ${STORAGE_SIZE}
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Secret
metadata:
name: couchdb-secret
labels:
app: ${APP_NAME:-rxminder}
component: database
type: Opaque
stringData:
# These values will be automatically base64 encoded by Kubernetes
# Update these in your .env file before deployment
username: ${COUCHDB_USER:-admin}
password: ${COUCHDB_PASSWORD:-change-this-secure-password}
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Secret
metadata:
name: couchdb-secret
labels:
app: ${APP_NAME}
component: database
type: Opaque
stringData:
# These values will be automatically base64 encoded by Kubernetes
# Update these in your .env file before deployment
username: ${COUCHDB_USER}
password: ${COUCHDB_PASSWORD}
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: ${APP_NAME}-couchdb-service
labels:
app: ${APP_NAME}
component: database
spec:
selector:
app: ${APP_NAME}
component: database
ports:
- name: couchdb
port: 5984
targetPort: 5984
protocol: TCP
type: ClusterIP
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: ${APP_NAME}-couchdb-service
labels:
app: ${APP_NAME}
component: database
spec:
selector:
app: ${APP_NAME}
component: database
ports:
- name: couchdb
port: 5984
targetPort: 5984
protocol: TCP
type: ClusterIP
+70
View File
@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ${APP_NAME}-couchdb
labels:
app: ${APP_NAME}
component: database
spec:
serviceName: ${APP_NAME}-couchdb-service
replicas: 1
selector:
matchLabels:
app: ${APP_NAME}
component: database
template:
metadata:
labels:
app: ${APP_NAME}
component: database
spec:
containers:
- name: couchdb
image: couchdb:3.3.2
ports:
- containerPort: 5984
env:
- name: COUCHDB_USER
valueFrom:
secretKeyRef:
name: couchdb-secret
key: username
- name: COUCHDB_PASSWORD
valueFrom:
secretKeyRef:
name: couchdb-secret
key: password
resources:
requests:
memory: '64Mi'
cpu: '30m'
limits:
memory: '128Mi'
cpu: '60m'
volumeMounts:
- name: couchdb-data
mountPath: /opt/couchdb/data
livenessProbe:
httpGet:
path: /_up
port: 5984
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /_up
port: 5984
initialDelaySeconds: 10
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: couchdb-data
labels:
app: ${APP_NAME}
component: database
spec:
accessModes: ['ReadWriteOnce']
storageClassName: ${STORAGE_CLASS}
resources:
requests:
storage: ${STORAGE_SIZE}
+70
View File
@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ${APP_NAME}-couchdb
labels:
app: ${APP_NAME}
component: database
spec:
serviceName: ${APP_NAME}-couchdb-service
replicas: 1
selector:
matchLabels:
app: ${APP_NAME}
component: database
template:
metadata:
labels:
app: ${APP_NAME}
component: database
spec:
containers:
- name: couchdb
image: couchdb:3.3.2
ports:
- containerPort: 5984
env:
- name: COUCHDB_USER
valueFrom:
secretKeyRef:
name: couchdb-secret
key: username
- name: COUCHDB_PASSWORD
valueFrom:
secretKeyRef:
name: couchdb-secret
key: password
resources:
requests:
memory: "64Mi"
cpu: "30m"
limits:
memory: "128Mi"
cpu: "60m"
volumeMounts:
- name: couchdb-data
mountPath: /opt/couchdb/data
livenessProbe:
httpGet:
path: /_up
port: 5984
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /_up
port: 5984
initialDelaySeconds: 10
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: couchdb-data
labels:
app: ${APP_NAME}
component: database
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ${STORAGE_CLASS}
resources:
requests:
storage: ${STORAGE_SIZE}
+107
View File
@@ -0,0 +1,107 @@
apiVersion: batch/v1
kind: Job
metadata:
name: db-seed
labels:
app: rxminder
component: database
spec:
template:
metadata:
labels:
app: rxminder
component: database
spec:
containers:
- name: db-seeder
image: couchdb:3.3.2
env:
- name: COUCHDB_USER
valueFrom:
secretKeyRef:
name: couchdb-secret
key: username
- name: COUCHDB_PASSWORD
valueFrom:
secretKeyRef:
name: couchdb-secret
key: password
command: ['/bin/sh', '-c']
args:
- |
# Wait for CouchDB to be ready
echo "Waiting for CouchDB to be ready..."
until curl -f http://couchdb-service:5984/_up 2>/dev/null; do
sleep 2
done
# Create databases
echo "Creating databases..."
curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app
# Create default admin user
echo "Creating default admin user..."
curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/_users/org.couchdb.user:$COUCHDB_USER \
-H "Content-Type: application/json" \
-d "{
\"name\": \"$COUCHDB_USER\",
\"password\": \"$COUCHDB_PASSWORD\",
\"roles\": [\"admin\"],
\"type\": \"user\"
}"
# Create design documents for views
echo "Creating design documents..."
curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app/_design/medications \
-H "Content-Type: application/json" \
-d '{
"views": {
"by_name": {
"map": "function(doc) { if (doc.type === \"medication\") emit(doc.name, doc); }"
},
"by_user": {
"map": "function(doc) { if (doc.type === \"medication\") emit(doc.userId, doc); }"
}
}
}'
curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app/_design/reminders \
-H "Content-Type: application/json" \
-d '{
"views": {
"by_medication": {
"map": "function(doc) { if (doc.type === \"reminder\") emit(doc.medicationId, doc); }"
},
"by_user": {
"map": "function(doc) { if (doc.type === \"reminder\") emit(doc.userId, doc); }"
}
}
}'
# Create a sample user document for reference
# Create design document for authentication users
curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app/_design/auth \
-H "Content-Type: application/json" \
-d '{
"views": {
"by_username": {
"map": "function(doc) { if (doc.type === \"user\" && doc.username) emit(doc.username, doc); }"
},
"by_email": {
"map": "function(doc) { if (doc.type === \"user\" && doc.email) emit(doc.email, doc); }"
}
}
}'
echo "Creating sample user document..."
curl -X POST http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app \
-H "Content-Type: application/json" \
-d '{
"type": "user",
"name": "sample_user",
"email": "user@example.com",
"createdAt": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
}'
echo "Database seeding completed with default admin user"
restartPolicy: Never
backoffLimit: 4
+46
View File
@@ -0,0 +1,46 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${APP_NAME}-frontend
labels:
app: ${APP_NAME}
component: frontend
spec:
replicas: 1
selector:
matchLabels:
app: ${APP_NAME}
component: frontend
template:
metadata:
labels:
app: ${APP_NAME}
component: frontend
spec:
containers:
- name: frontend
image: ${DOCKER_IMAGE}
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: ${APP_NAME}-config
resources:
requests:
memory: '32Mi'
cpu: '20m'
limits:
memory: '64Mi'
cpu: '40m'
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5

Some files were not shown because too many files have changed in this diff Show More