From 7553a34ce222eddd673105e22a9d06d693f2b968 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Thu, 25 Sep 2025 20:31:16 +0100 Subject: [PATCH 1/9] feat: add export button to calendar options sidebar --- src/components/calendar-options.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/calendar-options.tsx b/src/components/calendar-options.tsx index 90abcba..07c0954 100644 --- a/src/components/calendar-options.tsx +++ b/src/components/calendar-options.tsx @@ -1,19 +1,20 @@ "use client"; -import { useContext, useRef, useState, createContext, useEffect } from "react"; import TabsGroup, { PanelContainer, Tab, TabPanel, TabsContainer, } from "./tabs"; -import { ScheduleContext } from "@/contexts/schedule-provider"; -import AnimatedOptionsSection from "./animated-options-section"; -import { twMerge } from "tailwind-merge"; -import clsx from "clsx"; -import { IShiftsSorted } from "@/lib/types"; +import { createContext, useContext, useEffect, useRef, useState } from "react"; +import AnimatedOptionsSection from "./animated-options-section"; import CustomDisclosure from "./disclosure"; +import ExportButton from "@/components/calendar/export-button"; +import { IShiftsSorted } from "@/lib/types"; +import { ScheduleContext } from "@/contexts/schedule-provider"; +import clsx from "clsx"; +import { twMerge } from "tailwind-merge"; interface ICalendarOptionsProvider { removeShift: (id: string) => void; @@ -276,6 +277,9 @@ export default function CalendarOptions({

{schedule ? "Schedule" : "Calendar"}

+
+ +
{children} From 619e1035d9d3834006842e93388b68aefcc9ec5d Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Thu, 25 Sep 2025 20:31:16 +0100 Subject: [PATCH 4/9] chore: format --- src/lib/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index 5be822c..9affc2f 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,5 +1,5 @@ -import { useAuthStore } from "@/stores/authStore"; import axios from "axios"; +import { useAuthStore } from "@/stores/authStore"; export const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, From 111bf3c91889475c35469ad368584434d2c9568e Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Thu, 25 Sep 2025 20:31:16 +0100 Subject: [PATCH 5/9] feat: add calendar export button component --- src/components/calendar/export-button.tsx | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/components/calendar/export-button.tsx diff --git a/src/components/calendar/export-button.tsx b/src/components/calendar/export-button.tsx new file mode 100644 index 0000000..89fb00e --- /dev/null +++ b/src/components/calendar/export-button.tsx @@ -0,0 +1,55 @@ +"use client"; + +import React, { useState } from "react"; + +import CalendarExportModal from "@/components/calendar/calendar-export-modal"; +import { api } from "@/lib/api"; +import { useMutation } from "@tanstack/react-query"; + +export default function ExportButton() { + const [modalState, setModalState] = useState(false); + const [exportUrl, setExportUrl] = useState(""); + const [buttonLabel, setButtonLabel] = useState("Export"); + + const mutation = useMutation({ + mutationFn: async () => { + const res = await api.get("/export/student/calendar-url"); + return res.data.calendar_url; + }, + onSuccess: (data) => { + const url = typeof data === "string" ? data : data.url; + if (!url) { + setButtonLabel("Failed to export"); + return; + } + setExportUrl(url); + setModalState(true); + setButtonLabel("Export"); + }, + onError: (error) => { + console.error("Export failed:", error); + setButtonLabel("Failed to export"); + }, + }); + + return ( + <> + + + + + ); +} From 1fde1e0fab997234ddceee2c57bf36626bcb9e12 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Thu, 25 Sep 2025 20:31:16 +0100 Subject: [PATCH 6/9] feat: add calendar export modal component --- .../calendar/calendar-export-modal.tsx | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 src/components/calendar/calendar-export-modal.tsx diff --git a/src/components/calendar/calendar-export-modal.tsx b/src/components/calendar/calendar-export-modal.tsx new file mode 100644 index 0000000..3f712d1 --- /dev/null +++ b/src/components/calendar/calendar-export-modal.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { + Dialog, + DialogPanel, + DialogTitle, + Transition, + TransitionChild, +} from "@headlessui/react"; +import { Fragment, ReactNode, useRef, useState } from "react"; + +interface ModalProps { + modalState: boolean; + setModalState: (state: boolean) => void; + title?: string; + url: string; // Adding URL here +} + +export default function CalendarExportModal({ + modalState, + setModalState, + title = "Export Calendar", + url, +}: ModalProps) { + const [isCopied, setIsCopied] = useState(false); + const [openSections, setOpenSections] = useState>({}); + + const sectionRefs = { + how: useRef(null), + google: useRef(null), + apple: useRef(null), + outlook: useRef(null), + }; + + function copyToClipboard() { + navigator.clipboard.writeText(url).then(() => { + setIsCopied(true); + setTimeout(() => setIsCopied(false), 1200); + }); + } + + const toggleSection = (key: string) => { + setOpenSections((prev) => ({ ...prev, [key]: !prev[key] })); + }; + + const sections = [ + { + key: "how", + title: "How does it work?", + content: ( +
+

+ The URL above allows you to{" "} + subscribe to your shifts. +

+

You will see your shifts in your calendar app.

+
+ warning + If you change shifts, you will need to re-export and re-subscribe. +
+
+ ), + }, + { + key: "google", + title: "Google Calendar", + content: ( +
    +
  1. + Open{" "} + + Google Calendar + + . +
  2. +
  3. On the left, click Add → From URL.
  4. +
  5. Enter the above calendar’s address.
  6. +
  7. Click Add Calendar.
  8. +
+ ), + }, + { + key: "apple", + title: "Apple Calendar", + content: ( +
    +
  1. Open Calendar on your iPhone or Mac.
  2. +
  3. Click Add Calendar → Add Subscription Calendar.
  4. +
  5. Enter the above calendar’s address and subscribe.
  6. +
+ ), + }, + { + key: "outlook", + title: "Outlook Calendar", + content: ( +
    +
  1. Sign in to Outlook.com.
  2. +
  3. Select Add Calendar → Subscribe from web.
  4. +
  5. Enter the above calendar’s address.
  6. +
+ ), + }, + ]; + + return ( + + setModalState(false)} + > + {/* Background Overlay */} + +
+ + + {/* Modal Container */} +
+ + + {/* Optional Title */} + {title && ( +
+ + {title} + + +
+ )} + + {/* URL Box */} +
+
+ {url} +
+
+ {isCopied ? "Copied!" : "Click to copy"} +
+
+ + {/* Accordion */} +
+ {sections.map((section) => ( +
+ +
+
{section.content}
+
+
+ ))} +
+ + {/* Footer */} +
+ + lightbulb + + + You can also{" "} + + download as .ics file + + . + +
+
+
+
+
+
+ ); +} From 273fac84ee308f68c1f09cbf7f8b8e3d50a328ec Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Thu, 25 Sep 2025 20:41:41 +0100 Subject: [PATCH 7/9] chore: format and clean up --- src/components/calendar/calendar-export-modal.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/calendar/calendar-export-modal.tsx b/src/components/calendar/calendar-export-modal.tsx index 3f712d1..8376db1 100644 --- a/src/components/calendar/calendar-export-modal.tsx +++ b/src/components/calendar/calendar-export-modal.tsx @@ -7,7 +7,7 @@ import { Transition, TransitionChild, } from "@headlessui/react"; -import { Fragment, ReactNode, useRef, useState } from "react"; +import { Fragment, useRef, useState } from "react"; interface ModalProps { modalState: boolean; @@ -110,7 +110,6 @@ export default function CalendarExportModal({ className="relative z-50" onClose={() => setModalState(false)} > - {/* Background Overlay */} - {/* Modal Container */}
- {/* Optional Title */} {title && (
@@ -150,7 +147,6 @@ export default function CalendarExportModal({
)} - {/* URL Box */}
- {/* Accordion */}
{sections.map((section) => (
@@ -203,7 +198,6 @@ export default function CalendarExportModal({ ))}
- {/* Footer */}
lightbulb From 2da7e6cb8a1930a001df0c6bcc11bbf7a0229b12 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Thu, 25 Sep 2025 20:47:58 +0100 Subject: [PATCH 8/9] fix: typo and type error --- src/components/calendar/export-button.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/calendar/export-button.tsx b/src/components/calendar/export-button.tsx index 89fb00e..7c24710 100644 --- a/src/components/calendar/export-button.tsx +++ b/src/components/calendar/export-button.tsx @@ -16,8 +16,7 @@ export default function ExportButton() { const res = await api.get("/export/student/calendar-url"); return res.data.calendar_url; }, - onSuccess: (data) => { - const url = typeof data === "string" ? data : data.url; + onSuccess: (url) => { if (!url) { setButtonLabel("Failed to export"); return; From 7888a00051ed30fc2d52700cafa55a6ce5a327c1 Mon Sep 17 00:00:00 2001 From: GuilhermePSF Date: Wed, 15 Oct 2025 12:27:03 +0100 Subject: [PATCH 9/9] fix: warning message --- src/components/calendar/calendar-export-modal.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/calendar/calendar-export-modal.tsx b/src/components/calendar/calendar-export-modal.tsx index 8376db1..6765cbd 100644 --- a/src/components/calendar/calendar-export-modal.tsx +++ b/src/components/calendar/calendar-export-modal.tsx @@ -13,7 +13,7 @@ interface ModalProps { modalState: boolean; setModalState: (state: boolean) => void; title?: string; - url: string; // Adding URL here + url: string; } export default function CalendarExportModal({ @@ -54,9 +54,12 @@ export default function CalendarExportModal({ subscribe to your shifts.

You will see your shifts in your calendar app.

-
- warning - If you change shifts, you will need to re-export and re-subscribe. +
+ + check_circle + + If you change shifts, you won't need to re-export and + re-subscribe.
), @@ -134,7 +137,7 @@ export default function CalendarExportModal({ > {title && ( -
+
{title}