6 Commits

Author SHA1 Message Date
91e2c9d123 ci(version): 0.2.3 2026-06-10 11:49:56 +03:00
91d9c37612 build(vscode): add settings 2026-06-10 11:49:48 +03:00
b5f2c272bb feat(easel): update navbar 2026-06-10 11:37:49 +03:00
eec72c77ab docs: add screenshot 2026-05-07 01:11:42 +03:00
160ec2b48b ci(version): 0.2.2 2026-04-24 14:12:39 +03:00
7c57f939c0 feat(static): update and optimizate 2026-04-24 14:12:14 +03:00
33 changed files with 375 additions and 101 deletions

9
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"python-envs.pythonProjects": [
{
"path": ".",
"envManager": "ms-python.python:poetry",
"packageManager": "ms-python.python:poetry"
}
]
}

View File

@@ -1,11 +1,15 @@
FROM python:3.12 AS builder
ENV POETRY_HOME="/opt/poetry"
ENV PATH="$POETRY_HOME/bin:$PATH"
RUN apt update && \
apt install -y gettext
WORKDIR /app
RUN curl -sSL https://install.python-poetry.org | python3 -
COPY pyproject.toml poetry.lock README.md ./
RUN poetry config virtualenvs.in-project true
RUN poetry install --with app --no-root
COPY locales ./locales
RUN cd locales/ru/LC_MESSAGES && msgfmt messages.po
FROM node:24 AS node-builder
ENV PATH=/app/node_modules/.bin:$PATH
@@ -18,16 +22,12 @@ RUN npm run build
FROM python:3.12-slim
ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app
RUN apt update && \
apt install -y locales gettext && \
sed -i -e 's/# ru_RU.UTF-8 UTF-8/ru_RU.UTF-8 UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales
ENV LANG=ru_RU.UTF-8
ENV LC_ALL=ru_RU.UTF-8
ENV TZ="Europe/Moscow"
COPY --from=builder /app ./
COPY --from=node-builder /app/dist ./static/dist
COPY gallery gallery/
RUN cd gallery/easel/route/view/locales/ru/LC_MESSAGES && msgfmt messages.po
#COPY --from=builder /app/gallery/easel/route/view/locales /app/gallery/easel/route/view/locales
COPY --from=builder --parents locales/**/*.mo ./
CMD ["uvicorn", "gallery.main:app", "--host", "0.0.0.0", "--port", "80", "--log-config", "gallery/logging.yaml"]

View File

@@ -1,5 +1,9 @@
# API Gallery
Weather and TV program API
![API Gallery](docs/screenshot.png "API Gallery")
## View
https://api.shmyga.ru

BIN
docs/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

View File

