feat: UI improvements — scrollbar, multi-filters, pricing fix, routing, modals
- Global page scrollbar styled with gold theme - Schedule: multi-select for class types and status tags - Pricing: fix tab switch blink (display toggle vs conditional render) - OpenDay: trainer name more prominent, section divider added - Team: browser back button closes trainer bio (history API) - Modals: block scroll + compensate scrollbar width to prevent layout shift - Header: remove booking button from desktop nav
This commit is contained in:
@@ -10,7 +10,7 @@ import { ScheduleFilters } from "./schedule/ScheduleFilters";
|
||||
import { MobileSchedule } from "./schedule/MobileSchedule";
|
||||
import { GroupView } from "./schedule/GroupView";
|
||||
import { buildTypeDots, shortAddress, startTimeMinutes, TIME_PRESETS } from "./schedule/constants";
|
||||
import type { StatusFilter, TimeFilter, ScheduleDayMerged, ScheduleClassWithLocation } from "./schedule/constants";
|
||||
import type { StatusTag, TimeFilter, ScheduleDayMerged, ScheduleClassWithLocation } from "./schedule/constants";
|
||||
import type { SiteContent } from "@/types/content";
|
||||
|
||||
type ViewMode = "days" | "groups";
|
||||
@@ -20,8 +20,8 @@ interface ScheduleState {
|
||||
locationMode: LocationMode;
|
||||
viewMode: ViewMode;
|
||||
filterTrainer: string | null;
|
||||
filterType: string | null;
|
||||
filterStatus: StatusFilter;
|
||||
filterTypes: Set<string>;
|
||||
filterStatusSet: Set<StatusTag>;
|
||||
filterTime: TimeFilter;
|
||||
filterDaySet: Set<string>;
|
||||
bookingGroup: string | null;
|
||||
@@ -31,8 +31,8 @@ type ScheduleAction =
|
||||
| { type: "SET_LOCATION"; mode: LocationMode }
|
||||
| { type: "SET_VIEW"; mode: ViewMode }
|
||||
| { type: "SET_TRAINER"; value: string | null }
|
||||
| { type: "SET_TYPE"; value: string | null }
|
||||
| { type: "SET_STATUS"; value: StatusFilter }
|
||||
| { type: "TOGGLE_TYPE"; value: string }
|
||||
| { type: "TOGGLE_STATUS"; value: StatusTag }
|
||||
| { type: "SET_TIME"; value: TimeFilter }
|
||||
| { type: "TOGGLE_DAY"; day: string }
|
||||
| { type: "SET_BOOKING"; value: string | null }
|
||||
@@ -42,8 +42,8 @@ const initialState: ScheduleState = {
|
||||
locationMode: "all",
|
||||
viewMode: "groups",
|
||||
filterTrainer: null,
|
||||
filterType: null,
|
||||
filterStatus: "all",
|
||||
filterTypes: new Set(),
|
||||
filterStatusSet: new Set(),
|
||||
filterTime: "all",
|
||||
filterDaySet: new Set(),
|
||||
bookingGroup: null,
|
||||
@@ -57,10 +57,18 @@ function scheduleReducer(state: ScheduleState, action: ScheduleAction): Schedule
|
||||
return { ...state, viewMode: action.mode };
|
||||
case "SET_TRAINER":
|
||||
return { ...state, filterTrainer: action.value };
|
||||
case "SET_TYPE":
|
||||
return { ...state, filterType: action.value };
|
||||
case "SET_STATUS":
|
||||
return { ...state, filterStatus: action.value };
|
||||
case "TOGGLE_TYPE": {
|
||||
const next = new Set(state.filterTypes);
|
||||
if (next.has(action.value)) next.delete(action.value);
|
||||
else next.add(action.value);
|
||||
return { ...state, filterTypes: next };
|
||||
}
|
||||
case "TOGGLE_STATUS": {
|
||||
const next = new Set(state.filterStatusSet);
|
||||
if (next.has(action.value)) next.delete(action.value);
|
||||
else next.add(action.value);
|
||||
return { ...state, filterStatusSet: next };
|
||||
}
|
||||
case "SET_TIME":
|
||||
return { ...state, filterTime: action.value };
|
||||
case "TOGGLE_DAY": {
|
||||
@@ -72,7 +80,7 @@ function scheduleReducer(state: ScheduleState, action: ScheduleAction): Schedule
|
||||
case "SET_BOOKING":
|
||||
return { ...state, bookingGroup: action.value };
|
||||
case "CLEAR_FILTERS":
|
||||
return { ...state, filterTrainer: null, filterType: null, filterStatus: "all", filterTime: "all", filterDaySet: new Set() };
|
||||
return { ...state, filterTrainer: null, filterTypes: new Set(), filterStatusSet: new Set(), filterTime: "all", filterDaySet: new Set() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +92,7 @@ interface ScheduleProps {
|
||||
|
||||
export function Schedule({ data: schedule, classItems, teamMembers }: ScheduleProps) {
|
||||
const [state, dispatch] = useReducer(scheduleReducer, initialState);
|
||||
const { locationMode, viewMode, filterTrainer, filterType, filterStatus, filterTime, filterDaySet, bookingGroup } = state;
|
||||
const { locationMode, viewMode, filterTrainer, filterTypes, filterStatusSet, filterTime, filterDaySet, bookingGroup } = state;
|
||||
|
||||
const isAllMode = locationMode === "all";
|
||||
|
||||
@@ -94,8 +102,8 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
}, []);
|
||||
|
||||
const setFilterTrainer = useCallback((value: string | null) => dispatch({ type: "SET_TRAINER", value }), []);
|
||||
const setFilterType = useCallback((value: string | null) => dispatch({ type: "SET_TYPE", value }), []);
|
||||
const setFilterStatus = useCallback((value: StatusFilter) => dispatch({ type: "SET_STATUS", value }), []);
|
||||
const toggleFilterType = useCallback((value: string) => dispatch({ type: "TOGGLE_TYPE", value }), []);
|
||||
const toggleFilterStatus = useCallback((value: StatusTag) => dispatch({ type: "TOGGLE_STATUS", value }), []);
|
||||
const setFilterTime = useCallback((value: TimeFilter) => dispatch({ type: "SET_TIME", value }), []);
|
||||
|
||||
const setFilterTrainerFromCard = useCallback((trainer: string | null) => {
|
||||
@@ -103,9 +111,9 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
if (trainer) scrollToSchedule();
|
||||
}, [scrollToSchedule]);
|
||||
|
||||
const setFilterTypeFromCard = useCallback((type: string | null) => {
|
||||
dispatch({ type: "SET_TYPE", value: type });
|
||||
if (type) scrollToSchedule();
|
||||
const toggleFilterTypeFromCard = useCallback((type: string) => {
|
||||
dispatch({ type: "TOGGLE_TYPE", value: type });
|
||||
scrollToSchedule();
|
||||
}, [scrollToSchedule]);
|
||||
|
||||
const typeDots = useMemo(() => buildTypeDots(classItems), [classItems]);
|
||||
@@ -186,7 +194,7 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
: null;
|
||||
|
||||
const filteredDays: ScheduleDayMerged[] = useMemo(() => {
|
||||
const noFilter = !filterTrainer && !filterType && filterStatus === "all" && filterTime === "all" && filterDaySet.size === 0;
|
||||
const noFilter = !filterTrainer && filterTypes.size === 0 && filterStatusSet.size === 0 && filterTime === "all" && filterDaySet.size === 0;
|
||||
if (noFilter) return activeDays;
|
||||
|
||||
// First filter by day names if any selected
|
||||
@@ -200,10 +208,10 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
classes: day.classes.filter(
|
||||
(cls) =>
|
||||
(!filterTrainer || cls.trainer === filterTrainer) &&
|
||||
(!filterType || cls.type === filterType) &&
|
||||
(filterStatus === "all" ||
|
||||
(filterStatus === "hasSlots" && cls.hasSlots) ||
|
||||
(filterStatus === "recruiting" && cls.recruiting)) &&
|
||||
(filterTypes.size === 0 || filterTypes.has(cls.type)) &&
|
||||
(filterStatusSet.size === 0 ||
|
||||
(filterStatusSet.has("hasSlots") && cls.hasSlots) ||
|
||||
(filterStatusSet.has("recruiting") && cls.recruiting)) &&
|
||||
(!activeTimeRange || (() => {
|
||||
const m = startTimeMinutes(cls.time);
|
||||
return m >= activeTimeRange[0] && m < activeTimeRange[1];
|
||||
@@ -211,9 +219,9 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
),
|
||||
}))
|
||||
.filter((day) => day.classes.length > 0);
|
||||
}, [activeDays, filterTrainer, filterType, filterStatus, filterTime, activeTimeRange, filterDaySet]);
|
||||
}, [activeDays, filterTrainer, filterTypes, filterStatusSet, filterTime, activeTimeRange, filterDaySet]);
|
||||
|
||||
const hasActiveFilter = !!(filterTrainer || filterType || filterStatus !== "all" || filterTime !== "all" || filterDaySet.size > 0);
|
||||
const hasActiveFilter = !!(filterTrainer || filterTypes.size > 0 || filterStatusSet.size > 0 || filterTime !== "all" || filterDaySet.size > 0);
|
||||
|
||||
function clearFilters() {
|
||||
dispatch({ type: "CLEAR_FILTERS" });
|
||||
@@ -338,11 +346,11 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
types={types}
|
||||
hasAnySlots={hasAnySlots}
|
||||
hasAnyRecruiting={hasAnyRecruiting}
|
||||
filterType={filterType}
|
||||
setFilterType={setFilterType}
|
||||
filterTypes={filterTypes}
|
||||
toggleFilterType={toggleFilterType}
|
||||
filterTrainer={filterTrainer}
|
||||
filterStatus={filterStatus}
|
||||
setFilterStatus={setFilterStatus}
|
||||
filterStatusSet={filterStatusSet}
|
||||
toggleFilterStatus={toggleFilterStatus}
|
||||
filterTime={filterTime}
|
||||
setFilterTime={setFilterTime}
|
||||
availableDays={availableDays}
|
||||
@@ -361,8 +369,8 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
<MobileSchedule
|
||||
typeDots={typeDots}
|
||||
filteredDays={filteredDays}
|
||||
filterType={filterType}
|
||||
setFilterType={setFilterTypeFromCard}
|
||||
filterTypes={filterTypes}
|
||||
toggleFilterType={toggleFilterTypeFromCard}
|
||||
filterTrainer={filterTrainer}
|
||||
setFilterTrainer={setFilterTrainerFromCard}
|
||||
hasActiveFilter={hasActiveFilter}
|
||||
@@ -382,7 +390,7 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
key={day.day}
|
||||
className={filteredDays.length === 1 ? "w-full max-w-[340px]" : ""}
|
||||
>
|
||||
<DayCard day={day} typeDots={typeDots} showLocation={isAllMode} filterTrainer={filterTrainer} setFilterTrainer={setFilterTrainerFromCard} filterType={filterType} setFilterType={setFilterTypeFromCard} />
|
||||
<DayCard day={day} typeDots={typeDots} showLocation={isAllMode} filterTrainer={filterTrainer} setFilterTrainer={setFilterTrainerFromCard} filterTypes={filterTypes} toggleFilterType={toggleFilterTypeFromCard} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -400,8 +408,8 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
<GroupView
|
||||
typeDots={typeDots}
|
||||
filteredDays={filteredDays}
|
||||
filterType={filterType}
|
||||
setFilterType={setFilterTypeFromCard}
|
||||
filterTypes={filterTypes}
|
||||
toggleFilterType={toggleFilterTypeFromCard}
|
||||
filterTrainer={filterTrainer}
|
||||
setFilterTrainer={setFilterTrainerFromCard}
|
||||
showLocation={isAllMode}
|
||||
|
||||
Reference in New Issue
Block a user