Compare commits
3 Commits
190243081a
...
eabf295f2e
| Author | SHA1 | Date | |
|---|---|---|---|
| eabf295f2e | |||
| a9eaa7599c | |||
| bed32863da |
10
.env.example
10
.env.example
@@ -1,10 +0,0 @@
|
|||||||
# Firebase – copy to .env and fill with your Firebase project config
|
|
||||||
# Get these from Firebase Console → Project settings → General → Your apps
|
|
||||||
|
|
||||||
VITE_FIREBASE_API_KEY=
|
|
||||||
VITE_FIREBASE_AUTH_DOMAIN=
|
|
||||||
VITE_FIREBASE_DATABASE_URL=
|
|
||||||
VITE_FIREBASE_PROJECT_ID=
|
|
||||||
VITE_FIREBASE_STORAGE_BUCKET=
|
|
||||||
VITE_FIREBASE_MESSAGING_SENDER_ID=
|
|
||||||
VITE_FIREBASE_APP_ID=
|
|
||||||
198
.github/copilot-instructions.md
vendored
198
.github/copilot-instructions.md
vendored
@@ -1,116 +1,112 @@
|
|||||||
<!-- BMAD:START -->
|
# Grateful Journal — Project Instructions for Copilot
|
||||||
# BMAD Method — Project Instructions
|
|
||||||
|
|
||||||
## Project Configuration
|
## Project Overview
|
||||||
|
|
||||||
- **Project**: grateful-journal
|
**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.
|
||||||
- **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
|
**User:** Jeet
|
||||||
|
|
||||||
- **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
|
## Technology Stack & Versions
|
||||||
|
|
||||||
- Always load `_bmad/bmm/config.yaml` before any agent activation or workflow execution
|
| Layer | Technology | Notes |
|
||||||
- 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
|
| Frontend | React 19, TypeScript | Vite 7 build; port 8000 |
|
||||||
- YAML-based workflows require the workflow engine — load `workflow.xml` first, then pass the `.yaml` config
|
| Routing | react-router-dom 7 | Routes: `/`, `/history`, `/settings`, `/login` |
|
||||||
- Follow step-based workflow execution: load steps JIT, never multiple at once
|
| Auth | Firebase 12 | Google sign-in only (no database) |
|
||||||
- Save outputs after EACH step when using the workflow engine
|
| Styling | Plain CSS | `src/index.css` (globals), `src/App.css` (components) |
|
||||||
- The `{project-root}` variable resolves to the workspace root at runtime
|
| Backend | FastAPI 0.104 | Python; port 8001; modular routes |
|
||||||
|
| Database | MongoDB 6.x | Local instance; collections: users, entries, settings |
|
||||||
|
|
||||||
## Available Agents
|
---
|
||||||
|
|
||||||
| Agent | Persona | Title | Capabilities |
|
## Critical Implementation Rules
|
||||||
|---|---|---|---|
|
|
||||||
| 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
|
### Frontend
|
||||||
|
|
||||||
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.
|
- **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).
|
||||||
|
|
||||||
### Workflow Commands
|
### Backend
|
||||||
|
|
||||||
| Command | Action |
|
- **Framework:** FastAPI. APIs in Python only.
|
||||||
|---|---|
|
- **Modularity:** Separate file per route. Each feature (users, entries) has its own router module.
|
||||||
| `/bmad-help` | Load and follow `_bmad/core/tasks/help.md` |
|
- **Database:** MongoDB. Setup instructions in `docs/MONGODB_SETUP.md`.
|
||||||
| `/bmad-brainstorming` | Load and follow `_bmad/core/workflows/brainstorming/workflow.md` |
|
- **Port:** 8001 (backend); 8000 (frontend). CORS configured between them.
|
||||||
| `/bmad-party-mode` | Load and follow `_bmad/core/workflows/party-mode/workflow.md` |
|
- **Authentication:** Relies on Firebase Google Auth token from frontend (passed in Authorization header).
|
||||||
| `/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
|
### Conventions
|
||||||
|
|
||||||
| Command | Agent File |
|
- **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.
|
||||||
| `/bmad-bmad-master` | `_bmad/core/agents/bmad-master.md` |
|
- **Build:** Fixing the current TypeScript/ESLint build errors is deferred to a later step; do not assume a clean build when adding features.
|
||||||
| `/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.
|
---
|
||||||
<!-- BMAD:END -->
|
|
||||||
|
## 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 (Google sign-in only)
|
||||||
|
api.ts # API client for backend calls
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
✅ Firebase Google Auth kept (Firestore completely removed)
|
||||||
|
✅ MongoDB as single source of truth
|
||||||
|
### 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_
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
stepsCompleted: [1, 2]
|
||||||
|
inputDocuments: []
|
||||||
|
session_topic: 'Grateful Journal — minimal private-first gratitude journaling app. 3 pages (main writing, calendar, profile) + Google auth. Green palette (Coolors). Responsive web, client-side encryption.'
|
||||||
|
session_goals: 'Feature and UX ideas for writing page, calendar page, profile page, and auth flow; copy and minimal UI; edge cases and trust; differentiation.'
|
||||||
|
selected_approach: 'Progressive Technique Flow'
|
||||||
|
techniques_used: ['What If Scenarios', 'Mind Mapping', 'Yes And Building', 'Decision Tree Mapping']
|
||||||
|
ideas_generated: []
|
||||||
|
context_file: '_bmad-output/brainstorming/brainstorming-session-2025-02-18.md'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Brainstorming Session Results
|
||||||
|
|
||||||
|
**Facilitator:** Jeet
|
||||||
|
**Date:** 2025-03-04
|
||||||
|
|
||||||
|
## Session Overview
|
||||||
|
|
||||||
|
**Topic:** Grateful Journal — a minimal, private-first responsive writing app for gratitude and reflection. Three main pages (main writing page, calendar for previous writings, profile) plus one Google auth page. Green colour scheme from Coolors palette (#1be62c, #f1eee1, #ffffff, #cff2dc, #c3fd2f). No feeds, no algorithms; privacy by design with client-side encryption; daily use, even one sentence.
|
||||||
|
|
||||||
|
**Goals:** Generate ideas for features, UX, and flows for the writing space, calendar, profile, and auth; minimal UI and copy; edge cases and trust; what makes it feel distinct.
|
||||||
|
|
||||||
|
### Context Guidance
|
||||||
|
|
||||||
|
Session context resumed from **brainstorming-session-2025-02-18.md**. Same topic and goals; frontend-first (Vite + React), then backend (Python/FastAPI); agile learning focus.
|
||||||
|
|
||||||
|
### Session Setup
|
||||||
|
|
||||||
|
Resumed from previous discussion. Topic and goals confirmed; ready for technique selection.
|
||||||
|
|
||||||
|
## Technique Selection
|
||||||
|
|
||||||
|
**Approach:** Progressive Technique Flow
|
||||||
|
**Journey Design:** Systematic development from exploration to action (aligned with Grateful Journal scope and frontend-first, agile learning).
|
||||||
|
|
||||||
|
**Progressive Techniques:**
|
||||||
|
|
||||||
|
- **Phase 1 - Exploration:** What If Scenarios — maximum idea generation without constraints
|
||||||
|
- **Phase 2 - Pattern Recognition:** Mind Mapping — organise ideas and find connections
|
||||||
|
- **Phase 3 - Development:** Yes And Building — refine and deepen promising concepts
|
||||||
|
- **Phase 4 - Action Planning:** Decision Tree Mapping — implementation paths and next steps (frontend-first, then backend)
|
||||||
|
|
||||||
|
**Journey Rationale:** Broad-to-focused flow fits Grateful Journal’s scope (3 pages + auth, green UI) and your frontend-first approach; Phase 4 can target UI milestones and later backend integration.
|
||||||
|
|
||||||
|
## Remaining procedure (project roadmap)
|
||||||
|
|
||||||
|
- **Purpose & UI:** Website purpose decided; UI created. Next: UI changes only — **make layout responsive for all screens**.
|
||||||
|
- **Data:** Then **setup MongoDB**.
|
||||||
|
- **Backend:** Then backend in **Python**: **FastAPI**, **modular** — separate file per page and its functions; **each page has its own backend**.
|
||||||
6
backend/.env.example
Normal file
6
backend/.env.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
MONGODB_URI=mongodb://localhost:27017
|
||||||
|
MONGODB_DB_NAME=grateful_journal
|
||||||
|
API_PORT=8001
|
||||||
|
ENVIRONMENT=development
|
||||||
|
FRONTEND_URL=http://localhost:8000
|
||||||
|
|
||||||
88
backend/README.md
Normal file
88
backend/README.md
Normal file
@@ -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
|
||||||
19
backend/config.py
Normal file
19
backend/config.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from pydantic_settings import BaseSettings # type: ignore
|
||||||
|
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()
|
||||||
31
backend/db.py
Normal file
31
backend/db.py
Normal file
@@ -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()
|
||||||
65
backend/main.py
Normal file
65
backend/main.py
Normal file
@@ -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"
|
||||||
|
)
|
||||||
84
backend/models.py
Normal file
84
backend/models.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
from pydantic import BaseModel, Field # type: ignore
|
||||||
|
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
|
||||||
8
backend/requirements.txt
Normal file
8
backend/requirements.txt
Normal file
@@ -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
|
||||||
1
backend/routers/__init__.py
Normal file
1
backend/routers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Routers package
|
||||||
165
backend/routers/entries.py
Normal file
165
backend/routers/entries.py
Normal file
@@ -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))
|
||||||
99
backend/routers/users.py
Normal file
99
backend/routers/users.py
Normal file
@@ -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))
|
||||||
296
docs/FIRESTORE_SETUP.md
Normal file
296
docs/FIRESTORE_SETUP.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# Firebase Firestore Setup Guide
|
||||||
|
|
||||||
|
This document explains how to set up and use Firebase Firestore in the grateful-journal project.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Firebase project created at [console.firebase.google.com](https://console.firebase.google.com)
|
||||||
|
- Node.js and npm installed
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Firebase is already installed in this project. To verify, check the `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"firebase": "^12.9.0",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### 1. Set Environment Variables
|
||||||
|
|
||||||
|
Copy the existing `.env.example` to `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add Firebase Project Credentials
|
||||||
|
|
||||||
|
1. Go to [Firebase Console](https://console.firebase.google.com)
|
||||||
|
2. Select your project
|
||||||
|
3. Go to **Project Settings** (gear icon)
|
||||||
|
4. Under "Your apps", find your web app
|
||||||
|
5. Copy the Firebase config object
|
||||||
|
6. Fill in the following variables in `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_FIREBASE_API_KEY=your-api-key
|
||||||
|
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
|
||||||
|
VITE_FIREBASE_DATABASE_URL=https://your-project.firebaseio.com
|
||||||
|
VITE_FIREBASE_PROJECT_ID=your-project-id
|
||||||
|
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
|
||||||
|
VITE_FIREBASE_MESSAGING_SENDER_ID=your-sender-id
|
||||||
|
VITE_FIREBASE_APP_ID=your-app-id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Enable Firestore in Firebase Console
|
||||||
|
|
||||||
|
1. In Firebase Console, go to **Firestore Database**
|
||||||
|
2. Click **Create Database**
|
||||||
|
3. Choose **Start in test mode** (for development) or set up security rules
|
||||||
|
4. Select your region
|
||||||
|
5. Click **Create**
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/lib/
|
||||||
|
├── firebase.ts # Main Firebase initialization
|
||||||
|
├── firestoreService.ts # Generic Firestore CRUD operations
|
||||||
|
└── firestoreConfig.ts # Collection names and data interfaces
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Import the Firestore instance
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { db } from "@/lib/firebase";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use the Firestore service
|
||||||
|
|
||||||
|
The `firestoreService.ts` file provides generic CRUD operations:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
setDocument,
|
||||||
|
getDocument,
|
||||||
|
getDocuments,
|
||||||
|
queryDocuments,
|
||||||
|
updateDocument,
|
||||||
|
deleteDocument,
|
||||||
|
} from '@/lib/firestoreService'
|
||||||
|
import { COLLECTIONS, JournalEntry } from '@/lib/firestoreConfig'
|
||||||
|
|
||||||
|
// Create or update a journal entry
|
||||||
|
await setDocument(COLLECTIONS.ENTRIES, entryId, {
|
||||||
|
title: 'Today's thoughts',
|
||||||
|
content: 'I am grateful for...',
|
||||||
|
mood: 'grateful',
|
||||||
|
userId: userId,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get a single entry
|
||||||
|
const entry = await getDocument<JournalEntry>(COLLECTIONS.ENTRIES, entryId)
|
||||||
|
|
||||||
|
// Get all entries
|
||||||
|
const entries = await getDocuments<JournalEntry>(COLLECTIONS.ENTRIES)
|
||||||
|
|
||||||
|
// Query entries with conditions
|
||||||
|
import { where, orderBy } from 'firebase/firestore'
|
||||||
|
|
||||||
|
const userEntries = await queryDocuments<JournalEntry>(COLLECTIONS.ENTRIES, [
|
||||||
|
where('userId', '==', userId),
|
||||||
|
orderBy('createdAt', 'desc'),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Update an entry
|
||||||
|
await updateDocument(COLLECTIONS.ENTRIES, entryId, {
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete an entry
|
||||||
|
await deleteDocument(COLLECTIONS.ENTRIES, entryId)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createWriteBatch, commitBatch } from "@/lib/firestoreService";
|
||||||
|
|
||||||
|
const batch = createWriteBatch();
|
||||||
|
|
||||||
|
// Add operations to batch
|
||||||
|
batch.set(doc(db, COLLECTIONS.ENTRIES, entryId1), entryData1);
|
||||||
|
batch.update(doc(db, COLLECTIONS.ENTRIES, entryId2), { mood: "happy" });
|
||||||
|
batch.delete(doc(db, COLLECTIONS.ENTRIES, entryId3));
|
||||||
|
|
||||||
|
// Commit all at once
|
||||||
|
await commitBatch(batch);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Firestore Collections Schema
|
||||||
|
|
||||||
|
### users
|
||||||
|
|
||||||
|
Document ID: User's auth UID
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
email: string
|
||||||
|
displayName?: string
|
||||||
|
photoURL?: string
|
||||||
|
createdAt: number (timestamp)
|
||||||
|
updatedAt: number (timestamp)
|
||||||
|
theme?: 'light' | 'dark'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### entries
|
||||||
|
|
||||||
|
Document ID: Auto-generated or custom ID
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
userId: string (reference to user)
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
mood?: 'happy' | 'sad' | 'neutral' | 'anxious' | 'grateful'
|
||||||
|
tags?: string[]
|
||||||
|
isPublic?: boolean
|
||||||
|
createdAt: number (timestamp)
|
||||||
|
updatedAt: number (timestamp)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### settings
|
||||||
|
|
||||||
|
Document ID: User's auth UID
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
userId: string
|
||||||
|
notifications?: boolean
|
||||||
|
emailNotifications?: boolean
|
||||||
|
theme?: 'light' | 'dark' | 'system'
|
||||||
|
language?: string
|
||||||
|
updatedAt: number (timestamp)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### tags
|
||||||
|
|
||||||
|
Document ID: Auto-generated or custom ID
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
userId: string
|
||||||
|
name: string
|
||||||
|
color?: string
|
||||||
|
createdAt: number (timestamp)
|
||||||
|
updatedAt: number (timestamp)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Rules (Production)
|
||||||
|
|
||||||
|
For production, update Firestore security rules in the Firebase Console:
|
||||||
|
|
||||||
|
```firestore-rules
|
||||||
|
rules_version = '2';
|
||||||
|
service cloud.firestore {
|
||||||
|
match /databases/{database}/documents {
|
||||||
|
// Users can only read/write their own user document
|
||||||
|
match /users/{userId} {
|
||||||
|
allow read, write: if request.auth.uid == userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users can only read/write their own entries
|
||||||
|
match /entries/{document=**} {
|
||||||
|
allow read, write: if request.auth.uid == resource.data.userId;
|
||||||
|
allow create: if request.auth.uid == request.resource.data.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users can only read/write their own settings
|
||||||
|
match /settings/{userId} {
|
||||||
|
allow read, write: if request.auth.uid == userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users can only read/write their own tags
|
||||||
|
match /tags/{document=**} {
|
||||||
|
allow read, write: if request.auth.uid == resource.data.userId;
|
||||||
|
allow create: if request.auth.uid == request.resource.data.userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Emulator (Optional)
|
||||||
|
|
||||||
|
To test locally without hitting your Firebase backend:
|
||||||
|
|
||||||
|
### 1. Install Firebase CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g firebase-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Initialize Firebase Emulator
|
||||||
|
|
||||||
|
```bash
|
||||||
|
firebase init emulators
|
||||||
|
```
|
||||||
|
|
||||||
|
Select Firestore and Authentication emulators.
|
||||||
|
|
||||||
|
### 3. Start the Emulator
|
||||||
|
|
||||||
|
```bash
|
||||||
|
firebase emulators:start
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Enable Emulator in Your App (Optional)
|
||||||
|
|
||||||
|
Update `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_FIREBASE_EMULATOR_ENABLED=true
|
||||||
|
VITE_FIRESTORE_EMULATOR_HOST=localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
The emulator connection is already commented in `src/lib/firebase.ts` - uncomment it when needed.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Permission Denied Error
|
||||||
|
|
||||||
|
- Check your Firestore security rules in Firebase Console
|
||||||
|
- Ensure your `.env` variables are correct
|
||||||
|
- Make sure you're authenticated before writing to Firestore
|
||||||
|
|
||||||
|
### Document Not Found
|
||||||
|
|
||||||
|
- Verify the collection name and document ID
|
||||||
|
- Check if the document exists in Firestore Console
|
||||||
|
- Ensure the user has read permissions
|
||||||
|
|
||||||
|
### Emulator Not Connecting
|
||||||
|
|
||||||
|
- Ensure Firebase emulator is running: `firebase emulators:start`
|
||||||
|
- Check that the emulator host matches your `.env` configuration
|
||||||
|
- Verify port 8080 is available
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Firebase Firestore Documentation](https://firebase.google.com/docs/firestore)
|
||||||
|
- [Firestore Best Practices](https://firebase.google.com/docs/firestore/best-practices)
|
||||||
|
- [Firebase Security Rules](https://firebase.google.com/docs/firestore/security/start)
|
||||||
219
docs/MONGODB_SETUP.md
Normal file
219
docs/MONGODB_SETUP.md
Normal file
@@ -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
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lora:ital,wght@0,400;0,500;1,400&family=Playfair+Display:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lora:ital,wght@0,400;0,500;1,400&family=Playfair+Display:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|||||||
133
project-context.md
Normal file
133
project-context.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Project Context for AI Agents
|
||||||
|
|
||||||
|
_This file contains critical rules and patterns that AI agents must follow when implementing code in this project. Focus on unobvious details that agents might otherwise miss._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project overview
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**User:** Jeet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technology stack & versions
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical implementation rules
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- **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).
|
||||||
|
|
||||||
|
### Backend (when implemented)
|
||||||
|
|
||||||
|
- **Framework:** FastAPI. APIs in Python only.
|
||||||
|
- **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
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 (Google sign-in only)
|
||||||
|
api.ts # API client for backend calls
|
||||||
|
|
||||||
|
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: 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)
|
||||||
|
✅ Firebase Google Auth kept (Firestore completely removed)
|
||||||
|
✅ MongoDB as single source of truth
|
||||||
|
### 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
|
||||||
1914
src/App.css
1914
src/App.css
File diff suppressed because it is too large
Load Diff
@@ -8,38 +8,47 @@ export default function BottomNav() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bottom-nav">
|
<nav className="bottom-nav">
|
||||||
|
{/* Write */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`bottom-nav-btn ${isActive('/') ? 'bottom-nav-btn-active' : ''}`}
|
className={`bottom-nav-btn ${isActive('/') ? 'bottom-nav-btn-active' : ''}`}
|
||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
|
aria-label="Write"
|
||||||
>
|
>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
{/* Pencil / edit icon */}
|
||||||
<path d="M12 19l7-7 3 3-7 7-3-3z"></path>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path>
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
<path d="M2 2l7.586 7.586"></path>
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
<circle cx="11" cy="11" r="2"></circle>
|
|
||||||
</svg>
|
</svg>
|
||||||
<span>Write</span>
|
<span>Write</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* History */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`bottom-nav-btn ${isActive('/history') ? 'bottom-nav-btn-active' : ''}`}
|
className={`bottom-nav-btn ${isActive('/history') ? 'bottom-nav-btn-active' : ''}`}
|
||||||
onClick={() => navigate('/history')}
|
onClick={() => navigate('/history')}
|
||||||
|
aria-label="History"
|
||||||
>
|
>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
{/* Clock icon */}
|
||||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<polyline points="12 6 12 12 16 14" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>History</span>
|
<span>History</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Settings */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`bottom-nav-btn ${isActive('/settings') ? 'bottom-nav-btn-active' : ''}`}
|
className={`bottom-nav-btn ${isActive('/settings') ? 'bottom-nav-btn-active' : ''}`}
|
||||||
onClick={() => navigate('/settings')}
|
onClick={() => navigate('/settings')}
|
||||||
|
aria-label="Settings"
|
||||||
>
|
>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
{/* Gear icon */}
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<path d="M12 1v6m0 6v6m5.196-15.804l-4.243 4.243m-5.657 5.657l-4.243 4.243M23 12h-6m-6 0H1m15.804 5.196l-4.243-4.243m-5.657-5.657L2.661 2.661"></path>
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import {
|
|||||||
signOut as firebaseSignOut,
|
signOut as firebaseSignOut,
|
||||||
type User,
|
type User,
|
||||||
} from 'firebase/auth'
|
} from 'firebase/auth'
|
||||||
import { auth, googleProvider, db } from '../lib/firebase'
|
import { auth, googleProvider } from '../lib/firebase'
|
||||||
import { doc, setDoc } from 'firebase/firestore'
|
import { registerUser, getUserByEmail } from '../lib/api'
|
||||||
import { COLLECTIONS } from '../lib/firestoreConfig'
|
|
||||||
|
|
||||||
type AuthContextValue = {
|
type AuthContextValue = {
|
||||||
user: User | null
|
user: User | null
|
||||||
|
userId: string | null
|
||||||
loading: boolean
|
loading: boolean
|
||||||
signInWithGoogle: () => Promise<void>
|
signInWithGoogle: () => Promise<void>
|
||||||
signOut: () => Promise<void>
|
signOut: () => Promise<void>
|
||||||
@@ -28,21 +28,33 @@ const AuthContext = createContext<AuthContextValue | null>(null)
|
|||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [user, setUser] = useState<User | null>(null)
|
const [user, setUser] = useState<User | null>(null)
|
||||||
|
const [userId, setUserId] = useState<string | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
// Save user info to Firestore when they authenticate
|
// Register or fetch user from MongoDB
|
||||||
async function saveUserToFirestore(authUser: User) {
|
async function syncUserWithDatabase(authUser: User) {
|
||||||
try {
|
try {
|
||||||
const userRef = doc(db, COLLECTIONS.USERS, authUser.uid)
|
const token = await authUser.getIdToken()
|
||||||
await setDoc(userRef, {
|
const email = authUser.email!
|
||||||
id: authUser.uid,
|
|
||||||
email: authUser.email || '',
|
// Try to get existing user
|
||||||
displayName: authUser.displayName || '',
|
try {
|
||||||
photoURL: authUser.photoURL || '',
|
const existingUser = await getUserByEmail(email, token)
|
||||||
lastLoginAt: Date.now(),
|
setUserId(existingUser.id)
|
||||||
}, { merge: true })
|
} 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) {
|
} 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) => {
|
const unsubscribe = onAuthStateChanged(auth, async (u) => {
|
||||||
setUser(u)
|
setUser(u)
|
||||||
if (u) {
|
if (u) {
|
||||||
await saveUserToFirestore(u)
|
await syncUserWithDatabase(u)
|
||||||
|
} else {
|
||||||
|
setUserId(null)
|
||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
@@ -64,10 +78,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
async function signOut() {
|
async function signOut() {
|
||||||
await firebaseSignOut(auth)
|
await firebaseSignOut(auth)
|
||||||
|
setUserId(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const value: AuthContextValue = {
|
const value: AuthContextValue = {
|
||||||
user,
|
user,
|
||||||
|
userId,
|
||||||
loading,
|
loading,
|
||||||
signInWithGoogle,
|
signInWithGoogle,
|
||||||
signOut,
|
signOut,
|
||||||
|
|||||||
104
src/index.css
104
src/index.css
@@ -2,67 +2,95 @@
|
|||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
font-family:
|
||||||
line-height: 1.5;
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
font-weight: 400;
|
"Helvetica Neue", sans-serif;
|
||||||
/* Responsive base: 16px at 320px, scales up to 18px by 768px */
|
line-height: 1.5;
|
||||||
font-size: clamp(1rem, 0.9rem + 0.25vw, 1.125rem);
|
font-weight: 400;
|
||||||
|
/* Fixed 16px – we're always rendering at phone scale */
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
--color-primary: #1be62c;
|
--touch-min: 44px;
|
||||||
--color-primary-hover: #18c925;
|
|
||||||
--color-bg-soft: #f1eee1;
|
|
||||||
--color-surface: #ffffff;
|
|
||||||
--color-accent-light: #cff2dc;
|
|
||||||
--color-accent-bright: #c3fd2f;
|
|
||||||
--color-text: #1a1a1a;
|
|
||||||
--color-text-muted: #6b7280;
|
|
||||||
--color-border: #cff2dc;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
--color-primary: #22c55e;
|
||||||
background-color: var(--color-bg-soft);
|
--color-primary-hover: #16a34a;
|
||||||
font-synthesis: none;
|
--color-bg-soft: #f5f0e8;
|
||||||
text-rendering: optimizeLegibility;
|
--color-surface: #ffffff;
|
||||||
-webkit-font-smoothing: antialiased;
|
--color-accent-light: #dcfce7;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
--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;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--color-primary-hover);
|
color: var(--color-primary-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
overflow-x: hidden;
|
height: 100%;
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 280px;
|
height: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100dvh;
|
||||||
min-height: 100dvh;
|
overflow: hidden;
|
||||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 {
|
h1 {
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
button:focus,
|
button:focus,
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 2px solid var(--color-primary);
|
outline: 2px solid var(--color-primary);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
173
src/lib/api.ts
Normal file
173
src/lib/api.ts
Normal file
@@ -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<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: ApiOptions = {}
|
||||||
|
): Promise<T> {
|
||||||
|
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<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 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<JournalEntry>(`/api/entries/${userId}/${entryId}`, {
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateEntry(
|
||||||
|
userId: string,
|
||||||
|
entryId: string,
|
||||||
|
updates: Partial<JournalEntryCreate>,
|
||||||
|
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<JournalEntry[]>(
|
||||||
|
`/api/entries/${userId}/date-range?startDate=${startDate}&endDate=${endDate}`,
|
||||||
|
{ token }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { initializeApp } from 'firebase/app'
|
import { initializeApp } from 'firebase/app'
|
||||||
import { getAuth, GoogleAuthProvider } from 'firebase/auth'
|
import { getAuth, GoogleAuthProvider } from 'firebase/auth'
|
||||||
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore'
|
|
||||||
|
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
||||||
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
|
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
|
||||||
databaseURL: import.meta.env.VITE_FIREBASE_DATABASE_URL,
|
|
||||||
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
||||||
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
|
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
|
||||||
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
|
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
|
||||||
@@ -14,14 +12,6 @@ const firebaseConfig = {
|
|||||||
|
|
||||||
const app = initializeApp(firebaseConfig)
|
const app = initializeApp(firebaseConfig)
|
||||||
|
|
||||||
// Auth initialization
|
// Google Auth initialization
|
||||||
export const auth = getAuth(app)
|
export const auth = getAuth(app)
|
||||||
export const googleProvider = new GoogleAuthProvider()
|
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)
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
/**
|
|
||||||
* Firestore Collections Configuration
|
|
||||||
* Define all collection names and common data structure interfaces here
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Collection names
|
|
||||||
export const COLLECTIONS = {
|
|
||||||
USERS: 'users',
|
|
||||||
ENTRIES: 'entries',
|
|
||||||
SETTINGS: 'settings',
|
|
||||||
TAGS: 'tags',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
// User document interface
|
|
||||||
export interface FirestoreUser {
|
|
||||||
id: string
|
|
||||||
email: string
|
|
||||||
displayName?: string
|
|
||||||
photoURL?: string
|
|
||||||
createdAt: number
|
|
||||||
updatedAt: number
|
|
||||||
theme?: 'light' | 'dark'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entry (Journal Entry) document interface
|
|
||||||
export interface JournalEntry {
|
|
||||||
id: string
|
|
||||||
userId: string
|
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
mood?: 'happy' | 'sad' | 'neutral' | 'anxious' | 'grateful'
|
|
||||||
tags?: string[]
|
|
||||||
isPublic?: boolean
|
|
||||||
createdAt: number
|
|
||||||
updatedAt: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings document interface
|
|
||||||
export interface UserSettings {
|
|
||||||
userId: string
|
|
||||||
notifications?: boolean
|
|
||||||
emailNotifications?: boolean
|
|
||||||
theme?: 'light' | 'dark' | 'system'
|
|
||||||
language?: string
|
|
||||||
updatedAt: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tags document interface
|
|
||||||
export interface Tag {
|
|
||||||
id: string
|
|
||||||
userId: string
|
|
||||||
name: string
|
|
||||||
color?: string
|
|
||||||
createdAt: number
|
|
||||||
updatedAt: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Firestore emulator configuration
|
|
||||||
export function isEmulatorEnabled(): boolean {
|
|
||||||
return import.meta.env.VITE_FIREBASE_EMULATOR_ENABLED === 'true'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEmulatorHost(): string {
|
|
||||||
return import.meta.env.VITE_FIRESTORE_EMULATOR_HOST || 'localhost:8080'
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import {
|
|
||||||
collection,
|
|
||||||
doc,
|
|
||||||
getDoc,
|
|
||||||
getDocs,
|
|
||||||
setDoc,
|
|
||||||
updateDoc,
|
|
||||||
deleteDoc,
|
|
||||||
query,
|
|
||||||
where,
|
|
||||||
orderBy,
|
|
||||||
WriteBatch,
|
|
||||||
writeBatch,
|
|
||||||
} from 'firebase/firestore'
|
|
||||||
import { db } from './firebase'
|
|
||||||
|
|
||||||
type FirestoreData = Record<string, any>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic function to add or update a document
|
|
||||||
*/
|
|
||||||
export async function setDocument<T extends FirestoreData>(
|
|
||||||
collectionName: string,
|
|
||||||
docId: string,
|
|
||||||
data: T,
|
|
||||||
merge = true
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
await setDoc(doc(db, collectionName, docId), data, { merge })
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error setting document in ${collectionName}:`, error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic function to get a single document
|
|
||||||
*/
|
|
||||||
export async function getDocument<T extends FirestoreData>(
|
|
||||||
collectionName: string,
|
|
||||||
docId: string
|
|
||||||
): Promise<T | null> {
|
|
||||||
try {
|
|
||||||
const docRef = doc(db, collectionName, docId)
|
|
||||||
const docSnapshot = await getDoc(docRef)
|
|
||||||
return (docSnapshot.exists() ? docSnapshot.data() : null) as T | null
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error getting document from ${collectionName}:`, error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic function to get all documents from a collection
|
|
||||||
*/
|
|
||||||
export async function getDocuments<T extends FirestoreData>(
|
|
||||||
collectionName: string
|
|
||||||
): Promise<T[]> {
|
|
||||||
try {
|
|
||||||
const querySnapshot = await getDocs(collection(db, collectionName))
|
|
||||||
return querySnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as T))
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error getting documents from ${collectionName}:`, error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic function to query documents with conditions
|
|
||||||
*/
|
|
||||||
export async function queryDocuments<T extends FirestoreData>(
|
|
||||||
collectionName: string,
|
|
||||||
constraints: any[]
|
|
||||||
): Promise<T[]> {
|
|
||||||
try {
|
|
||||||
const q = query(collection(db, collectionName), ...constraints)
|
|
||||||
const querySnapshot = await getDocs(q)
|
|
||||||
return querySnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as T))
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error querying ${collectionName}:`, error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic function to update a document
|
|
||||||
*/
|
|
||||||
export async function updateDocument<T extends Partial<FirestoreData>>(
|
|
||||||
collectionName: string,
|
|
||||||
docId: string,
|
|
||||||
data: T
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
await updateDoc(doc(db, collectionName, docId), data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error updating document in ${collectionName}:`, error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic function to delete a document
|
|
||||||
*/
|
|
||||||
export async function deleteDocument(
|
|
||||||
collectionName: string,
|
|
||||||
docId: string
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
await deleteDoc(doc(db, collectionName, docId))
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error deleting document from ${collectionName}:`, error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Batch write operations
|
|
||||||
*/
|
|
||||||
export function createWriteBatch(): WriteBatch {
|
|
||||||
return writeBatch(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function commitBatch(batch: WriteBatch): Promise<void> {
|
|
||||||
try {
|
|
||||||
await batch.commit()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error committing batch write:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +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'
|
import BottomNav from '../components/BottomNav'
|
||||||
|
|
||||||
interface JournalEntry {
|
|
||||||
id: string
|
|
||||||
date: Date
|
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HistoryPage() {
|
export default function HistoryPage() {
|
||||||
|
const { user, userId, loading } = useAuth()
|
||||||
const [currentMonth, setCurrentMonth] = useState(new Date())
|
const [currentMonth, setCurrentMonth] = useState(new Date())
|
||||||
|
const [entries, setEntries] = useState<JournalEntry[]>([])
|
||||||
|
const [loadingEntries, setLoadingEntries] = useState(false)
|
||||||
|
|
||||||
// Mock data - replace with actual Firebase data later
|
// Fetch entries on mount and when userId changes
|
||||||
const mockEntries: JournalEntry[] = [
|
useEffect(() => {
|
||||||
{
|
if (!user || !userId) return
|
||||||
id: '1',
|
|
||||||
date: new Date(2026, 1, 12),
|
const fetchEntries = async () => {
|
||||||
title: 'Feeling much lighter today',
|
setLoadingEntries(true)
|
||||||
content: 'After the long conversation yesterday, I woke up with a sense of clarity I haven\'t felt in weeks...'
|
try {
|
||||||
},
|
const token = await user.getIdToken()
|
||||||
{
|
const response = await getUserEntries(userId, token, 100, 0)
|
||||||
id: '2',
|
setEntries(response.entries)
|
||||||
date: new Date(2026, 1, 5),
|
} catch (error) {
|
||||||
title: 'Morning thoughts',
|
console.error('Error fetching entries:', error)
|
||||||
content: 'The coffee smells amazing this morning. Simple pleasures like this remind me...'
|
} finally {
|
||||||
|
setLoadingEntries(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
|
||||||
|
fetchEntries()
|
||||||
|
}, [user, userId])
|
||||||
|
|
||||||
const getDaysInMonth = (date: Date) => {
|
const getDaysInMonth = (date: Date) => {
|
||||||
const year = date.getFullYear()
|
const year = date.getFullYear()
|
||||||
@@ -39,38 +41,45 @@ export default function HistoryPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasEntryOnDate = (day: number) => {
|
const hasEntryOnDate = (day: number) => {
|
||||||
return mockEntries.some(entry => {
|
return entries.some((entry) => {
|
||||||
const entryDate = new Date(entry.date)
|
const entryDate = new Date(entry.createdAt)
|
||||||
return entryDate.getDate() === day &&
|
return (
|
||||||
|
entryDate.getDate() === day &&
|
||||||
entryDate.getMonth() === currentMonth.getMonth() &&
|
entryDate.getMonth() === currentMonth.getMonth() &&
|
||||||
entryDate.getFullYear() === currentMonth.getFullYear()
|
entryDate.getFullYear() === currentMonth.getFullYear()
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const isToday = (day: number) => {
|
const isToday = (day: number) => {
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
return day === today.getDate() &&
|
return (
|
||||||
|
day === today.getDate() &&
|
||||||
currentMonth.getMonth() === today.getMonth() &&
|
currentMonth.getMonth() === today.getMonth() &&
|
||||||
currentMonth.getFullYear() === today.getFullYear()
|
currentMonth.getFullYear() === today.getFullYear()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (date: Date) => {
|
const formatDate = (date: string) => {
|
||||||
return date.toLocaleDateString('en-US', {
|
return new Date(date).toLocaleDateString('en-US', {
|
||||||
weekday: 'short',
|
weekday: 'short',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: '2-digit'
|
day: '2-digit',
|
||||||
}).toUpperCase()
|
}).toUpperCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTime = (date: Date) => {
|
const formatTime = (date: string) => {
|
||||||
return date.toLocaleTimeString('en-US', {
|
return new Date(date).toLocaleTimeString('en-US', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit',
|
||||||
}).toUpperCase()
|
}).toUpperCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
const { daysInMonth, startingDayOfWeek } = getDaysInMonth(currentMonth)
|
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 = () => {
|
const previousMonth = () => {
|
||||||
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1))
|
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1))
|
||||||
@@ -80,14 +89,26 @@ export default function HistoryPage() {
|
|||||||
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1))
|
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 (
|
||||||
|
<div className="history-page" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<p style={{ color: '#9ca3af' }}>Loading…</p>
|
||||||
|
<BottomNav />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="history-page">
|
<div className="history-page">
|
||||||
<div className="history-bg-decoration">
|
|
||||||
<div className="bg-orb bg-orb-1"></div>
|
|
||||||
<div className="bg-orb bg-orb-2"></div>
|
|
||||||
<div className="bg-pattern"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<header className="history-header">
|
<header className="history-header">
|
||||||
<div className="history-header-text">
|
<div className="history-header-text">
|
||||||
<h1>History</h1>
|
<h1>History</h1>
|
||||||
@@ -154,23 +175,35 @@ export default function HistoryPage() {
|
|||||||
<section className="recent-entries">
|
<section className="recent-entries">
|
||||||
<h3 className="recent-entries-title">RECENT ENTRIES</h3>
|
<h3 className="recent-entries-title">RECENT ENTRIES</h3>
|
||||||
|
|
||||||
<div className="entries-list">
|
{loadingEntries ? (
|
||||||
{mockEntries.map(entry => (
|
<p style={{ color: '#9ca3af', fontSize: '0.875rem', textAlign: 'center', padding: '1.5rem 0', fontFamily: 'Inter, sans-serif' }}>
|
||||||
<button
|
Loading entries…
|
||||||
key={entry.id}
|
</p>
|
||||||
type="button"
|
) : (
|
||||||
className="entry-card"
|
<div className="entries-list">
|
||||||
onClick={() => console.log('Open entry', entry.id)}
|
{currentMonthEntries.length === 0 ? (
|
||||||
>
|
<p style={{ color: '#9ca3af', fontSize: '0.875rem', textAlign: 'center', padding: '1.5rem 0', fontFamily: 'Inter, sans-serif' }}>
|
||||||
<div className="entry-header">
|
No entries for this month yet. Start writing!
|
||||||
<span className="entry-date">{formatDate(entry.date)}</span>
|
</p>
|
||||||
<span className="entry-time">{formatTime(entry.date)}</span>
|
) : (
|
||||||
</div>
|
currentMonthEntries.map((entry) => (
|
||||||
<h4 className="entry-title">{entry.title}</h4>
|
<button
|
||||||
<p className="entry-preview">{entry.content}</p>
|
key={entry.id}
|
||||||
</button>
|
type="button"
|
||||||
))}
|
className="entry-card"
|
||||||
</div>
|
onClick={() => console.log('Open entry', entry.id)}
|
||||||
|
>
|
||||||
|
<div className="entry-header">
|
||||||
|
<span className="entry-date">{formatDate(entry.createdAt)}</span>
|
||||||
|
<span className="entry-time">{formatTime(entry.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
<h4 className="entry-title">{entry.title}</h4>
|
||||||
|
<p className="entry-preview">{entry.content}</p>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +1,77 @@
|
|||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { createEntry } from '../lib/api'
|
||||||
import BottomNav from '../components/BottomNav'
|
import BottomNav from '../components/BottomNav'
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { user, loading, signOut } = useAuth()
|
const { user, userId, loading, signOut } = useAuth()
|
||||||
const [entry, setEntry] = useState('')
|
const [entry, setEntry] = useState('')
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="home-page">
|
<div className="home-page" style={{ alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<p>Loading…</p>
|
<p style={{ color: '#9ca3af' }}>Loading…</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<div className="home-page">
|
<div className="home-page" style={{ alignItems: 'center', justifyContent: 'center', gap: '1rem' }}>
|
||||||
<h1>Grateful Journal</h1>
|
<h1 style={{ fontFamily: 'Playfair Display, Georgia, serif', color: '#1a1a1a' }}>Grateful Journal</h1>
|
||||||
<p>Sign in to start your journal.</p>
|
<p style={{ color: '#6b7280' }}>Sign in to start your journal.</p>
|
||||||
<Link to="/login" className="home-login-link">
|
<Link to="/login" className="home-login-link">Go to login</Link>
|
||||||
Go to login
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = user.displayName ?? user.email ?? 'there'
|
// Format date: "THURSDAY, OCT 24"
|
||||||
|
|
||||||
// Get current date formatted like "THURSDAY, OCT 24"
|
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const dateString = today.toLocaleDateString('en-US', {
|
const dateString = today
|
||||||
weekday: 'long',
|
.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })
|
||||||
month: 'short',
|
.toUpperCase()
|
||||||
day: 'numeric'
|
|
||||||
}).toUpperCase()
|
|
||||||
|
|
||||||
const handleWrite = () => {
|
const handleWrite = async () => {
|
||||||
console.log('Saving entry:', { title, entry })
|
if (!userId || !title.trim() || !entry.trim()) {
|
||||||
// TODO: Save to Firebase
|
setMessage({ type: 'error', text: 'Please add a title and entry content' })
|
||||||
setTitle('')
|
return
|
||||||
setEntry('')
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="home-page">
|
<div className="home-page">
|
||||||
<div className="home-bg-decoration">
|
|
||||||
<div className="bg-orb bg-orb-1"></div>
|
|
||||||
<div className="bg-orb bg-orb-2"></div>
|
|
||||||
<div className="bg-orb bg-orb-3"></div>
|
|
||||||
<div className="bg-pattern"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<header className="home-header">
|
|
||||||
<h1>Grateful Journal</h1>
|
|
||||||
<div className="home-user">
|
|
||||||
<span className="home-username">{displayName}</span>
|
|
||||||
<button type="button" className="home-sign-out" onClick={() => signOut()}>
|
|
||||||
Sign out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="journal-container">
|
<main className="journal-container">
|
||||||
<div className="journal-card">
|
<div className="journal-card">
|
||||||
<div className="journal-date">{dateString}</div>
|
<div className="journal-date">{dateString}</div>
|
||||||
@@ -77,14 +85,40 @@ export default function HomePage() {
|
|||||||
placeholder="Title your thoughts..."
|
placeholder="Title your thoughts..."
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
className="journal-entry-textarea"
|
className="journal-entry-textarea"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
value={entry}
|
value={entry}
|
||||||
onChange={(e) => setEntry(e.target.value)}
|
onChange={(e) => setEntry(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div style={{
|
||||||
|
padding: '0.75rem',
|
||||||
|
marginTop: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
backgroundColor: message.type === 'success' ? '#f0fdf4' : '#fef2f2',
|
||||||
|
color: message.type === 'success' ? '#15803d' : '#b91c1c',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
{message.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '0.75rem' }}>
|
||||||
|
<button
|
||||||
|
className="journal-write-btn"
|
||||||
|
onClick={handleWrite}
|
||||||
|
disabled={saving || !title.trim() || !entry.trim()}
|
||||||
|
>
|
||||||
|
{saving ? 'Saving...' : 'Save Entry'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -92,3 +126,4 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,39 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
import { updateUserProfile } from '../lib/api'
|
||||||
import BottomNav from '../components/BottomNav'
|
import BottomNav from '../components/BottomNav'
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { user, signOut } = useAuth()
|
const { user, userId, signOut, loading } = useAuth()
|
||||||
const [passcodeEnabled, setPasscodeEnabled] = useState(false)
|
const [passcodeEnabled, setPasscodeEnabled] = useState(false)
|
||||||
const [faceIdEnabled, setFaceIdEnabled] = useState(false)
|
const [faceIdEnabled, setFaceIdEnabled] = useState(false)
|
||||||
|
const [theme, setTheme] = useState<'light' | 'dark'>('light')
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||||
|
|
||||||
const displayName = user?.displayName || 'User'
|
const displayName = user?.displayName || 'User'
|
||||||
const photoURL = user?.photoURL || ''
|
const photoURL = user?.photoURL || ''
|
||||||
|
|
||||||
|
const handleThemeChange = async (newTheme: 'light' | 'dark') => {
|
||||||
|
if (!userId || !user) return
|
||||||
|
|
||||||
|
setSaving(true)
|
||||||
|
setMessage(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await user.getIdToken()
|
||||||
|
await updateUserProfile(userId, { theme: newTheme }, token)
|
||||||
|
setTheme(newTheme)
|
||||||
|
setMessage({ type: 'success', text: 'Theme updated successfully!' })
|
||||||
|
setTimeout(() => setMessage(null), 2000)
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Failed to update theme'
|
||||||
|
setMessage({ type: 'error', text: errorMessage })
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleClearData = () => {
|
const handleClearData = () => {
|
||||||
if (window.confirm('Are you sure you want to clear all local data? This action cannot be undone.')) {
|
if (window.confirm('Are you sure you want to clear all local data? This action cannot be undone.')) {
|
||||||
// TODO: Implement clear local data
|
// TODO: Implement clear local data
|
||||||
@@ -17,14 +41,25 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
try {
|
||||||
|
await signOut()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error signing out:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="settings-page" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<p style={{ color: '#9ca3af' }}>Loading…</p>
|
||||||
|
<BottomNav />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-page">
|
<div className="settings-page">
|
||||||
<div className="settings-bg-decoration">
|
|
||||||
<div className="bg-orb bg-orb-1"></div>
|
|
||||||
<div className="bg-orb bg-orb-2"></div>
|
|
||||||
<div className="bg-pattern"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<header className="settings-header">
|
<header className="settings-header">
|
||||||
<div className="settings-header-text">
|
<div className="settings-header-text">
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
@@ -80,8 +115,12 @@ export default function SettingsPage() {
|
|||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<div className="settings-item-icon settings-item-icon-gray">
|
<div className="settings-item-icon settings-item-icon-gray">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2z"></path>
|
<path d="M2 12C2 6.5 6.5 2 12 2a10 10 0 0 1 8 4"></path>
|
||||||
<path d="M12 6v6l4 2"></path>
|
<path d="M5 19.5C5.5 18 6 15 6 12c0-1.73.37-3.36 1.03-4.83"></path>
|
||||||
|
<path d="M10 21c.2-2 .4-5 .4-6 0-1 .2-1.93.56-2.78"></path>
|
||||||
|
<path d="M14 21c.2-2 .4-5 .4-6 0-3-1-5-3.4-6.5"></path>
|
||||||
|
<path d="M18.5 21C18 18.5 18 17 18 12c0-1-.07-2-.2-3"></path>
|
||||||
|
<path d="M22 12a10 10 0 0 1-1.53 5.35"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-item-content">
|
<div className="settings-item-content">
|
||||||
@@ -123,7 +162,7 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
<div className="settings-divider"></div>
|
<div className="settings-divider"></div>
|
||||||
|
|
||||||
<button type="button" className="settings-item settings-item-button">
|
<div className="settings-item">
|
||||||
<div className="settings-item-icon settings-item-icon-blue">
|
<div className="settings-item-icon settings-item-icon-blue">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<circle cx="13.5" cy="6.5" r=".5"></circle>
|
<circle cx="13.5" cy="6.5" r=".5"></circle>
|
||||||
@@ -135,19 +174,44 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="settings-item-content">
|
<div className="settings-item-content">
|
||||||
<h4 className="settings-item-title">Theme</h4>
|
<h4 className="settings-item-title">Theme</h4>
|
||||||
<p className="settings-item-subtitle">Currently: Warm Beige</p>
|
<p className="settings-item-subtitle">Currently: {theme === 'light' ? 'Warm Beige' : 'Dark'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-theme-colors">
|
<div className="settings-theme-colors">
|
||||||
<span className="settings-theme-dot settings-theme-dot-beige"></span>
|
<button
|
||||||
<span className="settings-theme-dot settings-theme-dot-dark"></span>
|
type="button"
|
||||||
|
onClick={() => handleThemeChange('light')}
|
||||||
|
className="settings-theme-dot settings-theme-dot-beige"
|
||||||
|
style={{ opacity: theme === 'light' ? 1 : 0.5 }}
|
||||||
|
title="Light theme"
|
||||||
|
disabled={saving}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleThemeChange('dark')}
|
||||||
|
className="settings-theme-dot settings-theme-dot-dark"
|
||||||
|
style={{ opacity: theme === 'dark' ? 1 : 0.5 }}
|
||||||
|
title="Dark theme"
|
||||||
|
disabled={saving}
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<svg className="settings-item-arrow" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
</div>
|
||||||
<polyline points="9 18 15 12 9 6"></polyline>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div style={{
|
||||||
|
padding: '0.75rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
backgroundColor: message.type === 'success' ? '#f0fdf4' : '#fef2f2',
|
||||||
|
color: message.type === 'success' ? '#15803d' : '#b91c1c',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
{message.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Clear Data */}
|
{/* Clear Data */}
|
||||||
<button type="button" className="settings-clear-btn" onClick={handleClearData}>
|
<button type="button" className="settings-clear-btn" onClick={handleClearData}>
|
||||||
<span>Clear Local Data</span>
|
<span>Clear Local Data</span>
|
||||||
@@ -158,7 +222,7 @@ export default function SettingsPage() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Sign Out */}
|
{/* Sign Out */}
|
||||||
<button type="button" className="settings-signout-btn" onClick={() => signOut()}>
|
<button type="button" className="settings-signout-btn" onClick={handleSignOut}>
|
||||||
Sign Out
|
Sign Out
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
52
start-all.sh
Executable file
52
start-all.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Grateful Journal - Start All Services
|
||||||
|
# Runs MongoDB, FastAPI backend, and Vite frontend in one command
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 Starting Grateful Journal..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if MongoDB is running
|
||||||
|
echo "📦 Checking MongoDB..."
|
||||||
|
if lsof -Pi :27017 -sTCP:LISTEN -t >/dev/null 2>&1 ; then
|
||||||
|
echo "✓ MongoDB already running on port 27017"
|
||||||
|
else
|
||||||
|
echo "Starting MongoDB..."
|
||||||
|
brew services start mongodb-community
|
||||||
|
sleep 2
|
||||||
|
echo "✓ MongoDB started on port 27017"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Start Backend (FastAPI with conda environment)
|
||||||
|
echo "🔄 Starting FastAPI backend..."
|
||||||
|
# Activate conda and start backend
|
||||||
|
conda run -n yoyo python backend/main.py &
|
||||||
|
BACKEND_PID=$!
|
||||||
|
echo "✓ Backend running on http://localhost:8001 (PID: $BACKEND_PID)"
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Start Frontend (Vite)
|
||||||
|
echo "🔄 Starting Vite frontend..."
|
||||||
|
npm run dev -- --port 8000 &
|
||||||
|
FRONTEND_PID=$!
|
||||||
|
echo "✓ Frontend running on http://localhost:8000 (PID: $FRONTEND_PID)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ All services started!"
|
||||||
|
echo ""
|
||||||
|
echo "📱 Frontend: http://localhost:8000"
|
||||||
|
echo "🔌 Backend: http://localhost:8001"
|
||||||
|
echo "📄 API Docs: http://localhost:8001/docs"
|
||||||
|
echo ""
|
||||||
|
echo "To stop all services, press Ctrl+C"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Wait for both processes
|
||||||
|
wait $BACKEND_PID $FRONTEND_PID
|
||||||
@@ -4,4 +4,9 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 8000,
|
||||||
|
strictPort: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user