@@ -1,5 +1,3 @@
import locale as _locale
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
@@ -9,11 +7,8 @@ from gallery.util import root_path
from .route import api, doc
from .route.view import router as view_router
DEFAULT_LOCALE = "ru_RU.UTF-8"
def build_app(api_bundle: ApiBundle, *, locale: str = DEFAULT_LOCALE) -> FastAPI:
_locale.setlocale(_locale.LC_TIME, locale)
def build_app(api_bundle: ApiBundle) -> FastAPI:
app = FastAPI(
title="Gallery",
docs_url=None,

View File

@@ -21,10 +21,12 @@
<body>
<div class="app col-lg-8 mx-auto p-3 py-md-5">
<header class="d-flex align-items-center pb-3 mb-5 border-bottom">
<app-link href="/"
icon="gear">API Gallery</app-link>
{% block header %}{% endblock %}
<header class="app-header pb-3 mb-5 border-bottom">
<div class="link-list">
<app-link href="/"
icon="gear">API Gallery</app-link>
{% block header %}{% endblock %}
</div>
<ul class="navbar-nav flex-row flex-wrap ms-md-auto">
<li class="nav-item dropdown">
<button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center"
@@ -32,10 +34,10 @@
type="button"
aria-expanded="false"
data-bs-toggle="dropdown"
aria-label="Select language (default)">
<span class="me-2 language-icon-active">🇬🇧</span>
aria-label="{{_('Select language')}} (default)">
<span class="fi fir fi-gb me-2 language-icon-active"></span>
<span class="d-lg-none ms-2"
id="bd-language-text">Select language</span>
id="bd-language-text">{{_("Select language")}}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end"
aria-labelledby="bd-language-text">
@@ -44,8 +46,8 @@
class="dropdown-item d-flex align-items-center"
data-bs-language-value="en"
aria-pressed="false">
<span class="me-2 language-icon">🇬🇧</span>
English
<span class="fi fir fi-gb me-2 language-icon-active"></span>
{{_("English")}}
</button>
</li>
<li>
@@ -53,8 +55,8 @@
class="dropdown-item d-flex align-items-center"
data-bs-language-value="ru"
aria-pressed="false">
<span class="me-2 language-icon">🇷🇺</span>
Russian
<span class="fi fir fi-ru me-2 language-icon-active"></span>
{{_("Russian")}}
</button>
</li>
</ul>
@@ -66,9 +68,9 @@
aria-expanded="false"
data-bs-toggle="dropdown"
aria-label="Toggle theme (auto)">
<i class="bi me-2 opacity-50 theme-icon-active"></i>
<span class="bi bi-circle-half me-2 opacity-50 theme-icon-active"></span>
<span class="d-lg-none ms-2"
id="bd-theme-text">Toggle theme</span>
id="bd-theme-text">{{_("Toggle theme")}}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end"
aria-labelledby="bd-theme-text">
@@ -77,8 +79,8 @@
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="light"
aria-pressed="false">
<i class="bi bi-sun-fill me-2 opacity-50 theme-icon"></i>
Light
<span class="bi bi-sun-fill me-2 opacity-50 theme-icon"></span>
{{_("Light")}}
</button>
</li>
<li>
@@ -86,8 +88,8 @@
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="dark"
aria-pressed="false">
<i class="bi bi-moon-stars-fill me-2 opacity-50 theme-icon"></i>
Dark
<span class="bi bi-moon-stars-fill me-2 opacity-50 theme-icon"></span>
{{_("Dark")}}
</button>
</li>
<li>
@@ -95,8 +97,8 @@
class="dropdown-item d-flex align-items-center active"
data-bs-theme-value="auto"
aria-pressed="true">
<i class="bi bi-circle-half me-2 opacity-50 theme-icon"></i>
Auto
<span class="bi bi-circle-half me-2 opacity-50 theme-icon"></span>
{{_("Auto")}}
</button>
</li>
</ul>

View File

@@ -1,11 +1,11 @@
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block title %}{{_("Index")}}{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block content %}
<h1>View</h1>
<h1>{{_("View")}}</h1>
<div class="list-group mb-5">
{% for section in sections %}
<a href="{{section.link}}"
@@ -18,7 +18,7 @@
{% endfor %}
</div>
<hr class="col-3 col-md-2 mb-5">
<h1>Docs</h1>
<h1>{{_("Docs")}}</h1>
<a href="/docs"
target="_blank">
<h4>Swagger</h4>

View File

@@ -1,15 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Gallery\n"
"Last-Translator: shmyga <shmyga.z@gmail.com>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
msgid "Weather"
msgstr "Погода"
msgid "TV program"
msgstr "Телепрограмма"

View File

@@ -1,6 +1,7 @@
import datetime
from pathlib import Path
from babel.dates import format_date
from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
@@ -20,7 +21,13 @@ templates = Jinja2Templates(
base_dir / "templates",
]
)
templates.env.globals.update({"_": _})
templates.env.globals.update(
{
"_": _,
"version": __version__,
"format_date": format_date,
}
)
templates.env.filters["timedelta_format"] = timedelta_format
router = APIRouter()

View File

