Compare commits
3
Commits
No files matched your search
+13
-6
@@ -19,7 +19,7 @@
|
||||
<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>
|
||||
<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
|
||||
@@ -33,12 +33,19 @@
|
||||
<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 class="event-source-management" id="event-source-management">
|
||||
<h2>Event Sources</h2>
|
||||
<div id="event-source-list">
|
||||
<!-- Event source cards will be dynamically added here -->
|
||||
</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>
|
||||
Generated
+28
@@ -14,6 +14,7 @@
|
||||
"@fullcalendar/icalendar": "^6.1.17",
|
||||
"@fullcalendar/list": "^6.1.17",
|
||||
"@fullcalendar/multimonth": "^6.1.17",
|
||||
"@fullcalendar/rrule": "^6.1.17",
|
||||
"@fullcalendar/timegrid": "^6.1.17",
|
||||
"bootstrap": "^5.3.6",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
@@ -506,6 +507,16 @@
|
||||
"@fullcalendar/core": "~6.1.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/rrule": {
|
||||
"version": "6.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/rrule/-/rrule-6.1.17.tgz",
|
||||
"integrity": "sha512-p4CgAQQA/2HJUMmznhTE2b/YDiYyQsixs6D3/oM0+cCS0Sxm5zdWpfqwrMY3pr4iNwzJKKO4HGPwdtOLvAeJXw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.17",
|
||||
"rrule": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/timegrid": {
|
||||
"version": "6.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.17.tgz",
|
||||
@@ -1046,6 +1057,16 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rrule": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz",
|
||||
"integrity": "sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1082,6 +1103,13 @@
|
||||
"@popperjs/core": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@fullcalendar/icalendar": "^6.1.17",
|
||||
"@fullcalendar/list": "^6.1.17",
|
||||
"@fullcalendar/multimonth": "^6.1.17",
|
||||
"@fullcalendar/rrule": "^6.1.17",
|
||||
"@fullcalendar/timegrid": "^6.1.17",
|
||||
"bootstrap": "^5.3.6",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
|
||||
@@ -15,3 +15,158 @@ git clone this repo
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## ass pull
|
||||
|
||||
Got it! Let’s create a **long-term plan** to improve your project. The goal is to make it feature-rich, maintainable, and user-friendly while leveraging FullCalendar’s capabilities and your existing backend.
|
||||
|
||||
---
|
||||
|
||||
### **Long-Term Plan**
|
||||
|
||||
#### **1. Frontend Filtering**
|
||||
|
||||
Move filtering logic (date, regex, etc.) to the frontend using FullCalendar’s `getEvents()` and `eventDataTransform`.
|
||||
|
||||
##### **Steps:**
|
||||
|
||||
1. Use `getEvents()` to retrieve all events from the calendar.
|
||||
2. Apply filters (date range, regex, etc.) directly on the frontend.
|
||||
3. Add a UI for users to specify filters (e.g., date pickers, regex input).
|
||||
4. Use `eventDataTransform` to preprocess events when they are fetched.
|
||||
|
||||
---
|
||||
|
||||
#### **2. Loading State**
|
||||
|
||||
Replace the current placeholder/spinner logic with FullCalendar’s built-in `loading` callback.
|
||||
|
||||
##### **Steps:**
|
||||
|
||||
1. Use the `loading` callback to show/hide a spinner or loading message.
|
||||
2. Remove manual spinner logic from your code.
|
||||
|
||||
---
|
||||
|
||||
#### **3. List of Event Sources**
|
||||
|
||||
Display a list of all added event sources and allow users to:
|
||||
|
||||
- Remove sources.
|
||||
- Refetch events from a source.
|
||||
- Toggle visibility of events from a source.
|
||||
|
||||
##### **Steps:**
|
||||
|
||||
1. Use `getEventSources()` to retrieve all sources.
|
||||
2. Display them in a list with controls (e.g., remove, refetch, toggle).
|
||||
3. Use `EventSource::remove()` and `EventSource::refetch()` for interactivity.
|
||||
|
||||
---
|
||||
|
||||
#### **4. Improve CORS Handling**
|
||||
|
||||
If the backend is only for bypassing Google Calendar’s CORS issues, consider:
|
||||
|
||||
- Adding a proxy endpoint for other calendar services.
|
||||
- Allowing users to directly fetch events from public calendars without needing the backend.
|
||||
|
||||
---
|
||||
|
||||
#### **5. Modularize Calendar Configuration**
|
||||
|
||||
Centralize FullCalendar configuration into a reusable function or object.
|
||||
|
||||
##### **Steps:**
|
||||
|
||||
1. Create a `getCalendarConfig()` function that returns the calendar configuration.
|
||||
2. Pass dynamic options (e.g., theme, initial view) as arguments.
|
||||
|
||||
---
|
||||
|
||||
#### **6. Add Advanced Features**
|
||||
|
||||
Enhance the app with the following features:
|
||||
|
||||
- **Event Editing**: Allow users to edit event details (e.g., title, date).
|
||||
- **Event Creation**: Add a form to create new events.
|
||||
- **Export Events**: Allow users to export filtered events as `.ics` files.
|
||||
|
||||
---
|
||||
|
||||
#### **7. Improve UI/UX**
|
||||
|
||||
Make the app more user-friendly:
|
||||
|
||||
- Add tooltips for buttons and inputs.
|
||||
- Improve responsiveness for mobile devices.
|
||||
- Add a dark mode toggle.
|
||||
|
||||
---
|
||||
|
||||
#### **8. Clean Up CSS**
|
||||
|
||||
- Remove hacky CSS rules (e.g., `padding-bottom: 1em` for `#calendar`).
|
||||
- Use consistent spacing and alignment.
|
||||
- Add comments to explain key styles.
|
||||
|
||||
---
|
||||
|
||||
#### **9. Testing**
|
||||
|
||||
Add automated tests for:
|
||||
|
||||
- Filtering logic.
|
||||
- Event source management.
|
||||
- UI interactions.
|
||||
|
||||
---
|
||||
|
||||
### **Implementation Plan**
|
||||
|
||||
#### **Phase 1: Frontend Filtering**
|
||||
|
||||
1. Add a filter UI (date pickers, regex input).
|
||||
2. Use `getEvents()` to retrieve events and apply filters.
|
||||
3. Update the calendar dynamically based on the filtered events.
|
||||
|
||||
---
|
||||
|
||||
#### **Phase 2: Loading State**
|
||||
|
||||
1. Replace manual spinner logic with FullCalendar’s `loading` callback.
|
||||
2. Test loading behavior with multiple event sources.
|
||||
|
||||
---
|
||||
|
||||
#### **Phase 3: Event Source Management**
|
||||
|
||||
1. Use `getEventSources()` to display a list of sources.
|
||||
2. Add controls for removing, refetching, and toggling sources.
|
||||
|
||||
---
|
||||
|
||||
#### **Phase 4: Advanced Features**
|
||||
|
||||
1. Add event editing and creation forms.
|
||||
2. Implement event export functionality.
|
||||
|
||||
---
|
||||
|
||||
#### **Phase 5: UI/UX Improvements**
|
||||
|
||||
1. Add tooltips and improve responsiveness.
|
||||
2. Implement dark mode.
|
||||
|
||||
---
|
||||
|
||||
#### **Phase 6: Cleanup and Testing**
|
||||
|
||||
1. Refactor CSS and JavaScript for maintainability.
|
||||
2. Add automated tests.
|
||||
|
||||
---
|
||||
|
||||
### **Next Steps**
|
||||
|
||||
Let me know which phase you’d like to start with, and I’ll help you implement it step by step!
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Calendar } from "@fullcalendar/core";
|
||||
import iCalendarPlugin from "@fullcalendar/icalendar";
|
||||
import rrulePlugin from "@fullcalendar/rrule";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import bootstrap5Plugin from "@fullcalendar/bootstrap5";
|
||||
|
||||
import { getNextColor } from "./color_generator";
|
||||
// import { formatTooltip } from "./tooltip_parser";
|
||||
|
||||
import { glued } from "./main";
|
||||
|
||||
let calendar = null;
|
||||
|
||||
export function initializeCalendar(
|
||||
element,
|
||||
url,
|
||||
onEventMouseEnter,
|
||||
onEventMouseLeave
|
||||
) {
|
||||
if (calendar) {
|
||||
calendar.destroy();
|
||||
}
|
||||
|
||||
calendar = new Calendar(element, {
|
||||
plugins: [
|
||||
iCalendarPlugin,
|
||||
rrulePlugin,
|
||||
bootstrap5Plugin,
|
||||
listPlugin,
|
||||
timeGridPlugin,
|
||||
dayGridPlugin,
|
||||
],
|
||||
themeSystem: "bootstrap5",
|
||||
initialView: "listWeek", // trololo
|
||||
navLinks: true,
|
||||
dayMaxEvents: true,
|
||||
nowIndicator: true,
|
||||
weekNumbers: true,
|
||||
headerToolbar: {
|
||||
left: "prev,next",
|
||||
center: "title",
|
||||
right: "today dayGridMonth,timeGridWeek,timeGridDay,listMonth",
|
||||
},
|
||||
views: {
|
||||
dayGridMonth: { buttonText: "Month" },
|
||||
timeGridWeek: { buttonText: "Week" },
|
||||
timeGridDay: { buttonText: "Day" },
|
||||
listMonth: { buttonText: "List" },
|
||||
},
|
||||
events: init_ics_source(url),
|
||||
eventMouseEnter: onEventMouseEnter,
|
||||
eventMouseLeave: onEventMouseLeave,
|
||||
loading: glued,
|
||||
});
|
||||
// console.log(calendar);
|
||||
calendar.render();
|
||||
}
|
||||
|
||||
export const addEventSource = (url) => {
|
||||
if (calendar) {
|
||||
calendar.addEventSource(init_ics_source(url));
|
||||
}
|
||||
};
|
||||
|
||||
export const getCalendarInstance = () => {
|
||||
return calendar;
|
||||
};
|
||||
|
||||
const init_ics_source = (url) => {
|
||||
return {
|
||||
url: url,
|
||||
format: "ics",
|
||||
color: getNextColor(),
|
||||
};
|
||||
};
|
||||
+89
-111
@@ -1,27 +1,21 @@
|
||||
import { Calendar } from "@fullcalendar/core";
|
||||
import iCalendarPlugin from "@fullcalendar/icalendar";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
// import multiMonthPlugin from "@fullcalendar/multimonth";
|
||||
import {
|
||||
initializeCalendar,
|
||||
addEventSource,
|
||||
getCalendarInstance,
|
||||
} from "./calendar";
|
||||
import { newTippy, tippyDie } from "./tooltip";
|
||||
import { create_url } from "./utils";
|
||||
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
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 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;
|
||||
const eventSourceList = document.getElementById("event-source-list");
|
||||
|
||||
function showPlaceholder() {
|
||||
placeholder_message.style.display = "block";
|
||||
@@ -33,113 +27,97 @@ function hidePlaceholder() {
|
||||
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)
|
||||
// Destroy previous tippy if exists
|
||||
if (currentTippy) {
|
||||
currentTippy.destroy();
|
||||
currentTippy = null;
|
||||
}
|
||||
currentTippy = tippy(info.el, {
|
||||
content: formatTooltip(
|
||||
info.event.title,
|
||||
info.event.extendedProps.description
|
||||
),
|
||||
allowHTML: true,
|
||||
theme: "light",
|
||||
interactive: true,
|
||||
// interactiveDebounce: 200,
|
||||
interactiveBorder: 20,
|
||||
delay: [0, 300],
|
||||
// trigger: "manual", // this to then call .show()/
|
||||
// followCursor: "initial",
|
||||
// plugins: [followCursor],
|
||||
function updateEventSourceList(calendar) {
|
||||
if (!calendar) return;
|
||||
|
||||
// Clear the current list
|
||||
eventSourceList.innerHTML = "";
|
||||
|
||||
// Fetch all event sources
|
||||
calendar.getEventSources().forEach((source) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "event-source-card";
|
||||
card.style.display = "flex";
|
||||
card.style.justifyContent = "space-between";
|
||||
card.style.alignItems = "center";
|
||||
card.style.borderLeft = `4px solid ${source.color || "#007bff"}`;
|
||||
card.style.padding = "0.5em";
|
||||
card.style.marginBottom = "0.5em";
|
||||
|
||||
// Create info container
|
||||
const info = document.createElement("div");
|
||||
const url = source.url || "(No URL)";
|
||||
const format = source.format || "(Unknown Format)";
|
||||
|
||||
const sourceInfo = document.createElement("div");
|
||||
sourceInfo.innerHTML = `<strong>Source:</strong> ${url}<br><strong>Format:</strong> ${format}`;
|
||||
|
||||
const loadingStatus = document.createElement("div");
|
||||
loadingStatus.className = "loading-status";
|
||||
loadingStatus.textContent = source.internalEventSource.isFetching
|
||||
? "Fetching in progress"
|
||||
: "Loaded";
|
||||
|
||||
info.appendChild(sourceInfo);
|
||||
info.appendChild(loadingStatus);
|
||||
|
||||
// Add Refetch Button
|
||||
const refetchButton = document.createElement("button");
|
||||
refetchButton.textContent = "Refetch";
|
||||
refetchButton.className = "btn btn-sm btn-primary";
|
||||
refetchButton.style.marginRight = "0.5em";
|
||||
refetchButton.addEventListener("click", () => {
|
||||
source.refetch();
|
||||
alert("Source refetched!");
|
||||
});
|
||||
// Show the tooltip
|
||||
// currentTippy.show();
|
||||
}
|
||||
// console.log(currentTippy);
|
||||
|
||||
// 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 button container
|
||||
const buttonContainer = document.createElement("div");
|
||||
buttonContainer.appendChild(refetchButton);
|
||||
buttonContainer.appendChild(removeButton);
|
||||
|
||||
// Append info and buttons to the card
|
||||
card.appendChild(info);
|
||||
card.appendChild(buttonContainer);
|
||||
|
||||
// Append the card to the event source list
|
||||
eventSourceList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
export const glued = (e) => {
|
||||
// console.log("loading:", e); // half of it will be true false and half of it will be events
|
||||
|
||||
setTimeout(() => updateEventSourceList(getCalendarInstance()));
|
||||
};
|
||||
|
||||
const tippy_die = (info) => {
|
||||
// only hide. when another comes current will be killed
|
||||
currentTippy.hideWithInteractivity(info.jsEvent);
|
||||
};
|
||||
|
||||
confirm_button.addEventListener("click", (_e) => {
|
||||
console.log("nawgh:", text_field.value);
|
||||
confirm_button.addEventListener("click", () => {
|
||||
const url = create_url(text_field.value);
|
||||
if (!url && text_field.value) return alert("Please input valid url.");
|
||||
|
||||
if (calendar) calendar.destroy();
|
||||
|
||||
if (!text_field.value /* && !calendar */) {
|
||||
showPlaceholder(); // Show placeholder if no URL is provided
|
||||
if (!text_field.value) {
|
||||
showPlaceholder();
|
||||
return;
|
||||
}
|
||||
|
||||
hidePlaceholder(); // Hide placeholder when loading starts
|
||||
calendar = new Calendar(cal_ele, {
|
||||
// calendar config
|
||||
plugins: [
|
||||
bootstrap5Plugin,
|
||||
listPlugin,
|
||||
iCalendarPlugin,
|
||||
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,listMonth",
|
||||
// views
|
||||
},
|
||||
views: {
|
||||
dayGridMonth: { buttonText: "Month" },
|
||||
timeGridWeek: { buttonText: "Week" },
|
||||
timeGridDay: { buttonText: "Day" },
|
||||
listMonth: { buttonText: "List" },
|
||||
},
|
||||
|
||||
events: {
|
||||
url: url,
|
||||
format: "ics",
|
||||
color: getNextColor(),
|
||||
},
|
||||
// eventClick: new_tippy,
|
||||
eventMouseEnter: new_tippy,
|
||||
eventMouseLeave: tippy_die,
|
||||
});
|
||||
// end calendar config
|
||||
calendar.render();
|
||||
hidePlaceholder();
|
||||
initializeCalendar(cal_ele, url, newTippy, tippyDie);
|
||||
});
|
||||
|
||||
// text_field.value = "http://127.0.0.1:8080/filtered-ics";
|
||||
// confirm_button.click();
|
||||
// wow
|
||||
|
||||
add_button.addEventListener("click", () => {
|
||||
if (calendar) {
|
||||
const url = create_url(text_field.value);
|
||||
if (!url)
|
||||
return text_field.value ? alert("Please input valid url.") : undefined;
|
||||
calendar.addEventSource({
|
||||
url: text_field.value,
|
||||
format: "ics",
|
||||
color: getNextColor(),
|
||||
});
|
||||
const url = create_url(text_field.value);
|
||||
if (!url) {
|
||||
if (text_field.value) alert("Please input valid url.");
|
||||
return;
|
||||
}
|
||||
addEventSource(url);
|
||||
});
|
||||
+40
-13
@@ -11,13 +11,16 @@ body {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
/* max-width: 900px; */
|
||||
/* Removed to allow full responsiveness */
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
box-sizing: border-box;
|
||||
/* max-width: 900px; */
|
||||
/* Removed to allow Bootstrap container to handle layout */
|
||||
}
|
||||
|
||||
.app-header {
|
||||
@@ -29,8 +32,9 @@ body {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
margin: 2em auto 1em auto;
|
||||
max-width: 900px;
|
||||
align-items: center;
|
||||
/* max-width: 900px; */
|
||||
/* Removed to allow full responsiveness */
|
||||
}
|
||||
|
||||
#ics_file {
|
||||
@@ -60,30 +64,24 @@ input[type="button"]:hover {
|
||||
}
|
||||
|
||||
#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;
|
||||
/* max-width: 900px; */
|
||||
/* Removed to allow full responsiveness */
|
||||
}
|
||||
|
||||
/* Placeholder message */
|
||||
.placeholder-message {
|
||||
text-align: center;
|
||||
/* justify-self: center; */
|
||||
color: #6c757d;
|
||||
font-size: 1.2em;
|
||||
margin: 1em 0;
|
||||
@@ -93,19 +91,48 @@ input[type="button"]:hover {
|
||||
.flex_input {
|
||||
flex-direction: column;
|
||||
gap: 0.7em;
|
||||
max-width: 98vw;
|
||||
/* max-width: 98vw; */
|
||||
/* Removed to allow Bootstrap container to handle layout */
|
||||
}
|
||||
#calendar {
|
||||
max-width: 99vw;
|
||||
padding: 0.5em 0.1em 1em 0.1em;
|
||||
/* max-width: 99vw; */
|
||||
/* Removed to allow Bootstrap container to handle layout */
|
||||
}
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
/* display: flex; */
|
||||
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);
|
||||
}
|
||||
|
||||
.event-source-card {
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
padding: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.event-source-card button {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.event-source-card .loading-status {
|
||||
font-size: 0.9em;
|
||||
color: #6c757d;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import tippy from "tippy.js";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
import { formatTooltip } from "./tooltip_parser";
|
||||
|
||||
let currentTippy = null;
|
||||
|
||||
export function newTippy(info) {
|
||||
if (info.el._tippy === currentTippy) {
|
||||
currentTippy.show();
|
||||
} else {
|
||||
if (currentTippy) {
|
||||
currentTippy.destroy();
|
||||
currentTippy = null;
|
||||
}
|
||||
currentTippy = tippy(info.el, {
|
||||
content: formatTooltip(
|
||||
info.event.title,
|
||||
info.event.extendedProps.description
|
||||
),
|
||||
allowHTML: true,
|
||||
theme: "light",
|
||||
interactive: true,
|
||||
interactiveBorder: 20,
|
||||
delay: [0, 300],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function tippyDie(info) {
|
||||
if (currentTippy) {
|
||||
currentTippy.hideWithInteractivity(info.jsEvent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user