feat: shared GroupCard component, admin status select, schedule level filter
- Extract shared GroupCard component used by both Schedule GroupView and TeamProfile - Admin schedule: replace hasSlots/recruiting toggles with single Status select - User schedule: add level filter pills (Начинающий/Без опыта, Продвинутый) - Consistent group card styling across schedule and trainer bio views
This commit is contained in:
@@ -22,6 +22,7 @@ interface ScheduleState {
|
||||
filterTrainer: string | null;
|
||||
filterTypes: Set<string>;
|
||||
filterStatusSet: Set<StatusTag>;
|
||||
filterLevel: string | null;
|
||||
filterTime: TimeFilter;
|
||||
filterDaySet: Set<string>;
|
||||
bookingGroup: string | null;
|
||||
@@ -33,6 +34,7 @@ type ScheduleAction =
|
||||
| { type: "SET_TRAINER"; value: string | null }
|
||||
| { type: "TOGGLE_TYPE"; value: string }
|
||||
| { type: "TOGGLE_STATUS"; value: StatusTag }
|
||||
| { type: "SET_LEVEL"; value: string | null }
|
||||
| { type: "SET_TIME"; value: TimeFilter }
|
||||
| { type: "TOGGLE_DAY"; day: string }
|
||||
| { type: "SET_BOOKING"; value: string | null }
|
||||
@@ -44,6 +46,7 @@ const initialState: ScheduleState = {
|
||||
filterTrainer: null,
|
||||
filterTypes: new Set(),
|
||||
filterStatusSet: new Set(),
|
||||
filterLevel: null,
|
||||
filterTime: "all",
|
||||
filterDaySet: new Set(),
|
||||
bookingGroup: null,
|
||||
@@ -69,6 +72,8 @@ function scheduleReducer(state: ScheduleState, action: ScheduleAction): Schedule
|
||||
else next.add(action.value);
|
||||
return { ...state, filterStatusSet: next };
|
||||
}
|
||||
case "SET_LEVEL":
|
||||
return { ...state, filterLevel: action.value };
|
||||
case "SET_TIME":
|
||||
return { ...state, filterTime: action.value };
|
||||
case "TOGGLE_DAY": {
|
||||
@@ -80,7 +85,7 @@ function scheduleReducer(state: ScheduleState, action: ScheduleAction): Schedule
|
||||
case "SET_BOOKING":
|
||||
return { ...state, bookingGroup: action.value };
|
||||
case "CLEAR_FILTERS":
|
||||
return { ...state, filterTrainer: null, filterTypes: new Set(), filterStatusSet: new Set(), filterTime: "all", filterDaySet: new Set() };
|
||||
return { ...state, filterTrainer: null, filterTypes: new Set(), filterStatusSet: new Set(), filterLevel: null, filterTime: "all", filterDaySet: new Set() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +97,7 @@ interface ScheduleProps {
|
||||
|
||||
export function Schedule({ data: schedule, classItems, teamMembers }: ScheduleProps) {
|
||||
const [state, dispatch] = useReducer(scheduleReducer, initialState);
|
||||
const { locationMode, viewMode, filterTrainer, filterTypes, filterStatusSet, filterTime, filterDaySet, bookingGroup } = state;
|
||||
const { locationMode, viewMode, filterTrainer, filterTypes, filterStatusSet, filterLevel, filterTime, filterDaySet, bookingGroup } = state;
|
||||
|
||||
const isAllMode = locationMode === "all";
|
||||
|
||||
@@ -104,6 +109,7 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
const setFilterTrainer = useCallback((value: string | null) => dispatch({ type: "SET_TRAINER", value }), []);
|
||||
const toggleFilterType = useCallback((value: string) => dispatch({ type: "TOGGLE_TYPE", value }), []);
|
||||
const toggleFilterStatus = useCallback((value: StatusTag) => dispatch({ type: "TOGGLE_STATUS", value }), []);
|
||||
const setFilterLevel = useCallback((value: string | null) => dispatch({ type: "SET_LEVEL", value }), []);
|
||||
const setFilterTime = useCallback((value: TimeFilter) => dispatch({ type: "SET_TIME", value }), []);
|
||||
|
||||
const setFilterTrainerFromCard = useCallback((trainer: string | null) => {
|
||||
@@ -170,8 +176,9 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
.map((d) => dayMap.get(d)!);
|
||||
}, [locationMode, schedule.locations]);
|
||||
|
||||
const { types, hasAnySlots, hasAnyRecruiting } = useMemo(() => {
|
||||
const { types, hasAnySlots, hasAnyRecruiting, levels } = useMemo(() => {
|
||||
const typeSet = new Set<string>();
|
||||
const levelSet = new Set<string>();
|
||||
let slots = false;
|
||||
let recruiting = false;
|
||||
for (const day of activeDays) {
|
||||
@@ -179,12 +186,14 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
typeSet.add(cls.type);
|
||||
if (cls.hasSlots) slots = true;
|
||||
if (cls.recruiting) recruiting = true;
|
||||
if (cls.level) levelSet.add(cls.level);
|
||||
}
|
||||
}
|
||||
return {
|
||||
types: Array.from(typeSet).sort(),
|
||||
hasAnySlots: slots,
|
||||
hasAnyRecruiting: recruiting,
|
||||
levels: Array.from(levelSet).sort(),
|
||||
};
|
||||
}, [activeDays]);
|
||||
|
||||
@@ -194,7 +203,7 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
: null;
|
||||
|
||||
const filteredDays: ScheduleDayMerged[] = useMemo(() => {
|
||||
const noFilter = !filterTrainer && filterTypes.size === 0 && filterStatusSet.size === 0 && filterTime === "all" && filterDaySet.size === 0;
|
||||
const noFilter = !filterTrainer && filterTypes.size === 0 && filterStatusSet.size === 0 && !filterLevel && filterTime === "all" && filterDaySet.size === 0;
|
||||
if (noFilter) return activeDays;
|
||||
|
||||
// First filter by day names if any selected
|
||||
@@ -212,6 +221,7 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
(filterStatusSet.size === 0 ||
|
||||
(filterStatusSet.has("hasSlots") && cls.hasSlots) ||
|
||||
(filterStatusSet.has("recruiting") && cls.recruiting)) &&
|
||||
(!filterLevel || cls.level === filterLevel) &&
|
||||
(!activeTimeRange || (() => {
|
||||
const m = startTimeMinutes(cls.time);
|
||||
return m >= activeTimeRange[0] && m < activeTimeRange[1];
|
||||
@@ -219,9 +229,9 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
),
|
||||
}))
|
||||
.filter((day) => day.classes.length > 0);
|
||||
}, [activeDays, filterTrainer, filterTypes, filterStatusSet, filterTime, activeTimeRange, filterDaySet]);
|
||||
}, [activeDays, filterTrainer, filterTypes, filterStatusSet, filterLevel, filterTime, activeTimeRange, filterDaySet]);
|
||||
|
||||
const hasActiveFilter = !!(filterTrainer || filterTypes.size > 0 || filterStatusSet.size > 0 || filterTime !== "all" || filterDaySet.size > 0);
|
||||
const hasActiveFilter = !!(filterTrainer || filterTypes.size > 0 || filterStatusSet.size > 0 || filterLevel || filterTime !== "all" || filterDaySet.size > 0);
|
||||
|
||||
function clearFilters() {
|
||||
dispatch({ type: "CLEAR_FILTERS" });
|
||||
@@ -346,11 +356,14 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr
|
||||
types={types}
|
||||
hasAnySlots={hasAnySlots}
|
||||
hasAnyRecruiting={hasAnyRecruiting}
|
||||
levels={levels}
|
||||
filterTypes={filterTypes}
|
||||
toggleFilterType={toggleFilterType}
|
||||
filterTrainer={filterTrainer}
|
||||
filterStatusSet={filterStatusSet}
|
||||
toggleFilterStatus={toggleFilterStatus}
|
||||
filterLevel={filterLevel}
|
||||
setFilterLevel={setFilterLevel}
|
||||
filterTime={filterTime}
|
||||
setFilterTime={setFilterTime}
|
||||
availableDays={availableDays}
|
||||
|
||||
Reference in New Issue
Block a user