From a9eaa7599cec57d0274aea51748441bf85d74a92 Mon Sep 17 00:00:00 2001 From: Jeet Debnath Date: Wed, 4 Mar 2026 12:23:13 +0530 Subject: [PATCH] mongog setup --- .github/copilot-instructions.md | 236 ++-- BACKEND_QUICKSTART.md | 241 ++++ backend/.env.example | 6 + backend/README.md | 88 ++ backend/__pycache__/config.cpython-312.pyc | Bin 0 -> 1136 bytes backend/__pycache__/db.cpython-312.pyc | Bin 0 -> 1757 bytes backend/__pycache__/main.cpython-312.pyc | Bin 0 -> 2400 bytes backend/__pycache__/models.cpython-312.pyc | Bin 0 -> 3681 bytes backend/config.py | 19 + backend/db.py | 31 + backend/main.py | 65 + backend/models.py | 84 ++ backend/requirements.txt | 8 + backend/routers/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 165 bytes .../__pycache__/entries.cpython-312.pyc | Bin 0 -> 6779 bytes .../routers/__pycache__/users.cpython-312.pyc | Bin 0 -> 4582 bytes backend/routers/entries.py | 165 +++ backend/routers/users.py | 99 ++ docs/MONGODB_SETUP.md | 219 ++++ project-context.md | 102 +- src/App.css | 1133 +++++++++++------ src/contexts/AuthContext.tsx | 46 +- src/index.css | 116 +- src/lib/api.ts | 173 +++ src/lib/firebase.ts | 12 +- src/pages/HistoryPage.tsx | 145 ++- src/pages/HomePage.tsx | 68 +- src/pages/SettingsPage.tsx | 88 +- start-all.sh | 52 + start-dev.sh | 45 + vite.config.ts | 5 + 32 files changed, 2577 insertions(+), 670 deletions(-) create mode 100644 BACKEND_QUICKSTART.md create mode 100644 backend/.env.example create mode 100644 backend/README.md create mode 100644 backend/__pycache__/config.cpython-312.pyc create mode 100644 backend/__pycache__/db.cpython-312.pyc create mode 100644 backend/__pycache__/main.cpython-312.pyc create mode 100644 backend/__pycache__/models.cpython-312.pyc create mode 100644 backend/config.py create mode 100644 backend/db.py create mode 100644 backend/main.py create mode 100644 backend/models.py create mode 100644 backend/requirements.txt create mode 100644 backend/routers/__init__.py create mode 100644 backend/routers/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/routers/__pycache__/entries.cpython-312.pyc create mode 100644 backend/routers/__pycache__/users.cpython-312.pyc create mode 100644 backend/routers/entries.py create mode 100644 backend/routers/users.py create mode 100644 docs/MONGODB_SETUP.md create mode 100644 src/lib/api.ts create mode 100755 start-all.sh create mode 100644 start-dev.sh diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8ba127b..ffa1071 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,162 +1,112 @@ - +# Grateful Journal — Project Instructions for Copilot -# BMAD Method — Project Instructions +## Project Overview -## Project Configuration +**Grateful Journal** — A minimal, private-first gratitude journaling web app. Three main pages (Write, History/calendar, Settings/profile) plus Google auth. No feeds or algorithms; privacy by design with client-side encryption; daily use, even one sentence. -- **Project**: grateful-journal -- **User**: Jeet -- **Communication Language**: English -- **Document Output Language**: English -- **User Skill Level**: intermediate -- **Output Folder**: {project-root}/\_bmad-output -- **Planning Artifacts**: {project-root}/\_bmad-output/planning-artifacts -- **Implementation Artifacts**: {project-root}/\_bmad-output/implementation-artifacts -- **Project Knowledge**: {project-root}/docs - -## BMAD Runtime Structure - -- **Agent definitions**: `_bmad/bmm/agents/` (BMM module) and `_bmad/core/agents/` (core) -- **Workflow definitions**: `_bmad/bmm/workflows/` (organized by phase) -- **Core tasks**: `_bmad/core/tasks/` (help, editorial review, indexing, sharding, adversarial review) -- **Core workflows**: `_bmad/core/workflows/` (brainstorming, party-mode, advanced-elicitation) -- **Workflow engine**: `_bmad/core/tasks/workflow.xml` (executes YAML-based workflows) -- **Module configuration**: `_bmad/bmm/config.yaml` -- **Core configuration**: `_bmad/core/config.yaml` -- **Agent manifest**: `_bmad/_config/agent-manifest.csv` -- **Workflow manifest**: `_bmad/_config/workflow-manifest.csv` -- **Help manifest**: `_bmad/_config/bmad-help.csv` -- **Agent memory**: `_bmad/_memory/` - -## Key Conventions - -- Always load `_bmad/bmm/config.yaml` before any agent activation or workflow execution -- Store all config fields as session variables: `{user_name}`, `{communication_language}`, `{output_folder}`, `{planning_artifacts}`, `{implementation_artifacts}`, `{project_knowledge}` -- MD-based workflows execute directly — load and follow the `.md` file -- YAML-based workflows require the workflow engine — load `workflow.xml` first, then pass the `.yaml` config -- Follow step-based workflow execution: load steps JIT, never multiple at once -- Save outputs after EACH step when using the workflow engine -- The `{project-root}` variable resolves to the workspace root at runtime - -## Available Agents - -| Agent | Persona | Title | Capabilities | -| ------------------- | ----------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| bmad-master | BMad Master | BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator | runtime resource management, workflow orchestration, task execution, knowledge custodian | -| analyst | Mary | Business Analyst | market research, competitive analysis, requirements elicitation, domain expertise | -| architect | Winston | Architect | distributed systems, cloud infrastructure, API design, scalable patterns | -| dev | Amelia | Developer Agent | story execution, test-driven development, code implementation | -| pm | John | Product Manager | PRD creation, requirements discovery, stakeholder alignment, user interviews | -| qa | Quinn | QA Engineer | test automation, API testing, E2E testing, coverage analysis | -| quick-flow-solo-dev | Barry | Quick Flow Solo Dev | rapid spec creation, lean implementation, minimum ceremony | -| sm | Bob | Scrum Master | sprint planning, story preparation, agile ceremonies, backlog management | -| tech-writer | Paige | Technical Writer | documentation, Mermaid diagrams, standards compliance, concept explanation | -| ux-designer | Sally | UX Designer | user research, interaction design, UI patterns, experience strategy | - -## Slash Commands - -When the user's message starts with a `/bmad-` command (with or without additional text), execute it by following the steps below. Always load `_bmad/bmm/config.yaml` first and store config as session variables, then load and follow the referenced file exactly. - -### Workflow Commands - -| Command | Action | -| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | -| `/bmad-help` | Load and follow `_bmad/core/tasks/help.md` | -| `/bmad-brainstorming` | Load and follow `_bmad/core/workflows/brainstorming/workflow.md` | -| `/bmad-party-mode` | Load and follow `_bmad/core/workflows/party-mode/workflow.md` | -| `/bmad-bmm-create-product-brief` | Load and follow `_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md` | -| `/bmad-bmm-market-research` | Load and follow `_bmad/bmm/workflows/1-analysis/research/workflow-market-research.md` | -| `/bmad-bmm-domain-research` | Load and follow `_bmad/bmm/workflows/1-analysis/research/workflow-domain-research.md` | -| `/bmad-bmm-technical-research` | Load and follow `_bmad/bmm/workflows/1-analysis/research/workflow-technical-research.md` | -| `/bmad-bmm-create-prd` | Load and follow `_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md` | -| `/bmad-bmm-edit-prd` | Load and follow `_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md` | -| `/bmad-bmm-validate-prd` | Load and follow `_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md` | -| `/bmad-bmm-create-ux-design` | Load and follow `_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md` | -| `/bmad-bmm-create-architecture` | Load and follow `_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md` | -| `/bmad-bmm-create-epics-and-stories` | Load and follow `_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md` | -| `/bmad-bmm-check-implementation-readiness` | Load and follow `_bmad/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md` | -| `/bmad-bmm-sprint-planning` | Load `_bmad/core/tasks/workflow.xml` (engine), then execute `_bmad/bmm/workflows/4-implementation/sprint-planning/workflow.yaml` | -| `/bmad-bmm-sprint-status` | Load `_bmad/core/tasks/workflow.xml` (engine), then execute `_bmad/bmm/workflows/4-implementation/sprint-status/workflow.yaml` | -| `/bmad-bmm-create-story` | Load `_bmad/core/tasks/workflow.xml` (engine), then execute `_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml` | -| `/bmad-bmm-dev-story` | Load `_bmad/core/tasks/workflow.xml` (engine), then execute `_bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml` | -| `/bmad-bmm-code-review` | Load `_bmad/core/tasks/workflow.xml` (engine), then execute `_bmad/bmm/workflows/4-implementation/code-review/workflow.yaml` | -| `/bmad-bmm-retrospective` | Load `_bmad/core/tasks/workflow.xml` (engine), then execute `_bmad/bmm/workflows/4-implementation/retrospective/workflow.yaml` | -| `/bmad-bmm-correct-course` | Load `_bmad/core/tasks/workflow.xml` (engine), then execute `_bmad/bmm/workflows/4-implementation/correct-course/workflow.yaml` | -| `/bmad-bmm-qa-automate` | Load `_bmad/core/tasks/workflow.xml` (engine), then execute `_bmad/bmm/workflows/qa/automate/workflow.yaml` | -| `/bmad-bmm-quick-spec` | Load and follow `_bmad/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md` | -| `/bmad-bmm-quick-dev` | Load and follow `_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md` | -| `/bmad-bmm-document-project` | Load `_bmad/core/tasks/workflow.xml` (engine), then execute `_bmad/bmm/workflows/document-project/workflow.yaml` | -| `/bmad-bmm-generate-project-context` | Load and follow `_bmad/bmm/workflows/generate-project-context/workflow.md` | -| `/bmad-index-docs` | Load and execute `_bmad/core/tasks/index-docs.xml` | -| `/bmad-shard-doc` | Load and execute `_bmad/core/tasks/shard-doc.xml` | -| `/bmad-editorial-review-prose` | Load and execute `_bmad/core/tasks/editorial-review-prose.xml` | -| `/bmad-editorial-review-structure` | Load and execute `_bmad/core/tasks/editorial-review-structure.xml` | -| `/bmad-review-adversarial-general` | Load and execute `_bmad/core/tasks/review-adversarial-general.xml` | -| `/bmad-bmm-write-document` | Load `_bmad/bmm/agents/tech-writer/tech-writer.md`, activate Paige persona, execute Write Document (WD) | -| `/bmad-bmm-update-standards` | Load `_bmad/bmm/agents/tech-writer/tech-writer.md`, activate Paige persona, execute Update Standards (US) | -| `/bmad-bmm-mermaid-generate` | Load `_bmad/bmm/agents/tech-writer/tech-writer.md`, activate Paige persona, execute Mermaid Generate (MG) | -| `/bmad-bmm-validate-document` | Load `_bmad/bmm/agents/tech-writer/tech-writer.md`, activate Paige persona, execute Validate Document (VD) | -| `/bmad-bmm-explain-concept` | Load `_bmad/bmm/agents/tech-writer/tech-writer.md`, activate Paige persona, execute Explain Concept (EC) | - -### Agent Activator Commands - -| Command | Agent File | -| --------------------------- | --------------------------------------------- | -| `/bmad-bmad-master` | `_bmad/core/agents/bmad-master.md` | -| `/bmad-analyst` | `_bmad/bmm/agents/analyst.md` | -| `/bmad-architect` | `_bmad/bmm/agents/architect.md` | -| `/bmad-dev` | `_bmad/bmm/agents/dev.md` | -| `/bmad-pm` | `_bmad/bmm/agents/pm.md` | -| `/bmad-qa` | `_bmad/bmm/agents/qa.md` | -| `/bmad-quick-flow-solo-dev` | `_bmad/bmm/agents/quick-flow-solo-dev.md` | -| `/bmad-sm` | `_bmad/bmm/agents/sm.md` | -| `/bmad-tech-writer` | `_bmad/bmm/agents/tech-writer/tech-writer.md` | -| `/bmad-ux-designer` | `_bmad/bmm/agents/ux-designer.md` | - -For agent commands: load the agent file, follow ALL activation instructions, display the welcome/greeting, present the numbered menu, and wait for user input. +**User:** Jeet --- -## Project Context Maintenance (Critical) +## Technology Stack & Versions -**Purpose:** `project-context.md` serves as the single source of truth for the project state, implementation patterns, and active features. +| Layer | Technology | Notes | +| -------- | -------------------- | ----------------------------------------------------- | +| Frontend | React 19, TypeScript | Vite 7 build; port 8000 | +| Routing | react-router-dom 7 | Routes: `/`, `/history`, `/settings`, `/login` | +| Auth | Firebase 12 | Google sign-in only (no database) | +| Styling | Plain CSS | `src/index.css` (globals), `src/App.css` (components) | +| Backend | FastAPI 0.104 | Python; port 8001; modular routes | +| Database | MongoDB 6.x | Local instance; collections: users, entries, settings | -### When to Update `project-context.md` +--- -1. **After each feature implementation** — Update relevant sections: - - Technology stack (versions) - - Critical implementation rules (new patterns or constraints) - - File layout (new files/folders) - - Known issues or deferred work +## Critical Implementation Rules -2. **When user announces new features** — Immediately add to the file: - - Feature description in project overview - - Any new technology or dependency - - Updated file structure (if applicable) - - New implementation rules or conventions - - Status of feature (e.g., _In Progress_, _Planned_, _Complete_) +### Frontend -3. **Before starting implementation** — Ensure the file reflects current state - - Validate that all recent changes are documented - - Flag any deferred work or known blockers - - Cross-reference with current codebase +- **Colour palette (Coolors):** Use CSS variables from `src/index.css`. Primary green `#1be62c`, background soft `#f1eee1`, surface `#ffffff`, accent light `#cff2dc`, accent bright `#c3fd2f`. Do not introduce new palette colours without reason. +- **Layout:** Responsive for all screens. Breakpoints: `--bp-sm` 480px, `--bp-md` 768px, `--bp-lg` 1024px, `--bp-xl` 1280px. On laptop (1024px+), page is single-screen 100vh — no vertical scroll; fonts and spacing scaled so content fits one viewport. +- **Touch targets:** Minimum 44px (`--touch-min`) on interactive elements for small screens. +- **Safe areas:** Use `env(safe-area-inset-*)` for padding where the app can sit under notches or system UI. Viewport meta includes `viewport-fit=cover`. +- **Structure:** Main app layout: page container → header + main content + fixed `BottomNav`. Content max-width `min(680px, 100%)` (or `--content-max` 720px where appropriate). -### Format Rules +### Backend -- **Last updated:** Always update the timestamp at the bottom: `_Last updated: [YYYY-MM-DD]_` -- **Feature status markers:** Use `_Planning_`, `_In Progress_`, `_Complete)_`, `_Deferred_` -- **Keep it concise:** One-line descriptions; refer to external docs for details -- **Match codebase reality:** If the code differs from the document, the document is wrong—fix it immediately +- **Framework:** FastAPI. APIs in Python only. +- **Modularity:** Separate file per route. Each feature (users, entries) has its own router module. +- **Database:** MongoDB. Setup instructions in `docs/MONGODB_SETUP.md`. +- **Port:** 8001 (backend); 8000 (frontend). CORS configured between them. +- **Authentication:** Relies on Firebase Google Auth token from frontend (passed in Authorization header). -### Example Updates +### Conventions -When user says: "Add dark mode toggle to settings" +- **Fonts:** Inter for UI, Playfair Display for headings/editorial, Lora for body/entry text. Loaded via Google Fonts in `index.html`. +- **Naming:** CSS uses BEM-like class names (e.g. `.journal-card`, `.journal-prompt`). Keep the same pattern for new components. +- **Build:** Fixing the current TypeScript/ESLint build errors is deferred to a later step; do not assume a clean build when adding features. -```markdown -| Feature | Status | Notes | -| Dark mode toggle | In Progress | Added to SettingsPage; CSS variables predefined | +--- + +## File Layout (Reference) + +``` +src/ # Frontend + App.tsx, App.css # Root layout, routes, global page styles + index.css # Resets, :root vars, base typography + main.tsx + pages/ # HomePage, HistoryPage, SettingsPage, LoginPage + components/ # BottomNav, LoginCard, GoogleSignInButton, ProtectedRoute + contexts/ # AuthContext (Firebase Google Auth) + lib/ + firebase.ts # Firebase auth config (Firestore removed) + +backend/ # FastAPI backend (Port 8001) + main.py # FastAPI app, CORS, routes, lifespan + config.py # Settings, environment variables + db.py # MongoDB connection manager + models.py # Pydantic models (User, JournalEntry, Settings) + requirements.txt # Python dependencies + .env.example # Environment variables template + routers/ + users.py # User registration, update, delete endpoints + entries.py # Entry CRUD, date filtering endpoints ``` -When implementation is done: Update to `Complete` and add any implementation notes. +--- - +## Recent Changes & Status + +### Port Configuration (Updated) + +✅ Frontend port changed to **8000** (was 5173) +✅ Backend port remains **8001** +✅ CORS configuration updated in FastAPI +✅ Vite config updated with server port 8000 + +### Backend Setup (Completed) + +✅ FastAPI backend initialized (port 8001) +✅ MongoDB connection configured (local instance) +✅ Pydantic models for User, JournalEntry, UserSettings +✅ Route structure: `/api/users/*` and `/api/entries/*` +✅ CORS enabled for frontend (localhost:8000) +✅ Firestore database files removed (`firestoreService.ts`, `firestoreConfig.ts`) +✅ Firebase authentication kept (Google sign-in only) + +### API Ready + +- User registration, profile updates, deletion +- Entry CRUD (create, read, update, delete) +- Entry filtering by date +- Pagination support + +### Next Steps (Implementation) + +🔄 Connect frontend React app to backend APIs +🔄 Pass Firebase user ID from frontend to backend +🔄 Integrate Auth context with entry save/load +🔄 Add optional: Firebase token verification in backend middleware + +--- + +_Last updated: 2026-03-04_ diff --git a/BACKEND_QUICKSTART.md b/BACKEND_QUICKSTART.md new file mode 100644 index 0000000..abf7e8b --- /dev/null +++ b/BACKEND_QUICKSTART.md @@ -0,0 +1,241 @@ +# MongoDB & FastAPI Backend Setup - Quick Start + +## What's Been Set Up + +✅ **Backend directory structure** (`/backend`) +✅ **FastAPI application** (main.py with routes, CORS, lifecycle) +✅ **MongoDB connection** (config.py, db.py) +✅ **Pydantic models** (User, JournalEntry, UserSettings) +✅ **API routes** (users.py, entries.py) +✅ **Environment configuration** (.env.example) +✅ **Documentation** (README.md, MONGODB_SETUP.md) +✅ **Firebase auth preserved** (Google sign-in, no Firestore) + +--- + +## How to Start MongoDB + +### 1. Install MongoDB (if not already installed) + +```bash +# macOS +brew tap mongodb/brew +brew install mongodb-community + +# Linux (Ubuntu) +curl -fsSL https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add - +echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" \ + | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list +sudo apt-get update +sudo apt-get install -y mongodb-org + +# Windows: Download from https://www.mongodb.com/try/download/community +``` + +### 2. Start MongoDB Service + +```bash +# macOS +brew services start mongodb-community + +# Linux +sudo systemctl start mongod + +# Windows +net start MongoDB + +# Verify it's running +mongosh # Should connect successfully +``` + +--- + +## How to Start FastAPI Backend + +### 1. Navigate to backend directory + +```bash +cd backend +``` + +### 2. Create Python virtual environment + +```bash +python3 -m venv venv + +# Activate it +source venv/bin/activate # macOS/Linux +# or +venv\Scripts\activate # Windows +``` + +### 3. Install dependencies + +```bash +pip install -r requirements.txt +``` + +### 4. Configure environment (optional) + +```bash +cp .env.example .env +# Edit .env if you need to change defaults +``` + +### 5. Start the API server + +```bash +python main.py +``` + +You should see: + +``` +✓ Connected to MongoDB: grateful_journal +INFO: Uvicorn running on http://0.0.0.0:8001 +``` + +### 6. Access API Documentation + +Open in browser: + +- **Interactive API Docs**: http://localhost:8001/docs +- **Alternative Docs**: http://localhost:8001/redoc +- **Health Check**: http://localhost:8001/health + +--- + +## Complete Startup (All Services) + +### Terminal 1: Start MongoDB + +```bash +brew services start mongodb-community +mongosh # Verify connection +``` + +### Terminal 2: Start Frontend + +```bash +npm run dev -- --port 8000 +``` + +### Terminal 3: Start Backend + +```bash +cd backend +source venv/bin/activate +python main.py +``` + +Now you have: + +- **Frontend**: http://localhost:8000 +- **Backend**: http://localhost:8001 +- **MongoDB**: localhost:27017 + +--- + +## API Endpoints Ready to Use + +### User Management + +- `POST /api/users/register` — Register user after Firebase auth +- `GET /api/users/by-email/{email}` — Fetch user profile +- `PUT /api/users/update/{user_id}` — Update profile +- `DELETE /api/users/{user_id}` — Delete account & data + +### Journal Entries + +- `POST /api/entries/{user_id}` — Create entry +- `GET /api/entries/{user_id}` — List entries (paginated) +- `GET /api/entries/{user_id}/{entry_id}` — Get entry +- `PUT /api/entries/{user_id}/{entry_id}` — Update entry +- `DELETE /api/entries/{user_id}/{entry_id}` — Delete entry +- `GET /api/entries/{user_id}/date/{YYYY-MM-DD}` — Entries by date + +--- + +## Authentication Flow + +1. **Frontend**: User clicks "Sign in with Google" +2. **Firebase**: Returns auth token + user info (email, displayName, photoURL) +3. **Frontend**: Calls `POST /api/users/register` with user data +4. **Backend**: Stores user in MongoDB, returns user ID +5. **Frontend**: Uses user ID for all subsequent API calls + +--- + +## File Structure + +``` +grateful-journal/ +├── backend/ # NEW: FastAPI backend +│ ├── main.py # FastAPI app +│ ├── config.py # Settings +│ ├── db.py # MongoDB connection +│ ├── models.py # Pydantic models +│ ├── requirements.txt # Python dependencies +│ ├── .env.example # Environment template +│ ├── README.md # Backend docs +│ └── routers/ # API routes +│ ├── users.py +│ └── entries.py +├── src/ # Frontend (existing) +│ └── lib/ +│ └── firebase.ts # Auth only (Firestore removed) +├── docs/ +│ ├── FIRESTORE_SETUP.md # (old, keep for reference) +│ └── MONGODB_SETUP.md # NEW: MongoDB setup guide +└── project-context.md # Updated with backend info +``` + +--- + +## Removed Files (Firestore) + +- ❌ `src/lib/firestoreService.ts` (removed — use MongoDB API instead) +- ❌ `src/lib/firestoreConfig.ts` (removed — use MongoDB API instead) + +--- + +## Next Steps + +1. **Start MongoDB** (see above) +2. **Start FastAPI** (see above) +3. **Connect Frontend to Backend** + - Update `src/contexts/AuthContext.tsx` to call `/api/users/register` + - Create hooks to fetch/save entries from `/api/entries/*` + - Pass Firebase user ID to all API calls + +4. **Test in API Docs** (http://localhost:8001/docs) + - Try creating a user + - Try creating an entry + - Try fetching entries + +--- + +## Troubleshooting + +**"MongoDB connection refused"** + +- Is the service running? `brew services list` (macOS) or `systemctl status mongod` (Linux) +- Check port 27017 is free: `lsof -i :27017` + +**"ModuleNotFoundError: pymongo"** + +- Is venv activated? Run `. venv/bin/activate` again +- Did you run `pip install -r requirements.txt`? + +**"CORS error in browser console"** + +- Backend running on 8001? Check `http://localhost:8001/docs` +- Frontend URL in `.env`? Should be `http://localhost:8000` + +--- + +For detailed setup instructions, see: + +- [MONGODB_SETUP.md](./docs/MONGODB_SETUP.md) — Full MongoDB installation & configuration +- [backend/README.md](./backend/README.md) — Backend architecture & endpoints +- [project-context.md](./project-context.md) — Implementation rules & conventions diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..d9f6f1d --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,6 @@ +MONGODB_URI=mongodb://localhost:27017 +MONGODB_DB_NAME=grateful_journal +API_PORT=8001 +ENVIRONMENT=development +FRONTEND_URL=http://localhost:8000 + diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..c7d5b73 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,88 @@ +# Grateful Journal Backend API + +FastAPI backend for Grateful Journal - a private-first gratitude journaling app. + +**Port:** 8001 +**API Docs:** http://localhost:8001/docs + +## Quick Start + +### 1. Prerequisites + +- MongoDB running on `mongodb://localhost:27017` +- Python 3.9+ + +See [MongoDB Setup Guide](../docs/MONGODB_SETUP.md) for installation. + +### 2. Install & Run + +```bash +# Create virtual environment +python3 -m venv venv +source venv/bin/activate # macOS/Linux + +# Install dependencies +pip install -r requirements.txt + +# Run API +python main.py +``` + +API starts on http://0.0.0.0:8001 + +### 3. Environment Variables + +Copy `.env.example` to `.env`. Defaults work for local dev: + +```env +MONGODB_URI=mongodb://localhost:27017 +MONGODB_DB_NAME=grateful_journal +API_PORT=8001 +ENVIRONMENT=development +FRONTEND_URL=http://localhost:8000 +``` + +## Architecture + +- **`main.py`** — FastAPI app, CORS, route registration, lifespan events +- **`config.py`** — Settings management (environment variables) +- **`db.py`** — MongoDB connection (singleton pattern) +- **`models.py`** — Pydantic data models +- **`routers/`** — API endpoints + - `users.py` — User registration, profile updates, deletion + - `entries.py` — Journal entry CRUD, date filtering + +## API Endpoints + +### Users + +``` +POST /api/users/register Register user (after Firebase auth) +GET /api/users/by-email/{email} Get user by email +PUT /api/users/update/{user_id} Update user profile +DELETE /api/users/{user_id} Delete user & all data +``` + +### Entries + +``` +POST /api/entries/{user_id} Create new entry +GET /api/entries/{user_id} List entries (paginated) +GET /api/entries/{user_id}/{entry_id} Get single entry +PUT /api/entries/{user_id}/{entry_id} Update entry +DELETE /api/entries/{user_id}/{entry_id} Delete entry +GET /api/entries/{user_id}/date/{date} Get entries by date +``` + +## Authentication + +- Frontend authenticates via **Firebase Google Auth** +- User ID is passed in URL path (no token validation yet; implementation depends on frontend requirements) +- Optional: Add Firebase token verification in middleware later + +## Development Notes + +- **CORS** enabled for `localhost:8000` +- **Async/await** used throughout for scalability +- **Pydantic** models for request/response validation +- **MongoDB** auto-creates collections on first write diff --git a/backend/__pycache__/config.cpython-312.pyc b/backend/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57ecd89b7a93872dbeacce8b3e04199fbb80ae14 GIT binary patch literal 1136 zcmZ8g&1(}u6rb7MB%80MwO=VkBiPFd>Be3vp%nZ;MN2%CUiLC63fu8i@&6-Y;+Gz2AE??`yG`17eMbpS&*+ zfbWtdD>X63Tf#U6E^wg_I?#ep+KR7qR7-_G0ndS}9syUw@Jx-*SegQ`dNv8*rj9^j zTd989$o!NZ1M%iLFWb0@?!H1{xO=2IN*v!H9f^a3+!Om)&2bJuqE zMHQwK=~(Udc{-LvOCnG~{6YywU^KgQtdExNe3+sO;tyID&@c*gT9O$Fwyz!FRvpci2QQADp;jI(uEgGzQP~`9o!lF0<{wQ-Rel2jacCL!#%0NuCZ}aA zGoJ#MT;9J)*%kxRFkr0brbb?9VJ3PX&kemTBFH|3vcv)Sk)Qoo|EA4Hi5o_m&vg5) zO_=9gACZ}Pw%H>N3j#l+a>C+FGT47Dxzab{=deld3a?ZeRt5Z0A%y4P{yC^zrn9he b{Pq%v?V=2n{JZ4~1;T}~3Y21GMQZ*6orD&F literal 0 HcmV?d00001 diff --git a/backend/__pycache__/db.cpython-312.pyc b/backend/__pycache__/db.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ceae744f077539155f42ca9a9047b93d7012b9d GIT binary patch literal 1757 zcmah}&u<$=6rNdo*IRp?p9Dx!h$*S4Q&ICnLbM7#B%zIvFcMV}GE!G-&&1fZcg^lt z!VyS01gSxl0_q_LK)g(}s`NLAs&&5!1(*<=W16WJL_3ky|H43)`l$~cyW*_ZtK1f_G%8$}9v7l&S3FqjA2mhQv8{R; z^P>?9oxvyI91u>5l#!xx15g!XJkHg5h;xl8t1v?>!F3kfC5Pd$;u%;2vf(-jYxUGR z)uvg^Pe^DRYXx5`OMGJ(kP*Z&=zuvOf`-M1Rlg=8!b!d&wTQte4d%n}jL@Q>Im{Zx zYI?LgnxUTT)hL1<-gpLy|`9-qJ5J%9OV{>q)ior&Ju*@K(k-@LbZyu8|l z&)jKIqpYsu9)aQ=_0>*PKyWR(E30SDaY(2SsJ_*?+ci3n)0egU#P%m@1fEVzBKQhKRYEUx10?ruY7=3C) zO1n;6Br)Rt8CMFPYGw(mWPys7Ap2$bdMKeC(nEFZ<&RQ}o%o^zR>7fK6rSe6cp3v9rnkShMRu zTvZAM38Y@&()7?nZ>0*{IriQQE>y9mAX>GD_7)n{pPc$;{ga5KYwg>4^WMz+-ka}@ zzmG;c5IlcB`Nr5(5c*Xptw(JRHirWUts;U5Hj#yM*b3wVmXwpQm7xH!43|k$!4u7neomM0lv7))CuLsST70<;n3Lx6C)(n;FqH&^9Tnv!VExA40 z4L+EJNeAhq-47yba>G5BpoxXIT5z|&RH0o9!`pgmM4}{Cl1Q9%m9Tgpbgx0q;=u@Z zI@%cTlr*|eQOl(EoRJWY5(-q<7b9ZI3AFHQbE1?-pv;Vjm#g%e$}b z-M_=TkM@&+MW_XvfU_Dj2XX^Y%YlVcd&GN#?1#Ed?zX_})b3d_B=&ufX`Kc}%Y(~< z*#mzF*Q~_V_jK1gbNK@vegEp!%jd5bXxTFyo2%z&ncBq7Vjg?d4^qbg(I~+W>A+a6_+xxSyFeG?Lxt^J$l`< zbXzY`#wER6E;P&NrvOw&5{S(r3})cqSCQAYF9_>vPpj~Pv=8Mk@G8bTS2U@(bJ6yS zW%N+GhO>A!D>2bcyaV9bv;djS@le5ZTneKsW>GM@0!C)*l=iVmNL!%P)6P+M(R0e$ zMZ?ibO!sK9Vjf>`D$LeRZB8#NLLoIvH|){!G7p(Xk-BBwW(nA-Q*3z%#P8^z74#~I zl*qc=Ta$aA%KM(l;os!Wbvad&Q$NbR|GFZJ&$My8mAcT3lDXK3l7SPfo<7?MC6r9j zVVPZC$1`PSECIkw6CE5=25P^hjE#wHa6Ct9-lZlItIUSfBGuNBd}{!H%;eS-eE?`uwCBWPz#I_;D@1` zZp*`>-n>KF>O9p6#IAN|dfCu?3NcYJU{lv#-2)(eHf#h@GW{$)w4Od% zOCPPL_rFx-NN@wmO0da=o;2oI+dIQv*rt`C$brc1ER`=kwFs9L)R##DjW|pu{u+3?IX`N( zZ>iCO!(0{=)&QCF`7KG+58YLDMiprdd2MqHm<5SCTesMRM#vF@)4?8S`=vBY} zc}yoHza581ig5BCba=kPOdd1rf>|Ln-`E(i`vJHF7o~s1e&IwdY~7;yJXb4A20*pB z#40uq3G#@feYU(-`K{2D<&cU=r`Z{RBQ7F$3gm`_F@Awg)X<6NXyOGr{2UFvKnI_r zkzYE~pHHuZfJlL!jevsFU#fp1_-sn8iI*{?#_AoJde=-nlB`F2UWFAkup(`AAuO+> zL=7eCa-yCbSWgbulEd}n$m3)0LcrvC{Bv)AJ=uHrh%Yvx2q#}56$ds_?Ui@mS)IB! zbwBdx!lToVQQ|2&Uq@17)4L~Er|(VQAN?-;=o8;GBS0#*(>Kz0`kvtAOF$bGFjE3| z5a2Y8$(w_1TdZM=AvtoV`h(Q>6Cjo2D;I9h+?c6{67^^=B(xsgUyJUq$J6WaL$&y! VuRpED-~KDCD8UT{$^A_5`afa?L74ym literal 0 HcmV?d00001 diff --git a/backend/__pycache__/models.cpython-312.pyc b/backend/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..adb245bc9d0590f220fb967736a7fa142f55b1bf GIT binary patch literal 3681 zcmb_fOOM-B6t?5*#Cc7o)5lDwk9lN9)Ib>l1zIQ-^f6QV2neWzEJ5VPw=-^?2iK0& ztO#mB(_Ok^*s(*XKZOldx`?h+b-{`q(gYS=an5lv_N1XI5|Mm-?st9f_5IFwj&FX= z=Q9fY{_g(lmQsrHJ2r;D#PgYQjpW*iLv_RkJkmHIG$wORpxa zB(<5Bsv4G|DhXvxarC>2ljQ0HEu7Oz6Pg0lh@lxm(|~4TXqM0{pgEr7#sfB(BTsl9 z@QK*d6NDB3os6LcLiYeV6+kjv% zzUp$%F;!_ecEAI-%Aw!*xDmMZn(aw;#q|T|WfyAAY9)O0&W-)lsa( zHHB-OIqGRZorI{iLH za%nU+>OuY5r&mlv8s%~guyR>u%H?X^X?p1A%H^+{wm0a}{6NS;xop>J^}xpc_~o+5 z!FmFvA@X2;QMSPXP7UAEjiy4b$BxzX|ORUffM zv2QqPJgJwUJ5H*jT0kq;iOQstASzWW6~jV$Rm+HBA-(F5-fImEaw#MM_b>HegG0#E zVglc%fMya+MAIH@PNBg8Vg}7#G_-^L=uHm|j`v>$0}TAz=DTdMcWADCVe@3?+`ac7 zvAJ*}oMSu{0(#<9MEW8xq(a-JG}^YK0X>oWbfk;}IZkrNOA(ERm5x1^My#co8Ocx+ zGT}NR3s9W3bcJ;4d>F`dlQ>0J_uTc3fEY3}ql88@M${6aI?O$m)miL7gB<&3!2qj9 z_RiY##fAJ9rn_uzsU;IoxH%-ux0ZPi$%TPd&^)^TL4^xxSAkr{vtivE^n|plRt|dd1B_C-rGhkf(LStq$Kag=?p_dmL>lsC zPK**wPtxZhqC6Tnk5lZXTpUMiM6)Z1p?MuHzXS#tkBVNG&Gn8fwAVJj=v3}~*<}ka z5`$tCgJKv1Twr_*zK5Q8491mA!swepUdT6`fI!Sc=@YAv^v_CvSU58f8|F~SgtHBE zGE`d8%TgKD&791|6G31C%VZNO>qm~*iAyO8%ql5a9ERt_5in+cG)7Tmo|_C|WT?E3 zga@M{(42y67#^yuxx$^xVR?PQ)Yv}@VR5=_cBke(V$)%GaDnmR`3idC;rSm@WSBOT z9FPb}IfY-Z&v+2HwRL}x2r>^xA@_z`36Dc^Dm-V!G5Qoq!?SDaO?#c2*-`f2f|m~q z5GkKd25%!qCj*MjJJ=lI`~+fX*5LBr0|P0sbgX@~b8_qa{WD#5tY;qojUCuBe%klr zM3)^O$(J#wC!B3O!4~wy36AtXIFb$zql}IS%o&z)gPt6!8_NBTx}gq*>TYFG({Xae z8?fBx0*P4_M8#2XLn0HrCWsD{a_Lz%i7(=icqK|BHfiKI1*32U8muio4gfYuyt7Av z(i2^F6sk7T$*_R$nSnFFn_V{3+cyJxt)N$!-cc$|WbTC%cFVDAfm;z8&X0do1gxkkaKcOH?(Q{!}af)^T0|Uf{fQ*ve<^UIB>o3?HSXE~ literal 0 HcmV?d00001 diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..063c0c8 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,19 @@ +from pydantic_settings import BaseSettings +from functools import lru_cache + + +class Settings(BaseSettings): + mongodb_uri: str = "mongodb://localhost:27017" + mongodb_db_name: str = "grateful_journal" + api_port: int = 8001 + environment: str = "development" + frontend_url: str = "http://localhost:8000" + + class Config: + env_file = ".env" + case_sensitive = False + + +@lru_cache() +def get_settings(): + return Settings() diff --git a/backend/db.py b/backend/db.py new file mode 100644 index 0000000..f3f5855 --- /dev/null +++ b/backend/db.py @@ -0,0 +1,31 @@ +from pymongo import MongoClient +from config import get_settings +from typing import Optional + + +class MongoDB: + client: Optional[MongoClient] = None + db = None + + @staticmethod + def connect_db(): + settings = get_settings() + MongoDB.client = MongoClient(settings.mongodb_uri) + MongoDB.db = MongoDB.client[settings.mongodb_db_name] + print(f"✓ Connected to MongoDB: {settings.mongodb_db_name}") + + @staticmethod + def close_db(): + if MongoDB.client: + MongoDB.client.close() + print("✓ Disconnected from MongoDB") + + @staticmethod + def get_db(): + return MongoDB.db + +# Get database instance + + +def get_database(): + return MongoDB.get_db() diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..2045d7f --- /dev/null +++ b/backend/main.py @@ -0,0 +1,65 @@ +from fastapi import FastAPI, HTTPException, Depends +from fastapi.middleware.cors import CORSMiddleware +from db import MongoDB, get_database +from config import get_settings +from routers import entries, users +from contextlib import asynccontextmanager + +settings = get_settings() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup + MongoDB.connect_db() + yield + # Shutdown + MongoDB.close_db() + +app = FastAPI( + title="Grateful Journal API", + description="Backend API for Grateful Journal - private journaling app", + version="0.1.0", + lifespan=lifespan +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=[settings.frontend_url, + "http://localhost:8000", "http://127.0.0.1:8000"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include routers +app.include_router(users.router, prefix="/api/users", tags=["users"]) +app.include_router(entries.router, prefix="/api/entries", tags=["entries"]) + + +@app.get("/health") +async def health_check(): + return { + "status": "ok", + "environment": settings.environment, + "api_version": "0.1.0" + } + + +@app.get("/") +async def root(): + return { + "message": "Grateful Journal API", + "version": "0.1.0", + "docs": "/docs" + } + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "main:app", + host="0.0.0.0", + port=settings.api_port, + reload=settings.environment == "development" + ) diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..79d356e --- /dev/null +++ b/backend/models.py @@ -0,0 +1,84 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Optional, List +from enum import Enum + +# ========== User Models ========== + + +class UserCreate(BaseModel): + email: str + displayName: Optional[str] = None + photoURL: Optional[str] = None + + +class UserUpdate(BaseModel): + displayName: Optional[str] = None + photoURL: Optional[str] = None + theme: Optional[str] = None + + +class User(BaseModel): + id: str + email: str + displayName: Optional[str] = None + photoURL: Optional[str] = None + createdAt: datetime + updatedAt: datetime + theme: Optional[str] = "light" + +# ========== Journal Entry Models ========== + + +class MoodEnum(str, Enum): + happy = "happy" + sad = "sad" + neutral = "neutral" + anxious = "anxious" + grateful = "grateful" + + +class JournalEntryCreate(BaseModel): + title: str + content: str + mood: Optional[MoodEnum] = None + tags: Optional[List[str]] = None + isPublic: Optional[bool] = False + + +class JournalEntryUpdate(BaseModel): + title: Optional[str] = None + content: Optional[str] = None + mood: Optional[MoodEnum] = None + tags: Optional[List[str]] = None + isPublic: Optional[bool] = None + + +class JournalEntry(BaseModel): + id: str + userId: str + title: str + content: str + mood: Optional[MoodEnum] = None + tags: Optional[List[str]] = None + isPublic: bool = False + createdAt: datetime + updatedAt: datetime + +# ========== Settings Models ========== + + +class UserSettingsUpdate(BaseModel): + notifications: Optional[bool] = None + emailNotifications: Optional[bool] = None + theme: Optional[str] = None + language: Optional[str] = None + + +class UserSettings(BaseModel): + userId: str + notifications: bool = True + emailNotifications: bool = False + theme: str = "light" + language: str = "en" + updatedAt: datetime diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..332067e --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,8 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +pymongo==4.6.0 +pydantic==2.5.0 +python-dotenv==1.0.0 +pydantic-settings==2.1.0 +python-multipart==0.0.6 +cors==1.0.1 diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py new file mode 100644 index 0000000..873f7bb --- /dev/null +++ b/backend/routers/__init__.py @@ -0,0 +1 @@ +# Routers package diff --git a/backend/routers/__pycache__/__init__.cpython-312.pyc b/backend/routers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68d3a17e9046e9ab765047253eaab340f7940c6f GIT binary patch literal 165 zcmX@j%ge<81m7<&&lCdEk3k%C@R(dQGRI&NJo5pW?p7V le7s&kIV&v1k!|5iqpzkcE;|SeONQI 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 literal 0 HcmV?d00001 diff --git a/backend/routers/__pycache__/users.cpython-312.pyc b/backend/routers/__pycache__/users.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43d44e3656115d12818506b0886c74e91496ccb8 GIT binary patch literal 4582 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 literal 0 HcmV?d00001 diff --git a/backend/routers/entries.py b/backend/routers/entries.py new file mode 100644 index 0000000..d50c89f --- /dev/null +++ b/backend/routers/entries.py @@ -0,0 +1,165 @@ +"""Journal entry routes""" +from fastapi import APIRouter, HTTPException +from db import get_database +from models import JournalEntryCreate, JournalEntryUpdate +from datetime import datetime +from typing import List +from bson import ObjectId + +router = APIRouter() + + +@router.post("/{user_id}", response_model=dict) +async def create_entry(user_id: str, entry_data: JournalEntryCreate): + """Create a new journal entry""" + db = get_database() + + try: + entry_doc = { + "userId": user_id, + "title": entry_data.title, + "content": entry_data.content, + "mood": entry_data.mood, + "tags": entry_data.tags or [], + "isPublic": entry_data.isPublic, + "createdAt": datetime.utcnow(), + "updatedAt": datetime.utcnow() + } + + result = db.entries.insert_one(entry_doc) + entry_doc["id"] = str(result.inserted_id) + + return { + "id": entry_doc["id"], + "message": "Entry created successfully" + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/{user_id}") +async def get_user_entries(user_id: str, limit: int = 50, skip: int = 0): + """Get all entries for a user (paginated, most recent first)""" + db = get_database() + + try: + entries = list( + db.entries.find( + {"userId": user_id} + ).sort("createdAt", -1).skip(skip).limit(limit) + ) + + for entry in entries: + entry["id"] = str(entry["_id"]) + del entry["_id"] + + total = db.entries.count_documents({"userId": user_id}) + + return { + "entries": entries, + "total": total, + "skip": skip, + "limit": limit + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/{user_id}/{entry_id}") +async def get_entry(user_id: str, entry_id: str): + """Get a specific entry""" + db = get_database() + + try: + entry = db.entries.find_one({ + "_id": ObjectId(entry_id), + "userId": user_id + }) + + if not entry: + raise HTTPException(status_code=404, detail="Entry not found") + + entry["id"] = str(entry["_id"]) + del entry["_id"] + + return entry + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.put("/{user_id}/{entry_id}") +async def update_entry(user_id: str, entry_id: str, entry_data: JournalEntryUpdate): + """Update a journal entry""" + db = get_database() + + try: + update_data = entry_data.model_dump(exclude_unset=True) + update_data["updatedAt"] = datetime.utcnow() + + result = db.entries.update_one( + { + "_id": ObjectId(entry_id), + "userId": user_id + }, + {"$set": update_data} + ) + + if result.matched_count == 0: + raise HTTPException(status_code=404, detail="Entry not found") + + return {"message": "Entry updated successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.delete("/{user_id}/{entry_id}") +async def delete_entry(user_id: str, entry_id: str): + """Delete a journal entry""" + db = get_database() + + try: + result = db.entries.delete_one({ + "_id": ObjectId(entry_id), + "userId": user_id + }) + + if result.deleted_count == 0: + raise HTTPException(status_code=404, detail="Entry not found") + + return {"message": "Entry deleted successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/{user_id}/date/{date_str}") +async def get_entries_by_date(user_id: str, date_str: str): + """Get entries for a specific date (format: YYYY-MM-DD)""" + db = get_database() + + try: + from datetime import datetime as dt + + # Parse date + target_date = dt.strptime(date_str, "%Y-%m-%d") + next_date = dt.fromtimestamp(target_date.timestamp() + 86400) + + entries = list( + db.entries.find({ + "userId": user_id, + "createdAt": { + "$gte": target_date, + "$lt": next_date + } + }).sort("createdAt", -1) + ) + + for entry in entries: + entry["id"] = str(entry["_id"]) + del entry["_id"] + + return {"entries": entries, "date": date_str} + except ValueError: + raise HTTPException( + status_code=400, detail="Invalid date format. Use YYYY-MM-DD") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/routers/users.py b/backend/routers/users.py new file mode 100644 index 0000000..e950a7b --- /dev/null +++ b/backend/routers/users.py @@ -0,0 +1,99 @@ +"""User management routes""" +from fastapi import APIRouter, HTTPException, Header +from pymongo.errors import DuplicateKeyError +from db import get_database +from models import UserCreate, UserUpdate, User +from datetime import datetime +from typing import Optional, List + +router = APIRouter() + + +@router.post("/register", response_model=dict) +async def register_user(user_data: UserCreate): + """ + Register a new user (called after Firebase Google Auth) + Stores user profile in MongoDB + """ + db = get_database() + + try: + user_doc = { + "email": user_data.email, + "displayName": user_data.displayName or user_data.email.split("@")[0], + "photoURL": user_data.photoURL, + "createdAt": datetime.utcnow(), + "updatedAt": datetime.utcnow(), + "theme": "light" + } + + result = db.users.insert_one(user_doc) + user_doc["id"] = str(result.inserted_id) + + return { + "id": user_doc["id"], + "email": user_doc["email"], + "displayName": user_doc["displayName"], + "message": "User registered successfully" + } + except DuplicateKeyError: + raise HTTPException(status_code=400, detail="User already exists") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/by-email/{email}", response_model=dict) +async def get_user_by_email(email: str): + """Get user profile by email (called after Firebase Auth)""" + db = get_database() + + user = db.users.find_one({"email": email}) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + user["id"] = str(user["_id"]) + return user + + +@router.put("/update/{user_id}", response_model=dict) +async def update_user(user_id: str, user_data: UserUpdate): + """Update user profile""" + db = get_database() + from bson import ObjectId + + try: + update_data = user_data.model_dump(exclude_unset=True) + update_data["updatedAt"] = datetime.utcnow() + + result = db.users.update_one( + {"_id": ObjectId(user_id)}, + {"$set": update_data} + ) + + if result.matched_count == 0: + raise HTTPException(status_code=404, detail="User not found") + + return {"message": "User updated successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.delete("/{user_id}") +async def delete_user(user_id: str): + """Delete user account and all associated data""" + db = get_database() + from bson import ObjectId + + try: + # Delete user + db.users.delete_one({"_id": ObjectId(user_id)}) + + # Delete all entries by user + db.entries.delete_many({"userId": user_id}) + + # Delete user settings + db.settings.delete_one({"userId": user_id}) + + return {"message": "User and associated data deleted"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/docs/MONGODB_SETUP.md b/docs/MONGODB_SETUP.md new file mode 100644 index 0000000..540ca46 --- /dev/null +++ b/docs/MONGODB_SETUP.md @@ -0,0 +1,219 @@ +# MongoDB Setup Guide for Grateful Journal + +## Prerequisites + +- MongoDB installed on your system +- Python 3.9+ +- pip package manager + +## Installation Steps + +### 1. Install MongoDB + +#### macOS (using Homebrew) + +```bash +brew tap mongodb/brew +brew install mongodb-community +``` + +#### Linux (Ubuntu/Debian) + +```bash +curl -fsSL https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add - +echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list +sudo apt-get update +sudo apt-get install -y mongodb-org +``` + +#### Windows + +Download and run the installer from [MongoDB Community Download](https://www.mongodb.com/try/download/community) + +### 2. Start MongoDB Server + +#### macOS / Linux + +```bash +# Start as a service (recommended) +brew services start mongodb-community + +# Or run directly +mongod --config /usr/local/etc/mongod.conf +``` + +#### Windows + +MongoDB should run as a service. If not: + +```bash +net start MongoDB +``` + +### 3. Verify MongoDB is Running + +```bash +mongosh +``` + +If you see a connection prompt, MongoDB is running successfully. + +### 4. Set up Python Backend Environment + +Navigate to the backend directory: + +```bash +cd backend +``` + +Create a virtual environment: + +```bash +python3 -m venv venv +source venv/bin/activate # macOS/Linux +# or +venv\Scripts\activate # Windows +``` + +Install dependencies: + +```bash +pip install -r requirements.txt +``` + +### 5. Configure Environment Variables + +Copy the example env file: + +```bash +cp .env.example .env +``` + +Edit `.env` with your settings (defaults work for local development): + +```env +MONGODB_URI=mongodb://localhost:27017 +MONGODB_DB_NAME=grateful_journal +API_PORT=8001 +ENVIRONMENT=development +FRONTEND_URL=http://localhost:8000 +``` + +### 6. Run the FastAPI Server + +```bash +python main.py +``` + +You should see: + +``` +✓ Connected to MongoDB: grateful_journal +INFO: Uvicorn running on http://0.0.0.0:8001 +``` + +### 7. Access API Documentation + +Open your browser and go to: + +- **Swagger UI**: http://localhost:8001/docs +- **ReDoc**: http://localhost:8001/redoc +- **Health Check**: http://localhost:8001/health + +## MongoDB Collections Overview + +The following collections will be created automatically on first write: + +### `users` + +Stores user profiles after Firebase Google Auth. + +```json +{ + "_id": ObjectId, + "email": "user@example.com", + "displayName": "John Doe", + "photoURL": "https://...", + "theme": "light", + "createdAt": ISODate, + "updatedAt": ISODate +} +``` + +### `entries` + +Stores journal entries. + +```json +{ + "_id": ObjectId, + "userId": "user_id_string", + "title": "Today's thoughts", + "content": "Long-form journal content...", + "mood": "grateful", + "tags": ["reflection", "gratitude"], + "isPublic": false, + "createdAt": ISODate, + "updatedAt": ISODate +} +``` + +### `settings` + +Stores user preferences and settings. + +```json +{ + "_id": ObjectId, + "userId": "user_id_string", + "notifications": true, + "emailNotifications": false, + "theme": "light", + "language": "en", + "updatedAt": ISODate +} +``` + +## API Endpoints + +### Users + +- `POST /api/users/register` — Register user after Firebase auth +- `GET /api/users/by-email/{email}` — Get user profile by email +- `PUT /api/users/update/{user_id}` — Update user profile +- `DELETE /api/users/{user_id}` — Delete user and associated data + +### Entries + +- `POST /api/entries/{user_id}` — Create new entry +- `GET /api/entries/{user_id}` — Get all entries (paginated) +- `GET /api/entries/{user_id}/{entry_id}` — Get specific entry +- `PUT /api/entries/{user_id}/{entry_id}` — Update entry +- `DELETE /api/entries/{user_id}/{entry_id}` — Delete entry +- `GET /api/entries/{user_id}/date/{date_str}` — Get entries by date (YYYY-MM-DD) + +## Troubleshooting + +**MongoDB connection refused** + +- Check that MongoDB service is running: `brew services list` (macOS) +- Verify port 27017 is not blocked + +**ModuleNotFoundError: pymongo** + +- Ensure virtual environment is activated +- Run `pip install -r requirements.txt` again + +**CORS errors in frontend** + +- Check `FRONTEND_URL` in `.env` matches your frontend URL +- Default allows http://localhost:8000 + +## Next Steps + +Once MongoDB and FastAPI are running: + +1. Frontend calls Firebase Google Auth +2. Frontend sends auth token to `/api/users/register` +3. Backend creates user in MongoDB +4. Frontend can now call `/api/entries/*` endpoints with user_id diff --git a/project-context.md b/project-context.md index 3351874..8a8eb60 100644 --- a/project-context.md +++ b/project-context.md @@ -14,14 +14,14 @@ _This file contains critical rules and patterns that AI agents must follow when ## Technology stack & versions -| Layer | Technology | Notes | -|-----------|--------------------------|--------------------------------------------| -| Frontend | React 19, TypeScript | Vite 7 build | -| Routing | react-router-dom 7 | Routes: `/`, `/history`, `/settings`, `/login` | -| Auth | Firebase 12 | Google sign-in only | -| Styling | Plain CSS | `src/index.css` (globals), `src/App.css` (components) | -| Backend | _Planned_ Python/FastAPI | Modular: one file per page/function | -| Database | _Planned_ MongoDB | To be set up after responsive UI | +| Layer | Technology | Notes | +| -------- | -------------------- | ----------------------------------------------------- | +| Frontend | React 19, TypeScript | Vite 7 build; port 8000 | +| Routing | react-router-dom 7 | Routes: `/`, `/history`, `/settings`, `/login` | +| Auth | Firebase 12 | Google sign-in only (no database) | +| Styling | Plain CSS | `src/index.css` (globals), `src/App.css` (components) | +| Backend | FastAPI 0.104 | Python; port 8001; modular routes | +| Database | MongoDB 6.x | Local instance; collections: users, entries, settings | --- @@ -38,8 +38,10 @@ _This file contains critical rules and patterns that AI agents must follow when ### Backend (when implemented) - **Framework:** FastAPI. APIs in Python only. -- **Modularity:** Separate file per page and its functions. Each app page (write, history, settings, auth) has its own backend module; avoid one monolithic API file. -- **Database:** MongoDB. Setup comes after frontend responsive work. +- **Modularity:** Separate file per route. Each feature (users, entries) has its own router module. +- **Database:** MongoDB. Setup instructions below. +- **Port:** 8001 (backend); 8000 (frontend). CORS configured between them. +- **Authentication:** Relies on Firebase Google Auth token from frontend (passed in Authorization header). ### Conventions @@ -52,16 +54,80 @@ _This file contains critical rules and patterns that AI agents must follow when ## File layout (reference) ``` -src/ - App.tsx, App.css # Root layout, routes, global page styles - index.css # Resets, :root vars, base typography +src/ # Frontend + App.tsx, App.css # Root layout, routes, global page styles + index.css # Resets, :root vars, base typography main.tsx - pages/ # HomePage, HistoryPage, SettingsPage, LoginPage - components/ # BottomNav, LoginCard, GoogleSignInButton, ProtectedRoute - contexts/ # AuthContext (Firebase) - lib/ # firebase.ts, firestoreService.ts + pages/ # HomePage, HistoryPage, SettingsPage, LoginPage + components/ # BottomNav, LoginCard, GoogleSignInButton, ProtectedRoute + contexts/ # AuthContext (Firebase Google Auth) + lib/ + firebase.ts # Firebase auth config (Firestore removed) + +backend/ # FastAPI backend (Port 8001) + main.py # FastAPI app, CORS, routes, lifespan + config.py # Settings, environment variables + db.py # MongoDB connection manager + models.py # Pydantic models (User, JournalEntry, Settings) + requirements.txt # Python dependencies + .env.example # Environment variables template + routers/ + users.py # User registration, update, delete endpoints + entries.py # Entry CRUD, date filtering endpoints ``` --- -_Last updated from session context and codebase review._ +_Last updated: 2026-03-04_ + +## Recent Changes & Status + +### Port Configuration (Updated) + +✅ Frontend port changed to **8000** (was 5173) +✅ Backend port remains **8001** +✅ CORS configuration updated in FastAPI +✅ Vite config updated with server port 8000 + +### Backend Setup (Completed) + +✅ FastAPI backend initialized (port 8001) +✅ MongoDB connection configured (local instance) +✅ Pydantic models for User, JournalEntry, UserSettings +✅ Route structure: `/api/users/*` and `/api/entries/*` +✅ CORS enabled for frontend (localhost:8000) +✅ Firestore database files removed (`firestoreService.ts`, `firestoreConfig.ts`) +✅ Firebase authentication kept (Google sign-in only) + +### API Ready + +- User registration, profile updates, deletion +- Entry CRUD (create, read, update, delete) +- Entry filtering by date +- Pagination support + +### Frontend-Backend Integration (Completed) + +✅ **API Service Layer** — Created `src/lib/api.ts` with all backend calls +✅ **AuthContext Updated** — Now syncs users with MongoDB on login + +- Auto-registers new users in MongoDB +- Fetches existing user profiles +- Provides `userId` (MongoDB ID) to all pages + ✅ **HomePage** — Entry creation via POST `/api/entries/{userId}` +- Save with success/error feedback +- Clears form after save + ✅ **HistoryPage** — Fetches entries via GET `/api/entries/{userId}` +- Calendar shows days with entries +- Lists recent entries with timestamps +- Filters by current month + ✅ **SettingsPage** — Updates user settings via PUT `/api/users/update/{userId}` +- Theme selector (light/dark) with MongoDB persistence +- Profile info from Firebase + +### Next Steps (Implementation) + +🔄 Add entry detail view / edit functionality +🔄 Firebase token verification in backend middleware +🔄 Search/filter entries by date range +🔄 Client-side encryption for entries diff --git a/src/App.css b/src/App.css index df96a40..20f4016 100644 --- a/src/App.css +++ b/src/App.css @@ -4,118 +4,165 @@ ROOT / LAYOUT SHELL ============================ */ #root { - width: 100%; - height: 100dvh; - overflow: hidden; - display: flex; - flex-direction: column; + width: 100%; + height: 100dvh; + overflow: hidden; + display: flex; + flex-direction: column; } /* ============================ PROTECTED ROUTE SPINNER ============================ */ .protected-route__loading { - height: 100dvh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; - background: #f5f0e8; - color: #9ca3af; + height: 100dvh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + background: #f5f0e8; + color: #9ca3af; } .protected-route__spinner { - width: 28px; - height: 28px; - border: 3px solid #e5e7eb; - border-top-color: #22c55e; - border-radius: 50%; - animation: spin 0.7s linear infinite; + width: 28px; + height: 28px; + border: 3px solid #e5e7eb; + border-top-color: #22c55e; + border-radius: 50%; + animation: spin 0.7s linear infinite; } -@keyframes spin { to { transform: rotate(360deg); } } +@keyframes spin { + to { + transform: rotate(360deg); + } +} /* ============================ LOGIN PAGE ============================ */ .login-page { - height: 100dvh; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(160deg, #f5f0e8 0%, #dcfce7 100%); - padding: 1.5rem; - overflow: hidden; + height: 100dvh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(160deg, #f5f0e8 0%, #dcfce7 100%); + padding: 1.5rem; + overflow: hidden; } .login-page__loading { - display: flex; flex-direction: column; align-items: center; - gap: 1rem; color: #9ca3af; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + color: #9ca3af; } .login-page__spinner { - width: 28px; height: 28px; - border: 3px solid #e5e7eb; - border-top-color: #22c55e; - border-radius: 50%; - animation: spin 0.7s linear infinite; + width: 28px; + height: 28px; + border: 3px solid #e5e7eb; + border-top-color: #22c55e; + border-radius: 50%; + animation: spin 0.7s linear infinite; } .login-card { - background: #fff; - padding: 2rem; - border-radius: 20px; - border-top: 4px solid #22c55e; - box-shadow: 0 8px 32px rgba(0,0,0,0.08); - width: 100%; - max-width: 360px; - text-align: center; + background: #fff; + padding: 2rem; + border-radius: 20px; + border-top: 4px solid #22c55e; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); + width: 100%; + max-width: 360px; + text-align: center; } -.login-card__brand { margin-bottom: 1.75rem; } +.login-card__brand { + margin-bottom: 1.75rem; +} .login-card__title { - margin: 0 0 0.375rem; - font-size: 1.625rem; - font-weight: 700; - color: #22c55e; - letter-spacing: -0.02em; - font-family: 'Playfair Display', Georgia, serif; + margin: 0 0 0.375rem; + font-size: 1.625rem; + font-weight: 700; + color: #22c55e; + letter-spacing: -0.02em; + font-family: "Playfair Display", Georgia, serif; } -.login-card__tagline { margin: 0; color: #6b7280; font-size: 0.9375rem; } +.login-card__tagline { + margin: 0; + color: #6b7280; + font-size: 0.9375rem; +} -.login-card__actions { display: flex; flex-direction: column; gap: 1rem; } +.login-card__actions { + display: flex; + flex-direction: column; + gap: 1rem; +} .login-card__error { - margin: 0; padding: 0.625rem 0.75rem; - color: #b91c1c; font-size: 0.875rem; - background: #fef2f2; border-radius: 8px; text-align: left; + margin: 0; + padding: 0.625rem 0.75rem; + color: #b91c1c; + font-size: 0.875rem; + background: #fef2f2; + border-radius: 8px; + text-align: left; } .google-sign-in-btn { - display: inline-flex; align-items: center; justify-content: center; - gap: 0.75rem; width: 100%; min-height: 48px; - padding: 0.75rem 1.25rem; font-size: 0.9375rem; font-weight: 500; - color: #3c4043; background: #fff; border: 1px solid #dadce0; - border-radius: 10px; cursor: pointer; - transition: background 0.2s, box-shadow 0.2s; - -webkit-tap-highlight-color: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + width: 100%; + min-height: 48px; + padding: 0.75rem 1.25rem; + font-size: 0.9375rem; + font-weight: 500; + color: #3c4043; + background: #fff; + border: 1px solid #dadce0; + border-radius: 10px; + cursor: pointer; + transition: + background 0.2s, + box-shadow 0.2s; + -webkit-tap-highlight-color: transparent; } .google-sign-in-btn:hover:not(:disabled) { - background: #f8f9fa; box-shadow: 0 1px 6px rgba(0,0,0,0.1); + background: #f8f9fa; + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1); +} +.google-sign-in-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} +.google-sign-in-btn__logo { + flex-shrink: 0; } -.google-sign-in-btn:disabled { opacity: 0.7; cursor: not-allowed; } -.google-sign-in-btn__logo { flex-shrink: 0; } .home-login-link { - display: inline-block; margin-top: 1rem; padding: 0.75rem 1.5rem; - color: #fff; background: #22c55e; border-radius: 10px; - text-decoration: none; font-weight: 600; transition: background 0.2s; + display: inline-block; + margin-top: 1rem; + padding: 0.75rem 1.5rem; + color: #fff; + background: #22c55e; + border-radius: 10px; + text-decoration: none; + font-weight: 600; + transition: background 0.2s; +} +.home-login-link:hover { + background: #16a34a; } -.home-login-link:hover { background: #16a34a; } /* ============================ SHARED PAGE SHELL @@ -124,514 +171,874 @@ .home-page, .history-page, .settings-page { - flex: 1; - min-height: 0; - overflow: hidden; - display: flex; - flex-direction: column; - background: #f5f0e8; + flex: 1; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; + background: #f5f0e8; } /* ============================ HOME / WRITE PAGE ============================ */ .journal-container { - flex: 1; - min-height: 0; - overflow-y: auto; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE/Edge */ - display: flex; - flex-direction: column; - padding: 1.5rem 1.25rem 1rem; + flex: 1; + min-height: 0; + overflow-y: auto; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE/Edge */ + display: flex; + flex-direction: column; + padding: 1.5rem 1.25rem 1rem; } -.journal-container::-webkit-scrollbar { display: none; } +.journal-container::-webkit-scrollbar { + display: none; +} .journal-card { - background: #fff; - border-radius: 20px; - padding: 1.625rem 1.5rem; - box-shadow: 0 2px 12px rgba(0,0,0,0.07); - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; + background: #fff; + border-radius: 20px; + padding: 1.625rem 1.5rem; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.07); + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; } .journal-date { - font-size: 0.625rem; - font-weight: 700; - letter-spacing: 0.14em; - color: #22c55e; - text-transform: uppercase; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; - margin: 0 0 1rem; + font-size: 0.625rem; + font-weight: 700; + letter-spacing: 0.14em; + color: #22c55e; + text-transform: uppercase; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; + margin: 0 0 1rem; } .journal-prompt { - margin: 0 0 1.5rem; - font-size: 1.75rem; - font-weight: 700; - line-height: 1.2; - color: #1a1a1a; - letter-spacing: -0.02em; - font-family: 'Playfair Display', Georgia, 'Times New Roman', serif; + margin: 0 0 1.5rem; + font-size: 1.75rem; + font-weight: 700; + line-height: 1.2; + color: #1a1a1a; + letter-spacing: -0.02em; + font-family: "Playfair Display", Georgia, "Times New Roman", serif; } .journal-writing-area { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; } .journal-title-input { - width: 100%; - padding: 0 0 0.875rem; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; - font-size: 1rem; - font-weight: 400; - color: #374151; - background: transparent; - border: none; - border-bottom: 1px solid #f0ece4; - outline: none; - transition: border-color 0.2s; + width: 100%; + padding: 0 0 0.875rem; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; + font-size: 1rem; + font-weight: 400; + color: #374151; + background: transparent; + border: none; + border-bottom: 1px solid #f0ece4; + outline: none; + transition: border-color 0.2s; } -.journal-title-input::placeholder { color: #c4bfb5; } -.journal-title-input:focus { border-bottom-color: #22c55e; } +.journal-title-input::placeholder { + color: #c4bfb5; +} +.journal-title-input:focus { + border-bottom-color: #22c55e; +} .journal-entry-textarea { - flex: 1; - min-height: 0; - width: 100%; - padding: 0.875rem 0; - font-family: 'Lora', Georgia, serif; - font-size: 1rem; - line-height: 1.75; - color: #374151; - background: transparent; - border: none; - outline: none; - resize: none; - caret-color: #374151; + flex: 1; + min-height: 0; + width: 100%; + padding: 0.875rem 0; + font-family: "Lora", Georgia, serif; + font-size: 1rem; + line-height: 1.75; + color: #374151; + background: transparent; + border: none; + outline: none; + resize: none; + caret-color: #374151; } -.journal-entry-textarea::placeholder { color: #c4bfb5; font-style: italic; } +.journal-entry-textarea::placeholder { + color: #c4bfb5; + font-style: italic; +} .journal-write-btn { - display: inline-flex; align-items: center; justify-content: center; - gap: 0.5rem; min-height: 44px; padding: 0.625rem 1.5rem; - font-size: 0.9375rem; font-weight: 600; color: #fff; - background: #22c55e; border: none; border-radius: 100px; - cursor: pointer; transition: all 0.2s ease; - box-shadow: 0 4px 12px rgba(34,197,94,0.3); - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; - white-space: nowrap; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + min-height: 44px; + padding: 0.625rem 1.5rem; + font-size: 0.9375rem; + font-weight: 600; + color: #fff; + background: #22c55e; + border: none; + border-radius: 100px; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3); + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; + white-space: nowrap; } .journal-write-btn:hover:not(:disabled) { - background: #16a34a; transform: translateY(-1px); - box-shadow: 0 6px 18px rgba(34,197,94,0.4); + background: #16a34a; + transform: translateY(-1px); + box-shadow: 0 6px 18px rgba(34, 197, 94, 0.4); +} +.journal-write-btn:active:not(:disabled) { + transform: translateY(0); +} +.journal-write-btn:disabled { + opacity: 0.5; + cursor: not-allowed; } -.journal-write-btn:active:not(:disabled) { transform: translateY(0); } -.journal-write-btn:disabled { opacity: 0.5; cursor: not-allowed; } .journal-icon-btn { - display: inline-flex; align-items: center; justify-content: center; - width: 44px; height: 44px; background: transparent; border: none; - border-radius: 12px; cursor: pointer; color: #9ca3af; - transition: all 0.2s ease; -webkit-tap-highlight-color: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + background: transparent; + border: none; + border-radius: 12px; + cursor: pointer; + color: #9ca3af; + transition: all 0.2s ease; + -webkit-tap-highlight-color: transparent; +} +.journal-icon-btn:hover { + color: #6b7280; + background: rgba(0, 0, 0, 0.05); } -.journal-icon-btn:hover { color: #6b7280; background: rgba(0,0,0,0.05); } /* ============================ BOTTOM NAVIGATION — Static flex item, always at bottom ============================ */ .bottom-nav { - flex-shrink: 0; - position: relative; /* NOT fixed — lives in the flex column */ - background: rgba(255,255,255,0.96); - border-top: 1px solid rgba(0,0,0,0.07); - padding: 8px 12px 12px; - display: flex; - align-items: center; - justify-content: space-around; - gap: 4px; - z-index: 10; + flex-shrink: 0; + position: relative; /* NOT fixed — lives in the flex column */ + background: rgba(255, 255, 255, 0.96); + border-top: 1px solid rgba(0, 0, 0, 0.07); + padding: 8px 12px 12px; + display: flex; + align-items: center; + justify-content: space-around; + gap: 4px; + z-index: 10; } .bottom-nav-btn { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 5px; - padding: 8px 14px; - background: transparent; - border: none; - border-radius: 100px; - cursor: pointer; - color: #9ca3af; - transition: all 0.18s ease; - min-height: 44px; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; - -webkit-tap-highlight-color: transparent; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 5px; + padding: 8px 14px; + background: transparent; + border: none; + border-radius: 100px; + cursor: pointer; + color: #9ca3af; + transition: all 0.18s ease; + min-height: 44px; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; + -webkit-tap-highlight-color: transparent; } .bottom-nav-btn svg { - width: 18px; height: 18px; - flex-shrink: 0; stroke-width: 2; + width: 18px; + height: 18px; + flex-shrink: 0; + stroke-width: 2; } .bottom-nav-btn span { - font-size: 0.8125rem; font-weight: 600; - white-space: nowrap; display: none; + font-size: 0.8125rem; + font-weight: 600; + white-space: nowrap; + display: none; } -.bottom-nav-btn:hover { color: #6b7280; } +.bottom-nav-btn:hover { + color: #6b7280; +} .bottom-nav-btn-active { - background: #22c55e; - color: #fff; - padding: 9px 18px; + background: #22c55e; + color: #fff; + padding: 9px 18px; } -.bottom-nav-btn-active span { display: inline; } -.bottom-nav-btn-active:hover { background: #16a34a; color: #fff; } +.bottom-nav-btn-active span { + display: inline; +} +.bottom-nav-btn-active:hover { + background: #16a34a; + color: #fff; +} /* ============================ HISTORY PAGE ============================ */ .history-header { - flex-shrink: 0; - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; - padding: 1.5rem 1.25rem 0; + flex-shrink: 0; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + padding: 1.5rem 1.25rem 0; } .history-header-text h1 { - margin: 0 0 0.15rem; - font-size: 1.625rem; - font-weight: 700; - color: #1a1a1a; - letter-spacing: -0.02em; - font-family: 'Playfair Display', Georgia, serif; + margin: 0 0 0.15rem; + font-size: 1.625rem; + font-weight: 700; + color: #1a1a1a; + letter-spacing: -0.02em; + font-family: "Playfair Display", Georgia, serif; } .history-subtitle { - margin: 0; font-size: 0.875rem; - color: #6b7280; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + margin: 0; + font-size: 0.875rem; + color: #6b7280; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; } .history-search-btn { - width: 40px; height: 40px; flex-shrink: 0; - display: flex; align-items: center; justify-content: center; - background: #fff; border: 1px solid #e5e7eb; border-radius: 50%; - cursor: pointer; color: #6b7280; transition: all 0.2s ease; + width: 40px; + height: 40px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 50%; + cursor: pointer; + color: #6b7280; + transition: all 0.2s ease; +} +.history-search-btn:hover { + background: #f9fafb; + color: #374151; } -.history-search-btn:hover { background: #f9fafb; color: #374151; } /* scrollable content area for history */ .history-container { - flex: 1; - min-height: 0; - overflow-y: auto; - scrollbar-width: none; - -ms-overflow-style: none; - padding: 1rem 1.25rem 0.5rem; + flex: 1; + min-height: 0; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + padding: 1rem 1.25rem 0.5rem; +} +.history-container::-webkit-scrollbar { + display: none; } -.history-container::-webkit-scrollbar { display: none; } /* Calendar */ .calendar-card { - background: #fff; border-radius: 18px; - padding: 1.125rem 1rem; box-shadow: 0 2px 10px rgba(0,0,0,0.06); - margin-bottom: 1.125rem; + background: #fff; + border-radius: 18px; + padding: 1.125rem 1rem; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); + margin-bottom: 1.125rem; } .calendar-header { - display: flex; align-items: center; - justify-content: space-between; margin-bottom: 1rem; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; } .calendar-month { - margin: 0; font-size: 1.0625rem; font-weight: 600; color: #1a1a1a; - font-family: 'Playfair Display', Georgia, serif; + margin: 0; + font-size: 1.0625rem; + font-weight: 600; + color: #1a1a1a; + font-family: "Playfair Display", Georgia, serif; } -.calendar-nav { display: flex; gap: 2px; } +.calendar-nav { + display: flex; + gap: 2px; +} .calendar-nav-btn { - width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; - background: transparent; border: none; border-radius: 8px; cursor: pointer; - color: #9ca3af; transition: all 0.15s ease; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: 8px; + cursor: pointer; + color: #9ca3af; + transition: all 0.15s ease; +} +.calendar-nav-btn:hover { + background: #f3f4f6; + color: #374151; } -.calendar-nav-btn:hover { background: #f3f4f6; color: #374151; } .calendar-grid { - display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; } .calendar-weekday { - font-size: 0.6875rem; font-weight: 500; color: #9ca3af; - text-align: center; padding: 4px 0; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + font-size: 0.6875rem; + font-weight: 500; + color: #9ca3af; + text-align: center; + padding: 4px 0; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; } .calendar-day { - aspect-ratio: 1; display: flex; align-items: center; justify-content: center; - font-size: 0.8125rem; font-weight: 400; color: #374151; - background: transparent; border: none; border-radius: 50%; - cursor: pointer; transition: all 0.15s ease; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8125rem; + font-weight: 400; + color: #374151; + background: transparent; + border: none; + border-radius: 50%; + cursor: pointer; + transition: all 0.15s ease; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; } -.calendar-day-empty { cursor: default; } -.calendar-day:not(.calendar-day-empty):hover { background: #f0fdf4; color: #22c55e; } +.calendar-day-empty { + cursor: default; +} +.calendar-day:not(.calendar-day-empty):hover { + background: #f0fdf4; + color: #22c55e; +} .calendar-day-has-entry { - background: #dcfce7; color: #22c55e; font-weight: 600; + background: #dcfce7; + color: #22c55e; + font-weight: 600; +} +.calendar-day-has-entry:hover { + background: #bbf7d0; } -.calendar-day-has-entry:hover { background: #bbf7d0; } .calendar-day-today { - background: #22c55e; color: #fff; font-weight: 700; + background: #22c55e; + color: #fff; + font-weight: 700; +} +.calendar-day-today:hover { + background: #16a34a; } -.calendar-day-today:hover { background: #16a34a; } /* Recent Entries */ -.recent-entries { margin-bottom: 1rem; } +.recent-entries { + margin-bottom: 1rem; +} .recent-entries-title { - margin: 0 0 0.75rem; - font-size: 0.625rem; font-weight: 700; letter-spacing: 0.12em; - color: #9ca3af; text-transform: uppercase; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + margin: 0 0 0.75rem; + font-size: 0.625rem; + font-weight: 700; + letter-spacing: 0.12em; + color: #9ca3af; + text-transform: uppercase; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; } -.entries-list { display: flex; flex-direction: column; gap: 0.625rem; } +.entries-list { + display: flex; + flex-direction: column; + gap: 0.625rem; +} .entry-card { - background: #fff; padding: 1rem 1rem 1rem 0.875rem; - border-radius: 14px; border-left: 4px solid #22c55e; - box-shadow: 0 2px 6px rgba(0,0,0,0.05); cursor: pointer; - transition: all 0.2s ease; text-align: left; width: 100%; + background: #fff; + padding: 1rem 1rem 1rem 0.875rem; + border-radius: 14px; + border-left: 4px solid #22c55e; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); + cursor: pointer; + transition: all 0.2s ease; + text-align: left; + width: 100%; +} +.entry-card:hover { + transform: translateY(-2px); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.09); } -.entry-card:hover { transform: translateY(-2px); box-shadow: 0 5px 16px rgba(0,0,0,0.09); } .entry-header { - display: flex; align-items: center; justify-content: space-between; - margin-bottom: 0.375rem; gap: 0.5rem; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.375rem; + gap: 0.5rem; } .entry-date { - font-size: 0.625rem; font-weight: 600; letter-spacing: 0.08em; - color: #6b7280; text-transform: uppercase; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + font-size: 0.625rem; + font-weight: 600; + letter-spacing: 0.08em; + color: #6b7280; + text-transform: uppercase; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; } .entry-time { - font-size: 0.6875rem; color: #9ca3af; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + font-size: 0.6875rem; + color: #9ca3af; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; } .entry-title { - margin: 0 0 0.3rem; font-size: 1rem; font-weight: 600; - color: #1a1a1a; line-height: 1.3; - font-family: 'Playfair Display', Georgia, serif; + margin: 0 0 0.3rem; + font-size: 1rem; + font-weight: 600; + color: #1a1a1a; + line-height: 1.3; + font-family: "Playfair Display", Georgia, serif; } .entry-preview { - margin: 0; font-size: 0.8125rem; line-height: 1.5; color: #6b7280; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; - display: -webkit-box; -webkit-line-clamp: 2; - -webkit-box-orient: vertical; overflow: hidden; + margin: 0; + font-size: 0.8125rem; + line-height: 1.5; + color: #6b7280; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; } /* ============================ SETTINGS PAGE ============================ */ .settings-header { - flex-shrink: 0; - padding: 1.5rem 1.25rem 0; + flex-shrink: 0; + padding: 1.5rem 1.25rem 0; } .settings-header-text h1 { - margin: 0 0 0.2rem; - font-size: 1.75rem; font-weight: 700; color: #1a1a1a; - letter-spacing: -0.02em; - font-family: 'Playfair Display', Georgia, serif; + margin: 0 0 0.2rem; + font-size: 1.75rem; + font-weight: 700; + color: #1a1a1a; + letter-spacing: -0.02em; + font-family: "Playfair Display", Georgia, serif; } .settings-subtitle { - margin: 0; font-size: 0.875rem; color: #6b7280; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + margin: 0; + font-size: 0.875rem; + color: #6b7280; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; } /* scrollable content area for settings */ .settings-container { - flex: 1; - min-height: 0; - overflow-y: auto; - scrollbar-width: none; - -ms-overflow-style: none; - padding: 1rem 1.25rem 0.5rem; + flex: 1; + min-height: 0; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + padding: 1rem 1.25rem 0.5rem; +} +.settings-container::-webkit-scrollbar { + display: none; } -.settings-container::-webkit-scrollbar { display: none; } /* Profile */ .settings-profile { - display: flex; align-items: center; gap: 1rem; - margin-bottom: 1.25rem; background: #fff; - border-radius: 18px; padding: 1rem 1.125rem; - box-shadow: 0 2px 10px rgba(0,0,0,0.06); + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.25rem; + background: #fff; + border-radius: 18px; + padding: 1rem 1.125rem; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); } -.settings-avatar { width: 52px; height: 52px; flex-shrink: 0; } +.settings-avatar { + width: 52px; + height: 52px; + flex-shrink: 0; +} .settings-avatar-img { - width: 100%; height: 100%; border-radius: 50%; object-fit: cover; + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; } .settings-avatar-placeholder { - width: 100%; height: 100%; border-radius: 50%; - background: linear-gradient(135deg, #f9a8d4 0%, #f472b6 100%); - display: flex; align-items: center; justify-content: center; - font-size: 1.25rem; font-weight: 600; color: #fff; + width: 100%; + height: 100%; + border-radius: 50%; + background: linear-gradient(135deg, #f9a8d4 0%, #f472b6 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + font-weight: 600; + color: #fff; } -.settings-profile-info { flex: 1; min-width: 0; } +.settings-profile-info { + flex: 1; + min-width: 0; +} .settings-profile-name { - margin: 0 0 0.3rem; font-size: 1.0625rem; font-weight: 700; color: #1a1a1a; - font-family: 'Playfair Display', Georgia, serif; + margin: 0 0 0.3rem; + font-size: 1.0625rem; + font-weight: 700; + color: #1a1a1a; + font-family: "Playfair Display", Georgia, serif; } .settings-profile-badge { - display: inline-block; padding: 0.15rem 0.5rem; - font-size: 0.625rem; font-weight: 700; letter-spacing: 0.06em; - color: #fff; background: #22c55e; border-radius: 100px; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; - text-transform: uppercase; + display: inline-block; + padding: 0.15rem 0.5rem; + font-size: 0.625rem; + font-weight: 700; + letter-spacing: 0.06em; + color: #fff; + background: #22c55e; + border-radius: 100px; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; + text-transform: uppercase; } -.settings-section { margin-bottom: 1rem; } +.settings-section { + margin-bottom: 1rem; +} .settings-section-title { - margin: 0 0 0.5rem; - font-size: 0.625rem; font-weight: 700; letter-spacing: 0.12em; - color: #9ca3af; text-transform: uppercase; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + margin: 0 0 0.5rem; + font-size: 0.625rem; + font-weight: 700; + letter-spacing: 0.12em; + color: #9ca3af; + text-transform: uppercase; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; } .settings-card { - background: #fff; border-radius: 18px; - box-shadow: 0 2px 10px rgba(0,0,0,0.06); overflow: hidden; + background: #fff; + border-radius: 18px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); + overflow: hidden; } .settings-item { - display: flex; align-items: center; gap: 0.875rem; - padding: 0.875rem 1.125rem; background: transparent; - border: none; width: 100%; text-align: left; + display: flex; + align-items: center; + gap: 0.875rem; + padding: 0.875rem 1.125rem; + background: transparent; + border: none; + width: 100%; + text-align: left; } -.settings-item-button { cursor: pointer; transition: background 0.15s ease; } -.settings-item-button:hover { background: #f9fafb; } +.settings-item-button { + cursor: pointer; + transition: background 0.15s ease; +} +.settings-item-button:hover { + background: #f9fafb; +} .settings-item-icon { - width: 40px; height: 40px; flex-shrink: 0; - display: flex; align-items: center; justify-content: center; - border-radius: 10px; + width: 40px; + height: 40px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; } -.settings-item-icon-green { background: rgba(34,197,94,0.12); color: #22c55e; } -.settings-item-icon-gray { background: rgba(107,114,128,0.10); color: #6b7280; } -.settings-item-icon-orange { background: rgba(251,146,60,0.12); color: #f97316; } -.settings-item-icon-blue { background: rgba(59,130,246,0.12); color: #3b82f6; } +.settings-item-icon-green { + background: rgba(34, 197, 94, 0.12); + color: #22c55e; +} +.settings-item-icon-gray { + background: rgba(107, 114, 128, 0.1); + color: #6b7280; +} +.settings-item-icon-orange { + background: rgba(251, 146, 60, 0.12); + color: #f97316; +} +.settings-item-icon-blue { + background: rgba(59, 130, 246, 0.12); + color: #3b82f6; +} -.settings-item-content { flex: 1; min-width: 0; } +.settings-item-content { + flex: 1; + min-width: 0; +} .settings-item-title { - margin: 0 0 0.15rem; font-size: 0.9375rem; font-weight: 600; color: #1a1a1a; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + margin: 0 0 0.15rem; + font-size: 0.9375rem; + font-weight: 600; + color: #1a1a1a; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; } .settings-item-subtitle { - margin: 0; font-size: 0.75rem; color: #9ca3af; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + margin: 0; + font-size: 0.75rem; + color: #9ca3af; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; } -.settings-item-arrow { flex-shrink: 0; color: #d1d5db; } +.settings-item-arrow { + flex-shrink: 0; + color: #d1d5db; +} -.settings-divider { height: 1px; background: #f3f4f6; margin: 0 1.125rem; } +.settings-divider { + height: 1px; + background: #f3f4f6; + margin: 0 1.125rem; +} /* Toggle */ .settings-toggle { - position: relative; display: inline-block; - width: 46px; height: 26px; flex-shrink: 0; + position: relative; + display: inline-block; + width: 46px; + height: 26px; + flex-shrink: 0; } -.settings-toggle input { opacity: 0; width: 0; height: 0; } +.settings-toggle input { + opacity: 0; + width: 0; + height: 0; +} .settings-toggle-slider { - position: absolute; cursor: pointer; - top: 0; left: 0; right: 0; bottom: 0; - background: #d1d5db; transition: 0.25s; border-radius: 100px; + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #d1d5db; + transition: 0.25s; + border-radius: 100px; } .settings-toggle-slider:before { - position: absolute; content: ""; - height: 20px; width: 20px; left: 3px; bottom: 3px; - background: white; transition: 0.25s; border-radius: 50%; - box-shadow: 0 1px 3px rgba(0,0,0,0.15); + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 3px; + bottom: 3px; + background: white; + transition: 0.25s; + border-radius: 50%; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); } -.settings-toggle input:checked + .settings-toggle-slider { background: #22c55e; } -.settings-toggle input:checked + .settings-toggle-slider:before { transform: translateX(20px); } +.settings-toggle input:checked + .settings-toggle-slider { + background: #22c55e; +} +.settings-toggle input:checked + .settings-toggle-slider:before { + transform: translateX(20px); +} /* Theme dots */ -.settings-theme-colors { display: flex; gap: 5px; margin-right: 4px; } - -.settings-theme-dot { - width: 20px; height: 20px; border-radius: 50%; - border: 2px solid rgba(0,0,0,0.08); cursor: pointer; transition: all 0.2s; +.settings-theme-colors { + display: flex; + gap: 6px; + margin-right: 4px; } -.settings-theme-dot-beige { background: #f5f0e8; } -.settings-theme-dot-dark { background: #1a1a1a; } -.settings-theme-dot:hover { transform: scale(1.15); } +.settings-theme-dot { + width: 22px; + height: 22px; + border-radius: 50%; + border: 2px solid rgba(0, 0, 0, 0.08); + cursor: pointer; + transition: all 0.2s; + background: transparent; + padding: 0; + -webkit-tap-highlight-color: transparent; +} + +.settings-theme-dot-beige { + background: #f5f0e8; +} +.settings-theme-dot-dark { + background: #1a1a1a; +} +.settings-theme-dot:hover:not(:disabled) { + transform: scale(1.15); +} +.settings-theme-dot:disabled { + opacity: 0.6; + cursor: not-allowed; +} /* Clear Data */ .settings-clear-btn { - width: 100%; display: flex; align-items: center; justify-content: space-between; - padding: 0.875rem 1.125rem; margin-bottom: 0.75rem; - font-size: 0.9375rem; font-weight: 600; color: #dc2626; - background: #fff; border: none; border-radius: 14px; cursor: pointer; - transition: background 0.2s; font-family: 'Inter', sans-serif; - box-shadow: 0 2px 6px rgba(0,0,0,0.05); + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.875rem 1.125rem; + margin-bottom: 0.75rem; + font-size: 0.9375rem; + font-weight: 600; + color: #dc2626; + background: #fff; + border: none; + border-radius: 14px; + cursor: pointer; + transition: background 0.2s; + font-family: "Inter", sans-serif; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} +.settings-clear-btn:hover { + background: #fef2f2; } -.settings-clear-btn:hover { background: #fef2f2; } /* Sign Out */ .settings-signout-btn { - width: 100%; padding: 0.75rem; margin-bottom: 1rem; - font-size: 0.9375rem; font-weight: 600; color: #6b7280; - background: #fff; border: none; border-radius: 14px; cursor: pointer; - transition: background 0.2s; font-family: 'Inter', sans-serif; - box-shadow: 0 2px 6px rgba(0,0,0,0.05); + width: 100%; + padding: 0.75rem; + margin-bottom: 1rem; + font-size: 0.9375rem; + font-weight: 600; + color: #6b7280; + background: #fff; + border: none; + border-radius: 14px; + cursor: pointer; + transition: background 0.2s; + font-family: "Inter", sans-serif; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} +.settings-signout-btn:hover { + background: #f9fafb; + color: #374151; } -.settings-signout-btn:hover { background: #f9fafb; color: #374151; } /* Version */ .settings-version { - text-align: center; font-size: 0.625rem; font-weight: 500; - letter-spacing: 0.08em; color: #d1d5db; margin: 0.75rem 0 0.5rem; - text-transform: uppercase; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + text-align: center; + font-size: 0.625rem; + font-weight: 500; + letter-spacing: 0.08em; + color: #d1d5db; + margin: 0.75rem 0 0.5rem; + text-transform: uppercase; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; } diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 4aafeb1..3c4c86f 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -13,12 +13,12 @@ import { signOut as firebaseSignOut, type User, } from 'firebase/auth' -import { auth, googleProvider, db } from '../lib/firebase' -import { doc, setDoc } from 'firebase/firestore' -import { COLLECTIONS } from '../lib/firestoreConfig' +import { auth, googleProvider } from '../lib/firebase' +import { registerUser, getUserByEmail } from '../lib/api' type AuthContextValue = { user: User | null + userId: string | null loading: boolean signInWithGoogle: () => Promise signOut: () => Promise @@ -28,21 +28,33 @@ const AuthContext = createContext(null) export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null) + const [userId, setUserId] = useState(null) const [loading, setLoading] = useState(true) - // Save user info to Firestore when they authenticate - async function saveUserToFirestore(authUser: User) { + // Register or fetch user from MongoDB + async function syncUserWithDatabase(authUser: User) { try { - const userRef = doc(db, COLLECTIONS.USERS, authUser.uid) - await setDoc(userRef, { - id: authUser.uid, - email: authUser.email || '', - displayName: authUser.displayName || '', - photoURL: authUser.photoURL || '', - lastLoginAt: Date.now(), - }, { merge: true }) + const token = await authUser.getIdToken() + const email = authUser.email! + + // Try to get existing user + try { + const existingUser = await getUserByEmail(email, token) + setUserId(existingUser.id) + } catch (error) { + // User doesn't exist, register them + const newUser = await registerUser( + { + email, + displayName: authUser.displayName || undefined, + photoURL: authUser.photoURL || undefined, + }, + token + ) + setUserId(newUser.id) + } } catch (error) { - console.error('Error saving user to Firestore:', error) + console.error('Error syncing user with database:', error) } } @@ -50,7 +62,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { const unsubscribe = onAuthStateChanged(auth, async (u) => { setUser(u) if (u) { - await saveUserToFirestore(u) + await syncUserWithDatabase(u) + } else { + setUserId(null) } setLoading(false) }) @@ -64,10 +78,12 @@ export function AuthProvider({ children }: { children: ReactNode }) { async function signOut() { await firebaseSignOut(auth) + setUserId(null) } const value: AuthContextValue = { user, + userId, loading, signInWithGoogle, signOut, diff --git a/src/index.css b/src/index.css index 54fe27d..31698b5 100644 --- a/src/index.css +++ b/src/index.css @@ -2,91 +2,95 @@ *, *::before, *::after { - box-sizing: border-box; + box-sizing: border-box; } :root { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", sans-serif; - line-height: 1.5; - font-weight: 400; - /* Fixed 16px – we're always rendering at phone scale */ - font-size: 16px; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Helvetica Neue", sans-serif; + line-height: 1.5; + font-weight: 400; + /* Fixed 16px – we're always rendering at phone scale */ + font-size: 16px; - --touch-min: 44px; + --touch-min: 44px; - --color-primary: #22c55e; - --color-primary-hover: #16a34a; - --color-bg-soft: #f5f0e8; - --color-surface: #ffffff; - --color-accent-light: #dcfce7; - --color-text: #1a1a1a; - --color-text-muted: #6b7280; - --color-border: #e5e7eb; + --color-primary: #22c55e; + --color-primary-hover: #16a34a; + --color-bg-soft: #f5f0e8; + --color-surface: #ffffff; + --color-accent-light: #dcfce7; + --color-text: #1a1a1a; + --color-text-muted: #6b7280; + --color-border: #e5e7eb; - color: var(--color-text); - background-color: var(--color-bg-soft); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + color: var(--color-text); + background-color: var(--color-bg-soft); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } a { - font-weight: 500; - color: var(--color-primary); - text-decoration: inherit; + font-weight: 500; + color: var(--color-primary); + text-decoration: inherit; } a:hover { - color: var(--color-primary-hover); + color: var(--color-primary-hover); } html { - height: 100%; - -webkit-text-size-adjust: 100%; + height: 100%; + -webkit-text-size-adjust: 100%; } body { - margin: 0; - height: 100%; - min-height: 100dvh; - overflow: hidden; - /* Desktop: show as phone on a desk surface */ - background: #ccc8c0; + margin: 0; + height: 100%; + min-height: 100dvh; + overflow: hidden; + /* Desktop: show as phone on a desk surface */ + background: #ccc8c0; } /* ── Phone shell on desktop ───────────────────────────── */ @media (min-width: 600px) { - body { - display: flex; - align-items: center; - justify-content: center; - background: #bbb7af; - } + body { + display: flex; + align-items: center; + justify-content: center; + background: #bbb7af; + } - #root { - width: 390px !important; - max-width: 390px !important; - height: 100dvh; - max-height: 100dvh; - border-radius: 0; - overflow: hidden; - box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35), 0 4px 16px rgba(0, 0, 0, 0.2); - position: relative; - flex-shrink: 0; - } + #root { + width: 390px !important; + max-width: 390px !important; + height: 100dvh; + max-height: 100dvh; + border-radius: 0; + overflow: hidden; + box-shadow: + 0 24px 80px rgba(0, 0, 0, 0.35), + 0 4px 16px rgba(0, 0, 0, 0.2); + position: relative; + flex-shrink: 0; + } } h1 { - font-size: 1.75rem; - line-height: 1.2; + font-size: 1.75rem; + line-height: 1.2; } button { - font-family: inherit; - cursor: pointer; + font-family: inherit; + cursor: pointer; } button:focus, button:focus-visible { - outline: 2px solid var(--color-primary); - outline-offset: 2px; + outline: 2px solid var(--color-primary); + outline-offset: 2px; } diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..1def8e4 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,173 @@ +/** + * API Service Layer + * Handles all communication with the backend API + */ + +const API_BASE_URL = 'http://localhost:8001' + +type ApiOptions = { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' + body?: unknown + token?: string | null +} + +async function apiCall( + endpoint: string, + options: ApiOptions = {} +): Promise { + const { method = 'GET', body, token } = options + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + } + + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + + const config: RequestInit = { + method, + headers, + credentials: 'include', + } + + if (body) { + config.body = JSON.stringify(body) + } + + const response = await fetch(`${API_BASE_URL}${endpoint}`, config) + + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.detail || `API error: ${response.statusText}`) + } + + return response.json() as Promise +} + +// ============================================ +// USER ENDPOINTS +// ============================================ + +export async function registerUser( + userData: { + email: string + displayName?: string + photoURL?: string + }, + token: string +) { + return apiCall('/api/users/register', { + method: 'POST', + body: userData, + token, + }) +} + +export async function getUserByEmail(email: string, token: string) { + return apiCall(`/api/users/by-email/${email}`, { token }) +} + +export async function updateUserProfile( + userId: string, + updates: { displayName?: string; photoURL?: string; theme?: string }, + token: string +) { + return apiCall(`/api/users/update/${userId}`, { + method: 'PUT', + body: updates, + token, + }) +} + +// ============================================ +// ENTRY ENDPOINTS +// ============================================ + +export interface JournalEntryCreate { + title: string + content: string + mood?: string + tags?: string[] + isPublic?: boolean +} + +export interface JournalEntry extends JournalEntryCreate { + id: string + userId: string + createdAt: string + updatedAt: string +} + +export async function createEntry( + userId: string, + entryData: JournalEntryCreate, + token: string +) { + return apiCall<{ id: string; message: string }>( + `/api/entries/${userId}`, + { + method: 'POST', + body: entryData, + token, + } + ) +} + +export async function getUserEntries( + userId: string, + token: string, + limit = 50, + skip = 0 +) { + return apiCall<{ entries: JournalEntry[]; total: number }>( + `/api/entries/${userId}?limit=${limit}&skip=${skip}`, + { token } + ) +} + +export async function getEntry( + userId: string, + entryId: string, + token: string +) { + return apiCall(`/api/entries/${userId}/${entryId}`, { + token, + }) +} + +export async function updateEntry( + userId: string, + entryId: string, + updates: Partial, + token: string +) { + return apiCall(`/api/entries/${userId}/${entryId}`, { + method: 'PUT', + body: updates, + token, + }) +} + +export async function deleteEntry( + userId: string, + entryId: string, + token: string +) { + return apiCall(`/api/entries/${userId}/${entryId}`, { + method: 'DELETE', + token, + }) +} + +export async function getEntriesByDate( + userId: string, + startDate: string, + endDate: string, + token: string +) { + return apiCall( + `/api/entries/${userId}/date-range?startDate=${startDate}&endDate=${endDate}`, + { token } + ) +} diff --git a/src/lib/firebase.ts b/src/lib/firebase.ts index 7dcec13..ed7eacb 100644 --- a/src/lib/firebase.ts +++ b/src/lib/firebase.ts @@ -1,11 +1,9 @@ import { initializeApp } from 'firebase/app' import { getAuth, GoogleAuthProvider } from 'firebase/auth' -import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore' const firebaseConfig = { apiKey: import.meta.env.VITE_FIREBASE_API_KEY, authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, - databaseURL: import.meta.env.VITE_FIREBASE_DATABASE_URL, projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID, @@ -14,14 +12,6 @@ const firebaseConfig = { const app = initializeApp(firebaseConfig) -// Auth initialization +// Google Auth initialization export const auth = getAuth(app) export const googleProvider = new GoogleAuthProvider() - -// Firestore initialization -export const db = getFirestore(app) - -// Enable Firestore emulator in development (uncomment when testing locally) -// if (import.meta.env.DEV) { -// connectFirestoreEmulator(db, 'localhost', 8080) -// } diff --git a/src/pages/HistoryPage.tsx b/src/pages/HistoryPage.tsx index d194536..dc1f7a1 100644 --- a/src/pages/HistoryPage.tsx +++ b/src/pages/HistoryPage.tsx @@ -1,17 +1,33 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' +import { useAuth } from '../contexts/AuthContext' +import { getUserEntries, type JournalEntry } from '../lib/api' import BottomNav from '../components/BottomNav' -interface JournalEntry { - id: string - date: Date - title: string - content: string -} - export default function HistoryPage() { + const { user, userId, loading } = useAuth() const [currentMonth, setCurrentMonth] = useState(new Date()) - - const entries: JournalEntry[] = [] + const [entries, setEntries] = useState([]) + const [loadingEntries, setLoadingEntries] = useState(false) + + // Fetch entries on mount and when userId changes + useEffect(() => { + if (!user || !userId) return + + const fetchEntries = async () => { + setLoadingEntries(true) + try { + const token = await user.getIdToken() + const response = await getUserEntries(userId, token, 100, 0) + setEntries(response.entries) + } catch (error) { + console.error('Error fetching entries:', error) + } finally { + setLoadingEntries(false) + } + } + + fetchEntries() + }, [user, userId]) const getDaysInMonth = (date: Date) => { const year = date.getFullYear() @@ -20,43 +36,50 @@ export default function HistoryPage() { const lastDay = new Date(year, month + 1, 0) const daysInMonth = lastDay.getDate() const startingDayOfWeek = firstDay.getDay() - + return { daysInMonth, startingDayOfWeek } } const hasEntryOnDate = (day: number) => { - return entries.some(entry => { - const entryDate = new Date(entry.date) - return entryDate.getDate() === day && + return entries.some((entry) => { + const entryDate = new Date(entry.createdAt) + return ( + entryDate.getDate() === day && entryDate.getMonth() === currentMonth.getMonth() && entryDate.getFullYear() === currentMonth.getFullYear() + ) }) } const isToday = (day: number) => { const today = new Date() - return day === today.getDate() && + return ( + day === today.getDate() && currentMonth.getMonth() === today.getMonth() && currentMonth.getFullYear() === today.getFullYear() + ) } - const formatDate = (date: Date) => { - return date.toLocaleDateString('en-US', { - weekday: 'short', - month: 'short', - day: '2-digit' + const formatDate = (date: string) => { + return new Date(date).toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: '2-digit', }).toUpperCase() } - const formatTime = (date: Date) => { - return date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit' + const formatTime = (date: string) => { + return new Date(date).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', }).toUpperCase() } const { daysInMonth, startingDayOfWeek } = getDaysInMonth(currentMonth) - const monthName = currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }) + const monthName = currentMonth.toLocaleDateString('en-US', { + month: 'long', + year: 'numeric', + }) const previousMonth = () => { setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1)) @@ -66,6 +89,24 @@ export default function HistoryPage() { setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1)) } + // Get entries for current month + const currentMonthEntries = entries.filter((entry) => { + const entryDate = new Date(entry.createdAt) + return ( + entryDate.getMonth() === currentMonth.getMonth() && + entryDate.getFullYear() === currentMonth.getFullYear() + ) + }) + + if (loading) { + return ( +
+

