Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions src/pages/CoachProfilePage.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,159 @@
overscroll-behavior-x: none;
}

.coach-profile-fixed-chrome {
position: sticky;
top: 0;
z-index: 30;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(8px);
border-bottom: 1px solid #e2e8f0;
}

.coach-profile-fixed-chrome__header,
.coach-profile-fixed-chrome__tabs,
.coach-profile-fixed-chrome__coach {
max-width: 1240px;
margin: 0 auto;
padding: 10px 20px;
}

.coach-profile-fixed-chrome__header {
display: flex;
justify-content: space-between;
align-items: center;
}

.coach-profile-fixed-chrome__back,
.coach-profile-fixed-chrome__message {
display: inline-flex;
align-items: center;
gap: 8px;
color: #334155;
font-weight: 600;
text-decoration: none;
background: transparent;
border: 0;
}

.coach-profile-fixed-chrome__tabs {
display: flex;
gap: 8px;
border-top: 1px solid #f1f5f9;
}

.coach-profile-fixed-chrome__tab {
border: 0;
background: transparent;
padding: 8px 12px;
color: #64748b;
border-bottom: 2px solid transparent;
}

.coach-profile-fixed-chrome__tab--active {
color: #7c3aed;
border-bottom-color: #7c3aed;
}

.coach-profile-fixed-chrome__coach {
display: flex;
align-items: center;
gap: 10px;
border-top: 1px solid #f1f5f9;
}

.coach-profile-fixed-chrome__coach-avatar {
width: 34px;
height: 34px;
border-radius: 999px;
object-fit: cover;
}

.coach-profile-fixed-chrome__coach-copy {
display: flex;
flex-direction: column;
line-height: 1.1;
}

.coach-profile-fixed-chrome__coach-copy span {
font-size: 12px;
color: #64748b;
}

.coach-profile-fixed-chrome__coach-price {
margin-left: auto;
color: #7c3aed;
font-weight: 700;
}

.coach-profile-bio {
margin-top: 16px;
}

.coach-profile-bio__text {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}

.coach-profile-bio__text--expanded {
display: block;
}

.coach-profile-bio__toggle {
margin-top: 8px;
border: 0;
background: transparent;
color: #7c3aed;
font-weight: 600;
}

.coach-profile-about-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-bottom: 16px;
}

.coach-profile-about-grid > div {
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 12px;
background: #f8fafc;
}

.coach-profile-about-grid strong {
display: block;
font-size: 14px;
}

.coach-profile-about-grid span {
font-size: 12px;
color: #64748b;
}

.coach-profile-credit-strip {
margin: 0 0 12px;
border: 1px solid #ddd6fe;
border-radius: 12px;
background: #f5f3ff;
padding: 10px 12px;
display: flex;
justify-content: space-between;
align-items: center;
color: #6d28d9;
font-weight: 600;
font-size: 13px;
}

.coach-profile-credit-strip button {
border: 0;
background: transparent;
color: #7c3aed;
font-weight: 700;
}

