first working version
This commit is contained in:
parent
821cd9282a
commit
ff612d2b2a
@ -29,7 +29,7 @@ steps:
|
|||||||
path:
|
path:
|
||||||
include: [ backend/** ]
|
include: [ backend/** ]
|
||||||
settings:
|
settings:
|
||||||
registry: harbor.dvirlabs.com
|
registry: harbor-core.dev-tools.svc.cluster.local
|
||||||
repo: my-apps/${CI_REPO_NAME}-backend
|
repo: my-apps/${CI_REPO_NAME}-backend
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
context: backend
|
context: backend
|
||||||
@ -63,7 +63,7 @@ steps:
|
|||||||
- |
|
- |
|
||||||
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
||||||
echo "💡 Setting frontend tag to: $TAG"
|
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 add manifests/${CI_REPO_NAME}/values.yaml
|
||||||
git commit -m "frontend: update tag to $TAG" || echo "No changes"
|
git commit -m "frontend: update tag to $TAG" || echo "No changes"
|
||||||
git push origin HEAD
|
git push origin HEAD
|
||||||
@ -85,16 +85,24 @@ steps:
|
|||||||
- apk add --no-cache git yq
|
- apk add --no-cache git yq
|
||||||
- git config --global user.name "woodpecker-bot"
|
- git config --global user.name "woodpecker-bot"
|
||||||
- git config --global user.email "ci@dvirlabs.com"
|
- 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
|
- cd my-apps
|
||||||
- |
|
- |
|
||||||
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
||||||
echo "💡 Setting backend tag to: $TAG"
|
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 add manifests/${CI_REPO_NAME}/values.yaml
|
||||||
git commit -m "backend: update tag to $TAG" || echo "No changes"
|
git commit -m "backend: update tag to $TAG" || echo "No changes"
|
||||||
git push origin HEAD
|
git push origin HEAD
|
||||||
|
|
||||||
|
|
||||||
trigger-gitops-via-push:
|
trigger-gitops-via-push:
|
||||||
|
when:
|
||||||
|
branch: [ master, develop ]
|
||||||
|
event: [ push ]
|
||||||
name: Trigger apps-gitops via Git push
|
name: Trigger apps-gitops via Git push
|
||||||
image: alpine/git
|
image: alpine/git
|
||||||
environment:
|
environment:
|
||||||
@ -110,4 +118,4 @@ steps:
|
|||||||
echo "# trigger at $(date) by $${CI_REPO_NAME}" >> .trigger
|
echo "# trigger at $(date) by $${CI_REPO_NAME}" >> .trigger
|
||||||
git add .trigger
|
git add .trigger
|
||||||
git commit -m "ci: trigger apps-gitops build" || echo "no changes"
|
git commit -m "ci: trigger apps-gitops build" || echo "no changes"
|
||||||
git push origin HEAD
|
git push origin HEAD
|
||||||
262
DEPLOYMENT.md
Normal file
262
DEPLOYMENT.md
Normal file
@ -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 <app-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create `<app-name>/values.yaml`**:
|
||||||
|
```yaml
|
||||||
|
global:
|
||||||
|
namespace: my-apps
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image:
|
||||||
|
repository: harbor.dvirlabs.com/my-apps/<app-name>-backend
|
||||||
|
tag: "latest"
|
||||||
|
service:
|
||||||
|
port: 8000
|
||||||
|
targetPort: 8000
|
||||||
|
ingress:
|
||||||
|
hosts:
|
||||||
|
- host: api-<app-name>.dvirlabs.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: api-<app-name>-tls
|
||||||
|
hosts:
|
||||||
|
- api-<app-name>.dvirlabs.com
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image:
|
||||||
|
repository: harbor.dvirlabs.com/my-apps/<app-name>-frontend
|
||||||
|
tag: "latest"
|
||||||
|
env:
|
||||||
|
VITE_API_URL: "https://api-<app-name>.dvirlabs.com"
|
||||||
|
ingress:
|
||||||
|
hosts:
|
||||||
|
- host: <app-name>.dvirlabs.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: <app-name>-tls
|
||||||
|
hosts:
|
||||||
|
- <app-name>.dvirlabs.com
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
user: <app-name>_user
|
||||||
|
password: <secure-password>
|
||||||
|
database: <app-name>_db
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create `<app-name>/cname.yaml`**:
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: <app-name>-cname
|
||||||
|
namespace: my-apps
|
||||||
|
data:
|
||||||
|
frontend: <app-name>.dvirlabs.com
|
||||||
|
backend: api-<app-name>.dvirlabs.com
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Deploy**:
|
||||||
|
```bash
|
||||||
|
helm install <app-name> ./app-chart -f <app-name>/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:
|
||||||
|
|
||||||
|
```
|
||||||
|
<app-name>.dvirlabs.com CNAME/A <ingress-loadbalancer-ip>
|
||||||
|
api-<app-name>.dvirlabs.com CNAME/A <ingress-loadbalancer-ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 <pod-name>
|
||||||
|
kubectl logs -n my-apps <pod-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 <backend-pod> -- env | grep DB_
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ingress not working
|
||||||
|
```bash
|
||||||
|
# Check ingress configuration
|
||||||
|
kubectl describe ingress -n my-apps <app-name>-backend
|
||||||
|
kubectl describe ingress -n my-apps <app-name>-frontend
|
||||||
|
|
||||||
|
# Check cert-manager certificates
|
||||||
|
kubectl get certificate -n my-apps
|
||||||
|
kubectl describe certificate -n my-apps <app-name>-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.
|
||||||
24
backend/Dockerfile
Normal file
24
backend/Dockerfile
Normal file
@ -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"]
|
||||||
BIN
backend/__pycache__/database.cpython-313.pyc
Normal file
BIN
backend/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
37
backend/fix_tables.sql
Normal file
37
backend/fix_tables.sql
Normal file
@ -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
|
||||||
@ -417,4 +417,4 @@ def delete_task(task_id: str, authorization: Optional[str] = Header(None), db: S
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|||||||
1
frontend/.env.example
Normal file
1
frontend/.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=http://localhost:8000
|
||||||
30
frontend/Dockerfile
Normal file
30
frontend/Dockerfile
Normal file
@ -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;"]
|
||||||
22
frontend/nginx.conf
Normal file
22
frontend/nginx.conf
Normal file
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,11 +21,38 @@
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-title {
|
.sidebar-title {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #667eea;
|
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 {
|
.user-info {
|
||||||
@ -44,24 +71,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn {
|
.logout-btn {
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
background: #ff4757;
|
background: #ff4757;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
font-size: 1.2rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 0.35rem;
|
||||||
padding: 0;
|
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 {
|
.logout-btn:hover {
|
||||||
background: #ff3838;
|
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 {
|
.lists-container {
|
||||||
@ -631,6 +668,201 @@
|
|||||||
background: #e0e0e0;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.app {
|
.app {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
|
|||||||
import Auth from './Auth'
|
import Auth from './Auth'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
const API_URL = 'http://localhost:8001'
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
||||||
|
|
||||||
const AVAILABLE_ICONS = [
|
const AVAILABLE_ICONS = [
|
||||||
'📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️',
|
'📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️',
|
||||||
@ -26,6 +26,7 @@ function App() {
|
|||||||
const [editingTaskId, setEditingTaskId] = useState(null)
|
const [editingTaskId, setEditingTaskId] = useState(null)
|
||||||
const [editingTaskTitle, setEditingTaskTitle] = useState('')
|
const [editingTaskTitle, setEditingTaskTitle] = useState('')
|
||||||
const [confirmModal, setConfirmModal] = useState({ show: false, message: '', onConfirm: null })
|
const [confirmModal, setConfirmModal] = useState({ show: false, message: '', onConfirm: null })
|
||||||
|
const [darkMode, setDarkMode] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check for stored token on mount
|
// Check for stored token on mount
|
||||||
@ -34,7 +35,11 @@ function App() {
|
|||||||
|
|
||||||
if (storedToken && storedUser) {
|
if (storedToken && storedUser) {
|
||||||
setToken(storedToken)
|
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])
|
}, [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 = () => ({
|
const getAuthHeaders = () => ({
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
@ -58,25 +78,35 @@ function App() {
|
|||||||
const handleLogin = (userData, userToken) => {
|
const handleLogin = (userData, userToken) => {
|
||||||
setUser(userData)
|
setUser(userData)
|
||||||
setToken(userToken)
|
setToken(userToken)
|
||||||
|
// Load user's dark mode preference
|
||||||
|
const userDarkMode = localStorage.getItem(`darkMode_${userData.id}`)
|
||||||
|
setDarkMode(userDarkMode ? JSON.parse(userDarkMode) : false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = () => {
|
||||||
try {
|
setConfirmModal({
|
||||||
await fetch(`${API_URL}/logout`, {
|
show: true,
|
||||||
method: 'POST',
|
message: 'Are you sure you want to sign out?',
|
||||||
headers: getAuthHeaders()
|
onConfirm: async () => {
|
||||||
})
|
try {
|
||||||
} catch (error) {
|
await fetch(`${API_URL}/logout`, {
|
||||||
console.error('Logout error:', error)
|
method: 'POST',
|
||||||
}
|
headers: getAuthHeaders()
|
||||||
|
})
|
||||||
localStorage.removeItem('token')
|
} catch (error) {
|
||||||
localStorage.removeItem('user')
|
console.error('Logout error:', error)
|
||||||
setUser(null)
|
}
|
||||||
setToken(null)
|
|
||||||
setLists([])
|
localStorage.removeItem('token')
|
||||||
setTasks([])
|
localStorage.removeItem('user')
|
||||||
setSelectedList(null)
|
setUser(null)
|
||||||
|
setToken(null)
|
||||||
|
setLists([])
|
||||||
|
setTasks([])
|
||||||
|
setSelectedList(null)
|
||||||
|
setConfirmModal({ show: false, message: '', onConfirm: null })
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchLists = async () => {
|
const fetchLists = async () => {
|
||||||
@ -245,14 +275,20 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className={`app ${darkMode ? 'dark-mode' : ''}`}>
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
<div className="sidebar-header">
|
<div className="sidebar-header">
|
||||||
<h2 className="sidebar-title">✓ Tasko</h2>
|
<div className="header-top">
|
||||||
|
<h2 className="sidebar-title">✓ Tasko</h2>
|
||||||
|
<button onClick={toggleDarkMode} className="theme-toggle" title={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}>
|
||||||
|
{darkMode ? '☀️' : '🌙'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="user-info">
|
<div className="user-info">
|
||||||
<span className="username">Hello, {user.username}!</span>
|
<span className="username">Hello, {user.username}!</span>
|
||||||
<button onClick={handleLogout} className="logout-btn" title="Logout">
|
<button onClick={handleLogout} className="logout-btn" title="Sign out">
|
||||||
🚪
|
<span className="logout-icon">→</span>
|
||||||
|
<span className="logout-text">Sign Out</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import './Auth.css'
|
import './Auth.css'
|
||||||
|
|
||||||
const API_URL = 'http://localhost:8001'
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
||||||
|
|
||||||
function Auth({ onLogin }) {
|
function Auth({ onLogin }) {
|
||||||
const [isLogin, setIsLogin] = useState(true)
|
const [isLogin, setIsLogin] = useState(true)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user