Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion client/src/features/calendarHeader/CalendarHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { IoChatbubblesOutline } from "react-icons/io5";
import { BsCalendarPlusFill } from "react-icons/bs";
import { FaHome, FaQuestion } from "react-icons/fa";
import { RiArrowLeftCircleFill, RiArrowRightCircleFill } from "react-icons/ri";
import { BiCalendarHeart } from "react-icons/bi";
import MonthAndYear from "../calendar/MonthAndYear";
import Logo from "../../assets/images/togetherLogo.svg";
import { useAuthContext } from "../../contexts/AuthContext";
import { useFormModalContext } from "../../contexts/FormModalContext";
import { useNavigate } from "react-router-dom";

function CalendarHeader({ date }) {
function CalendarHeader({ date, setShowSubscribeModal }) {
const navigate = useNavigate();
const formModal = useFormModalContext();
const { isAuthenticated, logout } = useAuthContext();
Expand Down Expand Up @@ -48,6 +49,12 @@ function CalendarHeader({ date }) {
tooltipText={"Home"}
onClick={() => navigate("/")}
/>
{/* Subscription Button */}
<HeaderButton
Icon={BiCalendarHeart}
tooltipText="Subscribe"
onClick={() => setShowSubscribeModal(true)}
/>
</section>
<section className="flex items-center w-full order-first lg:w-min lg:space-x-3 lg:order-0 justify-between mb-4 lg:mb-0">
<img src={Logo} className="max-w-none max-[380px]:w-9" alt="Logo" />
Expand Down
155 changes: 155 additions & 0 deletions client/src/features/modal/CalendarSubscriptionModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import React, { useEffect, useState } from "react";

const CalendarSubscriptionModal = ({ onClose }) => {
const [icsUrl, setIcsUrl] = useState("");
const [copyMessage, setCopyMessage] = useState("");

useEffect(() => {
// Determine the correct URL for the ICS calendar
const getCalendarUrl = () => {
const currentOrigin = window.location.origin;

// Check if we're behind ngrok
// ngrok URLs look like: https://abc123.ngrok-free.app
// They contain "ngrok" in the hostname
if (currentOrigin.includes("ngrok")) {
console.log("[Modal] Detected ngrok URL:", currentOrigin);
return `${currentOrigin}/calendar.ics`;
}

// Check if we're in development (localhost)
if (
currentOrigin.includes("localhost") ||
currentOrigin.includes("127.0.0.1")
) {
console.log("[Modal] Detected localhost URL:", currentOrigin);
// In development, the frontend is on port 3000 but backend is on 2121
// We need to adjust the URL to hit the backend directly
const backendUrl = currentOrigin.replace(":3000", ":2121");
return `${backendUrl}/calendar.ics`;
}

// Production environment
console.log("[Modal] Using production URL:", currentOrigin);
return `${currentOrigin}/calendar.ics`;
};

setIcsUrl(getCalendarUrl());
}, []);

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(icsUrl);
setCopyMessage("URL copied!");
setTimeout(() => setCopyMessage(""), 2000);
} catch (err) {
setCopyMessage("Failed to copy. Try again.");
console.error("Copy failed:", err);
setTimeout(() => setCopyMessage(""), 2000);
}
};

const handleKeyDown = (e) => {
if (e.key === "Escape") {
onClose();
}
};

useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [onClose]);

if (!icsUrl) {
return null; // Don't render until URL is determined
}

// Encode the URL for use in calendar app links
const encodedUrl = encodeURIComponent(icsUrl);
const domain = new URL(icsUrl).hostname;

return (
<div
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white p-5 rounded-lg shadow-lg w-96 z-50"
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
>
<h3 id="modal-title" className="text-xl font-bold mb-4 text-center">
Add iCal Subscription
</h3>
<p className="text-sm text-gray-600 mb-6 text-center">
Add this event feed to your calendar app to stay up to date.
</p>

{/* Debug info - only show in development */}
{(icsUrl.includes("localhost") || icsUrl.includes("ngrok")) && (
<div className="bg-blue-50 p-2 rounded mb-4 text-xs text-blue-700">
<p>
<strong>Debug:</strong> {icsUrl}
</p>
</div>
)}

<div className="space-y-3">
<a
href={`https://calendar.google.com/calendar/r/settings/addbyurl?url=${encodedUrl}`}
target="_blank"
rel="noopener noreferrer"
className="block w-full text-center bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Google Calendar
</a>
<a
href={`https://outlook.office.com/calendar/0/addfromweb?url=webcal://${domain}/calendar.ics`}
target="_blank"
rel="noopener noreferrer"
className="block w-full text-center bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Outlook Calendar
</a>
<a
href={`webcal://${domain}/calendar.ics`}
className="block w-full text-center bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
System Calendar
</a>
<div
onClick={handleCopy}
className="text-center text-purple-600 cursor-pointer hover:underline mt-4"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleCopy();
}
}}
>
Copy URL to Clipboard
</div>
</div>

{copyMessage && (
<div
className={`text-sm mt-3 text-center ${
copyMessage.includes("Failed") ? "text-red-600" : "text-green-600"
}`}
>
{copyMessage}
</div>
)}

<div className="flex justify-end mt-6">
<button
onClick={onClose}
aria-label="Close modal"
className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded"
>
Close
</button>
</div>
</div>
);
};

export default CalendarSubscriptionModal;
20 changes: 18 additions & 2 deletions client/src/pages/CalendarPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ import { useFormModalContext } from "../contexts/FormModalContext";
import EventModal from "../features/modal/EventModal";
import { useModalContext } from "../contexts/ModalContext";
import RejectionModal from "../features/modal/RejectionModal";
import { useRef } from "react";
import { useRef, useState } from "react";
import CalendarSubscriptionModal from "../features/modal/CalendarSubscriptionModal";

function CalendarPage() {
const auth = useAuthContext();
const date = useDate();
const formModal = useFormModalContext();
const modal = useModalContext();
const canScrollMonthRef = useRef(true);
const [showSubscribeModal, setShowSubscribeModal] = useState(false);

const subscribeModalContext = {
isOpen: showSubscribeModal,
handleClose: () => setShowSubscribeModal(false),
};

const handleWheelScroll = (e) => {
if (!canScrollMonthRef.current) return;
Expand All @@ -38,7 +45,10 @@ function CalendarPage() {
onWheel={handleWheelScroll}
className="flex flex-col gap-3 p-3 shadow-xs min-h-screen max-w-[1920px] mx-auto"
>
<CalendarHeader date={date} />
<CalendarHeader
date={date}
setShowSubscribeModal={setShowSubscribeModal}
/>
<Calendar date={date} />
</main>
<div className="md:w-1/2 mx-auto shadow-xl rounded-2xl pb-2 bg-white">
Expand All @@ -52,6 +62,12 @@ function CalendarPage() {
<RejectionModal handleClose={formModal.handleClose} />
)}
</Modal>
{/* Calendar Subscription Modal */}
<Modal context={subscribeModalContext}>
<CalendarSubscriptionModal
onClose={subscribeModalContext.handleClose}
/>
</Modal>
</div>
</FormProvider>
);
Expand Down
5 changes: 5 additions & 0 deletions client/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export default defineConfig({
port: 3000,
proxy: {
"/api": "http://localhost:2121",
"/calendar.ics": {
target: "http://127.0.0.1:2121",
changeOrigin: true,
secure: false,
},
},
},
});
Loading