feat: schedule filters overhaul, local fonts, configurable statuses/levels

Schedule filters:
- Airbnb-style filter modal with sections: directions, trainer, status, level, days, time
- Multi-select trainer filter with search input
- Custom time range (from-to) with preset shortcuts
- Gold tag design for class types, statuses, and levels
- Hover tooltips on level/status options with descriptions from config
- Filter icon button inline with view toggle (По дням / По группам)

Admin schedule:
- Configurable experience levels and statuses (add/edit/reorder/delete)
- New scheduleConfig DB section with auto-save
- Status/level dropdowns in class editor read from config
- Status select built dynamically from config
- New status field on ScheduleClass for custom statuses

Other:
- Local fonts (Inter + Oswald) bundled in public/fonts — no Google Fonts dependency
- SelectField combobox: search in main input field, no separate search inside dropdown
- Fix carousel trainer label flash on drag release
This commit is contained in:
2026-03-27 19:13:43 +03:00
parent d5541a8bc9
commit a69c08482f
17 changed files with 755 additions and 266 deletions
+10 -10
View File
@@ -6,8 +6,8 @@ interface DayCardProps {
day: ScheduleDayMerged;
typeDots: Record<string, string>;
showLocation?: boolean;
filterTrainer: string | null;
setFilterTrainer: (trainer: string | null) => void;
filterTrainerSet: Set<string>;
toggleFilterTrainer: (trainer: string | null) => void;
filterTypes: Set<string>;
toggleFilterType: (type: string) => void;
}
@@ -15,15 +15,15 @@ interface DayCardProps {
function ClassRow({
cls,
typeDots,
filterTrainer,
setFilterTrainer,
filterTrainerSet,
toggleFilterTrainer,
filterTypes,
toggleFilterType,
}: {
cls: ScheduleClassWithLocation;
typeDots: Record<string, string>;
filterTrainer: string | null;
setFilterTrainer: (trainer: string | null) => void;
filterTrainerSet: Set<string>;
toggleFilterTrainer: (trainer: string | null) => void;
filterTypes: Set<string>;
toggleFilterType: (type: string) => void;
}) {
@@ -46,7 +46,7 @@ function ClassRow({
)}
</div>
<button
onClick={() => setFilterTrainer(filterTrainer === cls.trainer ? null : cls.trainer)}
onClick={() => toggleFilterTrainer(cls.trainer)}
className="mt-1.5 flex items-center gap-2 text-sm font-medium cursor-pointer active:opacity-60 text-neutral-800 dark:text-white/80"
>
<User size={13} className="shrink-0 text-neutral-400 dark:text-white/30" />
@@ -70,7 +70,7 @@ function ClassRow({
);
}
export function DayCard({ day, typeDots, showLocation, filterTrainer, setFilterTrainer, filterTypes, toggleFilterType }: DayCardProps) {
export function DayCard({ day, typeDots, showLocation, filterTrainerSet, toggleFilterTrainer, filterTypes, toggleFilterType }: DayCardProps) {
// Group classes by location when showLocation is true
const locationGroups = showLocation
? Array.from(
@@ -115,7 +115,7 @@ export function DayCard({ day, typeDots, showLocation, filterTrainer, setFilterT
</div>
<div className="divide-y divide-neutral-100 dark:divide-white/[0.04]">
{classes.map((cls, i) => (
<ClassRow key={i} cls={cls} typeDots={typeDots} filterTrainer={filterTrainer} setFilterTrainer={setFilterTrainer} filterTypes={filterTypes} toggleFilterType={toggleFilterType} />
<ClassRow key={i} cls={cls} typeDots={typeDots} filterTrainerSet={filterTrainerSet} toggleFilterTrainer={toggleFilterTrainer} filterTypes={filterTypes} toggleFilterType={toggleFilterType} />
))}
</div>
</div>
@@ -125,7 +125,7 @@ export function DayCard({ day, typeDots, showLocation, filterTrainer, setFilterT
// Single location — no sub-headers
<div className="divide-y divide-neutral-100 dark:divide-white/[0.04]">
{day.classes.map((cls, i) => (
<ClassRow key={i} cls={cls} typeDots={typeDots} filterTrainer={filterTrainer} setFilterTrainer={setFilterTrainer} filterTypes={filterTypes} toggleFilterType={toggleFilterType} />
<ClassRow key={i} cls={cls} typeDots={typeDots} filterTrainerSet={filterTrainerSet} toggleFilterTrainer={toggleFilterTrainer} filterTypes={filterTypes} toggleFilterType={toggleFilterType} />
))}
</div>
)}