feat: min/max participants — shared ParticipantLimits component

- New ParticipantLimits component in FormField.tsx (reusable)
- Used in both Open Day settings and MC editor — identical layout
- Open Day: event-level min/max (DB migration 15)
- MC: per-event min/max (JSON fields)
- Public: waiting list when full, spots counter, amber success modal
This commit is contained in:
2026-03-24 22:11:10 +03:00
parent 4acc88c1ab
commit d08905ee93
13 changed files with 484 additions and 50 deletions

View File

@@ -10,6 +10,7 @@ import type { SiteContent, MasterClassItem, MasterClassSlot } from "@/types";
interface MasterClassesProps {
data: SiteContent["masterClasses"];
regCounts?: Record<string, number>;
}
const MONTHS_RU = [
@@ -88,13 +89,17 @@ function isUpcoming(item: MasterClassItem): boolean {
function MasterClassCard({
item,
currentRegs,
onSignup,
}: {
item: MasterClassItem;
currentRegs: number;
onSignup: () => void;
}) {
const duration = item.slots[0] ? calcDuration(item.slots[0]) : "";
const slotsDisplay = formatSlots(item.slots);
const maxP = item.maxParticipants ?? 0;
const isFull = maxP > 0 && currentRegs >= maxP;
return (
<div className="group relative flex w-full max-w-sm flex-col overflow-hidden rounded-2xl bg-black">
@@ -160,13 +165,33 @@ function MasterClassCard({
)}
</div>
{/* Spots info */}
{(maxP > 0 || (item.minParticipants && item.minParticipants > 0)) && (
<div className="mb-3 flex items-center gap-3 text-[11px]">
{maxP > 0 && (
<span className={isFull ? "text-amber-400" : "text-white/40"}>
{currentRegs}/{maxP} мест
</span>
)}
{item.minParticipants && item.minParticipants > 0 && currentRegs < item.minParticipants && (
<span className="text-red-400/70">
мин. {item.minParticipants} для проведения
</span>
)}
</div>
)}
{/* Price + Actions */}
<div className="flex items-center gap-3">
<button
onClick={onSignup}
className="flex-1 rounded-xl bg-gold py-3 text-sm font-bold text-black uppercase tracking-wide transition-all hover:bg-gold-light hover:shadow-lg hover:shadow-gold/25 cursor-pointer"
className={`flex-1 rounded-xl py-3 text-sm font-bold uppercase tracking-wide transition-all cursor-pointer ${
isFull
? "bg-amber-500/15 text-amber-400 hover:bg-amber-500/25"
: "bg-gold text-black hover:bg-gold-light hover:shadow-lg hover:shadow-gold/25"
}`}
>
Записаться
{isFull ? "Лист ожидания" : "Записаться"}
</button>
{item.instagramUrl && (
<button
@@ -192,7 +217,7 @@ function MasterClassCard({
);
}
export function MasterClasses({ data }: MasterClassesProps) {
export function MasterClasses({ data, regCounts = {} }: MasterClassesProps) {
const [signupTitle, setSignupTitle] = useState<string | null>(null);
const upcoming = useMemo(() => {
@@ -238,6 +263,7 @@ export function MasterClasses({ data }: MasterClassesProps) {
<MasterClassCard
key={item.title}
item={item}
currentRegs={regCounts[item.title] ?? 0}
onSignup={() => setSignupTitle(item.title)}
/>
))}