Initial commit: MVP+ wish board with FastAPI + Tailwind
This commit is contained in:
commit
cd945a6128
2277 files changed
+843095
No files matched your search
+333
@@ -0,0 +1,333 @@
|
||||
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)
|
||||
Reference in new issue
Block a user