Skip to content
Merged
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
57 changes: 57 additions & 0 deletions src/Pages/Events/Calendar/CalendarCell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { sortEventsForDay } from './calendarUtils';
import { EventRow } from './EventRow';

export function CalendarCell({ date, isCurrentMonth, isToday, events, onSelectEvent, isAdminView }) {
const sortedEvents = sortEventsForDay(events);
const mobileRows = sortedEvents.slice(0, 2);
const desktopOnlyRows = sortedEvents.slice(2, 4);
const hiddenCount = Math.max(sortedEvents.length - 4, 0);

return (
<div
className={[
'min-h-[106px] sm:min-h-[124px] border-b border-r border-slate-700/60 p-1.5 transition-colors duration-150',
isCurrentMonth ? '' : 'opacity-30',
isToday ? 'bg-cyan-500/[0.12]' : 'hover:bg-slate-700/35',
].join(' ')}
>
<div className="flex justify-start mb-1">
<span
className={[
'flex h-6 min-w-6 items-center justify-center rounded-full px-1.5 text-xs font-semibold',
isToday ? 'bg-cyan-400 text-slate-950' : isCurrentMonth ? 'text-slate-200' : 'text-slate-600',
].join(' ')}
>
{date.getDate()}
</span>
</div>

<div className="flex flex-col gap-0.5">
{mobileRows.map((ev) => (
<EventRow
key={ev.id}
event={ev}
onSelect={onSelectEvent}
isAdminView={isAdminView}
/>
))}

{desktopOnlyRows.map((ev) => (
<div key={ev.id} className="hidden sm:block">
<EventRow
event={ev}
onSelect={onSelectEvent}
isAdminView={isAdminView}
/>
</div>
))}

{hiddenCount > 0 && (
<span className="pl-1 text-[10px] font-medium text-slate-400">
+{hiddenCount} more
</span>
)}
</div>
</div>
);
}
21 changes: 21 additions & 0 deletions src/Pages/Events/Calendar/CalendarGrid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DayLabels } from './DayLabels';
import { CalendarCell } from './CalendarCell';

export function CalendarGrid({ cells, onSelectEvent, isAdminView }) {
return (
<div className="hidden sm:block">
<DayLabels />

<div className="grid grid-cols-7 [&>*:nth-child(7n)]:border-r-0">
{cells.map((cell) => (
<CalendarCell
key={cell.key}
{...cell}
onSelectEvent={onSelectEvent}
isAdminView={isAdminView}
/>
))}
</div>
</div>
);
}
92 changes: 92 additions & 0 deletions src/Pages/Events/Calendar/CalendarHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Link } from 'react-router-dom';
import { ChevronLeft, ChevronRight, PlusIcon } from '../EventIcons';
import { MONTHS, YEAR_RANGE } from './calendarConstants';

export function CalendarHeader({
month,
year,
monthEventCount,
canCreateEvent,
onMonthChange,
onYearChange,
onTodayClick,
onPreviousMonth,
onNextMonth,
}) {
return (
<div className="flex flex-wrap items-start justify-between gap-3 px-5 py-4 border-b border-slate-600/50">
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-3">
<div className="relative w-28 sm:w-36">
<select
value={month}
onChange={onMonthChange}
aria-label="Select month"
className="h-10 w-full appearance-none rounded-lg border border-slate-400/40 bg-slate-800 px-4 pr-10 text-[14px] font-semibold text-slate-100 transition hover:border-slate-300 hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-400"
>
{MONTHS.map((monthName, index) => (
<option key={monthName} value={index} className="bg-slate-900">
{monthName}
</option>
))}
</select>
</div>

<div className="relative w-24">
<select
value={year}
onChange={onYearChange}
aria-label="Select year"
className="h-10 w-full appearance-none rounded-lg border border-slate-400/40 bg-slate-800 px-4 pr-10 text-[14px] font-semibold text-slate-100 transition hover:border-slate-300 hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-400"
>
{YEAR_RANGE.map((yearOption) => (
<option key={yearOption} value={yearOption} className="bg-slate-900">
{yearOption}
</option>
))}
</select>
</div>

<button
onClick={onTodayClick}
className="h-10 rounded-lg border border-slate-400/40 bg-slate-800 px-4 text-[14px] font-semibold text-slate-100 transition hover:border-slate-300 hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-400"
>
Today
</button>
</div>

<p className="text-sm font-medium tracking-wide text-slate-300">
{monthEventCount} event{monthEventCount !== 1 ? 's' : ''} this month
</p>
</div>

<div className="flex items-center gap-2">
{canCreateEvent && (
<Link
to="/events/create"
aria-label="Create event"
className="inline-flex items-center justify-center w-10 h-10 transition border rounded-lg border-slate-400/40 bg-slate-800 text-slate-100 hover:border-slate-300 hover:bg-slate-700 hover:text-white"
>
<PlusIcon />
</Link>
)}

<button
onClick={onPreviousMonth}
aria-label="Previous month"
className="flex items-center justify-center w-10 h-10 transition border rounded-lg border-slate-400/40 bg-slate-800 text-slate-100 hover:border-slate-300 hover:bg-slate-700 hover:text-white"
>
<ChevronLeft />
</button>

<button
onClick={onNextMonth}
aria-label="Next month"
className="flex items-center justify-center w-10 h-10 transition border rounded-lg border-slate-400/40 bg-slate-800 text-slate-100 hover:border-slate-300 hover:bg-slate-700 hover:text-white"
>
<ChevronRight />
</button>
</div>
</div>
);
}
26 changes: 26 additions & 0 deletions src/Pages/Events/Calendar/CalendarLegend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function CalendarLegend({ isAdminView }) {
const legendItems = isAdminView
? [
{ label: 'Scheduled', dot: 'bg-blue-300' },
{ label: 'Published', dot: 'bg-cyan-300' },
{ label: 'Private', dot: 'bg-violet-300' },
{ label: 'Draft', dot: 'bg-amber-300' },
{ label: 'Closed', dot: 'bg-rose-300' },
]
: [
{ label: 'Open', dot: 'bg-cyan-300' },
{ label: 'Members only', dot: 'bg-violet-300' },
{ label: 'Closed', dot: 'bg-rose-300' },
];

return (
<div className="flex flex-wrap items-center px-5 py-3 border-t gap-x-4 gap-y-1 border-slate-700/70 bg-slate-900/35">
{legendItems.map(({ label, dot }) => (
<span key={label} className="flex items-center gap-1.5 text-[11px] font-medium text-slate-300">
<span className={['h-2.5 w-2.5 rounded-full', dot].join(' ')} />
{label}
</span>
))}
</div>
);
}
123 changes: 123 additions & 0 deletions src/Pages/Events/Calendar/CalendarView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useState, useMemo } from 'react';
import { toDateKey } from '../eventUtils';
import { eventDateKey, sortEventsForDay } from './calendarUtils';
import { EventPopup } from './EventPopup';
import { CalendarHeader } from './CalendarHeader';
import { CalendarGrid } from './CalendarGrid';
import { MobileMonthAgenda } from './MobileMonthAgenda';
import { CalendarLegend } from './CalendarLegend';

