simple-web-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 86f6ceed6064a92690e70796a627166c3840462e
parent bfbc6ede8614d4cf0bdb2017daa340726ff35c34
Author: Silas Brack <silasbrack@gmail.com>
Date:   Wed,  1 Oct 2025 22:37:00 +0200

feat: add news

Diffstat:
MREADME.md | 5+++++
Msrc/simple_web_app/app.py | 60++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/simple_web_app/migration.py | 14+++++++-------
Asrc/simple_web_app/migrations/20251001193400_add_news_table.sql | 43+++++++++++++++++++++++++++++++++++++++++++
Dsrc/simple_web_app/migrations/first.sql | 6------
Dsrc/simple_web_app/migrations/second.sql | 1-
Msrc/simple_web_app/queries/basic.sql | 28+++++++++++++++++++++++++---
Msrc/simple_web_app/static/style.css | 1-
Msrc/simple_web_app/templates/index.html | 105+++++++++++--------------------------------------------------------------------
Asrc/simple_web_app/templates/load_more.html | 1+
Msrc/simple_web_app/templates/news_card.html | 25++++++++++++++++++-------
Asrc/simple_web_app/templates/news_cards.html | 3+++
Asrc/simple_web_app/templates/oob_swap.html | 2++
Msrc/simple_web_app/templates/search.html | 7-------
Msrc/simple_web_app/templates/search_results.html | 13+++++++++++--
Msrc/simple_web_app/templates/settings.html | 52+++++++++++++++-------------------------------------
Asrc/simple_web_app/templates/settings_tab.html | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
17 files changed, 327 insertions(+), 178 deletions(-)

