Spaces:
Paused
Paused
Upload 93 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env +0 -0
- .env.example +21 -0
- .gitignore +8 -0
- Dockerfile +61 -0
- README.md +1 -11
- data/projects.db +0 -0
- deploy.sh +38 -0
- docker-compose.yml +47 -0
- ecosystem.config.js +29 -0
- fly-deploy.sh +57 -0
- fly.toml +69 -0
- node.Dockerfile +39 -0
- nodemon.json +7 -0
- package-lock.json +0 -0
- package.json +49 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/index.html +15 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/package.json +34 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/postcss.config.js +6 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/App.tsx +7 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/components/TodoInput.tsx +44 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/components/TodoItem.tsx +49 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/components/TodoList.tsx +50 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/index.css +29 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/main.tsx +10 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/pages/TodoPage.tsx +62 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/routes.tsx +15 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/types.ts +5 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/utils/animations.ts +16 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/utils/storage.ts +16 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/tailwind.config.js +23 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/tsconfig.json +22 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/tsconfig.node.json +9 -0
- projects/060c1fce-28f4-472a-b360-97eb9dd5065d/vite.config.ts +11 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/index.html +15 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/package.json +34 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/postcss.config.js +6 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/App.tsx +7 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/components/TodoInput.tsx +44 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/components/TodoItem.tsx +49 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/components/TodoList.tsx +50 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/index.css +29 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/main.tsx +10 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/pages/TodoPage.tsx +62 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/routes.tsx +15 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/types.ts +5 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/utils/animations.ts +16 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/utils/storage.ts +16 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/tailwind.config.js +23 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/tsconfig.json +22 -0
- projects/706236f9-12ab-40ac-989e-75c18042cdb2/tsconfig.node.json +9 -0
.env
ADDED
|
File without changes
|
.env.example
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ── Server ────────────────────────────────────────────────────────────────────
|
| 2 |
+
PORT=4000
|
| 3 |
+
NODE_ENV=production
|
| 4 |
+
|
| 5 |
+
# ── Dynamic Container Port Range ──────────────────────────────────────────────
|
| 6 |
+
# Each user project gets a unique host port in this range.
|
| 7 |
+
# Make sure the same range is exposed in docker-compose.yml ports section.
|
| 8 |
+
BASE_HOST_PORT=3100
|
| 9 |
+
MAX_HOST_PORT=3300
|
| 10 |
+
|
| 11 |
+
# ── Docker ────────────────────────────────────────────────────────────────────
|
| 12 |
+
# Path to Docker socket (default for Linux; use 'npipe:////./pipe/docker_engine' on Windows)
|
| 13 |
+
DOCKER_SOCKET=/var/run/docker.sock
|
| 14 |
+
|
| 15 |
+
# ── Resource Limits per Container ─────────────────────────────────────────────
|
| 16 |
+
CONTAINER_MEMORY_MB=512
|
| 17 |
+
CONTAINER_CPU=0.5
|
| 18 |
+
|
| 19 |
+
# ── Cleanup ───────────────────────────────────────────────────────────────────
|
| 20 |
+
# How long (ms) a project can be idle before its container is stopped
|
| 21 |
+
CONTAINER_IDLE_TIMEOUT_MS=3600000
|
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
dist/
|
| 3 |
+
.env
|
| 4 |
+
data/*.db
|
| 5 |
+
data/*.db-wal
|
| 6 |
+
data/*.db-shm
|
| 7 |
+
logs/
|
| 8 |
+
projects/
|
Dockerfile
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ─── Stage 1: Build TypeScript ───────────────────────────────────────────────
|
| 2 |
+
FROM node:20-slim AS builder
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
COPY package*.json ./
|
| 6 |
+
RUN npm ci
|
| 7 |
+
COPY tsconfig.json ./
|
| 8 |
+
COPY src/ ./src/
|
| 9 |
+
RUN npm run build
|
| 10 |
+
|
| 11 |
+
# ─── Stage 2: Production Image ───────────────────────────────────────────────
|
| 12 |
+
# We use a full Debian image (not slim) so we can install Docker CLI.
|
| 13 |
+
# The Docker daemon itself runs on the HOST (Fly machine in privileged mode).
|
| 14 |
+
# This container only needs the Docker CLI + socket to talk to it.
|
| 15 |
+
FROM node:20-bookworm AS runner
|
| 16 |
+
|
| 17 |
+
WORKDIR /app
|
| 18 |
+
|
| 19 |
+
# ── Install Docker CLI (client only, not the daemon) ──────────────────────────
|
| 20 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 21 |
+
ca-certificates \
|
| 22 |
+
curl \
|
| 23 |
+
gnupg \
|
| 24 |
+
lsb-release \
|
| 25 |
+
&& install -m 0755 -d /etc/apt/keyrings \
|
| 26 |
+
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
|
| 27 |
+
&& chmod a+r /etc/apt/keyrings/docker.gpg \
|
| 28 |
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" \
|
| 29 |
+
> /etc/apt/sources.list.d/docker.list \
|
| 30 |
+
&& apt-get update \
|
| 31 |
+
&& apt-get install -y --no-install-recommends docker-ce-cli \
|
| 32 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 33 |
+
|
| 34 |
+
# ── Install native Node deps (better-sqlite3 needs build tools) ───────────────
|
| 35 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 36 |
+
python3 make g++ \
|
| 37 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 38 |
+
|
| 39 |
+
COPY package*.json ./
|
| 40 |
+
RUN npm ci --omit=dev
|
| 41 |
+
|
| 42 |
+
# Copy compiled output from builder
|
| 43 |
+
COPY --from=builder /app/dist ./dist
|
| 44 |
+
|
| 45 |
+
# Worker Dockerfiles needed at runtime for building project images
|
| 46 |
+
COPY node.Dockerfile ./node.Dockerfile
|
| 47 |
+
COPY python.Dockerfile ./python.Dockerfile
|
| 48 |
+
|
| 49 |
+
# Create directories for persistent data
|
| 50 |
+
RUN mkdir -p /app/projects /app/data /app/logs
|
| 51 |
+
|
| 52 |
+
EXPOSE 4000
|
| 53 |
+
|
| 54 |
+
ENV NODE_ENV=production
|
| 55 |
+
ENV PORT=4000
|
| 56 |
+
|
| 57 |
+
# Healthcheck
|
| 58 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
| 59 |
+
CMD curl -f http://localhost:4000/health || exit 1
|
| 60 |
+
|
| 61 |
+
CMD ["node", "dist/index.js"]
|
README.md
CHANGED
|
@@ -1,11 +1 @@
|
|
| 1 |
-
--
|
| 2 |
-
title: Docker Backend
|
| 3 |
-
emoji: 🐠
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: green
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
license: mit
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
+
# docker-container-manager
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data/projects.db
ADDED
|
Binary file (28.7 kB). View file
|
|
|
deploy.sh
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 5 |
+
echo " Coder AI — Deploy Script"
|
| 6 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 7 |
+
|
| 8 |
+
# ── 1. Pre-build worker images (if not already built) ──
|
| 9 |
+
echo ""
|
| 10 |
+
echo "▶ Building worker images..."
|
| 11 |
+
|
| 12 |
+
if ! docker image inspect coder-node &>/dev/null; then
|
| 13 |
+
echo " Building coder-node..."
|
| 14 |
+
docker build -f node.Dockerfile -t coder-node .
|
| 15 |
+
else
|
| 16 |
+
echo " coder-node already exists — skipping"
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
if ! docker image inspect coder-python &>/dev/null; then
|
| 20 |
+
echo " Building coder-python..."
|
| 21 |
+
docker build -f python.Dockerfile -t coder-python .
|
| 22 |
+
else
|
| 23 |
+
echo " coder-python already exists — skipping"
|
| 24 |
+
fi
|
| 25 |
+
|
| 26 |
+
# ── 2. Pull/build orchestrator image ──
|
| 27 |
+
echo ""
|
| 28 |
+
echo "▶ Building orchestrator image..."
|
| 29 |
+
docker-compose build --no-cache orchestrator
|
| 30 |
+
|
| 31 |
+
# ── 3. Start (or restart) via docker-compose ──
|
| 32 |
+
echo ""
|
| 33 |
+
echo "▶ Starting orchestrator..."
|
| 34 |
+
docker-compose up -d --remove-orphans
|
| 35 |
+
|
| 36 |
+
echo ""
|
| 37 |
+
echo "✅ Deployed! Orchestrator running on port ${PORT:-4000}"
|
| 38 |
+
echo " Logs: docker-compose logs -f orchestrator"
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.9'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
# ── Coder AI Orchestrator ────────────────────────────────────────────────────
|
| 5 |
+
orchestrator:
|
| 6 |
+
build:
|
| 7 |
+
context: .
|
| 8 |
+
dockerfile: Dockerfile
|
| 9 |
+
container_name: coderai-orchestrator
|
| 10 |
+
restart: unless-stopped
|
| 11 |
+
ports:
|
| 12 |
+
# Main API + proxy port
|
| 13 |
+
- "${PORT:-4000}:4000"
|
| 14 |
+
# Dynamic project preview ports range (3100–3300)
|
| 15 |
+
# Extend this range as you onboard more concurrent users
|
| 16 |
+
- "3100-3300:3100-3300"
|
| 17 |
+
volumes:
|
| 18 |
+
# Allow orchestrator to spawn sibling Docker containers
|
| 19 |
+
- /var/run/docker.sock:/var/run/docker.sock
|
| 20 |
+
# Persist SQLite DB across restarts
|
| 21 |
+
- orchestrator_data:/app/data
|
| 22 |
+
# Persist project source files across restarts
|
| 23 |
+
- orchestrator_projects:/app/projects
|
| 24 |
+
environment:
|
| 25 |
+
- NODE_ENV=production
|
| 26 |
+
- PORT=4000
|
| 27 |
+
- BASE_HOST_PORT=${BASE_HOST_PORT:-3100}
|
| 28 |
+
- MAX_HOST_PORT=${MAX_HOST_PORT:-3300}
|
| 29 |
+
env_file:
|
| 30 |
+
- .env
|
| 31 |
+
logging:
|
| 32 |
+
driver: "json-file"
|
| 33 |
+
options:
|
| 34 |
+
max-size: "10m"
|
| 35 |
+
max-file: "3"
|
| 36 |
+
healthcheck:
|
| 37 |
+
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
|
| 38 |
+
interval: 30s
|
| 39 |
+
timeout: 10s
|
| 40 |
+
retries: 3
|
| 41 |
+
start_period: 20s
|
| 42 |
+
|
| 43 |
+
volumes:
|
| 44 |
+
orchestrator_data:
|
| 45 |
+
driver: local
|
| 46 |
+
orchestrator_projects:
|
| 47 |
+
driver: local
|
ecosystem.config.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
apps: [
|
| 3 |
+
{
|
| 4 |
+
name: 'coderai-server',
|
| 5 |
+
script: 'dist/index.js',
|
| 6 |
+
instances: 1, // Must be 1 — Docker socket is not safe to share across workers
|
| 7 |
+
exec_mode: 'fork',
|
| 8 |
+
watch: false,
|
| 9 |
+
max_memory_restart: '512M',
|
| 10 |
+
env: {
|
| 11 |
+
NODE_ENV: 'development',
|
| 12 |
+
PORT: 4000,
|
| 13 |
+
},
|
| 14 |
+
env_production: {
|
| 15 |
+
NODE_ENV: 'production',
|
| 16 |
+
PORT: 4000,
|
| 17 |
+
},
|
| 18 |
+
// Log settings
|
| 19 |
+
out_file: './logs/out.log',
|
| 20 |
+
error_file: './logs/error.log',
|
| 21 |
+
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
| 22 |
+
merge_logs: true,
|
| 23 |
+
// Auto-restart on crash with exponential backoff
|
| 24 |
+
autorestart: true,
|
| 25 |
+
restart_delay: 2000,
|
| 26 |
+
max_restarts: 10,
|
| 27 |
+
},
|
| 28 |
+
],
|
| 29 |
+
};
|
fly-deploy.sh
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# fly-deploy.sh — Full Fly.io deploy + Docker-in-Docker setup
|
| 3 |
+
# Run this once for initial deploy, then just `fly deploy` for updates.
|
| 4 |
+
set -euo pipefail
|
| 5 |
+
|
| 6 |
+
APP=${FLY_APP_NAME:-"coderai-orchestrator"}
|
| 7 |
+
|
| 8 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 9 |
+
echo " Coder AI → Fly.io Deploy"
|
| 10 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 11 |
+
|
| 12 |
+
# ── Check flyctl is installed ──────────────────────────────────────────────
|
| 13 |
+
if ! command -v flyctl &>/dev/null; then
|
| 14 |
+
echo "Installing flyctl..."
|
| 15 |
+
curl -L https://fly.io/install.sh | sh
|
| 16 |
+
export PATH="$HOME/.fly/bin:$PATH"
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
# ── Create app if it doesn't exist ────────────────────────────────────────
|
| 20 |
+
flyctl apps list | grep -q "$APP" || {
|
| 21 |
+
echo "▶ Creating Fly app: $APP"
|
| 22 |
+
flyctl apps create "$APP" --machines
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
# ── Create persistent volume (only first time) ────────────────────────────
|
| 26 |
+
flyctl volumes list -a "$APP" | grep -q "coderai_data" || {
|
| 27 |
+
echo "▶ Creating persistent volume (1GB)..."
|
| 28 |
+
flyctl volumes create coderai_data --size 1 --region sin -a "$APP"
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
# ── Set secrets from .env ─────────────────────────────────────────────────
|
| 32 |
+
if [ -f .env ]; then
|
| 33 |
+
echo "▶ Setting secrets from .env..."
|
| 34 |
+
# Filter out comments and empty lines, set as Fly secrets
|
| 35 |
+
grep -v '^\s*#' .env | grep -v '^\s*$' | flyctl secrets import -a "$APP"
|
| 36 |
+
fi
|
| 37 |
+
|
| 38 |
+
# ── Deploy ────────────────────────────────────────────────────────────────
|
| 39 |
+
echo "▶ Deploying..."
|
| 40 |
+
flyctl deploy --remote-only -a "$APP"
|
| 41 |
+
|
| 42 |
+
# ── Enable Docker-in-Docker (privileged mode) ─────────────────────────────
|
| 43 |
+
echo ""
|
| 44 |
+
echo "▶ Enabling Docker socket access (privileged machine)..."
|
| 45 |
+
MACHINE_ID=$(flyctl machines list -a "$APP" --json | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")
|
| 46 |
+
|
| 47 |
+
flyctl machines update "$MACHINE_ID" \
|
| 48 |
+
--privileged \
|
| 49 |
+
-a "$APP" \
|
| 50 |
+
--yes
|
| 51 |
+
|
| 52 |
+
echo ""
|
| 53 |
+
echo "✅ Done! App running at: https://${APP}.fly.dev"
|
| 54 |
+
echo " Health: https://${APP}.fly.dev/health"
|
| 55 |
+
echo ""
|
| 56 |
+
echo " For updates: flyctl deploy -a $APP"
|
| 57 |
+
echo " For logs: flyctl logs -a $APP"
|
fly.toml
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# fly.toml — Fly.io deployment config for Coder AI Orchestrator
|
| 2 |
+
# Deploy: fly deploy
|
| 3 |
+
|
| 4 |
+
app = "coderai-orchestrator" # Change to your unique app name
|
| 5 |
+
primary_region = "sin" # Singapore — closest to India; or try "bom" if available
|
| 6 |
+
|
| 7 |
+
[build]
|
| 8 |
+
dockerfile = "Dockerfile"
|
| 9 |
+
|
| 10 |
+
[env]
|
| 11 |
+
NODE_ENV = "production"
|
| 12 |
+
PORT = "4000"
|
| 13 |
+
# Port range for user project containers (must match [[services]] TCP)
|
| 14 |
+
BASE_HOST_PORT = "3100"
|
| 15 |
+
MAX_HOST_PORT = "3200"
|
| 16 |
+
|
| 17 |
+
# ── Main HTTP service ──────────────────────────────────────────────────────────
|
| 18 |
+
[[services]]
|
| 19 |
+
protocol = "tcp"
|
| 20 |
+
internal_port = 4000
|
| 21 |
+
|
| 22 |
+
[[services.ports]]
|
| 23 |
+
port = 80
|
| 24 |
+
handlers = ["http"]
|
| 25 |
+
|
| 26 |
+
[[services.ports]]
|
| 27 |
+
port = 443
|
| 28 |
+
handlers = ["tls", "http"]
|
| 29 |
+
|
| 30 |
+
[services.concurrency]
|
| 31 |
+
type = "requests"
|
| 32 |
+
soft_limit = 50
|
| 33 |
+
hard_limit = 100
|
| 34 |
+
|
| 35 |
+
[[services.http_checks]]
|
| 36 |
+
interval = "15s"
|
| 37 |
+
timeout = "5s"
|
| 38 |
+
grace_period = "30s"
|
| 39 |
+
method = "GET"
|
| 40 |
+
path = "/health"
|
| 41 |
+
|
| 42 |
+
# ── Project preview port range (TCP passthrough) ──────────────────────────────
|
| 43 |
+
# Each user project gets a unique port in 3100-3200 range
|
| 44 |
+
# Fly.io exposes these as TCP services so users can hit preview URLs
|
| 45 |
+
[[services]]
|
| 46 |
+
protocol = "tcp"
|
| 47 |
+
internal_port = 3100
|
| 48 |
+
|
| 49 |
+
[[services.ports]]
|
| 50 |
+
port = 3100
|
| 51 |
+
handlers = ["tcp"]
|
| 52 |
+
|
| 53 |
+
# ── Machine/VM settings ───────────────────────────────────────────────────────
|
| 54 |
+
[vm]
|
| 55 |
+
cpu_kind = "shared"
|
| 56 |
+
cpus = 2
|
| 57 |
+
memory_mb = 2048 # 2GB — adjust based on concurrent users
|
| 58 |
+
|
| 59 |
+
# ── Persistent volumes (SQLite DB + project files) ────────────────────────────
|
| 60 |
+
[mounts]
|
| 61 |
+
source = "coderai_data"
|
| 62 |
+
destination = "/app/data"
|
| 63 |
+
|
| 64 |
+
# NOTE: Fly.io does NOT support Docker-in-Docker by default.
|
| 65 |
+
# To enable Docker socket access (required to spawn project containers),
|
| 66 |
+
# you must use a Fly Machine with --privileged flag.
|
| 67 |
+
# Run this once after initial deploy:
|
| 68 |
+
# fly machine update <machine-id> --privileged
|
| 69 |
+
# Or use the fly-machines.sh helper script included in this repo.
|
node.Dockerfile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ─── specialized Node.js Worker Image ──────────────────────────────────────────
|
| 2 |
+
FROM node:20-slim
|
| 3 |
+
|
| 4 |
+
# Avoid interactive prompts
|
| 5 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 6 |
+
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
curl \
|
| 9 |
+
git \
|
| 10 |
+
unzip \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
# Install Bun for speed
|
| 14 |
+
RUN curl -fsSL https://bun.sh/install | bash
|
| 15 |
+
ENV PATH="/root/.bun/bin:${PATH}"
|
| 16 |
+
|
| 17 |
+
# ─── Pre-cache common packages ────────────────────────────────────────────────
|
| 18 |
+
WORKDIR /tmp/precache
|
| 19 |
+
RUN echo '{\
|
| 20 |
+
"name":"precache",\
|
| 21 |
+
"version":"1.0.0",\
|
| 22 |
+
"dependencies":{\
|
| 23 |
+
"next":"14.2.0",\
|
| 24 |
+
"react":"^18.3.1",\
|
| 25 |
+
"react-dom":"^18.3.1",\
|
| 26 |
+
"typescript":"^5.5.3",\
|
| 27 |
+
"tailwindcss":"^3.4.1",\
|
| 28 |
+
"postcss":"^8.4.38",\
|
| 29 |
+
"vite":"^5.4.0",\
|
| 30 |
+
"@vitejs/plugin-react":"^4.3.1",\
|
| 31 |
+
"framer-motion":"^11.3.8",\
|
| 32 |
+
"lucide-react":"^0.395.0"\
|
| 33 |
+
}\
|
| 34 |
+
}' > package.json
|
| 35 |
+
RUN bun install
|
| 36 |
+
|
| 37 |
+
WORKDIR /app
|
| 38 |
+
EXPOSE 3000 5173 8080 4173
|
| 39 |
+
CMD ["tail", "-f", "/dev/null"]
|
nodemon.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"watch": ["src"],
|
| 3 |
+
"ignore": ["projects/**", "node_modules/**"],
|
| 4 |
+
"ext": "ts,json",
|
| 5 |
+
"exec": "ts-node src/index.ts",
|
| 6 |
+
"env": {}
|
| 7 |
+
}
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "server-orchestrator",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "",
|
| 5 |
+
"main": "index.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "nodemon",
|
| 8 |
+
"build": "tsc",
|
| 9 |
+
"start": "node dist/index.js",
|
| 10 |
+
"start:dev": "ts-node src/index.ts",
|
| 11 |
+
"start:prod": "npm run build && node dist/index.js",
|
| 12 |
+
"pm2:start": "npm run build && pm2 start ecosystem.config.js --env production",
|
| 13 |
+
"pm2:stop": "pm2 stop coderai-server",
|
| 14 |
+
"pm2:logs": "pm2 logs coderai-server",
|
| 15 |
+
"docker:build": "bash deploy.sh",
|
| 16 |
+
"test": "echo \"Error: no test specified\" && exit 1"
|
| 17 |
+
},
|
| 18 |
+
"keywords": [],
|
| 19 |
+
"author": "",
|
| 20 |
+
"license": "ISC",
|
| 21 |
+
"type": "commonjs",
|
| 22 |
+
"dependencies": {
|
| 23 |
+
"@types/better-sqlite3": "^7.6.13",
|
| 24 |
+
"@types/fs-extra": "^11.0.4",
|
| 25 |
+
"@types/http-proxy": "^1.17.17",
|
| 26 |
+
"@types/tar-fs": "^2.0.4",
|
| 27 |
+
"archiver": "^7.0.1",
|
| 28 |
+
"better-sqlite3": "^12.6.2",
|
| 29 |
+
"cors": "^2.8.6",
|
| 30 |
+
"dockerode": "^4.0.9",
|
| 31 |
+
"dotenv": "^17.3.1",
|
| 32 |
+
"express": "^5.2.1",
|
| 33 |
+
"fs-extra": "^11.3.4",
|
| 34 |
+
"http-proxy": "^1.18.1",
|
| 35 |
+
"tar-fs": "^3.1.2",
|
| 36 |
+
"uuid": "^13.0.0"
|
| 37 |
+
},
|
| 38 |
+
"devDependencies": {
|
| 39 |
+
"@types/archiver": "^7.0.0",
|
| 40 |
+
"@types/cors": "^2.8.19",
|
| 41 |
+
"@types/dockerode": "^4.0.1",
|
| 42 |
+
"@types/express": "^5.0.6",
|
| 43 |
+
"@types/node": "^25.4.0",
|
| 44 |
+
"@types/uuid": "^10.0.0",
|
| 45 |
+
"nodemon": "^3.1.14",
|
| 46 |
+
"ts-node": "^10.9.2",
|
| 47 |
+
"typescript": "^5.9.3"
|
| 48 |
+
}
|
| 49 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/index.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Neon Task</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
| 10 |
+
</head>
|
| 11 |
+
<body class="bg-background text-text font-['Syne']">
|
| 12 |
+
<div id="root"></div>
|
| 13 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 14 |
+
</body>
|
| 15 |
+
</html>
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "neon-tasks",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc && vite build",
|
| 9 |
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"framer-motion": "^11.3.8",
|
| 14 |
+
"lucide-react": "^0.395.0",
|
| 15 |
+
"react": "^18.3.1",
|
| 16 |
+
"react-dom": "^18.3.1",
|
| 17 |
+
"react-router-dom": "^6.26.0"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@types/react": "^18.3.3",
|
| 21 |
+
"@types/react-dom": "^18.3.0",
|
| 22 |
+
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
| 23 |
+
"@typescript-eslint/parser": "^6.10.0",
|
| 24 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 25 |
+
"autoprefixer": "^10.4.19",
|
| 26 |
+
"eslint": "^8.53.0",
|
| 27 |
+
"eslint-plugin-react-hooks": "^4.6.0",
|
| 28 |
+
"eslint-plugin-react-refresh": "^0.4.4",
|
| 29 |
+
"postcss": "^8.4.38",
|
| 30 |
+
"tailwindcss": "^3.4.1",
|
| 31 |
+
"typescript": "^5.5.3",
|
| 32 |
+
"vite": "^5.4.0"
|
| 33 |
+
}
|
| 34 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/App.tsx
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Routes from './routes';
|
| 2 |
+
|
| 3 |
+
function App() {
|
| 4 |
+
return <Routes />;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export default App;
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/components/TodoInput.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { motion } from 'framer-motion';
|
| 3 |
+
|
| 4 |
+
type TodoInputProps = {
|
| 5 |
+
onAdd: (text: string) => void;
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
export default function TodoInput({ onAdd }: TodoInputProps) {
|
| 9 |
+
const [text, setText] = useState('');
|
| 10 |
+
|
| 11 |
+
const handleSubmit = (e: React.FormEvent) => {
|
| 12 |
+
e.preventDefault();
|
| 13 |
+
if (text.trim()) {
|
| 14 |
+
onAdd(text);
|
| 15 |
+
setText('');
|
| 16 |
+
}
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
return (
|
| 20 |
+
<motion.form
|
| 21 |
+
onSubmit={handleSubmit}
|
| 22 |
+
initial={{ opacity: 0, y: 20 }}
|
| 23 |
+
animate={{ opacity: 1, y: 0 }}
|
| 24 |
+
transition={{ duration: 0.5 }}
|
| 25 |
+
className="flex gap-4 mb-8"
|
| 26 |
+
>
|
| 27 |
+
<input
|
| 28 |
+
type="text"
|
| 29 |
+
value={text}
|
| 30 |
+
onChange={(e) => setText(e.target.value)}
|
| 31 |
+
placeholder="What needs to be done?"
|
| 32 |
+
className="flex-1 px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-gray-100 placeholder-gray-400 focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/50 transition-all"
|
| 33 |
+
/>
|
| 34 |
+
<motion.button
|
| 35 |
+
whileHover={{ scale: 1.05 }}
|
| 36 |
+
whileTap={{ scale: 0.95 }}
|
| 37 |
+
type="submit"
|
| 38 |
+
className="px-6 py-3 bg-primary text-gray-900 font-bold rounded-lg hover:bg-primary/90 transition-all"
|
| 39 |
+
>
|
| 40 |
+
Add
|
| 41 |
+
</motion.button>
|
| 42 |
+
</motion.form>
|
| 43 |
+
);
|
| 44 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/components/TodoItem.tsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from 'framer-motion';
|
| 2 |
+
import { Todo } from '../types';
|
| 3 |
+
import { Check, X } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
interface TodoItemProps {
|
| 6 |
+
todo: Todo;
|
| 7 |
+
onToggle: (id: number) => void;
|
| 8 |
+
onDelete: (id: number) => void;
|
| 9 |
+
index: number;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default function TodoItem({ todo, onToggle, onDelete, index }: TodoItemProps) {
|
| 13 |
+
return (
|
| 14 |
+
<motion.li
|
| 15 |
+
initial={{ opacity: 0, y: 20 }}
|
| 16 |
+
animate={{ opacity: 1, y: 0 }}
|
| 17 |
+
exit={{ opacity: 0, x: -100 }}
|
| 18 |
+
transition={{ duration: 0.3, delay: index * 0.1 }}
|
| 19 |
+
className="flex items-center justify-between p-4 rounded-lg bg-black/30 border border-white/10 mb-3 last:mb-0 hover:bg-black/50 transition-all duration-300"
|
| 20 |
+
>
|
| 21 |
+
<div className="flex items-center gap-4">
|
| 22 |
+
<button
|
| 23 |
+
onClick={() => onToggle(todo.id)}
|
| 24 |
+
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all duration-300 ${
|
| 25 |
+
todo.completed
|
| 26 |
+
? 'border-primary bg-primary/20'
|
| 27 |
+
: 'border-white/30 hover:border-primary'
|
| 28 |
+
}`}
|
| 29 |
+
>
|
| 30 |
+
{todo.completed && <Check size={16} className="text-primary" />}
|
| 31 |
+
</button>
|
| 32 |
+
<span
|
| 33 |
+
className={`text-lg transition-all duration-300 ${
|
| 34 |
+
todo.completed ? 'text-white/50 line-through' : 'text-text'
|
| 35 |
+
}`}
|
| 36 |
+
>
|
| 37 |
+
{todo.text}
|
| 38 |
+
</span>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<button
|
| 42 |
+
onClick={() => onDelete(todo.id)}
|
| 43 |
+
className="text-white/30 hover:text-red-400 transition-colors duration-300 p-1 rounded-full hover:bg-red-400/10"
|
| 44 |
+
>
|
| 45 |
+
<X size={20} />
|
| 46 |
+
</button>
|
| 47 |
+
</motion.li>
|
| 48 |
+
);
|
| 49 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/components/TodoList.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from 'framer-motion';
|
| 2 |
+
import TodoItem from './TodoItem';
|
| 3 |
+
import { Todo } from '../types';
|
| 4 |
+
|
| 5 |
+
interface TodoListProps {
|
| 6 |
+
todos: Todo[];
|
| 7 |
+
onToggle: (id: number) => void;
|
| 8 |
+
onDelete: (id: number) => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default function TodoList({ todos, onToggle, onDelete }: TodoListProps) {
|
| 12 |
+
if (todos.length === 0) {
|
| 13 |
+
return (
|
| 14 |
+
<motion.div
|
| 15 |
+
initial={{ opacity: 0 }}
|
| 16 |
+
animate={{ opacity: 1 }}
|
| 17 |
+
className="text-center text-white/50 py-8"
|
| 18 |
+
>
|
| 19 |
+
No tasks yet. Add one above!
|
| 20 |
+
</motion.div>
|
| 21 |
+
);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
return (
|
| 25 |
+
<motion.ul
|
| 26 |
+
initial="hidden"
|
| 27 |
+
animate="visible"
|
| 28 |
+
variants={{
|
| 29 |
+
hidden: { opacity: 0 },
|
| 30 |
+
visible: {
|
| 31 |
+
opacity: 1,
|
| 32 |
+
transition: {
|
| 33 |
+
staggerChildren: 0.1
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}}
|
| 37 |
+
className="mt-6"
|
| 38 |
+
>
|
| 39 |
+
{todos.map((todo, index) => (
|
| 40 |
+
<TodoItem
|
| 41 |
+
key={todo.id}
|
| 42 |
+
todo={todo}
|
| 43 |
+
onToggle={onToggle}
|
| 44 |
+
onDelete={onDelete}
|
| 45 |
+
index={index}
|
| 46 |
+
/>
|
| 47 |
+
))}
|
| 48 |
+
</motion.ul>
|
| 49 |
+
);
|
| 50 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/index.css
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
@layer base {
|
| 6 |
+
html {
|
| 7 |
+
background: radial-gradient(ellipse at 50% 30%, rgba(0, 245, 255, 0.1) 0%, transparent 70%),
|
| 8 |
+
linear-gradient(135deg, rgba(10, 10, 26, 0.8) 0%, rgba(20, 20, 40, 0.8) 100%);
|
| 9 |
+
min-height: 100vh;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
body {
|
| 13 |
+
-webkit-font-smoothing: antialiased;
|
| 14 |
+
-moz-osx-font-smoothing: grayscale;
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
@keyframes glow {
|
| 19 |
+
0%, 100% {
|
| 20 |
+
box-shadow: 0 0 10px rgba(0, 245, 255, 0.3);
|
| 21 |
+
}
|
| 22 |
+
50% {
|
| 23 |
+
box-shadow: 0 0 20px rgba(0, 245, 255, 0.6);
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.glow {
|
| 28 |
+
animation: glow 3s ease-in-out infinite;
|
| 29 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App'
|
| 4 |
+
import './index.css'
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>,
|
| 10 |
+
)
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/pages/TodoPage.tsx
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
import { Todo } from '../types';
|
| 3 |
+
import TodoInput from '../components/TodoInput';
|
| 4 |
+
import TodoList from '../components/TodoList';
|
| 5 |
+
import { loadTodos, saveTodos } from '../utils/storage';
|
| 6 |
+
import { AnimatePresence } from 'framer-motion';
|
| 7 |
+
|
| 8 |
+
export default function TodoPage() {
|
| 9 |
+
const [todos, setTodos] = useState<Todo[]>([]);
|
| 10 |
+
const [animationKey, setAnimationKey] = useState(0);
|
| 11 |
+
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
setTodos(loadTodos());
|
| 14 |
+
}, []);
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
saveTodos(todos);
|
| 18 |
+
}, [todos]);
|
| 19 |
+
|
| 20 |
+
const addTodo = (text: string) => {
|
| 21 |
+
const newTodo: Todo = {
|
| 22 |
+
id: Date.now(),
|
| 23 |
+
text,
|
| 24 |
+
completed: false,
|
| 25 |
+
};
|
| 26 |
+
setTodos([...todos, newTodo]);
|
| 27 |
+
setAnimationKey(prev => prev + 1);
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
const toggleTodo = (id: number) => {
|
| 31 |
+
setTodos(todos.map(todo =>
|
| 32 |
+
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
| 33 |
+
));
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
const deleteTodo = (id: number) => {
|
| 37 |
+
setTodos(todos.filter(todo => todo.id !== id));
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
return (
|
| 41 |
+
<div className="min-h-screen bg-background text-text font-syne">
|
| 42 |
+
<div className="container mx-auto px-4 py-16 max-w-2xl">
|
| 43 |
+
<h1 className="text-5xl font-bold text-center mb-12 text-primary glow">
|
| 44 |
+
Neon Tasks
|
| 45 |
+
</h1>
|
| 46 |
+
|
| 47 |
+
<div className="bg-black/20 backdrop-blur-sm rounded-2xl p-8 border border-white/10 shadow-2xl shadow-primary/10">
|
| 48 |
+
<TodoInput onAdd={addTodo} />
|
| 49 |
+
|
| 50 |
+
<AnimatePresence mode="wait">
|
| 51 |
+
<TodoList
|
| 52 |
+
key={animationKey}
|
| 53 |
+
todos={todos}
|
| 54 |
+
onToggle={toggleTodo}
|
| 55 |
+
onDelete={deleteTodo}
|
| 56 |
+
/>
|
| 57 |
+
</AnimatePresence>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
);
|
| 62 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/routes.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
| 2 |
+
import TodoPage from './pages/TodoPage';
|
| 3 |
+
|
| 4 |
+
const router = createBrowserRouter([
|
| 5 |
+
{
|
| 6 |
+
path: '/',
|
| 7 |
+
element: <TodoPage />,
|
| 8 |
+
},
|
| 9 |
+
]);
|
| 10 |
+
|
| 11 |
+
const Routes = () => {
|
| 12 |
+
return <RouterProvider router={router} />;
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
export default Routes;
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/types.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface Todo {
|
| 2 |
+
id: number
|
| 3 |
+
text: string
|
| 4 |
+
completed: boolean
|
| 5 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/utils/animations.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Variants } from 'framer-motion';
|
| 2 |
+
|
| 3 |
+
export const fadeInUp: Variants = {
|
| 4 |
+
hidden: { opacity: 0, y: 20 },
|
| 5 |
+
visible: { opacity: 1, y: 0 },
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
export const staggerContainer = {
|
| 9 |
+
hidden: { opacity: 0 },
|
| 10 |
+
visible: {
|
| 11 |
+
opacity: 1,
|
| 12 |
+
transition: {
|
| 13 |
+
staggerChildren: 0.1,
|
| 14 |
+
},
|
| 15 |
+
},
|
| 16 |
+
};
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/src/utils/storage.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Todo } from '../types';
|
| 2 |
+
|
| 3 |
+
const STORAGE_KEY = 'neon-tasks';
|
| 4 |
+
|
| 5 |
+
export const loadTodos = (): Todo[] => {
|
| 6 |
+
try {
|
| 7 |
+
const stored = localStorage.getItem(STORAGE_KEY);
|
| 8 |
+
return stored ? JSON.parse(stored) : [];
|
| 9 |
+
} catch {
|
| 10 |
+
return [];
|
| 11 |
+
}
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
export const saveTodos = (todos: Todo[]): void => {
|
| 15 |
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
|
| 16 |
+
};
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/tailwind.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {
|
| 9 |
+
colors: {
|
| 10 |
+
primary: '#00f5ff',
|
| 11 |
+
background: '#0a0a1a',
|
| 12 |
+
text: '#e0e0e0',
|
| 13 |
+
},
|
| 14 |
+
fontFamily: {
|
| 15 |
+
sans: ['Syne', 'sans-serif'],
|
| 16 |
+
},
|
| 17 |
+
animation: {
|
| 18 |
+
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
},
|
| 22 |
+
plugins: [],
|
| 23 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
| 6 |
+
"allowJs": false,
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
"esModuleInterop": true,
|
| 9 |
+
"allowSyntheticDefaultImports": true,
|
| 10 |
+
"strict": true,
|
| 11 |
+
"forceConsistentCasingInFileNames": true,
|
| 12 |
+
"module": "ESNext",
|
| 13 |
+
"moduleResolution": "Node",
|
| 14 |
+
"resolveJsonModule": true,
|
| 15 |
+
"isolatedModules": true,
|
| 16 |
+
"noEmit": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
"types": ["vite/client"]
|
| 19 |
+
},
|
| 20 |
+
"include": ["src"],
|
| 21 |
+
"references": [{ "path": "./tsconfig.node.json" }]
|
| 22 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/tsconfig.node.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"composite": true,
|
| 4 |
+
"module": "ESNext",
|
| 5 |
+
"moduleResolution": "Node",
|
| 6 |
+
"allowSyntheticDefaultImports": true
|
| 7 |
+
},
|
| 8 |
+
"include": ["vite.config.ts"]
|
| 9 |
+
}
|
projects/060c1fce-28f4-472a-b360-97eb9dd5065d/vite.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
server: {
|
| 7 |
+
host: '0.0.0.0',
|
| 8 |
+
port: 3000,
|
| 9 |
+
strictPort: true
|
| 10 |
+
}
|
| 11 |
+
})
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/index.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Neon Task</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
| 10 |
+
</head>
|
| 11 |
+
<body class="bg-background text-text font-['Syne']">
|
| 12 |
+
<div id="root"></div>
|
| 13 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 14 |
+
</body>
|
| 15 |
+
</html>
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "neon-tasks",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc && vite build",
|
| 9 |
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"framer-motion": "^11.3.8",
|
| 14 |
+
"lucide-react": "^0.395.0",
|
| 15 |
+
"react": "^18.3.1",
|
| 16 |
+
"react-dom": "^18.3.1",
|
| 17 |
+
"react-router-dom": "^6.26.0"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@types/react": "^18.3.3",
|
| 21 |
+
"@types/react-dom": "^18.3.0",
|
| 22 |
+
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
| 23 |
+
"@typescript-eslint/parser": "^6.10.0",
|
| 24 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 25 |
+
"autoprefixer": "^10.4.19",
|
| 26 |
+
"eslint": "^8.53.0",
|
| 27 |
+
"eslint-plugin-react-hooks": "^4.6.0",
|
| 28 |
+
"eslint-plugin-react-refresh": "^0.4.4",
|
| 29 |
+
"postcss": "^8.4.38",
|
| 30 |
+
"tailwindcss": "^3.4.1",
|
| 31 |
+
"typescript": "^5.5.3",
|
| 32 |
+
"vite": "^5.4.0"
|
| 33 |
+
}
|
| 34 |
+
}
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/App.tsx
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Routes from './routes';
|
| 2 |
+
|
| 3 |
+
function App() {
|
| 4 |
+
return <Routes />;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export default App;
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/components/TodoInput.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { motion } from 'framer-motion';
|
| 3 |
+
|
| 4 |
+
type TodoInputProps = {
|
| 5 |
+
onAdd: (text: string) => void;
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
export default function TodoInput({ onAdd }: TodoInputProps) {
|
| 9 |
+
const [text, setText] = useState('');
|
| 10 |
+
|
| 11 |
+
const handleSubmit = (e: React.FormEvent) => {
|
| 12 |
+
e.preventDefault();
|
| 13 |
+
if (text.trim()) {
|
| 14 |
+
onAdd(text);
|
| 15 |
+
setText('');
|
| 16 |
+
}
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
return (
|
| 20 |
+
<motion.form
|
| 21 |
+
onSubmit={handleSubmit}
|
| 22 |
+
initial={{ opacity: 0, y: 20 }}
|
| 23 |
+
animate={{ opacity: 1, y: 0 }}
|
| 24 |
+
transition={{ duration: 0.5 }}
|
| 25 |
+
className="flex gap-4 mb-8"
|
| 26 |
+
>
|
| 27 |
+
<input
|
| 28 |
+
type="text"
|
| 29 |
+
value={text}
|
| 30 |
+
onChange={(e) => setText(e.target.value)}
|
| 31 |
+
placeholder="What needs to be done?"
|
| 32 |
+
className="flex-1 px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-gray-100 placeholder-gray-400 focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/50 transition-all"
|
| 33 |
+
/>
|
| 34 |
+
<motion.button
|
| 35 |
+
whileHover={{ scale: 1.05 }}
|
| 36 |
+
whileTap={{ scale: 0.95 }}
|
| 37 |
+
type="submit"
|
| 38 |
+
className="px-6 py-3 bg-primary text-gray-900 font-bold rounded-lg hover:bg-primary/90 transition-all"
|
| 39 |
+
>
|
| 40 |
+
Add
|
| 41 |
+
</motion.button>
|
| 42 |
+
</motion.form>
|
| 43 |
+
);
|
| 44 |
+
}
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/components/TodoItem.tsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from 'framer-motion';
|
| 2 |
+
import { Todo } from '../types';
|
| 3 |
+
import { Check, X } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
interface TodoItemProps {
|
| 6 |
+
todo: Todo;
|
| 7 |
+
onToggle: (id: number) => void;
|
| 8 |
+
onDelete: (id: number) => void;
|
| 9 |
+
index: number;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default function TodoItem({ todo, onToggle, onDelete, index }: TodoItemProps) {
|
| 13 |
+
return (
|
| 14 |
+
<motion.li
|
| 15 |
+
initial={{ opacity: 0, y: 20 }}
|
| 16 |
+
animate={{ opacity: 1, y: 0 }}
|
| 17 |
+
exit={{ opacity: 0, x: -100 }}
|
| 18 |
+
transition={{ duration: 0.3, delay: index * 0.1 }}
|
| 19 |
+
className="flex items-center justify-between p-4 rounded-lg bg-black/30 border border-white/10 mb-3 last:mb-0 hover:bg-black/50 transition-all duration-300"
|
| 20 |
+
>
|
| 21 |
+
<div className="flex items-center gap-4">
|
| 22 |
+
<button
|
| 23 |
+
onClick={() => onToggle(todo.id)}
|
| 24 |
+
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all duration-300 ${
|
| 25 |
+
todo.completed
|
| 26 |
+
? 'border-primary bg-primary/20'
|
| 27 |
+
: 'border-white/30 hover:border-primary'
|
| 28 |
+
}`}
|
| 29 |
+
>
|
| 30 |
+
{todo.completed && <Check size={16} className="text-primary" />}
|
| 31 |
+
</button>
|
| 32 |
+
<span
|
| 33 |
+
className={`text-lg transition-all duration-300 ${
|
| 34 |
+
todo.completed ? 'text-white/50 line-through' : 'text-text'
|
| 35 |
+
}`}
|
| 36 |
+
>
|
| 37 |
+
{todo.text}
|
| 38 |
+
</span>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<button
|
| 42 |
+
onClick={() => onDelete(todo.id)}
|
| 43 |
+
className="text-white/30 hover:text-red-400 transition-colors duration-300 p-1 rounded-full hover:bg-red-400/10"
|
| 44 |
+
>
|
| 45 |
+
<X size={20} />
|
| 46 |
+
</button>
|
| 47 |
+
</motion.li>
|
| 48 |
+
);
|
| 49 |
+
}
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/components/TodoList.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from 'framer-motion';
|
| 2 |
+
import TodoItem from './TodoItem';
|
| 3 |
+
import { Todo } from '../types';
|
| 4 |
+
|
| 5 |
+
interface TodoListProps {
|
| 6 |
+
todos: Todo[];
|
| 7 |
+
onToggle: (id: number) => void;
|
| 8 |
+
onDelete: (id: number) => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default function TodoList({ todos, onToggle, onDelete }: TodoListProps) {
|
| 12 |
+
if (todos.length === 0) {
|
| 13 |
+
return (
|
| 14 |
+
<motion.div
|
| 15 |
+
initial={{ opacity: 0 }}
|
| 16 |
+
animate={{ opacity: 1 }}
|
| 17 |
+
className="text-center text-white/50 py-8"
|
| 18 |
+
>
|
| 19 |
+
No tasks yet. Add one above!
|
| 20 |
+
</motion.div>
|
| 21 |
+
);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
return (
|
| 25 |
+
<motion.ul
|
| 26 |
+
initial="hidden"
|
| 27 |
+
animate="visible"
|
| 28 |
+
variants={{
|
| 29 |
+
hidden: { opacity: 0 },
|
| 30 |
+
visible: {
|
| 31 |
+
opacity: 1,
|
| 32 |
+
transition: {
|
| 33 |
+
staggerChildren: 0.1
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}}
|
| 37 |
+
className="mt-6"
|
| 38 |
+
>
|
| 39 |
+
{todos.map((todo, index) => (
|
| 40 |
+
<TodoItem
|
| 41 |
+
key={todo.id}
|
| 42 |
+
todo={todo}
|
| 43 |
+
onToggle={onToggle}
|
| 44 |
+
onDelete={onDelete}
|
| 45 |
+
index={index}
|
| 46 |
+
/>
|
| 47 |
+
))}
|
| 48 |
+
</motion.ul>
|
| 49 |
+
);
|
| 50 |
+
}
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/index.css
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
@layer base {
|
| 6 |
+
html {
|
| 7 |
+
background: radial-gradient(ellipse at 50% 30%, rgba(0, 245, 255, 0.1) 0%, transparent 70%),
|
| 8 |
+
linear-gradient(135deg, rgba(10, 10, 26, 0.8) 0%, rgba(20, 20, 40, 0.8) 100%);
|
| 9 |
+
min-height: 100vh;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
body {
|
| 13 |
+
-webkit-font-smoothing: antialiased;
|
| 14 |
+
-moz-osx-font-smoothing: grayscale;
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
@keyframes glow {
|
| 19 |
+
0%, 100% {
|
| 20 |
+
box-shadow: 0 0 10px rgba(0, 245, 255, 0.3);
|
| 21 |
+
}
|
| 22 |
+
50% {
|
| 23 |
+
box-shadow: 0 0 20px rgba(0, 245, 255, 0.6);
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.glow {
|
| 28 |
+
animation: glow 3s ease-in-out infinite;
|
| 29 |
+
}
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App'
|
| 4 |
+
import './index.css'
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>,
|
| 10 |
+
)
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/pages/TodoPage.tsx
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
import { Todo } from '../types';
|
| 3 |
+
import TodoInput from '../components/TodoInput';
|
| 4 |
+
import TodoList from '../components/TodoList';
|
| 5 |
+
import { loadTodos, saveTodos } from '../utils/storage';
|
| 6 |
+
import { AnimatePresence } from 'framer-motion';
|
| 7 |
+
|
| 8 |
+
export default function TodoPage() {
|
| 9 |
+
const [todos, setTodos] = useState<Todo[]>([]);
|
| 10 |
+
const [animationKey, setAnimationKey] = useState(0);
|
| 11 |
+
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
setTodos(loadTodos());
|
| 14 |
+
}, []);
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
saveTodos(todos);
|
| 18 |
+
}, [todos]);
|
| 19 |
+
|
| 20 |
+
const addTodo = (text: string) => {
|
| 21 |
+
const newTodo: Todo = {
|
| 22 |
+
id: Date.now(),
|
| 23 |
+
text,
|
| 24 |
+
completed: false,
|
| 25 |
+
};
|
| 26 |
+
setTodos([...todos, newTodo]);
|
| 27 |
+
setAnimationKey(prev => prev + 1);
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
const toggleTodo = (id: number) => {
|
| 31 |
+
setTodos(todos.map(todo =>
|
| 32 |
+
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
| 33 |
+
));
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
const deleteTodo = (id: number) => {
|
| 37 |
+
setTodos(todos.filter(todo => todo.id !== id));
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
return (
|
| 41 |
+
<div className="min-h-screen bg-background text-text font-syne">
|
| 42 |
+
<div className="container mx-auto px-4 py-16 max-w-2xl">
|
| 43 |
+
<h1 className="text-5xl font-bold text-center mb-12 text-primary glow">
|
| 44 |
+
Neon Tasks
|
| 45 |
+
</h1>
|
| 46 |
+
|
| 47 |
+
<div className="bg-black/20 backdrop-blur-sm rounded-2xl p-8 border border-white/10 shadow-2xl shadow-primary/10">
|
| 48 |
+
<TodoInput onAdd={addTodo} />
|
| 49 |
+
|
| 50 |
+
<AnimatePresence mode="wait">
|
| 51 |
+
<TodoList
|
| 52 |
+
key={animationKey}
|
| 53 |
+
todos={todos}
|
| 54 |
+
onToggle={toggleTodo}
|
| 55 |
+
onDelete={deleteTodo}
|
| 56 |
+
/>
|
| 57 |
+
</AnimatePresence>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
);
|
| 62 |
+
}
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/routes.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
| 2 |
+
import TodoPage from './pages/TodoPage';
|
| 3 |
+
|
| 4 |
+
const router = createBrowserRouter([
|
| 5 |
+
{
|
| 6 |
+
path: '/',
|
| 7 |
+
element: <TodoPage />,
|
| 8 |
+
},
|
| 9 |
+
]);
|
| 10 |
+
|
| 11 |
+
const Routes = () => {
|
| 12 |
+
return <RouterProvider router={router} />;
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
export default Routes;
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/types.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface Todo {
|
| 2 |
+
id: number
|
| 3 |
+
text: string
|
| 4 |
+
completed: boolean
|
| 5 |
+
}
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/utils/animations.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Variants } from 'framer-motion';
|
| 2 |
+
|
| 3 |
+
export const fadeInUp: Variants = {
|
| 4 |
+
hidden: { opacity: 0, y: 20 },
|
| 5 |
+
visible: { opacity: 1, y: 0 },
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
export const staggerContainer = {
|
| 9 |
+
hidden: { opacity: 0 },
|
| 10 |
+
visible: {
|
| 11 |
+
opacity: 1,
|
| 12 |
+
transition: {
|
| 13 |
+
staggerChildren: 0.1,
|
| 14 |
+
},
|
| 15 |
+
},
|
| 16 |
+
};
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/src/utils/storage.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Todo } from '../types';
|
| 2 |
+
|
| 3 |
+
const STORAGE_KEY = 'neon-tasks';
|
| 4 |
+
|
| 5 |
+
export const loadTodos = (): Todo[] => {
|
| 6 |
+
try {
|
| 7 |
+
const stored = localStorage.getItem(STORAGE_KEY);
|
| 8 |
+
return stored ? JSON.parse(stored) : [];
|
| 9 |
+
} catch {
|
| 10 |
+
return [];
|
| 11 |
+
}
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
export const saveTodos = (todos: Todo[]): void => {
|
| 15 |
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
|
| 16 |
+
};
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/tailwind.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {
|
| 9 |
+
colors: {
|
| 10 |
+
primary: '#00f5ff',
|
| 11 |
+
background: '#0a0a1a',
|
| 12 |
+
text: '#e0e0e0',
|
| 13 |
+
},
|
| 14 |
+
fontFamily: {
|
| 15 |
+
sans: ['Syne', 'sans-serif'],
|
| 16 |
+
},
|
| 17 |
+
animation: {
|
| 18 |
+
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
},
|
| 22 |
+
plugins: [],
|
| 23 |
+
}
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
| 6 |
+
"allowJs": false,
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
"esModuleInterop": true,
|
| 9 |
+
"allowSyntheticDefaultImports": true,
|
| 10 |
+
"strict": true,
|
| 11 |
+
"forceConsistentCasingInFileNames": true,
|
| 12 |
+
"module": "ESNext",
|
| 13 |
+
"moduleResolution": "Node",
|
| 14 |
+
"resolveJsonModule": true,
|
| 15 |
+
"isolatedModules": true,
|
| 16 |
+
"noEmit": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
"types": ["vite/client"]
|
| 19 |
+
},
|
| 20 |
+
"include": ["src"],
|
| 21 |
+
"references": [{ "path": "./tsconfig.node.json" }]
|
| 22 |
+
}
|
projects/706236f9-12ab-40ac-989e-75c18042cdb2/tsconfig.node.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"composite": true,
|
| 4 |
+
"module": "ESNext",
|
| 5 |
+
"moduleResolution": "Node",
|
| 6 |
+
"allowSyntheticDefaultImports": true
|
| 7 |
+
},
|
| 8 |
+
"include": ["vite.config.ts"]
|
| 9 |
+
}
|