feat: unified badges, filter UI overhaul, trainer bio link

- ScheduleBadge: shared gold badge component for all status/level tags
- Replace hardcoded emerald/sky/rose badge colors with unified gold style
- DayCard, GroupCard, MobileSchedule all use ScheduleBadge
- Location tag moved before status badges, gold styling
- GroupCard: flex-col with mt-auto button alignment
- TeamProfile: pass hasSlots/status to GroupCard

Filter modal redesign:
- Status: horizontal Airbnb-style toggle cards with ? info tips
- Experience: vertical radio list with gold dots
- When: days + time inputs on single row
- Click-to-toggle InfoTip replacing hover tooltips
- Gold speech bubble tooltip design

Schedule cards:
- Trainer name click opens bio instead of filtering
- DayCard location header: gold background, no duplicate address
This commit is contained in:
2026-03-29 17:04:40 +03:00
parent bdeedcfcc8
commit 024424c578
7 changed files with 155 additions and 162 deletions
+13 -24
View File
@@ -1,5 +1,6 @@
import { Clock, User, MapPin } from "lucide-react"; import { Clock, User, MapPin } from "lucide-react";
import { shortAddress } from "./constants"; import { shortAddress } from "./constants";
import { ScheduleBadge } from "@/components/ui/ScheduleBadge";
import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants"; import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants";
interface DayCardProps { interface DayCardProps {
@@ -34,25 +35,15 @@ function ClassRow({
<Clock size={13} /> <Clock size={13} />
<span className="font-semibold">{cls.time}</span> <span className="font-semibold">{cls.time}</span>
</div> </div>
{cls.hasSlots && ( {cls.hasSlots && <ScheduleBadge>есть места</ScheduleBadge>}
<span className="shrink-0 rounded-full bg-emerald-500/15 border border-emerald-500/25 px-2 py-0.5 text-[10px] font-semibold text-emerald-600 dark:text-emerald-400"> {cls.recruiting && <ScheduleBadge>набор</ScheduleBadge>}
есть места
</span>
)}
{cls.recruiting && (
<span className="shrink-0 rounded-full bg-sky-500/15 border border-sky-500/25 px-2 py-0.5 text-[10px] font-semibold text-sky-600 dark:text-sky-400">
набор
</span>
)}
{cls.status && cls.status !== "hasSlots" && cls.status !== "recruiting" && ( {cls.status && cls.status !== "hasSlots" && cls.status !== "recruiting" && (
<span className="shrink-0 rounded-full bg-gold/15 border border-gold/25 px-2 py-0.5 text-[10px] font-semibold text-gold"> <ScheduleBadge>{cls.status}</ScheduleBadge>
{cls.status}
</span>
)} )}
</div> </div>
<button <button
onClick={() => toggleFilterTrainer(cls.trainer)} onClick={() => window.dispatchEvent(new CustomEvent("openTrainerProfile", { detail: 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" 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 hover:text-gold transition-colors"
> >
<User size={13} className="shrink-0 text-neutral-400 dark:text-white/30" /> <User size={13} className="shrink-0 text-neutral-400 dark:text-white/30" />
{cls.trainer} {cls.trainer}
@@ -65,11 +56,7 @@ function ClassRow({
<span className={`h-2 w-2 shrink-0 rounded-full ${typeDots[cls.type] ?? "bg-white/30"}`} /> <span className={`h-2 w-2 shrink-0 rounded-full ${typeDots[cls.type] ?? "bg-white/30"}`} />
<span className="text-xs text-neutral-500 dark:text-white/40">{cls.type}</span> <span className="text-xs text-neutral-500 dark:text-white/40">{cls.type}</span>
</button> </button>
{cls.level && ( {cls.level && <ScheduleBadge>{cls.level}</ScheduleBadge>}
<span className="rounded-full bg-rose-500/15 border border-rose-500/25 px-2 py-0.5 text-[10px] font-semibold text-rose-600 dark:text-rose-400">
{cls.level}
</span>
)}
</div> </div>
</div> </div>
); );
@@ -111,11 +98,13 @@ export function DayCard({ day, typeDots, showLocation, filterTrainerSet, toggleF
{locationGroups.map(([locName, { address, classes }], gi) => ( {locationGroups.map(([locName, { address, classes }], gi) => (
<div key={locName}> <div key={locName}>
{/* Location sub-header */} {/* Location sub-header */}
<div className={`flex items-center gap-1.5 px-5 py-2 bg-neutral-100/60 dark:bg-white/[0.03] ${gi > 0 ? "border-t border-neutral-200 dark:border-white/[0.06]" : ""}`}> <div className={`flex items-center gap-1.5 px-5 py-2 bg-gold/10 ${gi > 0 ? "border-t border-gold/10" : ""}`}>
<MapPin size={11} className="shrink-0 text-gold/60" /> <MapPin size={11} className="shrink-0 text-gold" />
<span className="text-[11px] font-medium text-neutral-600 dark:text-white/50"> <span className="text-[11px] font-medium text-white">
{locName} {locName}
{address && <span className="text-neutral-400 dark:text-white/30"> · {shortAddress(address)}</span>} {address && shortAddress(address) !== locName && (
<span className="text-white/50"> · {shortAddress(address)}</span>
)}
</span> </span>
</div> </div>
<div className="divide-y divide-neutral-100 dark:divide-white/[0.04]"> <div className="divide-y divide-neutral-100 dark:divide-white/[0.04]">
@@ -195,13 +195,13 @@ export function GroupView({
</div> </div>
)} )}
</button> </button>
{/* Name — clicks to filter */} {/* Name — clicks to open bio */}
<button <button
onClick={() => toggleFilterTrainer(trainer)} onClick={() => window.dispatchEvent(new CustomEvent("openTrainerProfile", { detail: trainer }))}
className="cursor-pointer group" className="cursor-pointer group"
> >
<span className={`text-base font-semibold transition-colors ${ <span className={`text-base font-semibold transition-colors ${
isActive ? "text-gold" : "text-white/90 group-hover:text-white" isActive ? "text-gold" : "text-white/90 group-hover:text-gold"
}`}> }`}>
{trainer} {trainer}
</span> </span>
@@ -2,6 +2,7 @@
import { User, X, MapPin } from "lucide-react"; import { User, X, MapPin } from "lucide-react";
import { shortAddress } from "./constants"; import { shortAddress } from "./constants";
import { ScheduleBadge } from "@/components/ui/ScheduleBadge";
import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants"; import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants";
interface MobileScheduleProps { interface MobileScheduleProps {
@@ -46,31 +47,17 @@ function ClassRow({
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<button <button
onClick={() => toggleFilterTrainer(cls.trainer)} onClick={() => window.dispatchEvent(new CustomEvent("openTrainerProfile", { detail: cls.trainer }))}
className="truncate text-sm font-medium text-left active:opacity-60 text-neutral-800 dark:text-white/80" className="truncate text-sm font-medium text-left active:opacity-60 text-neutral-800 dark:text-white/80 hover:text-gold transition-colors"
> >
{cls.trainer} {cls.trainer}
</button> </button>
{cls.hasSlots && ( {cls.hasSlots && <ScheduleBadge size="sm">места</ScheduleBadge>}
<span className="shrink-0 rounded-full bg-emerald-500/15 border border-emerald-500/25 px-1.5 py-px text-[9px] font-semibold text-emerald-600 dark:text-emerald-400"> {cls.recruiting && <ScheduleBadge size="sm">набор</ScheduleBadge>}
места
</span>
)}
{cls.recruiting && (
<span className="shrink-0 rounded-full bg-sky-500/15 border border-sky-500/25 px-1.5 py-px text-[9px] font-semibold text-sky-600 dark:text-sky-400">
набор
</span>
)}
{cls.status && cls.status !== "hasSlots" && cls.status !== "recruiting" && ( {cls.status && cls.status !== "hasSlots" && cls.status !== "recruiting" && (
<span className="shrink-0 rounded-full bg-gold/15 border border-gold/25 px-1.5 py-px text-[9px] font-semibold text-gold"> <ScheduleBadge size="sm">{cls.status}</ScheduleBadge>
{cls.status}
</span>
)}
{cls.level && (
<span className="shrink-0 rounded-full bg-rose-500/15 border border-rose-500/25 px-1.5 py-px text-[9px] font-semibold text-rose-600 dark:text-rose-400">
{cls.level}
</span>
)} )}
{cls.level && <ScheduleBadge size="sm">{cls.level}</ScheduleBadge>}
</div> </div>
<div className="mt-0.5 flex items-center gap-2"> <div className="mt-0.5 flex items-center gap-2">
<button <button
@@ -125,7 +125,7 @@ export function ScheduleFilters({
</div> </div>
{/* Scrollable content */} {/* Scrollable content */}
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-5"> <div className="flex-1 overflow-y-auto overflow-x-hidden 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">
@@ -158,135 +158,100 @@ export function ScheduleFilters({
{/* Status — gold tags */} {/* Status — gold tags */}
{availableStatuses.length > 0 && ( {availableStatuses.length > 0 && (
<FilterSection title="Статус"> <FilterSection title="Статус">
{/* Desktop — card layout */} <div className="flex gap-2">
<div className="hidden sm:block space-y-2">
{availableStatuses.map((statusKey) => { {availableStatuses.map((statusKey) => {
const cfg = findStatusConfig(scheduleConfig?.statuses, statusKey); const cfg = findStatusConfig(scheduleConfig?.statuses, 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 (
<button <div key={statusKey} className="flex-1 relative">
key={statusKey} <button
onClick={() => toggleFilterStatus(statusKey)} onClick={() => toggleFilterStatus(statusKey)}
className={`flex items-center gap-3 w-full rounded-xl p-3 border-l-[3px] transition-all cursor-pointer ${ className={`w-full rounded-xl px-3 py-3 text-center text-xs font-semibold transition-all cursor-pointer border ${
active active
? "border-l-gold bg-gold/10" ? "border-gold bg-gold/10 text-white"
: "border-l-white/10 bg-white/[0.02] hover:bg-white/[0.05]" : "border-white/[0.08] bg-white/[0.02] text-neutral-400 hover:border-white/[0.15] hover:bg-white/[0.04]"
}`} }`}
> >
<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}
</span> </button>
{desc && ( {desc && (
<span className="group relative ml-auto" onClick={(e) => e.stopPropagation()}> <span className="absolute -top-1.5 -right-1.5 z-10">
<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> <InfoTip text={desc} />
<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>
);
})}
</div>
{/* Mobile — compact pills */}
<div className="flex flex-wrap gap-2 sm:hidden">
{availableStatuses.map((statusKey) => {
const cfg = findStatusConfig(scheduleConfig?.statuses, 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 — radio cards */} {/* Level — radio list */}
{levels.length > 0 && ( {levels.length > 0 && (
<FilterSection title="Опыт"> <FilterSection title="Опыт">
<div className="flex flex-wrap gap-2"> <div className="space-y-1">
{levels.map((level) => { {levels.map((level) => {
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 inline-flex items-center gap-1"> <button
<button key={level}
onClick={() => setFilterLevel(active ? null : level)} onClick={() => setFilterLevel(active ? null : level)}
className={`rounded-xl px-4 py-2 text-xs font-semibold transition-all cursor-pointer border ${ className={`flex items-center gap-2.5 w-full rounded-lg px-3 py-2 transition-all cursor-pointer ${
active active
? "border-gold bg-gold/10 text-white" ? "bg-gold/10"
: "border-white/[0.06] bg-white/[0.02] text-neutral-400 hover:border-white/[0.15] hover:bg-white/[0.04]" : "hover:bg-white/[0.03]"
}`} }`}
> >
<span className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 transition-colors ${
active ? "border-gold" : "border-white/20"
}`}>
{active && <span className="h-2 w-2 rounded-full bg-gold" />}
</span>
<span className={`text-xs font-medium ${active ? "text-white" : "text-neutral-400"}`}>
{level} {level}
</button> </span>
{desc && ( {desc && <InfoTip text={desc} />}
<span className="group relative"> </button>
<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>
); );
})} })}
</div> </div>
</FilterSection> </FilterSection>
)} )}
{/* When — days + time combined */} {/* When — days + time inline */}
<FilterSection title="Когда"> <FilterSection title="Когда">
<div className="flex flex-wrap gap-1.5"> <div className="flex items-center gap-2">
{availableDays.map(({ day, dayShort }) => ( <div className="flex gap-1">
<button {availableDays.map(({ day, dayShort }) => (
key={day} <button
onClick={() => toggleDay(day)} key={day}
className={`flex items-center justify-center rounded-lg w-9 h-9 text-[11px] font-semibold transition-all cursor-pointer ${ onClick={() => toggleDay(day)}
filterDaySet.has(day) className={`flex items-center justify-center rounded-md w-8 h-8 text-[10px] font-semibold transition-all cursor-pointer ${
? "bg-gold text-black" filterDaySet.has(day)
: "bg-white/[0.04] text-neutral-400 hover:bg-white/[0.08] hover:text-white" ? "bg-gold text-black"
}`} : "bg-white/[0.04] text-neutral-400 hover:bg-white/[0.08] hover:text-white"
> }`}
{dayShort} >
</button> {dayShort}
))} </button>
</div> ))}
<div className="flex items-center gap-2 mt-3"> </div>
<span className="h-6 w-px bg-white/[0.08] shrink-0" />
<input <input
type="time" type="time"
value={filterTime.from} value={filterTime.from}
onChange={(e) => setFilterTime({ ...filterTime, from: e.target.value })} onChange={(e) => setFilterTime({ ...filterTime, from: e.target.value })}
placeholder="С" 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]"
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> <span className="text-neutral-500 text-[10px]"></span>
<input <input
type="time" type="time"
value={filterTime.to} value={filterTime.to}
onChange={(e) => setFilterTime({ ...filterTime, to: e.target.value })} onChange={(e) => setFilterTime({ ...filterTime, to: e.target.value })}
placeholder="До" 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]"
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>
</FilterSection> </FilterSection>
@@ -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<HTMLSpanElement>(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 (
<span ref={ref} className="relative" onClick={(e) => e.stopPropagation()}>
<button
type="button"
onClick={() => setOpen(!open)}
className={`flex h-4 w-4 items-center justify-center rounded-full border text-[10px] transition-colors cursor-pointer ${
open ? "border-gold/40 text-gold" : "border-white/15 text-neutral-500 hover:text-white hover:border-white/30"
}`}
>
?
</button>
{open && (
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 z-20 w-52">
{/* Tail behind body — always centered */}
<div className="absolute left-1/2 -translate-x-1/2 -bottom-[5px] w-2.5 h-2.5 rotate-45 bg-gold" />
{/* Body on top */}
<div className="relative z-10 rounded-lg bg-gold px-3 py-2 text-[11px] leading-relaxed text-black shadow-lg">
{text}
</div>
</div>
)}
</span>
);
}
function TrainerMultiSelect({ function TrainerMultiSelect({
trainers, trainers,
selected, selected,
+7 -1
View File
@@ -30,7 +30,7 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) {
// Extract trainer's groups from schedule using groupId // Extract trainer's groups from schedule using groupId
const uniqueGroups = useMemo(() => { const uniqueGroups = useMemo(() => {
const groupMap = new Map<string, { type: string; location: string; address: string; slots: { day: string; dayShort: string; time: string }[]; level?: string; recruiting?: boolean }>(); const groupMap = new Map<string, { type: string; location: string; address: string; slots: { day: string; dayShort: string; time: string }[]; level?: string; recruiting?: boolean; hasSlots?: boolean; status?: string }>();
schedule?.forEach(location => { schedule?.forEach(location => {
location.days.forEach(day => { location.days.forEach(day => {
day.classes 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 }); existing.slots.push({ day: day.day, dayShort: day.dayShort, time: c.time });
if (c.level && !existing.level) existing.level = c.level; if (c.level && !existing.level) existing.level = c.level;
if (c.recruiting) existing.recruiting = true; if (c.recruiting) existing.recruiting = true;
if (c.hasSlots) existing.hasSlots = true;
if (c.status && !existing.status) existing.status = c.status;
} else { } else {
groupMap.set(key, { groupMap.set(key, {
type: c.type, type: c.type,
@@ -52,6 +54,8 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) {
slots: [{ day: day.day, dayShort: day.dayShort, time: c.time }], slots: [{ day: day.day, dayShort: day.dayShort, time: c.time }],
level: c.level, level: c.level,
recruiting: c.recruiting, recruiting: c.recruiting,
hasSlots: c.hasSlots,
status: c.status,
}); });
} }
}); });
@@ -183,6 +187,8 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) {
type={g.type} type={g.type}
level={g.level} level={g.level}
recruiting={g.recruiting} recruiting={g.recruiting}
hasSlots={g.hasSlots}
status={g.status}
address={g.address} address={g.address}
location={g.location} location={g.location}
merged={g.merged} merged={g.merged}
+11 -23
View File
@@ -1,5 +1,6 @@
import { MapPin } from "lucide-react"; import { MapPin } from "lucide-react";
import { shortAddress } from "@/components/sections/schedule/constants"; import { shortAddress } from "@/components/sections/schedule/constants";
import { ScheduleBadge } from "@/components/ui/ScheduleBadge";
export interface GroupCardSlot { export interface GroupCardSlot {
days: string[]; days: string[];
@@ -50,14 +51,11 @@ export function GroupCard({
const typeCls = compact ? "text-xs" : "text-sm"; 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 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 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 locSize = compact ? "px-2 py-0.5 text-[9px]" : "px-2.5 py-0.5 text-[10px]";
const locIcon = compact ? 8 : 9; const locIcon = compact ? 8 : 9;
const levelBadge = level ? ( const levelBadge = level ? (
<span className={`shrink-0 rounded-full bg-rose-500/15 border border-rose-500/25 ${badgeSize} font-semibold text-rose-400`}> <ScheduleBadge size={compact ? "sm" : "md"}>{level}</ScheduleBadge>
{level}
</span>
) : null; ) : null;
const typeContent = ( const typeContent = (
@@ -69,7 +67,7 @@ export function GroupCard({
); );
return ( return (
<div className="space-y-1.5"> <div className="flex flex-col flex-1 gap-1.5">
{/* Type + level + status badges + extras */} {/* Type + level + status badges + extras */}
<div className="flex items-center gap-1.5 flex-wrap"> <div className="flex items-center gap-1.5 flex-wrap">
{onTypeClick ? ( {onTypeClick ? (
@@ -79,27 +77,17 @@ export function GroupCard({
) : ( ) : (
<span className="inline-flex items-center gap-1.5">{typeContent}</span> <span className="inline-flex items-center gap-1.5">{typeContent}</span>
)} )}
{hasSlots && (
<span className={`rounded-full bg-emerald-500/15 border border-emerald-500/25 ${badgeSize} font-semibold text-emerald-400`}>
есть места
</span>
)}
{recruiting && (
<span className={`rounded-full bg-sky-500/15 border border-sky-500/25 ${badgeSize} font-semibold text-sky-400`}>
набор
</span>
)}
{status && status !== "hasSlots" && status !== "recruiting" && (
<span className={`rounded-full bg-gold/15 border border-gold/25 ${badgeSize} font-semibold text-gold`}>
{statusLabel || status}
</span>
)}
{showLocation && (address || location) && ( {showLocation && (address || location) && (
<span className={`inline-flex items-center gap-1 rounded-full bg-white/[0.04] border border-white/[0.08] ${locSize} font-medium text-white/40`}> <span className={`inline-flex items-center gap-1 rounded-full bg-gold/15 border border-gold/25 ${locSize} font-medium text-white`}>
<MapPin size={locIcon} /> <MapPin size={locIcon} className="text-gold" />
{shortAddress(address || location || "")} {shortAddress(address || location || "")}
</span> </span>
)} )}
{hasSlots && <ScheduleBadge size={compact ? "sm" : "md"}>есть места</ScheduleBadge>}
{recruiting && <ScheduleBadge size={compact ? "sm" : "md"}>набор</ScheduleBadge>}
{status && status !== "hasSlots" && status !== "recruiting" && (
<ScheduleBadge size={compact ? "sm" : "md"}>{statusLabel || status}</ScheduleBadge>
)}
{extraBadges} {extraBadges}
</div> </div>
@@ -122,7 +110,7 @@ export function GroupCard({
compact ? ( compact ? (
<button <button
onClick={onBook} onClick={onBook}
className="w-full rounded-lg bg-gold/15 border border-gold/25 py-1.5 text-[11px] font-semibold text-gold hover:bg-gold/25 transition-colors cursor-pointer" className="w-full mt-auto rounded-lg bg-gold/15 border border-gold/25 py-1.5 text-[11px] font-semibold text-gold hover:bg-gold/25 transition-colors cursor-pointer"
> >
Записаться Записаться
</button> </button>
+20
View File
@@ -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 (
<span className={`shrink-0 rounded-full bg-gold/15 border border-gold/25 ${sizeClass} font-semibold text-gold`}>
{children}
</span>
);
}