First version, for githup; UNSTABLE, DO NOT USE!
This commit is contained in:
529
www/intern/wk-leitung/kalender.php
Normal file
529
www/intern/wk-leitung/kalender.php
Normal file
@@ -0,0 +1,529 @@
|
||||
<?php
|
||||
$days = 2;
|
||||
$firstDay = "2026-04-18";
|
||||
|
||||
$dates = [];
|
||||
|
||||
$startDate = new DateTime($firstDay);
|
||||
|
||||
for ($i = 0; $i < $days; $i++) {
|
||||
$current = clone $startDate;
|
||||
$current->modify("+{$i} day");
|
||||
$dates[] = $current->format("Y-m-d");
|
||||
}
|
||||
|
||||
setlocale(LC_TIME, 'de_DE.UTF-8'); // set German locale
|
||||
|
||||
?>
|
||||
|
||||
<div class="calendar">
|
||||
|
||||
<!-- empty top-left corner -->
|
||||
<div class="corner fBorderRight fBorderBottom"></div>
|
||||
|
||||
<!-- headers -->
|
||||
|
||||
<?php foreach ($dates as $ind => $d): ?>
|
||||
<div class="day-header fBorderBottom"><h3><?= strftime("%a", strtotime($d)) ?> Halle</h3></div>
|
||||
<div class="day-header fBorderBottom"><h3><?= strftime("%a", strtotime($d)) ?> Aula</h3></div>
|
||||
<?php $notLast = ($ind !== count($dates) - 1) ? 'fBorderRight' : '';?>
|
||||
<div class="day-header <?= $notLast ?> fBorderBottom"><h3><?= strftime("%a", strtotime($d)) ?> Sonstige</h3></div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<!-- time column -->
|
||||
<div class="time-column fBorderRight">
|
||||
<?php for ($i = 0; $i <= 23; $i++) {
|
||||
$si = str_pad($i, 2, '0', STR_PAD_LEFT);
|
||||
echo '<div class="time">'.$si.':00</div>';
|
||||
} ?>
|
||||
</div>
|
||||
|
||||
<!-- day columns -->
|
||||
|
||||
<?php foreach ($dates as $ind => $d): ?>
|
||||
<div class="day-column" data-day="<?= $d ?>" data-type="halle"></div>
|
||||
<div class="day-column" data-day="<?= $d ?>" data-type="aula"></div>
|
||||
<?php $notLast = ($ind !== count($dates) - 1) ? 'fBorderRight' : '';?>
|
||||
<div class="day-column <?= $notLast ?>" data-day="<?= $d ?>" data-type="sonstige"></div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<button id="exportCal">Export Calendar</button>
|
||||
|
||||
<style>
|
||||
.calendar {
|
||||
display: grid;
|
||||
grid-template-columns: 80px <?php foreach ($dates as $d) { echo '1fr 1fr 1fr ';}?>;
|
||||
grid-template-rows: 40px auto;
|
||||
border: 1px solid #ccc;
|
||||
font-family: Arial, sans-serif;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.corner {
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.day-header {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ccc;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.day-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.time-column {
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.time {
|
||||
height: 90px;
|
||||
padding: 5px;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.day-column {
|
||||
position: relative;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.event {
|
||||
position: absolute;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
font-size: 12px;
|
||||
cursor: grab;
|
||||
transition: left 0.2s, width 0.2s;
|
||||
}
|
||||
|
||||
.event.dragging {
|
||||
opacity: 0.8;
|
||||
z-index: 1000;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.event div span {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.fBorderRight { border-right: 3px solid #000; }
|
||||
.fBorderBottom { border-bottom: 3px solid #000; }
|
||||
|
||||
.day-column.highlight {
|
||||
background-color: rgba(255, 255, 0, 0.1);
|
||||
}
|
||||
|
||||
.day-column.highlight-active {
|
||||
background-color: rgba(255, 255, 0, 0.25);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const events = [
|
||||
|
||||
{ day: "2026-04-18", start: 12.0, end: 13.0, title: "Kampfrichteressen", type: "sonstige", abt: "0", elem: "" },
|
||||
// Day 1 - 2026-04-18
|
||||
{ day: "2026-04-18", start: 8.0, end: 8.50, title: "Körperliches Aufwärmen", type: "aula", abt: "1", elem: "15" },
|
||||
{ day: "2026-04-18", start: 8.50, end: 9.0, title: "Geführtes Einturnen", type: "halle", abt: "1", elem: "15" },
|
||||
{ day: "2026-04-18", start: 9.0, end: 10.0, title: "Wettkampf", type: "halle", abt: "1", elem: "15" },
|
||||
{ day: "2026-04-18", start: 10.0, end: 10.50, title: "Rangverkündigung", type: "aula", abt: "1", elem: "15" },
|
||||
|
||||
{ day: "2026-04-18", start: 10.50, end: 11.0, title: "Körperliches Aufwärmen", type: "aula", abt: "2", elem: "13" },
|
||||
{ day: "2026-04-18", start: 11.0, end: 11.50, title: "Geführtes Einturnen", type: "halle", abt: "2", elem: "13" },
|
||||
{ day: "2026-04-18", start: 11.50, end: 12.50, title: "Wettkampf", type: "halle", abt: "2", elem: "13" },
|
||||
{ day: "2026-04-18", start: 12.50, end: 13.0, title: "Rangverkündigung", type: "aula", abt: "2", elem: "13" },
|
||||
|
||||
{ day: "2026-04-18", start: 13.0, end: 13.50, title: "Körperliches Aufwärmen", type: "aula", abt: "3", elem: "10" },
|
||||
{ day: "2026-04-18", start: 13.50, end: 14.0, title: "Geführtes Einturnen", type: "halle", abt: "3", elem: "10" },
|
||||
{ day: "2026-04-18", start: 14.0, end: 15.0, title: "Wettkampf", type: "halle", abt: "3", elem: "10" },
|
||||
{ day: "2026-04-18", start: 15.0, end: 15.50, title: "Rangverkündigung", type: "aula", abt: "3", elem: "10" },
|
||||
|
||||
{ day: "2026-04-18", start: 15.50, end: 16.0, title: "Körperliches Aufwärmen", type: "aula", abt: "4", elem: "12" },
|
||||
{ day: "2026-04-18", start: 16.0, end: 16.50, title: "Geführtes Einturnen", type: "halle", abt: "4", elem: "12" },
|
||||
{ day: "2026-04-18", start: 16.50, end: 17.50, title: "Wettkampf", type: "halle", abt: "4", elem: "12" },
|
||||
{ day: "2026-04-18", start: 17.30, end: 18.0, title: "Rangverkündigung", type: "aula", abt: "4", elem: "12" },
|
||||
|
||||
{ day: "2026-04-18", start: 18.0, end: 18.50, title: "Körperliches Aufwärmen", type: "aula", abt: "5", elem: "11" },
|
||||
{ day: "2026-04-18", start: 18.50, end: 19.0, title: "Geführtes Einturnen", type: "halle", abt: "5", elem: "11" },
|
||||
{ day: "2026-04-18", start: 19.0, end: 20.0, title: "Wettkampf", type: "halle", abt: "5", elem: "11" },
|
||||
{ day: "2026-04-18", start: 20.0, end: 20.50, title: "Rangverkündigung", type: "aula", abt: "5", elem: "11" },
|
||||
|
||||
{ day: "2026-04-18", start: 20.50, end: 21.0, title: "Körperliches Aufwärmen", type: "aula", abt: "6", elem: "14" },
|
||||
{ day: "2026-04-18", start: 21.0, end: 21.50, title: "Geführtes Einturnen", type: "halle", abt: "6", elem: "14" },
|
||||
{ day: "2026-04-18", start: 21.50, end: 22.50, title: "Wettkampf", type: "halle", abt: "6", elem: "14" },
|
||||
{ day: "2026-04-18", start: 22.50, end: 23.0, title: "Rangverkündigung", type: "aula", abt: "6", elem: "14" },
|
||||
|
||||
// Day 2 - 2026-04-19 (same pattern with shifted times)
|
||||
{ day: "2026-04-19", start: 8.0, end: 8.30, title: "Körperliches Aufwärmen", type: "aula", abt: "1", elem: "15" },
|
||||
{ day: "2026-04-19", start: 8.30, end: 9.0, title: "Geführtes Einturnen", type: "halle", abt: "1", elem: "15" },
|
||||
{ day: "2026-04-19", start: 9.0, end: 10.0, title: "Wettkampf", type: "halle", abt: "1", elem: "15" },
|
||||
{ day: "2026-04-19", start: 10.0, end: 10.30, title: "Rangverkündigung", type: "aula", abt: "1", elem: "15" },
|
||||
|
||||
{ day: "2026-04-19", start: 10.30, end: 11.0, title: "Körperliches Aufwärmen", type: "aula", abt: "2", elem: "13" },
|
||||
{ day: "2026-04-19", start: 11.0, end: 11.30, title: "Geführtes Einturnen", type: "halle", abt: "2", elem: "13" },
|
||||
{ day: "2026-04-19", start: 11.30, end: 12.30, title: "Wettkampf", type: "halle", abt: "2", elem: "13" },
|
||||
{ day: "2026-04-19", start: 12.30, end: 13.0, title: "Rangverkündigung", type: "aula", abt: "2", elem: "13" },
|
||||
|
||||
// ...repeat for abt 3–6 with times staggered
|
||||
];
|
||||
|
||||
const pixelsPerHour = 90;
|
||||
|
||||
// Convert hour float to Date
|
||||
function hourFloatToDate(day, hourFloat) {
|
||||
const d = new Date(day);
|
||||
const hours = Math.floor(hourFloat);
|
||||
const minutes = Math.round((hourFloat % 1) * 60);
|
||||
d.setHours(hours, minutes, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
function hoursToHHMM(hours) {
|
||||
const h = Math.floor(hours);
|
||||
const m = Math.round((hours - h) * 60);
|
||||
return `${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}`;
|
||||
}
|
||||
|
||||
// Convert HH:MM string to hours float
|
||||
function HHMMToHours(str) {
|
||||
const [h, m] = str.split(':').map(Number);
|
||||
return h + m/60;
|
||||
}
|
||||
|
||||
// Format Date to hh:mm
|
||||
function formatTime(date) {
|
||||
return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date);
|
||||
}
|
||||
|
||||
// Collision detection
|
||||
function eventsCollide(a, b) {
|
||||
return a.start < b.end && a.end > b.start && a.day === b.day && a.type === b.type;
|
||||
}
|
||||
|
||||
// Assign columns with proper overlap tracking
|
||||
function assignColumns(events) {
|
||||
events.sort((a,b) => a.start - b.start);
|
||||
const active = [];
|
||||
|
||||
events.forEach(event => {
|
||||
// Remove finished events
|
||||
for (let i = active.length-1; i>=0; i--) {
|
||||
if (active[i].end <= event.start) active.splice(i,1);
|
||||
}
|
||||
|
||||
// Find first free column index
|
||||
let colIndex = 0;
|
||||
while(active.some(e => e.columnIndex === colIndex)) colIndex++;
|
||||
event.columnIndex = colIndex;
|
||||
active.push(event);
|
||||
|
||||
// Update totalColumns for current overlap group
|
||||
const maxColumns = Math.max(...active.map(e => e.columnIndex + 1));
|
||||
active.forEach(e => e.totalColumns = maxColumns);
|
||||
});
|
||||
}
|
||||
|
||||
// Render events into the calendar
|
||||
function renderEvents(events, colors) {
|
||||
events.forEach(event => {
|
||||
const dayColumn = document.querySelector(`.day-column[data-day="${event.day}"][data-type="${event.type}"]`);
|
||||
const el = document.createElement("div");
|
||||
el.className = "event";
|
||||
|
||||
const startDate = hourFloatToDate(event.day, event.start);
|
||||
const endDate = hourFloatToDate(event.day, event.end);
|
||||
const duration = event.end - event.start; // in hours
|
||||
const lineBreak = duration >= 1 ? "<br>" : " "; // use space if < 1h
|
||||
|
||||
if (event.abt != 0){
|
||||
el.innerHTML = `<div><span><b>${event.title} - Abt. ${event.abt}</b> (${event.elem})${lineBreak}${formatTime(startDate)} - ${formatTime(endDate)}</span></div>`;
|
||||
} else {
|
||||
el.innerHTML = `<div><span><b>${event.title}</b>${lineBreak}${formatTime(startDate)} - ${formatTime(endDate)}</span></div>`;
|
||||
}
|
||||
|
||||
|
||||
el.style.background = colors[event.abt].background;
|
||||
el.style.color = colors[event.abt].foreground;
|
||||
el.style.borderTop = `3px solid ${colors[event.abt].borderTop}`;
|
||||
|
||||
const widthPercent = 100 / event.totalColumns;
|
||||
el.style.top = (event.start * pixelsPerHour) + "px";
|
||||
el.style.height = ((event.end - event.start) * pixelsPerHour - 11) + "px";
|
||||
el.style.width = `calc(${widthPercent}% - 10px)`;
|
||||
el.style.left = `${event.columnIndex * widthPercent}%`;
|
||||
|
||||
// Store reference for updates
|
||||
event.el = el;
|
||||
|
||||
addDragBehavior(el, event, colors);
|
||||
|
||||
dayColumn.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
// Drag & drop behavior
|
||||
function addDragBehavior(el, event, colors) {
|
||||
el.addEventListener("pointerdown", e => {
|
||||
e.preventDefault();
|
||||
el.setPointerCapture(e.pointerId);
|
||||
el.classList.add("dragging");
|
||||
|
||||
const startY = e.clientY;
|
||||
const origTop = parseFloat(el.style.top);
|
||||
const type = event.type;
|
||||
|
||||
const validColumns = Array.from(document.querySelectorAll(`.day-column[data-type="${type}"]`));
|
||||
validColumns.forEach(c => c.classList.add("highlight"));
|
||||
|
||||
function onPointerMove(eMove) {
|
||||
const deltaY = eMove.clientY - startY;
|
||||
|
||||
// Vertical movement (snapped to 15 min)
|
||||
let newTop = origTop + deltaY;
|
||||
newTop = Math.max(0, newTop);
|
||||
newTop = Math.round(newTop / (pixelsPerHour/12)) * (pixelsPerHour/12);
|
||||
el.style.top = newTop + "px";
|
||||
|
||||
// Horizontal movement locked to same type columns
|
||||
const pointerX = eMove.clientX;
|
||||
const targetColumn = validColumns.find(c => {
|
||||
const rect = c.getBoundingClientRect();
|
||||
return pointerX >= rect.left && pointerX <= rect.right;
|
||||
});
|
||||
|
||||
if (targetColumn && targetColumn !== el.parentElement) {
|
||||
targetColumn.appendChild(el);
|
||||
el.style.left = 0;
|
||||
}
|
||||
|
||||
// Update live time
|
||||
const newStart = newTop / pixelsPerHour;
|
||||
const newEnd = newStart + (event.end - event.start);
|
||||
const startDate = hourFloatToDate(event.day, newStart);
|
||||
const endDate = hourFloatToDate(event.day, newEnd);
|
||||
const duration = event.end - event.start; // in hours
|
||||
const lineBreak = duration >= 1 ? "<br>" : " "; // use space if < 1h
|
||||
|
||||
if (event.abt != 0){
|
||||
el.querySelector("span").innerHTML = `<b>${event.title} - Abt. ${event.abt}</b> (${event.elem})${lineBreak}${formatTime(startDate)} - ${formatTime(endDate)}`;
|
||||
} else {
|
||||
el.querySelector("span").innerHTML = `<b>${event.title}</b>${lineBreak}${formatTime(startDate)} - ${formatTime(endDate)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerUp(eUp) {
|
||||
el.classList.remove("dragging");
|
||||
el.releasePointerCapture(eUp.pointerId);
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
validColumns.forEach(c => c.classList.remove("highlight"));
|
||||
|
||||
// Update event day based on column
|
||||
const newColumn = validColumns.find(c => el.parentElement === c);
|
||||
if(newColumn) event.day = newColumn.dataset.day;
|
||||
|
||||
// Update start/end
|
||||
const newStart = parseFloat(el.style.top) / pixelsPerHour;
|
||||
const duration = event.end - event.start;
|
||||
event.start = newStart;
|
||||
event.end = newStart + duration;
|
||||
|
||||
// Recalculate columns for current day/type
|
||||
const dayEvents = events.filter(ev => ev.day === event.day && ev.type === event.type);
|
||||
assignColumns(dayEvents);
|
||||
|
||||
// Update positions, widths, and displayed time
|
||||
dayEvents.forEach(ev => {
|
||||
const widthPercent = 100 / ev.totalColumns;
|
||||
ev.el.style.width = `calc(${widthPercent}% - 10px)`;
|
||||
ev.el.style.left = `${ev.columnIndex * widthPercent}%`;
|
||||
|
||||
const startDate = hourFloatToDate(ev.day, ev.start);
|
||||
const endDate = hourFloatToDate(ev.day, ev.end);
|
||||
const duration = ev.end - ev.start; // in hours
|
||||
const lineBreak = duration >= 1 ? "<br>" : " "; // use space if < 1h
|
||||
|
||||
if (ev.abt != 0){
|
||||
ev.el.querySelector("span").innerHTML = `<b>${ev.title} - Abt. ${ev.abt}</b> (${ev.elem})${lineBreak}${formatTime(startDate)} - ${formatTime(endDate)}`;
|
||||
} else {
|
||||
ev.el.querySelector("span").innerHTML = `<b>${ev.title}</b>${lineBreak}${formatTime(startDate)} - ${formatTime(endDate)}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
});
|
||||
|
||||
el.addEventListener("dblclick", () => {
|
||||
const durationHours = prompt("Enter new duration in hours:", hoursToHHMM((event.end - event.start).toFixed(2)));
|
||||
if(durationHours !== null) {
|
||||
const duration = HHMMToHours(durationHours);
|
||||
if(!isNaN(duration) && duration > 0) {
|
||||
event.end = event.start + duration;
|
||||
|
||||
// Recalculate columns for the day/type
|
||||
const dayEvents = events.filter(ev => ev.day === event.day && ev.type === event.type);
|
||||
assignColumns(dayEvents);
|
||||
|
||||
// Update all event positions, widths, and displayed text
|
||||
dayEvents.forEach(ev => {
|
||||
const widthPercent = 100 / ev.totalColumns;
|
||||
ev.el.style.width = `calc(${widthPercent}% - 10px)`;
|
||||
ev.el.style.left = `${ev.columnIndex * widthPercent}%`;
|
||||
ev.el.style.top = (ev.start * pixelsPerHour) + "px";
|
||||
ev.el.style.height = ((ev.end - ev.start) * pixelsPerHour - 11) + "px";
|
||||
|
||||
// Update displayed time with conditional line break
|
||||
const startDate = hourFloatToDate(ev.day, ev.start);
|
||||
const endDate = hourFloatToDate(ev.day, ev.end);
|
||||
const dur = ev.end - ev.start;
|
||||
const lineBreak = dur >= 1 ? "<br>" : " ";
|
||||
if (ev.abt != 0){
|
||||
ev.el.querySelector("span").innerHTML = `<b>${ev.title} - Abt. ${ev.abt}</b> (${ev.elem})${lineBreak}${formatTime(startDate)} - ${formatTime(endDate)}`;
|
||||
} else {
|
||||
ev.el.querySelector("span").innerHTML = `<b>${ev.title}</b>${lineBreak}${formatTime(startDate)} - ${formatTime(endDate)}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------- USAGE -----------------
|
||||
|
||||
// Example colors
|
||||
const colors = {
|
||||
"0": {
|
||||
"background": "rgba(172, 172, 172, 0.18)",
|
||||
"foreground": "#4b4b4bff",
|
||||
"borderTop": "#5a5a5aff"
|
||||
},
|
||||
"1": {
|
||||
"background": "rgba(59, 130, 246, 0.18)",
|
||||
"foreground": "#1e3a8a",
|
||||
"borderTop": "#1e3a8a"
|
||||
},
|
||||
"2": {
|
||||
"background": "rgba(20, 184, 166, 0.18)",
|
||||
"foreground": "#065f46",
|
||||
"borderTop": "#065f46"
|
||||
},
|
||||
"3": {
|
||||
"background": "rgba(34, 197, 94, 0.18)",
|
||||
"foreground": "#14532d",
|
||||
"borderTop": "#14532d"
|
||||
},
|
||||
"4": {
|
||||
"background": "rgba(163, 230, 53, 0.20)",
|
||||
"foreground": "#365314",
|
||||
"borderTop": "#365314"
|
||||
},
|
||||
"5": {
|
||||
"background": "rgba(245, 158, 11, 0.20)",
|
||||
"foreground": "#92400e",
|
||||
"borderTop": "#92400e"
|
||||
},
|
||||
"6": {
|
||||
"background": "rgba(249, 115, 22, 0.20)",
|
||||
"foreground": "#9a3412",
|
||||
"borderTop": "#9a3412"
|
||||
},
|
||||
"7": {
|
||||
"background": "rgba(244, 63, 94, 0.18)",
|
||||
"foreground": "#9f1239",
|
||||
"borderTop": "#9f1239"
|
||||
},
|
||||
"8": {
|
||||
"background": "rgba(236, 72, 153, 0.18)",
|
||||
"foreground": "#9d174d",
|
||||
"borderTop": "#9d174d"
|
||||
},
|
||||
"9": {
|
||||
"background": "rgba(168, 85, 247, 0.18)",
|
||||
"foreground": "#581c87",
|
||||
"borderTop": "#581c87"
|
||||
},
|
||||
"10": {
|
||||
"background": "rgba(99, 102, 241, 0.18)",
|
||||
"foreground": "#312e81",
|
||||
"borderTop": "#312e81"
|
||||
},
|
||||
"11": {
|
||||
"background": "rgba(100, 116, 139, 0.20)",
|
||||
"foreground": "#1e293b",
|
||||
"borderTop": "#1e293b"
|
||||
},
|
||||
"12": {
|
||||
"background": "rgba(6, 182, 212, 0.18)",
|
||||
"foreground": "#164e63",
|
||||
"borderTop": "#164e63"
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
const grouped = {};
|
||||
events.forEach(e => {
|
||||
const key = `${e.day}-${e.type}`;
|
||||
if(!grouped[key]) grouped[key] = [];
|
||||
grouped[key].push(e);
|
||||
});
|
||||
Object.values(grouped).forEach(arr => assignColumns(arr));
|
||||
renderEvents(events, colors);
|
||||
|
||||
function exportToICS(events) {
|
||||
let icsContent = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//YourApp//Calendar//EN
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
`;
|
||||
|
||||
events.forEach(event => {
|
||||
const startDate = hourFloatToDate(event.day, event.start);
|
||||
const endDate = hourFloatToDate(event.day, event.end);
|
||||
|
||||
// Format as YYYYMMDDTHHMMSS
|
||||
function formatICSDate(d) {
|
||||
return d.toISOString().replace(/[-:]/g,'').split('.')[0] + 'Z';
|
||||
}
|
||||
|
||||
icsContent += `BEGIN:VEVENT
|
||||
UID:${event.day}-${event.start}-${event.title}
|
||||
DTSTAMP:${formatICSDate(new Date())}
|
||||
DTSTART:${formatICSDate(startDate)}
|
||||
DTEND:${formatICSDate(endDate)}
|
||||
SUMMARY:${event.title} - Abt. ${event.abt}
|
||||
DESCRIPTION:Type: ${event.type}, Elem: ${event.elem}
|
||||
END:VEVENT
|
||||
`;
|
||||
});
|
||||
|
||||
icsContent += `END:VCALENDAR`;
|
||||
|
||||
// Create downloadable link
|
||||
const blob = new Blob([icsContent], { type: 'text/calendar;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'calendar.ics';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Attach to button
|
||||
document.getElementById('exportCal').addEventListener('click', () => exportToICS(events));
|
||||
</script>
|
||||
Reference in New Issue
Block a user