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:
@@ -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({
|
||||
<Clock size={13} />
|
||||
<span className="font-semibold">{cls.time}</span>
|
||||
</div>
|
||||
{cls.hasSlots && (
|
||||
<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">
|
||||
есть места
|
||||
</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.hasSlots && <ScheduleBadge>есть места</ScheduleBadge>}
|
||||
{cls.recruiting && <ScheduleBadge>набор</ScheduleBadge>}
|
||||
{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">
|
||||
{cls.status}
|
||||
</span>
|
||||
<ScheduleBadge>{cls.status}</ScheduleBadge>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => toggleFilterTrainer(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"
|
||||
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 hover:text-gold transition-colors"
|
||||
>
|
||||
<User size={13} className="shrink-0 text-neutral-400 dark:text-white/30" />
|
||||
{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="text-xs text-neutral-500 dark:text-white/40">{cls.type}</span>
|
||||
</button>
|
||||
{cls.level && (
|
||||
<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>
|
||||
)}
|
||||
{cls.level && <ScheduleBadge>{cls.level}</ScheduleBadge>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -111,11 +98,13 @@ export function DayCard({ day, typeDots, showLocation, filterTrainerSet, toggleF
|
||||
{locationGroups.map(([locName, { address, classes }], gi) => (
|
||||
<div key={locName}>
|
||||
{/* 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]" : ""}`}>
|
||||
<MapPin size={11} className="shrink-0 text-gold/60" />
|
||||
<span className="text-[11px] font-medium text-neutral-600 dark:text-white/50">
|
||||
<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" />
|
||||
<span className="text-[11px] font-medium text-white">
|
||||
{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>
|
||||
</div>
|
||||
<div className="divide-y divide-neutral-100 dark:divide-white/[0.04]">
|
||||
|
||||
@@ -195,13 +195,13 @@ export function GroupView({
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
{/* Name — clicks to filter */}
|
||||
{/* Name — clicks to open bio */}
|
||||
<button
|
||||
onClick={() => toggleFilterTrainer(trainer)}
|
||||
onClick={() => window.dispatchEvent(new CustomEvent("openTrainerProfile", { detail: trainer }))}
|
||||
className="cursor-pointer group"
|
||||
>
|
||||
<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}
|
||||
</span>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { User, X, MapPin } from "lucide-react";
|
||||
import { shortAddress } from "./constants";
|
||||
import { ScheduleBadge } from "@/components/ui/ScheduleBadge";
|
||||
import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants";
|
||||
|
||||
interface MobileScheduleProps {
|
||||
@@ -46,31 +47,17 @@ function ClassRow({
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => toggleFilterTrainer(cls.trainer)}
|
||||
className="truncate text-sm font-medium text-left active:opacity-60 text-neutral-800 dark:text-white/80"
|
||||
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 hover:text-gold transition-colors"
|
||||
>
|
||||
{cls.trainer}
|
||||
</button>
|
||||
{cls.hasSlots && (
|
||||
<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">
|
||||
места
|
||||
</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.hasSlots && <ScheduleBadge size="sm">места</ScheduleBadge>}
|
||||
{cls.recruiting && <ScheduleBadge size="sm">набор</ScheduleBadge>}
|
||||
{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">
|
||||
{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>
|
||||
<ScheduleBadge size="sm">{cls.status}</ScheduleBadge>
|
||||
)}
|
||||
{cls.level && <ScheduleBadge size="sm">{cls.level}</ScheduleBadge>}
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-2">
|
||||
<button
|
||||
|
||||
@@ -125,7 +125,7 @@ export function ScheduleFilters({
|
||||
</div>
|
||||
|
||||
{/* 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 */}
|
||||
<FilterSection title="Направления">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -158,135 +158,100 @@ export function ScheduleFilters({
|
||||
{/* Status — gold tags */}
|
||||
{availableStatuses.length > 0 && (
|
||||
<FilterSection title="Статус">
|
||||
{/* Desktop — card layout */}
|
||||
<div className="hidden sm:block space-y-2">
|
||||
<div className="flex gap-2">
|
||||
{availableStatuses.map((statusKey) => {
|
||||
const cfg = findStatusConfig(scheduleConfig?.statuses, statusKey);
|
||||
const label = cfg?.label || statusKey;
|
||||
const desc = cfg?.description;
|
||||
const active = filterStatusSet.has(statusKey);
|
||||
return (
|
||||
<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"}`}>
|
||||
<div key={statusKey} className="flex-1 relative">
|
||||
<button
|
||||
onClick={() => toggleFilterStatus(statusKey)}
|
||||
className={`w-full rounded-xl px-3 py-3 text-center text-xs font-semibold transition-all cursor-pointer border ${
|
||||
active
|
||||
? "border-gold bg-gold/10 text-white"
|
||||
: "border-white/[0.08] bg-white/[0.02] text-neutral-400 hover:border-white/[0.15] hover:bg-white/[0.04]"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
{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 className="absolute -top-1.5 -right-1.5 z-10">
|
||||
<InfoTip text={desc} />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Level — radio cards */}
|
||||
{/* Level — radio list */}
|
||||
{levels.length > 0 && (
|
||||
<FilterSection title="Опыт">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="space-y-1">
|
||||
{levels.map((level) => {
|
||||
const desc = scheduleConfig?.levels?.find((l) => l.value === level)?.description;
|
||||
const active = filterLevel === level;
|
||||
return (
|
||||
<span key={level} className="relative inline-flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setFilterLevel(active ? null : level)}
|
||||
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] hover:bg-white/[0.04]"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
key={level}
|
||||
onClick={() => setFilterLevel(active ? null : level)}
|
||||
className={`flex items-center gap-2.5 w-full rounded-lg px-3 py-2 transition-all cursor-pointer ${
|
||||
active
|
||||
? "bg-gold/10"
|
||||
: "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}
|
||||
</button>
|
||||
{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>
|
||||
</span>
|
||||
{desc && <InfoTip text={desc} />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FilterSection>
|
||||
)}
|
||||
|
||||
{/* When — days + time combined */}
|
||||
{/* When — days + time inline */}
|
||||
<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 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"
|
||||
}`}
|
||||
>
|
||||
{dayShort}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1">
|
||||
{availableDays.map(({ day, dayShort }) => (
|
||||
<button
|
||||
key={day}
|
||||
onClick={() => toggleDay(day)}
|
||||
className={`flex items-center justify-center rounded-md w-8 h-8 text-[10px] 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"
|
||||
}`}
|
||||
>
|
||||
{dayShort}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="h-6 w-px bg-white/[0.08] shrink-0" />
|
||||
<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]"
|
||||
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]"
|
||||
/>
|
||||
<span className="text-neutral-500 text-xs">—</span>
|
||||
<span className="text-neutral-500 text-[10px]">—</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]"
|
||||
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]"
|
||||
/>
|
||||
</div>
|
||||
</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({
|
||||
trainers,
|
||||
selected,
|
||||
|
||||
@@ -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<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 => {
|
||||
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}
|
||||
|
||||
@@ -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 ? (
|
||||
<span className={`shrink-0 rounded-full bg-rose-500/15 border border-rose-500/25 ${badgeSize} font-semibold text-rose-400`}>
|
||||
{level}
|
||||
</span>
|
||||
<ScheduleBadge size={compact ? "sm" : "md"}>{level}</ScheduleBadge>
|
||||
) : null;
|
||||
|
||||
const typeContent = (
|
||||
@@ -69,7 +67,7 @@ export function GroupCard({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex flex-col flex-1 gap-1.5">
|
||||
{/* Type + level + status badges + extras */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{onTypeClick ? (
|
||||
@@ -79,27 +77,17 @@ export function GroupCard({
|
||||
) : (
|
||||
<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) && (
|
||||
<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`}>
|
||||
<MapPin size={locIcon} />
|
||||
<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} className="text-gold" />
|
||||
{shortAddress(address || location || "")}
|
||||
</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}
|
||||
</div>
|
||||
|
||||
@@ -122,7 +110,7 @@ export function GroupCard({
|
||||
compact ? (
|
||||
<button
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user