From 0f01ff767b18ffc98d21e542eb61a3156e0a5d4a Mon Sep 17 00:00:00 2001 From: Easha Mashud Date: Wed, 29 Apr 2026 23:18:23 -0400 Subject: [PATCH 1/2] Added calendar task UI with edit/delete and click behavior --- SMTplugins/Calendar/calendarWidget.py | 72 ++++++++-- calendar_events.json | 1 + flaskServer.py | 2 +- layout_client.json | 8 +- static/js/plugins/calendar_script.js | 198 +++++++++++++++++++++++++- static/widgetCSS/calendar_widget.css | 80 +++++++++++ templates/calendar_widget.html | 33 ++++- 7 files changed, 368 insertions(+), 26 deletions(-) create mode 100644 calendar_events.json diff --git a/SMTplugins/Calendar/calendarWidget.py b/SMTplugins/Calendar/calendarWidget.py index 2744cf6..2ef5de0 100644 --- a/SMTplugins/Calendar/calendarWidget.py +++ b/SMTplugins/Calendar/calendarWidget.py @@ -6,12 +6,20 @@ from datetime import datetime, date import calendar import json +import os +import time class calendarWidget(Widget): def __init__(self): self._preferences = self.widgetDefaultPreferences - self._events = {} # date_str -> list of event names + self.file = "calendar_events.json" + + if os.path.exists(self.file): + with open(self.file, "r") as f: + self._events = json.load(f) + else: + self._events = {} @property def widgetName(self): @@ -82,11 +90,11 @@ def update(self): if self._preferences.get("use_google_cal"): self._fetch_google_events() else: - # Placeholder: inject a couple of demo events so the UI isn't empty - today = date.today().isoformat() - self._events = { - today: ["Class 4PM – LC 22", "Gym 5PM"] - } + # Only add demo data if nothing exists yet + if not self._events: + today = date.today().isoformat() + self._events[today] = ["Class 4PM – LC 22", "Gym 5PM"] + print(f"Calendar Widget updated – {date.today().strftime('%B %Y')}") def _fetch_google_events(self): @@ -105,19 +113,57 @@ def _fetch_google_events(self): print(f"[calendarWidget] Google Calendar fetch failed: {e}") def handle_event(self, event, args): - """ - Supported events: - - "prev_month" : (future) navigate to previous month - - "next_month" : (future) navigate to next month - - "add_event" : args = {"date": "YYYY-MM-DD", "title": "..."} - """ if event == "add_event": date_str = args.get("date") title = args.get("title", "Event") + if date_str: if date_str not in self._events: self._events[date_str] = [] - self._events[date_str].append(title) + + task = { + "id": int(time.time() * 1000), # unique ID + "title": title + } + + self._events[date_str].append(task) + + with open(self.file, "w") as f: + json.dump(self._events, f) + print(f"[calendarWidget] Added event '{title}' on {date_str}") + + + elif event == "delete_event": + date_str = args.get("date") + task_id = args.get("id") + + if date_str in self._events: + self._events[date_str] = [ + t for t in self._events[date_str] if t["id"] != task_id + ] + + with open(self.file, "w") as f: + json.dump(self._events, f) + + print(f"[calendarWidget] Deleted task {task_id}") + + + elif event == "edit_event": + date_str = args.get("date") + task_id = args.get("id") + new_title = args.get("title") + + if date_str in self._events: + for t in self._events[date_str]: + if t["id"] == task_id: + t["title"] = new_title + + with open(self.file, "w") as f: + json.dump(self._events, f) + + print(f"[calendarWidget] Edited task {task_id}") + + else: print(f"[calendarWidget] Unhandled event: {event} args={args}") \ No newline at end of file diff --git a/calendar_events.json b/calendar_events.json new file mode 100644 index 0000000..f5bcd5f --- /dev/null +++ b/calendar_events.json @@ -0,0 +1 @@ +{"2026-04-29": ["Class 4PM \u2013 LC 22", "Gym 5PM"], "2026-04-30": []} \ No newline at end of file diff --git a/flaskServer.py b/flaskServer.py index 77d379e..a27a1a0 100644 --- a/flaskServer.py +++ b/flaskServer.py @@ -29,7 +29,7 @@ #allows for easier starting of flask from start file def run_flask(): - app.run(debug=True, port=5000, use_reloader=False) + app.run(debug=True, port=8000, use_reloader=False) diff --git a/layout_client.json b/layout_client.json index 6411891..9bc30f2 100644 --- a/layout_client.json +++ b/layout_client.json @@ -33,10 +33,10 @@ "col": 3 }, { - "id": "bj-container", - "name": "Blackjack", - "class": "blackjack-widget", - "css_name": "blackjack_widget.css", + "id": "calendar", + "name": "Calendar", + "class": "calendar-widget", + "css_name": "calendar_widget.css", "row": 2, "col": 2 }, diff --git a/static/js/plugins/calendar_script.js b/static/js/plugins/calendar_script.js index 60e78db..c919a2e 100644 --- a/static/js/plugins/calendar_script.js +++ b/static/js/plugins/calendar_script.js @@ -1,15 +1,205 @@ +let selectedDate = null; +let calendarInitialized = false; + +function attachCalendarListeners() { + const container = document.getElementById("calendar"); + + if (!container) return; + + calendarInitialized = true; // mark as initialized + + container.addEventListener("click", (e) => { + const day = e.target.closest(".cal-day"); + if (!day) return; + + selectedDate = day.dataset.date; + + console.log("Calendar data:", window.calendarEvents); + // SHOW TASKS FOR SELECTED DATE + const eventsContainer = document.getElementById("selectedEventsContainer"); + const title = document.getElementById("eventsTitle"); + + if (eventsContainer && title && window.calendarEvents) { + let events = []; + + window.calendarEvents.forEach(week => { + week.forEach(dayData => { + if (dayData.date_str === selectedDate) { + events = dayData.events; + } + }); + }); + + title.innerText = new Date(selectedDate).toDateString(); + eventsContainer.innerHTML = ""; + + if (events.length > 0) { + events.forEach(e => { + const div = document.createElement("div"); + div.className = "cal-event-item"; + div.innerHTML = ` + ${e.title || e} +
+ + +
+ `; + eventsContainer.appendChild(div); + }); + } else { + eventsContainer.innerHTML = `
No tasks
`; + } + } + + // highlight + document.querySelectorAll(".cal-day").forEach(d => d.style.outline = "none"); + day.style.outline = "2px solid #c084fc"; + + // show input + const taskSection = document.getElementById("taskSection"); + if (taskSection) taskSection.style.display = "block"; + + // label + const label = document.getElementById("selectedDateLabel"); + if (label) { + label.innerText = "Editing tasks for: " + new Date(selectedDate).toDateString(); + } + + console.log("Selected date:", selectedDate); + }); +} + +function addTask() { + const input = document.getElementById("taskInput"); + const task = input.value; + + if (!selectedDate || !task) { + alert("Select a date and enter a task"); + return; + } + + fetch("/api/calendar/event", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + event: "add_event", + args: { + date: selectedDate, + title: task + } + }) + }) + .then(() => { + input.value = ""; + + updateCalendar().then(() => { + setTimeout(() => { + const selected = document.querySelector( + `.cal-day[data-date="${selectedDate}"]` + ); + + if (selected) { + selected.click(); + + // optional safety re-click + setTimeout(() => { + selected.click(); + }, 50); + } + }, 50); + }); + }); +} + + +function deleteTask(id, date) { + fetch("/api/calendar/event", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + event: "delete_event", + args: { id, date } + }) + }).then(() => { + updateCalendar().then(() => { + setTimeout(() => { + const day = document.querySelector(`[data-date="${date}"]`); + if (day) day.click(); + }, 50); + }); +}); +} + +function editTask(id, date, oldTitle) { + const newTitle = prompt("Edit task:", oldTitle); + if (!newTitle) return; + + fetch("/api/calendar/event", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + event: "edit_event", + args: { id, date, title: newTitle } + }) + }).then(() => { + updateCalendar().then(() => { + setTimeout(() => { + const day = document.querySelector(`[data-date="${date}"]`); + if (day) day.click(); + }, 50); + }); +}); +} + +// ONLY ONE FUNCTION export async function updateCalendar() { try { const response = await fetch('/widget/calendar'); const html = await response.text(); const container = document.getElementById('calendar'); - if (container) container.innerHTML = html; + + if (container) { + container.innerHTML = html; + + // IMPORTANT: re-read fresh data from HTML + const scriptTag = container.querySelector("script"); + if (scriptTag) { + eval(scriptTag.innerText); // updates window.calendarEvents + } + + attachCalendarListeners(); + } + setTimeout(() => { + const today = document.querySelector(".cal-day.today"); + if (today) { + today.click(); + } + }, 50); + } catch (error) { console.error('Calendar update failed:', error); } } -// Update every 60 seconds + + +updateCalendar().then(() => { + setTimeout(() => { + const today = document.querySelector(".cal-day.today"); + if (today) today.click(); + }, 50); +}); + +// update every 60s setInterval(updateCalendar, 60000); -// Run once immediately -updateCalendar(); \ No newline at end of file + +// make button work globally +window.addTask = addTask; +window.deleteTask = deleteTask; +window.editTask = editTask; \ No newline at end of file diff --git a/static/widgetCSS/calendar_widget.css b/static/widgetCSS/calendar_widget.css index 10a0ef0..2c31d40 100644 --- a/static/widgetCSS/calendar_widget.css +++ b/static/widgetCSS/calendar_widget.css @@ -131,4 +131,84 @@ font-size: 0.65rem; color: rgba(255, 255, 255, 0.3); font-style: italic; +} + +.task-section { + margin-top: 12px; + display: none; + padding-top: 10px; + border-top: 1px solid rgba(255,255,255,0.08); +} + +.task-label { + font-size: 0.7rem; + color: #c084fc; + margin-bottom: 6px; + letter-spacing: 0.04em; +} + +.task-input-row { + display: flex; + gap: 6px; +} + +/* INPUT */ +.task-input-row input { + flex: 1; + padding: 6px 8px; + border-radius: 6px; + border: 1px solid rgba(255,255,255,0.1); + background: #1a1a24; + color: #e8e6f0; + font-size: 0.7rem; + outline: none; + transition: 0.2s; +} + +.task-input-row input::placeholder { + color: #5a5870; +} + +.task-input-row input:focus { + border-color: #7c6af7; + box-shadow: 0 0 6px rgba(124,106,247,0.4); +} + +/* BUTTON */ +.task-input-row button { + padding: 6px 12px; + border-radius: 6px; + border: none; + background: linear-gradient(135deg, #7c6af7, #c084fc); + color: white; + font-size: 0.7rem; + cursor: pointer; + transition: 0.2s; +} + +.task-input-row button:hover { + transform: translateY(-1px); + box-shadow: 0 4px 10px rgba(124,106,247,0.4); +} + +.task-input-row button:active { + transform: scale(0.96); +} + +.cal-event-item { + display: flex; + align-items: center; + justify-content: space-between; +} + +.cal-event-item button { + background: none; + border: none; + color: #c084fc; + cursor: pointer; + font-size: 0.8rem; +} + +.cal-event-item button:hover { + color: white; } \ No newline at end of file diff --git a/templates/calendar_widget.html b/templates/calendar_widget.html index 978a441..0dc8d76 100644 --- a/templates/calendar_widget.html +++ b/templates/calendar_widget.html @@ -203,10 +203,11 @@
{{ day.day }} - {% if day.events %} -
+ {% if day.is_today and day.events %} +
{% endif %}
{% endfor %} @@ -214,7 +215,7 @@ -
+ +
+
Select a day
+ +
+
Click a date to see tasks
+
+
+ + + +
+ +
+ +
-
\ No newline at end of file + + + + + + + From 23d86861048fa6ee73cc413305fd94561da924a8 Mon Sep 17 00:00:00 2001 From: Easha Mashud Date: Thu, 30 Apr 2026 00:06:03 -0400 Subject: [PATCH 2/2] Add Calendar Arrows that change the months --- SMTplugins/Calendar/calendarWidget.py | 180 +++++++++++++------------ SMTplugins/Calendar/calendar_plugin.py | 1 + calendar_state.json | 1 + static/js/plugins/calendar_script.js | 43 +++--- static/widgetCSS/calendar_widget.css | 23 ++++ templates/calendar_widget.html | 38 ++---- 6 files changed, 156 insertions(+), 130 deletions(-) create mode 100644 calendar_state.json diff --git a/SMTplugins/Calendar/calendarWidget.py b/SMTplugins/Calendar/calendarWidget.py index 2ef5de0..1de37d3 100644 --- a/SMTplugins/Calendar/calendarWidget.py +++ b/SMTplugins/Calendar/calendarWidget.py @@ -1,7 +1,3 @@ -# calendarWidget.py -# Displays a monthly calendar with Google Calendar event integration (optional) -# Falls back to a static calendar view if no credentials are provided - from widget import Widget from datetime import datetime, date import calendar @@ -9,18 +5,39 @@ import os import time + class calendarWidget(Widget): def __init__(self): - self._preferences = self.widgetDefaultPreferences - self.file = "calendar_events.json" - - if os.path.exists(self.file): - with open(self.file, "r") as f: - self._events = json.load(f) + # -------- FILES -------- + self.state_file = "calendar_state.json" + self.events_file = "calendar_events.json" + + # -------- DEFAULT STATE -------- + self.current_date = date.today() + + # -------- LOAD MONTH STATE -------- + if os.path.exists(self.state_file): + try: + with open(self.state_file, "r") as f: + state = json.load(f) + if "current_date" in state: + self.current_date = date.fromisoformat(state["current_date"]) + except: + print("State file corrupted, resetting") + + # -------- LOAD EVENTS -------- + if os.path.exists(self.events_file): + try: + with open(self.events_file, "r") as f: + self._events = json.load(f) + except: + print("Events file corrupted, resetting") + self._events = {} else: self._events = {} + # -------- METADATA -------- @property def widgetName(self): return "Calendar Widget" @@ -31,17 +48,16 @@ def widgetID(self): @property def widgetHTML(self): - """Returns the HTML template name; Flask renders it via Jinja.""" return "calendar_widget.html" + # -------- MAIN DATA -------- @property def widgetData(self): - """Returns current calendar data as a JSON-serialisable dict.""" today = date.today() - year = today.year - month = today.month + year = self.current_date.year + month = self.current_date.month - cal = calendar.Calendar(firstweekday=6) # week starts Sunday + cal = calendar.Calendar(firstweekday=6) weeks = cal.monthdatescalendar(year, month) weeks_data = [] @@ -58,12 +74,13 @@ def widgetData(self): weeks_data.append(days) return { - "month_name": today.strftime("%B"), + "month_name": self.current_date.strftime("%B"), "year": year, "weeks": weeks_data, "day_headers": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] } + # -------- PREFERENCES -------- @property def widgetPreferences(self): return self._preferences @@ -76,43 +93,39 @@ def widgetPreferences(self, value): def widgetDefaultPreferences(self): return { "show_week_numbers": False, - "use_google_cal": False, # Set True + provide creds to enable + "use_google_cal": False, "google_cal_id": "primary" } @property def updateTimer(self): - # Refresh every 10 minutes return 600_000 - def update(self): - """Called by the widget subsystem on a timer. Fetches events if enabled.""" - if self._preferences.get("use_google_cal"): - self._fetch_google_events() - else: - # Only add demo data if nothing exists yet - if not self._events: - today = date.today().isoformat() - self._events[today] = ["Class 4PM – LC 22", "Gym 5PM"] - - print(f"Calendar Widget updated – {date.today().strftime('%B %Y')}") - - def _fetch_google_events(self): - """ - Stub for Google Calendar API integration. - To enable: install google-auth + google-api-python-client, - create OAuth credentials, and fill in the logic below. - """ - try: - # TODO: implement OAuth flow and fetch events for current month - # from googleapiclient.discovery import build - # service = build("calendar", "v3", credentials=creds) - # events_result = service.events().list(...).execute() - raise NotImplementedError("Google Calendar auth not yet configured.") - except Exception as e: - print(f"[calendarWidget] Google Calendar fetch failed: {e}") + # -------- SAVE HELPERS -------- + def _save_state(self): + with open(self.state_file, "w") as f: + json.dump({ + "current_date": self.current_date.isoformat() + }, f) + + def _save_events(self): + with open(self.events_file, "w") as f: + json.dump(self._events, f) + # -------- OPTIONAL DEFAULT DATA -------- + def update(self): + if not self._events: + today = date.today().isoformat() + self._events[today] = [ + {"id": int(time.time()*1000), "title": "Class 4PM – LC 22"}, + {"id": int(time.time()*1000)+1, "title": "Gym 5PM"} + ] + self._save_events() + + # -------- EVENT HANDLER -------- def handle_event(self, event, args): + + # -------- ADD TASK -------- if event == "add_event": date_str = args.get("date") title = args.get("title", "Event") @@ -121,49 +134,46 @@ def handle_event(self, event, args): if date_str not in self._events: self._events[date_str] = [] - task = { - "id": int(time.time() * 1000), # unique ID + self._events[date_str].append({ + "id": int(time.time()*1000), "title": title - } - - self._events[date_str].append(task) - - with open(self.file, "w") as f: - json.dump(self._events, f) - - print(f"[calendarWidget] Added event '{title}' on {date_str}") - - - elif event == "delete_event": - date_str = args.get("date") - task_id = args.get("id") - - if date_str in self._events: - self._events[date_str] = [ - t for t in self._events[date_str] if t["id"] != task_id - ] - - with open(self.file, "w") as f: - json.dump(self._events, f) - - print(f"[calendarWidget] Deleted task {task_id}") - - - elif event == "edit_event": - date_str = args.get("date") - task_id = args.get("id") - new_title = args.get("title") - - if date_str in self._events: - for t in self._events[date_str]: - if t["id"] == task_id: - t["title"] = new_title - - with open(self.file, "w") as f: - json.dump(self._events, f) - - print(f"[calendarWidget] Edited task {task_id}") + }) + self._save_events() + + # -------- NEXT MONTH -------- + elif event == "next_month": + print("NEXT MONTH TRIGGERED") + + if self.current_date.month == 12: + self.current_date = self.current_date.replace( + year=self.current_date.year + 1, + month=1 + ) + else: + self.current_date = self.current_date.replace( + month=self.current_date.month + 1 + ) + + self._save_state() + print("NEW MONTH:", self.current_date) + + # -------- PREVIOUS MONTH -------- + elif event == "prev_month": + print("PREV MONTH TRIGGERED") + + if self.current_date.month == 1: + self.current_date = self.current_date.replace( + year=self.current_date.year - 1, + month=12 + ) + else: + self.current_date = self.current_date.replace( + month=self.current_date.month - 1 + ) + + self._save_state() + print("NEW MONTH:", self.current_date) else: print(f"[calendarWidget] Unhandled event: {event} args={args}") \ No newline at end of file diff --git a/SMTplugins/Calendar/calendar_plugin.py b/SMTplugins/Calendar/calendar_plugin.py index 1bf0d23..72e3322 100644 --- a/SMTplugins/Calendar/calendar_plugin.py +++ b/SMTplugins/Calendar/calendar_plugin.py @@ -10,6 +10,7 @@ @calendar_bp.route("/widget/calendar") def calendar_view(): + print("RENDERING MONTH:", _widget.current_date) # debug return render_template("calendar_widget.html", data=_widget.widgetData) @calendar_bp.route("/api/calendar/data") diff --git a/calendar_state.json b/calendar_state.json new file mode 100644 index 0000000..7593051 --- /dev/null +++ b/calendar_state.json @@ -0,0 +1 @@ +{"current_date": "2026-04-29"} \ No newline at end of file diff --git a/static/js/plugins/calendar_script.js b/static/js/plugins/calendar_script.js index c919a2e..ed333b2 100644 --- a/static/js/plugins/calendar_script.js +++ b/static/js/plugins/calendar_script.js @@ -95,20 +95,6 @@ function addTask() { input.value = ""; updateCalendar().then(() => { - setTimeout(() => { - const selected = document.querySelector( - `.cal-day[data-date="${selectedDate}"]` - ); - - if (selected) { - selected.click(); - - // optional safety re-click - setTimeout(() => { - selected.click(); - }, 50); - } - }, 50); }); }); } @@ -157,6 +143,28 @@ function editTask(id, date, oldTitle) { }); } +function changeMonth(direction) { + console.log("CLICKED:", direction); + + fetch("/api/calendar/event", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + event: direction === "next" ? "next_month" : "prev_month" + }) + }) + .then(res => res.json()) + .then(() => { + console.log("UPDATED MONTH"); + return updateCalendar(); + }) + .catch(err => console.error("ERROR:", err)); +} + +window.changeMonth = changeMonth; + // ONLY ONE FUNCTION export async function updateCalendar() { try { @@ -175,12 +183,7 @@ export async function updateCalendar() { attachCalendarListeners(); } - setTimeout(() => { - const today = document.querySelector(".cal-day.today"); - if (today) { - today.click(); - } - }, 50); + } catch (error) { console.error('Calendar update failed:', error); diff --git a/static/widgetCSS/calendar_widget.css b/static/widgetCSS/calendar_widget.css index 2c31d40..bf80d41 100644 --- a/static/widgetCSS/calendar_widget.css +++ b/static/widgetCSS/calendar_widget.css @@ -211,4 +211,27 @@ .cal-event-item button:hover { color: white; +} + +.cal-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.cal-nav-btn { + background: transparent; + border: none; + color: #c084fc; /* purple */ + font-size: 1.2rem; + cursor: pointer; + padding: 6px 10px; + border-radius: 6px; + transition: all 0.2s ease; +} + +.cal-nav-btn:hover { + background: rgba(192, 132, 252, 0.15); + color: white; + transform: scale(1.2); } \ No newline at end of file diff --git a/templates/calendar_widget.html b/templates/calendar_widget.html index 0dc8d76..7baf5d7 100644 --- a/templates/calendar_widget.html +++ b/templates/calendar_widget.html @@ -187,10 +187,21 @@
- {{ data.month_name }} + + + + + +
+ {{ data.month_name }}
{{ data.year }}
+ + + +
+
{% for header in data.day_headers %} @@ -214,30 +225,7 @@ {% endfor %}
- - +
Select a day