From ff612d2b2ac348380551361724e6374aa999aa4b Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Wed, 10 Dec 2025 19:45:05 +0200 Subject: [PATCH] first working version --- .woodpecker.yaml | 16 +- DEPLOYMENT.md | 262 +++++++++++++++++++ backend/Dockerfile | 24 ++ backend/__pycache__/database.cpython-313.pyc | Bin 0 -> 4019 bytes backend/fix_tables.sql | 37 +++ backend/main.py | 2 +- frontend/.env.example | 1 + frontend/Dockerfile | 30 +++ frontend/nginx.conf | 22 ++ frontend/src/App.css | 248 +++++++++++++++++- frontend/src/App.jsx | 82 ++++-- frontend/src/Auth.jsx | 2 +- 12 files changed, 689 insertions(+), 37 deletions(-) create mode 100644 DEPLOYMENT.md create mode 100644 backend/Dockerfile create mode 100644 backend/__pycache__/database.cpython-313.pyc create mode 100644 backend/fix_tables.sql create mode 100644 frontend/.env.example create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 2a80bcb..3d68271 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -29,7 +29,7 @@ steps: path: include: [ backend/** ] settings: - registry: harbor.dvirlabs.com + registry: harbor-core.dev-tools.svc.cluster.local repo: my-apps/${CI_REPO_NAME}-backend dockerfile: backend/Dockerfile context: backend @@ -63,7 +63,7 @@ steps: - | TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}" echo "💡 Setting frontend tag to: $TAG" - yq -i ".frontend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml + yq -i ".frontend.image.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml git add manifests/${CI_REPO_NAME}/values.yaml git commit -m "frontend: update tag to $TAG" || echo "No changes" git push origin HEAD @@ -85,16 +85,24 @@ steps: - apk add --no-cache git yq - git config --global user.name "woodpecker-bot" - git config --global user.email "ci@dvirlabs.com" + - | + if [ ! -d "my-apps" ]; then + git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git" + fi - cd my-apps - | TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}" echo "💡 Setting backend tag to: $TAG" - yq -i ".backend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml + yq -i ".backend.image.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml git add manifests/${CI_REPO_NAME}/values.yaml git commit -m "backend: update tag to $TAG" || echo "No changes" git push origin HEAD + trigger-gitops-via-push: + when: + branch: [ master, develop ] + event: [ push ] name: Trigger apps-gitops via Git push image: alpine/git environment: @@ -110,4 +118,4 @@ steps: echo "# trigger at $(date) by $${CI_REPO_NAME}" >> .trigger git add .trigger git commit -m "ci: trigger apps-gitops build" || echo "no changes" - git push origin HEAD + git push origin HEAD \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..83c2b57 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,262 @@ +# Kubernetes Deployment Guide + +This repository contains a shared Helm chart (`app-chart`) that can be used to deploy multiple applications with the same architecture pattern. + +## Repository Structure + +``` +. +├── app-chart/ # Shared Helm chart template +│ ├── Chart.yaml +│ ├── values.yaml # Default values +│ ├── README.md # Detailed documentation +│ └── templates/ # Kubernetes manifests templates +├── tasko/ # Tasko application overrides +│ ├── values.yaml # Tasko-specific values +│ └── cname.yaml # DNS configuration +├── my-recipes/ # My-Recipes application overrides +│ ├── values.yaml # My-Recipes-specific values +│ └── cname.yaml # DNS configuration +└── my-recipes-chart/ # Old my-recipes chart (deprecated) +``` + +## Quick Start + +### Deploy Tasko Application + +```bash +# Install +helm install tasko ./app-chart -f tasko/values.yaml -n my-apps --create-namespace + +# Upgrade +helm upgrade tasko ./app-chart -f tasko/values.yaml -n my-apps + +# Uninstall +helm uninstall tasko -n my-apps +``` + +### Deploy My-Recipes Application + +```bash +# Install +helm install my-recipes ./app-chart -f my-recipes/values.yaml -n my-apps --create-namespace + +# Upgrade +helm upgrade my-recipes ./app-chart -f my-recipes/values.yaml -n my-apps + +# Uninstall +helm uninstall my-recipes -n my-apps +``` + +## Application URLs + +### Tasko +- Frontend: https://tasko.dvirlabs.com +- Backend API: https://api-tasko.dvirlabs.com + +### My-Recipes +- Frontend: https://my-recipes.dvirlabs.com +- Backend API: https://api-my-recipes.dvirlabs.com + +## Adding a New Application + +1. **Create application directory**: + ```bash + mkdir + ``` + +2. **Create `/values.yaml`**: + ```yaml + global: + namespace: my-apps + + backend: + image: + repository: harbor.dvirlabs.com/my-apps/-backend + tag: "latest" + service: + port: 8000 + targetPort: 8000 + ingress: + hosts: + - host: api-.dvirlabs.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: api--tls + hosts: + - api-.dvirlabs.com + + frontend: + image: + repository: harbor.dvirlabs.com/my-apps/-frontend + tag: "latest" + env: + VITE_API_URL: "https://api-.dvirlabs.com" + ingress: + hosts: + - host: .dvirlabs.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: -tls + hosts: + - .dvirlabs.com + + postgres: + user: _user + password: + database: _db + ``` + +3. **Create `/cname.yaml`**: + ```yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: -cname + namespace: my-apps + data: + frontend: .dvirlabs.com + backend: api-.dvirlabs.com + ``` + +4. **Deploy**: + ```bash + helm install ./app-chart -f /values.yaml -n my-apps + ``` + +## Configuration + +Each application can override the base configuration by providing custom values in their `values.yaml` file. Common overrides include: + +- Image repository and tags +- Resource limits and requests +- Environment variables +- Ingress hostnames +- Database credentials +- Replica counts + +See `app-chart/README.md` for detailed configuration options. + +## Prerequisites + +- Kubernetes cluster (1.19+) +- Helm 3.0+ +- Traefik Ingress Controller installed +- cert-manager installed (for automatic TLS certificates) +- kubectl configured to access your cluster + +## DNS Configuration + +Configure DNS records to point to your ingress controller: + +``` +.dvirlabs.com CNAME/A +api-.dvirlabs.com CNAME/A +``` + +## Monitoring Deployments + +```bash +# Check all deployments +kubectl get all -n my-apps + +# Check specific application +kubectl get pods -n my-apps -l app.kubernetes.io/instance=tasko + +# View logs +kubectl logs -n my-apps -l component=backend --tail=100 -f +kubectl logs -n my-apps -l component=frontend --tail=100 -f + +# Check ingress +kubectl get ingress -n my-apps +``` + +## Database Management + +### Access PostgreSQL + +```bash +# Port forward to access database locally +kubectl port-forward -n my-apps svc/tasko-db 5432:5432 + +# Connect using psql +psql postgresql://tasko_user:tasko_password@localhost:5432/tasko_db +``` + +### Backup Database + +```bash +# Backup +kubectl exec -n my-apps tasko-db-0 -- pg_dump -U tasko_user tasko_db > backup.sql + +# Restore +kubectl exec -i -n my-apps tasko-db-0 -- psql -U tasko_user tasko_db < backup.sql +``` + +## CI/CD Integration + +The chart works seamlessly with CI/CD pipelines. Example with Woodpecker CI: + +```yaml +steps: + deploy: + image: alpine/helm:3.12.0 + commands: + - helm upgrade --install tasko ./app-chart + -f tasko/values.yaml + --set backend.image.tag=${CI_COMMIT_SHA} + --set frontend.image.tag=${CI_COMMIT_SHA} + -n my-apps +``` + +## Troubleshooting + +### Pod not starting +```bash +kubectl describe pod -n my-apps +kubectl logs -n my-apps +``` + +### Database connection issues +```bash +# Check if database is running +kubectl get pods -n my-apps -l component=database + +# Test connection from backend pod +kubectl exec -n my-apps -- env | grep DB_ +``` + +### Ingress not working +```bash +# Check ingress configuration +kubectl describe ingress -n my-apps -backend +kubectl describe ingress -n my-apps -frontend + +# Check cert-manager certificates +kubectl get certificate -n my-apps +kubectl describe certificate -n my-apps -tls +``` + +## Migration from Old Chart + +If migrating from the old `my-recipes-chart`: + +1. **Backup your data** +2. **Update DNS** if hostnames changed +3. **Deploy with new chart**: + ```bash + helm uninstall my-recipes # Old release + helm install my-recipes ./app-chart -f my-recipes/values.yaml -n my-apps + ``` + +## Support + +For detailed documentation, see: +- `app-chart/README.md` - Complete chart documentation +- `helm/README.md` - Docker image building and deployment + +For issues and questions, please open an issue in the repository. diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..05a42c7 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements file +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["python", "main.py"] diff --git a/backend/__pycache__/database.cpython-313.pyc b/backend/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d438edaa0cb865620dd379f2b62ba8873ed5bbe7 GIT binary patch literal 4019 zcmb_fO>7j&74Dwtp6Ti7`SlosG3+c5*dYNYfWfvl8}N>u#pXwH+Zz@}X|-p{cFR58 zLv;^eD~Cyt60%W(l?aKfv9s|f$eF#>CG}}%5rMewvG8K-e%T*2aP*IMWCWC(1swZ^~zLDj8YJ<;lKE1)` z^$hKp$(C|VngpY)cEDUrM*DaZ_aiLXpjO?a^Tr&RXEJP;+(3iGnvQj~NoW^pD^An4 z%_*DQ7wInOxG%DJjZB+O+h@uI*!!I2`Aot&KhK-C{k39^*u*DCT-um59r#Lm%B;>A z4Y$z*mlBV~tEN{qYlKNSoXHdJ9C7Xo7fJ=D7=~lkiD59+FzRltX=7V6jH^x4Zc8$T zF>O)Lw*h*N3*wYv__)AOP9<0*m2v42tHu;u7)qE->ZWD0)H)=LS<{=P=u-+QK!9EQ zRmZ)~)DYY?!}Mtq1+oo0^q7o@@>tBr<$A2sFgzb1S_M6T{ZtdSW*GDkQr&cv!dk}Pu@GQRC_QPXm7WM zR|?&CPc97yg=4MrE4i+_%F-Ku=)KB8c>zr4JpKHhWu0SVIO*0;Cu9XKntbR$Ui z-zFKY2@geu`LVW+p zlcxwV!}$3I$mk%F{Xke^V;=e%b{3I@DCm3z6wt4tK4O5r34}>Jvq)Ceb(marZI^xt zB#z^##dNtGvS72g5c8)Igve0XV%J_Z&Od zgXAd~#e+WiX!yNAyL@vvgc2@jdnl!OUG!?F-xoxAjLVcQ86Z3Z*I~8}tSCPp+rR<{ zBIwdDfG)#10Mge7alt5U0hl@-Ob%@R=fH1NEJqY|nCmsH9D!TlzKMVM&Yv&->0*$Z;D_JlAjtOIz5E~^WKZ8b8%6+b;e`mmJQIcg z8m8?#!WE$5Gv2P-*Li%8%bU5deFaUsY>J+SURo`m?0CUILUv zng!hpC5cHxP!ceuPJ9!k>m`Z-HT9p`|*ht*t5_=lwY==}H3 ze6xU7m&98qo(cGVwpS4cZk3-2@OrjG6R(K3e3V=PNk%-w_3-`A0}=7?ni>;3 HI6D6Wl+db% literal 0 HcmV?d00001 diff --git a/backend/fix_tables.sql b/backend/fix_tables.sql new file mode 100644 index 0000000..9380886 --- /dev/null +++ b/backend/fix_tables.sql @@ -0,0 +1,37 @@ +-- Create missing tables +CREATE TABLE task_lists ( + id VARCHAR(36) PRIMARY KEY, + user_id VARCHAR(36) NOT NULL, + name VARCHAR(200) NOT NULL, + icon VARCHAR(10) DEFAULT 'list', + color VARCHAR(7) DEFAULT '#667eea', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE TABLE tasks ( + id VARCHAR(36) PRIMARY KEY, + list_id VARCHAR(36) NOT NULL, + user_id VARCHAR(36) NOT NULL, + title VARCHAR(500) NOT NULL, + description TEXT, + completed BOOLEAN DEFAULT FALSE NOT NULL, + priority VARCHAR(20) DEFAULT 'medium', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (list_id) REFERENCES task_lists(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- Create indexes +CREATE INDEX idx_task_lists_user_id ON task_lists(user_id); +CREATE INDEX idx_tasks_list_id ON tasks(list_id); +CREATE INDEX idx_tasks_user_id ON tasks(user_id); +CREATE INDEX idx_tasks_completed ON tasks(completed); + +-- Grant permissions to tasko_user +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO tasko_user; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO tasko_user; + +-- Show tables +\dt diff --git a/backend/main.py b/backend/main.py index 6769662..84f9ab4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -417,4 +417,4 @@ def delete_task(task_id: str, authorization: Optional[str] = Header(None), db: S if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8001) + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..5934e2e --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:8000 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..83ee2ab --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,30 @@ +# Build stage +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built assets from builder +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port 80 +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..e7a8c64 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; + + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/frontend/src/App.css b/frontend/src/App.css index 3726a5e..749f99c 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -21,11 +21,38 @@ margin-bottom: 2rem; } +.header-top { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + .sidebar-title { font-size: 1.8rem; font-weight: 700; color: #667eea; - margin: 0 0 0.75rem 0; + margin: 0; +} + +.theme-toggle { + width: 36px; + height: 36px; + border: none; + border-radius: 8px; + background: #f5f5f5; + font-size: 1.2rem; + cursor: pointer; + transition: all 0.3s; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); +} + +.theme-toggle:hover { + background: #e5e5e5; + transform: rotate(20deg) scale(1.1); } .user-info { @@ -44,24 +71,34 @@ } .logout-btn { - width: 32px; - height: 32px; background: #ff4757; color: white; border: none; - border-radius: 8px; - font-size: 1.2rem; + border-radius: 6px; cursor: pointer; transition: all 0.3s; display: flex; align-items: center; - justify-content: center; - padding: 0; + gap: 0.35rem; + padding: 0.4rem 0.7rem; + font-size: 0.75rem; + font-weight: 600; + box-shadow: 0 2px 6px rgba(255, 71, 87, 0.25); } .logout-btn:hover { background: #ff3838; - transform: scale(1.1); + transform: translateY(-1px); + box-shadow: 0 3px 10px rgba(255, 71, 87, 0.35); +} + +.logout-icon { + font-size: 1rem; + font-weight: bold; +} + +.logout-text { + letter-spacing: 0.2px; } .lists-container { @@ -631,6 +668,201 @@ background: #e0e0e0; } +/* Dark Mode Styles */ +.app.dark-mode { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); +} + +.dark-mode .sidebar { + background: rgba(30, 30, 46, 0.98); + box-shadow: 2px 0 20px rgba(0, 0, 0, 0.5); +} + +.dark-mode .sidebar-title { + color: #8b9bea; +} + +.dark-mode .theme-toggle { + background: #2a2a3e; + color: #ffd700; +} + +.dark-mode .theme-toggle:hover { + background: #3a3a4e; +} + +.dark-mode .user-info { + background: #2a2a3e; +} + +.dark-mode .username { + color: #b0b0c0; +} + +.dark-mode .list-item { + background: #2a2a3e; + border-color: #3a3a4e; + color: #e0e0e0; +} + +.dark-mode .list-item:hover { + border-color: #8b9bea; +} + +.dark-mode .list-item.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.dark-mode .list-delete-btn { + color: #888; +} + +.dark-mode .list-delete-btn:hover { + background: rgba(255, 71, 87, 0.2); + color: #ff6b7a; +} + +.dark-mode .new-list-input { + background: #2a2a3e; + border-color: #8b9bea; + color: #e0e0e0; +} + +.dark-mode .picker-label { + color: #b0b0c0; +} + +.dark-mode .icon-option { + background: #2a2a3e; + border-color: #3a3a4e; +} + +.dark-mode .icon-option:hover { + border-color: #8b9bea; +} + +.dark-mode .icon-option.selected { + background: #667eea; + border-color: #667eea; +} + +.dark-mode .new-list-cancel { + background: #2a2a3e; + color: #b0b0c0; +} + +.dark-mode .new-list-cancel:hover { + background: #3a3a4e; +} + +.dark-mode .main-content { + background: rgba(30, 30, 46, 0.95); + box-shadow: 0 0 30px rgba(0, 0, 0, 0.5); +} + +.dark-mode .content-title { + color: #e0e0e0; +} + +.dark-mode .content-subtitle { + color: #888; +} + +.dark-mode .task-input { + background: #2a2a3e; + border-color: #3a3a4e; + color: #e0e0e0; +} + +.dark-mode .task-input:focus { + border-color: #8b9bea; +} + +.dark-mode .task-input::placeholder { + color: #666; +} + +.dark-mode .add-button { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.dark-mode .add-button:hover:not(:disabled) { + background: linear-gradient(135deg, #7688f0 0%, #8659b0 100%); +} + +.dark-mode .filter-tabs button { + color: #888; +} + +.dark-mode .filter-tabs button.active { + color: #8b9bea; + border-bottom-color: #8b9bea; +} + +.dark-mode .filter-tabs button:hover { + color: #b0b0c0; +} + +.dark-mode .task-item { + background: #2a2a3e; + border-color: #3a3a4e; +} + +.dark-mode .task-item:hover { + border-color: #667eea; + background: #323249; +} + +.dark-mode .task-title { + color: #e0e0e0; +} + +.dark-mode .task-title:hover { + color: #8b9bea; +} + +.dark-mode .task-item.completed .task-title { + color: #666; +} + +.dark-mode .task-edit-input { + background: #1a1a2e; + color: #e0e0e0; + border-color: #8b9bea; +} + +.dark-mode .empty-state, +.dark-mode .empty-state-main { + color: #888; +} + +.dark-mode .modal-overlay { + background: rgba(0, 0, 0, 0.7); +} + +.dark-mode .modal-content { + background: #2a2a3e; +} + +.dark-mode .modal-title { + color: #e0e0e0; +} + +.dark-mode .modal-message { + color: #b0b0c0; +} + +.dark-mode .modal-cancel-btn { + background: #1a1a2e; + color: #b0b0c0; +} + +.dark-mode .modal-cancel-btn:hover { + background: #252538; +} + @media (max-width: 768px) { .app { flex-direction: column; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 274c158..e562e19 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import Auth from './Auth' import './App.css' -const API_URL = 'http://localhost:8001' +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' const AVAILABLE_ICONS = [ '📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️', @@ -26,6 +26,7 @@ function App() { const [editingTaskId, setEditingTaskId] = useState(null) const [editingTaskTitle, setEditingTaskTitle] = useState('') const [confirmModal, setConfirmModal] = useState({ show: false, message: '', onConfirm: null }) + const [darkMode, setDarkMode] = useState(false) useEffect(() => { // Check for stored token on mount @@ -34,7 +35,11 @@ function App() { if (storedToken && storedUser) { setToken(storedToken) - setUser(JSON.parse(storedUser)) + const userData = JSON.parse(storedUser) + setUser(userData) + // Load user's dark mode preference + const userDarkMode = localStorage.getItem(`darkMode_${userData.id}`) + setDarkMode(userDarkMode ? JSON.parse(userDarkMode) : false) } }, []) @@ -50,6 +55,21 @@ function App() { } }, [selectedList, token]) + useEffect(() => { + if (user) { + localStorage.setItem(`darkMode_${user.id}`, JSON.stringify(darkMode)) + } + if (darkMode) { + document.body.classList.add('dark-mode') + } else { + document.body.classList.remove('dark-mode') + } + }, [darkMode, user]) + + const toggleDarkMode = () => { + setDarkMode(!darkMode) + } + const getAuthHeaders = () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` @@ -58,25 +78,35 @@ function App() { const handleLogin = (userData, userToken) => { setUser(userData) setToken(userToken) + // Load user's dark mode preference + const userDarkMode = localStorage.getItem(`darkMode_${userData.id}`) + setDarkMode(userDarkMode ? JSON.parse(userDarkMode) : false) } - const handleLogout = async () => { - try { - await fetch(`${API_URL}/logout`, { - method: 'POST', - headers: getAuthHeaders() - }) - } catch (error) { - console.error('Logout error:', error) - } - - localStorage.removeItem('token') - localStorage.removeItem('user') - setUser(null) - setToken(null) - setLists([]) - setTasks([]) - setSelectedList(null) + const handleLogout = () => { + setConfirmModal({ + show: true, + message: 'Are you sure you want to sign out?', + onConfirm: async () => { + try { + await fetch(`${API_URL}/logout`, { + method: 'POST', + headers: getAuthHeaders() + }) + } catch (error) { + console.error('Logout error:', error) + } + + localStorage.removeItem('token') + localStorage.removeItem('user') + setUser(null) + setToken(null) + setLists([]) + setTasks([]) + setSelectedList(null) + setConfirmModal({ show: false, message: '', onConfirm: null }) + } + }) } const fetchLists = async () => { @@ -245,14 +275,20 @@ function App() { } return ( -
+
-

✓ Tasko

+
+

✓ Tasko

+ +
Hello, {user.username}! -
diff --git a/frontend/src/Auth.jsx b/frontend/src/Auth.jsx index 84d2ac4..971fec2 100644 --- a/frontend/src/Auth.jsx +++ b/frontend/src/Auth.jsx @@ -1,7 +1,7 @@ import { useState } from 'react' import './Auth.css' -const API_URL = 'http://localhost:8001' +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' function Auth({ onLogin }) { const [isLogin, setIsLogin] = useState(true)