From d82fc4ea464bebad4305a17de539afb3a7935ec8 Mon Sep 17 00:00:00 2001 From: shmyga Date: Wed, 24 Jun 2026 15:45:15 +0300 Subject: [PATCH] feat(weather): improve weather locations list --- gallery/easel/route/view/weather/__init__.py | 10 +- .../route/view/weather/templates/index.html | 94 +++++--------- .../route/view/weather/templates/weather.html | 2 +- gallery/painting/gismeteo/api.py | 1 + gallery/painting/openweather/api.py | 1 + gallery/sketch/weather/model.py | 52 ++++---- static/src/index.ts | 1 + static/src/lib/bootstrap.scss | 2 +- static/src/main.scss | 2 +- static/src/{ => weather}/weather.scss | 0 static/src/weather/weather.ts | 116 ++++++++++++++++++ 11 files changed, 180 insertions(+), 101 deletions(-) rename static/src/{ => weather}/weather.scss (100%) create mode 100644 static/src/weather/weather.ts diff --git a/gallery/easel/route/view/weather/__init__.py b/gallery/easel/route/view/weather/__init__.py index 3c4b6d9..eaebf8f 100644 --- a/gallery/easel/route/view/weather/__init__.py +++ b/gallery/easel/route/view/weather/__init__.py @@ -50,13 +50,7 @@ def build_weather_response(request: AppRequest, response: WeatherResponse): router = APIRouter(prefix="/weather") -@router.get("/", response_class=RedirectResponse) -async def get_weather(request: AppRequest): - default_provider = request.app.state.api.get_api_providers(WeatherApi.TYPE)[0] - return RedirectResponse(default_provider) - - -@router.get("/{provider}", response_class=HTMLResponse) +@router.get("/", response_class=HTMLResponse) async def get_weather_index(request: AppRequest, weather_api: WeatherApiDepends, query: str | None = None): locations = (await weather_api.find_locations(query)) if query else [] return templates.TemplateResponse( @@ -69,7 +63,7 @@ async def get_weather_index(request: AppRequest, weather_api: WeatherApiDepends, @router.get("/{provider}/{location}", response_class=RedirectResponse) -async def get_weather_default(location: str): +async def get_weather(location: str): return RedirectResponse(f"{location}/tag/today") diff --git a/gallery/easel/route/view/weather/templates/index.html b/gallery/easel/route/view/weather/templates/index.html index 78d8044..21ea15a 100644 --- a/gallery/easel/route/view/weather/templates/index.html +++ b/gallery/easel/route/view/weather/templates/index.html @@ -1,23 +1,6 @@ {% extends "base.html" %} {% block title %}{{_("Weather")}}{% endblock %} -{# {% block header %} - -{% endblock %} #} - {% block content %}

{{_("Weather")}}

+ + +
+{% if locations %} +
+{% endif %} + {% endblock %} \ No newline at end of file diff --git a/gallery/easel/route/view/weather/templates/weather.html b/gallery/easel/route/view/weather/templates/weather.html index 3922e0a..b8d7a02 100644 --- a/gallery/easel/route/view/weather/templates/weather.html +++ b/gallery/easel/route/view/weather/templates/weather.html @@ -137,7 +137,7 @@ {% for value in response.values %} - {{value.precipitation or ' '}} + {{(value.precipitation | round(2)) if value.precipitation else ' '}} {% endfor %} diff --git a/gallery/painting/gismeteo/api.py b/gallery/painting/gismeteo/api.py index f553b6e..8e042c5 100644 --- a/gallery/painting/gismeteo/api.py +++ b/gallery/painting/gismeteo/api.py @@ -85,6 +85,7 @@ class GismeteoApi(WeatherApi): Location( id=f"{item['slug']}-{item['id']}", name=item["translations"]["kk"]["city"]["name"], + provider=self.provider, lat=item["coordinates"]["latitude"], lon=item["coordinates"]["longitude"], country=item["translations"]["kk"]["country"]["name"], diff --git a/gallery/painting/openweather/api.py b/gallery/painting/openweather/api.py index 6c66095..48c9f39 100644 --- a/gallery/painting/openweather/api.py +++ b/gallery/painting/openweather/api.py @@ -38,6 +38,7 @@ class OpenWeatherApi(WeatherApi): Location( id=f"{item.lat}:{item.lon}", name=item.name, + provider=self.provider, lat=item.lat, lon=item.lon, country=item.country, diff --git a/gallery/sketch/weather/model.py b/gallery/sketch/weather/model.py index 86c99da..f13ae37 100644 --- a/gallery/sketch/weather/model.py +++ b/gallery/sketch/weather/model.py @@ -1,5 +1,6 @@ import datetime -from enum import Enum +from enum import StrEnum, auto +from typing import Self from pydantic import BaseModel @@ -12,6 +13,7 @@ class Model(BaseModel): class Location(Model): id: str name: str + provider: str lat: float lon: float country: str @@ -20,21 +22,21 @@ class Location(Model): subdistrict: str -class Cloudness(str, Enum): - CLEAR = "clear" - PARTLY_CLOUDY = "party_cloudy" - CLOUDY = "cloudy" - MAINLY_CLOUDY = "mainly_cloudy" +class Cloudness(StrEnum): + CLEAR = auto() + PARTLY_CLOUDY = auto() + CLOUDY = auto() + MAINLY_CLOUDY = auto() -class Precipitation(str, Enum): - NO = "no" - SMALL_RAIN = "small_rain" - RAIN = "rain" - HEAVY_RAIN = "heavy_rain" - SHOWER = "shower" - SNOW = "snow" - HEAVY_SNOW = "heavy_snow" +class Precipitation(StrEnum): + NO = auto() + SMALL_RAIN = auto() + RAIN = auto() + HEAVY_RAIN = auto() + SHOWER = auto() + SNOW = auto() + HEAVY_SNOW = auto() class Sky(Model): @@ -44,16 +46,16 @@ class Sky(Model): fog: bool -class WindDirection(str, Enum): - CALM = "calm" - N = "N" - NE = "NE" - E = "E" - SE = "SE" - S = "S" - SW = "SW" - W = "W" - NW = "NW" +class WindDirection(StrEnum): + CALM = auto() + N = auto() + NE = auto() + E = auto() + SE = auto() + S = auto() + SW = auto() + W = auto() + NW = auto() class WindDirectionDeg(float): @@ -89,7 +91,7 @@ class WindDirectionDeg(float): raise ValueError(self) @classmethod - def from_direction(cls, direction: WindDirection) -> "WindDirectionDeg": + def from_direction(cls, direction: WindDirection) -> Self: return cls( { WindDirection.CALM: -1, diff --git a/static/src/index.ts b/static/src/index.ts index cd01b14..f0b971c 100644 --- a/static/src/index.ts +++ b/static/src/index.ts @@ -3,6 +3,7 @@ import "./components"; import "./language"; import "./main.scss"; import "./theme"; +import "./weather/weather"; document.addEventListener("DOMContentLoaded", (event) => { const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); diff --git a/static/src/lib/bootstrap.scss b/static/src/lib/bootstrap.scss index 406e2a8..c3010c3 100644 --- a/static/src/lib/bootstrap.scss +++ b/static/src/lib/bootstrap.scss @@ -30,7 +30,7 @@ //@import "bootstrap/scss/accordion"; //@import "bootstrap/scss/breadcrumb"; //@import "bootstrap/scss/pagination"; -//@import "bootstrap/scss/badge"; +@import "bootstrap/scss/badge"; //@import "bootstrap/scss/alert"; //@import "bootstrap/scss/progress"; @import "bootstrap/scss/list-group"; diff --git a/static/src/main.scss b/static/src/main.scss index 51d728e..6a1a80e 100644 --- a/static/src/main.scss +++ b/static/src/main.scss @@ -4,7 +4,7 @@ @import "./lib/weather-icons/weather-icons"; @import "./widget.scss"; -@import "./weather.scss"; +@import "./weather/weather.scss"; .table.table-compact { td { diff --git a/static/src/weather.scss b/static/src/weather/weather.scss similarity index 100% rename from static/src/weather.scss rename to static/src/weather/weather.scss diff --git a/static/src/weather/weather.ts b/static/src/weather/weather.ts new file mode 100644 index 0000000..9a0195c --- /dev/null +++ b/static/src/weather/weather.ts @@ -0,0 +1,116 @@ +export interface Location { + id: string; + name: string; + provider: string; + country_code: string; + country?: string; + district?: string; + subdistrict?: string; +} + +export class WeatherLocationStorage { + private storageKey: string = "weather:locations"; + + getAll(): Location[] { + const locations = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}"); + return Object.values(locations); + } + + add(location: Location) { + const locations = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}"); + locations[location.id] = location; + window.localStorage.setItem(this.storageKey, JSON.stringify(locations)); + } + + remove(id: string) { + const locations = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}"); + delete locations[id]; + window.localStorage.setItem(this.storageKey, JSON.stringify(locations)); + } +} + +export class WeatherLocationManager { + loadLocations(selector: string) { + const locations = window.weatherLocationStorage.getAll(); + const container = document.querySelector(selector); + if (container) { + container.innerHTML = ""; + for (const location of locations) { + const element = new WeatherLocationElement(); + element.setAttribute("location", JSON.stringify(location)); + element.setAttribute("removable", ""); + container.appendChild(element); + } + } + } +} + +class WeatherLocationElement extends HTMLElement { + static observedAttributes = ["location", "removable"]; + + location: Location | null = null; + + constructor() { + super(); + this.handleClick = this.handleClick.bind(this); + this.handleRemoveClick = this.handleRemoveClick.bind(this); + } + + connectedCallback() { + this.location = JSON.parse(this.getAttribute("location") || "{}"); + this.innerHTML = ` + + + + ${this.location?.name} + + ${this.location?.country}, ${this.location?.district}, ${this.location?.subdistrict} + + + + ${this.location?.provider} + + ✕ + + + `; + this.addEventListener("click", this.handleClick); + if (this.hasAttribute("removable")) { + this.querySelector("[data-remove]")?.addEventListener("click", this.handleRemoveClick); + } else { + this.querySelector("[data-remove]")?.remove(); + } + } + + disconnectedCallback() { + this.removeEventListener("click", this.handleClick); + } + + handleClick(event: MouseEvent) { + if (this.location) { + window.weatherLocationStorage.add(this.location); + } + } + + handleRemoveClick(event: MouseEvent) { + event.preventDefault(); + event.stopPropagation(); + if (this.location) { + window.weatherLocationStorage.remove(this.location.id); + } + this.remove(); + } +} + +customElements.define("weather-location", WeatherLocationElement); + +declare global { + interface Window { + weatherLocationStorage: WeatherLocationStorage; + weatherLocationManager: WeatherLocationManager; + } +} + +window.weatherLocationStorage = new WeatherLocationStorage(); +window.weatherLocationManager = new WeatherLocationManager();