first working version

This commit is contained in:
dvirlabs 2025-12-10 19:45:05 +02:00
parent 821cd9282a
commit ff612d2b2a
12 changed files with 689 additions and 37 deletions

View File

@ -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
View 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
View 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"]

Binary file not shown.

37
backend/fix_tables.sql Normal file
View 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

View File

@ -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
View File

@ -0,0 +1 @@
VITE_API_URL=http://localhost:8000

30
frontend/Dockerfile Normal file
View 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
View 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";
}
}

View File

@ -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;

View File

@ -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>

View File

@ -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)