Files
blackheart-website/src/components/ui/MasterClassSignupModal.tsx
diana.dolgolyova 84b0bc4d60 feat: add master classes section with registration system
- New master classes section on landing page with upcoming events grid
- Admin CRUD for master classes (image, slots, trainer, style, cost, location)
- User signup modal (name + Instagram required, Telegram optional)
- Admin registration management: view, add, edit, delete with quick-contact links
- Customizable success message for signup confirmation
- Auto-filter past events, Russian date formatting, duration auto-calculation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:29:06 +03:00

196 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useCallback } from "react";
import { createPortal } from "react-dom";
import { X, Instagram, Send, CheckCircle } from "lucide-react";
interface MasterClassSignupModalProps {
open: boolean;
onClose: () => void;
masterClassTitle: string;
successMessage?: string;
}
export function MasterClassSignupModal({
open,
onClose,
masterClassTitle,
successMessage,
}: MasterClassSignupModalProps) {
const [name, setName] = useState("");
const [instagram, setInstagram] = useState("");
const [telegram, setTelegram] = useState("");
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState("");
// Close on Escape
useEffect(() => {
if (!open) return;
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [open, onClose]);
// Lock body scroll
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [open]);
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setSubmitting(true);
try {
const res = await fetch("/api/master-class-register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
masterClassTitle,
name: name.trim(),
instagram: `@${instagram.trim()}`,
telegram: telegram.trim() ? `@${telegram.trim()}` : undefined,
}),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Ошибка регистрации");
}
setSubmitted(true);
} catch (err) {
setError(err instanceof Error ? err.message : "Ошибка регистрации");
} finally {
setSubmitting(false);
}
},
[masterClassTitle, name, instagram, telegram]
);
const handleClose = useCallback(() => {
onClose();
setTimeout(() => {
setName("");
setInstagram("");
setTelegram("");
setSubmitted(false);
setError("");
}, 300);
}, [onClose]);
if (!open) return null;
return createPortal(
<div
className="modal-overlay fixed inset-0 z-50 flex items-center justify-center p-4"
onClick={handleClose}
>
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" />
<div
className="modal-content relative w-full max-w-md rounded-2xl border border-white/[0.08] bg-[#0a0a0a] p-6 sm:p-8 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={handleClose}
className="absolute right-4 top-4 flex h-8 w-8 items-center justify-center rounded-full text-neutral-500 transition-colors hover:bg-white/[0.06] hover:text-white cursor-pointer"
>
<X size={18} />
</button>
{submitted ? (
<div className="py-4 text-center">
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-emerald-500/10">
<CheckCircle size={28} className="text-emerald-500" />
</div>
<h3 className="text-lg font-bold text-white">Отлично!</h3>
<p className="mt-2 text-sm text-neutral-400">
{successMessage || "Вы записаны! Мы свяжемся с вами"}
</p>
<button
onClick={handleClose}
className="mt-6 rounded-full bg-gold px-6 py-2.5 text-sm font-semibold text-black transition-all hover:bg-gold-light cursor-pointer"
>
Закрыть
</button>
</div>
) : (
<>
<div className="mb-6">
<h3 className="text-xl font-bold text-white">Записаться</h3>
<p className="mt-1 text-sm text-neutral-400">{masterClassTitle}</p>
</div>
<form onSubmit={handleSubmit} className="space-y-3">
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Ваше имя"
required
className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-gold/40 focus:bg-white/[0.06]"
/>
</div>
<div className="flex items-center gap-0 rounded-xl border border-white/[0.08] bg-white/[0.04] transition-colors focus-within:border-gold/40 focus-within:bg-white/[0.06]">
<span className="flex items-center gap-1.5 pl-4 text-sm text-neutral-500 select-none">
<Instagram size={14} className="text-pink-400" />
@
</span>
<input
type="text"
value={instagram}
onChange={(e) => setInstagram(e.target.value.replace(/^@/, ""))}
placeholder="username"
required
className="flex-1 bg-transparent px-2 py-3 text-sm text-white placeholder-neutral-500 outline-none"
/>
</div>
<div className="flex items-center gap-0 rounded-xl border border-white/[0.08] bg-white/[0.04] transition-colors focus-within:border-gold/40 focus-within:bg-white/[0.06]">
<span className="flex items-center gap-1.5 pl-4 text-sm text-neutral-500 select-none">
<Send size={14} className="text-blue-400" />
@
</span>
<input
type="text"
value={telegram}
onChange={(e) => setTelegram(e.target.value.replace(/^@/, ""))}
placeholder="username (необязательно)"
className="flex-1 bg-transparent px-2 py-3 text-sm text-white placeholder-neutral-500 outline-none"
/>
</div>
{error && (
<p className="text-sm text-red-400">{error}</p>
)}
<button
type="submit"
disabled={submitting}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-gold py-3 text-sm font-semibold text-black transition-all hover:bg-gold-light hover:shadow-lg hover:shadow-gold/20 cursor-pointer disabled:opacity-50"
>
{submitting ? "Отправка..." : "Записаться"}
</button>
</form>
</>
)}
</div>
</div>,
document.body
);
}