fix: schedule filters — restore status card design, responsive layout, cleanup

- Status: card layout on desktop, compact pills on mobile
- Experience: compact pills with ? hover tooltips (unchanged)
- Remove time presets (Morning/Day/Evening), keep only from-to inputs
- Combine Days + Time into single "Когда" section
- Fix tooltip overflow causing scrollbars (max-w + right-aligned)
- Tighten modal spacing (p-6→px-6 py-4, space-y-7→space-y-5)
- Clean unused imports (pillActive, pillInactive, User, Clock, etc.)
This commit is contained in:
2026-03-27 23:54:16 +03:00
parent a69c08482f
commit b322c969f2
@@ -2,14 +2,10 @@
import { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { User, X, Clock, SlidersHorizontal } from "lucide-react";
import { X, SlidersHorizontal } from "lucide-react";
import {
pillBase,
pillActive,
pillInactive,
TIME_PRESETS,
isTimeFilterActive,
TIME_FILTER_EMPTY,
type StatusTag,
type TimeFilter,
} from "./constants";
@@ -38,7 +34,6 @@ interface ScheduleFiltersProps {
clearFilters: () => void;
}
const divider = <span className="mx-0.5 h-3.5 w-px shrink-0 bg-white/[0.06]" />;
export function ScheduleFilters({
typeDots,
@@ -60,7 +55,6 @@ export function ScheduleFilters({
toggleDay,
trainerNames,
scheduleConfig,
hasActiveFilter,
clearFilters,
}: ScheduleFiltersProps) {
const [modalOpen, setModalOpen] = useState(false);
@@ -130,7 +124,7 @@ export function ScheduleFilters({
</div>
{/* Scrollable content */}
<div className="flex-1 overflow-y-auto p-6 space-y-7">
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-5">
{/* Class types — gold border, white text; gold bg when active */}
<FilterSection title="Направления">
<div className="flex flex-wrap gap-2">
@@ -163,37 +157,70 @@ export function ScheduleFilters({
{/* Status — gold tags */}
{availableStatuses.length > 0 && (
<FilterSection title="Статус">
<div className="flex flex-wrap gap-2">
{/* Desktop — card layout */}
<div className="hidden sm:block space-y-2">
{availableStatuses.map((statusKey) => {
const cfg = scheduleConfig?.statuses?.find((s) => s.key === statusKey);
const label = cfg?.label || statusKey;
const desc = cfg?.description;
const active = filterStatusSet.has(statusKey);
return (
<span key={statusKey} className="relative group">
<button
onClick={() => toggleFilterStatus(statusKey)}
className={`${pillBase} ${
active
? "bg-gold text-black border border-gold"
: "border border-gold/30 text-white hover:border-gold/60 hover:bg-gold/10"
}`}
>
<button
key={statusKey}
onClick={() => toggleFilterStatus(statusKey)}
className={`flex items-center gap-3 w-full rounded-xl p-3 border-l-[3px] transition-all cursor-pointer ${
active
? "border-l-gold bg-gold/10"
: "border-l-white/10 bg-white/[0.02] hover:bg-white/[0.05]"
}`}
>
<span className={`flex h-5 w-5 shrink-0 items-center justify-center rounded transition-colors ${
active ? "bg-gold" : "border border-white/20"
}`}>
{active && (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2.5 6L5 8.5L9.5 3.5" stroke="black" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
)}
</span>
<span className={`text-sm font-medium ${active ? "text-white" : "text-neutral-300"}`}>
{label}
</button>
</span>
{desc && (
<span className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 z-10 w-48 rounded-lg border border-white/10 px-3 py-2 text-[11px] leading-relaxed text-neutral-300 text-center shadow-xl opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity" style={{ backgroundColor: "#1a1a1a" }}>
{desc}
<span className="group relative ml-auto" onClick={(e) => e.stopPropagation()}>
<span className="flex h-4 w-4 items-center justify-center rounded-full border border-white/15 text-[10px] text-neutral-500 hover:text-white hover:border-white/30 transition-colors cursor-help">?</span>
<span className="absolute right-0 bottom-full mb-2 z-10 max-w-[200px] rounded-lg border border-white/10 px-3 py-2 text-[11px] leading-relaxed text-neutral-300 shadow-xl opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" style={{ backgroundColor: "#1a1a1a" }}>
{desc}
</span>
</span>
)}
</span>
</button>
);
})}
</div>
{/* Mobile — compact pills */}
<div className="flex flex-wrap gap-2 sm:hidden">
{availableStatuses.map((statusKey) => {
const cfg = scheduleConfig?.statuses?.find((s) => s.key === statusKey);
const label = cfg?.label || statusKey;
const active = filterStatusSet.has(statusKey);
return (
<button
key={statusKey}
onClick={() => toggleFilterStatus(statusKey)}
className={`rounded-xl px-4 py-2 text-xs font-semibold transition-all cursor-pointer border ${
active
? "border-gold bg-gold/10 text-white"
: "border-white/[0.06] bg-white/[0.02] text-neutral-400 hover:border-white/[0.15]"
}`}
>
{label}
</button>
);
})}
</div>
</FilterSection>
)}
{/* Level — gold tags with hover hints */}
{/* Level — radio cards */}
{levels.length > 0 && (
<FilterSection title="Опыт">
<div className="flex flex-wrap gap-2">
@@ -201,20 +228,23 @@ export function ScheduleFilters({
const desc = scheduleConfig?.levels?.find((l) => l.value === level)?.description;
const active = filterLevel === level;
return (
<span key={level} className="relative group">
<span key={level} className="relative group inline-flex items-center gap-1">
<button
onClick={() => setFilterLevel(active ? null : level)}
className={`${pillBase} ${
className={`rounded-xl px-4 py-2 text-xs font-semibold transition-all cursor-pointer border ${
active
? "bg-gold text-black border border-gold"
: "border border-gold/30 text-white hover:border-gold/60 hover:bg-gold/10"
? "border-gold bg-gold/10 text-white"
: "border-white/[0.06] bg-white/[0.02] text-neutral-400 hover:border-white/[0.15] hover:bg-white/[0.04]"
}`}
>
{level}
</button>
{desc && (
<span className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 z-10 w-48 rounded-lg border border-white/10 px-3 py-2 text-[11px] leading-relaxed text-neutral-300 text-center shadow-xl opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity" style={{ backgroundColor: "#1a1a1a" }}>
{desc}
<span className="group relative">
<span className="flex h-4 w-4 items-center justify-center rounded-full border border-white/15 text-[10px] text-neutral-500 hover:text-white hover:border-white/30 transition-colors cursor-help">?</span>
<span className="absolute right-0 bottom-full mb-2 z-10 max-w-[200px] rounded-lg border border-white/10 px-3 py-2 text-[11px] leading-relaxed text-neutral-300 shadow-xl opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" style={{ backgroundColor: "#1a1a1a" }}>
{desc}
</span>
</span>
)}
</span>
@@ -224,14 +254,14 @@ export function ScheduleFilters({
</FilterSection>
)}
{/* Days — calendar grid */}
<FilterSection title="День недели">
<div className="grid grid-cols-7 gap-1.5">
{/* When — days + time combined */}
<FilterSection title="Когда">
<div className="flex flex-wrap gap-1.5">
{availableDays.map(({ day, dayShort }) => (
<button
key={day}
onClick={() => toggleDay(day)}
className={`flex items-center justify-center rounded-lg py-2.5 text-xs font-semibold transition-all cursor-pointer ${
className={`flex items-center justify-center rounded-lg w-9 h-9 text-[11px] font-semibold transition-all cursor-pointer ${
filterDaySet.has(day)
? "bg-gold text-black"
: "bg-white/[0.04] text-neutral-400 hover:bg-white/[0.08] hover:text-white"
@@ -241,46 +271,22 @@ export function ScheduleFilters({
</button>
))}
</div>
</FilterSection>
{/* Time — from/to inputs + preset shortcuts */}
<FilterSection title="Время">
<div className="flex items-center gap-2 mb-3">
<div className="flex-1">
<label className="block text-[10px] text-neutral-500 mb-1">С</label>
<input
type="time"
value={filterTime.from}
onChange={(e) => setFilterTime({ ...filterTime, from: e.target.value })}
className="w-full rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-sm text-white outline-none focus:border-gold/40 transition-colors [color-scheme:dark]"
/>
</div>
<span className="text-neutral-500 mt-5"></span>
<div className="flex-1">
<label className="block text-[10px] text-neutral-500 mb-1">До</label>
<input
type="time"
value={filterTime.to}
onChange={(e) => setFilterTime({ ...filterTime, to: e.target.value })}
className="w-full rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-sm text-white outline-none focus:border-gold/40 transition-colors [color-scheme:dark]"
/>
</div>
</div>
<div className="flex gap-1.5">
{TIME_PRESETS.map((preset) => {
const isActive = filterTime.from === preset.from && filterTime.to === preset.to;
return (
<button
key={preset.label}
onClick={() => setFilterTime(isActive ? TIME_FILTER_EMPTY : { from: preset.from, to: preset.to })}
className={`${pillBase} ${
isActive ? "bg-gold/15 text-gold border border-gold/30" : pillInactive
}`}
>
{preset.label}
</button>
);
})}
<div className="flex items-center gap-2 mt-3">
<input
type="time"
value={filterTime.from}
onChange={(e) => setFilterTime({ ...filterTime, from: e.target.value })}
placeholder="С"
className="flex-1 rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-1.5 text-xs text-white outline-none focus:border-gold/40 transition-colors [color-scheme:dark]"
/>
<span className="text-neutral-500 text-xs"></span>
<input
type="time"
value={filterTime.to}
onChange={(e) => setFilterTime({ ...filterTime, to: e.target.value })}
placeholder="До"
className="flex-1 rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-1.5 text-xs text-white outline-none focus:border-gold/40 transition-colors [color-scheme:dark]"
/>
</div>
</FilterSection>
</div>
@@ -347,18 +353,6 @@ function FilterSection({ title, hint, children }: { title: string; hint?: string
);
}
function HintBubble({ text }: { text: string }) {
return (
<span className="group relative">
<span className="flex h-4 w-4 items-center justify-center rounded-full border border-white/15 text-[10px] text-neutral-500 hover:text-white hover:border-white/30 transition-colors cursor-help">
?
</span>
<span className="absolute left-6 top-1/2 -translate-y-1/2 z-10 w-48 rounded-lg border border-white/10 px-3 py-2 text-[11px] leading-relaxed text-neutral-300 shadow-xl opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" style={{ backgroundColor: "#1a1a1a" }}>
{text}
</span>
</span>
);
}
function TrainerMultiSelect({
trainers,