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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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/
|
||||
Executable
+8
@@ -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!"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
# Run lint-staged for file-specific checks
|
||||
bunx lint-staged
|
||||
|
||||
echo "✅ Pre-commit checks passed!"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"id": "@secretlint/secretlint-rule-preset-recommend"
|
||||
}
|
||||
],
|
||||
"allowMessageIds": [],
|
||||
"disabledMessages": [],
|
||||
"reporterOptions": {
|
||||
"formatter": "table"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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. 🌟
|
||||
@@ -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*
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## ✨ 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
@@ -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
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -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
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
// Admin Components
|
||||
export { default as AdminInterface } from './AdminInterface';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,4 @@
|
||||
// Authentication Components
|
||||
export { default as AuthPage } from './AuthPage';
|
||||
export { default as AvatarDropdown } from './AvatarDropdown';
|
||||
export { default as ChangePasswordModal } from './ChangePasswordModal';
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
>
|
||||
×
|
||||
</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} • {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;
|
||||
@@ -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';
|
||||
@@ -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'
|
||||
>
|
||||
×
|
||||
</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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
>
|
||||
×
|
||||
</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;
|
||||
@@ -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'
|
||||
>
|
||||
×
|
||||
</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;
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
>
|
||||
×
|
||||
</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;
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,4 @@
|
||||
// UI Components
|
||||
export { default as BarChart } from './BarChart';
|
||||
export { default as ReminderCard } from './ReminderCard';
|
||||
export { default as ThemeSwitcher } from './ThemeSwitcher';
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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/
|
||||
@@ -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;"]
|
||||
@@ -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.
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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! 🎉**
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
@@ -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
|
||||
```
|
||||
@@ -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!
|
||||
@@ -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/)
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
@@ -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! 🎉**
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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
@@ -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)
|
||||
@@ -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'
|
||||
@@ -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"
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user