Loading…

+ +
+ ) + } + return (
@@ -116,7 +157,7 @@ export default function HistoryPage() { const day = i + 1 const hasEntry = hasEntryOnDate(day) const isTodayDate = isToday(day) - + return ( - ))} -
+ + {loadingEntries ? ( +

+ Loading entries… +

+ ) : ( +
+ {currentMonthEntries.length === 0 ? ( +

+ No entries for this month yet. Start writing! +

+ ) : ( + currentMonthEntries.map((entry) => ( + + )) + )} +
+ )} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 5091845..4c3568c 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,12 +1,15 @@ import { useAuth } from '../contexts/AuthContext' import { Link } from 'react-router-dom' import { useState } from 'react' +import { createEntry } from '../lib/api' import BottomNav from '../components/BottomNav' export default function HomePage() { - const { user, loading, signOut } = useAuth() + const { user, userId, loading, signOut } = useAuth() const [entry, setEntry] = useState('') const [title, setTitle] = useState('') + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) if (loading) { return ( @@ -32,10 +35,39 @@ export default function HomePage() { .toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' }) .toUpperCase() - const handleWrite = () => { - // TODO: Save to Firebase - setTitle('') - setEntry('') + const handleWrite = async () => { + if (!userId || !title.trim() || !entry.trim()) { + setMessage({ type: 'error', text: 'Please add a title and entry content' }) + return + } + + setSaving(true) + setMessage(null) + + try { + const token = await user.getIdToken() + await createEntry( + userId, + { + title: title.trim(), + content: entry.trim(), + isPublic: false, + }, + token + ) + + setMessage({ type: 'success', text: 'Entry saved successfully!' }) + setTitle('') + setEntry('') + + // Clear success message after 3 seconds + setTimeout(() => setMessage(null), 3000) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to save entry' + setMessage({ type: 'error', text: errorMessage }) + } finally { + setSaving(false) + } } return ( @@ -53,14 +85,40 @@ export default function HomePage() { placeholder="Title your thoughts..." value={title} onChange={(e) => setTitle(e.target.value)} + disabled={saving} />