diff --git a/app/app.py b/app/app.py index 4d967f1..fbb05dc 100755 --- a/app/app.py +++ b/app/app.py @@ -1,13 +1,126 @@ -from flask import Flask, send_file, render_template, request +from flask import Flask, abort, render_template, request + +import datetime +import json +import sqlite3 import subprocess +import config app = Flask(__name__) +DEFAULT_CATEGORIES = [ + {"name": "linux commands", "items": ["cd", "touch", "cat", "find"], "level": 1}, + {"name": "types of HTML input", "items": ["radio", "password", "submit", "text"], "level": 2}, + {"name": "sticky ___", "items": ["tape", "fingers", "situation", "wicket"], "level": 3}, + {"name": "how i feel, spelled backwards", "items": ["live", "deliver", "desserts", "denier"], "level": 4}, +] + +def get_puzzle_numbers(cursor): + rows = cursor.execute("SELECT number FROM puzzle ORDER BY number").fetchall() + return [row[0] for row in rows] + +def get_puzzle_categories(cursor, puzzle_number): + row = cursor.execute("SELECT data FROM puzzle WHERE number = ?", [puzzle_number]).fetchone() + if not row: + return None + try: + payload = json.loads(row[0]) + categories = payload.get("categories") + if isinstance(categories, list) and len(categories) == 4: + return categories + except (json.JSONDecodeError, TypeError): + return None + return None + +def render_puzzle(puzzle_number=None): + connection = sqlite3.connect(config.db_file) + cursor = connection.cursor() + puzzle_numbers = get_puzzle_numbers(cursor) + + if puzzle_number is None and puzzle_numbers: + puzzle_number = puzzle_numbers[-1] + + if puzzle_number is not None: + categories = get_puzzle_categories(cursor, puzzle_number) + if categories is None and puzzle_number in puzzle_numbers: + categories = DEFAULT_CATEGORIES + elif categories is None: + connection.close() + abort(404) + else: + categories = DEFAULT_CATEGORIES + + connection.close() + return render_template( + 'index.html', + categories=categories, + puzzle_numbers=puzzle_numbers, + current_puzzle=puzzle_number, + ) + @app.route('/') def index(): - return render_template('index.html') + return render_puzzle() + +@app.route('/') +def puzzle_by_number(puzzle_number): + return render_puzzle(puzzle_number) + +@app.route('/new', methods=['GET', 'POST']) +def new_puzzle(): + if request.method == 'GET': + return render_template('new.html', creation_date=datetime.date.today().isoformat()) + + author = request.form.get("author", "").strip() + creation_date = request.form.get("creation_date", "").strip() or datetime.date.today().isoformat() + categories = [] + for i in range(1, 5): + name = request.form.get(f"category_{i}_name", "").strip() + words = [] + for j in range(1, 5): + word = request.form.get(f"category_{i}_word_{j}", "").strip() + if word: + words.append(word) + categories.append({"name": name, "items": words, "level": i}) + + if not author: + return render_template( + 'new.html', + error="Author is required.", + creation_date=creation_date, + form=request.form + ), 400 + + if any(not cat["name"] or len(cat["items"]) != 4 for cat in categories): + return render_template( + 'new.html', + error="Each category needs a name and exactly 4 words.", + creation_date=creation_date, + form=request.form + ), 400 + + data = json.dumps({"categories": categories}, separators=(",", ":")) + connection = sqlite3.connect(config.db_file) + cursor = connection.cursor() + row = cursor.execute("SELECT COALESCE(MAX(number), 0) + 1 FROM puzzle").fetchone() + number = row[0] + cursor.execute( + """ + INSERT INTO puzzle (number, author, creation_date, data) + VALUES (?, ?, ?, ?) + """, + [number, author, creation_date, data] + ) + connection.commit() + connection.close() + + return render_template( + 'new.html', + success=f"Saved puzzle #{number}.", + creation_date=creation_date + ) if __name__ == '__main__': - subprocess.run(["python3", "make_db.py"]) + subprocess.run(["python3", "make_db.py"], check=True) app.run(host='0.0.0.0', port=8000) diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..5d2c0c7 --- /dev/null +++ b/app/config.py @@ -0,0 +1,2 @@ +db_dir = "/connections/db" +db_file = f"{db_dir}/data.db" diff --git a/app/make_db.py b/app/make_db.py new file mode 100644 index 0000000..43a63d3 --- /dev/null +++ b/app/make_db.py @@ -0,0 +1,20 @@ +import os +import sqlite3 + +import config + +os.makedirs(config.db_dir, exist_ok=True) +connection = sqlite3.connect(config.db_file) +cursor = connection.cursor() +cursor.execute( + """ + CREATE TABLE IF NOT EXISTS puzzle( + number INTEGER PRIMARY KEY, + author TEXT, + creation_date TEXT, + data CLOB + ) + """ +) +connection.commit() +connection.close() diff --git a/app/static/css/style.css b/app/static/css/style.css index 1cc57ab..a0a6517 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -19,6 +19,26 @@ h1 { margin-bottom: 20px; letter-spacing: 2px; } +.header-links { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; + margin-bottom: 14px; +} +.header-links a { + border: 1px solid #000; + border-radius: 999px; + padding: 6px 10px; + text-decoration: none; + color: #000; + font-weight: bold; + font-size: 13px; +} +.header-links a.active { + background: #000; + color: #fff; +} #grid { display: grid; grid-template-columns: repeat(4, 1fr); @@ -84,3 +104,82 @@ button:disabled { height: 24px; color: #d33; } +#message.ok { + color: #2b7d32; +} +.subtitle { + margin-top: -8px; + margin-bottom: 18px; + font-weight: bold; + letter-spacing: 1px; +} +.builder { + width: 100%; + max-width: 700px; +} +.meta-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 14px; +} +.meta-row label { + display: flex; + flex-direction: column; + font-size: 13px; + gap: 6px; + font-weight: bold; +} +.meta-row input { + border: 1px solid #b8b8ae; + border-radius: 6px; + padding: 10px; + font-size: 14px; +} +.builder-grid { + max-width: 700px; +} +.builder-row { + height: auto; + padding: 10px; + align-items: stretch; +} +.builder-row > input { + border: 1px solid #8f8f8f; + border-radius: 6px; + padding: 10px; + font-size: 14px; + font-weight: bold; +} +.builder-words { + margin-top: 8px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} +.builder-words input { + border: 1px solid #8f8f8f; + border-radius: 6px; + padding: 10px; + font-size: 14px; +} +.ghost-link { + padding: 12px 24px; + border-radius: 25px; + border: 1px solid #000; + background: white; + color: #000; + cursor: pointer; + font-weight: bold; + font-size: 14px; + text-decoration: none; + line-height: 22px; +} +@media (max-width: 640px) { + .meta-row { + grid-template-columns: 1fr; + } + .builder-words { + grid-template-columns: 1fr; + } +} diff --git a/app/templates/index.html b/app/templates/index.html index 72712a2..0abf0e5 100755 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -9,6 +9,13 @@ +

Connections

@@ -23,13 +30,7 @@