From 51163b167cce01af6101438e5e61145ad798f213 Mon Sep 17 00:00:00 2001 From: Mitsuo Tokumori Date: Sat, 8 Mar 2025 16:03:30 +0900 Subject: Restructure python code to be modular The python code is now a package named app. app/models: db models app/routes: flask blueprints app/static: css, js app/templates: jinja html templates --- .gitignore | 1 + README | 25 +++++------ app.py | 40 ------------------ app/__init__.py | 8 ++++ app/config.def.py | 1 + app/routes/main.py | 43 +++++++++++++++++++ app/static/block_time.js | 49 ++++++++++++++++++++++ app/static/block_weather.js | 14 +++++++ app/static/script.js | 50 ++++++++++++++++++++++ app/static/style.css | 100 ++++++++++++++++++++++++++++++++++++++++++++ app/templates/index.html | 27 ++++++++++++ app/utils/block_weather.py | 66 +++++++++++++++++++++++++++++ block_weather.py | 67 ----------------------------- config.def.py | 1 - requirements.txt | 18 ++++++++ run.py | 5 +++ static/block_time.js | 49 ---------------------- static/block_weather.js | 14 ------- static/script.js | 50 ---------------------- static/style.css | 100 -------------------------------------------- templates/index.html | 27 ------------ 21 files changed, 392 insertions(+), 363 deletions(-) delete mode 100644 app.py create mode 100644 app/__init__.py create mode 100644 app/config.def.py create mode 100644 app/routes/main.py create mode 100644 app/static/block_time.js create mode 100644 app/static/block_weather.js create mode 100644 app/static/script.js create mode 100644 app/static/style.css create mode 100644 app/templates/index.html create mode 100644 app/utils/block_weather.py delete mode 100644 block_weather.py delete mode 100644 config.def.py create mode 100644 requirements.txt create mode 100644 run.py delete mode 100644 static/block_time.js delete mode 100644 static/block_weather.js delete mode 100644 static/script.js delete mode 100644 static/style.css delete mode 100644 templates/index.html diff --git a/.gitignore b/.gitignore index a58cf92..65bce24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .venv/ __pycache__/ tmp/ +instance/ config.py diff --git a/README b/README index 71c0ae3..e4f195b 100644 --- a/README +++ b/README @@ -1,34 +1,29 @@ MASU - minimalist dashboard -what: grid of blocks (clocks, weather, etc) w/ real-time updates - -how: flask + vanilla js, no bloat - -why: simple, tweakable, fast +A web based grid of blocks (e.g., clocks, weather) with "real-time" +updates. Implemented in flask and vanilla js (trying to minimize bloat). +Features: simple, tweakable, fast. setup: git clone python -m venv .venv source .venv/bin/activate - pip install flask requests - flask run + pip install -r requirements.txt + python run.py use: http://localhost:5000 reload/tweak in header -files: - - app.py: server - static/: css, js - templates/: html - hack: - add blocks: app.py + script.js - set intervals: script.js + Each block is defined by: + * html: front-end elements (modify: app/templates/index.html) (for now) + * css: front-end design (modify: app/static/style.css) + * js: front-end code (new file: app/static/block_name.js) + * python: back-end code (app/routes/main.py, app/utils/block_name.py) deploy: diff --git a/app.py b/app.py deleted file mode 100644 index e7a6075..0000000 --- a/app.py +++ /dev/null @@ -1,40 +0,0 @@ -import flask -import requests -import datetime -import zoneinfo -import locale - -import block_weather - -locale.setlocale(locale.LC_ALL, '') -app = flask.Flask(__name__) - -@app.route("/") -def index(): - return flask.render_template("index.html") - -@app.route("/time") -def get_time(): - # TODO: date should be passed as JSON - date = datetime.datetime.now(zoneinfo.ZoneInfo("Asia/Tokyo")) - quarter = (date.month - 1) // 3 + 1 - week = date.isocalendar().week - lines = [ - "Tokyo: " + date.strftime("%x (%a)") + f" (Q{quarter}W{week})", - "Tokyo: " + date.astimezone(zoneinfo.ZoneInfo("Asia/Tokyo")).strftime("%H:%M (%Z) (UTC%:z)"), - "Berlin: " + date.astimezone(zoneinfo.ZoneInfo("Europe/Berlin")).strftime("%H:%M (%Z) (UTC%:z)"), - "Lima: " + date.astimezone(zoneinfo.ZoneInfo("America/Lima")).strftime("%H:%M (%Z) (UTC%:z)"), - "New York: " + date.astimezone(zoneinfo.ZoneInfo("America/New_York")).strftime("%H:%M (%Z) (UTC%:z)"), - "Chicago: " + date.astimezone(zoneinfo.ZoneInfo("America/Chicago")).strftime("%H:%M (%Z) (UTC%:z)"), - "Denver: " + date.astimezone(zoneinfo.ZoneInfo("America/Denver")).strftime("%H:%M (%Z) (UTC%:z)"), - "Los Angeles: " + date.astimezone(zoneinfo.ZoneInfo("America/Los_Angeles")).strftime("%H:%M (%Z) (UTC%:z)"), - "Anchorage: " + date.astimezone(zoneinfo.ZoneInfo("America/Anchorage")).strftime("%H:%M (%Z) (UTC%:z)"), - "Honolulu: " + date.astimezone(zoneinfo.ZoneInfo("Pacific/Honolulu")).strftime("%H:%M (%Z) (UTC%:z)"), - ] - return flask.jsonify({"time": "\n".join(lines)}) - -@app.route("/weather") -def get_weather(): - city = flask.request.args.get("city", "Kofu, JP") - data = block_weather.get_current_weather(city) - return flask.jsonify(data) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..4bf2b5a --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,8 @@ +from flask import Flask +from .routes import main + +def create_app(): + app = Flask(__name__) + app.config.from_pyfile('config.py') + app.register_blueprint(routes.main.bp) + return app diff --git a/app/config.def.py b/app/config.def.py new file mode 100644 index 0000000..c80065d --- /dev/null +++ b/app/config.def.py @@ -0,0 +1 @@ +OPENWEATHER_API_KEY = "" diff --git a/app/routes/main.py b/app/routes/main.py new file mode 100644 index 0000000..842a09b --- /dev/null +++ b/app/routes/main.py @@ -0,0 +1,43 @@ +import flask +import requests +import datetime +import zoneinfo +import locale + +from ..utils import block_weather + +locale.setlocale(locale.LC_ALL, '') +bp = flask.Blueprint('main', __name__) + + +@bp.route("/") +def index(): + return flask.render_template("index.html") + + +@bp.route("/time") +def get_time(): + # TODO: date should be passed as JSON + date = datetime.datetime.now(zoneinfo.ZoneInfo("Asia/Tokyo")) + quarter = (date.month - 1) // 3 + 1 + week = date.isocalendar().week + lines = [ + "Tokyo: " + date.strftime("%x (%a)") + f" (Q{quarter}W{week})", + "Tokyo: " + date.astimezone(zoneinfo.ZoneInfo("Asia/Tokyo")).strftime("%H:%M (%Z) (UTC%:z)"), + "Berlin: " + date.astimezone(zoneinfo.ZoneInfo("Europe/Berlin")).strftime("%H:%M (%Z) (UTC%:z)"), + "Lima: " + date.astimezone(zoneinfo.ZoneInfo("America/Lima")).strftime("%H:%M (%Z) (UTC%:z)"), + "New York: " + date.astimezone(zoneinfo.ZoneInfo("America/New_York")).strftime("%H:%M (%Z) (UTC%:z)"), + "Chicago: " + date.astimezone(zoneinfo.ZoneInfo("America/Chicago")).strftime("%H:%M (%Z) (UTC%:z)"), + "Denver: " + date.astimezone(zoneinfo.ZoneInfo("America/Denver")).strftime("%H:%M (%Z) (UTC%:z)"), + "Los Angeles: " + date.astimezone(zoneinfo.ZoneInfo("America/Los_Angeles")).strftime("%H:%M (%Z) (UTC%:z)"), + "Anchorage: " + date.astimezone(zoneinfo.ZoneInfo("America/Anchorage")).strftime("%H:%M (%Z) (UTC%:z)"), + "Honolulu: " + date.astimezone(zoneinfo.ZoneInfo("Pacific/Honolulu")).strftime("%H:%M (%Z) (UTC%:z)"), + ] + return flask.jsonify({"time": "\n".join(lines)}) + + +@bp.route("/weather") +def get_weather(): + city = flask.request.args.get("city", "Kofu, JP") + data = block_weather.get_current_weather(city) + return flask.jsonify(data) diff --git a/app/static/block_time.js b/app/static/block_time.js new file mode 100644 index 0000000..d6510ae --- /dev/null +++ b/app/static/block_time.js @@ -0,0 +1,49 @@ +function init() { + initTimeVisualizer('timeVisualizer'); +} + +export function updateTime() { + fetch("/time") + .then(res => res.json()) + .then(data => { + document.getElementById("weatherSummary").innerText = data.time; + }); +} + +function initTimeVisualizer(containerId) { + const grid = document.createElement('div'); + grid.className = 'grid'; + document.getElementById(containerId).appendChild(grid); + + function updateTime() { + grid.innerHTML = ''; + const now = new Date(); + const hours = now.getHours(); + const minutes = now.getMinutes(); + + for (let i = 0; i < 24; i++) { + const cell = document.createElement('div'); + cell.className = 'cell'; + + if (i >= 20 || i < 4) cell.classList.add('sleep'); + if (i < hours) cell.classList.add('past'); + + if (i === hours) { + cell.classList.add('current'); + const fillPercentage = (minutes / 60) * 100; + cell.style.setProperty('--fill', `${fillPercentage}%`); + const line = document.createElement('div'); + line.className = 'timeline'; + line.style.left = `calc(${fillPercentage}% - 1px)`; + cell.appendChild(line); + } + + grid.appendChild(cell); + } + } + + updateTime(); + setInterval(updateTime, 60000); // 1min +} + +init() diff --git a/app/static/block_weather.js b/app/static/block_weather.js new file mode 100644 index 0000000..4ae6d2d --- /dev/null +++ b/app/static/block_weather.js @@ -0,0 +1,14 @@ +export function updateWeather() { + const city = document.getElementById("city").value; + fetch(`/weather?city=${encodeURIComponent(city)}`) + .then(res => res.json()) + .then(data => { + if (!data) { + document.getElementById("weather-summary").innerText = `Error, "${city}" city not found`; + return + } + document.getElementById("weather-summary").innerText = data.summary; + document.getElementById("weather-icon").src = data.icon_url; + }); +} + diff --git a/app/static/script.js b/app/static/script.js new file mode 100644 index 0000000..efd6836 --- /dev/null +++ b/app/static/script.js @@ -0,0 +1,50 @@ +import { updateTime } from './block_time.js'; +import { updateWeather } from './block_weather.js'; + +const config = { + polling: true +} + +function init() { + const blocks = { + time: { interval: 30 * 1000, lastUpdate: 0, update: updateTime }, // 30s + weather: { interval: 30 * 60000, lastUpdate: 0, update: updateWeather } // 30min + // Add more: { interval: X, lastUpdate: 0, update: updateFunction } + }; + + initHeaderControls(blocks) + + // Initial load + // Poll every 500ms to check intervals (fast enough for 1s updates, light on CPU) + // maybe the 1s updates should be special case, but for now let's keep it simple + reloadAll(blocks); + setInterval(() => pollUpdates(blocks), 500); +} + +function reloadAll(blocks) { + Object.keys(blocks).forEach(key => { + blocks[key].update(); + blocks[key].lastUpdate = Date.now(); + }); +} + +function pollUpdates(blocks) { + if (!config.polling) return; + const now = Date.now(); + Object.keys(blocks).forEach(key => { + const block = blocks[key]; + if (now - block.lastUpdate >= block.interval) { + block.update(); + block.lastUpdate = now; + } + }); +} + +function initHeaderControls(blocks) { + document.getElementById("reload").addEventListener("click", () => reloadAll(blocks)); + document.getElementById("pause").addEventListener("change", (e) => { + config.polling = e.target.checked; + }); +} + +init() diff --git a/app/static/style.css b/app/static/style.css new file mode 100644 index 0000000..a186f24 --- /dev/null +++ b/app/static/style.css @@ -0,0 +1,100 @@ +/* MASU */ +body { + margin: 0; + padding: 0; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; +} + +header { + width: 100%; + height: 80px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +header button, header label { + margin: 0 10px; +} + +#container { + width: 100%; + /* max-width: 1920px; */ + padding: 20px; + box-sizing: border-box; + display: flex; + flex-wrap: wrap; + gap: 20px; + justify-content: center; +} + +.block { + width: 420px; + height: 420px; + flex-shrink: 0; + + padding: 5px; + border: 1px solid black; +} + +@media (max-width: 440px) { + .grid { + padding: 20px 0; + } +} + +/* block_time */ +#time .block { + justify-content: center; +} + +#timeVisualizer .grid { + display: grid; + grid-template-columns: repeat(8, 42px); + grid-template-rows: repeat(3, 42px); + gap: 2px; + background: #f0f0f0; + padding: 5px; + width: fit-content; + margin: 0 auto; +} + +#timeVisualizer .cell { + width: 42px; + height: 42px; + background: #ffffff; + position: relative; + border: 1px solid #ddd; +} + +#timeVisualizer .sleep { + background: #e6f3ff; +} + +#timeVisualizer .past { + background: #cccccc; +} + +#timeVisualizer .current::after { + content: ''; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: var(--fill); + background: #cccccc; +} + +#timeVisualizer .timeline { + position: absolute; + width: 2px; + height: 100%; + background: #000000; + z-index: 1; +} + +/* block_weather */ diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..332b78b --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,27 @@ + + + + MASU + + + +
+ + + +
+
+ +
+

