feat: polish mobile ui and admin quick task settings
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
@@ -52,9 +54,16 @@ def create_app(config_class: type[Config] = Config) -> Flask:
|
|||||||
quick_task_form = QuickTaskForm(prefix="quick")
|
quick_task_form = QuickTaskForm(prefix="quick")
|
||||||
quick_task_config = get_quick_task_config()
|
quick_task_config = get_quick_task_config()
|
||||||
quick_task_form.effort.choices = [
|
quick_task_form.effort.choices = [
|
||||||
(key, f"{values['label']} · {values['points']} Punkte")
|
(key, values["label"])
|
||||||
for key, values in quick_task_config.items()
|
for key, values in quick_task_config.items()
|
||||||
]
|
]
|
||||||
|
def asset_version(filename: str) -> int:
|
||||||
|
path = Path(app.static_folder) / filename
|
||||||
|
try:
|
||||||
|
return int(path.stat().st_mtime)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"app_name": app.config["APP_NAME"],
|
"app_name": app.config["APP_NAME"],
|
||||||
"nav_items": [
|
"nav_items": [
|
||||||
@@ -65,7 +74,15 @@ def create_app(config_class: type[Config] = Config) -> Flask:
|
|||||||
("scoreboard.index", "Highscore", "trophy"),
|
("scoreboard.index", "Highscore", "trophy"),
|
||||||
("settings.index", "Optionen", "gear"),
|
("settings.index", "Optionen", "gear"),
|
||||||
],
|
],
|
||||||
|
"mobile_nav_items": [
|
||||||
|
("tasks.my_tasks", "Meine Aufgaben", "house"),
|
||||||
|
("tasks.all_tasks", "Alle Aufgaben", "list"),
|
||||||
|
("tasks.calendar_view", "Kalender", "calendar"),
|
||||||
|
("scoreboard.index", "Highscore", "trophy"),
|
||||||
|
("settings.index", "Optionen", "gear"),
|
||||||
|
],
|
||||||
"icon_svg": lambda name: load_icon_svg(name, app.static_folder),
|
"icon_svg": lambda name: load_icon_svg(name, app.static_folder),
|
||||||
|
"asset_version": asset_version,
|
||||||
"now_local": local_now(),
|
"now_local": local_now(),
|
||||||
"quick_task_form": quick_task_form,
|
"quick_task_form": quick_task_form,
|
||||||
"quick_task_config": quick_task_config,
|
"quick_task_config": quick_task_config,
|
||||||
|
|||||||
@@ -118,15 +118,19 @@ class QuickTaskForm(FlaskForm):
|
|||||||
title = StringField("Titel", validators=[DataRequired(), Length(min=2, max=160)])
|
title = StringField("Titel", validators=[DataRequired(), Length(min=2, max=160)])
|
||||||
effort = SelectField(
|
effort = SelectField(
|
||||||
"Aufwand",
|
"Aufwand",
|
||||||
choices=[(key, label) for key, label, _ in QUICK_TASK_EFFORTS],
|
choices=[(key, key) for key, _, _ in QUICK_TASK_EFFORTS],
|
||||||
validators=[DataRequired()],
|
validators=[DataRequired()],
|
||||||
)
|
)
|
||||||
submit = SubmitField("Schnellaufgabe speichern")
|
submit = SubmitField("Schnellaufgabe speichern")
|
||||||
|
|
||||||
|
|
||||||
class QuickTaskConfigForm(FlaskForm):
|
class QuickTaskConfigForm(FlaskForm):
|
||||||
|
fast_label = StringField("Bezeichnung", validators=[DataRequired(), Length(min=2, max=60)])
|
||||||
fast_points = IntegerField("Schnell", validators=[DataRequired(), NumberRange(min=1, max=500)])
|
fast_points = IntegerField("Schnell", validators=[DataRequired(), NumberRange(min=1, max=500)])
|
||||||
|
normal_label = StringField("Bezeichnung", validators=[DataRequired(), Length(min=2, max=60)])
|
||||||
normal_points = IntegerField("Normal", validators=[DataRequired(), NumberRange(min=1, max=500)])
|
normal_points = IntegerField("Normal", validators=[DataRequired(), NumberRange(min=1, max=500)])
|
||||||
|
medium_label = StringField("Bezeichnung", validators=[DataRequired(), Length(min=2, max=60)])
|
||||||
medium_points = IntegerField("Dauert etwas", validators=[DataRequired(), NumberRange(min=1, max=500)])
|
medium_points = IntegerField("Dauert etwas", validators=[DataRequired(), NumberRange(min=1, max=500)])
|
||||||
|
heavy_label = StringField("Bezeichnung", validators=[DataRequired(), Length(min=2, max=60)])
|
||||||
heavy_points = IntegerField("Aufwendig", validators=[DataRequired(), NumberRange(min=1, max=500)])
|
heavy_points = IntegerField("Aufwendig", validators=[DataRequired(), NumberRange(min=1, max=500)])
|
||||||
submit = SubmitField("Punkte speichern")
|
submit = SubmitField("Aufwand speichern")
|
||||||
|
|||||||
@@ -89,7 +89,14 @@ class TaskTemplate(TimestampMixin, db.Model):
|
|||||||
def recurrence_label(self) -> str:
|
def recurrence_label(self) -> str:
|
||||||
if self.recurrence_interval_unit == "none" or not self.recurrence_interval_value:
|
if self.recurrence_interval_unit == "none" or not self.recurrence_interval_value:
|
||||||
return "Einmalig"
|
return "Einmalig"
|
||||||
return f"Alle {self.recurrence_interval_value} {self.recurrence_interval_unit}"
|
units = {
|
||||||
|
"days": ("Tag", "Tage"),
|
||||||
|
"weeks": ("Woche", "Wochen"),
|
||||||
|
"months": ("Monat", "Monate"),
|
||||||
|
}
|
||||||
|
singular, plural = units.get(self.recurrence_interval_unit, (self.recurrence_interval_unit, self.recurrence_interval_unit))
|
||||||
|
unit_label = singular if self.recurrence_interval_value == 1 else plural
|
||||||
|
return f"Alle {self.recurrence_interval_value} {unit_label}"
|
||||||
|
|
||||||
|
|
||||||
class TaskInstance(TimestampMixin, db.Model):
|
class TaskInstance(TimestampMixin, db.Model):
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from werkzeug.utils import secure_filename
|
|||||||
from ..extensions import csrf, db
|
from ..extensions import csrf, db
|
||||||
from ..forms import AdminUserForm, QuickTaskConfigForm, SettingsProfileForm
|
from ..forms import AdminUserForm, QuickTaskConfigForm, SettingsProfileForm
|
||||||
from ..models import BadgeDefinition, MonthlyScoreSnapshot, NotificationLog, PushSubscription, TaskInstance, TaskTemplate, User
|
from ..models import BadgeDefinition, MonthlyScoreSnapshot, NotificationLog, PushSubscription, TaskInstance, TaskTemplate, User
|
||||||
from ..services.app_settings import get_quick_task_config, set_setting_int
|
from ..services.app_settings import get_quick_task_config, set_setting_int, set_setting_str
|
||||||
from ..services.badges import earned_badges_for_user
|
from ..services.badges import earned_badges_for_user
|
||||||
from ..services.notifications import push_enabled
|
from ..services.notifications import push_enabled
|
||||||
|
|
||||||
@@ -48,6 +48,16 @@ def index():
|
|||||||
form = SettingsProfileForm(original_email=current_user.email, obj=current_user)
|
form = SettingsProfileForm(original_email=current_user.email, obj=current_user)
|
||||||
admin_form = AdminUserForm(prefix="admin")
|
admin_form = AdminUserForm(prefix="admin")
|
||||||
quick_task_config_form = QuickTaskConfigForm(prefix="quickconfig")
|
quick_task_config_form = QuickTaskConfigForm(prefix="quickconfig")
|
||||||
|
quick_task_config = get_quick_task_config()
|
||||||
|
if request.method == "GET":
|
||||||
|
quick_task_config_form.fast_label.data = quick_task_config["fast"]["label"]
|
||||||
|
quick_task_config_form.fast_points.data = quick_task_config["fast"]["points"]
|
||||||
|
quick_task_config_form.normal_label.data = quick_task_config["normal"]["label"]
|
||||||
|
quick_task_config_form.normal_points.data = quick_task_config["normal"]["points"]
|
||||||
|
quick_task_config_form.medium_label.data = quick_task_config["medium"]["label"]
|
||||||
|
quick_task_config_form.medium_points.data = quick_task_config["medium"]["points"]
|
||||||
|
quick_task_config_form.heavy_label.data = quick_task_config["heavy"]["label"]
|
||||||
|
quick_task_config_form.heavy_points.data = quick_task_config["heavy"]["points"]
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
current_user.name = form.name.data.strip()
|
current_user.name = form.name.data.strip()
|
||||||
current_user.email = form.email.data.lower().strip()
|
current_user.email = form.email.data.lower().strip()
|
||||||
@@ -67,7 +77,7 @@ def index():
|
|||||||
form=form,
|
form=form,
|
||||||
admin_form=admin_form,
|
admin_form=admin_form,
|
||||||
quick_task_config_form=quick_task_config_form,
|
quick_task_config_form=quick_task_config_form,
|
||||||
quick_task_config=get_quick_task_config(),
|
quick_task_config=quick_task_config,
|
||||||
users=User.query.order_by(User.is_admin.desc(), User.name.asc()).all(),
|
users=User.query.order_by(User.is_admin.desc(), User.name.asc()).all(),
|
||||||
earned_badges=earned_badges_for_user(current_user.id),
|
earned_badges=earned_badges_for_user(current_user.id),
|
||||||
push_ready=push_enabled(),
|
push_ready=push_enabled(),
|
||||||
@@ -144,12 +154,16 @@ def update_quick_task_config():
|
|||||||
flash(error, "error")
|
flash(error, "error")
|
||||||
return redirect(url_for("settings.index"))
|
return redirect(url_for("settings.index"))
|
||||||
|
|
||||||
|
set_setting_str("quick_task_label_fast", form.fast_label.data)
|
||||||
set_setting_int("quick_task_points_fast", form.fast_points.data)
|
set_setting_int("quick_task_points_fast", form.fast_points.data)
|
||||||
|
set_setting_str("quick_task_label_normal", form.normal_label.data)
|
||||||
set_setting_int("quick_task_points_normal", form.normal_points.data)
|
set_setting_int("quick_task_points_normal", form.normal_points.data)
|
||||||
|
set_setting_str("quick_task_label_medium", form.medium_label.data)
|
||||||
set_setting_int("quick_task_points_medium", form.medium_points.data)
|
set_setting_int("quick_task_points_medium", form.medium_points.data)
|
||||||
|
set_setting_str("quick_task_label_heavy", form.heavy_label.data)
|
||||||
set_setting_int("quick_task_points_heavy", form.heavy_points.data)
|
set_setting_int("quick_task_points_heavy", form.heavy_points.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Schnellaufgaben-Punkte wurden aktualisiert.", "success")
|
flash("Schnellaufgaben-Aufwand und Punkte wurden aktualisiert.", "success")
|
||||||
return redirect(url_for("settings.index"))
|
return redirect(url_for("settings.index"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,11 @@ def calendar_view():
|
|||||||
for task in tasks:
|
for task in tasks:
|
||||||
tasks_by_day[task.due_date.day].append(task)
|
tasks_by_day[task.due_date.day].append(task)
|
||||||
|
|
||||||
|
mobile_day_groups = [
|
||||||
|
{"day": day, "tasks": grouped_tasks}
|
||||||
|
for day, grouped_tasks in sorted(tasks_by_day.items(), key=lambda item: item[0])
|
||||||
|
]
|
||||||
|
|
||||||
month_calendar = calendar.Calendar(firstweekday=0).monthdayscalendar(year, month)
|
month_calendar = calendar.Calendar(firstweekday=0).monthdayscalendar(year, month)
|
||||||
return render_template(
|
return render_template(
|
||||||
"tasks/calendar.html",
|
"tasks/calendar.html",
|
||||||
@@ -201,6 +206,7 @@ def calendar_view():
|
|||||||
current_label=month_label(year, month),
|
current_label=month_label(year, month),
|
||||||
month_calendar=month_calendar,
|
month_calendar=month_calendar,
|
||||||
tasks_by_day=tasks_by_day,
|
tasks_by_day=tasks_by_day,
|
||||||
|
mobile_day_groups=mobile_day_groups,
|
||||||
view=view,
|
view=view,
|
||||||
tasks=tasks,
|
tasks=tasks,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ from ..models import AppSetting
|
|||||||
|
|
||||||
|
|
||||||
QUICK_TASK_DEFAULTS = {
|
QUICK_TASK_DEFAULTS = {
|
||||||
|
"quick_task_label_fast": "Schnell",
|
||||||
|
"quick_task_label_normal": "Normal",
|
||||||
|
"quick_task_label_medium": "Dauert etwas",
|
||||||
|
"quick_task_label_heavy": "Aufwendig",
|
||||||
"quick_task_points_fast": 4,
|
"quick_task_points_fast": 4,
|
||||||
"quick_task_points_normal": 8,
|
"quick_task_points_normal": 8,
|
||||||
"quick_task_points_medium": 12,
|
"quick_task_points_medium": 12,
|
||||||
@@ -12,10 +16,10 @@ QUICK_TASK_DEFAULTS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QUICK_TASK_EFFORTS = [
|
QUICK_TASK_EFFORTS = [
|
||||||
("fast", "Schnell", "quick_task_points_fast"),
|
("fast", "quick_task_label_fast", "quick_task_points_fast"),
|
||||||
("normal", "Normal", "quick_task_points_normal"),
|
("normal", "quick_task_label_normal", "quick_task_points_normal"),
|
||||||
("medium", "Dauert etwas", "quick_task_points_medium"),
|
("medium", "quick_task_label_medium", "quick_task_points_medium"),
|
||||||
("heavy", "Aufwendig", "quick_task_points_heavy"),
|
("heavy", "quick_task_label_heavy", "quick_task_points_heavy"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -46,12 +50,30 @@ def set_setting_int(key: str, value: int) -> None:
|
|||||||
setting.value = str(value)
|
setting.value = str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def get_setting_str(key: str, default: str) -> str:
|
||||||
|
setting = AppSetting.query.filter_by(key=key).first()
|
||||||
|
if not setting or not setting.value.strip():
|
||||||
|
return default
|
||||||
|
return setting.value.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def set_setting_str(key: str, value: str) -> None:
|
||||||
|
setting = AppSetting.query.filter_by(key=key).first()
|
||||||
|
normalized = value.strip()
|
||||||
|
if not setting:
|
||||||
|
setting = AppSetting(key=key, value=normalized)
|
||||||
|
db.session.add(setting)
|
||||||
|
else:
|
||||||
|
setting.value = normalized
|
||||||
|
|
||||||
|
|
||||||
def get_quick_task_config() -> dict[str, dict]:
|
def get_quick_task_config() -> dict[str, dict]:
|
||||||
config: dict[str, dict] = {}
|
config: dict[str, dict] = {}
|
||||||
for effort_key, label, setting_key in QUICK_TASK_EFFORTS:
|
for effort_key, label_key, points_key in QUICK_TASK_EFFORTS:
|
||||||
config[effort_key] = {
|
config[effort_key] = {
|
||||||
"label": label,
|
"label_key": label_key,
|
||||||
"setting_key": setting_key,
|
"label": get_setting_str(label_key, str(QUICK_TASK_DEFAULTS[label_key])),
|
||||||
"points": get_setting_int(setting_key, QUICK_TASK_DEFAULTS[setting_key]),
|
"points_key": points_key,
|
||||||
|
"points": get_setting_int(points_key, int(QUICK_TASK_DEFAULTS[points_key])),
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
--bg: #eef3ff;
|
--bg: #eef3ff;
|
||||||
--bg-accent: #d8e6ff;
|
--bg-accent: #d8e6ff;
|
||||||
--surface: rgba(255, 255, 255, 0.85);
|
--surface: rgba(255, 255, 255, 0.85);
|
||||||
@@ -69,6 +70,14 @@
|
|||||||
--font-body: "InterLocal", system-ui, sans-serif;
|
--font-body: "InterLocal", system-ui, sans-serif;
|
||||||
--font-heading: "SpaceGroteskLocal", "InterLocal", sans-serif;
|
--font-heading: "SpaceGroteskLocal", "InterLocal", sans-serif;
|
||||||
--safe-bottom: max(24px, env(safe-area-inset-bottom));
|
--safe-bottom: max(24px, env(safe-area-inset-bottom));
|
||||||
|
--body-radial-a: rgba(181, 210, 255, 0.85);
|
||||||
|
--body-radial-b: rgba(255, 221, 196, 0.48);
|
||||||
|
--body-linear-start: #f8fbff;
|
||||||
|
--body-linear-mid: #eef3ff;
|
||||||
|
--body-linear-end: #edf2ff;
|
||||||
|
--input-bg: rgba(255, 255, 255, 0.86);
|
||||||
|
--nav-bg: rgba(255, 255, 255, 0.88);
|
||||||
|
--nav-active-bg: rgba(37, 99, 235, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -77,17 +86,19 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top left, rgba(181, 210, 255, 0.85), transparent 32%),
|
radial-gradient(circle at top left, var(--body-radial-a), transparent 32%),
|
||||||
radial-gradient(circle at top right, rgba(255, 221, 196, 0.48), transparent 32%),
|
radial-gradient(circle at top right, var(--body-radial-b), transparent 32%),
|
||||||
linear-gradient(180deg, #f8fbff 0%, #eef3ff 42%, #edf2ff 100%);
|
linear-gradient(180deg, var(--body-linear-start) 0%, var(--body-linear-mid) 42%, var(--body-linear-end) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -167,6 +178,7 @@ p {
|
|||||||
.content {
|
.content {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel,
|
.panel,
|
||||||
@@ -266,7 +278,7 @@ p {
|
|||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: rgba(255, 255, 255, 0.94);
|
background: var(--surface-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flash--success {
|
.flash--success {
|
||||||
@@ -287,7 +299,7 @@ p {
|
|||||||
margin-top: 22px;
|
margin-top: 22px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: rgba(255, 255, 255, 0.74);
|
background: var(--surface-soft);
|
||||||
border: 1px solid rgba(132, 152, 190, 0.2);
|
border: 1px solid rgba(132, 152, 190, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +378,7 @@ p {
|
|||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: rgba(255, 255, 255, 0.58);
|
background: var(--surface-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card {
|
.task-card {
|
||||||
@@ -396,7 +408,7 @@ p {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(37, 99, 235, 0.08);
|
background: var(--nav-active-bg);
|
||||||
color: var(--primary-strong);
|
color: var(--primary-strong);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@@ -408,7 +420,7 @@ p {
|
|||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(255, 255, 255, 0.72);
|
background: var(--surface-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
@@ -537,7 +549,7 @@ p {
|
|||||||
.icon-button {
|
.icon-button {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: rgba(255, 255, 255, 0.74);
|
background: var(--surface-soft);
|
||||||
border: 1px solid rgba(132, 152, 190, 0.24);
|
border: 1px solid rgba(132, 152, 190, 0.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,7 +583,7 @@ p {
|
|||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border: 1px solid rgba(132, 152, 190, 0.3);
|
border: 1px solid rgba(132, 152, 190, 0.3);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: rgba(255, 255, 255, 0.86);
|
background: var(--input-bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,28 +624,176 @@ p {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel--toolbar > * {
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmented {
|
.segmented {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
background: rgba(255, 255, 255, 0.74);
|
background: var(--surface-soft);
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmented a {
|
.segmented a {
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmented .is-active {
|
.segmented .is-active {
|
||||||
background: #fff;
|
background: var(--surface-strong);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-grid {
|
.calendar-toolbar-mobile {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-toolbar-mobile__header,
|
||||||
|
.calendar-toolbar-mobile__switch {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-toolbar-mobile__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-toolbar-mobile__header > div:first-child {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-toolbar-mobile__arrows {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-nav-button {
|
||||||
|
width: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-nav-button span {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-toolbar-mobile__switch a {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-toolbar-mobile__switch {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-toolbar-desktop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-strip {
|
||||||
|
padding: 14px 16px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-strip__scroller {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
scroll-snap-type: x proximity;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-pill {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 84px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface-strong);
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
box-shadow: 0 10px 20px rgba(52, 79, 131, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-pill strong {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-pill small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-pill.is-active {
|
||||||
|
border-color: rgba(37, 99, 235, 0.16);
|
||||||
|
background: linear-gradient(135deg, rgba(37, 99, 235, 0.14), rgba(52, 211, 153, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-day {
|
||||||
|
padding: 18px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-day__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-day__header strong {
|
||||||
|
font-size: 1.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-day__header span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-day__tasks {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-day .calendar-task {
|
||||||
|
background: var(--surface-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-grid {
|
||||||
|
display: none;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,9 +826,10 @@ p {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
padding: 9px 10px;
|
padding: 9px 10px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: rgba(255, 255, 255, 0.72);
|
background: var(--surface-soft);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,7 +923,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.badge-card--earned {
|
.badge-card--earned {
|
||||||
background: rgba(37, 99, 235, 0.03);
|
background: var(--nav-active-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-card__icon {
|
.badge-card__icon {
|
||||||
@@ -793,7 +954,7 @@ p {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
background: rgba(255, 255, 255, 0.7);
|
background: var(--surface-soft);
|
||||||
border: 1px solid rgba(132, 152, 190, 0.18);
|
border: 1px solid rgba(132, 152, 190, 0.18);
|
||||||
box-shadow: 0 12px 30px rgba(58, 82, 128, 0.1);
|
box-shadow: 0 12px 30px rgba(58, 82, 128, 0.1);
|
||||||
}
|
}
|
||||||
@@ -809,7 +970,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab.is-active {
|
.settings-tab.is-active {
|
||||||
background: #fff;
|
background: var(--surface-strong);
|
||||||
color: var(--primary-strong);
|
color: var(--primary-strong);
|
||||||
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.08);
|
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.08);
|
||||||
}
|
}
|
||||||
@@ -830,7 +991,7 @@ p {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: rgba(255, 255, 255, 0.76);
|
background: var(--surface-soft);
|
||||||
border: 1px solid rgba(132, 152, 190, 0.22);
|
border: 1px solid rgba(132, 152, 190, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,7 +1011,7 @@ p {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
background: rgba(255, 255, 255, 0.66);
|
background: var(--surface-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.push-box__state.is-disabled {
|
.push-box__state.is-disabled {
|
||||||
@@ -876,16 +1037,15 @@ p {
|
|||||||
|
|
||||||
.bottom-nav {
|
.bottom-nav {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 50%;
|
left: 8px;
|
||||||
transform: translateX(-50%);
|
right: 8px;
|
||||||
width: min(calc(100vw - 52px), 560px);
|
|
||||||
bottom: calc(10px + env(safe-area-inset-bottom));
|
bottom: calc(10px + env(safe-area-inset-bottom));
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, 1fr);
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
padding: 8px;
|
padding: 8px 8px;
|
||||||
border-radius: 24px;
|
border-radius: 22px;
|
||||||
background: rgba(255, 255, 255, 0.88);
|
background: var(--nav-bg);
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
box-shadow: 0 24px 44px rgba(58, 82, 128, 0.2);
|
box-shadow: 0 24px 44px rgba(58, 82, 128, 0.2);
|
||||||
border: 1px solid rgba(132, 152, 190, 0.22);
|
border: 1px solid rgba(132, 152, 190, 0.22);
|
||||||
@@ -897,19 +1057,29 @@ p {
|
|||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 10px 4px;
|
min-width: 0;
|
||||||
|
padding: 10px 4px 9px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.69rem;
|
font-size: 0.66rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.1;
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav__item span {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 2.1em;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: keep-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-nav__item.is-active,
|
.bottom-nav__item.is-active,
|
||||||
.nav-link.is-active {
|
.nav-link.is-active {
|
||||||
color: var(--primary-strong);
|
color: var(--primary-strong);
|
||||||
background: rgba(37, 99, 235, 0.1);
|
background: var(--nav-active-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-icon,
|
.nav-icon,
|
||||||
@@ -934,7 +1104,7 @@ p {
|
|||||||
width: min(460px, calc(100vw - 24px));
|
width: min(460px, calc(100vw - 24px));
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-radius: 28px;
|
border-radius: 28px;
|
||||||
background: #fff;
|
background: var(--surface-strong);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
@@ -962,15 +1132,15 @@ p {
|
|||||||
|
|
||||||
.fab-quick-task {
|
.fab-quick-task {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 18px;
|
right: max(16px, env(safe-area-inset-right));
|
||||||
bottom: calc(96px + env(safe-area-inset-bottom));
|
bottom: calc(72px + var(--safe-bottom));
|
||||||
width: 62px;
|
width: 62px;
|
||||||
height: 62px;
|
height: 62px;
|
||||||
border: 0;
|
border: 2px solid rgba(255, 255, 255, 0.55);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: linear-gradient(135deg, #2563eb, #34d399);
|
background: linear-gradient(135deg, #2563eb, #34d399);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 24px 40px rgba(37, 99, 235, 0.28);
|
box-shadow: 0 24px 40px rgba(37, 99, 235, 0.34);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -984,7 +1154,128 @@ p {
|
|||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 759px) {
|
||||||
|
.calendar-toolbar-mobile__header {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-toolbar-mobile__header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #0b1220;
|
||||||
|
--bg-accent: #15233d;
|
||||||
|
--surface: rgba(17, 24, 39, 0.82);
|
||||||
|
--surface-strong: rgba(15, 23, 38, 0.96);
|
||||||
|
--surface-soft: rgba(20, 30, 49, 0.92);
|
||||||
|
--text: #e5eefc;
|
||||||
|
--muted: #9eb0cc;
|
||||||
|
--border: rgba(121, 146, 191, 0.2);
|
||||||
|
--primary: #5ea8ff;
|
||||||
|
--primary-strong: #d7e7ff;
|
||||||
|
--secondary: rgba(37, 99, 235, 0.16);
|
||||||
|
--shadow: 0 28px 70px rgba(0, 0, 0, 0.34);
|
||||||
|
--body-radial-a: rgba(37, 99, 235, 0.28);
|
||||||
|
--body-radial-b: rgba(20, 184, 166, 0.14);
|
||||||
|
--body-linear-start: #0b1220;
|
||||||
|
--body-linear-mid: #0f172a;
|
||||||
|
--body-linear-end: #131f35;
|
||||||
|
--input-bg: rgba(16, 24, 38, 0.92);
|
||||||
|
--nav-bg: rgba(15, 23, 38, 0.9);
|
||||||
|
--nav-active-bg: rgba(59, 130, 246, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(16, 24, 38, 0.98), rgba(20, 30, 49, 0.92)),
|
||||||
|
linear-gradient(135deg, rgba(37, 99, 235, 0.1), rgba(20, 184, 166, 0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats div {
|
||||||
|
background: var(--surface-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
border-color: rgba(121, 146, 191, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash--success {
|
||||||
|
color: #7df1c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash--error {
|
||||||
|
color: #ff94b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--open {
|
||||||
|
background: rgba(37, 99, 235, 0.16);
|
||||||
|
color: #8db7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--soon {
|
||||||
|
background: rgba(245, 158, 11, 0.18);
|
||||||
|
color: #ffd38a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--overdue {
|
||||||
|
background: rgba(225, 29, 72, 0.18);
|
||||||
|
color: #ff9cb2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--completed {
|
||||||
|
background: rgba(5, 150, 105, 0.18);
|
||||||
|
color: #8df0cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--ghost {
|
||||||
|
border-color: rgba(121, 146, 191, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-row--leader {
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(18, 29, 49, 0.98), rgba(24, 39, 62, 0.94)),
|
||||||
|
linear-gradient(135deg, rgba(52, 211, 153, 0.12), rgba(94, 168, 255, 0.08));
|
||||||
|
border-color: rgba(94, 168, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-row--leader .rank-badge,
|
||||||
|
.score-row--leader .earned-badge {
|
||||||
|
background: rgba(94, 168, 255, 0.16);
|
||||||
|
color: var(--primary-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-row--leader .earned-badge__icon {
|
||||||
|
background: rgba(10, 18, 32, 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-row__points span,
|
||||||
|
.archive-row__right span {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background: rgba(10, 16, 28, 0.76);
|
||||||
|
border-right-color: rgba(121, 146, 191, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-user {
|
||||||
|
border-color: rgba(121, 146, 191, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 760px) {
|
@media (min-width: 760px) {
|
||||||
|
.calendar-toolbar-mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-toolbar-desktop {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.page-shell {
|
.page-shell {
|
||||||
padding: 28px 28px 32px;
|
padding: 28px 28px 32px;
|
||||||
}
|
}
|
||||||
@@ -1005,8 +1296,15 @@ p {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-mobile-strip,
|
||||||
|
.calendar-mobile-list {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-grid {
|
.calendar-grid {
|
||||||
|
display: grid;
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-grid__weekdays {
|
.calendar-grid__weekdays {
|
||||||
@@ -1026,6 +1324,34 @@ p {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-day {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 152px;
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day__tasks {
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-task {
|
||||||
|
padding: 8px 9px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-task__title {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.18;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-task__person {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1100px) {
|
@media (min-width: 1100px) {
|
||||||
@@ -1043,7 +1369,7 @@ p {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
padding: 28px 20px;
|
padding: 28px 20px;
|
||||||
border-right: 1px solid rgba(132, 152, 190, 0.2);
|
border-right: 1px solid rgba(132, 152, 190, 0.2);
|
||||||
background: rgba(248, 251, 255, 0.72);
|
background: var(--surface-soft);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1070,7 +1396,7 @@ p {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 8px 10px 8px 18px;
|
padding: 8px 10px 8px 18px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(255, 255, 255, 0.85);
|
background: var(--nav-bg);
|
||||||
border: 1px solid rgba(132, 152, 190, 0.2);
|
border: 1px solid rgba(132, 152, 190, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<meta name="theme-color" content="#f5f7ff">
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f5f7ff">
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#101826">
|
||||||
|
<meta name="color-scheme" content="light dark">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
<meta name="apple-mobile-web-app-title" content="{{ app_name }}">
|
<meta name="apple-mobile-web-app-title" content="{{ app_name }}">
|
||||||
@@ -12,7 +14,7 @@
|
|||||||
<link rel="manifest" href="{{ url_for('main.manifest') }}">
|
<link rel="manifest" href="{{ url_for('main.manifest') }}">
|
||||||
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='images/favicon.svg') }}">
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='images/favicon.svg') }}">
|
||||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='images/apple-touch-icon.png') }}">
|
<link rel="apple-touch-icon" href="{{ url_for('static', filename='images/apple-touch-icon.png') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css', v=asset_version('css/style.css')) }}">
|
||||||
</head>
|
</head>
|
||||||
<body data-push-key="{{ config['VAPID_PUBLIC_KEY'] if current_user.is_authenticated else '' }}">
|
<body data-push-key="{{ config['VAPID_PUBLIC_KEY'] if current_user.is_authenticated else '' }}">
|
||||||
{% from "partials/macros.html" import nav_icon %}
|
{% from "partials/macros.html" import nav_icon %}
|
||||||
@@ -89,7 +91,7 @@
|
|||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<nav class="bottom-nav" aria-label="Mobile Navigation">
|
<nav class="bottom-nav" aria-label="Mobile Navigation">
|
||||||
{% for endpoint, label, icon in nav_items %}
|
{% for endpoint, label, icon in mobile_nav_items %}
|
||||||
<a href="{{ url_for(endpoint) }}" class="bottom-nav__item {% if request.endpoint == endpoint %}is-active{% endif %}">
|
<a href="{{ url_for(endpoint) }}" class="bottom-nav__item {% if request.endpoint == endpoint %}is-active{% endif %}">
|
||||||
{{ nav_icon(icon) }}
|
{{ nav_icon(icon) }}
|
||||||
<span>{{ label }}</span>
|
<span>{{ label }}</span>
|
||||||
@@ -142,6 +144,6 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/app.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='js/app.js', v=asset_version('js/app.js')) }}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -166,48 +166,64 @@
|
|||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<p class="eyebrow">Admin</p>
|
<p class="eyebrow">Admin</p>
|
||||||
<h2>Schnellaufgabe-Punkte</h2>
|
<h2>Schnellaufgabe-Aufwand</h2>
|
||||||
<p class="muted">Diese Werte erscheinen direkt im Schnellaufgaben-Dialog hinter den Aufwand-Stufen.</p>
|
<p class="muted">Hier definierst du die sichtbaren Aufwand-Stufen und die dazugehörigen Punkte. Im Schnellaufgaben-Dialog wird nur die Bezeichnung angezeigt.</p>
|
||||||
<form method="post" action="{{ url_for('settings.update_quick_task_config') }}" class="badge-settings">
|
<form method="post" action="{{ url_for('settings.update_quick_task_config') }}" class="badge-settings">
|
||||||
{{ quick_task_config_form.hidden_tag() }}
|
{{ quick_task_config_form.hidden_tag() }}
|
||||||
<div class="badge-setting-card">
|
<div class="badge-setting-card">
|
||||||
<div>
|
<div>
|
||||||
<strong>Schnell</strong>
|
<strong>Slot 1</strong>
|
||||||
<p class="muted">Kleine Sache für zwischendurch.</p>
|
<p class="muted">Kleine Sache für zwischendurch.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{ quick_task_config_form.fast_points.label }}
|
{{ quick_task_config_form.fast_label.label }}
|
||||||
{{ quick_task_config_form.fast_points(value=quick_task_config['fast']['points']) }}
|
{{ quick_task_config_form.fast_label() }}
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="{{ quick_task_config_form.fast_points.id }}">Punkte</label>
|
||||||
|
{{ quick_task_config_form.fast_points() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="badge-setting-card">
|
<div class="badge-setting-card">
|
||||||
<div>
|
<div>
|
||||||
<strong>Normal</strong>
|
<strong>Slot 2</strong>
|
||||||
<p class="muted">Typische Alltagssache.</p>
|
<p class="muted">Typische Alltagssache.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{ quick_task_config_form.normal_points.label }}
|
{{ quick_task_config_form.normal_label.label }}
|
||||||
{{ quick_task_config_form.normal_points(value=quick_task_config['normal']['points']) }}
|
{{ quick_task_config_form.normal_label() }}
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="{{ quick_task_config_form.normal_points.id }}">Punkte</label>
|
||||||
|
{{ quick_task_config_form.normal_points() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="badge-setting-card">
|
<div class="badge-setting-card">
|
||||||
<div>
|
<div>
|
||||||
<strong>Dauert etwas</strong>
|
<strong>Slot 3</strong>
|
||||||
<p class="muted">Braucht etwas mehr Zeit oder Konzentration.</p>
|
<p class="muted">Braucht etwas mehr Zeit oder Konzentration.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{ quick_task_config_form.medium_points.label }}
|
{{ quick_task_config_form.medium_label.label }}
|
||||||
{{ quick_task_config_form.medium_points(value=quick_task_config['medium']['points']) }}
|
{{ quick_task_config_form.medium_label() }}
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="{{ quick_task_config_form.medium_points.id }}">Punkte</label>
|
||||||
|
{{ quick_task_config_form.medium_points() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="badge-setting-card">
|
<div class="badge-setting-card">
|
||||||
<div>
|
<div>
|
||||||
<strong>Aufwendig</strong>
|
<strong>Slot 4</strong>
|
||||||
<p class="muted">Spürbarer Aufwand mit mehr Punkten.</p>
|
<p class="muted">Spürbarer Aufwand mit mehr Punkten.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{ quick_task_config_form.heavy_points.label }}
|
{{ quick_task_config_form.heavy_label.label }}
|
||||||
{{ quick_task_config_form.heavy_points(value=quick_task_config['heavy']['points']) }}
|
{{ quick_task_config_form.heavy_label() }}
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="{{ quick_task_config_form.heavy_points.id }}">Punkte</label>
|
||||||
|
{{ quick_task_config_form.heavy_points() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field field--full">
|
<div class="field field--full">
|
||||||
|
|||||||
@@ -3,7 +3,36 @@
|
|||||||
{% block title %}Kalender · Putzliga{% endblock %}
|
{% block title %}Kalender · Putzliga{% endblock %}
|
||||||
{% block page_title %}Kalender & Liste{% endblock %}
|
{% block page_title %}Kalender & Liste{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="panel panel--toolbar">
|
<section class="panel calendar-toolbar-mobile">
|
||||||
|
<div class="calendar-toolbar-mobile__header">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Monatsansicht</p>
|
||||||
|
<h2>{{ current_label }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-toolbar-mobile__arrows">
|
||||||
|
<a
|
||||||
|
class="icon-button calendar-nav-button"
|
||||||
|
href="{{ url_for('tasks.calendar_view', year=current_year if current_month > 1 else current_year - 1, month=current_month - 1 if current_month > 1 else 12, view=view) }}"
|
||||||
|
aria-label="Vorheriger Monat"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">‹</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="icon-button calendar-nav-button"
|
||||||
|
href="{{ url_for('tasks.calendar_view', year=current_year if current_month < 12 else current_year + 1, month=current_month + 1 if current_month < 12 else 1, view=view) }}"
|
||||||
|
aria-label="Nächster Monat"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">›</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="segmented calendar-toolbar-mobile__switch">
|
||||||
|
<a href="{{ url_for('tasks.calendar_view', year=current_year, month=current_month, view='calendar') }}" class="{% if view == 'calendar' %}is-active{% endif %}">Kalender</a>
|
||||||
|
<a href="{{ url_for('tasks.calendar_view', year=current_year, month=current_month, view='list') }}" class="{% if view == 'list' %}is-active{% endif %}">Liste</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel panel--toolbar calendar-toolbar-desktop">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Monatsansicht</p>
|
<p class="eyebrow">Monatsansicht</p>
|
||||||
<h2>{{ current_label }}</h2>
|
<h2>{{ current_label }}</h2>
|
||||||
@@ -19,6 +48,48 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% if view == 'calendar' %}
|
{% if view == 'calendar' %}
|
||||||
|
{% if mobile_day_groups %}
|
||||||
|
<section class="panel calendar-mobile-strip">
|
||||||
|
<div class="calendar-mobile-strip__scroller">
|
||||||
|
{% for group in mobile_day_groups %}
|
||||||
|
<a href="#day-{{ group.day }}" class="calendar-mobile-pill {% if loop.first %}is-active{% endif %}">
|
||||||
|
<strong>{{ group.day }}.</strong>
|
||||||
|
<small>{{ group.tasks|length }} Aufgabe{% if group.tasks|length != 1 %}n{% endif %}</small>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<section class="calendar-mobile-list">
|
||||||
|
{% for group in mobile_day_groups %}
|
||||||
|
<article class="panel calendar-mobile-day" id="day-{{ group.day }}">
|
||||||
|
<div class="calendar-mobile-day__header">
|
||||||
|
<strong>{{ group.day }}. {{ current_month|month_name }}</strong>
|
||||||
|
<span>{{ group.tasks|length }} Aufgabe{% if group.tasks|length != 1 %}n{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-mobile-day__tasks">
|
||||||
|
{% for task in group.tasks %}
|
||||||
|
<a href="{{ url_for('tasks.edit', task_id=task.id) }}" class="calendar-task calendar-task--{{ task.status }}">
|
||||||
|
<strong class="calendar-task__title">{{ task.title }}</strong>
|
||||||
|
<small class="calendar-task__person">
|
||||||
|
{% if task.completed_by_user %}
|
||||||
|
{{ task.completed_by_user.name }}
|
||||||
|
{% elif task.assigned_user %}
|
||||||
|
{{ task.assigned_user.name }}
|
||||||
|
{% else %}
|
||||||
|
Ohne Zuweisung
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state">In diesem Monat sind noch keine Aufgaben hinterlegt.</div>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="calendar-grid">
|
<section class="calendar-grid">
|
||||||
<div class="calendar-grid__weekdays">
|
<div class="calendar-grid__weekdays">
|
||||||
{% for label in ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'] %}
|
{% for label in ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'] %}
|
||||||
@@ -36,9 +107,9 @@
|
|||||||
<strong class="calendar-task__title">{{ task.title }}</strong>
|
<strong class="calendar-task__title">{{ task.title }}</strong>
|
||||||
<small class="calendar-task__person">
|
<small class="calendar-task__person">
|
||||||
{% if task.completed_by_user %}
|
{% if task.completed_by_user %}
|
||||||
Erledigt: {{ task.completed_by_user.name }}
|
{{ task.completed_by_user.name }}
|
||||||
{% elif task.assigned_user %}
|
{% elif task.assigned_user %}
|
||||||
Zuständig: {{ task.assigned_user.name }}
|
{{ task.assigned_user.name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
Ohne Zuweisung
|
Ohne Zuweisung
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user