From c7021d31e71a0a26f8a0041aacefdeccd3948298 Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Sat, 21 Feb 2026 08:14:23 -0800 Subject: [PATCH 01/16] Redesign matchplay home layout and navigation --- src/App.css | 405 ++++++++- src/components/MainLayout.tsx | 15 +- src/pages/DashboardPage.jsx | 1444 ++++++--------------------------- 3 files changed, 666 insertions(+), 1198 deletions(-) diff --git a/src/App.css b/src/App.css index f87fe884..035c16a9 100644 --- a/src/App.css +++ b/src/App.css @@ -34,6 +34,7 @@ font-size: 20px; color: #0f172a; flex-shrink: 0; + text-decoration: none; } .brand-badge { @@ -67,8 +68,8 @@ .nav-link:hover, .nav-link.active { - background: rgba(22, 163, 74, 0.1); - color: #15803d; + background: #ede9fe; + color: #7c3aed; } .header-actions { @@ -2328,3 +2329,403 @@ align-items: flex-start; } } + +/* Matchplay home redesign */ +.home-redesign { + display: grid; + grid-template-columns: minmax(0, 1fr) 340px; + gap: 24px; +} + +.home-redesign__main, +.home-redesign__sidebar { + display: flex; + flex-direction: column; + gap: 16px; +} + +.hero-cards { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.hero-card { + border: 0; + border-radius: 16px; + padding: 24px; + color: #fff; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + cursor: pointer; +} + +.hero-card h2 { + font-size: 34px; + margin: 0 0 4px; +} + +.hero-card p { + margin: 0; + opacity: 0.95; + font-size: 22px; +} + +.hero-card > span { + display: grid; + place-items: center; + width: 38px; + height: 38px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.2); + font-size: 20px; +} + +.hero-card--match { background: linear-gradient(135deg, #10b981, #059669); } +.hero-card--lesson { background: linear-gradient(135deg, #8b5cf6, #7c3aed); } + +.date-selector-v2, +.filter-bar-v2, +.available-section, +.sidebar-card-v2 { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 14px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.date-selector-v2 { + display: flex; + align-items: stretch; + gap: 8px; + padding: 16px; + overflow-x: auto; +} + +.date-nav-btn { + width: 36px; + border: 1px solid #e2e8f0; + background: #f8fafc; + border-radius: 8px; +} + +.date-option { + min-width: 92px; + padding: 10px 12px; + border: 1px solid #eef2f7; + background: #f8fafc; + border-radius: 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + color: #334155; +} + +.date-option strong { font-size: 13px; } +.date-option span { font-size: 12px; color: #64748b; } + +.date-option.selected { + border: 2px solid #8b5cf6; + background: #ede9fe; +} + +.date-calendar-btn { + margin-left: auto; + border: 2px dashed #e2e8f0; + border-radius: 10px; + background: #fff; + padding: 0 14px; + color: #475569; + display: flex; + align-items: center; + gap: 8px; +} + +.date-picker-input { + position: absolute; + opacity: 0; + width: 0; + pointer-events: none; +} + +.filter-bar-v2 { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 20px; +} + +.filter-item { + border: 1px solid #e2e8f0; + background: #f8fafc; + border-radius: 10px; + padding: 10px 14px; + color: #475569; +} + +.filter-divider { + width: 1px; + align-self: stretch; + background: #e2e8f0; + margin: 0 6px; +} + +.filter-tab { + border: 0; + border-radius: 8px; + background: transparent; + padding: 8px 12px; + font-weight: 600; + color: #64748b; +} + +.filter-tab span { + background: #e2e8f0; + border-radius: 999px; + padding: 1px 6px; + margin-left: 6px; + font-size: 12px; +} + +.filter-tab.active { + background: #ede9fe; + color: #7c3aed; +} + +.available-section { padding: 18px; } + +.available-section__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.available-section__header h2 { + margin: 0; + font-size: 32px; + color: #1e293b; +} + +.available-section__header button { + border: 0; + background: transparent; + color: #8b5cf6; + font-weight: 600; +} + +.activity-list-v2 { + display: flex; + flex-direction: column; + gap: 14px; +} + +.activity-card-v2 { + border: 1px solid #e2e8f0; + border-radius: 14px; + padding: 16px; + display: flex; + gap: 16px; + align-items: flex-start; +} + +.activity-type-icon { + width: 48px; + height: 48px; + border-radius: 12px; + display: grid; + place-items: center; + font-size: 24px; +} + +.activity-type-icon.match { background: #d1fae5; } +.activity-type-icon.lesson { background: #ede9fe; } +.activity-type-icon.group { background: #fef3c7; } + +.activity-content { flex: 1; } +.activity-top-row { display: flex; gap: 8px; align-items: center; } +.activity-top-row h3 { margin: 0; } + +.activity-type-label { + font-size: 11px; + font-weight: 700; +} + +.activity-type-label.match { color: #059669; } +.activity-type-label.lesson { color: #7c3aed; } +.activity-type-label.group { color: #d97706; } + +.activity-badge { + font-size: 10px; + font-weight: 700; + color: #ef4444; + background: #fee2e2; + border-radius: 6px; + padding: 2px 6px; +} + +.activity-content h3 { + margin: 6px 0; + font-size: 30px; + color: #1e293b; +} + +.activity-meta-row { + display: flex; + gap: 12px; + flex-wrap: wrap; + color: #64748b; + font-size: 22px; +} + +.activity-meta-row span { display: inline-flex; align-items: center; gap: 4px; } + +.activity-right { + border-left: 1px solid #f1f5f9; + padding-left: 16px; + min-width: 120px; + display: flex; + flex-direction: column; + gap: 8px; + align-items: flex-end; +} + +.activity-price { + font-size: 40px; + font-weight: 700; + color: #0f172a; +} + +.activity-spots { font-size: 20px; color: #64748b; } +.activity-spots.urgent { color: #dc2626; font-weight: 600; } + +.activity-btn { + border: 0; + border-radius: 10px; + color: #fff; + padding: 8px 22px; + font-weight: 600; +} + +.activity-btn.match { background: #10b981; } +.activity-btn.lesson, +.activity-btn.group { background: #8b5cf6; } + +.sidebar-card-v2 { + padding: 16px; +} + +.sidebar-card-v2__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + +.sidebar-card-v2__header h3, +.sidebar-card-v2 > h3 { + margin: 0; + color: #1e293b; +} + +.sidebar-card-v2__header button { + border: 0; + background: transparent; + color: #8b5cf6; + font-weight: 600; +} + +.schedule-item-v2 { + display: flex; + gap: 10px; + background: #f8fafc; + border-radius: 10px; + padding: 10px; + margin-bottom: 10px; +} + +.schedule-type { + width: 4px; + border-radius: 3px; + background: #8b5cf6; +} + +.schedule-type.group { background: #f59e0b; } + +.schedule-item-v2 p, +.schedule-item-v2 h4, +.schedule-item-v2 span { + margin: 0; +} + +.schedule-item-v2 h4 { + font-size: 15px; + color: #0f172a; +} + +.schedule-item-v2 span, +.sidebar-empty { color: #64748b; font-size: 13px; } + +.stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin-top: 12px; +} + +.stats-grid div { + background: #f8fafc; + border-radius: 10px; + padding: 16px 10px; + text-align: center; +} + +.stats-grid strong { + display: block; + font-size: 32px; + color: #0f172a; +} + +.stats-grid span { + color: #64748b; + font-size: 12px; +} + +.find-players-card { + text-align: center; +} + +.find-emoji { + font-size: 52px; + margin: 8px 0; +} + +.find-players-card h4 { + margin: 0; + color: #0f172a; +} + +.find-players-card p { + color: #64748b; +} + +.find-players-btn { + border: 0; + background: #8b5cf6; + color: #fff; + border-radius: 10px; + padding: 10px 16px; + font-weight: 600; +} + +@media (max-width: 1100px) { + .home-redesign { + grid-template-columns: 1fr; + } + + .hero-cards { + grid-template-columns: 1fr; + } +} diff --git a/src/components/MainLayout.tsx b/src/components/MainLayout.tsx index 29116ba3..6aabbe6f 100644 --- a/src/components/MainLayout.tsx +++ b/src/components/MainLayout.tsx @@ -3,17 +3,13 @@ import { useEffect, useRef, useState } from "react"; import { Link, NavLink } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; import usePlayerIdentity from "../hooks/usePlayerIdentity"; -import { Bell, ChevronDown, CreditCard, LogOut, ShieldX, Target, UserRound } from "lucide-react"; +import { Bell, ChevronDown, CreditCard, LogOut, MessageCircle, ShieldX, Target, UserRound } from "lucide-react"; const navLinks = [ - { label: "Home", to: "/" }, { label: "Browse Matches", to: "/matches" }, { label: "Find Players", to: "/find-players" }, { label: "Group Lessons", to: "/group-lessons" }, - { label: "Find Coaches", to: "/find-coaches" }, - { label: "My Coaches", to: "/my-coaches" }, - { label: "Credits", to: "/credits" }, - { label: "My Activity", href: "#activity" }, + { label: "Coaches", to: "/find-coaches" }, ]; interface MainLayoutProps { @@ -50,10 +46,10 @@ const MainLayout = ({ children }: MainLayoutProps) => { return (
-
+
MP
Matchplay -
+
+ - - - ); -}; - -const QuickBookButton = ({ onClick, isOpen }) => ( - -); - -const QuickBookModal = ({ coaches, onClose }) => ( -
-
-
-
-
-

Quick Book a Lesson

-

- Book with one of our featured coaches right now -

-
- -
-
- {coaches.map((coach) => ( -
- -
-
- {coach.name} - ⭐ {coach.rating} -
-
{coach.specialty}
-
Next available Β· {coach.nextAvailable}
-
-
-
{coach.price}
- -
-
- ))} -
-
- -
-
-
-); - -const matches = [ - { - type: "Doubles", - title: "Friendly Ladder", - details: ["Tomorrow β€’ 6:30 PM", "Court 4 β€’ 2 spots left"], - }, - { - type: "Singles", - title: "Skill Challenge", - details: ["Thursday β€’ 5:00 PM", "Court 1 β€’ Intermediate"], - }, - { - type: "Cardio", - title: "Endurance Clinic", - details: ["Saturday β€’ 9:00 AM", "Fitness Center β€’ 6 spots"], - }, -]; - -const coaches = [ - { name: "Mia Roberts", speciality: "USTA Certified", rating: "4.9", sessions: "32 lessons" }, - { name: "David Park", speciality: "High Performance", rating: "4.8", sessions: "28 lessons" }, - { name: "Jamie Lee", speciality: "Junior Development", rating: "4.9", sessions: "19 lessons" }, - { name: "Carlos Ramirez", speciality: "Serve Specialist", rating: "4.7", sessions: "24 lessons" }, -]; - -const bottomActions = [ - { - title: "AI Match Me", - description: "Get matched instantly with players at your level.", - action: "Start", - accent: "#16a34a", - }, - { - title: "Get Gear", - description: "Shop curated gear recommended by pros.", - action: "Shop", - accent: "#f97316", + price: "Free", + startTime: moment().add(2, "days").hour(14).minute(0).toISOString(), }, ]; const DashboardPage = () => { const navigate = useNavigate(); - const { displayName } = usePlayerIdentity(); - const [locationState, setLocationState] = useState({ - status: "idle", - coords: null, - error: null, - accuracyMiles: null, - locationName: null, - lookupFailed: false, - }); + const datePickerRef = useRef(null); + const [locationState, setLocationState] = useState({ status: "idle", locationName: null }); const [distanceFilter, setDistanceFilter] = useState("10"); - const [scheduleState, setScheduleState] = useState({ - status: "idle", - items: [], - error: null, - }); + const distanceOptions = ["5", "10", "15", "20"]; + const [scheduleState, setScheduleState] = useState({ status: "idle", items: [], error: null }); const [dateFilter, setDateFilter] = useState({ type: "all" }); - const [isCustomRangeOpen, setIsCustomRangeOpen] = useState(false); - const [customRangeStart, setCustomRangeStart] = useState(""); - const [customRangeEnd, setCustomRangeEnd] = useState(""); - const [customRangeError, setCustomRangeError] = useState(null); const [activeFilter, setActiveFilter] = useState("all"); - const [showAllActivities, setShowAllActivities] = useState(false); - const [showQuickBook, setShowQuickBook] = useState(false); + const [weekOffset, setWeekOffset] = useState(0); - const distanceOptions = ["5", "10", "15", "20", "all"]; - const todayAnchor = useMemo(() => moment().startOf("day"), []); - const todayIso = useMemo(() => todayAnchor.format("YYYY-MM-DD"), [todayAnchor]); - const maxSelectableDate = useMemo( - () => todayAnchor.clone().add(30, "days").format("YYYY-MM-DD"), - [todayAnchor], - ); + const weekStart = useMemo(() => moment().startOf("day").add(weekOffset * 7, "days"), [weekOffset]); const dayOptions = useMemo(() => { - const countsByDay = activities.reduce((accumulator, activity) => { + const countsByDay = activities.reduce((acc, activity) => { const key = moment(activity.startTime).startOf("day").format("YYYY-MM-DD"); - accumulator[key] = (accumulator[key] || 0) + 1; - return accumulator; + acc[key] = (acc[key] || 0) + 1; + return acc; }, {}); - return Array.from({ length: 14 }).map((_, index) => { - const dayMoment = todayAnchor.clone().add(index, "days"); + return Array.from({ length: 5 }).map((_, index) => { + const dayMoment = weekStart.clone().add(index, "days"); const key = dayMoment.format("YYYY-MM-DD"); - const isToday = index === 0; - const isTomorrow = index === 1; - + const isToday = dayMoment.isSame(moment(), "day"); return { value: key, - label: isToday ? "Today" : isTomorrow ? "Tomorrow" : dayMoment.format("ddd"), + label: index === 0 && weekOffset === 0 ? "TODAY" : dayMoment.format("ddd").toUpperCase(), date: dayMoment.format("D"), events: countsByDay[key] || 0, - fullLabel: `${dayMoment.format("ddd, MMM D")}`, + isToday, }; }); - }, [todayAnchor]); - - const scopedActivities = useMemo(() => { - return activities.filter((activity) => { - if (dateFilter.type === "all") { - return true; - } - - const activityDay = moment(activity.startTime).startOf("day"); - - if (dateFilter.type === "day") { - return activityDay.format("YYYY-MM-DD") === dateFilter.iso; - } - - const rangeStart = moment(dateFilter.start).startOf("day"); - const rangeEnd = moment(dateFilter.end).endOf("day"); - return activityDay.isBetween(rangeStart, rangeEnd, undefined, "[]"); - }); - }, [dateFilter]); + }, [weekOffset, weekStart]); + + const scopedActivities = useMemo( + () => + activities.filter((activity) => { + if (dateFilter.type === "all") return true; + return moment(activity.startTime).format("YYYY-MM-DD") === dateFilter.iso; + }), + [dateFilter], + ); - const typeCounts = useMemo(() => { - const base = scopedActivities; - return { - all: base.length, - match: base.filter((activity) => activity.type === "match").length, - private: base.filter((activity) => activity.type === "private").length, - group: base.filter((activity) => activity.type === "group").length, - }; - }, [scopedActivities]); + const typeCounts = useMemo( + () => ({ + all: scopedActivities.length, + match: scopedActivities.filter((activity) => activity.type === "match").length, + private: scopedActivities.filter((activity) => activity.type === "private").length, + group: scopedActivities.filter((activity) => activity.type === "group").length, + }), + [scopedActivities], + ); - const typeFilterOptions = [ - { id: "all", label: "All Activities" }, - { id: "match", label: "Matches" }, - { id: "private", label: "Private Lessons" }, - { id: "group", label: "Group Sessions" }, - ]; + const filteredActivities = useMemo( + () => + scopedActivities.filter((activity) => activeFilter === "all" || activity.type === activeFilter), + [activeFilter, scopedActivities], + ); - const filteredActivities = useMemo(() => { - return scopedActivities - .filter((activity) => activeFilter === "all" || activity.type === activeFilter) - .sort((first, second) => - moment(first.startTime).valueOf() - moment(second.startTime).valueOf(), - ); - }, [activeFilter, scopedActivities]); + const stats = useMemo( + () => ({ + matches: activities.filter((item) => item.type === "match").length * 3, + rating: "4.0", + lessons: activities.filter((item) => item.type !== "match").length * 2, + connections: 24, + }), + [], + ); useEffect(() => { - setShowAllActivities(false); - }, [activeFilter, dateFilter]); - - const displayedActivities = showAllActivities - ? filteredActivities - : filteredActivities.slice(0, 3); - const remainingActivityCount = filteredActivities.length - displayedActivities.length; - - const selectedDayMeta = - dateFilter.type === "day" - ? dayOptions.find((option) => option.value === dateFilter.iso) ?? null - : null; - - const dateFilterChipLabel = (() => { - if (dateFilter.type === "all") { - return "All Days"; - } - if (dateFilter.type === "day") { - return selectedDayMeta?.fullLabel ?? "Selected Day"; - } - const startLabel = moment(dateFilter.start).format("ddd, MMM D"); - const endLabel = moment(dateFilter.end).format("ddd, MMM D"); - return startLabel === endLabel ? startLabel : `${startLabel} – ${endLabel}`; - })(); - - const customRangeButtonLabel = (() => { - if (dateFilter.type !== "range") { - return "Choose dates"; - } - const startLabel = moment(dateFilter.start).format("MMM D"); - const endLabel = moment(dateFilter.end).format("MMM D"); - const summary = startLabel === endLabel ? startLabel : `${startLabel} – ${endLabel}`; - return `Custom: ${summary}`; - })(); - - const emptyStateMessage = (() => { - if (dateFilter.type === "all") { - return "Try adjusting your filters to discover more sessions."; - } - if (dateFilter.type === "day") { - const label = selectedDayMeta?.fullLabel ?? "this day"; - return `Nothing is scheduled for ${label} with these filters. Try expanding your search.`; - } - const startFull = moment(dateFilter.start).format("ddd, MMM D"); - const endFull = moment(dateFilter.end).format("ddd, MMM D"); - if (dateFilter.start === dateFilter.end) { - return `Nothing is scheduled for ${startFull} with these filters. Try expanding your search.`; - } - return `Nothing is scheduled from ${startFull} to ${endFull} with these filters. Try expanding your search.`; - })(); - - const activeFilterLabel = - activeFilter === "all" - ? "All Activities" - : activityTypeMeta[activeFilter]?.label ?? "All Activities"; - - const hasActiveFilters = dateFilter.type !== "all" || activeFilter !== "all"; - - const clearFilters = () => { - setDateFilter({ type: "all" }); - setActiveFilter("all"); - setIsCustomRangeOpen(false); - setCustomRangeStart(""); - setCustomRangeEnd(""); - setCustomRangeError(null); - }; - - const handleToggleCustomRange = () => { - setIsCustomRangeOpen((open) => { - if (open) { - setCustomRangeError(null); - return false; - } - - if (dateFilter.type === "range") { - setCustomRangeStart(dateFilter.start); - setCustomRangeEnd(dateFilter.end); - } else if (dateFilter.type === "day") { - setCustomRangeStart(dateFilter.iso); - setCustomRangeEnd(dateFilter.iso); - } else { - setCustomRangeStart(todayIso); - setCustomRangeEnd(todayIso); - } - - setCustomRangeError(null); - return true; - }); - }; - - const handleApplyCustomRange = () => { - if (!customRangeStart || !customRangeEnd) { - setCustomRangeError("Select both a start and end date."); - return; - } - if (customRangeStart > customRangeEnd) { - setCustomRangeError("Start date must be before the end date."); - return; - } - setCustomRangeError(null); - setDateFilter({ type: "range", start: customRangeStart, end: customRangeEnd }); - setIsCustomRangeOpen(false); - }; - - const handleClearCustomRange = () => { - setCustomRangeStart(""); - setCustomRangeEnd(""); - setCustomRangeError(null); - setDateFilter({ type: "all" }); - setIsCustomRangeOpen(false); - }; - - const formatDistanceLabel = (value) => (value === "all" ? "All" : `${value} mi`); - - const formatCoordinatesLabel = (coords) => { - if (!coords) { - return "Your area"; - } - - const latitude = Math.abs(coords.latitude).toFixed(2); - const longitude = Math.abs(coords.longitude).toFixed(2); - const latHemisphere = coords.latitude >= 0 ? "N" : "S"; - const lonHemisphere = coords.longitude >= 0 ? "E" : "W"; - - return `${latitude}Β° ${latHemisphere}, ${longitude}Β° ${lonHemisphere}`; - }; - - const resolveLocationName = async (coords) => { - if (!coords) { - return; - } - - try { - const query = new URLSearchParams({ - format: "jsonv2", - lat: coords.latitude.toString(), - lon: coords.longitude.toString(), - }); - - const response = await fetch(`https://nominatim.openstreetmap.org/reverse?${query.toString()}`, { - headers: { - Accept: "application/json", - }, - }); - - if (!response.ok) { - throw new Error("Failed to lookup location"); - } - - const data = await response.json(); - const address = data?.address ?? {}; - - const locality = - address.city || - address.town || - address.village || - address.hamlet || - address.suburb || - address.county; - const region = address.state || address.region; - const countryCode = address.country_code ? address.country_code.toUpperCase() : null; - - const labelParts = [locality, region, countryCode].filter(Boolean); - const locationLabel = labelParts.length - ? labelParts.join(", ") - : data?.display_name?.split(",").slice(0, 2).join(", ") || null; - - setLocationState((previous) => { - if (previous.status !== "ready") { - return previous; - } - - return { - ...previous, - locationName: locationLabel, - lookupFailed: !locationLabel, - }; - }); - } catch (error) { - console.error("Failed to resolve location", error); - setLocationState((previous) => { - if (previous.status !== "ready") { - return previous; - } - - return { - ...previous, - locationName: null, - lookupFailed: true, - }; - }); - } - }; - - const detectLocation = () => { - if (!("geolocation" in navigator)) { - setLocationState({ - status: "error", - coords: null, - error: "Location services are not supported in this browser.", - accuracyMiles: null, - locationName: null, - lookupFailed: false, - }); - return; - } - - setLocationState((previous) => ({ - ...previous, - status: "loading", - error: null, - lookupFailed: false, - })); - + if (!navigator.geolocation) return; navigator.geolocation.getCurrentPosition( - (position) => { - const { coords } = position; - const accuracyMiles = coords.accuracy - ? Math.max(1, Math.round(coords.accuracy / 1609.34)) - : null; - - setLocationState({ - status: "ready", - coords, - error: null, - accuracyMiles, - locationName: null, - lookupFailed: false, - }); - resolveLocationName(coords); - }, - (error) => { - setLocationState({ - status: "error", - coords: null, - error: error.message || "We couldn't determine your location.", - accuracyMiles: null, - locationName: null, - lookupFailed: false, - }); - } + () => setLocationState({ status: "ready", locationName: "Los Angeles" }), + () => setLocationState({ status: "error", locationName: "Location unavailable" }), ); - }; - - useEffect(() => { - detectLocation(); }, []); useEffect(() => { @@ -901,12 +211,7 @@ const DashboardPage = () => { return; } - setScheduleState((previous) => ({ - ...previous, - status: "loading", - error: null, - })); - + setScheduleState((prev) => ({ ...prev, status: "loading", error: null })); try { const [lessonsResponse, groupLessonsResponse] = await Promise.all([ getPlayerFutureLessons({ token, perPage: 5, signal: controller.signal }), @@ -917,28 +222,10 @@ const DashboardPage = () => { const privateLessons = buildScheduleItems(extractLessons(lessonsResponse), "lesson"); const groupLessons = buildScheduleItems(extractLessons(groupLessonsResponse), "group"); - const combined = [...privateLessons, ...groupLessons].sort((a, b) => { - if (a.startAt && b.startAt) { - return a.startAt.getTime() - b.startAt.getTime(); - } - if (a.startAt) return -1; - if (b.startAt) return 1; - return 0; - }); - - const annotated = combined.map((item, index) => ({ - ...item, - highlight: index === 0 && !!item.startAt, - })); - setScheduleState({ - status: "ready", - items: annotated, - error: null, - }); + setScheduleState({ status: "ready", items: [...privateLessons, ...groupLessons].slice(0, 3), error: null }); } catch (error) { if (cancelled) return; - console.error("Failed to load upcoming lessons", error); setScheduleState({ status: "error", items: [], @@ -955,407 +242,188 @@ const DashboardPage = () => { }; }, []); - const locationChipLabel = () => { - if (locationState.status === "ready") { - if (locationState.locationName) { - return locationState.locationName; - } - - if (locationState.coords) { - return formatCoordinatesLabel(locationState.coords); - } - - return "Los Angeles, California, US"; - } - - if (locationState.status === "loading") { - return "Locating…"; - } - - if (locationState.status === "error") { - return "Location unavailable"; - } - - return "Los Angeles, California, US"; - }; + const filterOptions = [ + { id: "all", label: "All" }, + { id: "match", label: "Matches" }, + { id: "private", label: "Lessons" }, + { id: "group", label: "Groups" }, + ]; return ( -
-
-
-
-

Ready to Play?

-

Welcome back, {displayName}. Let’s get you on court.

-

- Discover curated matches, lessons, and group sessions tailored to your level and - schedule. -

-
-
-
-
- - {distanceOptions.map((value) => ( - - ))} -
+
+
+
+ - {dayOptions.map((day) => { - const classes = ["day-selector__day"]; - if (dateFilter.type === "day" && dateFilter.iso === day.value) { - classes.push("is-active"); - } - if (day.events === 0) { - classes.push("is-empty"); - } - return ( - - ); - })} -
-
- - -
-
- {isCustomRangeOpen ? ( -
-
- - -
-

- {customRangeStart && customRangeEnd - ? customRangeStart === customRangeEnd - ? `Showing activities for ${moment(customRangeStart).format("MMM D")}.` - : `Showing activities from ${moment(customRangeStart).format("MMM D")} to ${moment(customRangeEnd).format("MMM D")}.` - : "Select a start and end date to filter activities."} -

- {customRangeError ? ( -

{customRangeError}

- ) : null} -
- - -
-
- ) : null} -
-
- {typeFilterOptions.map((option) => ( - - ))} + β†’ + +
-
-
-
- Next booking - Today Β· 5:30 PM - Court 4 with Jamie -
-
-
-
+ β†’ + + -
-
-
-

Available Activities

-

- Browse matches, lessons, and group sessions starting soon near you. -

-
- -
- {hasActiveFilters ? ( -
- Showing: -
- {dateFilterChipLabel} - {activeFilter !== "all" ? ( - {activeFilterLabel} - ) : null} -
- -
- ) : null} - {filteredActivities.length === 0 ? ( -
- -

No activities scheduled

-

{emptyStateMessage}

-
- + {dayOptions.map((day) => ( + + ))} + + + setDateFilter({ type: "day", iso: event.target.value })} + /> +
+ +
+ + + + {filterOptions.map((option) => ( -
-
- ) : ( - <> -
- {displayedActivities.map((activity) => ( - - ))} -
- {filteredActivities.length > 3 ? ( -
- -
- ) : null} - - )} - - -
-
-
-

My Schedule

-

Your upcoming matches and coaching sessions for the day.

-
-
- {scheduleState.status === "loading" || scheduleState.status === "idle" ? ( -
Loading your schedule…
- ) : scheduleState.status === "error" ? ( -
- We couldn’t load your upcoming lessons. Please try again. -
- ) : scheduleState.status === "unauthenticated" ? ( -
Sign in to view your upcoming lessons.
- ) : scheduleState.items.length === 0 ? ( -
- You don’t have any upcoming lessons yet. Book a session to get started! -
- ) : ( -
- {scheduleState.items.slice(0, 3).map((item) => ( -
-
- {item.timeLabel} - {item.secondaryLabel} -
-
-
{item.title}
- {item.coachLabel ?
{item.coachLabel}
: null} - {item.locationLabel ?
{item.locationLabel}
: null} - {item.durationLabel ? ( -
⏱ {item.durationLabel}
- ) : null} -
-
- {item.badgeLabel ?
{item.badgeLabel}
: null} - {item.statusLabel ?
{item.statusLabel}
: null} -
-
))} -
- )} -
+ -
-
-
-

Matches Near You

-

Join competitive and social matches happening soon.

-
- -
-
- {matches.map((match) => ( -
-
{match.type}
-
{match.title}
-
- {match.details.map((detail) => ( - {detail} - ))} -
- -
- ))} +
+
+

Available Near You

+ +
+
+ {filteredActivities.map((activity) => { + const meta = activityTypeMeta[activity.type]; + const startLabel = moment(activity.startTime).calendar(null, { + sameDay: "[Today] Β· h:mm A", + nextDay: "[Tomorrow] Β· h:mm A", + nextWeek: "ddd Β· h:mm A", + sameElse: "ddd Β· h:mm A", + }); + return ( +
+
{meta.icon}
+
+
+ {meta.label} + {activity.badge ? {activity.badge} : null} +
+

{activity.title}

+
+ {startLabel} + {activity.venue} Β· {activity.distance} + {activity.level} +
+
+
+
{activity.price}
+
+ {activity.spotsRemaining} spots left +
+ +
+
+ ); + })} +
+
-
-
-
-
-

Featured Coaches

-

Top coaches with stellar reviews from players like you.

-
- -
-
- {coaches.map((coach) => ( -
-
{coach.name.split(" ").map((part) => part[0]).join("")}
-
{coach.name}
-
{coach.speciality}
-
{coach.sessions}
-
⭐ {coach.rating}
- -
- ))} -
-
+ +
); }; From fdde1f71c31b005a067741b6961cda4455b23b81 Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Sat, 21 Feb 2026 11:16:57 -0800 Subject: [PATCH 02/16] Fix home page typography to system font scale --- src/App.css | 20 ++++++++++++++------ src/index.css | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/App.css b/src/App.css index 035c16a9..2d4d2a80 100644 --- a/src/App.css +++ b/src/App.css @@ -2331,6 +2331,11 @@ } /* Matchplay home redesign */ + +.home-redesign, +.home-redesign * { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} .home-redesign { display: grid; grid-template-columns: minmax(0, 1fr) 340px; @@ -2370,7 +2375,7 @@ .hero-card p { margin: 0; opacity: 0.95; - font-size: 22px; + font-size: 13px; } .hero-card > span { @@ -2505,7 +2510,8 @@ .available-section__header h2 { margin: 0; - font-size: 32px; + font-size: 18px; + font-weight: 700; color: #1e293b; } @@ -2551,6 +2557,7 @@ .activity-type-label { font-size: 11px; font-weight: 700; + text-transform: uppercase; } .activity-type-label.match { color: #059669; } @@ -2568,7 +2575,8 @@ .activity-content h3 { margin: 6px 0; - font-size: 30px; + font-size: 16px; + font-weight: 600; color: #1e293b; } @@ -2577,7 +2585,7 @@ gap: 12px; flex-wrap: wrap; color: #64748b; - font-size: 22px; + font-size: 13px; } .activity-meta-row span { display: inline-flex; align-items: center; gap: 4px; } @@ -2593,12 +2601,12 @@ } .activity-price { - font-size: 40px; + font-size: 22px; font-weight: 700; color: #0f172a; } -.activity-spots { font-size: 20px; color: #64748b; } +.activity-spots { font-size: 12px; color: #64748b; } .activity-spots.urgent { color: #dc2626; font-weight: 600; } .activity-btn { diff --git a/src/index.css b/src/index.css index 4f6e1129..bc13901f 100644 --- a/src/index.css +++ b/src/index.css @@ -26,7 +26,7 @@ --coach-color-focus: #bbd4ff; color: var(--coach-color-heading); - font-family: "Inter", "SF Pro Text", "Segoe UI", system-ui, -apple-system, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-weight: 400; line-height: 1.5; text-rendering: optimizeLegibility; From c6bb6675d9bbad5c0a3827f06a875957b8ef8a92 Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Sun, 22 Feb 2026 08:21:55 -0800 Subject: [PATCH 03/16] Add mobile responsive layout for matchplay home --- src/App.css | 320 ++++++++++++++++++++++++++++++++++ src/components/MainLayout.tsx | 19 +- src/pages/DashboardPage.jsx | 100 ++++++++--- 3 files changed, 408 insertions(+), 31 deletions(-) diff --git a/src/App.css b/src/App.css index 2d4d2a80..88eed894 100644 --- a/src/App.css +++ b/src/App.css @@ -130,6 +130,10 @@ box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.15); } +.mobile-badge { + display: none; +} + .user-menu { position: relative; } @@ -2349,6 +2353,11 @@ gap: 16px; } +.schedule-section-mobile, +.bottom-nav-mobile { + display: none; +} + .hero-cards { display: grid; grid-template-columns: 1fr 1fr; @@ -2391,6 +2400,15 @@ .hero-card--match { background: linear-gradient(135deg, #10b981, #059669); } .hero-card--lesson { background: linear-gradient(135deg, #8b5cf6, #7c3aed); } +.hero-card__icon, +.hero-card__mobile-title { + display: none; +} + +.hero-card__desktop-copy { + display: block; +} + .date-selector-v2, .filter-bar-v2, .available-section, @@ -2462,6 +2480,17 @@ padding: 14px 20px; } +.filter-row-v2, +.filter-tabs-v2 { + display: flex; + align-items: center; + gap: 8px; +} + +.filter-tabs-v2 { + flex: 1; +} + .filter-item { border: 1px solid #e2e8f0; background: #f8fafc; @@ -2737,3 +2766,294 @@ grid-template-columns: 1fr; } } + + +@media (max-width: 768px) { + .main-nav.mobile-header { + position: sticky; + top: 0; + z-index: 120; + padding: 12px 16px; + border-radius: 0; + border-bottom: 1px solid #f1f5f9; + box-shadow: none; + margin: -24px -24px 0; + } + + .desktop-nav-links { + display: none; + } + + .mobile-header-left { + gap: 10px; + } + + .mobile-logo { + width: 36px; + height: 36px; + border-radius: 10px; + background: linear-gradient(135deg, #a3e635, #84cc16); + font-size: 0; + } + + .mobile-brand { + font-size: 20px; + font-weight: 700; + color: #1e293b; + } + + .mobile-header-right { + gap: 8px; + } + + .mobile-header-btn { + width: 40px; + height: 40px; + border-radius: 12px; + background: #f1f5f9; + } + + .notification-indicator { + display: none; + } + + .mobile-badge { + position: absolute; + top: -4px; + right: -4px; + width: 18px; + height: 18px; + border-radius: 9px; + border: 2px solid #fff; + background: #ef4444; + color: #fff; + font-size: 10px; + font-weight: 700; + display: grid; + place-items: center; + } + + .mobile-avatar { + width: 40px; + height: 40px; + background: linear-gradient(135deg, #8b5cf6, #7c3aed); + font-size: 14px; + font-weight: 700; + } + + .home-redesign { + grid-template-columns: 1fr; + gap: 12px; + padding-bottom: 90px; + } + + .home-redesign__sidebar { + display: none; + } + + .hero-cards { + grid-template-columns: 1fr 1fr; + gap: 10px; + } + + .hero-card { + padding: 12px; + border-radius: 12px; + gap: 10px; + } + + .hero-card__desktop-copy, + .hero-card > span { + display: none; + } + + .hero-card__icon { + width: 32px; + height: 32px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.2); + display: grid; + place-items: center; + font-size: 14px; + } + + .hero-card__mobile-title { + display: block; + font-size: 13px; + font-weight: 600; + color: #fff; + } + + .schedule-section-mobile { + display: block; + background: #fff; + border-radius: 14px; + border: 1px solid #e2e8f0; + margin-top: 6px; + } + + .schedule-header-mobile { + display: flex; + justify-content: space-between; + padding: 14px 16px; + border-bottom: 1px solid #f1f5f9; + } + + .schedule-title-mobile { font-size: 15px; font-weight: 700; color: #1e293b; } + .schedule-link-mobile { border: 0; background: transparent; color: #8b5cf6; font-size: 13px; font-weight: 600; } + + .schedule-items-mobile { + display: flex; + gap: 8px; + padding: 12px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .schedule-items-mobile::-webkit-scrollbar { display: none; } + + .schedule-item-mobile { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + background: #f8fafc; + border-radius: 10px; + min-width: 170px; + } + + .schedule-time-mobile { display: flex; flex-direction: column; } + .schedule-time-value { font-size: 15px; font-weight: 700; color: #1e293b; line-height: 1; } + .schedule-time-period { font-size: 11px; font-weight: 700; color: #94a3b8; } + .schedule-item-title-mobile { font-size: 13px; font-weight: 600; color: #1e293b; } + .schedule-meta-mobile { font-size: 12px; color: #64748b; } + + .date-selector-v2 { + gap: 6px; + padding: 12px; + overflow-x: auto; + } + + .date-nav-btn { width: 32px; height: 32px; flex-shrink: 0; } + .date-option { padding: 8px 12px; min-width: 56px; } + .date-option strong { font-size: 16px; font-weight: 700; } + .date-calendar-btn { padding: 8px 10px; min-width: 48px; } + + .filter-bar-v2 { + flex-direction: column; + align-items: stretch; + gap: 8px; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; + } + + .filter-row-v2 { + display: flex; + gap: 8px; + } + + .filter-item { + flex: 1; + padding: 10px 14px; + background: #fff; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + } + + .filter-tabs-v2 { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 10px; + padding: 4px; + gap: 4px; + } + + .filter-tab { + flex: 1; + padding: 10px 8px; + font-size: 12px; + border-radius: 8px; + } + + .filter-tab span { + font-size: 10px; + border-radius: 4px; + padding: 2px 6px; + } + + .available-section { + padding: 0; + border: 0; + box-shadow: none; + background: transparent; + } + + .activity-list-v2 { gap: 12px; } + + .activity-card-v2 { + background: #fff; + border-radius: 14px; + border: 1px solid #e2e8f0; + padding: 16px; + flex-direction: column; + gap: 12px; + } + + .activity-content { + width: 100%; + } + + .activity-meta-row { + gap: 10px; + margin-bottom: 10px; + } + + .activity-right { + width: 100%; + border-left: 0; + border-top: 1px solid #f1f5f9; + padding: 14px 0 0; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + + .activity-btn { padding: 12px 28px; font-size: 14px; } + + .bottom-nav-mobile { + display: flex; + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 120; + justify-content: space-around; + padding: 10px 8px 20px; + background: #fff; + border-top: 1px solid #e2e8f0; + } + + .bottom-nav-mobile__item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + font-size: 10px; + font-weight: 600; + color: #94a3b8; + text-decoration: none; + } + + .bottom-nav-mobile__item.active { + color: #8b5cf6; + } + + .bottom-nav-mobile__icon { + font-size: 22px; + line-height: 1; + } +} diff --git a/src/components/MainLayout.tsx b/src/components/MainLayout.tsx index 6aabbe6f..68a5a047 100644 --- a/src/components/MainLayout.tsx +++ b/src/components/MainLayout.tsx @@ -45,12 +45,12 @@ const MainLayout = ({ children }: MainLayoutProps) => { return (
-
- -
MP
- Matchplay +
+ +
MP
+ Matchplay - -
- - From 53356a2c9361f76bbbc15bb16661b37bd8a59233 Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Sun, 22 Feb 2026 11:32:49 -0800 Subject: [PATCH 05/16] Enforce single-row mobile header and full mobile controls --- src/App.css | 61 +++++++++++++++++++++++++++++++++++ src/components/MainLayout.tsx | 2 +- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/App.css b/src/App.css index a0c4625d..0aa690a0 100644 --- a/src/App.css +++ b/src/App.css @@ -3101,3 +3101,64 @@ line-height: 1; } } + + +@media (max-width: 768px) { + .mobile-header { + display: flex !important; + flex-direction: row !important; + justify-content: space-between !important; + align-items: center !important; + } + + .main-nav.mobile-header { + flex-wrap: nowrap !important; + } + + .main-nav.mobile-header .desktop-nav-links { + display: none !important; + } + + .main-nav.mobile-header .header-left { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1; + } + + .main-nav.mobile-header .header-right { + display: flex !important; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + + .main-nav.mobile-header .user-menu { + flex-shrink: 0; + } + + .hero-scroll .hero-card { + flex: 0 0 280px; + } + + .filter-row-v2 { + display: flex !important; + flex-direction: row; + } + + .filter-row-v2 .filter-item { + flex: 1 1 0; + } + + .filter-tabs-v2 { + display: flex !important; + overflow: visible; + } + + .filter-tabs-v2 .filter-tab { + flex: 1 1 0; + min-width: 0; + white-space: nowrap; + } +} diff --git a/src/components/MainLayout.tsx b/src/components/MainLayout.tsx index d99b9065..218a1590 100644 --- a/src/components/MainLayout.tsx +++ b/src/components/MainLayout.tsx @@ -47,7 +47,7 @@ const MainLayout = ({ children }: MainLayoutProps) => {
-
MP
+
🎾
Matchplay