Skip to content
Merged

Htmx #40

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ git reset --hard origin/$BRANCH

# Optionally run build or install commands here if needed
# e.g., npm install, yarn install for Node.js projects
npm install
npx vite build

/home/loki/.local/bin/uv sync || exit
/home/loki/.local/bin/uv run flask db upgrade || exit

Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@ test-coverage:

check: lint test

css-build:
npm install
npx vite build

run:
uv run flask db upgrade
npx vite build # Build CSS before running the app
uv run flask run --host="0.0.0.0" --debug

docker-build:
Expand Down
58 changes: 27 additions & 31 deletions blog/post/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import datetime
from typing import cast, ParamSpec, TypeVar
from functools import wraps

from collections.abc import Callable
import markdown
from flask import (
Blueprint,
Expand All @@ -13,6 +11,7 @@
render_template,
request,
url_for,
abort,
)


Expand All @@ -26,48 +25,47 @@
R = TypeVar("R")


def pages_gen(
f: Callable[P, R],
) -> Callable[P, R]:
@wraps(f)
def decorated_function(*args: P.args, **kwargs: P.kwargs) -> R:
page_category = cast("list[int]", current_app.config["PAGE_CATEGORY"])

post_service = ServiceFactory.create_post_service()
pages = post_service.get_page_posts(page_category)
@post.route("/")
@cache.cached(timeout=50) # pyright: ignore[reportUntypedFunctionDecorator]
def index(**kwargs: str) -> Response | str:
return render_template("index.html", **kwargs)

icon_service = ServiceFactory.create_icon_service()
icons = icon_service.get_all_icons()
kwargs["icons"] = icons
kwargs["pages"] = pages
return f(*args, **kwargs)

return decorated_function
@post.route("/posts")
@cache.cached(timeout=50) # pyright: ignore[reportUntypedFunctionDecorator]
def posts(**kwargs: str) -> Response | str:
post_service = ServiceFactory.create_post_service()
posts = post_service.get_all_published_content()
return render_template("posts.html", posts=posts, **kwargs)


@post.route("/")
@post.route("/hx/pages")
@cache.cached(timeout=50) # pyright: ignore[reportUntypedFunctionDecorator]
@pages_gen
def index(**kwargs: str) -> Response | str:
def pages_hx() -> Response | str:
page_category = cast("list[int]", current_app.config["PAGE_CATEGORY"])
post_service = ServiceFactory.create_post_service()
posts = post_service.get_published_posts()
return render_template("posts.html", posts=posts, **kwargs)
pages = post_service.get_page_posts(page_category)
return render_template("pages.htmx", pages=pages)


@post.route("/hx/icons")
@cache.cached(timeout=50) # pyright: ignore[reportUntypedFunctionDecorator]
def icons_hx() -> Response | str:
icon_service = ServiceFactory.create_icon_service()
icons = icon_service.get_all_icons()
return render_template("icons/icons.htmx", icons=icons)


@post.route("/<alias>")
@cache.cached(timeout=50) # pyright: ignore[reportUntypedFunctionDecorator]
@pages_gen
def view(alias: str | None = None, **kwargs: str) -> Response | str:
template = "post.htmx" if request.headers.get("HX-Request") else "post.html"
if alias is None:
from flask import abort

abort(404)

post_service = ServiceFactory.create_post_service()
post = post_service.get_post_by_alias(alias)
if not post:
from flask import abort

abort(404)

# For page categories, we need to check if it's a page or a regular post
Expand All @@ -76,8 +74,6 @@ def view(alias: str | None = None, **kwargs: str) -> Response | str:
is_published = post.publishedon is not None

if not (is_published or is_page):
from flask import abort

abort(404)

# Get tags for the post
Expand All @@ -89,8 +85,8 @@ def view(alias: str | None = None, **kwargs: str) -> Response | str:
page_category_obj = category_service.get_category_by_id(post.category_id)

if page_category_obj and page_category_obj.template:
return render_template("post.html", post=post, tags=tags, **kwargs)
return render_template("post.html", post=post, tags=tags, **kwargs)
return render_template(template, post=post, tags=tags, **kwargs)
return render_template(template, post=post, tags=tags, **kwargs)


@flask_sitemap.register_generator
Expand Down
11 changes: 11 additions & 0 deletions blog/repos/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ def get_published_posts(self) -> list[PostDomain]:
posts_orm = list(self.session.scalars(stmt).all())
return [self._to_domain_model(post_orm) for post_orm in posts_orm]

def get_all_published_content(self) -> list[PostDomain]:
"""Get all published content including posts and pages."""
stmt = (
sa.select(PostORM)
.where(PostORM.publishedon.isnot(None))
.where(PostORM.category_id.isnot(1))
.order_by(PostORM.publishedon.desc())
)
posts_orm = list(self.session.scalars(stmt).all())
return [self._to_domain_model(post_orm) for post_orm in posts_orm]

