Skip to content

Commit 000df81

Browse files
Merge pull request #4717 from OneCommunityGlobal/kristin-add-drop-down-to-community-portal-calendar
Kristin Add Calendar View Dropdown (Year / Month / Week / Day) on Community Calendar
2 parents 6438a57 + aa9451c commit 000df81

2 files changed

Lines changed: 284 additions & 11 deletions

File tree

src/components/CommunityPortal/Calendar/CommunityCalendar.jsx

Lines changed: 138 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default function CommunityCalendar() {
2222
const [showEventModal, setShowEventModal] = useState(false);
2323
const [overflowDate, setOverflowDate] = useState(null);
2424
const popupRef = useRef(null);
25+
const [calendarView, setCalendarView] = useState('month');
2526

2627
const currentDate = new Date();
2728
const currentMonth = currentDate.getMonth();
@@ -192,6 +193,106 @@ export default function CommunityCalendar() {
192193
'Full Event': 'statusFull',
193194
};
194195

196+
function WeeklyTimeGrid({ events, selectedDate, onEventClick, darkMode }) {
197+
const hours = Array.from({ length: 24 }, (_, i) => i);
198+
199+
const startOfWeek = useMemo(() => {
200+
const d = new Date(selectedDate);
201+
d.setDate(d.getDate() - d.getDay());
202+
return d;
203+
}, [selectedDate]);
204+
205+
const weekDays = useMemo(() => {
206+
return Array.from({ length: 7 }, (_, i) => {
207+
const day = new Date(startOfWeek);
208+
day.setDate(startOfWeek.getDate() + i);
209+
return day;
210+
});
211+
}, [startOfWeek]);
212+
213+
return (
214+
<div
215+
className={`${styles.weekGridContainer} ${darkMode ? styles.weekGridContainerDark : ''}`}
216+
>
217+
<div className={`${styles.weekGridHeader} ${darkMode ? styles.weekGridHeaderDark : ''}`}>
218+
<div className={styles.timeGutter} />
219+
{weekDays.map(date => (
220+
<div key={date.toString()} className={styles.dayColumnHeader}>
221+
<div className={`${styles.dayLabel} ${darkMode ? styles.dayLabelDark : ''}`}>
222+
{date.toLocaleDateString('en-US', { weekday: 'short' })}
223+
</div>
224+
<div className={`${styles.dateLabel} ${darkMode ? styles.dateLabelDark : ''}`}>
225+
{date.getDate()}
226+
</div>
227+
</div>
228+
))}
229+
</div>
230+
231+
<div className={styles.weekGridBody}>
232+
{hours.map(hour => (
233+
<div key={hour} className={`${styles.hourRow} ${darkMode ? styles.hourRowDark : ''}`}>
234+
<div className={`${styles.timeLabel} ${darkMode ? styles.timeLabelDark : ''}`}>
235+
{hour === 0
236+
? '12 AM'
237+
: hour > 12
238+
? `${hour - 12} PM`
239+
: hour === 12
240+
? '12 PM'
241+
: `${hour} AM`}
242+
</div>
243+
244+
{weekDays.map(date => {
245+
const cellEvents = events.filter(e => {
246+
const eventDate = new Date(e.date);
247+
const [hStr] = e.time.split(':');
248+
let h = parseInt(hStr, 10);
249+
const isPM = e.time.toLowerCase().includes('pm');
250+
const isAM = e.time.toLowerCase().includes('am');
251+
if (isPM && h !== 12) h += 12;
252+
if (isAM && h === 12) h = 0;
253+
254+
return eventDate.toDateString() === date.toDateString() && h === hour;
255+
});
256+
257+
return (
258+
<div
259+
key={date.toString()}
260+
className={`${styles.gridCell} ${darkMode ? styles.gridCellDark : ''}`}
261+
>
262+
{cellEvents.map(ev => (
263+
<button
264+
key={ev.id}
265+
type="button"
266+
className={`${styles.gridEvent} ${darkMode ? styles.gridEventDark : ''}`}
267+
onClick={() => onEventClick(ev)}
268+
aria-label={`Open event ${ev.title} at ${ev.time}`}
269+
>
270+
<div
271+
className={`${styles.gridEventTime} ${
272+
darkMode ? styles.gridEventTimeDark : ''
273+
}`}
274+
>
275+
{ev.time}
276+
</div>
277+
<div
278+
className={`${styles.gridEventTitle} ${
279+
darkMode ? styles.gridEventTitleDark : ''
280+
}`}
281+
>
282+
{ev.title}
283+
</div>
284+
</button>
285+
))}
286+
</div>
287+
);
288+
})}
289+
</div>
290+
))}
291+
</div>
292+
</div>
293+
);
294+
}
295+
195296
// Render event tiles
196297
const tileContent = useCallback(
197298
({ date, view }) => {
@@ -211,20 +312,24 @@ export default function CommunityCalendar() {
211312
key={e.id}
212313
type="button"
213314
className={`${styles.eventItem} ${styles[statusKey] || ''}`}
214-
onClick={() => handleEventClick(e)}
315+
onClick={e_obj => {
316+
e_obj.stopPropagation();
317+
handleEventClick(e);
318+
}}
215319
title={e.title}
216320
>
217321
{e.title}
218322
</button>
219323
);
220324
})}
221-
222325
{hiddenCount > 0 && (
223326
<button
224327
type="button"
225328
className={styles.moreEvents}
226-
onClick={() => setOverflowDate(date)}
227-
title="View all events"
329+
onClick={e_obj => {
330+
e_obj.stopPropagation();
331+
setOverflowDate(date);
332+
}}
228333
>
229334
+{hiddenCount} more
230335
</button>
@@ -345,6 +450,16 @@ export default function CommunityCalendar() {
345450
<header className={calendarClasses.header}>
346451
<h1>Community Calendar</h1>
347452
<div className={calendarClasses.filters}>
453+
<select
454+
value={calendarView}
455+
onChange={e => setCalendarView(e.target.value)}
456+
className={styles.viewSelector}
457+
>
458+
<option value="month">Day View (Month)</option>
459+
<option value="week">Week View (Time Grid)</option>
460+
<option value="year">Month View (Year)</option>
461+
<option value="decade">Year View (Decade)</option>
462+
</select>
348463
<select value={filter.type} onChange={handleFilterChange('type')}>
349464
<option value="all">All Types</option>
350465
{uniqueFilterValues.types.map(t => (
@@ -378,13 +493,25 @@ export default function CommunityCalendar() {
378493
/>
379494
</div>
380495
<div className={calendarClasses.calendarSection}>
381-
<ReactCalendar
382-
className={calendarClasses.reactCalendar}
383-
tileContent={tileContent}
384-
tileClassName={tileClassName}
385-
onClickDay={handleDateSelect}
386-
value={selectedDate}
387-
/>
496+
{calendarView === 'week' ? (
497+
<WeeklyTimeGrid
498+
events={filteredEvents}
499+
selectedDate={selectedDate}
500+
onEventClick={handleEventClick}
501+
darkMode={darkMode}
502+
/>
503+
) : (
504+
<ReactCalendar
505+
className={calendarClasses.reactCalendar}
506+
tileContent={tileContent}
507+
tileClassName={tileClassName}
508+
onClickDay={handleDateSelect}
509+
value={selectedDate}
510+
view={calendarView}
511+
onViewChange={({ view }) => setCalendarView(view)}
512+
/>
513+
)}
514+
388515
<section
389516
className={`${styles.selectedDatePanel} ${
390517
darkMode ? styles.selectedDatePanelDarkMode : ''

src/components/CommunityPortal/Calendar/CommunityCalendar.module.css

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,3 +1154,149 @@
11541154
letter-spacing: 0.5px;
11551155
display: block;
11561156
}
1157+
1158+
/* Week Grid Styles */
1159+
.weekGridContainer {
1160+
display: flex;
1161+
flex-direction: column;
1162+
background: #ffffff;
1163+
border: 1px solid #e2e8f0;
1164+
border-radius: 12px;
1165+
height: 700px;
1166+
overflow: hidden;
1167+
margin-bottom: 20px;
1168+
}
1169+
1170+
.weekGridBody {
1171+
flex: 1;
1172+
overflow-y: scroll;
1173+
background: #ffffff;
1174+
}
1175+
1176+
.weekGridHeader {
1177+
display: grid;
1178+
grid-template-columns: 80px repeat(7, 1fr);
1179+
background: #f8fafc;
1180+
border-bottom: 2px solid #e2e8f0;
1181+
padding-right: 15px;
1182+
}
1183+
1184+
.dayColumnHeader {
1185+
padding: 12px 0;
1186+
text-align: center;
1187+
border-left: 1px solid #e2e8f0;
1188+
}
1189+
1190+
.dayLabel {
1191+
font-size: 0.7rem;
1192+
font-weight: 700;
1193+
color: #64748b;
1194+
text-transform: uppercase;
1195+
}
1196+
1197+
.dateLabel {
1198+
font-size: 1.1rem;
1199+
font-weight: 600;
1200+
color: #1e293b;
1201+
}
1202+
1203+
.hourRow {
1204+
display: grid;
1205+
grid-template-columns: 80px repeat(7, 1fr);
1206+
min-height: 80px;
1207+
border-bottom: 1px solid #f1f5f9;
1208+
}
1209+
1210+
.timeLabel {
1211+
font-size: 0.75rem;
1212+
color: #94a3b8;
1213+
text-align: right;
1214+
padding-right: 12px;
1215+
padding-top: 8px;
1216+
font-weight: 500;
1217+
border-right: 1px solid #e2e8f0;
1218+
}
1219+
1220+
.gridCell {
1221+
border-left: 1px solid #f1f5f9;
1222+
position: relative;
1223+
padding: 4px;
1224+
transition: background 0.2s;
1225+
}
1226+
1227+
.gridCell:hover {
1228+
background-color: #f8fafc;
1229+
}
1230+
1231+
.gridEvent {
1232+
background: #dcfce7;
1233+
border-left: 4px solid #22c55e;
1234+
border-radius: 6px;
1235+
padding: 6px;
1236+
font-size: 0.75rem;
1237+
cursor: pointer;
1238+
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
1239+
margin-bottom: 4px;
1240+
}
1241+
1242+
.gridEventTitle {
1243+
font-weight: 600;
1244+
color: #166534;
1245+
white-space: nowrap;
1246+
overflow: hidden;
1247+
text-overflow: ellipsis;
1248+
}
1249+
1250+
/* Dark Mode Support */
1251+
.weekGridContainerDark {
1252+
background: #1a2332;
1253+
border-color: #2d3748;
1254+
}
1255+
1256+
.weekGridContainerDark .weekGridBody {
1257+
background: #0f172a;
1258+
}
1259+
1260+
.weekGridHeaderDark {
1261+
background: #232d3f;
1262+
border-bottom-color: #2d3748;
1263+
}
1264+
1265+
.dayLabelDark {
1266+
color: #a0aec0;
1267+
}
1268+
1269+
.dateLabelDark {
1270+
color: #ffffff;
1271+
}
1272+
1273+
.hourRowDark {
1274+
border-bottom-color: #2d3748;
1275+
}
1276+
1277+
.timeLabelDark {
1278+
color: #718096;
1279+
border-right-color: #2d3748;
1280+
}
1281+
1282+
.gridCellDark {
1283+
border-left-color: #2d3748;
1284+
}
1285+
1286+
.gridCellDark:hover {
1287+
background-color: rgba(255, 255, 255, 0.03);
1288+
}
1289+
1290+
.gridEventDark {
1291+
background: #064e3b;
1292+
border-left: 4px solid #10b981;
1293+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
1294+
}
1295+
1296+
.gridEventTitleDark {
1297+
color: #d1fae5;
1298+
}
1299+
1300+
.gridEventTimeDark {
1301+
color: #a7f3d0;
1302+
}

0 commit comments

Comments
 (0)