Compare commits
9 Commits
b991d60e01
...
9b8da8c2b6
Author | SHA1 | Date | |
---|---|---|---|
9b8da8c2b6 | |||
cba66c794e | |||
05f985d611 | |||
78eeac7ab5 | |||
df427c2c52 | |||
98f9802a8e | |||
56f529f28d | |||
4af54f3287 | |||
8209049486 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ __pycache__/
|
|||||||
.idea/
|
.idea/
|
||||||
*.*swp*
|
*.*swp*
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
dist/
|
||||||
|
@ -12,10 +12,10 @@ COPY poetry.lock pyproject.toml /app/
|
|||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
RUN pip install -U poetry && \
|
RUN pip install -U poetry && \
|
||||||
poetry install --no-dev
|
poetry install --only main
|
||||||
|
|
||||||
COPY main.py /app/
|
COPY fabcal /app/fabcal/
|
||||||
COPY static/ /app/static/
|
COPY static/ /app/static/
|
||||||
COPY templates/ /app/templates/
|
COPY templates/ /app/templates/
|
||||||
|
|
||||||
ENTRYPOINT ["poetry", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000", "--use-colors"]
|
ENTRYPOINT ["poetry", "run", "uvicorn", "fabcal.app:app", "--host", "0.0.0.0", "--port", "5000", "--use-colors"]
|
||||||
|
0
fabcal/__init__.py
Normal file
0
fabcal/__init__.py
Normal file
33
fabcal/app.py
Normal file
33
fabcal/app.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import locale
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi_cache import FastAPICache
|
||||||
|
from fastapi_cache.backends.inmemory import InMemoryBackend
|
||||||
|
|
||||||
|
from fabcal.routers import api_v1, frontend
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
|
app.include_router(api_v1.router, prefix="/api/v1")
|
||||||
|
app.include_router(frontend.router, prefix="")
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=[
|
||||||
|
"https://fablab-altmuehlfranken.de",
|
||||||
|
"https://www.fablab-altmuehlfranken.de",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
locale.setlocale(locale.LC_TIME, locale.getlocale())
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup():
|
||||||
|
FastAPICache.init(InMemoryBackend())
|
@ -1,53 +1,16 @@
|
|||||||
import base64
|
|
||||||
import locale
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import date, datetime, timedelta, timezone
|
from datetime import date, datetime, timedelta, timezone
|
||||||
from typing import List, NamedTuple
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import babel.dates
|
|
||||||
import recurring_ical_events
|
import recurring_ical_events
|
||||||
|
|
||||||
from fastapi import FastAPI
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.requests import Request
|
|
||||||
from fastapi.responses import Response
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from fastapi_cache import FastAPICache
|
|
||||||
from fastapi_cache.backends.inmemory import InMemoryBackend
|
|
||||||
from fastapi_cache.decorator import cache
|
from fastapi_cache.decorator import cache
|
||||||
from icalendar import Calendar, vText
|
from icalendar import Calendar, vText
|
||||||
|
|
||||||
|
from fabcal.models import CalendarEvent
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
|
||||||
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=[
|
|
||||||
"https://fablab-altmuehlfranken.de",
|
|
||||||
"https://www.fablab-altmuehlfranken.de",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
locale.setlocale(locale.LC_TIME, locale.getlocale())
|
|
||||||
|
|
||||||
|
|
||||||
def get_calendar_url():
|
|
||||||
url = os.environ["CALENDAR_URL"]
|
|
||||||
|
|
||||||
# convenience feature
|
|
||||||
url = url.replace("webcal://", "https://")
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize(data: str):
|
def sanitize(data: str):
|
||||||
@ -60,6 +23,15 @@ def sanitize(data: str):
|
|||||||
return cal.to_ical()
|
return cal.to_ical()
|
||||||
|
|
||||||
|
|
||||||
|
def get_calendar_url():
|
||||||
|
url = os.environ["CALENDAR_URL"]
|
||||||
|
|
||||||
|
# convenience feature
|
||||||
|
url = url.replace("webcal://", "https://")
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
# caching strings works better than caching calendar objects
|
# caching strings works better than caching calendar objects
|
||||||
@cache(expire=120)
|
@cache(expire=120)
|
||||||
async def get_data() -> str:
|
async def get_data() -> str:
|
||||||
@ -71,18 +43,6 @@ async def get_data() -> str:
|
|||||||
return sanitize(await response.text())
|
return sanitize(await response.text())
|
||||||
|
|
||||||
|
|
||||||
class CalendarEvent(NamedTuple):
|
|
||||||
start: datetime
|
|
||||||
end: datetime
|
|
||||||
# just a convenience thing, we want to keep start/date as datetime and save the client from guessing this themselves
|
|
||||||
all_day_event: bool
|
|
||||||
summary: str
|
|
||||||
description: str
|
|
||||||
location: str
|
|
||||||
color: str
|
|
||||||
uid: str
|
|
||||||
|
|
||||||
|
|
||||||
def get_tzinfo():
|
def get_tzinfo():
|
||||||
return timezone(timedelta(hours=1))
|
return timezone(timedelta(hours=1))
|
||||||
|
|
||||||
@ -163,16 +123,6 @@ async def get_future_events():
|
|||||||
return future_events
|
return future_events
|
||||||
|
|
||||||
|
|
||||||
@app.get("/events.ics")
|
|
||||||
async def ics():
|
|
||||||
return Response(
|
|
||||||
await get_data(),
|
|
||||||
headers={
|
|
||||||
"content-type": "text/calendar",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def group_by_date(events: List[CalendarEvent]):
|
def group_by_date(events: List[CalendarEvent]):
|
||||||
grouped_events: OrderedDict[date, List[CalendarEvent]] = OrderedDict()
|
grouped_events: OrderedDict[date, List[CalendarEvent]] = OrderedDict()
|
||||||
|
|
||||||
@ -185,41 +135,3 @@ def group_by_date(events: List[CalendarEvent]):
|
|||||||
grouped_events[start_date].append(event)
|
grouped_events[start_date].append(event)
|
||||||
|
|
||||||
return grouped_events
|
return grouped_events
|
||||||
|
|
||||||
|
|
||||||
@app.get("/embed-sidebar.html")
|
|
||||||
async def embed(request: Request, max_width: str = None):
|
|
||||||
# await asyncio.sleep(1)
|
|
||||||
|
|
||||||
events = await get_future_events()
|
|
||||||
|
|
||||||
grouped_events = list(group_by_date(events).items())
|
|
||||||
|
|
||||||
# couple of helpers
|
|
||||||
def localized_abbreviated_month(dt: datetime):
|
|
||||||
return babel.dates.format_datetime(dt, format="%b", locale="de_DE")
|
|
||||||
|
|
||||||
# couple of helpers
|
|
||||||
def localized_abbreviated_weekday(dt: datetime):
|
|
||||||
return babel.dates.format_datetime(dt, format="%b", locale="de_DE")
|
|
||||||
|
|
||||||
def base64_encode(s: str):
|
|
||||||
return base64.b64encode(s.encode()).decode()
|
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"embed-sidebar.html",
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"grouped_events": grouped_events,
|
|
||||||
"dir": dir,
|
|
||||||
"localized_abbreviated_month": localized_abbreviated_month,
|
|
||||||
"localized_abbreviated_weekday": localized_abbreviated_weekday,
|
|
||||||
"base64_encode": base64_encode,
|
|
||||||
"max_width": max_width,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
|
||||||
async def startup():
|
|
||||||
FastAPICache.init(InMemoryBackend())
|
|
14
fabcal/models.py
Normal file
14
fabcal/models.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarEvent(NamedTuple):
|
||||||
|
start: datetime
|
||||||
|
end: datetime
|
||||||
|
# just a convenience thing, we want to keep start/date as datetime and save the client from guessing this themselves
|
||||||
|
all_day_event: bool
|
||||||
|
summary: str
|
||||||
|
description: str
|
||||||
|
location: str
|
||||||
|
color: str
|
||||||
|
uid: str
|
31
fabcal/routers/__init__.py
Normal file
31
fabcal/routers/__init__.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from jinja2 import pass_eval_context
|
||||||
|
from markupsafe import Markup, escape
|
||||||
|
from starlette.templating import Jinja2Templates
|
||||||
|
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
|
@pass_eval_context
|
||||||
|
def nl2br(eval_ctx, value):
|
||||||
|
br = "<br>\n"
|
||||||
|
|
||||||
|
if eval_ctx.autoescape:
|
||||||
|
value = escape(value)
|
||||||
|
br = Markup(br)
|
||||||
|
|
||||||
|
result = "\n\n".join(
|
||||||
|
# need to convert p to Markup again after applying re.split(...)
|
||||||
|
f"<p>{br.join(Markup(p).splitlines())}</p>"
|
||||||
|
for p in re.split(r"(?:\r\n|\r(?!\n)|\n){2,}", value)
|
||||||
|
)
|
||||||
|
|
||||||
|
if eval_ctx.autoescape:
|
||||||
|
result = Markup(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
templates.env.filters["nl2br"] = nl2br
|
17
fabcal/routers/api_v1.py
Normal file
17
fabcal/routers/api_v1.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from starlette.responses import Response
|
||||||
|
|
||||||
|
from fabcal.calendar_client import get_data
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/events.ics")
|
||||||
|
async def events_ics():
|
||||||
|
return Response(
|
||||||
|
await get_data(),
|
||||||
|
headers={
|
||||||
|
"content-type": "text/calendar",
|
||||||
|
},
|
||||||
|
)
|
60
fabcal/routers/frontend.py
Normal file
60
fabcal/routers/frontend.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import base64
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import babel.dates
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.requests import Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
from fabcal.calendar_client import get_future_events, group_by_date
|
||||||
|
from fabcal.routers import templates
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_response(request: Request, template_name: str, **additional_context):
|
||||||
|
events = await get_future_events()
|
||||||
|
|
||||||
|
grouped_events = list(group_by_date(events).items())
|
||||||
|
|
||||||
|
# couple of helpers
|
||||||
|
def localized_abbreviated_month(dt: datetime):
|
||||||
|
return babel.dates.format_datetime(dt, format="%b", locale="de_DE")
|
||||||
|
|
||||||
|
# couple of helpers
|
||||||
|
def localized_abbreviated_weekday(dt: datetime):
|
||||||
|
return babel.dates.format_datetime(dt, format="%b", locale="de_DE")
|
||||||
|
|
||||||
|
def base64_encode(s: str):
|
||||||
|
return base64.b64encode(s.encode()).decode()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"request": request,
|
||||||
|
"grouped_events": grouped_events,
|
||||||
|
"dir": dir,
|
||||||
|
"localized_abbreviated_month": localized_abbreviated_month,
|
||||||
|
"localized_abbreviated_weekday": localized_abbreviated_weekday,
|
||||||
|
"base64_encode": base64_encode,
|
||||||
|
}
|
||||||
|
|
||||||
|
context.update(additional_context)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
template_name,
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"grouped_events": grouped_events,
|
||||||
|
"dir": dir,
|
||||||
|
"localized_abbreviated_month": localized_abbreviated_month,
|
||||||
|
"localized_abbreviated_weekday": localized_abbreviated_weekday,
|
||||||
|
"base64_encode": base64_encode,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sidebar/embed.html", response_class=HTMLResponse)
|
||||||
|
async def embed_sidebar(request: Request):
|
||||||
|
return await generate_response(request, "sidebar/embed.html")
|
1261
poetry.lock
generated
1261
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,10 @@ name = "fabcal"
|
|||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Fabian Müller <fabian@fablab-altmuehlfranken.de>"]
|
authors = ["Fabian Müller <fabian@fablab-altmuehlfranken.de>"]
|
||||||
|
include = [
|
||||||
|
{ path = "static", format = ["sdist", "wheel"] },
|
||||||
|
{ path = "templates", format = ["sdist", "wheel"] },
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
python = "^3.8"
|
||||||
@ -31,4 +35,3 @@ profile = "black"
|
|||||||
line_length = 120
|
line_length = 120
|
||||||
lines_between_types = 1
|
lines_between_types = 1
|
||||||
lines_after_imports = 2
|
lines_after_imports = 2
|
||||||
lines_between_sections = 2
|
|
||||||
|
@ -13,7 +13,9 @@ body {
|
|||||||
font-size: 9pt;
|
font-size: 9pt;
|
||||||
|
|
||||||
height: auto;
|
height: auto;
|
||||||
overflow: auto
|
overflow: auto;
|
||||||
|
|
||||||
|
/*background: repeating-linear-gradient(135deg, #eee, #eee 100px, #ccc 100px, #ccc 200px);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar {
|
.calendar {
|
||||||
@ -26,7 +28,6 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
background-color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-date-date {
|
.calendar-date-date {
|
||||||
@ -36,6 +37,7 @@ body {
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
flex: 35px 0 0;
|
flex: 35px 0 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
background-color: white;
|
||||||
}
|
}
|
||||||
.calendar-date-month {
|
.calendar-date-month {
|
||||||
background-color: var(--calendar-fablab-red);
|
background-color: var(--calendar-fablab-red);
|
||||||
@ -96,10 +98,19 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.calendar-event-description p {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
.calendar-event-description p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
.calendar-event-type {
|
.calendar-event-type {
|
||||||
flex: 0 0 12px;
|
flex: 0 0 12px;
|
||||||
margin: -5px -5px -5px 0;
|
margin: -5px -5px -5px 0;
|
||||||
}
|
}
|
||||||
|
.calendar-subscription-buttons {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 960px) {
|
@media only screen and (max-width: 960px) {
|
||||||
.calendar-event {
|
.calendar-event {
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
{#- TODO: replace with locally served files #}
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', path='style.css') }}" type="text/css"/>
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', path='roboto.css') }}" type="text/css"/>
|
|
||||||
<title>Embeddable calendar</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body{% if max_width %} style="max-width: {{ max_width }}"{% endif %}>
|
|
||||||
<div class="calendar">
|
|
||||||
{% for start_date, events in grouped_events[:3] %}
|
|
||||||
<div class="calendar-date">
|
|
||||||
<div class="calendar-date-date">
|
|
||||||
<div class="calendar-date-month">
|
|
||||||
{{ start_date.strftime("%b") }}
|
|
||||||
</div>
|
|
||||||
<div class="calendar-date-day">
|
|
||||||
{{ start_date.strftime("%d") }}
|
|
||||||
</div>
|
|
||||||
<div class="calendar-date-weekday">
|
|
||||||
{{ start_date.strftime("%a") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="calendar-events">
|
|
||||||
{% for event in events %}
|
|
||||||
<div class="calendar-event" title="{{ event.summary }}{% if event.description %} — {{ event.description }}{% endif %}">
|
|
||||||
<div class="calendar-event-time">
|
|
||||||
<div class="calendar-event-starttime">{{ event.start.strftime("%H:%M") }}</div>
|
|
||||||
<div class="calendar-event-timesep"></div>
|
|
||||||
<div class="calendar-event-endtime">{{ event.end.strftime("%H:%M") }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="calendar-event-description">
|
|
||||||
{% if event.description or event.location %}
|
|
||||||
<details class="calendar-event-details">
|
|
||||||
<summary>{{ event.summary }}</summary>
|
|
||||||
<div class="calendar-popover-content">
|
|
||||||
{% if event.description %}
|
|
||||||
<div class="calendar-popover-entry">
|
|
||||||
{#- description doesn't need an icon, just wastes space #}
|
|
||||||
<div class="calendar-popover-entry-text">{{ event.description }}</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if event.location %}
|
|
||||||
<div class="calendar-popover-entry">
|
|
||||||
<div class="calendar-popover-entry-icon"><i class="fa-solid fa-location-dot"></i></div>
|
|
||||||
<div class="calendar-popover-entry-text">{{ event.location }}</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
{% else %}
|
|
||||||
<span>{{ event.summary }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="calendar-event-type" style="background-color: {{ event.color }};"></div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
21
templates/sidebar/base.html
Normal file
21
templates/sidebar/base.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
{% block header %}
|
||||||
|
{#- TODO: replace with locally served files #}
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', path='style.css') }}" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', path='roboto.css') }}" type="text/css"/>
|
||||||
|
|
||||||
|
<title>Embeddable calendar</title>
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% block body %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
21
templates/sidebar/embed.html
Normal file
21
templates/sidebar/embed.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
{#- TODO: replace with locally served files #}
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', path='style.css') }}" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', path='roboto.css') }}" type="text/css"/>
|
||||||
|
<title>Embeddable calendar</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="calendar">
|
||||||
|
{% include "sidebar/includes/header.html" %}
|
||||||
|
{% include "sidebar/includes/events-list.html" %}
|
||||||
|
{% include "sidebar/includes/footer.html" %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
57
templates/sidebar/includes/events-list.html
Normal file
57
templates/sidebar/includes/events-list.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{% for start_date, events in grouped_events %}
|
||||||
|
<div class="calendar-date">
|
||||||
|
<div class="calendar-date-date">
|
||||||
|
<div class="calendar-date-month">
|
||||||
|
{{ start_date.strftime("%b") }}
|
||||||
|
</div>
|
||||||
|
<div class="calendar-date-day">
|
||||||
|
{{ start_date.strftime("%d") }}
|
||||||
|
</div>
|
||||||
|
<div class="calendar-date-weekday">
|
||||||
|
{{ start_date.strftime("%a") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-events">
|
||||||
|
{% for event in events %}
|
||||||
|
<div class="calendar-event" title="{{ event.summary }}{% if event.description %} — {{ event.description }}{% endif %}">
|
||||||
|
<div class="calendar-event-time">
|
||||||
|
<div class="calendar-event-starttime">{{ event.start.strftime("%H:%M") }}</div>
|
||||||
|
<div class="calendar-event-timesep"></div>
|
||||||
|
<div class="calendar-event-endtime">{{ event.end.strftime("%H:%M") }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-event-description">
|
||||||
|
{% if event.description or event.location %}
|
||||||
|
<details class="calendar-event-details">
|
||||||
|
<summary>{{ event.summary | urlize }}</summary>
|
||||||
|
<div class="calendar-popover-content">
|
||||||
|
{% if event.description %}
|
||||||
|
<div class="calendar-popover-entry">
|
||||||
|
{#- description doesn't need an icon, just wastes space #}
|
||||||
|
<div class="calendar-popover-entry-text">{{ event.description | urlize | nl2br }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if event.location %}
|
||||||
|
<div class="calendar-popover-entry">
|
||||||
|
<div class="calendar-popover-entry-icon"><i class="fa-solid fa-location-dot"></i></div>
|
||||||
|
<div class="calendar-popover-entry-text">{{ event.location | urlize | nl2br }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{% else %}
|
||||||
|
<span>{{ event.summary }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{%- if "AusbauLab" in event.categories %}
|
||||||
|
<div class="calendar-event-type calendar-event-type-ausbau"></div>
|
||||||
|
{%- elif event.color %}
|
||||||
|
<div class="calendar-event-type" style="background-color: {{ event.color }};"></div>
|
||||||
|
{%- else %}
|
||||||
|
<div class="calendar-event-type calendar-event-type-unknown"></div>
|
||||||
|
{%- endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
3
templates/sidebar/includes/footer.html
Normal file
3
templates/sidebar/includes/footer.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div class="calendar-subscription-buttons">
|
||||||
|
<a href="{{ url_for('events_ics') }}"><i class="fa-solid fa-calendar"></i> Kalender abonnieren</a>
|
||||||
|
</div>
|
1
templates/sidebar/includes/header.html
Normal file
1
templates/sidebar/includes/header.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<h3>Veranstaltungen</h3>
|
Loading…
x
Reference in New Issue
Block a user