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 { useState, useEffect, useRef } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { User, X, Clock, SlidersHorizontal } from "lucide-react";
|
import { X, SlidersHorizontal } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
pillBase,
|
pillBase,
|
||||||
pillActive,
|
|
||||||
pillInactive,
|
|
||||||
TIME_PRESETS,
|
|
||||||
isTimeFilterActive,
|
isTimeFilterActive,
|
||||||
TIME_FILTER_EMPTY,
|
|
||||||
type StatusTag,
|
type StatusTag,
|
||||||
type TimeFilter,
|
type TimeFilter,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
@@ -38,7 +34,6 @@ interface ScheduleFiltersProps {
|
|||||||
clearFilters: () => void;
|
clearFilters: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const divider = <span className="mx-0.5 h-3.5 w-px shrink-0 bg-white/[0.06]" />;
|
|
||||||
|
|
||||||
export function ScheduleFilters({
|
export function ScheduleFilters({
|
||||||
typeDots,
|
typeDots,
|
||||||
@@ -60,7 +55,6 @@ export function ScheduleFilters({
|
|||||||
toggleDay,
|
toggleDay,
|
||||||
trainerNames,
|
trainerNames,
|
||||||
scheduleConfig,
|
scheduleConfig,
|
||||||
hasActiveFilter,
|
|
||||||
clearFilters,
|
clearFilters,
|
||||||
}: ScheduleFiltersProps) {
|
}: ScheduleFiltersProps) {
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
@@ -130,7 +124,7 @@ export function ScheduleFilters({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable content */}
|
{/* 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 */}
|
{/* Class types — gold border, white text; gold bg when active */}
|
||||||
<FilterSection title="Направления">
|
<FilterSection title="Направления">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -163,37 +157,70 @@ export function ScheduleFilters({
|
|||||||
{/* Status — gold tags */}
|
{/* Status — gold tags */}
|
||||||
{availableStatuses.length > 0 && (
|
{availableStatuses.length > 0 && (
|
||||||
<FilterSection title="Статус">
|
<FilterSection title="Статус">
|
||||||
<div className="flex flex-wrap gap-2">
|
{/* Desktop — card layout */}
|
||||||
|
<div className="hidden sm:block space-y-2">
|
||||||
{availableStatuses.map((statusKey) => {
|
{availableStatuses.map((statusKey) => {
|
||||||
const cfg = scheduleConfig?.statuses?.find((s) => s.key === statusKey);
|
const cfg = scheduleConfig?.statuses?.find((s) => s.key === statusKey);
|
||||||
const label = cfg?.label || statusKey;
|
const label = cfg?.label || statusKey;
|
||||||
const desc = cfg?.description;
|
const desc = cfg?.description;
|
||||||
const active = filterStatusSet.has(statusKey);
|
const active = filterStatusSet.has(statusKey);
|
||||||
return (
|
return (
|
||||||
<span key={statusKey} className="relative group">
|
<button
|
||||||
<button
|
key={statusKey}
|
||||||
onClick={() => toggleFilterStatus(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
|
active
|
||||||
? "bg-gold text-black border border-gold"
|
? "border-l-gold bg-gold/10"
|
||||||
: "border border-gold/30 text-white hover:border-gold/60 hover: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}
|
{label}
|
||||||
</button>
|
</span>
|
||||||
{desc && (
|
{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 ml-auto" onClick={(e) => e.stopPropagation()}>
|
||||||
{desc}
|
<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>
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
</FilterSection>
|
</FilterSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Level — gold tags with hover hints */}
|
{/* Level — radio cards */}
|
||||||
{levels.length > 0 && (
|
{levels.length > 0 && (
|
||||||
<FilterSection title="Опыт">
|
<FilterSection title="Опыт">
|
||||||
<div className="flex flex-wrap gap-2">
|
<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 desc = scheduleConfig?.levels?.find((l) => l.value === level)?.description;
|
||||||
const active = filterLevel === level;
|
const active = filterLevel === level;
|
||||||
return (
|
return (
|
||||||
<span key={level} className="relative group">
|
<span key={level} className="relative group inline-flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterLevel(active ? null : level)}
|
onClick={() => setFilterLevel(active ? null : level)}
|
||||||
className={`${pillBase} ${
|
className={`rounded-xl px-4 py-2 text-xs font-semibold transition-all cursor-pointer border ${
|
||||||
active
|
active
|
||||||
? "bg-gold text-black border border-gold"
|
? "border-gold bg-gold/10 text-white"
|
||||||
: "border border-gold/30 text-white hover:border-gold/60 hover:bg-gold/10"
|
: "border-white/[0.06] bg-white/[0.02] text-neutral-400 hover:border-white/[0.15] hover:bg-white/[0.04]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{level}
|
{level}
|
||||||
</button>
|
</button>
|
||||||
{desc && (
|
{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">
|
||||||
{desc}
|
<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>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -224,14 +254,14 @@ export function ScheduleFilters({
|
|||||||
</FilterSection>
|
</FilterSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Days — calendar grid */}
|
{/* When — days + time combined */}
|
||||||
<FilterSection title="День недели">
|
<FilterSection title="Когда">
|
||||||
<div className="grid grid-cols-7 gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{availableDays.map(({ day, dayShort }) => (
|
{availableDays.map(({ day, dayShort }) => (
|
||||||
<button
|
<button
|
||||||
key={day}
|
key={day}
|
||||||
onClick={() => toggleDay(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)
|
filterDaySet.has(day)
|
||||||
? "bg-gold text-black"
|
? "bg-gold text-black"
|
||||||
: "bg-white/[0.04] text-neutral-400 hover:bg-white/[0.08] hover:text-white"
|
: "bg-white/[0.04] text-neutral-400 hover:bg-white/[0.08] hover:text-white"
|
||||||
@@ -241,46 +271,22 @@ export function ScheduleFilters({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</FilterSection>
|
<div className="flex items-center gap-2 mt-3">
|
||||||
|
<input
|
||||||
{/* Time — from/to inputs + preset shortcuts */}
|
type="time"
|
||||||
<FilterSection title="Время">
|
value={filterTime.from}
|
||||||
<div className="flex items-center gap-2 mb-3">
|
onChange={(e) => setFilterTime({ ...filterTime, from: e.target.value })}
|
||||||
<div className="flex-1">
|
placeholder="С"
|
||||||
<label className="block text-[10px] text-neutral-500 mb-1">С</label>
|
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]"
|
||||||
<input
|
/>
|
||||||
type="time"
|
<span className="text-neutral-500 text-xs">—</span>
|
||||||
value={filterTime.from}
|
<input
|
||||||
onChange={(e) => setFilterTime({ ...filterTime, from: e.target.value })}
|
type="time"
|
||||||
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]"
|
value={filterTime.to}
|
||||||
/>
|
onChange={(e) => setFilterTime({ ...filterTime, to: e.target.value })}
|
||||||
</div>
|
placeholder="До"
|
||||||
<span className="text-neutral-500 mt-5">—</span>
|
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 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>
|
</div>
|
||||||
</FilterSection>
|
</FilterSection>
|
||||||
</div>
|
</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({
|
function TrainerMultiSelect({
|
||||||
trainers,
|
trainers,
|
||||||
|
|||||||
Reference in New Issue
Block a user