Compare commits

...

3 Commits

Author SHA1 Message Date
07df39184e responsive ui for all screens 2026-03-16 11:26:32 +05:30
3a096bbc37 initial docker setup 2026-03-16 11:05:44 +05:30
8bea06be5e small changes 2026-03-16 10:56:52 +05:30
17 changed files with 520 additions and 47 deletions

12
.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
node_modules
dist
dist-ssr
.git
.gitignore
Dockerfile
docker-compose.yml
backend
*.log
.env
.env.*
coverage

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ dist
dist-ssr
*.local
.env
.env.*
.env.local
# Editor directories and files

34
Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
FROM node:20-alpine AS build
WORKDIR /app
ARG VITE_FIREBASE_API_KEY
ARG VITE_FIREBASE_AUTH_DOMAIN
ARG VITE_FIREBASE_PROJECT_ID
ARG VITE_FIREBASE_STORAGE_BUCKET
ARG VITE_FIREBASE_MESSAGING_SENDER_ID
ARG VITE_FIREBASE_APP_ID
ARG VITE_API_URL=/api
ENV VITE_FIREBASE_API_KEY=${VITE_FIREBASE_API_KEY}
ENV VITE_FIREBASE_AUTH_DOMAIN=${VITE_FIREBASE_AUTH_DOMAIN}
ENV VITE_FIREBASE_PROJECT_ID=${VITE_FIREBASE_PROJECT_ID}
ENV VITE_FIREBASE_STORAGE_BUCKET=${VITE_FIREBASE_STORAGE_BUCKET}
ENV VITE_FIREBASE_MESSAGING_SENDER_ID=${VITE_FIREBASE_MESSAGING_SENDER_ID}
ENV VITE_FIREBASE_APP_ID=${VITE_FIREBASE_APP_ID}
ENV VITE_API_URL=${VITE_API_URL}
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:1.27-alpine AS runtime
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

12
backend/.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.pytest_cache
.mypy_cache
.ruff_cache
.venv
venv
.env
*.log

View File

@@ -4,3 +4,7 @@ API_PORT=8001
ENVIRONMENT=development
FRONTEND_URL=http://localhost:8000
# Docker Compose values:
# MONGODB_URI=mongodb://mongo:27017
# ENVIRONMENT=production

15
backend/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8001
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]

View File

@@ -24,10 +24,17 @@ app = FastAPI(
)
# CORS middleware (MUST be before routes)
cors_origins = [settings.frontend_url]
if settings.environment == "development":
cors_origins.extend([
"http://localhost:8000",
"http://127.0.0.1:8000",
"http://localhost:5173",
])
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:8000",
"http://127.0.0.1:8000", "http://localhost:5173"],
allow_origins=cors_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],

View File

@@ -5,4 +5,3 @@ pydantic==2.5.0
python-dotenv==1.0.0
pydantic-settings==2.1.0
python-multipart==0.0.6
cors==1.0.1

58
docker-compose.yml Normal file
View File

@@ -0,0 +1,58 @@
services:
frontend:
build:
context: .
dockerfile: Dockerfile
args:
VITE_FIREBASE_API_KEY: ${VITE_FIREBASE_API_KEY}
VITE_FIREBASE_AUTH_DOMAIN: ${VITE_FIREBASE_AUTH_DOMAIN}
VITE_FIREBASE_PROJECT_ID: ${VITE_FIREBASE_PROJECT_ID}
VITE_FIREBASE_STORAGE_BUCKET: ${VITE_FIREBASE_STORAGE_BUCKET}
VITE_FIREBASE_MESSAGING_SENDER_ID: ${VITE_FIREBASE_MESSAGING_SENDER_ID}
VITE_FIREBASE_APP_ID: ${VITE_FIREBASE_APP_ID}
VITE_API_URL: ${VITE_API_URL:-/api}
depends_on:
backend:
condition: service_started
ports:
- "127.0.0.1:8000:80"
restart: unless-stopped
networks:
- app_net
backend:
build:
context: ./backend
dockerfile: Dockerfile
env_file:
- ./backend/.env
expose:
- "8001"
depends_on:
mongo:
condition: service_healthy
restart: unless-stopped
networks:
- app_net
mongo:
image: mongo:6
command: ["mongod", "--bind_ip", "0.0.0.0"]
volumes:
- mongo_data:/data/db
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
networks:
- app_net
volumes:
mongo_data:
networks:
app_net:
driver: bridge

191
docs/DOCKER_SETUP.md Normal file
View File

