initial development version
This commit is contained in:
commit
d7d86601f0
53
README.md
Normal file
53
README.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Show upcoming appointments from church.tools instance
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
* Upload `css/ct-events.css` and `js/ct-events.js` to your site
|
||||||
|
* Download `mithril.min.js` from https://unpkg.com/browse/mithril@2.2.11/ (or a newer version, but tested with this)
|
||||||
|
* Upload `mithril.min.js` to the same folder as `ct-events.js`
|
||||||
|
* Download `fontawesome-free-6.7.1-web.zip` from https://fontawesome.com/download ("Free for Web"), tested with version 6.7.1
|
||||||
|
* Extract and upload `css/all.min.css` and `webfonts/fa-solid-900*`; the CSS expects the font to be in `../webfonts/`, so keep the folder hierarchy.
|
||||||
|
* Add the following to your site template header (adapt paths to your local setup), i.e. in `<head>...</head>`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/ct-events.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/all.min.css">
|
||||||
|
<link rel="modulepreload" href="/js/ct-events.js" />
|
||||||
|
<link rel="modulepreload" href="/js/mithril.min.js" />
|
||||||
|
```
|
||||||
|
|
||||||
|
* Add the following to your site template footer (adapt paths to your local setup), i.e. at the end of `<body>...</body>`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="module">
|
||||||
|
import { load_all_events } from "/js/ct-events.js";
|
||||||
|
await load_all_events({
|
||||||
|
// site default options
|
||||||
|
instance: "https://your-church-tools-instance",
|
||||||
|
default_image: "/img/welcome.jpg",
|
||||||
|
default_image_mobile: "/img/welcome-mobile.jpg",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Allow church-tools API CORS from your site
|
||||||
|
|
||||||
|
Add your site (`https://your-site`) to `Access-Control-Allow-Origins` sent by your church-tool instance, see: <https://churchtools.academy/de/help/system-einstellungen/api/0-cors/>
|
||||||
|
|
||||||
|
## Add calendar events
|
||||||
|
|
||||||
|
Add this snippet to your page:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div
|
||||||
|
class="cte-events"
|
||||||
|
style="max-width: 1100px;"
|
||||||
|
data-cte-calendar-regex="Erwachsene|Chor"
|
||||||
|
data-cte-event-regex="Ausrufezeichen|Chor AZ"
|
||||||
|
data-cte-limit="5"
|
||||||
|
></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The class `cte-events` triggers the javascript; options can be overridden and set via `data-cte-*` attributes.
|
||||||
|
|
||||||
|
You can add multiple snippets on a page with different settings.
|
168
css/ct-events.css
Normal file
168
css/ct-events.css
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
:root {
|
||||||
|
--cte-bg-color: #f9e3c8;
|
||||||
|
--cte-primary-color: #557e76;
|
||||||
|
--cte-header-color: #525f7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events {
|
||||||
|
background-color: var(--cte-bg-color);
|
||||||
|
font-family: roboto, sans-serif;
|
||||||
|
margin: 30px auto;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events .cte-event {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 3em;
|
||||||
|
}
|
||||||
|
.cte-events /* .cte-event */ .cte-summary {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
grid-template-columns: 1fr 4fr;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-template-areas: "image info";
|
||||||
|
|
||||||
|
/* position / z-index needed to have box-shadow over details collapsible */
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .15);
|
||||||
|
word-wrap: break-word;
|
||||||
|
background-color: #fff;
|
||||||
|
background-clip: border-box;
|
||||||
|
border-radius: .375rem;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.cte-events /* .cte-event .cte-summary */ .cte-image {
|
||||||
|
grid-area: image
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.cte-events /* .cte-event .cte-summary */ .cte-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
.cte-events /* .cte-event .cte-summary */ .cte-info {
|
||||||
|
grid-area: info
|
||||||
|
}
|
||||||
|
.cte-events /* .cte-event .cte-summary .cte-info */ .cte-info-header {
|
||||||
|
color: var(--cte-header-color);
|
||||||
|
fill: var(--cte-header-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
gap: .5rem;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.cte-events /* .cte-event .cte-summary .cte-info .cte-info-header */ .cte-date {
|
||||||
|
}
|
||||||
|
.cte-events /* .cte-event .cte-summary .cte-info .cte-info-header */ .cte-download {}
|
||||||
|
.cte-events /* .cte-event .cte-summary .cte-info .cte-info-header */ .cte-download a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.cte-events /* .cte-event .cte-summary .cte-info */ .cte-description {
|
||||||
|
color: var(--cte-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events /* .cte-event .cte-summary .cte-info */ .cte-description :is(h1, h2) {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events /* .cte-event .cte-summary .cte-info */ .cte-description h2 {
|
||||||
|
font-size: 1.0625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events .cte-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events /* .cte-event */ .cte-details-collapser {
|
||||||
|
margin: 0 1rem;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events /* .cte-event */ .cte-details-collapser.collapsed {
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events /* .cte-event */ .cte-details-collapser.collapsed .cte-details {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events /* .cte-event */ .cte-details-collapser .toggle-collapse {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events /* .cte-event */ .cte-details-collapser .toggle-collapse {
|
||||||
|
height: 1.875rem;
|
||||||
|
width: 13.25rem;
|
||||||
|
color: var(--cte-primary-color);
|
||||||
|
fill: var(--cte-primary-color);
|
||||||
|
border-color: var(--cte-primary-color);
|
||||||
|
background-color: var(--cte-bg-color);
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: .025em;
|
||||||
|
font-size: .875rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: .375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events /* .cte-event */ .cte-details-collapser.collapsed .toggle-collapse {
|
||||||
|
bottom: -1.775rem;
|
||||||
|
border-top: none;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
.cte-events /* .cte-event */ .cte-details-collapser:not(.collapsed) .toggle-collapse {
|
||||||
|
bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.cte-events /* .cte-event */ .cte-details-collapser .toggle-collapse:hover {
|
||||||
|
background-color: var(--cte-primary-color);
|
||||||
|
color: #fff;
|
||||||
|
fill: #fff;
|
||||||
|
border-color: var(--cte-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events /* .cte-event .cte-details-collapser */ .cte-details {
|
||||||
|
padding: 1.5rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events /* .cte-event .cte-details-collapser */ .cte-details .cte-information-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: first baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cte-events /* .cte-event .cte-details-collapser */ .cte-details :is(h1, h2, h3) {
|
||||||
|
color: var(--cte-primary-color);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.cte-events /* .cte-event .cte-details-collapser */ .cte-details h2 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-top: .8rem;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
28
example.html
Normal file
28
example.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/ct-events.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/all.min.css"><!-- load fontawesome -->
|
||||||
|
<link rel="modulepreload" href="js/ct-events.js" />
|
||||||
|
<link rel="modulepreload" href="js/mithril.min.js" />
|
||||||
|
</head>
|
||||||
|
<body style="background-color: var(--cte-bg-color);">
|
||||||
|
<div
|
||||||
|
class="cte-events"
|
||||||
|
style="max-width: 1100px;"
|
||||||
|
data-cte-calendar-regex="Erwachsene|Chor"
|
||||||
|
data-cte-event-regex="Ausrufezeichen|Chor AZ"
|
||||||
|
data-cte-limit="5"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<script type="module">
|
||||||
|
import { load_all_events } from "./js/ct-events.js";
|
||||||
|
await load_all_events({
|
||||||
|
instance: "https://elkw2808.krz.tools",
|
||||||
|
default_image: "https://maierbn.github.io/churchtools_custom_calendar/img/welcome.jpg",
|
||||||
|
default_image_mobile: "https://maierbn.github.io/churchtools_custom_calendar/img/calendar.jpg",
|
||||||
|
// fontawesome_svg_path: "img",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
425
js/ct-events.js
Normal file
425
js/ct-events.js
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
import "./mithril.min.js";
|
||||||
|
|
||||||
|
function fa_solid_icon(settings, name) {
|
||||||
|
if (settings.fontawesome_svg_path) {
|
||||||
|
// modify upstream svg to start with `<svg id="root" ...>`
|
||||||
|
return m("svg.cte-icon", m("use", {href: `${settings.fontawesome_svg_path}/${name}-solid.svg#root`}));
|
||||||
|
} else {
|
||||||
|
return m(`i.fa-solid.fa-${name}.cte-icon`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
var AppointmentImage = {
|
||||||
|
view: function(vnode) {
|
||||||
|
const {item, settings} = vnode.attrs;
|
||||||
|
const image_url = item.base.image?.fileUrl ?? (is_mobile ? default_image_mobile : null) ?? settings.default_image;
|
||||||
|
|
||||||
|
return m(".cte-image",
|
||||||
|
image_url ? m(
|
||||||
|
"img",
|
||||||
|
{
|
||||||
|
src: image_url,
|
||||||
|
alt: item.base.caption,
|
||||||
|
},
|
||||||
|
) : null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options for date and time formatting
|
||||||
|
const fmtoptions_date = { weekday: 'short', day: 'numeric', month: 'long', year: 'numeric', timeZone: 'Europe/Berlin' };
|
||||||
|
const fmtoptions_date_short = { day: 'numeric', month: 'numeric', year: 'numeric', timeZone: 'Europe/Berlin' };
|
||||||
|
const fmtoptions_date_utc = { weekday: 'short', day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' };
|
||||||
|
const fmtoptions_time = { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Berlin' };
|
||||||
|
const fmt_date = new Intl.DateTimeFormat('de-DE', fmtoptions_date);
|
||||||
|
const fmt_date_short = new Intl.DateTimeFormat('de-DE', fmtoptions_date_short);
|
||||||
|
const fmt_date_utc = new Intl.DateTimeFormat('de-DE', fmtoptions_date_utc);
|
||||||
|
const fmt_time = new Intl.DateTimeFormat('de-DE', fmtoptions_time);
|
||||||
|
|
||||||
|
function display_date_utc(date, content) {
|
||||||
|
const repr = date.toISOString().split("T")[0];
|
||||||
|
return m("time", {datetime: repr}, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function display_date_local(date, content) {
|
||||||
|
const [d, m, y] = fmtoptions_date_short.format(date).split(".");
|
||||||
|
const repr = [y, m, d].join("-");
|
||||||
|
return m("time", {datetime: repr}, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function display_datetime(date, content) {
|
||||||
|
const repr = date.toISOString();
|
||||||
|
return m("time", {datetime: repr}, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function display_event_date(item) {
|
||||||
|
const start = new Date(item.calculated.startDate);
|
||||||
|
const end = new Date(item.calculated.endDate);
|
||||||
|
|
||||||
|
const start_date = fmt_date.format(start);
|
||||||
|
const end_date = fmt_date.format(end);
|
||||||
|
const start_time = fmt_time.format(start);
|
||||||
|
const end_time = fmt_time.format(end);
|
||||||
|
|
||||||
|
if (start === end) {
|
||||||
|
return display_datetime(start, `${start_date}, ${start_time}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start_time === "00:00" && end_time === "00:00") {
|
||||||
|
// localtime "all-day" event
|
||||||
|
const end_day = new Date(end.valueOf() - 86400);
|
||||||
|
const end_day_date = fmt_date.format(end_day);
|
||||||
|
if (start_date === end_day_date) {
|
||||||
|
return display_date_local(start, start_date);
|
||||||
|
} else {
|
||||||
|
return m("span.timerange", display_date_local(start, start_date), " - ", display_date_local(end_day, end_day_date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start.getUTCHours() === 0 && start.getUTCMinutes() === 0 && end.getUTCHours() === 0 && end.getUTCMinutes() === 0) {
|
||||||
|
// UTC "all-day" event
|
||||||
|
const end_day = new Date(end.valueOf() - 86400);
|
||||||
|
const utc_start_date = fmt_date_utc.format(start);
|
||||||
|
const utc_end_day_date = fmt_date_utc.format(end_day);
|
||||||
|
if (utc_start_date === utc_end_day_date) {
|
||||||
|
return display_date_utc(start, utc_start_date);
|
||||||
|
} else {
|
||||||
|
return m("span.timerange", display_date_utc(start, utc_start_date), " - ", display_date_utc(end_day, utc_end_day_date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start_date === end_date) {
|
||||||
|
return m("span.timerange", display_datetime(start, `${start_date}, ${start_time}`), " - ", display_datetime(end, end_time));
|
||||||
|
} else {
|
||||||
|
return m(
|
||||||
|
"span.timerange",
|
||||||
|
display_datetime(start, `${start_date}, ${start_time}`),
|
||||||
|
" - ",
|
||||||
|
display_datetime(end, `${end_date}, ${end_time}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeICSField(text) {
|
||||||
|
return text.replace(/\\/g, "\\\\")
|
||||||
|
.replace(/[,;]/g, "\\$&")
|
||||||
|
.replace(/\n/g, "\\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createICSLink(item, settings) {
|
||||||
|
const start = new Date(item.calculated.startDate).toISOString().replace(/-|:|\.\d+/g, '');
|
||||||
|
const end = new Date(item.calculated.endDate).toISOString().replace(/-|:|\.\d+/g, '');
|
||||||
|
|
||||||
|
const summary = item.base.caption;
|
||||||
|
const description = item.base.information || "";
|
||||||
|
const location = item.base.address?.meetingAt || "";
|
||||||
|
|
||||||
|
// Escape special characters for ICS fields
|
||||||
|
const escapedSummary = escapeICSField(summary /* .replace(/['"]/g, "") */);
|
||||||
|
const escapedDescription = escapeICSField(description /* .replace(/['"]/g, "") */);
|
||||||
|
const escapedLocation = escapeICSField(location);
|
||||||
|
|
||||||
|
const icsContent = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Evang Kirchengemeinde Malmsheim//Gottesdienste//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:${crypto.randomUUID()}@malmsheim-evangelisch.de
|
||||||
|
DTSTAMP:${new Date().toISOString().replace(/-|:|\.\d+/g, '')}
|
||||||
|
DTSTART:${start}
|
||||||
|
DTEND:${end}
|
||||||
|
SUMMARY:${escapedSummary}
|
||||||
|
DESCRIPTION:${escapedDescription}
|
||||||
|
LOCATION:${escapedLocation}
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
`;
|
||||||
|
|
||||||
|
const blob = new Blob([icsContent], { type: 'text/calendar' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
return m(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
href: url,
|
||||||
|
download: "event.ics",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
fa_solid_icon(settings, "calendar-plus"),
|
||||||
|
is_mobile ? "" : " Termin herunterladen",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var AppointmentHeader = {
|
||||||
|
view: function(vnode) {
|
||||||
|
const {item, settings} = vnode.attrs;
|
||||||
|
|
||||||
|
return m(".cte-info-header", [
|
||||||
|
m("span.cte-date", display_event_date(item)),
|
||||||
|
m("span.cte-download", createICSLink(item, settings)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var AppointmentDescription = {
|
||||||
|
view: function(vnode) {
|
||||||
|
const {item, settings} = vnode.attrs;
|
||||||
|
const note_parts = [];
|
||||||
|
if (item.base.note) {
|
||||||
|
note_parts.push(item.base.note);
|
||||||
|
}
|
||||||
|
if (item.base.address?.meetingAt) {
|
||||||
|
note_parts.push(item.base.address.meetingAt);
|
||||||
|
}
|
||||||
|
return m(".cte-description", [
|
||||||
|
m("h1", item.base.caption),
|
||||||
|
m("h2", note_parts.join(" | ")),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* find `http://`, `https://` and `www.` links
|
||||||
|
* - requires whitespace, parantheses `(, `)` or line start/end before and after it
|
||||||
|
* - find non-links as first capture, links as second capture (might be empty)
|
||||||
|
*/
|
||||||
|
const REGEX_FIND_LINKS = /(?<=^|[\s()])(?:https?:\/\/|www\.)[^\s()*]*(?=[()\s]|$)/;
|
||||||
|
const REGEX_FIND_LINKS_CONT = /(?<=[\s()])(?:https?:\/\/|www\.)[^\s()*]*(?=[()\s]|$)/;
|
||||||
|
|
||||||
|
function* format_links(part) {
|
||||||
|
let regex = REGEX_FIND_LINKS;
|
||||||
|
while (true) {
|
||||||
|
const match = part.match(regex);
|
||||||
|
regex = REGEX_FIND_LINKS_CONT;
|
||||||
|
const normal = part.substring(0, match?.index ?? part.length);
|
||||||
|
yield normal;
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
part = part.substring(match.index + match[0].length);
|
||||||
|
const url = match[0];
|
||||||
|
yield m("a", {href: url, target: "_blank"}, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* find `*...*` highlighted text:
|
||||||
|
* - requires whitespace or line start/end before and after it
|
||||||
|
* - requires non-space on the inside as first and last character
|
||||||
|
* - `*` not allowed inside
|
||||||
|
* - find non-highlighted text as first capture, highlighted as second capture (might be empty)
|
||||||
|
*/
|
||||||
|
const REGEX_FIND_HIGHLIGHTED = /(?<=^|\s)\*[^*\s](?:[^*]*[^*\s])?\*(?=\s|$)/;
|
||||||
|
const REGEX_FIND_HIGHLIGHTED_CONT = /(?<=\s)\*[^*\s](?:[^*]*[^*\s])?\*(?=\s|$)/;
|
||||||
|
|
||||||
|
function* format_highlights_and_links(line) {
|
||||||
|
let regex = REGEX_FIND_HIGHLIGHTED;
|
||||||
|
while (true) {
|
||||||
|
const match = line.match(regex);
|
||||||
|
regex = REGEX_FIND_HIGHLIGHTED_CONT;
|
||||||
|
const normal = line.substring(0, match?.index ?? line.length);
|
||||||
|
for (const part of format_links(normal)) {
|
||||||
|
yield part;
|
||||||
|
}
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
line = line.substring(match.index + match[0].length);
|
||||||
|
yield m("strong", ["*", ...format_links(match[0].slice(1, -1)), "*"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_information(item) {
|
||||||
|
const information = item.base.information || "";
|
||||||
|
const paragraphs = [];
|
||||||
|
let cur_lines = [];
|
||||||
|
|
||||||
|
for (const line of information.split(/\r?\n/)) {
|
||||||
|
if (!line.trim()) {
|
||||||
|
// split paragraphs
|
||||||
|
if (cur_lines.length) {
|
||||||
|
paragraphs.push(cur_lines);
|
||||||
|
cur_lines = [];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* find `*...*` highlighted text */
|
||||||
|
const line_parts = [];
|
||||||
|
if (cur_lines.length) {
|
||||||
|
line_parts.push(m("br"));
|
||||||
|
}
|
||||||
|
line_parts.push(...format_highlights_and_links(line));
|
||||||
|
cur_lines.push(m("[", line_parts));
|
||||||
|
}
|
||||||
|
if (cur_lines.length) {
|
||||||
|
paragraphs.push(cur_lines);
|
||||||
|
}
|
||||||
|
return m("[", paragraphs.map((para) => m("p", para)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var AppointmentLocation = {
|
||||||
|
view: function(vnode) {
|
||||||
|
const {item, settings} = vnode.attrs;
|
||||||
|
|
||||||
|
const location_parts = [];
|
||||||
|
if (item.base.address?.meetingAt) {
|
||||||
|
location_parts.push(item.base.address.meetingAt);
|
||||||
|
}
|
||||||
|
if (item.base.address?.street) {
|
||||||
|
location_parts.push(item.base.address.street);
|
||||||
|
}
|
||||||
|
const location = location_parts.join(", ");
|
||||||
|
|
||||||
|
if (!location) {
|
||||||
|
return m("[");
|
||||||
|
}
|
||||||
|
|
||||||
|
return m("p", [
|
||||||
|
fa_solid_icon(settings, "location-dot"),
|
||||||
|
" ",
|
||||||
|
location,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var AppointmentLink = {
|
||||||
|
view: function(vnode) {
|
||||||
|
const {item, settings} = vnode.attrs;
|
||||||
|
if (!item.base.link) {
|
||||||
|
return m("[");
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = item.base.link;
|
||||||
|
if (!url.startsWith("http")) {
|
||||||
|
url = "https://" + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return m("p", [
|
||||||
|
fa_solid_icon(settings, "link"),
|
||||||
|
" ",
|
||||||
|
m("a", {href: url, target: "_blank"}, item.base.link),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppointmentDetails() {
|
||||||
|
let collapsed = true;
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
collapsed = !collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: function(vnode) {
|
||||||
|
const {item, settings} = vnode.attrs;
|
||||||
|
|
||||||
|
const show_details = item.base.information /* || item.base.address?.meetingAt || item.base.address?.street */ || item.base.link;
|
||||||
|
|
||||||
|
if (!show_details) {
|
||||||
|
return m("[");
|
||||||
|
}
|
||||||
|
return m(
|
||||||
|
".cte-details-collapser",
|
||||||
|
{
|
||||||
|
class: collapsed ? "collapsed" : "",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
m(".cte-details", [
|
||||||
|
m(".cte-information-group", [
|
||||||
|
m("h3.cte-information-label", "Beschreibung"),
|
||||||
|
m(".cte-information", [
|
||||||
|
format_information(item),
|
||||||
|
m(AppointmentLocation,{item, settings}),
|
||||||
|
m(AppointmentLink, {item, settings}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
m(
|
||||||
|
"button.toggle-collapse",
|
||||||
|
{onclick: toggle},
|
||||||
|
collapsed ? "Details anzeigen" : "Details verbergen",
|
||||||
|
" ",
|
||||||
|
fa_solid_icon(settings, collapsed ? "chevron-down" : "chevron-up"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var Appointment = {
|
||||||
|
view: function(vnode) {
|
||||||
|
const {item, settings} = vnode.attrs;
|
||||||
|
|
||||||
|
return m(".cte-event", [
|
||||||
|
m(".cte-summary", [
|
||||||
|
m(AppointmentImage, {item, settings}),
|
||||||
|
m(".cte-info", [
|
||||||
|
m(AppointmentHeader, {item, settings}),
|
||||||
|
m(AppointmentDescription, {item, settings}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
m(AppointmentDetails, {item, settings}),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function load_events(dom_node, defaults) {
|
||||||
|
if (dom_node.dataset.cteActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dom_node.dataset.cteActive = true;
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
instance: dom_node.dataset.cteInstance ?? defaults.instance,
|
||||||
|
days: dom_node.dataset.cteDays ?? defaults.days ?? 31,
|
||||||
|
calendar_regex: dom_node.dataset.cteCalendarRegex ?? defaults.calendar_regex,
|
||||||
|
event_regex: dom_node.dataset.cteEventRegex ?? defaults.event_regex,
|
||||||
|
limit: dom_node.dataset.cteLimit ?? defaults.limit ?? 5,
|
||||||
|
default_image: dom_node.dataset.cteDefaultImage ?? defaults.default_image,
|
||||||
|
default_image_mobile: dom_node.dataset.cteDefaultImageMobile ?? defaults.default_image_mobile,
|
||||||
|
fontawesome_svg_path: dom_node.dataset.cteFontawesomeSvgPath ?? defaults.fontawesome_svg_path,
|
||||||
|
};
|
||||||
|
|
||||||
|
function format_date(date) {
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const req_config = {
|
||||||
|
credentials: "omit",
|
||||||
|
mode: "cors",
|
||||||
|
}
|
||||||
|
|
||||||
|
const r_cal_regex = new RegExp(settings.calendar_regex, "i");
|
||||||
|
const r_event_regex = new RegExp(settings.event_regex, "i");
|
||||||
|
const all_calendars = (await (await fetch(settings.instance + "/api/calendars", req_config)).json()).data;
|
||||||
|
const calendars = all_calendars.filter((calendar) => r_cal_regex.exec(calendar.name));
|
||||||
|
const calendar_ids = calendars.map((calendar) => calendar.id);
|
||||||
|
|
||||||
|
const filters = calendar_ids.map((cal_id) => `calendar_ids[]=${cal_id}`);
|
||||||
|
const from = new Date();
|
||||||
|
filters.push(`from=${format_date(from)}`);
|
||||||
|
const to = new Date(from.valueOf() + settings.days * 86400 * 1000);
|
||||||
|
filters.push(`to=${format_date(to)}`);
|
||||||
|
const query = filters.join("&");
|
||||||
|
const all_appointments = (await (await fetch(settings.instance + `/api/calendars/appointments?${query}`, req_config)).json()).data;
|
||||||
|
const appointments = all_appointments.filter((appointment) => r_event_regex.exec(appointment.base.title));
|
||||||
|
|
||||||
|
if (appointments.length > settings.limit) {
|
||||||
|
appointments.length = settings.limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Appointments = {
|
||||||
|
view: function(vnode) {
|
||||||
|
return m("[", appointments.map((item) => m(Appointment, {item, settings})));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
m.mount(dom_node, Appointments);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load_all_events(defaults) {
|
||||||
|
await Promise.all(Array.from(document.querySelectorAll(".cte-events")).map((node) => load_events(node, defaults)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { load_all_events };
|
Loading…
Reference in New Issue
Block a user