diff --git a/README.md b/README.md @@ -8,3 +8,8 @@ Configuration, on the other hand, should, whenever possible, be kept outside of Keep the application itself configuration-agnostic whenever possible and choose to instead maintain the `.env.example` file containing a suggested default configuration for developers. Configuration should thus always be performed via environment variables. +## Todo +- ~~Use OOB for loading more articles~~ +- Look into Jinja macros for components +- ~~Fix tabs~~ + diff --git a/src/simple_web_app/app.py b/src/simple_web_app/app.py @@ -1,9 +1,11 @@ import contextlib import importlib.resources import logging +import time import os import random import sqlite3 +import datetime from pathlib import Path import aiosql @@ -79,14 +81,41 @@ def render( ) -async def show_home_page(request: Request): - return render(request, "index.html") +def format_timedelta(tdelta, fmt): + d = {"days": tdelta.days} + d["hours"], rem = divmod(tdelta.seconds, 3600) + d["minutes"], d["seconds"] = divmod(rem, 60) + return fmt.format(**d) -async def join_chat(request: Request): - async with request.form() as form: - username = form["username"] - return render(request, "chat.html") +async def show_home_page(request: Request): + page = request.query_params.get("page", default=0) + page = int(page) if page is not None else page + current_time = request.query_params.get("current_time", default=datetime.datetime.now(tz=datetime.UTC)) + + limit = 5 + offset = page * limit + + t0 = time.time() + rows = await queries_basic.get_categories(request.state.conn, limit=20) + all_categories = [row["category"] for row in rows] + + rows = await queries_basic.get_news(request.state.conn, limit=limit, offset=offset, max_published_time=current_time) + news = [dict(row) for row in rows] + for new in news: + rows = await queries_basic.get_categories_for_news(request.state.conn, id=new["id"]) + categories = [row["category"] for row in rows] + new["categories"] = categories + published = datetime.datetime.strptime(new["published"], "%Y-%m-%dT%H:%M:%S.%f%z") + new["time_since_published"] = format_timedelta(datetime.datetime.now(tz=datetime.UTC) - published, "{days} days, {hours} hours ago") + elapsed = time.time() - t0 + logger.info({"event": "load_page", "page": "/home", "time_s": elapsed}) + + context = {"news": news, "categories": all_categories, "page": page, "current_time": current_time} + if is_htmx_request(request): + context = context | {"oob": True} + template_name = "oob_swap.html" if is_htmx_request(request) else "index.html" + return render(request, template_name, context=context) async def open_search(request: Request): @@ -94,23 +123,23 @@ async def open_search(request: Request): async def search(request: Request): - values = [ - {"firstname": "Venus", "lastname": "Grimes", "email": "lectus.rutrum@Duisa.edu"}, - {"firstname": "Fletcher", "lastname": "Owen", "email": "metus@Aenean.org"}, - {"firstname": "William", "lastname": "Hale", "email": "eu.dolor@risusodio.edu"}, - {"firstname": "TaShya", "lastname": "Cash", "email": "tincidunt.orci.quis@nuncnullavulputate.co.uk"}, - ] - random.shuffle(values) - return render(request, "search_results.html", context={"people": values}) + async with request.form() as form: + query = form["search"] + rows = await queries_basic.search_news(request.state.conn, query=query, limit=10) if query else [] + return render(request, "search_results.html", context={"news": rows}) async def open_settings(request: Request): - return render(request, "settings.html") + tab = request.query_params.get("tab", default=None) + if tab: + return render(request, "settings_tab.html", context={"tab": tab}) + return render(request, "settings.html", context={"tab": "general"}) @contextlib.asynccontextmanager async def lifespan(app: Starlette): conn = sqlite3.connect(DATABASE_PATH) + conn.autocommit = True create_migrations_table_if_not_exists(conn) migration_files = sorted(MIGRATION_DIR.glob("*.sql")) migration_queries = [p.read_text() for p in migration_files] @@ -128,7 +157,6 @@ routes = [ Route("/search", methods=["GET"], endpoint=open_search), Route("/search", methods=["POST"], endpoint=search), Route("/settings", methods=["GET"], endpoint=open_settings), - Route("/chat/join", methods=["POST"], endpoint=join_chat), Mount("/static", StaticFiles(directory=STATIC_DIR), name="static"), ] middleware = [ diff --git a/src/simple_web_app/migration.py b/src/simple_web_app/migration.py @@ -23,17 +23,17 @@ def apply_migrations(conn: sqlite3.Connection, migration_queries: list[str]) -> num_migrations = len(migration_queries) for i in range(db_version, num_migrations): query = migration_queries[i] - logger.info(f"Running migration {i}: {query}") - conn.execute("BEGIN") + print(f"Running migration {i}: {query}") + conn.execute("BEGIN TRANSACTION") try: _ = conn.executescript(query) - conn.commit() + _ = conn.execute("UPDATE migration_version SET version = ?", [i + 1]) + conn.execute("COMMIT TRANSACTION") except Exception as e: - logger.error(f"Encountered the following error while running the migration: {e}") - conn.rollback() + conn.execute("ROLLBACK") + logger.exception(f"Encountered the following error while running the migration: {e}") raise - _ = conn.execute("UPDATE migration_version SET version = ?", [i + 1]) - conn.commit() + def create_migration(name: str, migrations_dir: Path): diff --git a/src/simple_web_app/migrations/20251001193400_add_news_table.sql b/src/simple_web_app/migrations/20251001193400_add_news_table.sql @@ -0,0 +1,43 @@ +CREATE TABLE news_item ( + id INTEGER PRIMARY KEY, + url TEXT, + title TEXT NOT NULL, + text TEXT NOT NULL, + published TEXT NOT NULL, + author TEXT, + language TEXT NOT NULL +) STRICT; + +CREATE TABLE category ( + id INTEGER PRIMARY KEY, + name TEXT +) STRICT; + +CREATE TABLE news_item_category ( + id INTEGER PRIMARY KEY, + news_item_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + FOREIGN KEY(news_item_id) REFERENCES news_item(id), + FOREIGN KEY(category_id) REFERENCES category(id) +) STRICT; + +CREATE VIRTUAL TABLE news_item_fts USING fts5( + title, + text, + content='news_item', + content_rowid='id' +); + +CREATE TRIGGER news_item_ai AFTER INSERT ON news_item BEGIN + INSERT INTO news_item_fts(rowid, title, text) + VALUES (new.id, new.title, new.text); +END; +CREATE TRIGGER news_item_au AFTER UPDATE ON news_item BEGIN + UPDATE news_item_fts SET title=new.title, text=new.text + WHERE rowid=old.id; +END; +CREATE TRIGGER news_item_ad AFTER DELETE ON news_item BEGIN + DELETE FROM news_item_fts WHERE rowid=old.id; +END; + + diff --git a/src/simple_web_app/migrations/first.sql b/src/simple_web_app/migrations/first.sql @@ -1,6 +0,0 @@ -CREATE TABLE todos ( - id INTEGER PRIMARY KEY, - content TEXT, - create_timestamp TEXT, - done INT DEFAULT 0 -) STRICT; diff --git a/src/simple_web_app/migrations/second.sql b/src/simple_web_app/migrations/second.sql @@ -1 +0,0 @@ -INSERT INTO todos (content, create_timestamp) VALUES ('Go to the dentist', CURRENT_TIMESTAMP); diff --git a/src/simple_web_app/queries/basic.sql b/src/simple_web_app/queries/basic.sql @@ -1,4 +1,26 @@ --- name: get_todos(limit) -SELECT * -FROM todos +-- name: get_news(limit, offset, max_published_time) +SELECT id, title, text, published +FROM news_item +WHERE language = 'english' + AND (published < :max_published_time) +ORDER BY published DESC, id ASC +LIMIT :limit +OFFSET :offset; + +-- name: search_news(query, limit) +SELECT title, text +FROM news_item_fts(:query) LIMIT :limit; + +-- name: get_categories_for_news(id) +SELECT category_id +FROM news_item_category +WHERE id = :id; + +-- name: get_categories(limit) +SELECT category_id +FROM news_item_category +GROUP BY category_id +ORDER BY COUNT(*) DESC +LIMIT :limit; + diff --git a/src/simple_web_app/static/style.css b/src/simple_web_app/static/style.css @@ -1 +0,0 @@ - diff --git a/src/simple_web_app/templates/index.html b/src/simple_web_app/templates/index.html @@ -2,117 +2,40 @@ <!-- <article> --> <nav> <ul> - <li><a href="#" class="secondary"><img src="https://kite.kagi.com/svg/kagi_news_compact_dark.svg"></img></a></li> + <li><a href="/" class="secondary"><img src="https://kite.kagi.com/svg/kagi_news_compact_dark.svg"></img></a></li> </ul> <ul> - <li><strong>Tuesday, September 30</strong></li> + <li><strong style="cursor: pointer;" data-tooltip="Choose another date" data-placement="bottom">Tuesday, September 30</strong></li> </ul> <ul> - <li><a style="cursor: pointer;" hx-get="/search" hx-target="#modal-placeholder" hx-swap="innerHTML" class="secondary"><svg height="20pt" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM272 416C351.5 416 416 351.5 416 272C416 192.5 351.5 128 272 128C192.5 128 128 192.5 128 272C128 351.5 192.5 416 272 416z"/></svg></a></li> - <li><a class="secondary"><svg height="20pt" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M349.1 114.7C343.9 103.3 332.5 96 320 96C307.5 96 296.1 103.3 290.9 114.7L123.5 480L112 480C94.3 480 80 494.3 80 512C80 529.7 94.3 544 112 544L200 544C217.7 544 232 529.7 232 512C232 494.3 217.7 480 200 480L193.9 480L215.9 432L424.2 432L446.2 480L440.1 480C422.4 480 408.1 494.3 408.1 512C408.1 529.7 422.4 544 440.1 544L528.1 544C545.8 544 560.1 529.7 560.1 512C560.1 494.3 545.8 480 528.1 480L516.6 480L349.2 114.7zM394.8 368L245.2 368L320 204.8L394.8 368z"/></svg></a></li> - <li><a style="cursor: pointer;" hx-get="/settings" hx-target="#modal-placeholder" hx-swap="innerHTML" class="secondary"><svg height="20pt" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M259.1 73.5C262.1 58.7 275.2 48 290.4 48L350.2 48C365.4 48 378.5 58.7 381.5 73.5L396 143.5C410.1 149.5 423.3 157.2 435.3 166.3L503.1 143.8C517.5 139 533.3 145 540.9 158.2L570.8 210C578.4 223.2 575.7 239.8 564.3 249.9L511 297.3C511.9 304.7 512.3 312.3 512.3 320C512.3 327.7 511.8 335.3 511 342.7L564.4 390.2C575.8 400.3 578.4 417 570.9 430.1L541 481.9C533.4 495 517.6 501.1 503.2 496.3L435.4 473.8C423.3 482.9 410.1 490.5 396.1 496.6L381.7 566.5C378.6 581.4 365.5 592 350.4 592L290.6 592C275.4 592 262.3 581.3 259.3 566.5L244.9 496.6C230.8 490.6 217.7 482.9 205.6 473.8L137.5 496.3C123.1 501.1 107.3 495.1 99.7 481.9L69.8 430.1C62.2 416.9 64.9 400.3 76.3 390.2L129.7 342.7C128.8 335.3 128.4 327.7 128.4 320C128.4 312.3 128.9 304.7 129.7 297.3L76.3 249.8C64.9 239.7 62.3 223 69.8 209.9L99.7 158.1C107.3 144.9 123.1 138.9 137.5 143.7L205.3 166.2C217.4 157.1 230.6 149.5 244.6 143.4L259.1 73.5zM320.3 400C364.5 399.8 400.2 363.9 400 319.7C399.8 275.5 363.9 239.8 319.7 240C275.5 240.2 239.8 276.1 240 320.3C240.2 364.5 276.1 400.2 320.3 400z"/></svg></a></li> + <li><a style="cursor: pointer;" data-tooltip="Search" hx-trigger="click, keyup[ctrlKey&&shiftKey&&key=='O']" hx-get="/search" hx-target="#modal-placeholder" hx-swap="innerHTML" class="secondary"><svg height="20pt" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM272 416C351.5 416 416 351.5 416 272C416 192.5 351.5 128 272 128C192.5 128 128 192.5 128 272C128 351.5 192.5 416 272 416z"/></svg></a></li> + <li><a data-tooltip="Change font size" class="secondary"><svg height="20pt" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M349.1 114.7C343.9 103.3 332.5 96 320 96C307.5 96 296.1 103.3 290.9 114.7L123.5 480L112 480C94.3 480 80 494.3 80 512C80 529.7 94.3 544 112 544L200 544C217.7 544 232 529.7 232 512C232 494.3 217.7 480 200 480L193.9 480L215.9 432L424.2 432L446.2 480L440.1 480C422.4 480 408.1 494.3 408.1 512C408.1 529.7 422.4 544 440.1 544L528.1 544C545.8 544 560.1 529.7 560.1 512C560.1 494.3 545.8 480 528.1 480L516.6 480L349.2 114.7zM394.8 368L245.2 368L320 204.8L394.8 368z"/></svg></a></li> + <li><a style="cursor: pointer;" data-tooltip="Settings" hx-get="/settings" hx-target="#modal-placeholder" hx-swap="innerHTML" class="secondary"><svg height="20pt" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M259.1 73.5C262.1 58.7 275.2 48 290.4 48L350.2 48C365.4 48 378.5 58.7 381.5 73.5L396 143.5C410.1 149.5 423.3 157.2 435.3 166.3L503.1 143.8C517.5 139 533.3 145 540.9 158.2L570.8 210C578.4 223.2 575.7 239.8 564.3 249.9L511 297.3C511.9 304.7 512.3 312.3 512.3 320C512.3 327.7 511.8 335.3 511 342.7L564.4 390.2C575.8 400.3 578.4 417 570.9 430.1L541 481.9C533.4 495 517.6 501.1 503.2 496.3L435.4 473.8C423.3 482.9 410.1 490.5 396.1 496.6L381.7 566.5C378.6 581.4 365.5 592 350.4 592L290.6 592C275.4 592 262.3 581.3 259.3 566.5L244.9 496.6C230.8 490.6 217.7 482.9 205.6 473.8L137.5 496.3C123.1 501.1 107.3 495.1 99.7 481.9L69.8 430.1C62.2 416.9 64.9 400.3 76.3 390.2L129.7 342.7C128.8 335.3 128.4 327.7 128.4 320C128.4 312.3 128.9 304.7 129.7 297.3L76.3 249.8C64.9 239.7 62.3 223 69.8 209.9L99.7 158.1C107.3 144.9 123.1 138.9 137.5 143.7L205.3 166.2C217.4 157.1 230.6 149.5 244.6 143.4L259.1 73.5zM320.3 400C364.5 399.8 400.2 363.9 400 319.7C399.8 275.5 363.9 239.8 319.7 240C275.5 240.2 239.8 276.1 240 320.3C240.2 364.5 276.1 400.2 320.3 400z"/></svg></a></li> </ul> </nav> - <div class="overflow-auto"> + <div class="overflow-auto" style="text-wrap: nowrap;"> <table> <tr> - <th><strong><a href="#"><strong><a href="#">World</a></strong></th> - <th><strong><a href="#"><strong><a href="#">USA</a></strong></th> - <th><strong><a href="#"><strong><a href="#">Business</a></strong></th> - <th><strong><a href="#"><strong><a href="#">Technology</a></strong></th> - <th><strong><a href="#"><strong><a href="#">Science</a></strong></th> - <th><strong><a href="#"><strong><a href="#">Sports</a></strong></th> - <th><strong><a href="#"><strong><a href="#">Gaming</a></strong></th> - <th><strong><a href="#"><strong><a href="#">Today in History</a></strong></th> + {% for category in categories %} + <th><div hx-get="/" hx-target="#news-cards" style="cursor: pointer;">{{ category }}</div></th> + {% endfor %} </tr> </table> </div> <div id="modal-placeholder"></div> - <p> - <article> - <header><small><a href="">US Politics</a></small></header> - <details> - <summary> - <strong>Trump, Congress deadlock as shutdown deadline nears</strong> - </summary> - <p><small> - Washington is bracing for a government shutdown as federal funding expires at midnight after President Trump and congressional leaders ended a White House meeting without an agreement [1][2][3][4][5][6]. Republicans are pressing a clean stopgap bill to fund agencies through November 21, while Democrats insist on extending health-care subsidies and reversing recent Medicaid cuts in any deal [7][8][9]. A lapse would quickly disrupt services and likely delay key economic reports on jobs and inflation, adding uncertainty for households and markets [10][11][12][13][14]. - </small></p> - </details> - </article> - - <article> - <header><a href=""><small>Censorship</small></a></header> - <details> - <summary> - <strong>Taliban cuts internet nationwide, flights grounded in Afghanistan </strong> - </summary> - <p> - Washington is bracing for a government shutdown as federal funding expires at midnight after President Trump and congressional leaders ended a White House meeting without an agreement [1][2][3][4][5][6]. Republicans are pressing a clean stopgap bill to fund agencies through November 21, while Democrats insist on extending health-care subsidies and reversing recent Medicaid cuts in any deal [7][8][9]. A lapse would quickly disrupt services and likely delay key economic reports on jobs and inflation, adding uncertainty for households and markets [10][11][12][13][14]. - </p> - </details> - </article> - - <article> - <header><small><a href="">Disaster</a></small></header> - - <details> - <summary> - <strong> - Indonesian school collapse leaves 38 missing, 77 hurt - </strong> - </summary> - <p> - Washington is bracing for a government shutdown as federal funding expires at midnight after President Trump and congressional leaders ended a White House meeting without an agreement [1][2][3][4][5][6]. Republicans are pressing a clean stopgap bill to fund agencies through November 21, while Democrats insist on extending health-care subsidies and reversing recent Medicaid cuts in any deal [7][8][9]. A lapse would quickly disrupt services and likely delay key economic reports on jobs and inflation, adding uncertainty for households and markets [10][11][12][13][14]. - </p> - </details> - </article> - - <article> - <header><small><a href="">Social Media</a></small></header> - <details> - <summary> - <strong>Youtube settles Trump suspension lawsuit for $24.5m</strong> - </summary> - <p> - Washington is bracing for a government shutdown as federal funding expires at midnight after President Trump and congressional leaders ended a White House meeting without an agreement [1][2][3][4][5][6]. Republicans are pressing a clean stopgap bill to fund agencies through November 21, while Democrats insist on extending health-care subsidies and reversing recent Medicaid cuts in any deal [7][8][9]. A lapse would quickly disrupt services and likely delay key economic reports on jobs and inflation, adding uncertainty for households and markets [10][11][12][13][14]. - </p> - </details> - </article> - - <article> - <header><small><a href="">Espionage</a></small></header> - <details> - <summary> - <strong>German court jails AfD aide for China spying</strong> - </summary> - <p> - Washington is bracing for a government shutdown as federal funding expires at midnight after President Trump and congressional leaders ended a White House meeting without an agreement [1][2][3][4][5][6]. Republicans are pressing a clean stopgap bill to fund agencies through November 21, while Democrats insist on extending health-care subsidies and reversing recent Medicaid cuts in any deal [7][8][9]. A lapse would quickly disrupt services and likely delay key economic reports on jobs and inflation, adding uncertainty for households and markets [10][11][12][13][14]. - </p> - </details> - </article> - - <article> - <header><small><a href="">Migration</a></small></header> - <details> - <summary> - <strong>US deports 120 Iranians after deal</strong> - </summary> - <p> - Washington is bracing for a government shutdown as federal funding expires at midnight after President Trump and congressional leaders ended a White House meeting without an agreement [1][2][3][4][5][6]. Republicans are pressing a clean stopgap bill to fund agencies through November 21, while Democrats insist on extending health-care subsidies and reversing recent Medicaid cuts in any deal [7][8][9]. A lapse would quickly disrupt services and likely delay key economic reports on jobs and inflation, adding uncertainty for households and markets [10][11][12][13][14]. - </p> - </details> - </article> - </p> - <nav><ul></ul><ul><li> <button>Mark all as read</button></li></ul><ul></ul></nav> + <div id="news-cards"> + {% include 'news_cards.html' %} + </div> + <nav><ul></ul><ul><li>{% include 'load_more.html' %}</li><li> <button>Mark all as read</button></li></ul><ul></ul></nav> <!-- </article> --> <footer> <nav> <ul></ul> <ul> <li><a href=""><svg height="24pt" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M237.9 461.4C237.9 463.4 235.6 465 232.7 465C229.4 465.3 227.1 463.7 227.1 461.4C227.1 459.4 229.4 457.8 232.3 457.8C235.3 457.5 237.9 459.1 237.9 461.4zM206.8 456.9C206.1 458.9 208.1 461.2 211.1 461.8C213.7 462.8 216.7 461.8 217.3 459.8C217.9 457.8 216 455.5 213 454.6C210.4 453.9 207.5 454.9 206.8 456.9zM251 455.2C248.1 455.9 246.1 457.8 246.4 460.1C246.7 462.1 249.3 463.4 252.3 462.7C255.2 462 257.2 460.1 256.9 458.1C256.6 456.2 253.9 454.9 251 455.2zM316.8 72C178.1 72 72 177.3 72 316C72 426.9 141.8 521.8 241.5 555.2C254.3 557.5 258.8 549.6 258.8 543.1C258.8 536.9 258.5 502.7 258.5 481.7C258.5 481.7 188.5 496.7 173.8 451.9C173.8 451.9 162.4 422.8 146 415.3C146 415.3 123.1 399.6 147.6 399.9C147.6 399.9 172.5 401.9 186.2 425.7C208.1 464.3 244.8 453.2 259.1 446.6C261.4 430.6 267.9 419.5 275.1 412.9C219.2 406.7 162.8 398.6 162.8 302.4C162.8 274.9 170.4 261.1 186.4 243.5C183.8 237 175.3 210.2 189 175.6C209.9 169.1 258 202.6 258 202.6C278 197 299.5 194.1 320.8 194.1C342.1 194.1 363.6 197 383.6 202.6C383.6 202.6 431.7 169 452.6 175.6C466.3 210.3 457.8 237 455.2 243.5C471.2 261.2 481 275 481 302.4C481 398.9 422.1 406.6 366.2 412.9C375.4 420.8 383.2 435.8 383.2 459.3C383.2 493 382.9 534.7 382.9 542.9C382.9 549.4 387.5 557.3 400.2 555C500.2 521.8 568 426.9 568 316C568 177.3 455.5 72 316.8 72zM169.2 416.9C167.9 417.9 168.2 420.2 169.9 422.1C171.5 423.7 173.8 424.4 175.1 423.1C176.4 422.1 176.1 419.8 174.4 417.9C172.8 416.3 170.5 415.6 169.2 416.9zM158.4 408.8C157.7 410.1 158.7 411.7 160.7 412.7C162.3 413.7 164.3 413.4 165 412C165.7 410.7 164.7 409.1 162.7 408.1C160.7 407.5 159.1 407.8 158.4 408.8zM190.8 444.4C189.2 445.7 189.8 448.7 192.1 450.6C194.4 452.9 197.3 453.2 198.6 451.6C199.9 450.3 199.3 447.3 197.3 445.4C195.1 443.1 192.1 442.8 190.8 444.4zM179.4 429.7C177.8 430.7 177.8 433.3 179.4 435.6C181 437.9 183.7 438.9 185 437.9C186.6 436.6 186.6 434 185 431.7C183.6 429.4 181 428.4 179.4 429.7z"/></svg> Contribute</a></li> + <li><a href=""><svg height="20pt" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" /> </svg> About</a></li> <li><a href=""><svg height="20pt" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6"> <path fill-rule="evenodd" d="M3.75 4.5a.75.75 0 0 1 .75-.75h.75c8.284 0 15 6.716 15 15v.75a.75.75 0 0 1-.75.75h-.75a.75.75 0 0 1-.75-.75v-.75C18 11.708 12.292 6 5.25 6H4.5a.75.75 0 0 1-.75-.75V4.5Zm0 6.75a.75.75 0 0 1 .75-.75h.75a8.25 8.25 0 0 1 8.25 8.25v.75a.75.75 0 0 1-.75.75H12a.75.75 0 0 1-.75-.75v-.75a6 6 0 0 0-6-6H4.5a.75.75 0 0 1-.75-.75v-.75Zm0 7.5a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Z" clip-rule="evenodd" /> </svg> RSS Feed</a></li> diff --git a/src/simple_web_app/templates/load_more.html b/src/simple_web_app/templates/load_more.html @@ -0,0 +1 @@ +<button id="load-more-btn" {% if oob %}hx-swap-oob="true"{% endif %} hx-target="#news-cards" hx-swap="beforeend" hx-get="/?page={{ page + 1 }}&current_time={{ current_time }}" class="secondary">Load more</button> diff --git a/src/simple_web_app/templates/news_card.html b/src/simple_web_app/templates/news_card.html @@ -1,9 +1,20 @@ <article> -<header><small><a href="">{{ tag }}</a></small></header> -<details> -<summary> -<strong>{{ title }}</strong> -</summary> -<p><small>{{ content }}</small></p> -</details> + <header> + {% for category in new.categories %} + <small><a href="">{{ category }}</a></small> + {% endfor %} + </header> + <details> + <summary> + <strong>{{ new.title }}</strong> + </summary> + <p><small>{{ new.text }}</small></p> + </details> + <footer> + <small> + <em> + {{ new.time_since_published }} + </em> + </small> + </footer> </article> diff --git a/src/simple_web_app/templates/news_cards.html b/src/simple_web_app/templates/news_cards.html @@ -0,0 +1,3 @@ +{% for new in news %} +{% include 'news_card.html' %} +{% endfor %} diff --git a/src/simple_web_app/templates/oob_swap.html b/src/simple_web_app/templates/oob_swap.html @@ -0,0 +1,2 @@ +{% include 'news_cards.html' %} +{% include 'load_more.html' %} diff --git a/src/simple_web_app/templates/search.html b/src/simple_web_app/templates/search.html @@ -16,13 +16,6 @@ <div class="overflow-auto"> <table class="table"> - <thead> - <tr> - <th>First Name</th> - <th>Last Name</th> - <th>Email</th> - </tr> - </thead> <tbody id="search-results"> </tbody> </table> diff --git a/src/simple_web_app/templates/search_results.html b/src/simple_web_app/templates/search_results.html @@ -1,4 +1,13 @@ -{% for person in people %} -<tr><td>{{ person.firstname }}</td><td>{{ person.lastname }}</td><td>{{ person.email }}</td></tr> +{% for new in news %} +<tr> +<td> + <details> + <summary> + {{ new.title }} + </summary> + <p><small>{{ new.text }}</small></p> + </details> +</td> +</tr> {% endfor %} diff --git a/src/simple_web_app/templates/settings.html b/src/simple_web_app/templates/settings.html @@ -2,44 +2,22 @@ <dialog _="on click if target is me then trigger closeModal" open> <article> - <div class="overflow-auto"> - <table> - <tr> - <th><strong><a href="#">General</a></strong></th> - <th><strong><a href="#">Categories</a></strong></th> - <th><strong><a href="#">Sections</a></strong></th> - <th><strong><a href="#">Content Filter</a></strong></th> - <th><strong><a href="#">Syncing</a></strong></th> - <th><strong><a href="#">Experimental</a></strong></th> - </tr> - </table> + <div class="overflow-auto" style="text-wrap: nowrap;"> + <nav> + <ul> + <li><div hx-get="/settings?tab=general" hx-target="#settings-tab" hx-swap="innerHTML" style="cursor: pointer;">General</div></li> + <li><div hx-get="/settings?tab=categories" hx-target="#settings-tab" hx-swap="innerHTML" style="cursor: pointer;">Categories</div></li> + <li><div hx-get="/settings?tab=sections" hx-target="#settings-tab" hx-swap="innerHTML" style="cursor: pointer;">Sections</div></li> + <li><div hx-get="/settings?tab=content-filter" hx-target="#settings-tab" hx-swap="innerHTML" style="cursor: pointer;">Content Filter</div></li> + <li><div hx-get="/settings?tab=syncing" hx-target="#settings-tab" hx-swap="innerHTML" style="cursor: pointer;">Syncing</div></li> + <li><div hx-get="/settings?tab=experimental" hx-target="#settings-tab" hx-swap="innerHTML" style="cursor: pointer;">Experimental</div></li> + </ul> + </nav> + </div> + <hr> + <div id="settings-tab"> + {% include 'settings_tab.html' %} </div> - - <p> - <h4>Appearance</h4> - - <details class="dropdown"> - <summary>Theme</summary> - <ul> - <li><a>System</a></li> - <li><a>Light</a></li> - <li><a>Dark</a></li> - </ul> - </details> - - <fieldset> - <legend>Font Size</legend> - <details class="dropdown"> - <summary>Dropdown</summary> - <ul> - <li><a href="#">Normal</a></li> - <li><a href="#">Small</a></li> - <li><a href="#">Large</a></li> - </ul> - </details> - </fieldset> - - </p> <footer> <button _="on click trigger closeModal" class="secondary"> Cancel diff --git a/src/simple_web_app/templates/settings_tab.html b/src/simple_web_app/templates/settings_tab.html @@ -0,0 +1,139 @@ +<section> +{% if tab == "general" %} +<h4>Appearance</h4> + +<label> +Theme +<details class="dropdown"> + <summary>System</summary> + <ul> + <li><a>System</a></li> + <li><a>Light</a></li> + <li><a>Dark</a></li> + </ul> +</details> +</label> + +<label> +Font Size +<details class="dropdown"> + <summary>Normal</summary> + <ul> + <li><a href="#">Small</a></li> + <li><a href="#">Normal</a></li> + <li><a href="#">Large</a></li> + </ul> +</details> +</label> + +<h4>Language & Region</h4> + +<label> +Interface Language +<details class="dropdown"> + <summary>English</summary> + <ul> + <li>English</li> + <li>Spanish</li> + <li>Danish</li> + <li>Chinese</li> + </ul> +</details> +</label> + +<label> +Content Language +<details class="dropdown"> + <summary>English</summary> + <ul> + <li>English</li> + <li>Spanish</li> + <li>Danish</li> + <li>Chinese</li> + </ul> +</details> +</label> + +<h4>Reading Experience</h4> + +<label> +Number of Stories Shown: <output>10</output> +<input type="range" value="10" min="1" max="30" oninput="this.previousElementSibling.value = this.value"/> +</label> + +<label> +Story Open Mode +<details class="dropdown"> + <summary>Multiple stories</summary> + <ul> + <li>Multiple stories</li> + <li>One story at a time</li> + </ul> +</details> +</label> + +<label> +Story Expand Mode +<details class="dropdown"> + <summary>Double-click to expand all</summary> + <ul> + <li>Always expand all</li> + <li>Double-click to expand all</li> + <li>Never expand all</li> + </ul> +</details> +</label> + +<h4>Navigation</h4> + +<label> + Use Latest URLs +<details class="dropdown"> + <summary>Disabled</summary> + <ul> + <li>Disabled</li> + <li>Enabled</li> + </ul> +</details> +</label> + +<h4>About</h4> + +<label> + Maps Provider +<details class="dropdown"> + <summary>Auto</summary> + <ul> + <li>Auto</li> + <li>Kagi Maps</li> + <li>Google Maps</li> + <li>OpenStreetMap</li> + <li>Apple Maps</li> + </ul> +</details> +</label> +{% elif tab == "sections" %} +<nav> +<ul><li> +<hgroup> +<h4>Article Sections</h4> +<h6>Drag to reorder sections. Use the switch to enable/disable.</small> +</hgroup> +</li></ul> +<ul><li><button>Toggle All</button></li></ul> +</nav> +<article><nav><ul><li>Summary</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Primary Image</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Sources</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Highlights</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Quotes</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Secondary Image</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Perspectives</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Historical Background</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Humanitarian Impact</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Technical Details</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Business Angle</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Scientific Significance</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +<article><nav><ul><li>Travel Advisory</li></ul><ul><li><label><input name="summary" type="checkbox" role="switch" /></label></li></ul></nav></article> +{% endif %} +</section>