+
+
+
+ weather icon +

+
+ +
+ + + diff --git a/app/utils/block_weather.py b/app/utils/block_weather.py new file mode 100644 index 0000000..eec4e4a --- /dev/null +++ b/app/utils/block_weather.py @@ -0,0 +1,66 @@ +""" +Handle external requests and pre-process weather data + +Providers: +* OpenWeather + Free Access on OpenWeather allows for some API calls, see + https://openweathermap.org/price +""" + +import flask +import requests +import pandas as pd + +def get_current_weather(city: str) -> dict: + """https://openweathermap.org/current""" + pos = _get_position(city) + if not pos: + return None + url = f"http://api.openweathermap.org/data/2.5/weather?lat={pos['lat']}&lon={pos['lon']}&appid={flask.current_app.config['OPENWEATHER_API_KEY']}&units=metric" + data = requests.get(url).json() + + data['summary'] = _format_current_weather(data) + # On icon URL codes: https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 + data['icon_url'] = f"https://openweathermap.org/img/wn/{data['weather'][0]['icon']}.png" + return data + + +def _format_current_weather(data: dict) -> str: + """final formatting should occur in HTML/JS, not here""" + ts = [data['dt'], data['sys']['sunrise'], data['sys']['sunset']] + ts = [pd.to_datetime(x + data['timezone'], unit='s') for x in ts] + s = f"""{data['name']}, {data['sys']['country']} ({ts[0]}) +{data['weather'][0]['description']} +{data['main']['temp']}°C (feels like {data['main']['feels_like']}°C), humidity: {data['main']['humidity']}%, pressure: {data['main']['grnd_level']} hPa +visibility: {data.get('visibility', 0)/1000:.1f}km +wind: {data['wind']['speed']}m/s from {data['wind']['deg']}°N +clouds: {data['clouds']['all']}% +sunrise & sunset: {ts[1].strftime('%H:%M')}, {ts[2].strftime('%H:%M')}""" + return s + + +def get_forecast(city: str) -> dict: + """https://openweathermap.org/forecast5""" + # 5 days, 3 hours forecast data + lat, lon = _get_position(city) + url = f"http://api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&appid={flask.current_app.config['OPENWEATHER_API_KEY']}&units=metric" + data = requests.get(url).json() + return data + + +def _get_position(name) -> dict: + """direct geocoding + name: City name, state code (only for the US) and country code divided by comma. Please use ISO 3166 country codes. + + https://openweathermap.org/api/geocoding-api#direct + """ + limit = 5 + url = f"http://api.openweathermap.org/geo/1.0/direct?q={name}&limit={limit}&appid={flask.current_app.config['OPENWEATHER_API_KEY']}" + data = requests.get(url).json() + if not data: + return None + return data[0] + + +def get_weathermap() -> dict: + """https://openweathermap.org/api/weathermaps""" diff --git a/block_weather.py b/block_weather.py deleted file mode 100644 index 84c0092..0000000 --- a/block_weather.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Handle external requests and pre-process weather data - -Providers: -* OpenWeather - Free Access on OpenWeather allows for some API calls, see - https://openweathermap.org/price -""" - -import requests -import pandas as pd - -import config - -def get_current_weather(city: str) -> dict: - """https://openweathermap.org/current""" - pos = _get_position(city) - if not pos: - return None - url = f"http://api.openweathermap.org/data/2.5/weather?lat={pos['lat']}&lon={pos['lon']}&appid={config.OPENWEATHER_API_KEY}&units=metric" - data = requests.get(url).json() - - data['summary'] = _format_current_weather(data) - # On icon URL codes: https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 - data['icon_url'] = f"https://openweathermap.org/img/wn/{data['weather'][0]['icon']}.png" - return data - - -def _format_current_weather(data: dict) -> str: - """final formatting should occur in HTML/JS, not here""" - ts = [data['dt'], data['sys']['sunrise'], data['sys']['sunset']] - ts = [pd.to_datetime(x + data['timezone'], unit='s') for x in ts] - s = f"""{data['name']}, {data['sys']['country']} ({ts[0]}) -{data['weather'][0]['description']} -{data['main']['temp']}°C (feels like {data['main']['feels_like']}°C), humidity: {data['main']['humidity']}%, pressure: {data['main']['grnd_level']} hPa -visibility: {data.get('visibility', 0)/1000:.1f}km -wind: {data['wind']['speed']}m/s from {data['wind']['deg']}°N -clouds: {data['clouds']['all']}% -sunrise & sunset: {ts[1].strftime('%H:%M')}, {ts[2].strftime('%H:%M')}""" - return s - - -def get_forecast(city: str) -> dict: - """https://openweathermap.org/forecast5""" - # 5 days, 3 hours forecast data - lat, lon = _get_position(city) - url = f"http://api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&appid={config.OPENWEATHER_API_KEY}&units=metric" - data = requests.get(url).json() - return data - - -def _get_position(name) -> dict: - """direct geocoding - name: City name, state code (only for the US) and country code divided by comma. Please use ISO 3166 country codes. - - https://openweathermap.org/api/geocoding-api#direct - """ - limit = 5 - url = f"http://api.openweathermap.org/geo/1.0/direct?q={name}&limit={limit}&appid={config.OPENWEATHER_API_KEY}" - data = requests.get(url).json() - if not data: - return None - return data[0] - - -def get_weathermap() -> dict: - """https://openweathermap.org/api/weathermaps""" diff --git a/config.def.py b/config.def.py deleted file mode 100644 index c80065d..0000000 --- a/config.def.py +++ /dev/null @@ -1 +0,0 @@ -OPENWEATHER_API_KEY = "" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..52b87df --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +blinker==1.9.0 +certifi==2025.1.31 +charset-normalizer==3.4.1 +click==8.1.8 +Flask==3.1.0 +idna==3.10 +itsdangerous==2.2.0 +Jinja2==3.1.5 +MarkupSafe==3.0.2 +numpy==2.2.3 +pandas==2.2.3 +python-dateutil==2.9.0.post0 +pytz==2025.1 +requests==2.32.3 +six==1.17.0 +tzdata==2025.1 +urllib3==2.3.0 +Werkzeug==3.1.3 diff --git a/run.py b/run.py new file mode 100644 index 0000000..f6fe47e --- /dev/null +++ b/run.py @@ -0,0 +1,5 @@ +from app import create_app + +app = create_app() +if __name__ == '__main__': + app.run() diff --git a/static/block_time.js b/static/block_time.js deleted file mode 100644 index d6510ae..0000000 --- a/static/block_time.js +++ /dev/null @@ -1,49 +0,0 @@ -function init() { - initTimeVisualizer('timeVisualizer'); -} - -export function updateTime() { - fetch("/time") - .then(res => res.json()) - .then(data => { - document.getElementById("weatherSummary").innerText = data.time; - }); -} - -function initTimeVisualizer(containerId) { - const grid = document.createElement('div'); - grid.className = 'grid'; - document.getElementById(containerId).appendChild(grid); - - function updateTime() { - grid.innerHTML = ''; - const now = new Date(); - const hours = now.getHours(); - const minutes = now.getMinutes(); - - for (let i = 0; i < 24; i++) { - const cell = document.createElement('div'); - cell.className = 'cell'; - - if (i >= 20 || i < 4) cell.classList.add('sleep'); - if (i < hours) cell.classList.add('past'); - - if (i === hours) { - cell.classList.add('current'); - const fillPercentage = (minutes / 60) * 100; - cell.style.setProperty('--fill', `${fillPercentage}%`); - const line = document.createElement('div'); - line.className = 'timeline'; - line.style.left = `calc(${fillPercentage}% - 1px)`; - cell.appendChild(line); - } - - grid.appendChild(cell); - } - } - - updateTime(); - setInterval(updateTime, 60000); // 1min -} - -init() diff --git a/static/block_weather.js b/static/block_weather.js deleted file mode 100644 index 4ae6d2d..0000000 --- a/static/block_weather.js +++ /dev/null @@ -1,14 +0,0 @@ -export function updateWeather() { - const city = document.getElementById("city").value; - fetch(`/weather?city=${encodeURIComponent(city)}`) - .then(res => res.json()) - .then(data => { - if (!data) { - document.getElementById("weather-summary").innerText = `Error, "${city}" city not found`; - return - } - document.getElementById("weather-summary").innerText = data.summary; - document.getElementById("weather-icon").src = data.icon_url; - }); -} - diff --git a/static/script.js b/static/script.js deleted file mode 100644 index efd6836..0000000 --- a/static/script.js +++ /dev/null @@ -1,50 +0,0 @@ -import { updateTime } from './block_time.js'; -import { updateWeather } from './block_weather.js'; - -const config = { - polling: true -} - -function init() { - const blocks = { - time: { interval: 30 * 1000, lastUpdate: 0, update: updateTime }, // 30s - weather: { interval: 30 * 60000, lastUpdate: 0, update: updateWeather } // 30min - // Add more: { interval: X, lastUpdate: 0, update: updateFunction } - }; - - initHeaderControls(blocks) - - // Initial load - // Poll every 500ms to check intervals (fast enough for 1s updates, light on CPU) - // maybe the 1s updates should be special case, but for now let's keep it simple - reloadAll(blocks); - setInterval(() => pollUpdates(blocks), 500); -} - -function reloadAll(blocks) { - Object.keys(blocks).forEach(key => { - blocks[key].update(); - blocks[key].lastUpdate = Date.now(); - }); -} - -function pollUpdates(blocks) { - if (!config.polling) return; - const now = Date.now(); - Object.keys(blocks).forEach(key => { - const block = blocks[key]; - if (now - block.lastUpdate >= block.interval) { - block.update(); - block.lastUpdate = now; - } - }); -} - -function initHeaderControls(blocks) { - document.getElementById("reload").addEventListener("click", () => reloadAll(blocks)); - document.getElementById("pause").addEventListener("change", (e) => { - config.polling = e.target.checked; - }); -} - -init() diff --git a/static/style.css b/static/style.css deleted file mode 100644 index a186f24..0000000 --- a/static/style.css +++ /dev/null @@ -1,100 +0,0 @@ -/* MASU */ -body { - margin: 0; - padding: 0; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; -} - -header { - width: 100%; - height: 80px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -header button, header label { - margin: 0 10px; -} - -#container { - width: 100%; - /* max-width: 1920px; */ - padding: 20px; - box-sizing: border-box; - display: flex; - flex-wrap: wrap; - gap: 20px; - justify-content: center; -} - -.block { - width: 420px; - height: 420px; - flex-shrink: 0; - - padding: 5px; - border: 1px solid black; -} - -@media (max-width: 440px) { - .grid { - padding: 20px 0; - } -} - -/* block_time */ -#time .block { - justify-content: center; -} - -#timeVisualizer .grid { - display: grid; - grid-template-columns: repeat(8, 42px); - grid-template-rows: repeat(3, 42px); - gap: 2px; - background: #f0f0f0; - padding: 5px; - width: fit-content; - margin: 0 auto; -} - -#timeVisualizer .cell { - width: 42px; - height: 42px; - background: #ffffff; - position: relative; - border: 1px solid #ddd; -} - -#timeVisualizer .sleep { - background: #e6f3ff; -} - -#timeVisualizer .past { - background: #cccccc; -} - -#timeVisualizer .current::after { - content: ''; - position: absolute; - left: 0; - top: 0; - height: 100%; - width: var(--fill); - background: #cccccc; -} - -#timeVisualizer .timeline { - position: absolute; - width: 2px; - height: 100%; - background: #000000; - z-index: 1; -} - -/* block_weather */ diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 332b78b..0000000 --- a/templates/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - MASU - - - -
- - - -
-
- -
-

-
-
-
- weather icon -

-
- -
- - - -- cgit v1.2.3