334 lines
13 KiB
Python
334 lines
13 KiB
Python
from fastapi import FastAPI, HTTPException
|
||
from fastapi.staticfiles import StaticFiles
|
||
from fastapi.responses import FileResponse
|
||
from pydantic import BaseModel
|
||
from typing import Optional
|
||
import sqlite3
|
||
import uuid
|
||
from datetime import datetime
|
||
import os
|
||
|
||
app = FastAPI()
|
||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||
|
||
DB_PATH = "wish_board.db"
|
||
|
||
def get_db():
|
||
conn = sqlite3.connect(DB_PATH)
|
||
conn.row_factory = sqlite3.Row
|
||
return conn
|
||
|
||
def init_db():
|
||
conn = get_db()
|
||
conn.execute("""
|
||
CREATE TABLE IF NOT EXISTS boards (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
color TEXT DEFAULT '#6366f1',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
""")
|
||
conn.execute("""
|
||
CREATE TABLE IF NOT EXISTS lists (
|
||
id TEXT PRIMARY KEY,
|
||
board_id TEXT NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
|
||
title TEXT NOT NULL,
|
||
position INTEGER NOT NULL DEFAULT 0,
|
||
FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE
|
||
)
|
||
""")
|
||
conn.execute("""
|
||
CREATE TABLE IF NOT EXISTS cards (
|
||
id TEXT PRIMARY KEY,
|
||
list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
|
||
board_id TEXT NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
|
||
title TEXT NOT NULL,
|
||
description TEXT DEFAULT '',
|
||
position INTEGER NOT NULL DEFAULT 0,
|
||
FOREIGN KEY (list_id) REFERENCES lists(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE
|
||
)
|
||
""")
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
init_db()
|
||
|
||
class BoardCreate(BaseModel):
|
||
name: str
|
||
color: str = "#6366f1"
|
||
|
||
class ListCreate(BaseModel):
|
||
title: str
|
||
|
||
class CardCreate(BaseModel):
|
||
title: str
|
||
description: str = ""
|
||
|
||
class BoardUpdate(BaseModel):
|
||
name: Optional[str] = None
|
||
color: Optional[str] = None
|
||
|
||
class ListUpdate(BaseModel):
|
||
title: Optional[str] = None
|
||
position: Optional[int] = None
|
||
|
||
class CardUpdate(BaseModel):
|
||
title: Optional[str] = None
|
||
description: Optional[str] = None
|
||
position: Optional[int] = None
|
||
list_id: Optional[str] = None
|
||
|
||
# Boards
|
||
@app.get("/api/boards")
|
||
def get_boards():
|
||
conn = get_db()
|
||
boards = conn.execute("SELECT * FROM boards ORDER BY created_at DESC").fetchall()
|
||
conn.close()
|
||
return [dict(b) for b in boards]
|
||
|
||
@app.post("/api/boards")
|
||
def create_board(data: BoardCreate):
|
||
board_id = str(uuid.uuid4())
|
||
conn = get_db()
|
||
conn.execute("INSERT INTO boards (id, name, color) VALUES (?, ?, ?)",
|
||
(board_id, data.name, data.color))
|
||
conn.commit()
|
||
conn.close()
|
||
return {"id": board_id, "name": data.name, "color": data.color}
|
||
|
||
@app.delete("/api/boards/{board_id}")
|
||
def delete_board(board_id: str):
|
||
conn = get_db()
|
||
conn.execute("DELETE FROM boards WHERE id = ?", (board_id,))
|
||
conn.commit()
|
||
conn.close()
|
||
return {"ok": True}
|
||
|
||
@app.patch("/api/boards/{board_id}")
|
||
def update_board(board_id: str, data: BoardUpdate):
|
||
conn = get_db()
|
||
updates = []
|
||
params = []
|
||
if data.name is not None:
|
||
updates.append("name = ?")
|
||
params.append(data.name)
|
||
if data.color is not None:
|
||
updates.append("color = ?")
|
||
params.append(data.color)
|
||
if updates:
|
||
params.append(board_id)
|
||
conn.execute(f"UPDATE boards SET {', '.join(updates)} WHERE id = ?", params)
|
||
conn.commit()
|
||
conn.close()
|
||
return {"ok": True}
|
||
|
||
# Lists
|
||
@app.get("/api/boards/{board_id}/lists")
|
||
def get_lists(board_id: str):
|
||
conn = get_db()
|
||
lists = conn.execute("SELECT * FROM lists WHERE board_id = ? ORDER BY position", (board_id,)).fetchall()
|
||
conn.close()
|
||
return [dict(l) for l in lists]
|
||
|
||
@app.post("/api/boards/{board_id}/lists")
|
||
def create_list(board_id: str, data: ListCreate):
|
||
list_id = str(uuid.uuid4())
|
||
conn = get_db()
|
||
max_pos = conn.execute("SELECT COALESCE(MAX(position), -1) as max FROM lists WHERE board_id = ?", (board_id,)).fetchone()["max"]
|
||
conn.execute("INSERT INTO lists (id, board_id, title, position) VALUES (?, ?, ?, ?)",
|
||
(list_id, board_id, data.title, max_pos + 1))
|
||
conn.commit()
|
||
conn.close()
|
||
return {"id": list_id, "board_id": board_id, "title": data.title, "position": max_pos + 1}
|
||
|
||
@app.patch("/api/lists/{list_id}")
|
||
def update_list(list_id: str, data: ListUpdate):
|
||
conn = get_db()
|
||
updates = []
|
||
params = []
|
||
if data.title is not None:
|
||
updates.append("title = ?")
|
||
params.append(data.title)
|
||
if data.position is not None:
|
||
updates.append("position = ?")
|
||
params.append(data.position)
|
||
if updates:
|
||
params.append(list_id)
|
||
conn.execute(f"UPDATE lists SET {', '.join(updates)} WHERE id = ?", params)
|
||
conn.commit()
|
||
conn.close()
|
||
return {"ok": True}
|
||
|
||
@app.delete("/api/lists/{list_id}")
|
||
def delete_list(list_id: str):
|
||
conn = get_db()
|
||
conn.execute("DELETE FROM lists WHERE id = ?", (list_id,))
|
||
conn.commit()
|
||
conn.close()
|
||
return {"ok": True}
|
||
|
||
# Cards
|
||
@app.get("/api/lists/{list_id}/cards")
|
||
def get_cards(list_id: str):
|
||
conn = get_db()
|
||
cards = conn.execute("SELECT * FROM cards WHERE list_id = ? ORDER BY position", (list_id,)).fetchall()
|
||
conn.close()
|
||
return [dict(c) for c in cards]
|
||
|
||
@app.post("/api/lists/{list_id}/cards")
|
||
def create_card(list_id: str, data: CardCreate):
|
||
card_id = str(uuid.uuid4())
|
||
conn = get_db()
|
||
list_row = conn.execute("SELECT board_id FROM lists WHERE id = ?", (list_id,)).fetchone()
|
||
board_id = list_row["board_id"]
|
||
max_pos = conn.execute("SELECT COALESCE(MAX(position), -1) as max FROM cards WHERE list_id = ?", (list_id,)).fetchone()["max"]
|
||
conn.execute("INSERT INTO cards (id, list_id, board_id, title, description, position) VALUES (?, ?, ?, ?, ?, ?)",
|
||
(card_id, list_id, board_id, data.title, data.description, max_pos + 1))
|
||
conn.commit()
|
||
conn.close()
|
||
return {"id": card_id, "list_id": list_id, "board_id": board_id, "title": data.title, "description": data.description, "position": max_pos + 1}
|
||
|
||
@app.patch("/api/cards/{card_id}")
|
||
def update_card(card_id: str, data: CardUpdate):
|
||
conn = get_db()
|
||
updates = []
|
||
params = []
|
||
if data.title is not None:
|
||
updates.append("title = ?")
|
||
params.append(data.title)
|
||
if data.description is not None:
|
||
updates.append("description = ?")
|
||
params.append(data.description)
|
||
if data.position is not None:
|
||
updates.append("position = ?")
|
||
params.append(data.position)
|
||
if data.list_id is not None:
|
||
updates.append("list_id = ?")
|
||
params.append(data.list_id)
|
||
if updates:
|
||
params.append(card_id)
|
||
conn.execute(f"UPDATE cards SET {', '.join(updates)} WHERE id = ?", params)
|
||
conn.commit()
|
||
conn.close()
|
||
return {"ok": True}
|
||
|
||
@app.delete("/api/cards/{card_id}")
|
||
def delete_card(card_id: str):
|
||
conn = get_db()
|
||
conn.execute("DELETE FROM cards WHERE id = ?", (card_id,))
|
||
conn.commit()
|
||
conn.close()
|
||
return {"ok": True}
|
||
|
||
# Serve frontend
|
||
@app.get("/")
|
||
def serve_index():
|
||
return FileResponse("static/index.html")
|
||
|
||
@app.get("/api/generate-cards")
|
||
def generate_random_cards(count: int = 5):
|
||
"""Generate random cards from predefined list."""
|
||
CARD_OPTIONS = [
|
||
"Изучить React и TypeScript",
|
||
"Написать мобильное приложение",
|
||
"Запустить свой SaaS-продукт",
|
||
"Собрать первый миллион на крипто",
|
||
"Путешествие в Японию",
|
||
"Купить дом в деревне",
|
||
"Научиться играть на гитаре",
|
||
"Прочитать 50 книг за год",
|
||
"Запустить YouTube канал",
|
||
"Создать AI-бота для бизнеса",
|
||
"Собрать свой первый ПК",
|
||
"Выучить английский на C1",
|
||
"Написать книгу",
|
||
"Открыть кофейню",
|
||
"Сделать татуировку",
|
||
"Прыгнуть с парашютом",
|
||
"Пройти марафон",
|
||
"Выучить Python с нуля",
|
||
"Собрать сервер для дома",
|
||
"Запустить Telegram-бот",
|
||
"Создать свой дизайн-систему",
|
||
"Написать игру на Unity",
|
||
"Собрать умный дом",
|
||
"Выучить три языка",
|
||
"Построить баню",
|
||
"Купить машину мечты",
|
||
"Собрать коллекцию винила",
|
||
"Пройти курс по UX/UI",
|
||
"Создать подкаст",
|
||
"Написать приложение для заметок",
|
||
"Собрать свой первый drone",
|
||
"Выучить JavaScript",
|
||
"Создать свой сайт-портфолио",
|
||
"Научиться фотографировать",
|
||
"Собрать свой первый сервер",
|
||
"Запустить свой блог",
|
||
"Создать свой мессенджер",
|
||
"Выучить Rust",
|
||
"Собрать свой первый VR-шлем",
|
||
"Написать свой фреймворк",
|
||
"Создать свою операционную систему",
|
||
"Выучить машинное обучение",
|
||
"Собрать свой первый робот",
|
||
"Написать свой компилятор",
|
||
"Создать свою базу данных",
|
||
"Выучить ассемблер",
|
||
"Собрать свой первый суперкомпьютер",
|
||
"Написать свой процессор",
|
||
"Создать свою криптовалюту",
|
||
"Выучить квантовые вычисления",
|
||
"Собрать свой первый квантовый компьютер",
|
||
"Написать свой AI-модель",
|
||
"Создать свою нейросеть",
|
||
"Выучить нейронные сети",
|
||
"Собрать свой первый GPU-кластер",
|
||
"Написать свой GPU-драйвер",
|
||
"Создать свою операционную систему для GPU",
|
||
"Выучить FPGA-программирование",
|
||
"Собрать свой первый FPGA-ускоритель",
|
||
"Написать свой ASIC",
|
||
"Создать свою квантовую программу",
|
||
"Выучить квантовую физику",
|
||
"Собрать свой первый квантовый датчик",
|
||
"Написать свой квантовый алгоритм",
|
||
"Создать свою квантовую сеть",
|
||
"Выучить квантовую криптографию",
|
||
"Собрать свой первый квантовый ретранслятор",
|
||
"Написать свой квантовый протокол",
|
||
"Создать свою квантовую облачную платформу",
|
||
"Выучить квантовое машинное обучение",
|
||
"Собрать свой первый квантовый симулятор",
|
||
"Написать свой квантовый компилятор",
|
||
"Создать свою квантовую IDE",
|
||
"Выучить квантовое программирование",
|
||
"Собрать свой первый квантовый отладчик",
|
||
"Написать свой квантовый тестер",
|
||
"Создать свою квантовую документацию",
|
||
"Выучить квантовую математику",
|
||
"Собрать свой первый квантовый калькулятор",
|
||
"Написать свой квантовый solver",
|
||
"Создать свою квантовую оптимизацию",
|
||
"Выучить квантовую статистику",
|
||
"Собрать свой первый квантовый генератор случайных чисел",
|
||
"Написать свой квантовый шифр",
|
||
"Создать свою квантовую стеганографию",
|
||
"Выучить квантовую теорию игр",
|
||
"Собрать свой первый квантовый аукцион",
|
||
"Написать свой квантовый консенсус",
|
||
"Создать свою квантовую блокчейн",
|
||
]
|
||
|
||
import random
|
||
selected = random.sample(CARD_OPTIONS, min(count, len(CARD_OPTIONS)))
|
||
return {
|
||
"cards": selected,
|
||
"total": len(CARD_OPTIONS)
|
||
}
|
||
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
uvicorn.run(app, host="0.0.0.0", port=4000)
|