This commit is contained in:
1 parent
0a0e623dad
commit
6a74253339
8 files changed
+267
-277
No files matched your search
+56
-29
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user