diff --git a/src/components/sections/schedule/DayCard.tsx b/src/components/sections/schedule/DayCard.tsx index c47391c..f11c63a 100644 --- a/src/components/sections/schedule/DayCard.tsx +++ b/src/components/sections/schedule/DayCard.tsx @@ -1,5 +1,6 @@ import { Clock, User, MapPin } from "lucide-react"; import { shortAddress } from "./constants"; +import { ScheduleBadge } from "@/components/ui/ScheduleBadge"; import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants"; interface DayCardProps { @@ -34,25 +35,15 @@ function ClassRow({ {cls.time} - {cls.hasSlots && ( - - есть места - - )} - {cls.recruiting && ( - - набор - - )} + {cls.hasSlots && есть места} + {cls.recruiting && набор} {cls.status && cls.status !== "hasSlots" && cls.status !== "recruiting" && ( - - {cls.status} - + {cls.status} )} - {cls.level && ( - - {cls.level} - - )} + {cls.level && {cls.level}} ); @@ -111,11 +98,13 @@ export function DayCard({ day, typeDots, showLocation, filterTrainerSet, toggleF {locationGroups.map(([locName, { address, classes }], gi) => (
{/* Location sub-header */} -
0 ? "border-t border-neutral-200 dark:border-white/[0.06]" : ""}`}> - - +
0 ? "border-t border-gold/10" : ""}`}> + + {locName} - {address && · {shortAddress(address)}} + {address && shortAddress(address) !== locName && ( + · {shortAddress(address)} + )}
diff --git a/src/components/sections/schedule/GroupView.tsx b/src/components/sections/schedule/GroupView.tsx index c7514a7..767e2fb 100644 --- a/src/components/sections/schedule/GroupView.tsx +++ b/src/components/sections/schedule/GroupView.tsx @@ -195,13 +195,13 @@ export function GroupView({
)} - {/* Name — clicks to filter */} + {/* Name — clicks to open bio */} - {cls.hasSlots && ( - - места - - )} - {cls.recruiting && ( - - набор - - )} + {cls.hasSlots && места} + {cls.recruiting && набор} {cls.status && cls.status !== "hasSlots" && cls.status !== "recruiting" && ( - - {cls.status} - - )} - {cls.level && ( - - {cls.level} - + {cls.status} )} + {cls.level && {cls.level}}
{desc && ( - e.stopPropagation()}> - ? - - {desc} - + + )} - - ); - })} -
- {/* Mobile — compact pills */} -
- {availableStatuses.map((statusKey) => { - const cfg = findStatusConfig(scheduleConfig?.statuses, statusKey); - const label = cfg?.label || statusKey; - const active = filterStatusSet.has(statusKey); - return ( - +
); })}
)} - {/* Level — radio cards */} + {/* Level — radio list */} {levels.length > 0 && ( -
+
{levels.map((level) => { const desc = scheduleConfig?.levels?.find((l) => l.value === level)?.description; const active = filterLevel === level; return ( - - - {desc && ( - - ? - - {desc} - - - )} - + + {desc && } + ); })}
)} - {/* When — days + time combined */} + {/* When — days + time inline */} -
- {availableDays.map(({ day, dayShort }) => ( - - ))} -
-
+
+
+ {availableDays.map(({ day, dayShort }) => ( + + ))} +
+ 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]" + className="w-16 shrink-0 rounded-md border border-white/[0.08] bg-white/[0.04] px-1.5 py-1.5 text-[11px] text-white text-center outline-none focus:border-gold/40 transition-colors [color-scheme:dark]" /> - + 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]" + className="w-16 shrink-0 rounded-md border border-white/[0.08] bg-white/[0.04] px-1.5 py-1.5 text-[11px] text-white text-center outline-none focus:border-gold/40 transition-colors [color-scheme:dark]" />
@@ -355,6 +320,44 @@ function FilterSection({ title, hint, children }: { title: string; hint?: string } +function InfoTip({ text }: { text: string }) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + function handle(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + } + document.addEventListener("mousedown", handle); + return () => document.removeEventListener("mousedown", handle); + }, [open]); + + return ( + e.stopPropagation()}> + + {open && ( +
+ {/* Tail behind body — always centered */} +
+ {/* Body on top */} +
+ {text} +
+
+ )} + + ); +} + function TrainerMultiSelect({ trainers, selected, diff --git a/src/components/sections/team/TeamProfile.tsx b/src/components/sections/team/TeamProfile.tsx index ae632c0..d94a56b 100644 --- a/src/components/sections/team/TeamProfile.tsx +++ b/src/components/sections/team/TeamProfile.tsx @@ -30,7 +30,7 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) { // Extract trainer's groups from schedule using groupId const uniqueGroups = useMemo(() => { - const groupMap = new Map(); + const groupMap = new Map(); schedule?.forEach(location => { location.days.forEach(day => { day.classes @@ -44,6 +44,8 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) { existing.slots.push({ day: day.day, dayShort: day.dayShort, time: c.time }); if (c.level && !existing.level) existing.level = c.level; if (c.recruiting) existing.recruiting = true; + if (c.hasSlots) existing.hasSlots = true; + if (c.status && !existing.status) existing.status = c.status; } else { groupMap.set(key, { type: c.type, @@ -52,6 +54,8 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) { slots: [{ day: day.day, dayShort: day.dayShort, time: c.time }], level: c.level, recruiting: c.recruiting, + hasSlots: c.hasSlots, + status: c.status, }); } }); @@ -183,6 +187,8 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) { type={g.type} level={g.level} recruiting={g.recruiting} + hasSlots={g.hasSlots} + status={g.status} address={g.address} location={g.location} merged={g.merged} diff --git a/src/components/ui/GroupCard.tsx b/src/components/ui/GroupCard.tsx index 28fe9dd..0ff1206 100644 --- a/src/components/ui/GroupCard.tsx +++ b/src/components/ui/GroupCard.tsx @@ -1,5 +1,6 @@ import { MapPin } from "lucide-react"; import { shortAddress } from "@/components/sections/schedule/constants"; +import { ScheduleBadge } from "@/components/ui/ScheduleBadge"; export interface GroupCardSlot { days: string[]; @@ -50,14 +51,11 @@ export function GroupCard({ const typeCls = compact ? "text-xs" : "text-sm"; const dayPad = compact ? "px-1.5 py-px text-[10px] min-w-[40px]" : "px-2 py-0.5 text-[11px] min-w-[52px]"; const timeCls = compact ? "text-xs" : "text-sm font-medium"; - const badgeSize = compact ? "px-2 py-0.5 text-[9px]" : "px-2.5 py-0.5 text-[10px]"; const locSize = compact ? "px-2 py-0.5 text-[9px]" : "px-2.5 py-0.5 text-[10px]"; const locIcon = compact ? 8 : 9; const levelBadge = level ? ( - - {level} - + {level} ) : null; const typeContent = ( @@ -69,7 +67,7 @@ export function GroupCard({ ); return ( -
+
{/* Type + level + status badges + extras */}
{onTypeClick ? ( @@ -79,27 +77,17 @@ export function GroupCard({ ) : ( {typeContent} )} - {hasSlots && ( - - есть места - - )} - {recruiting && ( - - набор - - )} - {status && status !== "hasSlots" && status !== "recruiting" && ( - - {statusLabel || status} - - )} {showLocation && (address || location) && ( - - + + {shortAddress(address || location || "")} )} + {hasSlots && есть места} + {recruiting && набор} + {status && status !== "hasSlots" && status !== "recruiting" && ( + {statusLabel || status} + )} {extraBadges}
@@ -122,7 +110,7 @@ export function GroupCard({ compact ? ( diff --git a/src/components/ui/ScheduleBadge.tsx b/src/components/ui/ScheduleBadge.tsx new file mode 100644 index 0000000..6d1d5ad --- /dev/null +++ b/src/components/ui/ScheduleBadge.tsx @@ -0,0 +1,20 @@ +interface ScheduleBadgeProps { + children: React.ReactNode; + size?: "sm" | "md"; +} + +/** + * Unified badge for schedule status and level tags. + * Single gold style used consistently across DayCard, GroupCard, MobileSchedule. + */ +export function ScheduleBadge({ children, size = "md" }: ScheduleBadgeProps) { + const sizeClass = size === "sm" + ? "px-1.5 py-px text-[9px]" + : "px-2.5 py-0.5 text-[10px]"; + + return ( + + {children} + + ); +}