feat: add Записаться button to group cards with pre-filled Instagram DM
- BookingModal now accepts optional groupInfo for pre-filled message - Trainer profile: each group card has Записаться button - Schedule group view: each group card has Записаться button - Message includes class type, trainer, days, and time Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from "react";
|
||||||
|
import { BookingModal } from "@/components/ui/BookingModal";
|
||||||
import { CalendarDays, Users, LayoutGrid } from "lucide-react";
|
import { CalendarDays, Users, LayoutGrid } from "lucide-react";
|
||||||
import { SectionHeading } from "@/components/ui/SectionHeading";
|
import { SectionHeading } from "@/components/ui/SectionHeading";
|
||||||
import { Reveal } from "@/components/ui/Reveal";
|
import { Reveal } from "@/components/ui/Reveal";
|
||||||
@@ -28,6 +29,7 @@ export function Schedule({ data: schedule, classItems }: ScheduleProps) {
|
|||||||
const [filterStatus, setFilterStatus] = useState<StatusFilter>("all");
|
const [filterStatus, setFilterStatus] = useState<StatusFilter>("all");
|
||||||
const [filterTime, setFilterTime] = useState<TimeFilter>("all");
|
const [filterTime, setFilterTime] = useState<TimeFilter>("all");
|
||||||
const [filterDaySet, setFilterDaySet] = useState<Set<string>>(new Set());
|
const [filterDaySet, setFilterDaySet] = useState<Set<string>>(new Set());
|
||||||
|
const [bookingGroup, setBookingGroup] = useState<string | null>(null);
|
||||||
|
|
||||||
const isAllMode = locationMode === "all";
|
const isAllMode = locationMode === "all";
|
||||||
|
|
||||||
@@ -329,9 +331,15 @@ export function Schedule({ data: schedule, classItems }: ScheduleProps) {
|
|||||||
filterTrainer={filterTrainer}
|
filterTrainer={filterTrainer}
|
||||||
setFilterTrainer={setFilterTrainerFromCard}
|
setFilterTrainer={setFilterTrainerFromCard}
|
||||||
showLocation={isAllMode}
|
showLocation={isAllMode}
|
||||||
|
onBook={setBookingGroup}
|
||||||
/>
|
/>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
)}
|
)}
|
||||||
|
<BookingModal
|
||||||
|
open={bookingGroup !== null}
|
||||||
|
onClose={() => setBookingGroup(null)}
|
||||||
|
groupInfo={bookingGroup ?? undefined}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ interface GroupViewProps {
|
|||||||
filterTrainer: string | null;
|
filterTrainer: string | null;
|
||||||
setFilterTrainer: (trainer: string | null) => void;
|
setFilterTrainer: (trainer: string | null) => void;
|
||||||
showLocation?: boolean;
|
showLocation?: boolean;
|
||||||
|
onBook?: (groupInfo: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupView({
|
export function GroupView({
|
||||||
@@ -65,6 +66,7 @@ export function GroupView({
|
|||||||
filterTrainer,
|
filterTrainer,
|
||||||
setFilterTrainer,
|
setFilterTrainer,
|
||||||
showLocation,
|
showLocation,
|
||||||
|
onBook,
|
||||||
}: GroupViewProps) {
|
}: GroupViewProps) {
|
||||||
const groups = buildGroups(filteredDays);
|
const groups = buildGroups(filteredDays);
|
||||||
|
|
||||||
@@ -177,6 +179,14 @@ export function GroupView({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{onBook && (
|
||||||
|
<button
|
||||||
|
onClick={() => onBook(`${group.type}, ${group.trainer}, ${group.slots.map(s => s.dayShort).join("/")} ${group.slots[0]?.time ?? ""}`)}
|
||||||
|
className="w-full mt-3 rounded-xl bg-gold/15 border border-gold/25 py-2 text-xs font-semibold text-gold-dark dark:text-gold hover:bg-gold/25 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Записаться
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from "react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { ArrowLeft, Instagram, Trophy, GraduationCap, ExternalLink, X, Award, Scale, Clock, MapPin } from "lucide-react";
|
import { ArrowLeft, Instagram, Trophy, GraduationCap, ExternalLink, X, Award, Scale, Clock, MapPin } from "lucide-react";
|
||||||
import type { TeamMember, RichListItem, VictoryItem, ScheduleLocation } from "@/types/content";
|
import type { TeamMember, RichListItem, VictoryItem, ScheduleLocation } from "@/types/content";
|
||||||
|
import { BookingModal } from "@/components/ui/BookingModal";
|
||||||
|
|
||||||
interface TeamProfileProps {
|
interface TeamProfileProps {
|
||||||
member: TeamMember;
|
member: TeamMember;
|
||||||
@@ -11,6 +12,7 @@ interface TeamProfileProps {
|
|||||||
|
|
||||||
export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) {
|
export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) {
|
||||||
const [lightbox, setLightbox] = useState<string | null>(null);
|
const [lightbox, setLightbox] = useState<string | null>(null);
|
||||||
|
const [bookingGroup, setBookingGroup] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
@@ -177,7 +179,7 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) {
|
|||||||
</span>
|
</span>
|
||||||
<ScrollRow>
|
<ScrollRow>
|
||||||
{uniqueGroups.map((g, i) => (
|
{uniqueGroups.map((g, i) => (
|
||||||
<div key={i} className="w-44 shrink-0 rounded-xl border border-white/[0.08] bg-white/[0.03] p-3 space-y-1.5">
|
<div key={i} className="w-48 shrink-0 rounded-xl border border-white/[0.08] bg-white/[0.03] p-3 space-y-1.5">
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-white/80">{g.type}</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-white/80">{g.type}</p>
|
||||||
<div className="flex items-center gap-1.5 text-xs text-white/50">
|
<div className="flex items-center gap-1.5 text-xs text-white/50">
|
||||||
<Clock size={11} />
|
<Clock size={11} />
|
||||||
@@ -195,6 +197,12 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) {
|
|||||||
Набор открыт
|
Набор открыт
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setBookingGroup(`${g.type}, ${g.days.join("/")} ${g.time}`)}
|
||||||
|
className="w-full mt-1 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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</ScrollRow>
|
</ScrollRow>
|
||||||
@@ -274,6 +282,12 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<BookingModal
|
||||||
|
open={bookingGroup !== null}
|
||||||
|
onClose={() => setBookingGroup(null)}
|
||||||
|
groupInfo={bookingGroup ?? undefined}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import { siteContent } from "@/data/content";
|
|||||||
interface BookingModalProps {
|
interface BookingModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
groupInfo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BookingModal({ open, onClose }: BookingModalProps) {
|
export function BookingModal({ open, onClose, groupInfo }: BookingModalProps) {
|
||||||
const { contact } = siteContent;
|
const { contact } = siteContent;
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [phone, setPhone] = useState("+375 ");
|
const [phone, setPhone] = useState("+375 ");
|
||||||
@@ -65,7 +66,8 @@ export function BookingModal({ open, onClose }: BookingModalProps) {
|
|||||||
(e: React.FormEvent) => {
|
(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Build Instagram DM message with pre-filled text
|
// Build Instagram DM message with pre-filled text
|
||||||
const message = `Здравствуйте! Меня зовут ${name}, хочу записаться на занятие. Мой телефон: ${phone}`;
|
const groupText = groupInfo ? ` (${groupInfo})` : "";
|
||||||
|
const message = `Здравствуйте! Меня зовут ${name}, хочу записаться на занятие${groupText}. Мой телефон: ${phone}`;
|
||||||
const instagramUrl = `https://ig.me/m/blackheartdancehouse?text=${encodeURIComponent(message)}`;
|
const instagramUrl = `https://ig.me/m/blackheartdancehouse?text=${encodeURIComponent(message)}`;
|
||||||
window.open(instagramUrl, "_blank");
|
window.open(instagramUrl, "_blank");
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user