def get_page_posts(self, page_category_ids: list[int]) -> list[PostDomain]:
stmt = sa.select(PostORM).where(PostORM.category_id.in_(page_category_ids))
posts_orm = list(self.session.scalars(stmt).all())
Expand Down
3 changes: 3 additions & 0 deletions blog/services/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def get_all_posts(self) -> list[Post]:
def get_published_posts(self) -> list[Post]:
return self.post_repository.get_published_posts()

def get_all_published_content(self) -> list[Post]:
return self.post_repository.get_all_published_content()

def get_page_posts(self, page_category_ids: list[int]) -> list[Post]:
return self.post_repository.get_page_posts(page_category_ids)

Expand Down
6 changes: 3 additions & 3 deletions blog/static/src/global/fonts.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(../../fonts/OpenSans.ttf) format('truetype');
src: local('Open Sans'), local('OpenSans'), url(/static/fonts/OpenSans.ttf) format('truetype');
}

@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../../fonts/OpenSans-Bold.ttf) format('truetype');
}
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(/static/fonts/OpenSans-Bold.ttf) format('truetype');
}
2 changes: 0 additions & 2 deletions blog/static/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,4 @@
@import 'components/page.css';
@import 'components/post.css';
@import 'components/nav.css';
@import 'components/works.css';
@import 'components/work.css';
@import 'components/refs.css';
21 changes: 8 additions & 13 deletions blog/tags/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from typing import TYPE_CHECKING, ParamSpecKwargs
from typing import TYPE_CHECKING

from flask import Blueprint, render_template, Response
from flask import Blueprint, render_template, Response, abort, request

from blog.post.views import pages_gen
from blog.services.factory import ServiceFactory

if TYPE_CHECKING:
Expand All @@ -12,29 +11,25 @@


@tags.route("/")
@pages_gen
def index(**kwargs: ParamSpecKwargs) -> Response | str:
def index() -> Response | str:
template = "tags.htmx" if request.headers.get("HX-Request") else "tags.html"
tag_service = ServiceFactory.create_tag_service()
tags = tag_service.get_all_tags()
return render_template("tags.html", tags=tags, **kwargs)
return render_template(template, tags=tags)


@tags.route("/<alias>")
@pages_gen
def view(alias: str | None = None, **kwargs: ParamSpecKwargs) -> Response | str:
def view(alias: str | None = None) -> Response | str:
template = "posts.htmx" if request.headers.get("HX-Request") else "tag.html"
if alias is None:
from flask import abort

abort(404)

tag_service = ServiceFactory.create_tag_service()
tag = tag_service.get_tag_by_alias(alias)
if not tag:
from flask import abort

abort(404)

# Get posts for this tag through the post service
post_service = ServiceFactory.create_post_service()
posts = post_service.get_posts_by_tag(tag.id) if tag.id else []
return render_template("posts.html", posts=posts, tag=tag, **kwargs)
return render_template(template, posts=posts, tag=tag)
5 changes: 5 additions & 0 deletions blog/templates/footer.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<footer class="footer">

<div class="footer__content">
<div hx-swap="innerHTML" hx-get="{{url_for('post.icons_hx')}}"
hx-trigger="load"
hx-target=".footer__links">
</div>

<div class="footer__links">
{% for icon in icons %}
<a href="{{icon.url}}" title="{{icon.title}}" target="_blank" aria-label="{{icon.title}}" class="nav__link">
Expand Down
6 changes: 6 additions & 0 deletions blog/templates/icons/icons.htmx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% for icon in icons %}
<a href="{{icon.url}}" title="{{icon.title}}" target="_blank" aria-label="{{icon.title}}" class="nav__link">
{{icon.content|safe}}
</a>
{% endfor %}

15 changes: 15 additions & 0 deletions blog/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% extends "layout.html" %}

{% block content %}
<div hx-swap="posts" hx-get="/posts"
hx-trigger="load"
hx-target=".page__content">
</div>
<article class="page__content">
{% if tag %}
<h3 class="page__title">Посты c тэгом: {{tag.title}}</h3>
{% endif %}
<div id="posts" class="posts">
</div>
</article>
{% endblock %}
41 changes: 20 additions & 21 deletions blog/templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,36 @@
<title>{% if post %}{{post.pagetitle}}{% else %} Неразумный перфекционизм{% endif %}</title>
<meta name="description" content="{% if post %}{{post.pagetitle}}{% endif %}">
{% if config['YANDEX_VERIFICATION'] %}
<meta name="yandex-verification" content="{{config['YANDEX_VERIFICATION']}}"/>
<meta name="yandex-verification" content="{{ config['YANDEX_VERIFICATION'] }}"/>
{% endif %}
<link href="/static/img/favicon.ico" rel="shortcut icon" />
<base href="{{config['BASE']}}">
<link rel="stylesheet" href="/static/src/global/normalize.css">
<link rel="stylesheet" href="/static/src/global/fonts.css">
<link rel="stylesheet" href="/static/src/global/vars.css">

