diff --git a/src/screens/Player/PlayerCalendar/index.tsx b/src/screens/Player/PlayerCalendar/index.tsx index 93c42351..b1413bf4 100644 --- a/src/screens/Player/PlayerCalendar/index.tsx +++ b/src/screens/Player/PlayerCalendar/index.tsx @@ -149,6 +149,8 @@ const PlayerCalendar = () => { const [coachScheduleLoading, setCoachScheduleLoading] = useState(false); const [coachLessonLoading, setCoachLessonLoading] = useState(false); const [visibleDates, setVisibleDates] = useState(() => buildWeekDates(new Date())); + const [timeFilter, setTimeFilter] = useState<"all" | "today" | "next3" | "week">("all"); + const [activityFilter, setActivityFilter] = useState<"all" | "lessons" | "booked" | "availability">("all"); const authToken = useMemo( () => getStoredAuthToken({ preferScheme: "token" }) ?? undefined, @@ -467,6 +469,117 @@ const PlayerCalendar = () => { [availabilityEvents, lessonEvents], ); + const timeFilterStats = useMemo(() => { + const now = moment(); + const startOfToday = now.clone().startOf("day"); + const endOfToday = now.clone().endOf("day"); + const endOfNext3Days = now.clone().add(2, "days").endOf("day"); + const startOfWeek = now.clone().startOf("isoWeek"); + const endOfWeek = now.clone().endOf("isoWeek"); + + let today = 0; + let next3 = 0; + let week = 0; + + calendarEvents.forEach((event) => { + const start = moment(event.start as Date); + if (start.isBetween(startOfToday, endOfToday, undefined, "[]")) { + today += 1; + } + if (start.isBetween(startOfToday, endOfNext3Days, undefined, "[]")) { + next3 += 1; + } + if (start.isBetween(startOfWeek, endOfWeek, undefined, "[]")) { + week += 1; + } + }); + + return { + total: calendarEvents.length, + today, + next3, + week, + }; + }, [calendarEvents]); + + const timeFilterOptions = useMemo( + () => [ + { id: "all" as const, label: "All Activities", count: timeFilterStats.total }, + { id: "today" as const, label: "Today", count: timeFilterStats.today }, + { id: "next3" as const, label: "Next 3 Days", count: timeFilterStats.next3 }, + { id: "week" as const, label: "This Week", count: timeFilterStats.week }, + ], + [timeFilterStats], + ); + + const bookedLessonCount = useMemo( + () => lessonEvents.filter((event) => event.type === "booked").length, + [lessonEvents], + ); + + const activityFilterOptions = useMemo( + () => [ + { id: "all" as const, label: "All Types", count: calendarEvents.length }, + { id: "lessons" as const, label: "Lessons", count: lessonEvents.length }, + { id: "booked" as const, label: "My Bookings", count: bookedLessonCount }, + { id: "availability" as const, label: "Coach Availability", count: availabilityEvents.length }, + ], + [availabilityEvents.length, bookedLessonCount, calendarEvents.length, lessonEvents.length], + ); + + const nextBookedLesson = useMemo(() => { + const now = moment(); + const upcoming = lessonEvents + .filter((event) => event.type === "booked" && moment(event.start as Date).isSameOrAfter(now)) + .sort((a, b) => moment(a.start as Date).diff(moment(b.start as Date))); + return upcoming[0] ?? null; + }, [lessonEvents]); + + const filteredCalendarEvents = useMemo(() => { + const now = moment(); + const startOfToday = now.clone().startOf("day"); + const endOfToday = now.clone().endOf("day"); + const endOfNext3Days = now.clone().add(2, "days").endOf("day"); + const startOfWeek = now.clone().startOf("isoWeek"); + const endOfWeek = now.clone().endOf("isoWeek"); + + return calendarEvents.filter((event) => { + const eventStart = moment(event.start as Date); + + if (timeFilter === "today" && !eventStart.isBetween(startOfToday, endOfToday, undefined, "[]")) { + return false; + } + + if (timeFilter === "next3" && !eventStart.isBetween(startOfToday, endOfNext3Days, undefined, "[]")) { + return false; + } + + if (timeFilter === "week" && !eventStart.isBetween(startOfWeek, endOfWeek, undefined, "[]")) { + return false; + } + + if (activityFilter === "lessons" && isAvailabilityEvent(event)) { + return false; + } + + if (activityFilter === "availability" && !isAvailabilityEvent(event)) { + return false; + } + + if (activityFilter === "booked") { + if (isAvailabilityEvent(event)) { + return false; + } + const lessonEvent = event as LessonEvent; + if (lessonEvent.type !== "booked") { + return false; + } + } + + return true; + }); + }, [activityFilter, calendarEvents, timeFilter]); + const calendarBusy = loading || coachScheduleLoading || coachLessonLoading; const busyMessage = loading ? "Loading lessons..." @@ -561,6 +674,18 @@ const PlayerCalendar = () => { [], ); + const handlePreviousDay = useCallback( + () => setCurrentDate((prevDate) => moment(prevDate).subtract(1, "day").toDate()), + [], + ); + + const handleNextDay = useCallback( + () => setCurrentDate((prevDate) => moment(prevDate).add(1, "day").toDate()), + [], + ); + + const handleResetToToday = useCallback(() => setCurrentDate(new Date()), []); + const fallbackCoachOptions = useMemo(() => { const seen = new Map(); rawLessons.forEach((lesson) => { @@ -736,94 +861,229 @@ const PlayerCalendar = () => { selectedLesson && determineEventType(selectedLesson, bookingSet); return ( -
-
-

- Find a Lesson -

-

- Browse open lesson times, see your bookings, and reserve a spot directly from the calendar. -

-
- -
-
- - -
-
- - -
-
- - +
+
+
+
+ + Ready to play? + +
+

+ What do you want to play today? +

+

+ Find your next match, lesson, or workout. Filter by coach, location, or time and book in just a few clicks. +

+
+
+
+ Next booking + {nextBookedLesson ? ( + <> +

+ {formatLessonTitle(nextBookedLesson.resource)} +

+
+ {moment(nextBookedLesson.start as Date).format("dddd • MMM D, h:mm A")} –{" "} + {moment(nextBookedLesson.end as Date).format("h:mm A")} +
+ {nextBookedLesson.resource.location_name ? ( +
+
+ ) : null} + + ) : ( +

+ You don't have any upcoming bookings. Explore the schedule and reserve your next activity. +

+ )} +
-
- - setSearchQuery(event.target.value)} - className="h-11 rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-900 shadow-sm transition placeholder:text-slate-400 focus:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-100" - /> + +
+
+
+
+ + +
+
+
+ Select day +
+ +
+ {moment(currentDate).format("dddd, MMM D")} +
+ + +
+
+
+
+ +
+
+ Timeframe +
+ {timeFilterOptions.map((option) => { + const isActive = timeFilter === option.id; + return ( + + ); + })} +
+
+ +
+ Activity type +
+ {activityFilterOptions.map((option) => { + const isActive = activityFilter === option.id; + return ( + + ); + })} +
+
+
+
+ +
+
+ + +
+
+ + +
+
+ + setSearchQuery(event.target.value)} + className="h-11 rounded-2xl border border-slate-200 bg-white px-3 text-sm text-slate-900 shadow-sm transition placeholder:text-slate-400 focus:border-emerald-400 focus:outline-none focus:ring-2 focus:ring-emerald-100" + /> +
+
@@ -836,7 +1096,7 @@ const PlayerCalendar = () => {