@supports not (overflow-x: clip) {
.coach-profile-page {
overflow-x: hidden;
Expand Down
119 changes: 109 additions & 10 deletions src/pages/CoachProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,13 @@ const CoachProfilePage = () => {
const [selection, setSelection] = useState<BookingSelections>(() => ({
lessonType: "all",
}));
const [showFullBio, setShowFullBio] = useState(false);
const profileScrollRef = useRef<HTMLDivElement | null>(null);
const packagesRef = useRef<HTMLElement | null>(null);
const aboutRef = useRef<HTMLElement | null>(null);
const specialtiesRef = useRef<HTMLElement | null>(null);
const courtsRef = useRef<HTMLElement | null>(null);
const [activeTab, setActiveTab] = useState<"about" | "specialties" | "courts">("about");
const [bookingInFlight, setBookingInFlight] = useState<string | null>(null);
const [bookingError, setBookingError] = useState<string | null>(null);
const [bookingSuccess, setBookingSuccess] = useState<string | null>(null);
Expand Down Expand Up @@ -1637,8 +1644,8 @@ const CoachProfilePage = () => {

const lessonFilters = [
{ id: "all", label: "All", ariaLabel: "All lesson formats" },
{ id: "private", label: "Privates", ariaLabel: "Private lessons" },
{ id: "group", label: "Groups", ariaLabel: "Group sessions" },
{ id: "private", label: "Private", ariaLabel: "Private lessons" },
{ id: "group", label: "Group", ariaLabel: "Group sessions" },
];

const playerLessonCredits = useMemo(() => {
Expand Down Expand Up @@ -1787,6 +1794,10 @@ const CoachProfilePage = () => {
const selectedDate = selectedDateEntry?.date;

const filteredSlots = selectedDateEntry?.slots ?? [];
const slotsThisWeek = useMemo(
() => availableDates.reduce((sum, date) => sum + (date.totalSlots ?? date.slots.length ?? 0), 0),
[availableDates],
);

const lessonTypeDetailMap = useMemo(() => {
return bookingLessonTypes.reduce(
Expand Down Expand Up @@ -1874,6 +1885,30 @@ const CoachProfilePage = () => {

return profile.highlightChips.filter((chip) => !/utr/i.test(chip.label));
}, [profile]);
const experienceLabel = useMemo(
() => highlightChips.find((chip) => /year|yrs|experience/i.test(chip.label))?.label ?? "Experience listed",
[highlightChips],
);
const studentsLabel = useMemo(
() => highlightChips.find((chip) => /student/i.test(chip.label))?.label ?? "Students coached",
[highlightChips],
);

const scrollToSection = (section: "about" | "specialties" | "courts") => {
setActiveTab(section);
const targetMap = {
about: aboutRef,
specialties: specialtiesRef,
courts: courtsRef,
};
const target = targetMap[section].current;
if (!target) return;
target.scrollIntoView({ behavior: "smooth", block: "start" });
};

const scrollToPackages = () => {
packagesRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
};

const resolveIsoDate = (date?: BookingDate) => {
if (!date) return undefined;
Expand Down Expand Up @@ -2637,7 +2672,45 @@ const extractLocationId = (slot?: BookingSlot) => {
rosterError={rosterStatusError ?? undefined}
rosterLoading={rosterStatusLoading}
/>
<section className="coach-profile-hero">
<section className="coach-profile-hero" ref={profileScrollRef}>
<div className="coach-profile-fixed-chrome">
<div className="coach-profile-fixed-chrome__header">
<Link to="/find-coaches" className="coach-profile-fixed-chrome__back">
<ArrowLeft size={16} /> Coaches
</Link>
<button type="button" className="coach-profile-fixed-chrome__message">
<MessageCircle size={16} /> Message
</button>
</div>
<div className="coach-profile-fixed-chrome__tabs">
{([
["about", "About"],
["specialties", "Specialties"],
["courts", "Courts"],
] as const).map(([key, label]) => (
<button
key={key}
type="button"
className={`coach-profile-fixed-chrome__tab${
activeTab === key ? " coach-profile-fixed-chrome__tab--active" : ""
}`}
onClick={() => scrollToSection(key)}
>
{label}
</button>
))}
</div>
<div className="coach-profile-fixed-chrome__coach">
<img src={coachAvatar} alt={coachDisplayName} className="coach-profile-fixed-chrome__coach-avatar" />
<div className="coach-profile-fixed-chrome__coach-copy">
<strong>{coachDisplayName}</strong>
<span>{certifications[0] ?? coachTitle}</span>
</div>
<div className="coach-profile-fixed-chrome__coach-price">
{lessonDetails.find((lesson) => lesson.id === "private")?.price ?? "Pricing available"}
</div>
</div>
</div>
<div className="coach-profile-hero__inner">
<div className="coach-profile-identity coach-profile-hero__identity">
<div className="coach-profile-identity__avatar-block">
Expand Down Expand Up @@ -2684,6 +2757,18 @@ const extractLocationId = (slot?: BookingSlot) => {
);
})}
</div>
<div className="coach-profile-bio">
<p className={`coach-profile-bio__text${showFullBio ? " coach-profile-bio__text--expanded" : ""}`}>
{profile.about}
</p>
<button
type="button"
className="coach-profile-bio__toggle"
onClick={() => setShowFullBio((prev) => !prev)}
>
{showFullBio ? "See less" : "See more"}
</button>
</div>
</div>
</div>
</div>
Expand All @@ -2696,12 +2781,16 @@ const extractLocationId = (slot?: BookingSlot) => {
<div className="coach-profile-layout">
<section className="coach-profile-main">
<section className="coach-profile-sections">
<div className="coach-profile-panel coach-profile-panel--about">
<div ref={aboutRef} className="coach-profile-panel coach-profile-panel--about">
<div className="coach-profile-panel__header">
<h2 className="coach-profile-panel__title">About {coachFirstName}</h2>
<MessageCircle className="coach-profile-panel__icon" strokeWidth={2.4} />
</div>
<p className="coach-profile-about__copy">{profile.about}</p>
<div className="coach-profile-about-grid">
<div><strong>{experienceLabel}</strong><span>Experience</span></div>
<div><strong>{studentsLabel}</strong><span>Students</span></div>
<div><strong>{languages.join(", ") || "—"}</strong><span>Languages</span></div>
</div>
{certifications.length > 0 && (
<div className="coach-profile-certifications">
{certifications.map((certification) => (
Expand All @@ -2714,7 +2803,7 @@ const extractLocationId = (slot?: BookingSlot) => {
)}
</div>

<div className="coach-profile-panel">
<div ref={specialtiesRef} className="coach-profile-panel">
<div className="coach-profile-panel__header">
<h2 className="coach-profile-panel__title">Specialties</h2>
<Sparkles className="coach-profile-panel__icon" strokeWidth={2.4} />
Expand All @@ -2731,9 +2820,9 @@ const extractLocationId = (slot?: BookingSlot) => {
)}
</div>

<div className="coach-profile-panel">
<div ref={courtsRef} className="coach-profile-panel">
<div className="coach-profile-panel__header">
<h2 className="coach-profile-panel__title">Coaching Locations</h2>
<h2 className="coach-profile-panel__title">Courts</h2>
<MapPin className="coach-profile-panel__icon" strokeWidth={2.4} />
</div>
<p className="coach-profile-panel__copy">Certified to coach at these nearby courts and clubs.</p>
Expand Down Expand Up @@ -2773,7 +2862,7 @@ const extractLocationId = (slot?: BookingSlot) => {
))}
</ul>
)}
<div className="coach-profile-packages">
<div className="coach-profile-packages" ref={packagesRef}>
<div className="coach-profile-packages__header">
<div className="coach-profile-packages__intro">
<h3 className="coach-profile-packages__title">Package deals</h3>
Expand Down Expand Up @@ -2903,10 +2992,20 @@ const extractLocationId = (slot?: BookingSlot) => {
<div className="coach-booking__header">
<div className="coach-booking__header-copy">
<h2 className="coach-booking__title">{bookingHeadline}</h2>
<p className="coach-booking__subtitle">Select your preferred date and time</p>
<p className="coach-booking__subtitle">
{slotsThisWeek > 0 ? `${slotsThisWeek} slots this week` : "No slots this week"}
</p>
</div>
<CalendarDays className="coach-booking__icon" strokeWidth={2.4} />
</div>
{hasCreditsRemaining ? (
<div className="coach-profile-credit-strip">
<span>{availableCredits} credits · can be applied at booking</span>
<button type="button" onClick={scrollToPackages}>
Top up
</button>
</div>
) : null}

<div className="coach-booking__wallet">
<div
Expand Down
Loading