parent
3dd322475f
commit
0e769c3de6
|
@ -2,78 +2,106 @@
|
||||||
|
|
||||||
import typing
|
import typing
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import uuid
|
||||||
|
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
|
||||||
event_type_color_map = {
|
# A map to associate event types with a specific calendar ID
|
||||||
"bank_holiday": "success-subtle",
|
event_type_calendar_map = {
|
||||||
"conference": "primary-subtle",
|
"bank_holiday": "uk_holidays",
|
||||||
"us_holiday": "secondary-subtle",
|
"conference": "conferences",
|
||||||
"birthday": "info-subtle",
|
"us_holiday": "us_holidays",
|
||||||
"waste_schedule": "danger-subtle",
|
"birthday": "birthdays",
|
||||||
|
"waste_schedule": "home",
|
||||||
|
"accommodation": "travel",
|
||||||
|
"market": "markets",
|
||||||
}
|
}
|
||||||
|
|
||||||
colors = {
|
# Define the calendars (categories) for TOAST UI
|
||||||
"primary-subtle": "#cfe2ff",
|
# These will be passed to the frontend to configure colors and names
|
||||||
"secondary-subtle": "#e2e3e5",
|
toastui_calendars = [
|
||||||
"success-subtle": "#d1e7dd",
|
{"id": "default", "name": "General", "backgroundColor": "#00a9ff"},
|
||||||
"info-subtle": "#cff4fc",
|
{"id": "uk_holidays", "name": "UK Bank Holiday", "backgroundColor": "#28a745"},
|
||||||
"warning-subtle": "#fff3cd",
|
{"id": "us_holidays", "name": "US Holiday", "backgroundColor": "#6c757d"},
|
||||||
"danger-subtle": "#f8d7da",
|
{"id": "conferences", "name": "Conference", "backgroundColor": "#007bff"},
|
||||||
}
|
{"id": "birthdays", "name": "Birthday", "backgroundColor": "#17a2b8"},
|
||||||
|
{"id": "home", "name": "Home", "backgroundColor": "#dc3545"},
|
||||||
|
{"id": "travel", "name": "Travel", "backgroundColor": "#ffc107", "color": "#000"},
|
||||||
|
{"id": "markets", "name": "Markets", "backgroundColor": "#e2e3e5", "color": "#000"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def build_events(events: list[Event]) -> list[dict[str, typing.Any]]:
|
def build_toastui_events(events: list[Event]) -> list[dict[str, typing.Any]]:
|
||||||
"""Build list of events for FullCalendar."""
|
"""Build a list of event objects for TOAST UI Calendar."""
|
||||||
items: list[dict[str, typing.Any]] = []
|
items: list[dict[str, typing.Any]] = []
|
||||||
|
|
||||||
one_day = timedelta(days=1)
|
one_day = timedelta(days=1)
|
||||||
|
|
||||||
for e in events:
|
for e in events:
|
||||||
if e.name == "today":
|
if e.name == "today":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Determine the calendar ID for the event, defaulting if not mapped
|
||||||
|
calendar_id = event_type_calendar_map.get(e.name, "default")
|
||||||
|
|
||||||
|
# Handle special case for 'accommodation'
|
||||||
if e.name == "accommodation":
|
if e.name == "accommodation":
|
||||||
assert e.title and e.end_date
|
assert e.title and e.end_date
|
||||||
item = {
|
# All-day event for the duration of the stay
|
||||||
"allDay": True,
|
items.append(
|
||||||
"title": e.title_with_emoji,
|
{
|
||||||
"start": e.as_date.isoformat(),
|
"id": str(uuid.uuid4()),
|
||||||
"end": (e.end_as_date + one_day).isoformat(),
|
"calendarId": calendar_id,
|
||||||
"url": e.url,
|
"title": e.title_with_emoji,
|
||||||
}
|
"start": e.as_date.isoformat(),
|
||||||
items.append(item)
|
"end": (e.end_as_date + one_day).isoformat(),
|
||||||
|
"isAllday": True,
|
||||||
item = {
|
"raw": {"url": e.url},
|
||||||
"allDay": False,
|
}
|
||||||
"title": "check-in: " + e.title,
|
)
|
||||||
"start": e.date.isoformat(),
|
# Timed event for check-in
|
||||||
"url": e.url,
|
items.append(
|
||||||
}
|
{
|
||||||
items.append(item)
|
"id": str(uuid.uuid4()),
|
||||||
item = {
|
"calendarId": calendar_id,
|
||||||
"allDay": False,
|
"title": f"Check-in: {e.title}",
|
||||||
"title": "checkout: " + e.title,
|
"start": e.date.isoformat(),
|
||||||
"start": e.end_date.isoformat(),
|
"end": (e.date + timedelta(hours=1)).isoformat(),
|
||||||
"url": e.url,
|
"isAllday": False,
|
||||||
}
|
"raw": {"url": e.url},
|
||||||
items.append(item)
|
}
|
||||||
|
)
|
||||||
|
# Timed event for check-out
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"calendarId": calendar_id,
|
||||||
|
"title": f"Checkout: {e.title}",
|
||||||
|
"start": e.end_date.isoformat(),
|
||||||
|
"end": (e.end_date + timedelta(hours=1)).isoformat(),
|
||||||
|
"isAllday": False,
|
||||||
|
"raw": {"url": e.url},
|
||||||
|
}
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Handle all other events
|
||||||
|
start_iso = e.date.isoformat()
|
||||||
if e.has_time:
|
if e.has_time:
|
||||||
end = e.end_date or e.date + timedelta(minutes=30)
|
end_iso = (e.end_date or e.date + timedelta(minutes=30)).isoformat()
|
||||||
else:
|
else:
|
||||||
end = (e.end_as_date if e.end_date else e.as_date) + one_day
|
end_date = (e.end_as_date if e.end_date else e.as_date) + one_day
|
||||||
|
end_iso = end_date.isoformat()
|
||||||
|
|
||||||
item = {
|
item = {
|
||||||
"allDay": not e.has_time,
|
"id": str(uuid.uuid4()),
|
||||||
|
"calendarId": calendar_id,
|
||||||
"title": e.title_with_emoji,
|
"title": e.title_with_emoji,
|
||||||
"start": e.date.isoformat(),
|
"start": start_iso,
|
||||||
"end": end.isoformat(),
|
"end": end_iso,
|
||||||
|
"isAllday": not e.has_time,
|
||||||
}
|
}
|
||||||
if e.name in event_type_color_map:
|
|
||||||
item["color"] = colors[event_type_color_map[e.name]]
|
|
||||||
item["textColor"] = "black"
|
|
||||||
if e.url:
|
if e.url:
|
||||||
item["url"] = e.url
|
item["raw"] = {"url": e.url}
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
|
@ -7,69 +7,18 @@
|
||||||
<link href="{{ url_for("static", filename="bootstrap5/css/bootstrap.min.css") }}" rel="stylesheet">
|
<link href="{{ url_for("static", filename="bootstrap5/css/bootstrap.min.css") }}" rel="stylesheet">
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📅</text></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📅</text></svg>">
|
||||||
|
|
||||||
<script async src="{{ url_for("static", filename="es-module-shims/es-module-shims.js") }}"></script>
|
<!-- TOAST UI Calendar CSS -->
|
||||||
|
<link rel="stylesheet" href="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css" />
|
||||||
<script type='importmap'>
|
<style>
|
||||||
{
|
/* Custom styles to better integrate with Bootstrap */
|
||||||
"imports": {
|
.toastui-calendar-layout {
|
||||||
"@fullcalendar/core": "https://cdn.skypack.dev/@fullcalendar/core@6.1.9",
|
border: 1px solid #dee2e6;
|
||||||
"@fullcalendar/daygrid": "https://cdn.skypack.dev/@fullcalendar/daygrid@6.1.9",
|
border-radius: 0.375rem;
|
||||||
"@fullcalendar/timegrid": "https://cdn.skypack.dev/@fullcalendar/timegrid@6.1.9",
|
}
|
||||||
"@fullcalendar/list": "https://cdn.skypack.dev/@fullcalendar/list@6.1.9",
|
#calendar-header {
|
||||||
"@fullcalendar/core/locales/en-gb": "https://cdn.skypack.dev/@fullcalendar/core@6.1.9/locales/en-gb"
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
}
|
</style>
|
||||||
</script>
|
|
||||||
<script type='module'>
|
|
||||||
import { Calendar } from '@fullcalendar/core'
|
|
||||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
|
||||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
|
||||||
import listPlugin from '@fullcalendar/list'
|
|
||||||
import gbLocale from '@fullcalendar/core/locales/en-gb';
|
|
||||||
|
|
||||||
// Function to save the current view to local storage
|
|
||||||
function saveView(view) {
|
|
||||||
localStorage.setItem('fullCalendarDefaultView', view);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to get the saved view from local storage
|
|
||||||
function getSavedView() {
|
|
||||||
return localStorage.getItem('fullCalendarDefaultView') || 'dayGridMonth';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const calendarEl = document.getElementById('calendar')
|
|
||||||
const calendar = new Calendar(calendarEl, {
|
|
||||||
locale: gbLocale,
|
|
||||||
plugins: [dayGridPlugin, timeGridPlugin, listPlugin ],
|
|
||||||
themeSystem: 'bootstrap5',
|
|
||||||
firstDay: 1,
|
|
||||||
initialView: getSavedView(),
|
|
||||||
viewDidMount: function(info) {
|
|
||||||
saveView(info.view.type);
|
|
||||||
},
|
|
||||||
headerToolbar: {
|
|
||||||
left: 'prev,next today',
|
|
||||||
center: 'title',
|
|
||||||
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
|
||||||
},
|
|
||||||
nowIndicator: true,
|
|
||||||
weekNumbers: true,
|
|
||||||
eventTimeFormat: {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
},
|
|
||||||
events: {{ fullcalendar_events | tojson(indent=2) }},
|
|
||||||
eventDidMount: function(info) {
|
|
||||||
info.el.title = info.event.title;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
calendar.render()
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
{% set event_labels = {
|
{% set event_labels = {
|
||||||
|
@ -91,16 +40,6 @@
|
||||||
}
|
}
|
||||||
%}
|
%}
|
||||||
|
|
||||||
{%set class_map = {
|
|
||||||
"bank_holiday": "bg-success-subtle",
|
|
||||||
"conference": "bg-primary-subtle",
|
|
||||||
"us_holiday": "bg-secondary-subtle",
|
|
||||||
"birthday": "bg-info-subtle",
|
|
||||||
"waste_schedule": "bg-danger-subtle",
|
|
||||||
} %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% from "navbar.html" import navbar with context %}
|
{% from "navbar.html" import navbar with context %}
|
||||||
<body>
|
<body>
|
||||||
{{ navbar() }}
|
{{ navbar() }}
|
||||||
|
@ -108,7 +47,7 @@
|
||||||
<div class="container-fluid mt-2">
|
<div class="container-fluid mt-2">
|
||||||
<h1>Agenda</h1>
|
<h1>Agenda</h1>
|
||||||
<p>
|
<p>
|
||||||
<a href="/tools">← personal tools</a>
|
<a href="/tools">← personal tools</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if errors %}
|
{% if errors %}
|
||||||
|
@ -126,8 +65,25 @@
|
||||||
| <a href="{{ url_for(request.endpoint, markets="hide") }}">Hide all</a>
|
| <a href="{{ url_for(request.endpoint, markets="hide") }}">Hide all</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3" id="calendar"></div>
|
<!-- Header for calendar controls -->
|
||||||
|
<div id="calendar-header" class="d-flex justify-content-between align-items-center mt-3">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-primary" id="prevBtn">Prev</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="nextBtn">Next</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="todayBtn">Today</button>
|
||||||
|
</div>
|
||||||
|
<h3 id="calendar-title" class="mb-0"></h3>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="monthViewBtn">Month</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="weekViewBtn">Week</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="dayViewBtn">Day</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mb-3" id="calendar" style="height: 80vh;"></div>
|
||||||
|
|
||||||
|
{#
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<h5>Page generation time</h5>
|
<h5>Page generation time</h5>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -137,11 +93,137 @@
|
||||||
{% for name, seconds in timings %}
|
{% for name, seconds in timings %}
|
||||||
<li>{{ name }} took {{ "%.1f" | format(seconds) }} seconds</li>
|
<li>{{ name }} took {{ "%.1f" | format(seconds) }} seconds</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
#}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- TOAST UI Calendar JS -->
|
||||||
|
<script src="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js"></script>
|
||||||
<script src="{{ url_for("static", filename="bootstrap5/js/bootstrap.bundle.min.js") }}"></script>
|
<script src="{{ url_for("static", filename="bootstrap5/js/bootstrap.bundle.min.js") }}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const Calendar = tui.Calendar;
|
||||||
|
const container = document.getElementById('calendar');
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
const calendars = {{ toastui_calendars | tojson }};
|
||||||
|
const events = {{ toastui_events | tojson }};
|
||||||
|
|
||||||
|
function getSavedView() {
|
||||||
|
return localStorage.getItem('toastUiCalendarDefaultView') || 'month';
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveView(view) {
|
||||||
|
localStorage.setItem('toastUiCalendarDefaultView', view);
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
defaultView: getSavedView(),
|
||||||
|
useDetailPopup: true,
|
||||||
|
useCreationPopup: false,
|
||||||
|
calendars: calendars,
|
||||||
|
week: {
|
||||||
|
startDayOfWeek: 1, // Monday
|
||||||
|
taskView: false,
|
||||||
|
eventView: ['time'],
|
||||||
|
showNowIndicator: true,
|
||||||
|
},
|
||||||
|
month: {
|
||||||
|
startDayOfWeek: 1, // Monday
|
||||||
|
// We've removed `visibleWeeksCount: 6` to allow the calendar
|
||||||
|
// to dynamically show 4, 5, or 6 weeks as needed.
|
||||||
|
},
|
||||||
|
timezone: {
|
||||||
|
zones: [{
|
||||||
|
timezoneName: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
allday(event) {
|
||||||
|
return `<span title="${event.title}">${event.title}</span>`;
|
||||||
|
},
|
||||||
|
time(event) {
|
||||||
|
return `<span title="${event.title}">${event.title}</span>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const calendar = new Calendar(container, options);
|
||||||
|
calendar.createEvents(events);
|
||||||
|
|
||||||
|
// --- Event Handlers ---
|
||||||
|
calendar.on('clickEvent', ({ event }) => {
|
||||||
|
if (event.raw && event.raw.url) {
|
||||||
|
window.open(event.raw.url, '_blank');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.on('afterRenderEvent', () => {
|
||||||
|
const currentView = calendar.getViewName();
|
||||||
|
saveView(currentView);
|
||||||
|
updateCalendarTitle();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- UI Control Logic ---
|
||||||
|
const prevBtn = document.getElementById('prevBtn');
|
||||||
|
const nextBtn = document.getElementById('nextBtn');
|
||||||
|
const todayBtn = document.getElementById('todayBtn');
|
||||||
|
const monthViewBtn = document.getElementById('monthViewBtn');
|
||||||
|
const weekViewBtn = document.getElementById('weekViewBtn');
|
||||||
|
const dayViewBtn = document.getElementById('dayViewBtn');
|
||||||
|
const calendarTitle = document.getElementById('calendar-title');
|
||||||
|
|
||||||
|
function updateCalendarTitle() {
|
||||||
|
const tzDate = calendar.getDate();
|
||||||
|
const nativeDate = tzDate.toDate();
|
||||||
|
|
||||||
|
const year = nativeDate.getFullYear();
|
||||||
|
const monthName = nativeDate.toLocaleDateString('en-GB', { month: 'long' });
|
||||||
|
|
||||||
|
calendarTitle.textContent = `${monthName} ${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CORRECTED NAVIGATION LOGIC ---
|
||||||
|
prevBtn.addEventListener('click', () => {
|
||||||
|
const currentDate = calendar.getDate().toDate();
|
||||||
|
// Go to the previous month
|
||||||
|
currentDate.setMonth(currentDate.getMonth() - 1);
|
||||||
|
// **Crucially, set the day to the 1st to align the view**
|
||||||
|
currentDate.setDate(1);
|
||||||
|
calendar.setDate(currentDate);
|
||||||
|
updateCalendarTitle();
|
||||||
|
});
|
||||||
|
|
||||||
|
nextBtn.addEventListener('click', () => {
|
||||||
|
const currentDate = calendar.getDate().toDate();
|
||||||
|
// Go to the next month
|
||||||
|
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||||
|
// **Crucially, set the day to the 1st to align the view**
|
||||||
|
currentDate.setDate(1);
|
||||||
|
calendar.setDate(currentDate);
|
||||||
|
updateCalendarTitle();
|
||||||
|
});
|
||||||
|
|
||||||
|
todayBtn.addEventListener('click', () => {
|
||||||
|
calendar.today();
|
||||||
|
updateCalendarTitle();
|
||||||
|
});
|
||||||
|
|
||||||
|
monthViewBtn.addEventListener('click', () => calendar.changeView('month'));
|
||||||
|
weekViewBtn.addEventListener('click', () => calendar.changeView('week'));
|
||||||
|
dayViewBtn.addEventListener('click', () => calendar.changeView('day'));
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
updateCalendarTitle();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -107,7 +107,6 @@ async def index() -> str:
|
||||||
events=events,
|
events=events,
|
||||||
get_country=agenda.get_country,
|
get_country=agenda.get_country,
|
||||||
current_trip=get_current_trip(today),
|
current_trip=get_current_trip(today),
|
||||||
fullcalendar_events=calendar.build_events(events),
|
|
||||||
start_event_list=date.today() - timedelta(days=1),
|
start_event_list=date.today() - timedelta(days=1),
|
||||||
end_event_list=date.today() + timedelta(days=365 * 2),
|
end_event_list=date.today() + timedelta(days=365 * 2),
|
||||||
render_time=(time.time() - t0),
|
render_time=(time.time() - t0),
|
||||||
|
@ -129,11 +128,13 @@ async def calendar_page() -> str:
|
||||||
if markets_arg != "show":
|
if markets_arg != "show":
|
||||||
agenda.data.hide_markets_while_away(events, data["accommodation_events"])
|
agenda.data.hide_markets_while_away(events, data["accommodation_events"])
|
||||||
|
|
||||||
|
# Use the new function to build events and pass both calendars and events
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"calendar.html",
|
"calendar.html",
|
||||||
today=now.date(),
|
today=now.date(),
|
||||||
events=events,
|
events=events,
|
||||||
fullcalendar_events=calendar.build_events(events),
|
toastui_events=calendar.build_toastui_events(events),
|
||||||
|
toastui_calendars=calendar.toastui_calendars,
|
||||||
**data,
|
**data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -374,6 +375,7 @@ def past_conference_list() -> str:
|
||||||
fx_rate=agenda.fx.get_rates(app.config),
|
fx_rate=agenda.fx.get_rates(app.config),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/accommodation")
|
@app.route("/accommodation")
|
||||||
def accommodation_list() -> str:
|
def accommodation_list() -> str:
|
||||||
"""Page showing a list of past, present and future accommodation."""
|
"""Page showing a list of past, present and future accommodation."""
|
||||||
|
|
Loading…
Reference in a new issue