first working version
This commit is contained in:
parent
821cd9282a
commit
ff612d2b2a
@ -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
|
||||
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__":
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
@ -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 (
|
||||
<div className="app">
|
||||
<div className={`app ${darkMode ? 'dark-mode' : ''}`}>
|
||||
<div className="sidebar">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user