generated from nathanwoodburn/python-webserver-template
feat: Add highlighing for upcoming week
All checks were successful
Build Docker / BuildImage (push) Successful in 1m44s
All checks were successful
Build Docker / BuildImage (push) Successful in 1m44s
This commit is contained in:
73
server.py
73
server.py
@@ -12,6 +12,7 @@ import requests
|
|||||||
import dotenv
|
import dotenv
|
||||||
import csv
|
import csv
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
@@ -86,6 +87,69 @@ SCHEDULE_DATA = load_schedule_data()
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_schedule_date(raw_date: str) -> date | None:
|
||||||
|
"""Parse a schedule date string using common formats."""
|
||||||
|
if not raw_date:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cleaned = raw_date.strip()
|
||||||
|
|
||||||
|
# Try ISO date first.
|
||||||
|
try:
|
||||||
|
return date.fromisoformat(cleaned)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
formats = [
|
||||||
|
"%d/%m/%Y",
|
||||||
|
"%m/%d/%Y",
|
||||||
|
"%d/%m/%y",
|
||||||
|
"%m/%d/%y",
|
||||||
|
"%d-%m-%Y",
|
||||||
|
"%m-%d-%Y",
|
||||||
|
"%d %b %Y",
|
||||||
|
"%d %B %Y",
|
||||||
|
"%b %d, %Y",
|
||||||
|
"%B %d, %Y",
|
||||||
|
"%a %d %b %Y",
|
||||||
|
"%A %d %B %Y",
|
||||||
|
]
|
||||||
|
|
||||||
|
for fmt in formats:
|
||||||
|
try:
|
||||||
|
return datetime.strptime(cleaned, fmt).date()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
month_day_formats = [
|
||||||
|
"%B %d",
|
||||||
|
"%b %d",
|
||||||
|
"%B %d,",
|
||||||
|
"%b %d,",
|
||||||
|
]
|
||||||
|
|
||||||
|
current_year = date.today().year
|
||||||
|
for fmt in month_day_formats:
|
||||||
|
try:
|
||||||
|
# Add current year to the end of the string for parsing
|
||||||
|
date_str_with_year = f"{cleaned} {current_year}"
|
||||||
|
return datetime.strptime(date_str_with_year, fmt + " %Y").date()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_upcoming_week_index(schedule: list[dict]) -> int | None:
|
||||||
|
"""Return the index of the next schedule item that is today or later."""
|
||||||
|
today = date.today()
|
||||||
|
for index, item in enumerate(schedule):
|
||||||
|
parsed_date = parse_schedule_date(item.get("date", ""))
|
||||||
|
if parsed_date and parsed_date >= today:
|
||||||
|
return index
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def find(name, path):
|
def find(name, path):
|
||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
if name in files:
|
if name in files:
|
||||||
@@ -140,7 +204,12 @@ def wellknown(path):
|
|||||||
# region Main routes
|
# region Main routes
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template("schedule.html", schedule=SCHEDULE_DATA)
|
upcoming_week_index = find_upcoming_week_index(SCHEDULE_DATA)
|
||||||
|
return render_template(
|
||||||
|
"schedule.html",
|
||||||
|
schedule=SCHEDULE_DATA,
|
||||||
|
upcoming_week_index=upcoming_week_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
@@ -196,4 +265,4 @@ def not_found(e):
|
|||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(debug=True, port=5000, host="0.0.0.0")
|
app.run(debug=True, port=5000, host="127.0.0.1")
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
--empty-leader-color: #666;
|
--empty-leader-color: #666;
|
||||||
--select-bg: rgba(50, 50, 50, 0.8);
|
--select-bg: rgba(50, 50, 50, 0.8);
|
||||||
--select-border: #666;
|
--select-border: #666;
|
||||||
|
--upcoming-accent: #7dd3fc;
|
||||||
|
--upcoming-row-bg: rgba(125, 211, 252, 0.14);
|
||||||
|
--upcoming-glow: rgba(125, 211, 252, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="pink"] {
|
[data-theme="pink"] {
|
||||||
@@ -34,6 +37,9 @@
|
|||||||
--empty-leader-color: #996677;
|
--empty-leader-color: #996677;
|
||||||
--select-bg: rgba(80, 40, 60, 0.8);
|
--select-bg: rgba(80, 40, 60, 0.8);
|
||||||
--select-border: #a55577;
|
--select-border: #a55577;
|
||||||
|
--upcoming-accent: #f9a8d4;
|
||||||
|
--upcoming-row-bg: rgba(249, 168, 212, 0.16);
|
||||||
|
--upcoming-glow: rgba(249, 168, 212, 0.32);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -207,6 +213,36 @@ h1 {
|
|||||||
background-color: var(--special-event-hover);
|
background-color: var(--special-event-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upcoming-week {
|
||||||
|
background: linear-gradient(90deg, var(--upcoming-row-bg) 0%, transparent 72%);
|
||||||
|
box-shadow: inset 4px 0 0 var(--upcoming-accent), inset 0 0 0 1px var(--upcoming-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-week:hover {
|
||||||
|
background: linear-gradient(90deg, var(--upcoming-row-bg) 0%, var(--row-hover-bg) 72%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-date {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-date::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-date::after {
|
||||||
|
content: "next up";
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 1.2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--upcoming-accent);
|
||||||
|
}
|
||||||
|
|
||||||
.date-cell {
|
.date-cell {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--date-color);
|
color: var(--date-color);
|
||||||
@@ -330,6 +366,15 @@ h1 {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upcoming-date {
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-date::after {
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.leader-cell {
|
.leader-cell {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -378,6 +423,11 @@ h1 {
|
|||||||
.date-cell {
|
.date-cell {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
padding-left: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-date::after {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-cell {
|
.topic-cell {
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in schedule %}
|
{% for item in schedule %}
|
||||||
<tr class="{% if not item.leaders %}special-event{% endif %}">
|
<tr class="{% if not item.leaders %}special-event{% endif %} {% if upcoming_week_index is not none and loop.index0 == upcoming_week_index %}upcoming-week{% endif %}">
|
||||||
<td class="date-cell">{{ item.date }}</td>
|
<td class="date-cell{% if upcoming_week_index is not none and loop.index0 == upcoming_week_index %} upcoming-date{% endif %}">{{ item.date }}</td>
|
||||||
<td class="leader-cell">{% if item.leaders %}{% if item.leaders|length > 1 %}{{ item.leaders[:-1]|join(', ') }} & {{ item.leaders[-1] }}{% else %}{{ item.leaders[0] }}{% endif %}{% endif %}</td>
|
<td class="leader-cell">{% if item.leaders %}{% if item.leaders|length > 1 %}{{ item.leaders[:-1]|join(', ') }} & {{ item.leaders[-1] }}{% else %}{{ item.leaders[0] }}{% endif %}{% endif %}</td>
|
||||||
<td class="topic-cell" {% if item.leaders %}data-leaders="Leaders: {% if item.leaders|length > 1 %}{{ item.leaders[:-1]|join(', ') }} & {{ item.leaders[-1] }}{% else %}{{ item.leaders[0] }}{% endif %}"{% endif %}>{{ item.topic }}</td>
|
<td class="topic-cell" {% if item.leaders %}data-leaders="Leaders: {% if item.leaders|length > 1 %}{{ item.leaders[:-1]|join(', ') }} & {{ item.leaders[-1] }}{% else %}{{ item.leaders[0] }}{% endif %}"{% endif %}>{{ item.topic }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user