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:
@@ -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
|
||||
key={statusKey}
|
||||
onClick={() => toggleFilterStatus(statusKey)}
|
||||
className={`${pillBase} ${
|
||||
className={`flex items-center gap-3 w-full rounded-xl p-3 border-l-[3px] transition-all cursor-pointer ${
|
||||
active
|
||||
? "bg-gold text-black border border-gold"
|
||||
: "border border-gold/30 text-white hover:border-gold/60 hover:bg-gold/10"
|
||||
? "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}
|
||||
</span>
|
||||
{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>
|
||||
)}
|
||||
</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>
|
||||
{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>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FilterSection>
|
||||
)}
|
||||
|
||||
{/* Level — gold tags with hover hints */}
|
||||
{/* Level — radio cards */}
|
||||
{levels.length > 0 && (
|
||||
<FilterSection title="Опыт">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -201,21 +228,24 @@ 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" }}>
|
||||
<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,47 +271,23 @@ 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>
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<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]"
|
||||
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>
|
||||
<span className="text-neutral-500 mt-5">—</span>
|
||||
<div className="flex-1">
|
||||
<label className="block text-[10px] text-neutral-500 mb-1">До</label>
|
||||
<span className="text-neutral-500 text-xs">—</span>
|
||||
<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]"
|
||||
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>
|
||||
</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>
|
||||
</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,
|
||||
|
||||
Reference in New Issue
Block a user