assest pull ever

i should have thought
This commit is contained in:
lda committed 2025-05-27 03:47:21 +07:00
1 parent 1a2582a8a8
commit 50a90db0b2
9 files changed
+218 -200

No files matched your search

+67 -69
View File
@@ -1,85 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<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>
</head>
<body>
<body>
<div class="container mt-5">
<header class="text-center mb-4">
<h1 class="display-4">🗓️ Calendar Filter App</h1>
<p class="lead">Load, filter, and combine calendars with style.</p>
</header>
<header class="text-center mb-4">
<h1 class="display-4">🗓️ Calendar Filter App</h1>
<p class="lead">Load, filter, and combine calendars with style.</p>
</header>
<form class="row g-3" onsubmit="return false;">
<div class="col-12">
<label for="url_mode" class="form-label">Select Mode</label>
<select id="url_mode" class="form-select">
<option value="manual">Manual URL</option>
<option value="args" id="proxy_opt">Send Arguments to Proxy</option>
</select>
</div>
<div id="manual-url-input" class="col-12">
<label for="ics_file" class="form-label">ICS URL</label>
<input type="text" id="ics_file" class="form-control"
placeholder="Paste .ics URL or API endpoint here" />
</div>
<div id="args-form" class="col-12" style="display: none">
<label for="be_url" class="form-label">Calendar URL</label>
<input type="text" id="be_url" class="form-control mb-2" placeholder="Enter .ics URL to query" />
<label for="regex_exc" class="form-label">Regex Exclude Rules</label>
<input type="text" id="regex_exc" class="form-control mb-2"
placeholder="Enter regex to exclude events" />
<label for="regex_inc" class="form-label">Regex Include Rules</label>
<input type="text" id="regex_inc" class="form-control mb-2"
placeholder="Enter regex to include events" />
</div>
<div class="col-12 d-flex justify-content-end flex-wrap gap-2">
<button type="button" class="btn btn-primary" id="ics_run">
New Calendar
</button>
<button type="button" class="btn btn-secondary" id="ics_add">
Add a Calendar
</button>
</div>
</form>
<div id="placeholder-message" class="alert alert-warning mt-4">
No calendar loaded yet. Paste a URL to get started!
<form class="row g-3" onsubmit="return false;">
<div class="col-auto">
<label for="url_mode" class="form-label">Select Mode</label>
<select id="url_mode" class="form-select">
<option value="manual">Manual URL</option>
<option value="args">Send Arguments to Proxy</option>
</select>
</div>
<div id="event-source-management" class="card mt-4" style="display: none">
<div class="card-header mt-1 d-flex justify-content-between align-items-center bg-transparent border-0">
<h4 class="mb-0">Event Sources</h4>
<button id="reload" class="btn btn-outline-primary btn-sm">
<i class="bi bi-arrow-clockwise"></i> Reload
</button>
</div>
<div id="event-source-list" class="card-body">
<!-- Event source cards will be dynamically added here -->
</div>
<div id="manual-url-input" class="col">
<label for="ics_file" class="form-label">ICS URL</label>
<input type="text" id="ics_file" class="form-control" placeholder="Paste .ics URL or API endpoint here" />
</div>
<div id="calendar" class="mt-4"></div>
<div id="args-form" class="col-12" style="display: none;">
<label for="be_url" class="form-label">Calendar URL</label>
<input type="text" id="be_url" class="form-control mb-2" placeholder="Enter .ics URL to query" />
<footer class="text-center mt-4">
<p>Made with Rust, TS, and ❤️</p>
<p>
by
<a href="//ldlda.com" target="_blank" rel="noopener noreferrer">lđa</a>
</p>
</footer>
<label for="regex_exc" class="form-label">Regex Exclude Rules</label>
<input type="text" id="regex_exc" class="form-control mb-2" placeholder="Enter regex to exclude events" />
<label for="regex_inc" class="form-label">Regex Include Rules</label>
<input type="text" id="regex_inc" class="form-control mb-2" placeholder="Enter regex to include events" />
</div>
<div class="col-12 d-flex justify-content-end flex-wrap gap-2">
<button type="button" class="btn btn-primary" id="ics_run">New Calendar</button>
<button type="button" class="btn btn-secondary" id="ics_add">Add a Calendar</button>
</div>
</form>
<div id="placeholder-message" class="alert alert-warning mt-4">
No calendar loaded yet. Paste a URL to get started!
</div>
<div id="event-source-management" class="card mt-4" style="display: none">
<div
class="card-header mt-1 d-flex justify-content-between align-items-center bg-transparent border-0"
>
<h4 class="mb-0">Event Sources</h4>
<button id="reload" class="btn btn-outline-primary btn-sm">
<i class="bi bi-arrow-clockwise"></i> Reload
</button>
</div>
<div id="event-source-list" class="card-body">
<!-- Event source cards will be dynamically added here -->
<div id="placeholder-message" class="alert alert-warning">
no source
</div>
</div>
</div>
<div id="calendar" class="mt-4"></div>
<footer class="text-center mt-4">
<p>Made with Rust, TS, and ❤️</p>
<p>
by
<a href="//ldlda.com" target="_blank" rel="noopener noreferrer"
>lđa</a
>
</p>
</footer>
</div>
</body>
</html>
</body>
</html>
+12 -10
View File
@@ -1,17 +1,17 @@
import bootstrap5Plugin from "@fullcalendar/bootstrap5";
import {
Calendar,
EventHoveringArg,
EventSourceInput,
} from "@fullcalendar/core";
import iCalendarPlugin from "@fullcalendar/icalendar";
import rrulePlugin from "@fullcalendar/rrule";
import listPlugin from "@fullcalendar/list";
import dayGridPlugin from "@fullcalendar/daygrid";
import iCalendarPlugin from "@fullcalendar/icalendar";
import listPlugin from "@fullcalendar/list";
import rrulePlugin from "@fullcalendar/rrule";
import timeGridPlugin from "@fullcalendar/timegrid";
import bootstrap5Plugin from "@fullcalendar/bootstrap5";
import { getNextColor } from "./color_generator";
import { updateEventSourceList } from "./main";
import { loading_callback } from "./main";
let calendar: Calendar | null = null;
@@ -61,7 +61,11 @@ export function initializeCalendar(
events: init_ics_source(url.toString()),
eventMouseEnter: onEventMouseEnter,
eventMouseLeave: onEventMouseLeave,
loading: () => calendar && updateEventSourceList(calendar),
// loading: () => calendar && updateEventSourceList(calendar),
loading: (isloading) => {
calendar?.getEventSources().forEach(console.log);
loading_callback(isloading);
},
});
calendar.render();
return calendar; // its real now
@@ -71,10 +75,8 @@ export function initializeCalendar(
* Adds a new event source to the calendar.
* @param url - The URL for the event source.
*/
export const addIcsEventSource = (url: string): void => {
if (calendar) {
calendar.addEventSource(init_ics_source(url));
}
export const addIcsEventSource = (url: string) => {
return calendar?.addEventSource(init_ics_source(url));
};
/**
+1 -1
View File
@@ -1,5 +1,5 @@
// Start with a random hue, but keep saturation and lightness fixed for "nice" colors
let sourceCount = Math.floor(Math.random() * 25);
let sourceCount = Math.floor(Math.random() * 360);
/**
* Generates the next color in a sequence using the golden angle.
+97 -81
View File
@@ -1,85 +1,101 @@
import { Calendar } from "@fullcalendar/core";
import { Calendar, EventSourceApi } from "@fullcalendar/core";
import { loading_callback } from "./main";
/**
* Updates the event source list in the DOM.
* @param eventSourceList - The DOM element to display the event sources.
* @returns A function that updates the event source list for a given calendar.
*/
export const updateEventSourceList2 = (eventSourceList: HTMLElement | null) => {
return function updateEventSourceList(calendar: Calendar): void {
if (!eventSourceList) return;
if (!calendar) return;
export interface InternalEventSource {
isFetching: boolean;
ui: { borderColor?: string };
}
// Clear the current list
export interface CustomEventSource extends EventSourceApi {
internalEventSource: InternalEventSource;
}
// interface loadingIndicatorCard extends HTMLDivElement {
// refreshLoading: Function;
// }
export function addEventSourceCard(
eventSourceList: HTMLElement,
source: CustomEventSource
) {
const internalEventSource = source.internalEventSource;
const card = document.createElement("div");
const color = internalEventSource.ui.borderColor || "#007bff";
card.className = "card mb-3";
card.style.borderLeft = `4px solid ${color}`;
card.dataset.sourceUrl = source.url;
const cardBody = document.createElement("div");
cardBody.className = "card-body";
const info = document.createElement("div");
info.className = "mb-2";
const url = source.url || "(No URL)";
const format = source.format || "(Unknown Format)";
info.innerHTML = `
<div><strong>Source:</strong> <a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a></div>
<div><strong>Format:</strong> <span>${format}</span></div>
`;
const loadingStatus = document.createElement("div");
loadingStatus.className = "text-muted small flex-grow-1";
updateLoadingStatus(loadingStatus, source);
const refetchButton = document.createElement("button");
refetchButton.textContent = "Refetch";
refetchButton.className = "btn btn-sm btn-primary";
refetchButton.addEventListener("click", () => {
source.refetch();
});
const removeButton = document.createElement("button");
removeButton.textContent = "Remove";
removeButton.className = "btn btn-sm btn-danger";
removeButton.addEventListener("click", () => {
source.remove();
loading_callback(false);
});
const actionContainer = document.createElement("div");
actionContainer.className = "d-flex justify-content-between flex-wrap";
actionContainer.appendChild(loadingStatus);
const buttonContainer = document.createElement("div");
buttonContainer.className = "d-flex align-items-center gap-1 flex-wrap";
buttonContainer.appendChild(refetchButton);
buttonContainer.appendChild(removeButton);
actionContainer.appendChild(buttonContainer);
cardBody.appendChild(info);
cardBody.appendChild(actionContainer);
card.appendChild(cardBody);
eventSourceList.appendChild(card);
}
export function updateLoadingStatus(
loadingStatus: HTMLElement,
source: CustomEventSource
) {
loadingStatus.textContent = source.internalEventSource.isFetching
? "Fetching in progress"
: "Loaded";
}
export const rerender_all_statuses = (
eventSourceList: HTMLElement,
calendar: Calendar | null
) =>
setTimeout(() => {
eventSourceList.innerHTML = "";
// Fetch all event sources
calendar.getEventSources().forEach((source) => {
console.log(source);
const card = document.createElement("div");
const color = source.internalEventSource.ui.borderColor || "#007bff"; // Default to Bootstrap primary color
card.className = "card mb-3";
card.style.borderLeft = `4px solid ${color}`;
// cardBody serves as a container for all card content
const cardBody = document.createElement("div");
cardBody.className = "card-body";
const info = document.createElement("div");
info.className = "mb-2";
const url = source.url || "(No URL)";
const format = source.format || "(Unknown Format)";
info.innerHTML = `
<div>
<strong>Source:</strong>
<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>
</div>
<div><strong>Format:</strong> <span>${format}</span></div>
`;
const loadingStatus = document.createElement("div");
loadingStatus.className = "text-muted small flex-grow-1";
loadingStatus.textContent = source.internalEventSource.isFetching
? "Fetching in progress"
: "Loaded";
// Add Refetch Button
const refetchButton = document.createElement("button");
refetchButton.textContent = "Refetch";
refetchButton.className = "btn btn-sm btn-primary";
refetchButton.addEventListener("click", () => {
source.refetch();
});
// Add Remove Button
const removeButton = document.createElement("button");
removeButton.textContent = "Remove";
removeButton.className = "btn btn-sm btn-danger";
removeButton.addEventListener("click", () => {
source.remove();
updateEventSourceList(calendar); // Refresh the list after removal
});
// Create a container for loading status and buttons
const actionContainer = document.createElement("div");
actionContainer.className = "d-flex align-items-center gap-2 flex-wrap";
// Add loading status to the container
actionContainer.appendChild(loadingStatus);
// Add buttons to the container
actionContainer.appendChild(refetchButton);
actionContainer.appendChild(removeButton);
// Append info and action container to the card body
cardBody.appendChild(info);
cardBody.appendChild(actionContainer);
// Append the card body to the card
card.appendChild(cardBody);
// Append the card to the event source list
eventSourceList.appendChild(card);
calendar?.getEventSources().forEach((eventSource) => {
addEventSourceCard(eventSourceList, eventSource);
});
};
};
const parent = eventSourceList.parentElement;
if (parent)
!eventSourceList.innerHTML
? (parent.style.display = "none")
: (parent.style.display = "");
}) && undefined;
// eventSourceList.innerHTML = `
// <div id="placeholder-message" class="alert alert-warning">
// no source
// </div>`;
+13 -3
View File
@@ -13,9 +13,9 @@ export function getFormParams(): Record<string, string> {
return params;
}
export function toggleFormVisibility(mode: string): void {
const manualUrlInput = getElementById<HTMLElement>("manual-url-input");
const argsForm = getElementById<HTMLElement>("args-form");
const manualUrlInput = getElementById<HTMLElement>("manual-url-input");
const argsForm = getElementById<HTMLElement>("args-form");
export function handle_form_change(mode: string): void {
if (mode === "manual") {
manualUrlInput.style.display = "block";
@@ -27,4 +27,14 @@ export function toggleFormVisibility(mode: string): void {
manualUrlInput.style.display = "block";
argsForm.style.display = "none";
}
// // Re-enable all form elements
// manualUrlInput.querySelectorAll("input, select, button").forEach((el) => {
// (el as HTMLInputElement | HTMLSelectElement | HTMLButtonElement).disabled =
// false;
// });
// argsForm.querySelectorAll("input, select, button").forEach((el) => {
// (el as HTMLInputElement | HTMLSelectElement | HTMLButtonElement).disabled =
// false;
// });
}
+22 -28
View File
@@ -1,13 +1,13 @@
import {
initializeCalendar,
addIcsEventSource,
getCalendarInstance,
initializeCalendar,
} from "./calendar";
import { newTippy, tippyDie } from "./tooltip";
import { updateEventSourceList2 } from "./eventSourceManager";
import { getElementById } from "./utils";
import { rerender_all_statuses } from "./eventSourceManager";
import { getFormParams, handle_form_change } from "./formHandler";
import { manageUrl } from "./queryHandler";
import { getFormParams, toggleFormVisibility } from "./formHandler";
import { newTippy, tippyDie } from "./tooltip";
import { getElementById } from "./utils";
const text_field = getElementById<HTMLInputElement>("ics_file");
const confirm_button = getElementById<HTMLButtonElement>("ics_run");
@@ -18,13 +18,9 @@ const eventSourceList = getElementById<HTMLElement>("event-source-list");
const eventmanage = getElementById<HTMLElement>("event-source-management");
const reload_button = getElementById<HTMLButtonElement>("reload");
const urlMode = getElementById<HTMLSelectElement>("url_mode");
const proxy_option = getElementById<HTMLSelectElement>("proxy_opt");
export const updateEventSourceList = updateEventSourceList2(eventSourceList);
export const baseUrl = "http://127.0.0.1:8080/filtered-ics";
urlMode.addEventListener("change", () => {
toggleFormVisibility(urlMode.value);
handle_form_change(urlMode.value);
});
function handleConfirmClick(): void {
@@ -33,14 +29,12 @@ function handleConfirmClick(): void {
return alert("Please input a valid URL.");
}
{
placeholder_message.style.display = "none";
eventmanage.style.display = "block";
cal_ele.style.display = "block";
}
placeholder_message.style.display = "none";
eventmanage.style.display = "block";
cal_ele.style.display = "block";
const calendar = initializeCalendar(cal_ele, url, newTippy, tippyDie);
updateEventSourceList(calendar);
rerender_all_statuses(eventSourceList, calendar);
}
function handleAddClick(): void {
@@ -52,27 +46,27 @@ function handleAddClick(): void {
const calendar = getCalendarInstance();
if (calendar) {
addIcsEventSource(url);
updateEventSourceList(calendar);
rerender_all_statuses(eventSourceList, calendar);
}
}
function fetch_url_form() {
const params = getFormParams();
const url = manageUrl(urlMode.value, text_field.value, baseUrl, params);
const baseUrl = text_field.value || "http://127.0.0.1:8080/filtered-ics";
const url = manageUrl(urlMode.value, baseUrl, params);
return url;
}
function handleReloadClick(): void {
const calendar = getCalendarInstance();
if (calendar) {
updateEventSourceList(calendar);
}
rerender_all_statuses(eventSourceList, calendar);
}
// running
{
proxy_option.textContent += ` (currently "${baseUrl}")`;
confirm_button.addEventListener("click", handleConfirmClick);
add_button.addEventListener("click", handleAddClick);
reload_button.addEventListener("click", handleReloadClick);
}
export const loading_callback = (_: unknown) => {
const calendar = getCalendarInstance();
rerender_all_statuses(eventSourceList, calendar);
};
confirm_button.addEventListener("click", handleConfirmClick);
add_button.addEventListener("click", handleAddClick);
reload_button.addEventListener("click", handleReloadClick);
+4 -3
View File
@@ -22,14 +22,15 @@ export function queryCustomUrl(
export function manageUrl(
mode: string,
manualInput: string,
base_url: string,
params: Record<string, string>
): string | null {
if (mode === "manual") {
return create_url(manualInput);
const url = create_url(base_url);
return url;
} else if (mode === "args") {
return constructUrl(base_url, params);
const url = constructUrl(base_url, params);
return url;
}
return null;
}
+1 -4
View File
@@ -8,10 +8,7 @@
@import "tippy.js/dist/tippy.css";
body {
/* font-family: system-ui, sans-serif; */
background: #f8f9fa !important; /* cant be used since overriden */
/* margin: 0;
padding: 0; */
background: #f8f9fa !important; /* override bootstrap body */
display: flex;
justify-content: center;
align-items: center;
+1 -1
View File
@@ -1,7 +1,7 @@
import { EventHoveringArg } from "@fullcalendar/core";
import tippy, { Instance, Props } from "tippy.js";
import "tippy.js/dist/tippy.css";
import { formatTooltip } from "./tooltip_parser";
import { EventHoveringArg } from "@fullcalendar/core";
let currentTippy: Instance<Props> | null = null;