Compare commits
2
Commits
6800a75825
...
fbbb044b83
No files matched your search
+32
-5
@@ -2,16 +2,43 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>cal app</title>
|
||||
<link rel="stylesheet" href="/src/style.css" />
|
||||
<script type="module" src="/src/main.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<input type="text" id="ics_file">
|
||||
<input type="button" value="hi" id="ics_run">
|
||||
<div id="calendar"></div>
|
||||
<div class="container">
|
||||
<header class="app-header">
|
||||
<h1>🗓️ Calendar Filter App</h1>
|
||||
<p class="subtitle">Load, filter, and combine calendars with style.</p>
|
||||
</header>
|
||||
<div class="flex_input">
|
||||
<input type="text" id="ics_file" placeholder="Paste .ics URL or API endpoint here" />
|
||||
<input type="button" value="new calendar" id="ics_run" />
|
||||
<input type="button" value="add a calendar" id="ics_add" />
|
||||
</div>
|
||||
<div class="info-bar" style="text-align: center">
|
||||
<span>Tip: You can add multiple calendars. Hover events for details.
|
||||
<a href="https://fullcalendar.io/" target="_blank" rel="noopener noreferrer">Powered by
|
||||
FullCalendar</a></span>
|
||||
</div>
|
||||
<div class="calendar-wrapper">
|
||||
<div class="placeholder-message" id="placeholder-message">
|
||||
No calendar loaded yet. Paste a URL to get started!
|
||||
</div>
|
||||
<div id="calendar">
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="app-footer">
|
||||
<p>Made with Rust, JS, and ❤️</p>
|
||||
<p>by
|
||||
<a href="//ldlda.com" target="_blank" rel="noopener noreferrer">lđa</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
let sourceCount = 0;
|
||||
|
||||
/** how ass would this shit be? */
|
||||
export function getNextColor() {
|
||||
// Spread hues evenly around the color wheel
|
||||
const hue = (sourceCount * 137) % 360; // 137 is the golden angle
|
||||
sourceCount++;
|
||||
return `hsl(${hue}, 70%, 50%)`;
|
||||
}
|
||||
+53
-15
@@ -11,23 +11,45 @@ import bootstrap5Plugin from "@fullcalendar/bootstrap5";
|
||||
import tippy, { followCursor } from "tippy.js";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
|
||||
import { getNextColor } from "./color_generator";
|
||||
import { formatTooltip } from "./tooltip_parser";
|
||||
import { create_url } from "./utils";
|
||||
|
||||
const text_field = document.getElementById("ics_file");
|
||||
const confirm_button = document.getElementById("ics_run");
|
||||
const cal = document.getElementById("calendar");
|
||||
const add_button = document.getElementById("ics_add");
|
||||
const cal_ele = document.getElementById("calendar");
|
||||
const placeholder_message = document.getElementById("placeholder-message");
|
||||
let currentTippy = null;
|
||||
let calendar = null;
|
||||
|
||||
function showPlaceholder() {
|
||||
placeholder_message.style.display = "block";
|
||||
cal_ele.style.display = "none";
|
||||
}
|
||||
|
||||
function hidePlaceholder() {
|
||||
placeholder_message.style.display = "none";
|
||||
cal_ele.style.display = "block";
|
||||
|
||||
}
|
||||
|
||||
const new_tippy = (info) => {
|
||||
if (info.el._tippy === currentTippy) {
|
||||
currentTippy.show();
|
||||
} // what does this even MEAN
|
||||
else if (info.event.extendedProps.description) {
|
||||
else {
|
||||
// if (info.event.extendedProps.description)
|
||||
// Destroy previous tippy if exists
|
||||
if (currentTippy) {
|
||||
currentTippy.destroy();
|
||||
currentTippy = null;
|
||||
}
|
||||
currentTippy = tippy(info.el, {
|
||||
content: info.event.extendedProps.description,
|
||||
content: formatTooltip(
|
||||
info.event.title,
|
||||
info.event.extendedProps.description
|
||||
),
|
||||
allowHTML: true,
|
||||
theme: "light",
|
||||
interactive: true,
|
||||
@@ -45,19 +67,18 @@ const new_tippy = (info) => {
|
||||
};
|
||||
|
||||
const tippy_die = (info) => {
|
||||
// if hovering current tippy then dont die. if not kill kill kill
|
||||
// if (currentTippy) {
|
||||
// currentTippy.destroy();
|
||||
// currentTippy = null;
|
||||
// }
|
||||
// fuck it
|
||||
// only hide. when another comes current will be killed
|
||||
currentTippy.hideWithInteractivity(info.jsEvent);
|
||||
};
|
||||
|
||||
confirm_button.addEventListener("click", (_e) => {
|
||||
console.log("nawgh:", text_field.value);
|
||||
if (text_field.value) {
|
||||
const calendar = new Calendar(cal, {
|
||||
const url = create_url(text_field.value);
|
||||
if (url) {
|
||||
hidePlaceholder(); // Hide placeholder when loading starts
|
||||
if (calendar) calendar.destroy();
|
||||
calendar = new Calendar(cal_ele, {
|
||||
// calendar config
|
||||
plugins: [
|
||||
bootstrap5Plugin,
|
||||
listPlugin,
|
||||
@@ -65,37 +86,54 @@ confirm_button.addEventListener("click", (_e) => {
|
||||
timeGridPlugin,
|
||||
dayGridPlugin,
|
||||
],
|
||||
themeSystem: "bootstrap5", // Enable Bootstrap 5 theme
|
||||
initialView: "listWeek",
|
||||
// multiMonthMaxColumns: 1,
|
||||
navLinks: true,
|
||||
dayMaxEvents: true,
|
||||
eventMaxStack: 3,
|
||||
dayMaxEventRows: true,
|
||||
nowIndicator: true,
|
||||
weekNumbers: true,
|
||||
headerToolbar: {
|
||||
left: "prev,next",
|
||||
center: "title",
|
||||
right: "today dayGridMonth,timeGridWeek,timeGridDay,listWeek", // Add the views you want
|
||||
right: "today dayGridMonth,timeGridWeek,timeGridDay,listMonth",
|
||||
// views
|
||||
},
|
||||
views: {
|
||||
dayGridMonth: { buttonText: "Month" },
|
||||
timeGridWeek: { buttonText: "Week" },
|
||||
timeGridDay: { buttonText: "Day" },
|
||||
listWeek: { buttonText: "List" },
|
||||
listMonth: { buttonText: "List" },
|
||||
},
|
||||
|
||||
events: {
|
||||
url: text_field.value,
|
||||
url: url,
|
||||
format: "ics",
|
||||
color: getNextColor(),
|
||||
},
|
||||
// eventClick: new_tippy,
|
||||
eventMouseEnter: new_tippy,
|
||||
eventMouseLeave: tippy_die,
|
||||
});
|
||||
|
||||
// end calendar config
|
||||
calendar.render();
|
||||
} else if (!calendar) {
|
||||
showPlaceholder(); // Show placeholder if no URL is provided
|
||||
}
|
||||
});
|
||||
|
||||
// text_field.value = "http://127.0.0.1:8080/filtered-ics";
|
||||
// confirm_button.click();
|
||||
// wow
|
||||
|
||||
add_button.addEventListener("click", () => {
|
||||
if (calendar && text_field.value) {
|
||||
calendar.addEventSource({
|
||||
url: text_field.value,
|
||||
format: "ics",
|
||||
color: getNextColor(),
|
||||
});
|
||||
}
|
||||
});
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
cal_filter_js/src/style.css
|
||||
Custom styles for calendar app
|
||||
*/
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.flex_input {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
margin: 2em auto 1em auto;
|
||||
max-width: 900px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#ics_file {
|
||||
flex: 1 1 0;
|
||||
min-width: 200px;
|
||||
max-width: 100%;
|
||||
font-size: 1.1em;
|
||||
padding: 0.5em 0.8em;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input[type="button"] {
|
||||
padding: 0.5em 1.2em;
|
||||
font-size: 1em;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
input[type="button"]:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
#calendar {
|
||||
/* max-width: 900px; */
|
||||
/* margin: 0 auto 2em auto; */
|
||||
/* background: #fff; */
|
||||
/* border-radius: 8px; */
|
||||
/* box-shadow: 0 2px 12px rgba(0, 0, 0, 0.07); */
|
||||
/* padding: 1em 0.5em 2em 0.5em; */
|
||||
padding-bottom: 1em; /* ultra mega hack */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.calendar-wrapper {
|
||||
position: relative;
|
||||
max-width: 900px;
|
||||
margin: 0 auto 2em auto;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.07);
|
||||
padding: 1em 0.5em 1em 0.5em;
|
||||
}
|
||||
|
||||
/* Placeholder message */
|
||||
.placeholder-message {
|
||||
text-align: center;
|
||||
/* justify-self: center; */
|
||||
color: #6c757d;
|
||||
font-size: 1.2em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.flex_input {
|
||||
flex-direction: column;
|
||||
gap: 0.7em;
|
||||
max-width: 98vw;
|
||||
}
|
||||
#calendar {
|
||||
max-width: 99vw;
|
||||
padding: 0.5em 0.1em 1em 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
/* display: flex; */
|
||||
justify-items: center;
|
||||
padding: 1em 0;
|
||||
border-top: 1px solid #eee;
|
||||
margin-top: 2em;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/** Escapes HTML special characters to prevent XSS */
|
||||
export function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.innerText = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/** Converts URLs in text to clickable links, after escaping */
|
||||
export function linkify(text) {
|
||||
let safe = escapeHtml(text);
|
||||
return safe.replace(
|
||||
/(https?:\/\/[^\s<]+)/g,
|
||||
(url) =>
|
||||
`<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`
|
||||
);
|
||||
}
|
||||
|
||||
/** Formats tooltip content: escapes, linkifies, and replaces newlines with \<br> */
|
||||
export function formatTooltip(title, description) {
|
||||
const safeTitle = escapeHtml(title);
|
||||
const safeDesc = description
|
||||
? `<br><br>${linkify(description).replace(/\n/g, "<br>")}`
|
||||
: "";
|
||||
return `<strong>${safeTitle}</strong>${safeDesc}`;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
function create_url(url) {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export { create_url }
|
||||
Reference in new issue
Block a user