feat: shared GroupCard component, admin status select, schedule level filter
- Extract shared GroupCard component used by both Schedule GroupView and TeamProfile - Admin schedule: replace hasSlots/recruiting toggles with single Status select - User schedule: add level filter pills (Начинающий/Без опыта, Продвинутый) - Consistent group card styling across schedule and trainer bio views
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { MapPin } from "lucide-react";
|
||||
import { shortAddress } from "@/components/sections/schedule/constants";
|
||||
|
||||
export interface GroupCardSlot {
|
||||
days: string[];
|
||||
times: string[];
|
||||
}
|
||||
|
||||
export interface GroupCardProps {
|
||||
type: string;
|
||||
level?: string;
|
||||
recruiting?: boolean;
|
||||
hasSlots?: boolean;
|
||||
address?: string;
|
||||
location?: string;
|
||||
merged: GroupCardSlot[];
|
||||
dotColor?: string;
|
||||
/** Compact mode for small cards (e.g. trainer bio scroll row) */
|
||||
compact?: boolean;
|
||||
/** Show location badge */
|
||||
showLocation?: boolean;
|
||||
/** Extra badges (e.g. "Сегодня") rendered after level badge */
|
||||
extraBadges?: React.ReactNode;
|
||||
/** Click handler for type name (e.g. filter toggle in schedule) */
|
||||
onTypeClick?: () => void;
|
||||
/** Click handler for book button */
|
||||
onBook?: () => void;
|
||||
}
|
||||
|
||||
export function GroupCard({
|
||||
type,
|
||||
level,
|
||||
recruiting,
|
||||
hasSlots,
|
||||
address,
|
||||
location,
|
||||
merged,
|
||||
dotColor = "bg-gold",
|
||||
compact = false,
|
||||
showLocation = true,
|
||||
extraBadges,
|
||||
onTypeClick,
|
||||
onBook,
|
||||
}: GroupCardProps) {
|
||||
const dot = compact ? "h-2 w-2" : "h-2.5 w-2.5";
|
||||
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>
|
||||
) : null;
|
||||
|
||||
const typeContent = (
|
||||
<>
|
||||
<span className={`${dot} shrink-0 rounded-full ${dotColor}`} />
|
||||
<span className={`${typeCls} font-semibold text-white/90`}>{type}</span>
|
||||
{levelBadge}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
{/* Type + level + status badges + extras */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{onTypeClick ? (
|
||||
<button onClick={onTypeClick} className="flex items-center gap-1.5 cursor-pointer">
|
||||
{typeContent}
|
||||
</button>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
{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} />
|
||||
{shortAddress(address || location || "")}
|
||||
</span>
|
||||
)}
|
||||
{extraBadges}
|
||||
</div>
|
||||
|
||||
{/* Schedule rows */}
|
||||
<div className={compact ? "space-y-0.5" : "space-y-1"}>
|
||||
{merged.map((m, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5">
|
||||
<span className={`rounded-md bg-gold/10 ${dayPad} font-bold text-gold text-center`}>
|
||||
{m.days.join(", ")}
|
||||
</span>
|
||||
<span className={`${timeCls} tabular-nums text-white/60`}>
|
||||
{m.times.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Book button */}
|
||||
{onBook && (
|
||||
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"
|
||||
>
|
||||
Записаться
|
||||
</button>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user