@@ -0,0 +1,191 @@
# Docker Setup Guide for Grateful Journal
## Goal
This Docker setup runs the full app locally with three containers:
- Frontend (React app served by nginx)
- Backend (FastAPI)
- MongoDB
The setup is intentionally private to the local machine:
- Frontend is available only at `http://127.0.0.1:8000`
- Backend is not published to the host
- MongoDB is not published to the host
- Backend and MongoDB are reachable only from other containers in the same Docker Compose network
This means other devices on the same network cannot access the UI, backend, or database.
## Files Added for Docker
- Root `Dockerfile` for the frontend build and nginx runtime
- `backend/Dockerfile` for FastAPI
- `docker-compose.yml` for orchestration
- `nginx/default.conf` for SPA serving and API proxying
- Root `.env` for frontend build variables
- `backend/.env` for backend runtime variables
## Prerequisites
- Docker Desktop installed and running
- Docker Compose available via `docker compose`
## Environment Files
### Frontend
The root `.env` file is used during the frontend image build.
Current values:
```env
VITE_FIREBASE_API_KEY=...
VITE_FIREBASE_AUTH_DOMAIN=react-test-8cb04.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=react-test-8cb04
VITE_FIREBASE_STORAGE_BUCKET=react-test-8cb04.firebasestorage.app
VITE_FIREBASE_MESSAGING_SENDER_ID=1036594341832
VITE_FIREBASE_APP_ID=1:1036594341832:web:9db6fa337e9cd2e953c2fd
VITE_API_URL=/api
```
`VITE_API_URL=/api` is important because nginx proxies `/api` requests to the backend container internally.
### Backend
The `backend/.env` file is loaded by the backend container at runtime.
Current values:
```env
MONGODB_URI=mongodb://mongo:27017
MONGODB_DB_NAME=grateful_journal
API_PORT=8001
ENVIRONMENT=production
FRONTEND_URL=http://localhost:8000
```
`MONGODB_URI=mongodb://mongo:27017` works because Docker Compose gives the MongoDB service the hostname `mongo` on the internal network.
## Network Model
### Frontend
The frontend service is published with:
```yaml
ports:
- "127.0.0.1:8000:80"
```
This binds the container to localhost only. The app is reachable from your machine, but not from another device on your LAN.
### Backend
The backend uses:
```yaml
expose:
- "8001"
```
`expose` makes port 8001 available to other containers, but not to your host machine or network.
### MongoDB
MongoDB has no `ports` section, so it is not reachable from outside Docker. Only the backend can talk to it over the Compose network.
## Start the Stack
From the project root:
```bash
docker compose up --build
```
Then open:
- Frontend: `http://127.0.0.1:8000`
The backend API and MongoDB stay internal.
## Stop the Stack
```bash
docker compose down
```
To also remove the database volume:
```bash
docker compose down -v
```
## Rebuild After Changes
If you change frontend code, backend code, or environment variables:
```bash
docker compose up --build
```
If you want a full rebuild without cache:
```bash
docker compose build --no-cache
docker compose up
```
## Data Persistence
MongoDB data is stored in the named Docker volume `mongo_data`.
That means:
- Restarting containers keeps the data
- Removing the containers keeps the data
- Running `docker compose down -v` removes the data
## API Flow
Browser requests follow this path:
1. Browser loads the frontend from nginx on `127.0.0.1:8000`
2. Frontend sends API requests to `/api`
3. nginx forwards `/api` to `http://backend:8001/api/`
4. Backend connects to MongoDB at `mongodb://mongo:27017`
This avoids exposing the backend directly to the host.
## Firebase Note
The frontend still requires the Firebase JavaScript SDK because login happens in the browser.
The backend does not currently verify Firebase ID tokens, so `firebase-admin` is not part of this Docker setup.
If backend token verification is added later, that would be a separate change.
## Troubleshooting
### Docker command not found
Install Docker Desktop and confirm this works:
```bash
docker --version
docker compose version
```
### Frontend loads but API calls fail
Check that:
- `backend/.env` contains `MONGODB_URI=mongodb://mongo:27017`
- Root `.env` contains `VITE_API_URL=/api`
- All containers are healthy with `docker compose ps`
### Want to inspect MongoDB from the host
This setup does not expose MongoDB intentionally.
If you want host access temporarily for debugging, add a port mapping to the MongoDB service, but that weakens the local-only isolation model.

29
nginx/default.conf Normal file
View File