@@ -1,10 +1,9 @@
{% extends "base.html" %}
{% block title %}
Программа | {{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}
{{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}
{% endblock %}
{% block header %}
<span class="fs-4 text-body ms-2 me-2">/</span>
<app-link href="/schedule"
icon="tv">{{_("TV program")}}</app-link>
{% endblock %}
@@ -15,7 +14,7 @@
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<a class="button"
href="../..">⬆️</a>
<span>{{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
<span>{{response.channel.name}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span>
<a class="button"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
</h4>

View File

@@ -1,10 +1,9 @@
{% extends "base.html" %}
{% block title %}
{{'Прямые трансляции' if live else _("TV program")}} | {{response.date.strftime('%a, %d %B %Y')}}
{{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}
{% endblock %}
{% block header %}
<span class="fs-4 text-body ms-2 me-2">/</span>
<app-link href="/schedule"
icon="tv">{{_("TV program")}}</app-link>
{% endblock %}
@@ -15,7 +14,7 @@
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<a class="button"
href="..">⬆️</a>
<span>{{'Прямые трансляции' if live else 'Программа'}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
<span>{{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span>
<a class="button"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
</h4>
@@ -36,8 +35,6 @@
<td colspan="3">
<div>{{response.channel.name}}</div>
</td>
<td></td>
<td></td>
</tr>
{% for value in values %}
<tr class="{{'table-success' if not live and value.live else ''}}">

View File

@@ -1,9 +1,10 @@
import gettext
from contextvars import ContextVar
from pathlib import Path
from fastapi import Cookie, Header, Request
from gallery.util import root_path
_translation: ContextVar[gettext.GNUTranslations | gettext.NullTranslations] = (
ContextVar("translation")
)
@@ -19,7 +20,7 @@ async def set_language(
try:
t = gettext.translation(
"messages", localedir=Path(__file__).parent / "locales", languages=[lang]
"messages", localedir=root_path / "locales", languages=[lang]
)
except FileNotFoundError:
t = gettext.NullTranslations()

View File

@@ -1,11 +1,10 @@
import datetime
from pathlib import Path
from fastapi import APIRouter, FastAPI
from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from babel.dates import format_date
from gallery.easel.core import AppRequest
from gallery.sketch.weather.model import WeatherResponse
from gallery.version import __version__
@@ -22,7 +21,13 @@ templates = Jinja2Templates(
base_dir / "templates",
]
)
templates.env.globals.update({"_": _})
templates.env.globals.update(
{
"_": _,
"version": __version__,
"format_date": format_date,
}
)
templates.env.filters["wind_direction_icon"] = wind_direction_icon
templates.env.filters["cloudness_icon"] = cloudness_icon
@@ -32,15 +37,16 @@ def build_weather_response(request: AppRequest, response: WeatherResponse):
request=request,
name="weather.html",
context={
"version": __version__,
"tag_util": TagUtil,
"datetime": datetime,
"response": response,
},
)
router = APIRouter()
@router.get("/weather", response_class=HTMLResponse)
async def get_weather_index(request: AppRequest, query: str | None = None):
weather_api = request.app.state.api.weather
@@ -49,7 +55,6 @@ async def get_weather_index(request: AppRequest, query: str | None = None):
request=request,
name="index.html",
context={
"version": __version__,
"locations": locations,
},
)

View File

@@ -1,8 +1,8 @@
{% extends "base.html" %}
{% block title %}Weather{% endblock %}
{% block title %}{{_("Weather")}}{% endblock %}
{% block content %}
<h1>Weather</h1>
<h1>{{_("Weather")}}</h1>
<form action=""
method="get"
class="mb-4">
@@ -11,9 +11,9 @@
class="form-control"
id="query"
name="query"
placeholder="Enter the city name">
placeholder="{{_('Enter the city name')}}">
<button class="btn btn-primary"
type="submit">Search</button>
type="submit">{{_("Search")}}</button>
</div>
</form>
<ul id="locations"
@@ -22,6 +22,7 @@
<a href="weather/{{location.id}}"
class="list-group-item list-group-item-action px-4"
onclick="saveLocation({id:'{{location.id}}', name:'{{location.name}}'});">
<span class="fi fi-{{location.country_code}} me-1"></span>
<span class="text-primary">{{location.name}}</span>
<span class="small ms-1 text-secondary">
{{location.country}}, {{location.district}}, {{location.subdistrict}}

View File

@@ -2,8 +2,8 @@
{% block title %}{{_("Weather")}} | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}{% endblock %}
{% block header %}
<span class="fs-4 text-body ms-2 me-2">/</span>
<app-link href="/weather" icon="brightness-high">{{_("Weather")}}</app-link>
<app-link href="/weather"
icon="brightness-high">{{_("Weather")}}</app-link>
{% endblock %}
{% block content %}
@@ -13,12 +13,12 @@
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<a class="button"
href="../tag/days-10">⬆️</a>
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
<span>{{response.location}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span>
<a class="button"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
{% endif %}
{% if response.period == 'days' %}
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
<span>{{response.location}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span>
{% endif %}
</h4>
<div class="table-responsive">
@@ -36,9 +36,9 @@
{% endif %}
{% if response.period == 'days' %}
<td class="date {{'now' if value.date.date() == datetime.date.today() else ''}}">
<span class="value">
<span class="value {{'text-danger' if value.date.weekday() in [5,6] else ''}}">
<a href="../tag/{{tag_util.create_tag('day', value.date.date())}}">
{{value.date.strftime('%a %d')}}
{{format_date(value.date, 'E d', locale=request.state.language)}}
</a>
</span>
</td>
@@ -49,7 +49,7 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Облачность
{{_("Cloudiness")}}
</td>
</tr>
<tr>
@@ -65,7 +65,7 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Температура, °C
{{_("Temperature, °C")}}
</td>
</tr>
<tr>
@@ -84,7 +84,7 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Направление ветра
{{_("Wind direction")}}
</td>
</tr>
<tr>
@@ -98,7 +98,7 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Скорость ветра, м/с
{{_("Wind speed, m/s")}}
</td>
</tr>
<tr>
@@ -118,7 +118,7 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Осадки, мм
{{_("Precipitation, mm")}}
</td>
</tr>
<tr>
@@ -133,7 +133,7 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Давление, мм рт. ст.
{{_("Pressure, mmHg")}}
</td>
</tr>
<tr>
@@ -151,7 +151,7 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Влажность, %
{{_("Humidity, %")}}
</td>
</tr>
<tr>

View File

@@ -87,6 +87,7 @@ class GismeteoApi(WeatherApi):
lat=item["coordinates"]["latitude"],
lon=item["coordinates"]["longitude"],
country=item["translations"]["kk"]["country"]["name"],
country_code=item["country"]["code"].lower(),
district=item["translations"]["kk"]["district"]["name"],
subdistrict=(
item["translations"]["kk"]["subdistrict"]["name"]

View File

@@ -15,6 +15,7 @@ class Location(Model):
lat: float
lon: float
country: str
country_code: str
district: str
subdistrict: str

View File

@@ -1 +1 @@
__version__ = "0.2.1"
__version__ = "0.2.2"

View File

@@ -0,0 +1,77 @@
msgid ""
msgstr ""
"Project-Id-Version: Gallery\n"
"Last-Translator: shmyga <shmyga.z@gmail.com>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
msgid "Index"
msgstr "Содержание"
msgid "View"
msgstr "Просмотр"
msgid "Docs"
msgstr "Документация"
msgid "Toggle theme"
msgstr "Переключить тему"
msgid "Light"
msgstr "Светлая"
msgid "Dark"
msgstr "Тёмная"
msgid "Auto"
msgstr "Авто"
msgid "Select language"
msgstr "Выберите язык"
msgid "English"
msgstr "Английский"
msgid "Russian"
msgstr "Русский"
# weather
msgid "Weather"
msgstr "Погода"
msgid "Enter the city name"
msgstr "Введите название города"
msgid "Search"
msgstr "Поиск"
msgid "Cloudiness"
msgstr "Облачность"
msgid "Temperature, °C"
msgstr "Температура, °C"
msgid "Wind direction"
msgstr "Направление ветра"
msgid "Wind speed, m/s"
msgstr "Скорость ветра, м/с"
msgid "Precipitation, mm"
msgstr "Осадки, мм"
msgid "Pressure, mmHg"
msgstr "Давление, мм рт. ст."
msgid "Humidity, %"
msgstr "Влажность, %"
# tv
msgid "TV program"
msgstr "Телепрограмма"
msgid "Live broadcasts"
msgstr "Прямые трансляции"

17
poetry.lock generated
View File

@@ -196,6 +196,21 @@ tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-mypy = ["mypy (>=1.6) ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\""]
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
[[package]]
name = "babel"
version = "2.18.0"
description = "Internationalization utilities"
optional = false
python-versions = ">=3.8"
groups = ["app"]
files = [
{file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"},
{file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"},
]
[package.extras]
dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""]
[[package]]
name = "beautifulsoup4"
version = "4.12.3"
@@ -1898,4 +1913,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "7295e9ec7f7492017c5bbda489026f19bbf155f0ea82402d348b0aa4c03beaca"
content-hash = "39107ded0683832d1ca3d637dc50fac7dd441375ead30cdb56d64edee13c97e2"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "gallery"
version = "0.2.1"
version = "0.2.3"
description = ""
authors = ["shmyga <shmyga.z@gmail.com>"]
readme = "README.md"
@@ -17,6 +17,7 @@ aiocache = {extras = ["redis"], version = "^0.12.2"}
[tool.poetry.group.app.dependencies]
fastapi = "^0.111.1"
jinja2 = "^3.1.4"
babel = "^2.18.0"
[tool.poetry.group.test.dependencies]
pytest = "^8.3.1"

View File

@@ -2,5 +2,5 @@
set -e
cd "$(dirname $(dirname "$0"))" || exit
cd gallery/easel/route/view/locales/ru/LC_MESSAGES || exit
cd locales/ru/LC_MESSAGES || exit
msgfmt messages.po

View File

@@ -1,17 +1,18 @@
{
"name": "gallery-static",
"version": "0.2.1",
"name": "gallery",
"version": "0.2.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gallery-static",
"version": "0.2.1",
"name": "gallery",
"version": "0.2.3",
"license": "ISC",
"dependencies": {
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"flag-icons": "^7.5.0",
"sass": "^1.99.0"
},
"devDependencies": {
@@ -699,6 +700,12 @@
}
}
},
"node_modules/flag-icons": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.5.0.tgz",
"integrity": "sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==",
"license": "MIT"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "gallery",
"version": "0.2.1",
"version": "0.2.3",
"scripts": {
"build": "vite build",
"dev": "vite build --watch"
@@ -12,6 +12,7 @@
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"flag-icons": "^7.5.0",
"sass": "^1.99.0"
},
"devDependencies": {

View File

@@ -7,7 +7,7 @@ class AppLinkElement extends HTMLElement {
<a href="${this.getAttribute("href")}"
class="d-flex align-items-center text-body text-decoration-none">
<span class="fs-4">
<i class="bi bi-${this.getAttribute("icon")}"></i>
<span class="bi bi-${this.getAttribute("icon")} me-1"></span>
<span>${this.textContent}</span>
</span>
</a>`;

View File

@@ -28,7 +28,7 @@
const languageSwitcherText = document.querySelector("#bd-language-text");
const activeLanguageIcon = document.querySelector(".language-icon-active");
const btnToActive = document.querySelector(`[data-bs-language-value="${language}"]`);
const activeLanguageIconContent = btnToActive?.querySelector("span")?.textContent;
const activeLanguageIconClass = btnToActive.querySelector(".fi").className.match(/fi-[\w-]+/)[0];
document.querySelectorAll("[data-bs-language-value]").forEach((element) => {
element.classList.remove("active");
@@ -37,7 +37,9 @@
btnToActive.classList.add("active");
btnToActive.setAttribute("aria-pressed", "true");
activeLanguageIcon.textContent = activeLanguageIconContent;
const classesToRemove = Array.from(activeLanguageIcon.classList).filter((className) => className.startsWith("fi-"));
activeLanguageIcon.classList.remove(...classesToRemove);
activeLanguageIcon.classList.add(activeLanguageIconClass);
const languageSwitcherLabel = `${languageSwitcherText.textContent} (${btnToActive.dataset.bsLanguageValue})`;
languageSwitcher.setAttribute("aria-label", languageSwitcherLabel);

40
static/src/lib/bootstrap-icons.scss vendored Normal file
View File

@@ -0,0 +1,40 @@
//$bootstrap-icons: "circle-half", "moon-stars-fill", "brightness-high", "gear", "sun-fill", "tv";
.bi {
display: inline-block;
text-transform: none;
line-height: 1;
vertical-align: -0.125em;
width: 1em;
height: 1em;
mask-size: contain;
mask-position: 50%;
mask-repeat: no-repeat;
background-color: currentColor;
//@each $icon in $bootstrap-icons {
// &.bi-#{$icon} {
// mask-image: url("bootstrap-icons/icons/#{$icon}.svg");
// }
///
&.bi-circle-half {
mask-image: url(bootstrap-icons/icons/circle-half.svg);
}
&.bi-moon-stars-fill {
mask-image: url(bootstrap-icons/icons/moon-stars-fill.svg);
}
&.bi-brightness-high {
mask-image: url(bootstrap-icons/icons/brightness-high.svg);
}
&.bi-gear {
mask-image: url(bootstrap-icons/icons/gear.svg);
}
&.bi-sun-fill {
mask-image: url(bootstrap-icons/icons/sun-fill.svg);
}
&.bi-tv {
mask-image: url(bootstrap-icons/icons/tv.svg);
}
}

52
static/src/lib/bootstrap.scss vendored Normal file
View File

@@ -0,0 +1,52 @@
@import "bootstrap/scss/mixins/banner";
@include bsBanner("");
// scss-docs-start import-stack
// Configuration
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";
// Layout & components
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
//@import "bootstrap/scss/images";
//@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/tables";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/buttons";
//@import "bootstrap/scss/transitions";
@import "bootstrap/scss/dropdown";
//@import "bootstrap/scss/button-group";
@import "bootstrap/scss/nav";
@import "bootstrap/scss/navbar";
//@import "bootstrap/scss/card";
//@import "bootstrap/scss/accordion";
//@import "bootstrap/scss/breadcrumb";
//@import "bootstrap/scss/pagination";
//@import "bootstrap/scss/badge";
//@import "bootstrap/scss/alert";
//@import "bootstrap/scss/progress";
@import "bootstrap/scss/list-group";
//@import "bootstrap/scss/close";
//@import "bootstrap/scss/toasts";
//@import "bootstrap/scss/modal";
//@import "bootstrap/scss/tooltip";
//@import "bootstrap/scss/popover";
//@import "bootstrap/scss/carousel";
//@import "bootstrap/scss/spinners";
//@import "bootstrap/scss/offcanvas";
//@import "bootstrap/scss/placeholders";
// Helpers
@import "bootstrap/scss/helpers";
// Utilities
@import "bootstrap/scss/utilities/api";
// scss-docs-end import-stack

View File

@@ -0,0 +1,15 @@
@use "flag-icons/sass/flag-icons" with (
$flag-icons-path: "flag-icons/flags",
$flag-icons-included-countries: (
"gb",
"ru",
"by",
"ua",
"kz",
)
);
.fir {
@extend .fis;
border-radius: 50%;
}

View File

@@ -1,6 +1,6 @@
@import "bootstrap/scss/bootstrap";
$bootstrap-icons-font-dir: "bootstrap-icons/font/fonts";
@import "bootstrap-icons/font/bootstrap-icons";
@import "./lib/flag-icons";
@import "./lib/bootstrap";
@import "./lib/bootstrap-icons";
@import "./widget.scss";
@import "./weather.scss";
@@ -17,3 +17,26 @@ $bootstrap-icons-font-dir: "bootstrap-icons/font/fonts";
height: 2rem;
background-size: contain;
}
.app-header {
display: flex;
flex-direction: row;
> .link-list {
display: flex;
flex-direction: row;
gap: 1rem;
app-link {
display: flex;
align-items: center;
&:not(:last-child)::after {
margin-left: 1rem;
content: "|";
}
}
}
}
@import "./mobile";

34
static/src/mobile.scss Normal file
View File

@@ -0,0 +1,34 @@
@import "./lib/bootstrap";
@include media-breakpoint-down(md) {
.app-header {
flex-direction: column;
> .link-list {
border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
padding-bottom: map-get($spacers, 1);
margin-bottom: map-get($spacers, 1);
flex-direction: column;
gap: 0;
app-link {
display: flex;
align-items: center;
&:not(:last-child)::after {
content: "";
}
}
}
}
.navbar-nav {
flex-direction: column !important;
> .nav-item {
.btn {
padding: 0.125rem !important;
}
}
}
}

View File

@@ -34,7 +34,7 @@
const themeSwitcherText = document.querySelector("#bd-theme-text");
const activeThemeIcon = document.querySelector(".theme-icon-active");
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
const activeThemeIconClass = btnToActive.querySelector("i.bi").className.match(/bi-[\w-]+/)[0];
const activeThemeIconClass = btnToActive.querySelector(".bi").className.match(/bi-[\w-]+/)[0];
document.querySelectorAll("[data-bs-theme-value]").forEach((element) => {
element.classList.remove("active");

View File

@@ -31,7 +31,7 @@
}
.temperature {
padding: 0;
padding: 0 !important;
}
.temperature .value {