export default function CalendarView({
events,
isAdminView = false,
user,
canCreateEvent = false,
cursor,
setCursor,
}) {
const today = useMemo(() => new Date(), []);
const [selectedEvent, setSelectedEvent] = useState(null);

const eventsByDate = useMemo(() => {
const map = {};
events.forEach((ev) => {
const key = eventDateKey(ev);
if (!key) return;
if (!map[key]) map[key] = [];
map[key].push(ev);
});
Object.keys(map).forEach((key) => {
map[key] = sortEventsForDay(map[key]);
});
return map;
}, [events]);

const year = cursor.getFullYear();
const month = cursor.getMonth();

function handleMonthChange(e) {
const nextMonth = Number(e.target.value);
setCursor(new Date(year, nextMonth, 1));
}

function handleYearChange(e) {
const nextYear = Number(e.target.value);
setCursor(new Date(nextYear, month, 1));
}

const firstDayOfMonth = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const totalCells = Math.ceil((firstDayOfMonth + daysInMonth) / 7) * 7;
const todayKey = toDateKey(today);

const cells = Array.from({ length: totalCells }, (_, i) => {
const dayOffset = i - firstDayOfMonth;
const date = new Date(year, month, dayOffset + 1);
const key = toDateKey(date);
return {
date,
key,
isCurrentMonth: date.getMonth() === month,
isToday: key === todayKey,
events: eventsByDate[key] || [],
};
});

const monthEventCount = cells
.filter((c) => c.isCurrentMonth)
.reduce((sum, c) => sum + c.events.length, 0);

const monthEvents = useMemo(() => {
return cells
.filter((c) => c.isCurrentMonth && c.events.length > 0)
.flatMap((c) =>
c.events.map((event) => ({
event,
dateKey: c.key,
}))
);
}, [cells]);

return (
<>
{selectedEvent && (
<EventPopup
event={selectedEvent}
onClose={() => setSelectedEvent(null)}
isAdminView={isAdminView}
user={user}
/>
)}

<div className="flex h-full max-h-full flex-col overflow-hidden rounded-xl border border-slate-500/50 bg-slate-800/85 shadow-[0_0_0_1px_rgba(148,163,184,0.05)] sm:h-auto sm:max-h-none">
<CalendarHeader
month={month}
year={year}
monthEventCount={monthEventCount}
canCreateEvent={canCreateEvent}
onMonthChange={handleMonthChange}
onYearChange={handleYearChange}
onTodayClick={() => setCursor(new Date(today.getFullYear(), today.getMonth(), 1))}
onPreviousMonth={() => setCursor(new Date(year, month - 1, 1))}
onNextMonth={() => setCursor(new Date(year, month + 1, 1))}
/>

<div className="flex-1 min-h-0 overflow-y-auto sm:hidden">
<MobileMonthAgenda
monthEvents={monthEvents}
onSelectEvent={setSelectedEvent}
isAdminView={isAdminView}
/>
</div>

<CalendarGrid
cells={cells}
onSelectEvent={setSelectedEvent}
isAdminView={isAdminView}
/>

<CalendarLegend isAdminView={isAdminView} />
</div>
</>
);
}
16 changes: 16 additions & 0 deletions src/Pages/Events/Calendar/DayLabels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DAYS } from './calendarConstants';

export function DayLabels() {
return (
<div className="grid grid-cols-7 border-b border-slate-700/70 bg-slate-900/35">
{DAYS.map((d) => (
<div
key={d}
className="py-2 text-center text-[10px] font-bold uppercase tracking-[0.22em] text-slate-400"
>
{d}
</div>
))}
</div>
);
}
Loading
Loading