@@ -0,0 +1,29 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://backend:8001/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /health {
proxy_pass http://backend:8001/health;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -395,6 +395,11 @@
color: #fff;
}
/* Brand element hidden on mobile, shown in sidebar on desktop */
.bottom-nav-brand {
display: none;
}
/* ============================
HISTORY PAGE
============================ */
@@ -1176,8 +1181,8 @@
}
}
/* ---- Responsive: desktop (≥ 1024px) ---- */
@media (min-width: 1024px) {
/* ---- Responsive: desktop (≥ 768px) ---- */
@media (min-width: 768px) {
.entry-modal {
max-width: 620px;
padding: 2rem;
@@ -1305,6 +1310,147 @@
cursor: not-allowed;
}
/* ============================
RESPONSIVE DESKTOP (≥ 860px)
Sidebar navigation + single-column content
============================ */
@media (min-width: 860px) {
/* ---- Sidebar navigation ---- */
.bottom-nav {
position: fixed;
left: 0;
top: 0;
height: 100dvh;
width: 232px;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
padding: 0 0.875rem 1.5rem;
gap: 4px;
border-top: none;
border-right: 1px solid var(--color-border);
background: var(--color-surface);
z-index: 100;
}
/* Brand header in sidebar */
.bottom-nav-brand {
display: block;
font-family: "Sniglet", system-ui;
font-size: 1.0625rem;
font-weight: 700;
color: var(--color-primary);
padding: 1.75rem calc(0.875rem + 10px) 1.25rem;
margin: 0 -0.875rem 0.5rem;
border-bottom: 1px solid var(--color-border);
letter-spacing: -0.01em;
}
/* Nav buttons become full-width rows */
.bottom-nav-btn {
flex-direction: row;
justify-content: flex-start;
width: 100%;
padding: 10px 14px;
border-radius: 12px;
min-height: 42px;
}
/* Always show labels in sidebar */
.bottom-nav-btn span {
display: inline;
}
/* Active pill keeps green but full-width */
.bottom-nav-btn-active {
padding: 10px 14px;
}
/* Offset all pages for the sidebar width */
.home-page,
.history-page,
.settings-page {
margin-left: 232px;
}
/* ---- Write / Home page ---- */
.journal-container {
padding: 2.5rem 2rem;
align-items: center;
}
.journal-card {
max-width: 760px;
width: 100%;
align-self: center;
}
/* ---- History page ---- */
.history-header {
padding: 2rem 2.5rem 0;
}
.history-container {
padding: 1.25rem 2.5rem 1.5rem;
}
/* ---- Settings page ---- */
.settings-header {
padding: 2rem 2.5rem 0;
}
.settings-container {
padding: 1.25rem 2.5rem 1.5rem;
max-width: 720px;
}
}
/* Desktop dark theme adjustments */
@media (min-width: 860px) {
[data-theme="dark"] .bottom-nav {
background: #141414;
border-right-color: rgba(74, 222, 128, 0.1);
}
[data-theme="dark"] .bottom-nav-brand {
border-bottom-color: rgba(74, 222, 128, 0.1);
}
}
/* ============================
RESPONSIVE WIDE DESKTOP (≥ 1100px)
Two-column history: calendar left, entries right
============================ */
@media (min-width: 1100px) {
/* Two-column layout: calendar left, entries right */
.history-container {
display: flex;
flex-direction: row;
gap: 1.5rem;
overflow: hidden;
}
.calendar-card {
flex-shrink: 0;
width: 320px;
align-self: flex-start;
margin-bottom: 0;
}
.recent-entries {
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
margin-bottom: 0;
}
.recent-entries::-webkit-scrollbar {
display: none;
}
}
/* ============================
DARK THEME
============================ */

View File

@@ -8,6 +8,9 @@ export default function BottomNav() {
return (
<nav className="bottom-nav">
{/* Brand visible only in desktop sidebar */}
<div className="bottom-nav-brand">Grateful Journal</div>
{/* Write */}
<button
type="button"

View File

@@ -53,32 +53,7 @@ body {
height: 100%;
min-height: 100dvh;
overflow: hidden;
/* Desktop: show as phone on a desk surface */
background: #c0ccc0;
}
/* ── Phone shell on desktop ───────────────────────────── */
@media (min-width: 600px) {
body {
display: flex;
align-items: center;
justify-content: center;
background: #b0bfb0;
}
#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;
}
background: var(--color-bg-soft);
}
h1 {
@@ -115,16 +90,3 @@ button:focus-visible {
[data-theme="dark"] body {
background: #0a0a0a;
}
@media (min-width: 600px) {
[data-theme="dark"] body {
background: #111;
}
[data-theme="dark"] #root {
box-shadow:
0 24px 80px rgba(0, 0, 0, 0.6),
0 4px 16px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(74, 222, 128, 0.08);
}
}

View File

@@ -3,7 +3,7 @@
* Handles all communication with the backend API
*/
const API_BASE_URL = 'http://localhost:8001'
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8001'
type ApiOptions = {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'

View File

@@ -180,12 +180,12 @@ export default function HistoryPage() {
<h1>History</h1>
<p className="history-subtitle">Your past reflections</p>
</div>
<button type="button" className="history-search-btn" title="Search entries">
{/* <button type="button" className="history-search-btn" title="Search entries">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</button>
</button> */}
</header>
<main className="history-container">