This commit is contained in:
lda committed 2025-05-26 06:41:12 +07:00
1 parent 0a0e623dad
commit 6a74253339
8 files changed
+267 -277

No files matched your search

+56 -29
View File
@@ -5,48 +5,75 @@
<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" />
<link rel="stylesheet" href="/src/style.css"/>
<script type="module" src="/src/main.js" defer></script>
</head>
<body>
<div class="container">
<header class="app-header">
<h1>🗓️ Calendar Filter App</h1>
<p class="subtitle">Load, filter, and combine calendars with style.</p>
<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>
<form class="flex_input" onsubmit="return false;">
<input type="text" id="ics_file" placeholder="Paste .ics URL or API endpoint here" />
<button type="button" class="btn btn-primary" id="ics_run">New Calendar</button>
<button type="button" class="btn btn-primary" id="ics_add">Add a Calendar</button>
<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 class="info-bar">
<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 id="placeholder-message" class="alert alert-warning mt-4">
No calendar loaded yet. Paste a URL to get started!
</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>
<div class="event-source-management" id="event-source-management">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4>Event Sources</h4>
<button id="reload" class="btn btn-outline-primary">
<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">
<div id="event-source-list" class="card-body">
<!-- Event source cards will be dynamically added here -->
</div>
</div>
<footer class="app-footer">
<p>Made with Rust, JS, and ❤️</p>
<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>
+4 -4
View File
@@ -7,7 +7,7 @@ import timeGridPlugin from "@fullcalendar/timegrid";
import bootstrap5Plugin from "@fullcalendar/bootstrap5";
import { getNextColor } from "./color_generator";
import { glued } from "./main";
import { updateEventSourceList } from "./main";
let calendar: Calendar | null = null;
@@ -57,17 +57,17 @@ export function initializeCalendar(
events: init_ics_source(url.toString()),
eventMouseEnter: onEventMouseEnter,
eventMouseLeave: onEventMouseLeave,
loading: glued,
loading: () => calendar && updateEventSourceList(calendar),
});
calendar.render();
return calendar // its real now
return calendar; // its real now
}
/**
* Adds a new event source to the calendar.
* @param url - The URL for the event source.
*/
export const addEventSource = (url: string): void => {
export const addIcsEventSource = (url: string): void => {
if (calendar) {
calendar.addEventSource(init_ics_source(url));
}
+37 -19
View File
@@ -15,18 +15,30 @@ export const updateEventSourceList2 = (eventSourceList: HTMLElement | null) => {
// Fetch all event sources
calendar.getEventSources().forEach((source) => {
console.log(source);
const card = document.createElement("div");
card.className = "event-source-card";
card.style.borderLeftColor = source.internalEventSource.ui.borderColor;
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 = "info"; // Add class for styling
info.className = "mb-2";
const url = source.url || "(No URL)";
const format = source.format || "(Unknown Format)";
info.innerHTML = `<strong>Source:</strong> <a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a><br><strong>Format:</strong> <span>${format}</span>`;
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 = "loading-status";
loadingStatus.className = "text-muted small flex-grow-1";
loadingStatus.textContent = source.internalEventSource.isFetching
? "Fetching in progress"
: "Loaded";
@@ -34,31 +46,37 @@ export const updateEventSourceList2 = (eventSourceList: HTMLElement | null) => {
// Add Refetch Button
const refetchButton = document.createElement("button");
refetchButton.textContent = "Refetch";
refetchButton.className = "btn btn-sm btn-primary align-right";
refetchButton.className = "btn btn-sm btn-primary";
refetchButton.addEventListener("click", () => {
source.refetch(); // loading callback handled thing
// alert("Source refetched!");
source.refetch();
});
// Add Remove Button
const removeButton = document.createElement("button");
removeButton.textContent = "Remove";
removeButton.className = "btn btn-sm btn-danger align-right";
removeButton.className = "btn btn-sm btn-danger";
removeButton.addEventListener("click", () => {
source.remove();
updateEventSourceList(calendar); // Refresh the list after removal
});
// Create a button container
const buttonContainer = document.createElement("div");
buttonContainer.className = "button-container";
buttonContainer.appendChild(refetchButton);
buttonContainer.appendChild(removeButton);
// Create a container for loading status and buttons
const actionContainer = document.createElement("div");
actionContainer.className = "d-flex align-items-center gap-2 flex-wrap";
// Append info and buttons to the card
card.appendChild(info);
card.appendChild(loadingStatus);
card.appendChild(buttonContainer);
// 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);
+31
View File
@@ -0,0 +1,31 @@
import { getElementById } from "./utils";
const backend_url = getElementById<HTMLInputElement>("be_url");
const excludedRegexInput = getElementById<HTMLInputElement>("regex_exc");
const includeRegexInput = getElementById<HTMLInputElement>("regex_inc");
export function getFormParams(): Record<string, string> {
const params = {
url: backend_url.value,
regex: excludedRegexInput.value,
include: includeRegexInput.value,
};
return params;
}
export function toggleFormVisibility(mode: string): void {
const manualUrlInput = getElementById<HTMLElement>("manual-url-input");
const argsForm = getElementById<HTMLElement>("args-form");
if (mode === "manual") {
manualUrlInput.style.display = "block";
argsForm.style.display = "none";
} else if (mode === "args") {
manualUrlInput.style.display = "none";
argsForm.style.display = "block";
} else {
manualUrlInput.style.display = "block";
argsForm.style.display = "none";
}
}
+47 -76
View File
@@ -1,111 +1,82 @@
import {
initializeCalendar,
addEventSource,
addIcsEventSource,
getCalendarInstance,
} from "./calendar";
import { newTippy, tippyDie } from "./tooltip";
import { updateEventSourceList2 } from "./eventSourceManager";
import { create_url } from "./utils";
import { getElementById } from "./utils";
import { manageUrl } from "./queryHandler";
import { getFormParams, toggleFormVisibility } from "./formHandler";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-icons/font/bootstrap-icons.css";
import "tippy.js/dist/tippy.css";
const text_field = document.getElementById("ics_file") as HTMLInputElement;
const text_field = getElementById<HTMLInputElement>("ics_file");
const confirm_button = getElementById<HTMLButtonElement>("ics_run");
const add_button = getElementById<HTMLButtonElement>("ics_add");
const cal_ele = getElementById<HTMLElement>("calendar");
const placeholder_message = getElementById<HTMLElement>("placeholder-message");
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");
const confirm_button = document.getElementById("ics_run") as HTMLButtonElement;
export const updateEventSourceList = updateEventSourceList2(eventSourceList);
export const baseUrl = "http://127.0.0.1:8080/filtered-ics";
const add_button = document.getElementById("ics_add") as HTMLButtonElement;
urlMode.addEventListener("change", () => {
toggleFormVisibility(urlMode.value);
});
const cal_ele = document.getElementById("calendar") as HTMLElement;
const placeholder_message = document.getElementById(
"placeholder-message"
) as HTMLElement;
const eventSourceList = document.getElementById(
"event-source-list"
) as HTMLElement;
const eventmanage = document.getElementById(
"event-source-management"
) as HTMLElement;
const reload_button = document.getElementById("reload") as HTMLButtonElement;
const updateEventSourceList = updateEventSourceList2(eventSourceList);
/**
* Shows the placeholder message and hides the calendar.
*/
function showPlaceholder(): void {
placeholder_message.style.display = "block";
eventmanage.style.display = "none";
cal_ele.style.display = "none";
}
/**
* Hides the placeholder message and shows the calendar.
*/
function hidePlaceholder(): void {
placeholder_message.style.display = "none";
eventmanage.style.display = "block";
cal_ele.style.display = "block";
}
/**
* Updates the event source list when the calendar is modified.
* @param _e - The event triggering the update (unused).
*/
export const glued = (_e: unknown): void => {
const calendar = getCalendarInstance();
if (calendar) {
setTimeout(() => updateEventSourceList(calendar)); // ahh
}
};
// Remove existing listeners before adding new ones
confirm_button.removeEventListener("click", handleConfirmClick);
confirm_button.addEventListener("click", handleConfirmClick);
add_button.removeEventListener("click", handleAddClick);
add_button.addEventListener("click", handleAddClick);
reload_button.removeEventListener("click", handleReloadClick);
reload_button.addEventListener("click", handleReloadClick);
// Define the event handler functions
function handleConfirmClick(): void {
const url = create_url(text_field.value);
if (!url)
return text_field.value
? alert("Please input valid url.")
: showPlaceholder();
const url = fetch_url_form();
if (!url) {
return alert("Please input a valid URL.");
}
{
placeholder_message.style.display = "none";
eventmanage.style.display = "block";
cal_ele.style.display = "block";
}
hidePlaceholder();
const calendar = initializeCalendar(cal_ele, url, newTippy, tippyDie);
updateEventSourceList(calendar);
}
function handleAddClick(): void {
const url = create_url(text_field.value);
const url = fetch_url_form();
if (!url) {
if (text_field.value) alert("Please input valid url.");
return;
return alert("Please input a valid URL.");
}
const calendar = getCalendarInstance();
if (calendar) {
addEventSource(url);
addIcsEventSource(url);
updateEventSourceList(calendar);
}
}
function fetch_url_form() {
const params = getFormParams();
const url = manageUrl(urlMode.value, text_field.value, baseUrl, params);
return url;
}
function handleReloadClick(): void {
const calendar = getCalendarInstance();
if (calendar) {
updateEventSourceList(calendar);
alert("Event sources reloaded!");
} else {
alert("No calendar instance found.");
}
}
// running
{
proxy_option.textContent += ` (currently "${baseUrl}")`;
confirm_button.addEventListener("click", handleConfirmClick);
add_button.addEventListener("click", handleAddClick);
reload_button.addEventListener("click", handleReloadClick);
}
+36
View File
@@ -0,0 +1,36 @@
import { constructUrl, create_url } from "./utils";
const localhost = "http://127.0.0.1:8080/filtered-ics";
export function queryLocalhost(params: Record<string, string>): string {
return constructUrl(localhost, params) as string;
}
export function queryCustomUrl(
baseUrl: string,
params: Record<string, string>
): string | null {
return constructUrl(baseUrl, params);
}
/**
* Manages the URL based on the selected mode (manual or proxy) and validates it.
* @param mode - The selected mode ('manual' or 'args').
* @param manualInput - The manual URL input.
* @param params - The query parameters for the proxy mode.
* @returns The constructed and validated URL or null if invalid.
*/
export function manageUrl(
mode: string,
manualInput: string,
base_url: string,
params: Record<string, string>
): string | null {
if (mode === "manual") {
return create_url(manualInput);
} else if (mode === "args") {
return constructUrl(base_url, params);
}
return null;
}
+6 -149
View File
@@ -2,157 +2,14 @@
cal_filter_js/src/style.css
Custom styles for calendar app
*/
body {
font-family: system-ui, sans-serif;
background: #f8f9fa;
margin: 0;
padding: 0;
/* font-family: system-ui, sans-serif; */
background: #f8f9fa !important; /* cant be used since overriden */
/* margin: 0;
padding: 0; */
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
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;
align-items: center;
justify-content: flex-end;
}
#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;
}
/* trying bootstraps button */
/* 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;
} */
.info-bar {
text-align: center;
}
#calendar {
padding-bottom: 1em; /* ultra mega hack */
display: none;
}
.calendar-wrapper {
position: relative;
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;
color: #6c757d;
font-size: 1.2em;
margin: 1em 0;
}
@media (max-width: 768px) {
.flex_input {
flex-wrap: wrap;
gap: 0.7em;
}
#calendar {
padding: 0.5em 0.1em 1em 0.1em;
}
#ics_file {
flex: 1 1 100%;
}
}
.app-footer {
justify-items: center;
padding: 1em 0;
border-top: 1px solid #eee;
margin-top: 2em;
font-size: 0.95em;
}
.event-source-management {
margin-top: 2em;
padding: 1em;
background: #f8f9fa;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.07);
display: none;
}
.event-source-card {
display: flex;
justify-content: space-between;
align-items: stretch;
border: 1px solid #ddd;
border-left: 4px solid #007bff; /* Default border color */
padding: 0.5em;
margin-bottom: 0.5em;
background: #ffffff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
background-color: #f9f9f9;
flex-wrap: wrap; /* Allow wrapping for smaller screens */
}
.event-source-card .button-container {
display: flex;
gap: 0.5em;
flex-wrap: wrap;
flex-direction: row; /* Align buttons horizontally */
justify-content: space-between; /* Spread buttons evenly */
}
.event-source-card .loading-status {
font-size: 0.9em;
color: #6c757d;
margin-top: 0.5em;
}
.event-source-card .info {
overflow: hidden; /* Prevent content from overflowing */
text-overflow: ellipsis; /* Add ellipsis for long text */
flex: 1 1 100%; /* Make the info section span full width */
margin-bottom: 0.5em; /* Add spacing below the info */
}
.loading-status {
font-size: 0.9em;
color: #6c757d;
}
}
+50
View File
@@ -11,3 +11,53 @@ export function create_url(input: string): string | null {
return null;
}
}
/**
* Constructs a URL with query parameters.
* @param baseUrl - The base URL to which query parameters will be added.
* @param params - An object representing the query parameters.
* @returns The constructed URL with query parameters.
*/
export function constructUrl(
baseUrl: string,
params: Record<string, string>
): string | null {
let url;
try {
url = new URL(baseUrl);
} catch {
return null;
}
Object.entries(params).forEach(([key, value]) => {
value !== "" && url.searchParams.append(key, value);
});
return url.toString();
}
/**
* Validates if the given string is a valid URL.
* @param url - The URL string to validate.
* @returns True if the URL is valid, false otherwise.
*/
export function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
/**
* Gets an element by its ID and casts it to the specified type.
* @param id - The ID of the element to retrieve.
* @returns The element with the specified ID.
* @throws If the element is not found.
*/
export function getElementById<T extends HTMLElement>(id: string): T {
const element = document.getElementById(id) as T;
if (!element) {
throw new Error(`Element with id '${id}' not found.`);
}
return element;
}