Skip to content

Commit 76e264a

Browse files
authored
Merge pull request #114 from Mechack08/dev-ex-meetup
Add DevEx WG Calendar Integration (Dev ex meetup)
2 parents 1909de1 + 7ae9176 commit 76e264a

File tree

13 files changed

+1666
-1
lines changed

13 files changed

+1666
-1
lines changed
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import React from "react";
2+
import { MeetupSession } from "../types";
3+
import {
4+
formatMeetupDate,
5+
formatCountdown,
6+
MeetingStatus,
7+
} from "../utils/dateUtils";
8+
import { generateGoogleCalendarUrl } from "../utils/calendarUtils";
9+
import styles from "../styles.module.css";
10+
11+
interface SessionDetailsProps {
12+
session: MeetupSession;
13+
sessionDate: Date;
14+
meetingStatus: MeetingStatus;
15+
copied: boolean;
16+
onBack: () => void;
17+
onCopyLink: (link: string) => void;
18+
}
19+
20+
/**
21+
* Component to display detailed session information
22+
*/
23+
export const SessionDetails: React.FC<SessionDetailsProps> = ({
24+
session,
25+
sessionDate,
26+
meetingStatus,
27+
copied,
28+
onBack,
29+
onCopyLink,
30+
}) => {
31+
const handleAddToCalendar = () => {
32+
const calendarUrl = generateGoogleCalendarUrl(session, sessionDate);
33+
window.open(calendarUrl, "_blank", "noopener,noreferrer");
34+
};
35+
36+
// Join button is enabled from start of meeting day until meeting ends
37+
const isJoinEnabled =
38+
meetingStatus === MeetingStatus.STARTING_SOON ||
39+
meetingStatus === MeetingStatus.ONGOING;
40+
41+
// Get appropriate date/time display text
42+
const getDateTimeDisplay = (): string => {
43+
switch (meetingStatus) {
44+
case MeetingStatus.STARTING_SOON:
45+
return `Today's meeting ${formatCountdown(sessionDate)}`;
46+
case MeetingStatus.ONGOING:
47+
return "Meeting ongoing";
48+
case MeetingStatus.ENDED:
49+
case MeetingStatus.BEFORE_DAY:
50+
default:
51+
return formatMeetupDate(sessionDate);
52+
}
53+
};
54+
55+
return (
56+
<>
57+
<button className={styles.backButton} onClick={onBack} type="button">
58+
<svg
59+
width="16"
60+
height="16"
61+
viewBox="0 0 24 24"
62+
fill="none"
63+
xmlns="http://www.w3.org/2000/svg"
64+
aria-hidden="true"
65+
>
66+
<path
67+
d="M15 18L9 12L15 6"
68+
stroke="currentColor"
69+
strokeWidth="2"
70+
strokeLinecap="round"
71+
strokeLinejoin="round"
72+
/>
73+
</svg>
74+
Back to sessions
75+
</button>
76+
77+
<div className={styles.dateSection}>
78+
<svg
79+
className={styles.sectionIcon}
80+
width="20"
81+
height="20"
82+
viewBox="0 0 24 24"
83+
fill="none"
84+
xmlns="http://www.w3.org/2000/svg"
85+
aria-hidden="true"
86+
>
87+
<circle
88+
cx="12"
89+
cy="12"
90+
r="10"
91+
stroke="currentColor"
92+
strokeWidth="2"
93+
/>
94+
<path
95+
d="M12 6V12L16 14"
96+
stroke="currentColor"
97+
strokeWidth="2"
98+
strokeLinecap="round"
99+
/>
100+
</svg>
101+
<div className={styles.dateText}>
102+
<p className={styles.dateLabel}>Date & Time</p>
103+
<p className={styles.dateValue}>{getDateTimeDisplay()}</p>
104+
</div>
105+
</div>
106+
107+
<div className={styles.linkSection}>
108+
<p className={styles.linkLabel}>Meeting Link</p>
109+
<div className={styles.linkActions}>
110+
<a
111+
href={isJoinEnabled ? session.meetupLink : undefined}
112+
target="_blank"
113+
rel="noopener noreferrer"
114+
className={`${styles.linkButton} ${
115+
!isJoinEnabled ? styles.disabled : ""
116+
}`}
117+
aria-label={
118+
isJoinEnabled
119+
? "Open meetup link in new tab"
120+
: meetingStatus === MeetingStatus.ENDED
121+
? "Meeting has ended"
122+
: "Event link will be available on meeting day"
123+
}
124+
aria-disabled={!isJoinEnabled}
125+
onClick={(e) => {
126+
if (!isJoinEnabled) {
127+
e.preventDefault();
128+
}
129+
}}
130+
>
131+
<svg
132+
width="18"
133+
height="18"
134+
viewBox="0 0 24 24"
135+
fill="none"
136+
xmlns="http://www.w3.org/2000/svg"
137+
aria-hidden="true"
138+
>
139+
<path
140+
d="M18 13V19C18 19.5304 17.7893 20.0391 17.4142 20.4142C17.0391 20.7893 16.5304 21 16 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V8C3 7.46957 3.21071 6.96086 3.58579 6.58579C3.96086 6.21071 4.46957 6 5 6H11"
141+
stroke="currentColor"
142+
strokeWidth="2"
143+
strokeLinecap="round"
144+
strokeLinejoin="round"
145+
/>
146+
<path
147+
d="M15 3H21V9"
148+
stroke="currentColor"
149+
strokeWidth="2"
150+
strokeLinecap="round"
151+
strokeLinejoin="round"
152+
/>
153+
<path
154+
d="M10 14L21 3"
155+
stroke="currentColor"
156+
strokeWidth="2"
157+
strokeLinecap="round"
158+
strokeLinejoin="round"
159+
/>
160+
</svg>
161+
Join Event
162+
</a>
163+
<button
164+
onClick={() => onCopyLink(session.meetupLink)}
165+
className={`${styles.linkButton} ${copied ? styles.copied : ""}`}
166+
aria-label={copied ? "Link copied" : "Copy meetup link"}
167+
type="button"
168+
>
169+
{copied ? (
170+
<>
171+
<svg
172+
width="18"
173+
height="18"
174+
viewBox="0 0 24 24"
175+
fill="none"
176+
xmlns="http://www.w3.org/2000/svg"
177+
aria-hidden="true"
178+
>
179+
<path
180+
d="M20 6L9 17L4 12"
181+
stroke="currentColor"
182+
strokeWidth="2"
183+
strokeLinecap="round"
184+
strokeLinejoin="round"
185+
/>
186+
</svg>
187+
Copied!
188+
</>
189+
) : (
190+
<>
191+
<svg
192+
width="18"
193+
height="18"
194+
viewBox="0 0 24 24"
195+
fill="none"
196+
xmlns="http://www.w3.org/2000/svg"
197+
aria-hidden="true"
198+
>
199+
<rect
200+
x="9"
201+
y="9"
202+
width="13"
203+
height="13"
204+
rx="2"
205+
ry="2"
206+
stroke="currentColor"
207+
strokeWidth="2"
208+
strokeLinecap="round"
209+
strokeLinejoin="round"
210+
/>
211+
<path
212+
d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5"
213+
stroke="currentColor"
214+
strokeWidth="2"
215+
strokeLinecap="round"
216+
strokeLinejoin="round"
217+
/>
218+
</svg>
219+
Copy Link
220+
</>
221+
)}
222+
</button>
223+
</div>
224+
</div>
225+
226+
<div className={styles.calendarSection}>
227+
<button
228+
onClick={handleAddToCalendar}
229+
className={styles.calendarButton}
230+
aria-label="Add to Google Calendar"
231+
type="button"
232+
>
233+
<svg
234+
width="20"
235+
height="20"
236+
viewBox="0 0 24 24"
237+
fill="none"
238+
xmlns="http://www.w3.org/2000/svg"
239+
aria-hidden="true"
240+
>
241+
<rect
242+
x="3"
243+
y="4"
244+
width="18"
245+
height="18"
246+
rx="2"
247+
ry="2"
248+
stroke="currentColor"
249+
strokeWidth="2"
250+
strokeLinecap="round"
251+
strokeLinejoin="round"
252+
/>
253+
<path
254+
d="M16 2V6"
255+
stroke="currentColor"
256+
strokeWidth="2"
257+
strokeLinecap="round"
258+
strokeLinejoin="round"
259+
/>
260+
<path
261+
d="M8 2V6"
262+
stroke="currentColor"
263+
strokeWidth="2"
264+
strokeLinecap="round"
265+
strokeLinejoin="round"
266+
/>
267+
<path
268+
d="M3 10H21"
269+
stroke="currentColor"
270+
strokeWidth="2"
271+
strokeLinecap="round"
272+
strokeLinejoin="round"
273+
/>
274+
<path
275+
d="M12 14L12 14.01"
276+
stroke="currentColor"
277+
strokeWidth="2.5"
278+
strokeLinecap="round"
279+
strokeLinejoin="round"
280+
/>
281+
</svg>
282+
Add to Google Calendar
283+
</button>
284+
</div>
285+
</>
286+
);
287+
};
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React from "react";
2+
import { MeetupSession } from "../types";
3+
import {
4+
formatMeetupDate,
5+
formatCountdown,
6+
MeetingStatus,
7+
} from "../utils/dateUtils";
8+
import styles from "../styles.module.css";
9+
10+
interface SessionListProps {
11+
sessions: MeetupSession[];
12+
sessionDates: Map<string, Date>;
13+
meetingStatuses: Map<string, MeetingStatus>;
14+
onSelectSession: (sessionId: string) => void;
15+
}
16+
17+
/**
18+
* Component to display session selection list
19+
*/
20+
export const SessionList: React.FC<SessionListProps> = ({
21+
sessions,
22+
sessionDates,
23+
meetingStatuses,
24+
onSelectSession,
25+
}) => {
26+
const getSessionStatusText = (
27+
sessionId: string,
28+
date: Date | undefined
29+
): string => {
30+
if (!date) return "";
31+
32+
const status = meetingStatuses.get(sessionId);
33+
34+
switch (status) {
35+
case MeetingStatus.STARTING_SOON:
36+
return `Today's meeting ${formatCountdown(date)}`;
37+
case MeetingStatus.ONGOING:
38+
return "Meeting ongoing";
39+
case MeetingStatus.ENDED:
40+
case MeetingStatus.BEFORE_DAY:
41+
default:
42+
return `Next: ${formatMeetupDate(date)}`;
43+
}
44+
};
45+
46+
return (
47+
<div className={styles.sessionSelection}>
48+
<p className={styles.selectionPrompt}>
49+
Choose which session you'd like to view:
50+
</p>
51+
<div className={styles.sessionList}>
52+
{sessions.map((session) => {
53+
const nextDate = sessionDates.get(session.id);
54+
return (
55+
<button
56+
key={session.id}
57+
className={styles.sessionOption}
58+
onClick={() => onSelectSession(session.id)}
59+
type="button"
60+
>
61+
<div className={styles.sessionOptionContent}>
62+
<h4 className={styles.sessionOptionTitle}>{session.name}</h4>
63+
{nextDate && (
64+
<p className={styles.sessionOptionDate}>
65+
{getSessionStatusText(session.id, nextDate)}
66+
</p>
67+
)}
68+
</div>
69+
<svg
70+
width="20"
71+
height="20"
72+
viewBox="0 0 24 24"
73+
fill="none"
74+
xmlns="http://www.w3.org/2000/svg"
75+
aria-hidden="true"
76+
>
77+
<path
78+
d="M9 18L15 12L9 6"
79+
stroke="currentColor"
80+
strokeWidth="2"
81+
strokeLinecap="round"
82+
strokeLinejoin="round"
83+
/>
84+
</svg>
85+
</button>
86+
);
87+
})}
88+
</div>
89+
</div>
90+
);
91+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import meetupSessionsData from "./meetup-sessions.json";
2+
import { MeetupSession } from "./types";
3+
4+
/**
5+
* Load meetup sessions from JSON configuration file
6+
* This allows easy updates to session details without code changes
7+
*/
8+
export const DEFAULT_SESSIONS: MeetupSession[] = meetupSessionsData.sessions;

0 commit comments

Comments
 (0)