<link rel="stylesheet" href="/static/src/global/reboot.css">
<link rel="stylesheet" href="/static/src/global/global.css">
<link rel="stylesheet" href="/static/src/components/header.css">
<link rel="stylesheet" href="/static/src/components/minipost.css">
<link rel="stylesheet" href="/static/src/components/postGroup.css">
<link rel="stylesheet" href="/static/src/components/page.css">
<link rel="stylesheet" href="/static/src/components/post.css">
<link rel="stylesheet" href="/static/src/components/nav.css">
<link rel="stylesheet" href="/static/src/components/tags.css">
<link rel="stylesheet" href="/static/src/components/refs.css">
<link rel="stylesheet" href="/static/src/components/footer.css">
<link rel="stylesheet" href="/static/dist/css/bundle.css">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
{% block styles %}
{% endblock %}
</head>
<body class="page {% block body_classes %} {% endblock %}">
<div class="header">
<a href="/" class="header__logo" title="На главную">@gunlinux</a>
<a href="/"
hx-swap="posts" hx-get="{{url_for('post.posts')}}"
hx-trigger="click"
hx-target=".page__content"
class="header__logo" title="На главную">@gunlinux</a>
<div class="header__nav">
<div hx-swap="innerHTML" hx-get="{{url_for('post.pages_hx')}}"
hx-trigger="load"
hx-target=".pages_nav">
</div>
<nav class="nav">
<a href="{{url_for('tags.index')}}" class="nav__link">tags</a>
{% for page in pages %}
<a href="{{url_for('post.view',alias=page.alias)}}" class="nav__link">{{page.pagetitle}}</a>
{% endfor %}
<a
hx-get="{{url_for('tags.index')}}"
hx-trigger="click"
hx-target=".page__content"
hx-push-url="true"
href="{{url_for('tags.index')}}" class="nav__link">
tags</a>
<span class="pages_nav"></span>
</nav>
</div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions blog/templates/pages.htmx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% for page in pages %}
<a href="{{url_for('post.view',alias=page.alias)}}" class="nav__link"
hx-swap="posts"
hx-get="{{url_for('post.view',alias=page.alias)}}"
hx-trigger="click"
hx-target=".page__content"
hx-push-url="true"
>{{page.pagetitle}}</a>
{% endfor %}
13 changes: 12 additions & 1 deletion blog/templates/post.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
{% endblock %}

{% block content %}
<div hx-swap="posts" hx-get="{{url_for('post.view',alias=post.alias)}}"
hx-trigger="load"
hx-target=".page__content">
</div>

<article class="page__content post">
<h1 class="post__title">{{post.pagetitle}}</h1>
{% if post.category_id != config['PAGE_CATEGORY'] %}
Expand All @@ -23,7 +28,13 @@ <h1 class="post__title">{{post.pagetitle}}</h1>
<div class="post__tags tags">
{% if tags %}
{% for tag in tags %}
<a class="tags__item" href="{{url_for('tags.view', alias=tag.alias)}}">{{tag}}</a>
<a class="tags__item"
href="{{url_for('tags.view', alias=tag.alias)}}"
hx-get="{{url_for('tags.index')}}"
hx-trigger="click"
hx-target=".page__content"
href="{{url_for('tags.index')}}" class="nav__link">
tags</a>
{% endfor %}
{% endif %}
</div>
Expand Down
29 changes: 29 additions & 0 deletions blog/templates/post.htmx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<article class="page__content post">
<h1 class="post__title">{{post.pagetitle}}</h1>
{% if post.category_id != config['PAGE_CATEGORY'] %}
<div class="post__date">
{% if post.publishedon %}
{{post.publishedon.strftime("%B %d, %Y")}}
{% endif %}
</div>
{% endif %}

{% if post.category_id != config['PAGE_CATEGORY'] %}
<div class="post__tags tags">
{% if tags %}
{% for tag in tags %}
<a
hx-get="{{url_for('tags.view',alias=tag.alias)}}"
hx-trigger="click"
hx-target=".page__content"
hx-push-url="true"
class="tags__item" href="{{url_for('tags.view', alias=tag.alias)}}">{{tag}}</a>
{% endfor %}
{% endif %}
</div>
{% endif %}
<div class="post__body">
{{ post.markdown|safe }}
</div>
</article>

Loading