From 80bcd58917150f377e10432d7e9c9ee2de167f4b Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Tue, 28 Oct 2025 18:32:19 -0700 Subject: [PATCH 1/4] feat: redesign browse home dashboard --- src/TennisMatchApp.jsx | 1016 ++++++++++++++++++++++++++-------------- 1 file changed, 674 insertions(+), 342 deletions(-) diff --git a/src/TennisMatchApp.jsx b/src/TennisMatchApp.jsx index fd18736e..59469ab3 100644 --- a/src/TennisMatchApp.jsx +++ b/src/TennisMatchApp.jsx @@ -75,6 +75,7 @@ import { Trophy, Sparkles, Target, + ShieldCheck, } from "lucide-react"; import Autocomplete from "react-google-autocomplete"; import AppHeader from "./components/AppHeader"; @@ -3212,6 +3213,27 @@ const TennisMatchApp = () => { respondToInvite, ]); + const personalScheduleMatches = useMemo(() => { + const relevant = matches.filter((match) => { + if (!match) return false; + const status = typeof match.status === "string" ? match.status.toLowerCase() : match.status; + if (status === "archived" || status === "cancelled" || status === "canceled") { + return false; + } + if (match.type === "hosted" || match.type === "joined") { + return true; + } + if (match.isInvited) { + return true; + } + return false; + }); + + return sortMatchesByRecency(relevant).slice(0, 3); + }, [matches, sortMatchesByRecency]); + + const nearbyMatchesPreview = useMemo(() => displayedMatches.slice(0, 6), [displayedMatches]); + const getMatchCount = useCallback( (filterId) => { if (!matchCounts) return 0; @@ -3228,144 +3250,486 @@ const TennisMatchApp = () => { [matchCounts], ); - const BrowseScreen = () => ( -
- {/* Hero Section with Action Button */} -
-
-
-
-

- {currentUser ? "Browse Local Matches" : "Find Your Next Match"} -

-

- {currentUser - ? "See what's nearby and jump back in." - : "Discover active players around North County."} -

+ const BrowseScreen = () => { + if (!currentUser) { + return ( +
+
+

Find your next match in seconds

+

+ Create matches, invite partners, and discover open courts once you sign in. +

+ +
+
+ ); + } + + const welcomeName = currentUser?.name?.split(" ")[0] || "there"; + const unreadUpdates = Number(notificationSummary.unread ?? 0); + const highlightStats = [ + { + id: "my", + label: "Matches Hosting / Playing", + value: getMatchCount("my"), + icon: Users, + gradient: "from-emerald-50 to-emerald-100", + iconColor: "text-emerald-600", + }, + { + id: "open", + label: "Open Spots Nearby", + value: getMatchCount("open"), + icon: Target, + gradient: "from-sky-50 to-indigo-100", + iconColor: "text-indigo-600", + }, + { + id: "invites", + label: "Pending Invites", + value: pendingInvites.length, + icon: Mail, + gradient: "from-amber-50 to-orange-100", + iconColor: "text-orange-600", + }, + { + id: "updates", + label: "New Updates", + value: unreadUpdates, + icon: Bell, + gradient: "from-purple-50 to-fuchsia-100", + iconColor: "text-purple-600", + }, + ]; + + const nextHighlightedMatch = personalScheduleMatches[0] || nearbyMatchesPreview[0] || null; + const quickActions = [ + { + id: "create", + title: "Create Match", + description: "Start a match and invite players in seconds.", + icon: Sparkles, + accent: "from-emerald-500 to-green-500", + action: () => navigate("/create"), + }, + { + id: "browse", + title: "Browse Matches", + description: "See local matches ready for another player.", + icon: Search, + accent: "from-sky-500 to-indigo-500", + action: () => { + setActiveFilter("open"); + const section = + typeof document !== "undefined" + ? document.getElementById("matches-nearby-section") + : null; + section?.scrollIntoView({ behavior: "smooth", block: "start" }); + }, + }, + { + id: "players", + title: "Find Players", + description: "Discover partners by skill level and availability.", + icon: Users, + accent: "from-purple-500 to-fuchsia-500", + action: () => goToPlayers(), + }, + { + id: "courts", + title: "Find Courts", + description: "Explore nearby clubs and public courts to host.", + icon: MapPin, + accent: "from-amber-500 to-orange-500", + action: () => navigate("/courts"), + }, + ]; + + const featuredCoaches = [ + { + id: "coach-rogers", + name: "Mia Rogers", + specialty: "Singles Strategy", + rating: "4.9", + price: "$85/hr", + color: "from-emerald-100 to-emerald-200", + initials: "MR", + }, + { + id: "coach-lane", + name: "David Lane", + specialty: "Serve & Volley", + rating: "4.8", + price: "$95/hr", + color: "from-blue-100 to-indigo-200", + initials: "DL", + }, + { + id: "coach-carter", + name: "Jill Carter", + specialty: "Doubles Chemistry", + rating: "5.0", + price: "$75/hr", + color: "from-purple-100 to-fuchsia-200", + initials: "JC", + }, + { + id: "coach-ramirez", + name: "Carlos Ramirez", + specialty: "Footwork & Agility", + rating: "4.7", + price: "$90/hr", + color: "from-amber-100 to-orange-200", + initials: "CR", + }, + ]; + + const filterOptions = [ + { + id: "my", + label: "My Matches", + count: getMatchCount("my"), + gradient: "from-emerald-500 to-green-500", + icon: "⭐", + }, + { + id: "open", + label: "Open Matches", + count: getMatchCount("open"), + gradient: "from-sky-500 to-indigo-500", + icon: "🔥", + }, + { + id: "today", + label: "Today", + count: getMatchCount("today"), + gradient: "from-blue-500 to-cyan-500", + icon: "📅", + }, + { + id: "tomorrow", + label: "Tomorrow", + count: getMatchCount("tomorrow"), + gradient: "from-amber-500 to-orange-500", + icon: "⏰", + }, + { + id: "weekend", + label: "Weekend", + count: getMatchCount("weekend"), + gradient: "from-purple-500 to-fuchsia-500", + icon: "🎉", + }, + { + id: "draft", + label: "Drafts", + count: getMatchCount("draft"), + gradient: "from-slate-500 to-slate-600", + icon: "📝", + }, + { + id: "archived", + label: "Archived", + count: getMatchCount("archived"), + gradient: "from-gray-500 to-slate-600", + icon: "🗂️", + }, + ]; + + return ( +
+
+
+
+
+ + Welcome Back + +

+ Welcome back, {welcomeName}! +

+

+ Your complete match dashboard for players, invites, and courts. +

+
+
+ {highlightStats.map((stat) => { + const Icon = stat.icon; + return ( +
+
+
+ +
+ + {stat.label} + +
+

{stat.value}

+
+ ); + })} +
-
- + +
+
+ + Next on your calendar + + {nextHighlightedMatch ? ( + <> +

+ {nextHighlightedMatch.format || "Match"} at {nextHighlightedMatch.location || "Location TBA"} +

+

+ {nextHighlightedMatch.dateTime ? formatDateTime(nextHighlightedMatch.dateTime) : "Time to be announced"} +

+
+ {nextHighlightedMatch.type === "hosted" && ( + Hosting + )} + {nextHighlightedMatch.type === "joined" && ( + Playing + )} + {Number.isFinite(nextHighlightedMatch.spotsAvailable) && ( + + {nextHighlightedMatch.spotsAvailable} spot{nextHighlightedMatch.spotsAvailable === 1 ? "" : "s"} open + + )} +
+ + ) : ( + <> +

Plan your next tennis session

+

+ Create a match or explore the browse feed to get something on the books. +

+ + )} +
-
-
-
+ - {currentUser ? ( - <> -
-
-
-
-
- - - {hasLocationFilter - ? activeLocationLabel - : "Showing matches from every location"} - -
- +
+
+
+
+

My Schedule

+

+ Upcoming matches you're hosting or playing in. +

-
- {distanceOptions.map((distance) => ( + +
+ + {personalScheduleMatches.length > 0 ? ( +
    + {personalScheduleMatches.map((match) => { + const spotsOpen = Number.isFinite(match.spotsAvailable) + ? match.spotsAvailable + : Number.isFinite(match.rosterSpotsRemaining) + ? match.rosterSpotsRemaining + : null; + return ( +
  • +
    + + {match.format || "Match"} + + + {match.dateTime ? formatDateTime(match.dateTime) : "Time coming soon"} + + + {match.location || "Location to be announced"} + +
    + {match.type === "hosted" && ( + + Hosting + + )} + {match.type === "joined" && ( + + Playing + + )} + {Number.isFinite(spotsOpen) && spotsOpen > 0 && ( + + {spotsOpen} spot{spotsOpen === 1 ? "" : "s"} open + + )} +
    +
    +
    + + {match.type === "hosted" && ( + + )} +
    +
  • + ); + })} +
+ ) : ( +
+ No upcoming matches yet. Create one or join an open match to see it here. +
+ )} +
+ +
+
+ {quickActions.map((action) => { + const Icon = action.icon; + return ( - ))} -
+ ); + })}
- {hasLocationFilter && ( -

- Showing matches within {distanceFilter} miles of your selected location. -

- )} - {showLocationPicker && ( -
- setLocationSearchTerm(event.target.value)} - onPlaceSelected={(place) => { - if (!place) { - setGeoError("Please choose a location from the suggestions."); - return; - } - const lat = place.geometry?.location?.lat?.(); - const lng = place.geometry?.location?.lng?.(); - const label = - place.formatted_address || place.name || locationSearchTerm || "Custom location"; - if ( - typeof lat === "number" && - !Number.isNaN(lat) && - typeof lng === "number" && - !Number.isNaN(lng) - ) { - setLocationFilter({ label, lat, lng }); + +
+
+
+
+
+ +
+
+

Match Location

+

+ {hasLocationFilter ? activeLocationLabel : "Showing all locations"} +

+
+
+ +
+
+ {distanceOptions.map((distance) => ( + + ))} +
+ {hasLocationFilter && ( +

+ Showing matches within {distanceFilter} miles of your saved location. +

+ )} +
+ + {showLocationPicker && ( +
+ setLocationSearchTerm(event.target.value)} + onPlaceSelected={(place) => { + if (!place) { + setGeoError("Please choose a location from the suggestions."); + return; + } + const lat = place.geometry?.location?.lat?.(); + const lng = place.geometry?.location?.lng?.(); + const label = + place.formatted_address || place.name || locationSearchTerm || "Custom location"; + if ( + typeof lat === "number" && + !Number.isNaN(lat) && + typeof lng === "number" && + !Number.isNaN(lng) + ) { + setLocationFilter({ label, lat, lng }); + setGeoError(""); + setShowLocationPicker(false); + } else { + setGeoError("We couldn't read that location. Try another search."); + } + }} + options={{ types: ["geocode", "establishment"], fields: [ "formatted_address", @@ -3374,244 +3738,212 @@ const TennisMatchApp = () => { "address_components", ], }} - /> -
- -
- {hasLocationFilter && ( + /> +
+ +
+ {hasLocationFilter && ( + + )} - )} - +
+ {geoError && ( +

{geoError}

+ )} + {!import.meta.env.VITE_GOOGLE_API_KEY && ( +

+ Tip: Add a Google Places API key to enable autocomplete suggestions. +

+ )}
- {geoError && ( -

{geoError}

- )} - {!import.meta.env.VITE_GOOGLE_API_KEY && ( -

- Tip: Provide a Google Places API key to enable location search suggestions. -

- )} + )} +
+ + refreshMatchesAndInvites()} + onViewAll={goToInvites} + pendingInviteCount={pendingInvites.length} + unreadUpdateCount={unreadUpdates} + /> +
+
+ +
+
+
+

Matches Near You

+

+ Browse open matches, filter by time, and jump in when a spot opens up. +

+
+
+
+ + setMatchSearch(event.target.value)} + placeholder="Search matches..." + className="w-full rounded-2xl border-2 border-gray-200 bg-white px-10 py-2.5 text-sm font-semibold text-gray-700 focus:border-emerald-500 focus:outline-none focus:ring-2 focus:ring-emerald-500" + />
- )} -
- - refreshMatchesAndInvites()} - onViewAll={goToInvites} - pendingInviteCount={pendingInvites.length} - unreadUpdateCount={Number(notificationSummary.unread ?? 0)} - /> -
+
+
- {/* Filter Tabs */} -
-
-
- {[ - { - id: "my", - label: "My Matches", - count: getMatchCount("my"), - color: "violet", - icon: "⭐", - }, - { - id: "open", - label: "Open Matches", - count: getMatchCount("open"), - color: "green", - icon: "🔥", - }, - { - id: "today", - label: "Today", - count: getMatchCount("today"), - color: "blue", - icon: "📅", - }, - { - id: "tomorrow", - label: "Tomorrow", - count: getMatchCount("tomorrow"), - color: "amber", - icon: "⏰", - }, - { - id: "weekend", - label: "Weekend", - count: getMatchCount("weekend"), - color: "purple", - icon: "🎉", - }, - { - id: "draft", - label: "Drafts", - count: getMatchCount("draft"), - color: "gray", - icon: "📝", - }, - { - id: "archived", - label: "Archived", - count: getMatchCount("archived"), - color: "slate", - icon: "🗂️", - }, - ].map((filter) => ( - - ))} -
-
-
+ {filter.icon} + {filter.label} + {filter.count > 0 && ( + + {filter.count} + + )} + + ))} +
- {/* Match Cards */} -
-
- setMatchSearch(e.target.value)} - className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-green-500 focus:border-green-500 font-semibold text-gray-800" - /> -
+ {hasLocationFilter && displayedMatches.length === 0 && ( +
+ No matches within {distanceFilter} miles yet. Expand your distance filter or check back soon! +
+ )} - {hasLocationFilter && displayedMatches.length === 0 && ( -
- No matches within {distanceFilter} miles of your location yet. Try expanding the distance filter or check back soon! -
- )} +
+ {displayedMatches.map((match) => ( + + ))} +
-
- {displayedMatches.map((match) => ( - - ))} -
+ {matchPagination && !hasLocationFilter && ( +
+ + + Page {matchPagination.page} of {Math.max(1, Math.ceil(getMatchCount(activeFilter) / matchPagination.perPage))} + + +
+ )} + - {matchPagination && !hasLocationFilter && ( -
- - - Page {matchPagination.page} of - {" "} - {Math.max( - 1, - Math.ceil( - getMatchCount(activeFilter) / - matchPagination.perPage - ) - )} - - -
- )} -
- - ) : ( -
-

- Sign up or log in to view available matches. -

- +
+
+
+

Featured Coaches

+

+ Sharpen your game with local pros who love Matchplay players. +

+
+ +
+ +
+ {featuredCoaches.map((coach) => ( +
+
+
+ {coach.initials} +
+ + + {coach.rating} + +
+

{coach.name}

+

{coach.specialty}

+
+ {coach.price} + +
+
+ ))} +
+
- )} -
- ); +
+ ); + }; const MatchCard = ({ match }) => { const isHosted = match.type === "hosted"; From 4eb02bcf98f1338c8865d0cd788541217af41583 Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Tue, 28 Oct 2025 20:51:08 -0700 Subject: [PATCH 2/4] Refine browse experience to match redesign --- src/TennisMatchApp.jsx | 1252 ++++++++++++++++++++++++---------------- 1 file changed, 739 insertions(+), 513 deletions(-) diff --git a/src/TennisMatchApp.jsx b/src/TennisMatchApp.jsx index 59469ab3..493bac34 100644 --- a/src/TennisMatchApp.jsx +++ b/src/TennisMatchApp.jsx @@ -70,12 +70,13 @@ import { MessageCircle, Phone, AlertCircle, + Compass, + Navigation, ArrowRight, Zap, Trophy, Sparkles, Target, - ShieldCheck, } from "lucide-react"; import Autocomplete from "react-google-autocomplete"; import AppHeader from "./components/AppHeader"; @@ -3213,27 +3214,6 @@ const TennisMatchApp = () => { respondToInvite, ]); - const personalScheduleMatches = useMemo(() => { - const relevant = matches.filter((match) => { - if (!match) return false; - const status = typeof match.status === "string" ? match.status.toLowerCase() : match.status; - if (status === "archived" || status === "cancelled" || status === "canceled") { - return false; - } - if (match.type === "hosted" || match.type === "joined") { - return true; - } - if (match.isInvited) { - return true; - } - return false; - }); - - return sortMatchesByRecency(relevant).slice(0, 3); - }, [matches, sortMatchesByRecency]); - - const nearbyMatchesPreview = useMemo(() => displayedMatches.slice(0, 6), [displayedMatches]); - const getMatchCount = useCallback( (filterId) => { if (!matchCounts) return 0; @@ -3250,461 +3230,494 @@ const TennisMatchApp = () => { [matchCounts], ); - const BrowseScreen = () => { - if (!currentUser) { - return ( -
-
-

Find your next match in seconds

-

- Create matches, invite partners, and discover open courts once you sign in. -

- -
-
+ const personalScheduleMatches = useMemo(() => { + if (!Array.isArray(matches) || matches.length === 0) return []; + + const now = Date.now(); + const normalizeString = (value) => { + if (typeof value !== "string") return ""; + const trimmed = value.trim(); + return trimmed; + }; + + const pickString = (...candidates) => { + for (const candidate of candidates) { + const normalized = normalizeString(candidate); + if (normalized) return normalized; + } + return ""; + }; + + return matches + .filter((match) => { + if (!match) return false; + const membership = normalizeString(match.type).toLowerCase(); + if (membership !== "hosted" && membership !== "joined") return false; + const startTs = getMatchTimestamp(match); + if (startTs === null) return false; + // Keep matches that are upcoming or started in the last two hours + return startTs >= now - 1000 * 60 * 60 * 2; + }) + .map((match) => { + const start = parseDateValue(match.dateTime); + const locationLabel = pickString( + match.location, + match.location_text, + match.locationText, + match.venue, + match.court_name, + match.courtName, + ); + const formatLabel = pickString( + match.match_format, + match.matchFormat, + match.format, + match.title, + match.name, + ); + const skillLabel = pickString(match.skill_level, match.skillLevel, match.skill); + const startLabel = start ? formatDateTime(start) : "Date TBA"; + return { + id: match.id, + match, + start, + startLabel, + relativeTime: formatRelativeTimeFromNow(start), + locationLabel, + formatLabel, + skillLabel, + membership: normalizeString(match.type).toLowerCase(), + status: normalizeString(match.status).toLowerCase(), + }; + }) + .sort((a, b) => { + const aTime = a.start instanceof Date ? a.start.getTime() : Number.POSITIVE_INFINITY; + const bTime = b.start instanceof Date ? b.start.getTime() : Number.POSITIVE_INFINITY; + return aTime - bTime; + }) + .slice(0, 5); + }, [ + matches, + formatDateTime, + formatRelativeTimeFromNow, + getMatchTimestamp, + parseDateValue, + ]); + + const nearbyMatchesPreview = useMemo(() => { + if (!Array.isArray(displayedMatches) || displayedMatches.length === 0) return []; + + const normalizeString = (value) => { + if (typeof value !== "string") return ""; + const trimmed = value.trim(); + return trimmed; + }; + + const pickString = (...candidates) => { + for (const candidate of candidates) { + const normalized = normalizeString(candidate); + if (normalized) return normalized; + } + return ""; + }; + + return displayedMatches.slice(0, 4).map((match) => { + const start = parseDateValue(match.dateTime); + const formatLabel = pickString( + match.match_format, + match.matchFormat, + match.format, + match.title, + match.name, ); - } + const locationLabel = pickString( + match.location, + match.location_text, + match.locationText, + match.venue, + match.court_name, + match.courtName, + ); + const distanceLabel = Number.isFinite(match.distanceMiles) + ? `${match.distanceMiles.toFixed(match.distanceMiles < 10 ? 1 : 0)} mi` + : ""; + return { + id: match.id, + match, + start, + startLabel: start ? formatDateTime(start) : "Date TBA", + relativeTime: formatRelativeTimeFromNow(start), + locationLabel, + formatLabel, + distanceLabel, + }; + }); + }, [ + displayedMatches, + formatDateTime, + formatRelativeTimeFromNow, + parseDateValue, + ]); + + const BrowseScreen = () => { + const nextMatch = personalScheduleMatches[0]; + const remainingSchedule = personalScheduleMatches.slice(1); - const welcomeName = currentUser?.name?.split(" ")[0] || "there"; - const unreadUpdates = Number(notificationSummary.unread ?? 0); - const highlightStats = [ + const statCards = [ { - id: "my", - label: "Matches Hosting / Playing", - value: getMatchCount("my"), - icon: Users, - gradient: "from-emerald-50 to-emerald-100", - iconColor: "text-emerald-600", + label: "Upcoming matches", + value: personalScheduleMatches.length, + icon: Calendar, }, { - id: "open", - label: "Open Spots Nearby", - value: getMatchCount("open"), - icon: Target, - gradient: "from-sky-50 to-indigo-100", - iconColor: "text-indigo-600", + label: "Open nearby", + value: getMatchCount("open") || displayedMatches.length, + icon: Users, }, { - id: "invites", - label: "Pending Invites", + label: "Invites waiting", value: pendingInvites.length, icon: Mail, - gradient: "from-amber-50 to-orange-100", - iconColor: "text-orange-600", }, { - id: "updates", - label: "New Updates", - value: unreadUpdates, - icon: Bell, - gradient: "from-purple-50 to-fuchsia-100", - iconColor: "text-purple-600", + label: "Needs players", + value: matchesNeedingAttention.length, + icon: AlertCircle, }, ]; - const nextHighlightedMatch = personalScheduleMatches[0] || nearbyMatchesPreview[0] || null; const quickActions = [ { id: "create", - title: "Create Match", - description: "Start a match and invite players in seconds.", + label: "Create a match", + description: "Host a new meetup with your preferred format.", icon: Sparkles, - accent: "from-emerald-500 to-green-500", - action: () => navigate("/create"), + onClick: () => { + if (!currentUser) { + setShowSignInModal(true); + } else { + navigate("/create"); + } + }, }, { - id: "browse", - title: "Browse Matches", - description: "See local matches ready for another player.", - icon: Search, - accent: "from-sky-500 to-indigo-500", - action: () => { - setActiveFilter("open"); - const section = - typeof document !== "undefined" - ? document.getElementById("matches-nearby-section") - : null; - section?.scrollIntoView({ behavior: "smooth", block: "start" }); - }, + id: "invites", + label: "Review invites", + description: "Confirm spots and respond to requests.", + icon: Bell, + onClick: () => goToInvites(), }, { id: "players", - title: "Find Players", - description: "Discover partners by skill level and availability.", + label: "Discover players", + description: "Find partners that match your level.", icon: Users, - accent: "from-purple-500 to-fuchsia-500", - action: () => goToPlayers(), + onClick: () => goToPlayers(), }, { - id: "courts", - title: "Find Courts", - description: "Explore nearby clubs and public courts to host.", + id: "location", + label: hasLocationFilter ? "Adjust location" : "Set location", + description: "Tune the feed to courts near you.", icon: MapPin, - accent: "from-amber-500 to-orange-500", - action: () => navigate("/courts"), + onClick: () => { + setShowLocationPicker(true); + setGeoError(""); + }, }, ]; - const featuredCoaches = [ - { - id: "coach-rogers", - name: "Mia Rogers", - specialty: "Singles Strategy", - rating: "4.9", - price: "$85/hr", - color: "from-emerald-100 to-emerald-200", - initials: "MR", - }, - { - id: "coach-lane", - name: "David Lane", - specialty: "Serve & Volley", - rating: "4.8", - price: "$95/hr", - color: "from-blue-100 to-indigo-200", - initials: "DL", - }, - { - id: "coach-carter", - name: "Jill Carter", - specialty: "Doubles Chemistry", - rating: "5.0", - price: "$75/hr", - color: "from-purple-100 to-fuchsia-200", - initials: "JC", - }, - { - id: "coach-ramirez", - name: "Carlos Ramirez", - specialty: "Footwork & Agility", - rating: "4.7", - price: "$90/hr", - color: "from-amber-100 to-orange-200", - initials: "CR", - }, - ]; + const highlightTitle = (() => { + if (!nextMatch) return "No upcoming matches"; + const source = nextMatch.match || {}; + const pickString = (...candidates) => { + for (const candidate of candidates) { + if (typeof candidate === "string" && candidate.trim()) { + return candidate.trim(); + } + } + return "Match"; + }; + return ( + pickString( + source.title, + source.name, + source.match_format, + source.matchFormat, + source.format, + ) || "Match" + ); + })(); - const filterOptions = [ + const highlightMeta = [ + nextMatch?.startLabel && { icon: Calendar, label: nextMatch.startLabel }, + nextMatch?.relativeTime && { icon: Clock, label: nextMatch.relativeTime }, + nextMatch?.locationLabel && { icon: MapPin, label: nextMatch.locationLabel }, + nextMatch?.skillLabel && { icon: Star, label: nextMatch.skillLabel }, + ].filter(Boolean); + + const filterDefinitions = [ { id: "my", label: "My Matches", - count: getMatchCount("my"), - gradient: "from-emerald-500 to-green-500", + gradient: "linear-gradient(135deg, rgb(16 185 129), rgb(5 150 105))", icon: "⭐", }, { id: "open", label: "Open Matches", - count: getMatchCount("open"), - gradient: "from-sky-500 to-indigo-500", + gradient: "linear-gradient(135deg, rgb(56 189 248), rgb(99 102 241))", icon: "🔥", }, { id: "today", label: "Today", - count: getMatchCount("today"), - gradient: "from-blue-500 to-cyan-500", + gradient: "linear-gradient(135deg, rgb(245 158 11), rgb(234 88 12))", icon: "📅", }, { id: "tomorrow", label: "Tomorrow", - count: getMatchCount("tomorrow"), - gradient: "from-amber-500 to-orange-500", + gradient: "linear-gradient(135deg, rgb(244 114 182), rgb(168 85 247))", icon: "⏰", }, { id: "weekend", label: "Weekend", - count: getMatchCount("weekend"), - gradient: "from-purple-500 to-fuchsia-500", + gradient: "linear-gradient(135deg, rgb(59 130 246), rgb(124 58 237))", icon: "🎉", }, { id: "draft", label: "Drafts", - count: getMatchCount("draft"), - gradient: "from-slate-500 to-slate-600", + gradient: "linear-gradient(135deg, rgb(148 163 184), rgb(100 116 139))", icon: "📝", }, { id: "archived", label: "Archived", - count: getMatchCount("archived"), - gradient: "from-gray-500 to-slate-600", + gradient: "linear-gradient(135deg, rgb(113 113 122), rgb(82 82 91))", icon: "🗂️", }, ]; return ( -
-
-
-
-
- - Welcome Back - -

- Welcome back, {welcomeName}! +
+
+

-
-
-
-
-

My Schedule

-

- Upcoming matches you're hosting or playing in. -

-
- -
- - {personalScheduleMatches.length > 0 ? ( -
    - {personalScheduleMatches.map((match) => { - const spotsOpen = Number.isFinite(match.spotsAvailable) - ? match.spotsAvailable - : Number.isFinite(match.rosterSpotsRemaining) - ? match.rosterSpotsRemaining - : null; - return ( -
  • -
    - - {match.format || "Match"} - - - {match.dateTime ? formatDateTime(match.dateTime) : "Time coming soon"} - - - {match.location || "Location to be announced"} - -
    - {match.type === "hosted" && ( - - Hosting - - )} - {match.type === "joined" && ( - - Playing - - )} - {Number.isFinite(spotsOpen) && spotsOpen > 0 && ( - - {spotsOpen} spot{spotsOpen === 1 ? "" : "s"} open - - )} -
    -
    -
    - - {match.type === "hosted" && ( - - )} -
    -
  • - ); - })} -
- ) : ( -
- No upcoming matches yet. Create one or join an open match to see it here. +
+
+ +
+
+

+ {card.label} +

+

{card.value}

+
+
- )} + ))}
+
+
+
+
-
- {quickActions.map((action) => { - const Icon = action.icon; - return ( - - ); - })} -
- -
-
-
-
-
- -
-
-

Match Location

-

- {hasLocationFilter ? activeLocationLabel : "Showing all locations"} -

-
+
+
+
+
+ +
+
+

+ Location focus +

+

+ {hasLocationFilter ? activeLocationLabel : "All locations"} +

+

+ {hasLocationFilter + ? `Within ${distanceFilter} miles` + : "Set a location to prioritize nearby matches"} +

+
+
+ {hasLocationFilter && ( + + )}
-
- {distanceOptions.map((distance) => ( +
+ +
+ {distanceOptions.map((distance) => { + const isActive = distanceFilter === distance; + return ( - ))} -
- {hasLocationFilter && ( -

- Showing matches within {distanceFilter} miles of your saved location. -

- )} + ); + })}
+ {hasLocationFilter && ( +

+ Showing matches within {distanceFilter} miles of {activeLocationLabel}. +

+ )} + {showLocationPicker && ( -
+
setLocationSearchTerm(event.target.value)} onPlaceSelected={(place) => { @@ -3715,7 +3728,10 @@ const TennisMatchApp = () => { const lat = place.geometry?.location?.lat?.(); const lng = place.geometry?.location?.lng?.(); const label = - place.formatted_address || place.name || locationSearchTerm || "Custom location"; + place.formatted_address || + place.name || + locationSearchTerm || + "Custom location"; if ( typeof lat === "number" && !Number.isNaN(lat) && @@ -3726,43 +3742,26 @@ const TennisMatchApp = () => { setGeoError(""); setShowLocationPicker(false); } else { - setGeoError("We couldn't read that location. Try another search."); + setGeoError( + "We couldn't read that location's coordinates. Try another search.", + ); } }} options={{ - types: ["geocode", "establishment"], - fields: [ - "formatted_address", - "geometry", - "name", - "address_components", - ], - }} + types: ["geocode", "establishment"], + fields: ["formatted_address", "geometry", "name", "address_components"], + }} /> -
+
- {hasLocationFilter && ( - - )}
- {geoError && ( -

{geoError}

- )} + {geoError &&

{geoError}

} {!import.meta.env.VITE_GOOGLE_API_KEY && (

- Tip: Add a Google Places API key to enable autocomplete suggestions. + Tip: Provide a Google Places API key to enable location search suggestions.

)}
)} -
+
+ +
+
+
+

+ Your quick actions +

+

Jump back in

+
+
+
+ {quickActions.map((action) => ( + + ))} +
+
+ +
+
+
+

+ Personal schedule +

+

+ {personalScheduleMatches.length > 0 + ? "Coming up for you" + : "Save time slots" + } +

+
+ +
+
+ {nextMatch ? ( +
+

+ Next match +

+

{highlightTitle}

+
    + {highlightMeta.map((meta) => ( +
  • + {meta.icon && } + {meta.label} +
  • + ))} +
+
+ ) : ( +
+ No upcoming matches yet. Create one or join an open match to fill your calendar. +
+ )} + + {remainingSchedule.length > 0 && ( +
+ {remainingSchedule.map((entry) => ( +
+
+

+ {entry.membership === "hosted" ? "Hosting" : "Playing"} +

+

{entry.formatLabel || "Match"}

+
+ {entry.startLabel && ( + + + {entry.startLabel} + + )} + {entry.locationLabel && ( + + + {entry.locationLabel} + + )} + {entry.relativeTime && ( + + + {entry.relativeTime} + + )} +
+
+
+ + {entry.membership === "hosted" && ( + + )} +
+
+ ))} +
+ )} +
+
+ +
+
+
+

+ Nearby highlights +

+

Matches worth a look

+
+ +
+
+ {nearbyMatchesPreview.length > 0 ? ( + nearbyMatchesPreview.map((preview) => ( +
+
+

{preview.formatLabel || "Match"}

+
+ {preview.startLabel && ( + + + {preview.startLabel} + + )} + {preview.locationLabel && ( + + + {preview.locationLabel} + + )} + {preview.distanceLabel && ( + + + {preview.distanceLabel} + + )} +
+
+
+ {isOpenMatch(preview.match) && ( + + Open match + + )} + +
+
+ )) + ) : ( +
+ We’ll highlight nearby matches here once you set a location or new events are posted. +
+ )} +
+
+
+
- -
-
-
-

Matches Near You

-

- Browse open matches, filter by time, and jump in when a spot opens up. -

-
-
-
- + {matchesNeedingAttention.length > 0 && ( +
+
+
+ +
+
+

+ Needs players +

+

Help fill your matches

+
+
+
+ {matchesNeedingAttention.slice(0, 4).map((match) => { + const start = parseDateValue(match.dateTime); + const startLabel = start ? formatDateTime(start) : "Date TBA"; + const locationLabel = [ + match.location, + match.location_text, + match.locationText, + match.venue, + match.court_name, + match.courtName, + ] + .map((value) => (typeof value === "string" ? value.trim() : "")) + .find((value) => value); + return ( +
+

+ {match.match_format || match.matchFormat || match.format || "Match"} +

+
+ {startLabel && ( + + + {startLabel} + + )} + {locationLabel && ( + + + {locationLabel} + + )} +
+
+ + +
+
+ ); + })} +
+
+ )} + +
+ +
+
+
+
+ {filterDefinitions.map((filter) => { + const isActive = activeFilter === filter.id; + return ( + + ); + })} +
+
setMatchSearch(event.target.value)} - placeholder="Search matches..." - className="w-full rounded-2xl border-2 border-gray-200 bg-white px-10 py-2.5 text-sm font-semibold text-gray-700 focus:border-emerald-500 focus:outline-none focus:ring-2 focus:ring-emerald-500" + className="w-full rounded-full border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 shadow-sm focus:border-emerald-400 focus:outline-none focus:ring-2 focus:ring-emerald-200" />
-
- {filterOptions.map((filter) => ( - +
+ {displayedMatches.map((match) => ( + ))}
- {hasLocationFilter && displayedMatches.length === 0 && ( -
- No matches within {distanceFilter} miles yet. Expand your distance filter or check back soon! + {displayedMatches.length === 0 && ( +
+
+ {hasLocationFilter + ? `No matches within ${distanceFilter} miles just yet. Try widening your search radius or check back soon.` + : "No matches match your filters right now. Try a different filter or refresh shortly."} +
)} -
- {displayedMatches.map((match) => ( - - ))} -
- {matchPagination && !hasLocationFilter && ( -
+
- + Page {matchPagination.page} of {Math.max(1, Math.ceil(getMatchCount(activeFilter) / matchPagination.perPage))}
)} -
- -
-
-
-

Featured Coaches

-

- Sharpen your game with local pros who love Matchplay players. -

-
- -
- -
- {featuredCoaches.map((coach) => ( -
-
-
- {coach.initials} -
- - - {coach.rating} - -
-

{coach.name}

-

{coach.specialty}

-
- {coach.price} - -
-
- ))} -
-
+
); }; + const MatchCard = ({ match }) => { const isHosted = match.type === "hosted"; const isJoined = match.type === "joined"; From 2d7207d6ab04b32a49879bc628adcfac73586833 Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Wed, 29 Oct 2025 07:43:02 -0700 Subject: [PATCH 3/4] Rework browse hero actions and quick links --- src/TennisMatchApp.jsx | 269 ++++++++++++++++++++++++++++++----------- 1 file changed, 198 insertions(+), 71 deletions(-) diff --git a/src/TennisMatchApp.jsx b/src/TennisMatchApp.jsx index 493bac34..e3d5b77b 100644 --- a/src/TennisMatchApp.jsx +++ b/src/TennisMatchApp.jsx @@ -748,6 +748,8 @@ const TennisMatchApp = () => { const totalSelectedInvitees = selectedPlayers.size + manualContacts.size; const lastInviteLoadRef = useRef(null); const autoDetectAttemptedRef = useRef(false); + const locationToolsRef = useRef(null); + const matchListingsRef = useRef(null); const hydratedProfileIdsRef = useRef(new Set()); const notificationSummaryErrorLoggedRef = useRef(false); const inviteSummaryErrorLoggedRef = useRef(false); @@ -3386,12 +3388,84 @@ const TennisMatchApp = () => { }, ]; + const handleBrowseMatches = useCallback(() => { + setActiveFilter("open"); + setMatchSearch(""); + setTimeout(() => { + if (matchListingsRef.current) { + matchListingsRef.current.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + } + }, 0); + }, [matchListingsRef, setActiveFilter, setMatchSearch]); + + const handleLocationToolsFocus = useCallback(() => { + setShowLocationPicker(true); + setGeoError(""); + setTimeout(() => { + if (locationToolsRef.current) { + locationToolsRef.current.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + } + }, 0); + }, [locationToolsRef, setGeoError, setShowLocationPicker]); + + const handleFindCourts = useCallback(() => { + handleLocationToolsFocus(); + }, [handleLocationToolsFocus]); + const quickActions = [ + { + id: "browse", + label: "Browse matches", + description: "See what's open nearby and plan ahead.", + details: [ + "Filter by format, day, or skill level.", + "Save options to revisit later.", + ], + icon: Search, + onClick: handleBrowseMatches, + }, + { + id: "players", + label: "Find players", + description: "Match with partners at your pace.", + details: [ + "Search by level or availability.", + "Check shared connections before inviting.", + ], + icon: Users, + onClick: () => goToPlayers(), + }, + { + id: "ai-match", + label: "AI Match Me", + description: "Let the assistant suggest matchups.", + details: [ + "Share your availability and skill level.", + "Preview smart recommendations before joining.", + ], + icon: Sparkles, + onClick: () => { + displayToast( + "AI Match Me is coming soon. We'll surface smart suggestions here shortly.", + "info", + ); + }, + }, { id: "create", label: "Create a match", description: "Host a new meetup with your preferred format.", - icon: Sparkles, + details: [ + "Pick the format, skill range, and roster size.", + "Send invites or open it up to the community.", + ], + icon: Plus, onClick: () => { if (!currentUser) { setShowSignInModal(true); @@ -3400,30 +3474,28 @@ const TennisMatchApp = () => { } }, }, + { + id: "courts", + label: "Find courts", + description: "Dial in the best places to play.", + details: [ + "Search clubs, parks, and favorite venues.", + "Adjust your radius for the ideal drive time.", + ], + icon: MapPin, + onClick: handleFindCourts, + }, { id: "invites", label: "Review invites", description: "Confirm spots and respond to requests.", + details: [ + "See who’s waiting on your reply.", + "Accept, decline, or message hosts in one place.", + ], icon: Bell, onClick: () => goToInvites(), }, - { - id: "players", - label: "Discover players", - description: "Find partners that match your level.", - icon: Users, - onClick: () => goToPlayers(), - }, - { - id: "location", - label: hasLocationFilter ? "Adjust location" : "Set location", - description: "Tune the feed to courts near you.", - icon: MapPin, - onClick: () => { - setShowLocationPicker(true); - setGeoError(""); - }, - }, ]; const highlightTitle = (() => { @@ -3521,10 +3593,10 @@ const TennisMatchApp = () => {

Rally up for your next play date

-

- Discover nearby games, keep an eye on your schedule, and jump back into the action with a couple of taps. +

+ Plan matches, find partners, and keep tabs on what's happening in your tennis circle.

-
+
+
+
+
+ + + +
+

+ Location focus +

+

+ {hasLocationFilter ? activeLocationLabel : "All locations"} +

+

+ {hasLocationFilter + ? `Within ${distanceFilter} miles` + : "Set a home base to personalize recommendations."} +

+
+
+
+ {hasLocationFilter && ( + + )} + +
+
+
-
- Next on your schedule - {nextMatch?.membership && ( - - {nextMatch.membership === "hosted" ? "Hosting" : "Playing"} - - )} -
-
-
-

{highlightTitle}

- {nextMatch?.skillLabel && ( -

- {nextMatch.skillLabel} +

+
+
+ +
+
+

+ Personalize your feed

- )} +

Find the perfect play date

+

+ Explore upcoming matches, coordinate with teammates, and let AI matchmaking help when you need a hand. +

+
- {nextMatch ? ( -
    - {highlightMeta.map((meta) => ( -
  • - {meta.icon && } - {meta.label} -
  • - ))} -
- ) : ( -

- Add or join a match to see it spotlighted here. -

- )} -
+
+ + - {nextMatch?.membership === "hosted" && ( - - )}
@@ -3632,7 +3745,10 @@ const TennisMatchApp = () => {
-
+
@@ -3800,7 +3916,7 @@ const TennisMatchApp = () => { key={action.id} type="button" onClick={action.onClick} - className="group flex h-full flex-col justify-between rounded-3xl border border-slate-200 bg-gradient-to-br from-white to-slate-50 p-4 text-left shadow-sm transition hover:-translate-y-0.5 hover:shadow-lg" + className="group flex h-full flex-col gap-3 rounded-3xl border border-slate-200 bg-gradient-to-br from-white to-slate-50 p-4 text-left shadow-sm transition hover:-translate-y-0.5 hover:shadow-lg" >
@@ -3808,9 +3924,17 @@ const TennisMatchApp = () => {

{action.label}

-

- {action.description} -

+

{action.description}

+ {Array.isArray(action.details) && action.details.length > 0 && ( +
    + {action.details.map((detail) => ( +
  • + + {detail} +
  • + ))} +
+ )} ))}
@@ -4075,7 +4199,10 @@ const TennisMatchApp = () => {
-
+
From 2ef92d10983e89644389d6a76022ec52cc7df921 Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Wed, 29 Oct 2025 07:56:23 -0700 Subject: [PATCH 4/4] Restyle browse hero and streamline schedule --- src/TennisMatchApp.jsx | 374 ++++++++++++++++++----------------------- 1 file changed, 165 insertions(+), 209 deletions(-) diff --git a/src/TennisMatchApp.jsx b/src/TennisMatchApp.jsx index e3d5b77b..a63bc27f 100644 --- a/src/TennisMatchApp.jsx +++ b/src/TennisMatchApp.jsx @@ -3362,9 +3362,6 @@ const TennisMatchApp = () => { ]); const BrowseScreen = () => { - const nextMatch = personalScheduleMatches[0]; - const remainingSchedule = personalScheduleMatches.slice(1); - const statCards = [ { label: "Upcoming matches", @@ -3429,6 +3426,7 @@ const TennisMatchApp = () => { ], icon: Search, onClick: handleBrowseMatches, + ctaLabel: "Browse matches", }, { id: "players", @@ -3440,6 +3438,7 @@ const TennisMatchApp = () => { ], icon: Users, onClick: () => goToPlayers(), + ctaLabel: "Find players", }, { id: "ai-match", @@ -3456,6 +3455,7 @@ const TennisMatchApp = () => { "info", ); }, + ctaLabel: "Preview AI Match Me", }, { id: "create", @@ -3473,6 +3473,7 @@ const TennisMatchApp = () => { navigate("/create"); } }, + ctaLabel: "Create a match", }, { id: "courts", @@ -3484,6 +3485,7 @@ const TennisMatchApp = () => { ], icon: MapPin, onClick: handleFindCourts, + ctaLabel: "Adjust location tools", }, { id: "invites", @@ -3495,37 +3497,13 @@ const TennisMatchApp = () => { ], icon: Bell, onClick: () => goToInvites(), + ctaLabel: "View invites", }, ]; - const highlightTitle = (() => { - if (!nextMatch) return "No upcoming matches"; - const source = nextMatch.match || {}; - const pickString = (...candidates) => { - for (const candidate of candidates) { - if (typeof candidate === "string" && candidate.trim()) { - return candidate.trim(); - } - } - return "Match"; - }; - return ( - pickString( - source.title, - source.name, - source.match_format, - source.matchFormat, - source.format, - ) || "Match" - ); - })(); - - const highlightMeta = [ - nextMatch?.startLabel && { icon: Calendar, label: nextMatch.startLabel }, - nextMatch?.relativeTime && { icon: Clock, label: nextMatch.relativeTime }, - nextMatch?.locationLabel && { icon: MapPin, label: nextMatch.locationLabel }, - nextMatch?.skillLabel && { icon: Star, label: nextMatch.skillLabel }, - ].filter(Boolean); + const heroActionIds = new Set(["browse", "players", "ai-match", "courts"]); + const heroActions = quickActions.filter((action) => heroActionIds.has(action.id)); + const secondaryActions = quickActions.filter((action) => !heroActionIds.has(action.id)); const filterDefinitions = [ { @@ -3573,176 +3551,161 @@ const TennisMatchApp = () => { ]; return ( -
-
-
@@ -3962,30 +3935,13 @@ const TennisMatchApp = () => {
- {nextMatch ? ( -
-

- Next match -

-

{highlightTitle}

-
    - {highlightMeta.map((meta) => ( -
  • - {meta.icon && } - {meta.label} -
  • - ))} -
-
- ) : ( + {personalScheduleMatches.length === 0 ? (
No upcoming matches yet. Create one or join an open match to fill your calendar.
- )} - - {remainingSchedule.length > 0 && ( + ) : (
- {remainingSchedule.map((entry) => ( + {personalScheduleMatches.map((entry) => (