-
-
- {hasLocationFilter
- ? activeLocationLabel
- : "Showing matches from every location"}
-
+ 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 statCards = [
+ {
+ label: "Upcoming matches",
+ value: personalScheduleMatches.length,
+ icon: Calendar,
+ },
+ {
+ label: "Open nearby",
+ value: getMatchCount("open") || displayedMatches.length,
+ icon: Users,
+ },
+ {
+ label: "Invites waiting",
+ value: pendingInvites.length,
+ icon: Mail,
+ },
+ {
+ label: "Needs players",
+ value: matchesNeedingAttention.length,
+ icon: AlertCircle,
+ },
+ ];
+
+ 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,
+ ctaLabel: "Browse matches",
+ },
+ {
+ 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(),
+ ctaLabel: "Find players",
+ },
+ {
+ 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",
+ );
+ },
+ ctaLabel: "Preview AI Match Me",
+ },
+ {
+ id: "create",
+ label: "Create a match",
+ description: "Host a new meetup with your preferred format.",
+ 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);
+ } else {
+ navigate("/create");
+ }
+ },
+ ctaLabel: "Create a match",
+ },
+ {
+ 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,
+ ctaLabel: "Adjust location tools",
+ },
+ {
+ 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(),
+ ctaLabel: "View invites",
+ },
+ ];
+
+ 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 = [
+ {
+ id: "my",
+ label: "My Matches",
+ gradient: "linear-gradient(135deg, rgb(16 185 129), rgb(5 150 105))",
+ icon: "⭐",
+ },
+ {
+ id: "open",
+ label: "Open Matches",
+ gradient: "linear-gradient(135deg, rgb(56 189 248), rgb(99 102 241))",
+ icon: "🔥",
+ },
+ {
+ id: "today",
+ label: "Today",
+ gradient: "linear-gradient(135deg, rgb(245 158 11), rgb(234 88 12))",
+ icon: "📅",
+ },
+ {
+ id: "tomorrow",
+ label: "Tomorrow",
+ gradient: "linear-gradient(135deg, rgb(244 114 182), rgb(168 85 247))",
+ icon: "⏰",
+ },
+ {
+ id: "weekend",
+ label: "Weekend",
+ gradient: "linear-gradient(135deg, rgb(59 130 246), rgb(124 58 237))",
+ icon: "🎉",
+ },
+ {
+ id: "draft",
+ label: "Drafts",
+ gradient: "linear-gradient(135deg, rgb(148 163 184), rgb(100 116 139))",
+ icon: "📝",
+ },
+ {
+ id: "archived",
+ label: "Archived",
+ gradient: "linear-gradient(135deg, rgb(113 113 122), rgb(82 82 91))",
+ icon: "🗂️",
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ Welcome
+ {currentUser?.name && (
+
+ {currentUser.name.split(" ")[0]}
+
+ )}
-
{
- setShowLocationPicker((prev) => !prev);
- setGeoError("");
- }}
- className="px-4 py-2 rounded-xl bg-gray-900 text-white text-sm font-bold shadow hover:bg-gray-700 transition-colors"
- >
- {showLocationPicker
- ? "Hide location tools"
- : hasLocationFilter
- ? "Change location"
- : "Set location"}
-
-
-
- {distanceOptions.map((distance) => (
+
+ Rally up for your next play date
+
+
+ Plan matches, find partners, and keep tabs on what's happening in your tennis circle.
+
+
setDistanceFilter(distance)}
- disabled={!hasLocationFilter && distance !== distanceFilter}
- className={`px-3 py-1.5 text-sm font-bold rounded-full border transition-colors ${
- distanceFilter === distance
- ? "bg-green-500 text-white border-green-500 shadow"
- : "bg-white text-gray-600 border-gray-200 hover:border-green-400 hover:text-green-600"
- } ${
- !hasLocationFilter && distance !== distanceFilter
- ? "opacity-50 cursor-not-allowed"
- : ""
- }`}
type="button"
+ onClick={() => {
+ if (!currentUser) {
+ setShowSignInModal(true);
+ } else {
+ navigate("/create");
+ }
+ }}
+ className="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-gradient-to-r from-emerald-500 to-emerald-600 px-6 py-3 text-sm font-bold text-white shadow-md transition hover:-translate-y-0.5 hover:shadow-lg sm:w-auto sm:text-base"
>
- {distance} mi
+
+ Create match
+ goToInvites()}
+ className="inline-flex w-full items-center justify-center gap-2 rounded-2xl border border-emerald-200 bg-white px-6 py-3 text-sm font-bold text-emerald-700 shadow-sm transition hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-800 sm:w-auto sm:text-base"
+ >
+
+ View invites
+
+
+
+
+ {heroActions.map((action) => (
+
+
+
+
+
+
+
{action.label}
+
{action.description}
+
+
+ {Array.isArray(action.details) && action.details.length > 0 && (
+
+ {action.details.map((detail) => (
+
+
+ {detail}
+
+ ))}
+
+ )}
+
+
+
+ {action.ctaLabel}
+
+
+
))}
- {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 });
- setGeoError("");
- setShowLocationPicker(false);
- } else {
- setGeoError(
- "We couldn't read that location's coordinates. Try another search.",
- );
- }
- }}
- options={{
- types: ["geocode", "establishment"],
- fields: [
- "formatted_address",
- "geometry",
- "name",
- "address_components",
- ],
- }}
- />
-
+
+
+
+
+
+
+
+
+ Location focus
+
+
+ {hasLocationFilter ? activeLocationLabel : "All locations"}
+
+
+ {hasLocationFilter
+ ? `Within ${distanceFilter} miles`
+ : "Set a home base to personalize recommendations."}
+
+
+ {hasLocationFilter && (
+ {
+ setLocationFilter(null);
+ setLocationSearchTerm("");
+ setGeoError("");
+ }}
+ className="inline-flex items-center gap-2 rounded-2xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50"
+ >
+
+ Clear
+
+ )}
+
+
+ {hasLocationFilter ? "Adjust location" : "Set location"}
+
+
+
+
+
+
+ {statCards.map((card) => (
+
+
+
+
+
+
+
+ {card.label}
+
+
{card.value}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Location focus
+
+
+ {hasLocationFilter ? activeLocationLabel : "All locations"}
+
+
+ {hasLocationFilter
+ ? `Within ${distanceFilter} miles`
+ : "Set a location to prioritize nearby matches"}
+
+
+
+
+ {hasLocationFilter && (
+
{
+ setLocationFilter(null);
+ setLocationSearchTerm("");
+ setGeoError("");
+ }}
+ className="inline-flex items-center gap-2 rounded-2xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50"
+ >
+
+ Clear
+
+ )}
{
+ setShowLocationPicker((prev) => !prev);
+ setGeoError("");
+ }}
+ className="inline-flex items-center gap-2 rounded-2xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-slate-700"
>
- {isDetectingLocation ? "Detecting location..." : "Use my current location"}
+
+ {showLocationPicker ? "Hide tools" : hasLocationFilter ? "Change" : "Set location"}
-
- {hasLocationFilter && (
+
+
+
+
+ {distanceOptions.map((distance) => {
+ const isActive = distanceFilter === distance;
+ return (
+ setDistanceFilter(distance)}
+ disabled={!hasLocationFilter && !isActive}
+ className={`inline-flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold transition ${
+ isActive
+ ? "border-emerald-500 bg-emerald-500 text-white shadow"
+ : "border-slate-200 bg-white text-slate-600 hover:border-emerald-400 hover:text-emerald-600"
+ } ${
+ !hasLocationFilter && !isActive
+ ? "cursor-not-allowed opacity-50"
+ : ""
+ }`}
+ >
+ {distance} mi
+
+ );
+ })}
+
+
+ {hasLocationFilter && (
+
+ Showing matches within {distanceFilter} miles of {activeLocationLabel}.
+
+ )}
+
+ {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's coordinates. Try another search.",
+ );
+ }
+ }}
+ options={{
+ types: ["geocode", "establishment"],
+ fields: ["formatted_address", "geometry", "name", "address_components"],
+ }}
+ />
+
+
+ {isDetectingLocation ? "Detecting location..." : "Use my current location"}
+
+
{
- setLocationFilter(null);
- setLocationSearchTerm("");
setShowLocationPicker(false);
setGeoError("");
+ setLocationSearchTerm(locationFilter?.label || "");
}}
- className="px-4 py-2 rounded-xl bg-gray-100 text-gray-700 font-bold text-sm hover:bg-gray-200 transition-colors"
+ className="inline-flex items-center gap-2 rounded-2xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-slate-700"
>
- Clear location
+ Close
+
+
+ {geoError && {geoError}
}
+ {!import.meta.env.VITE_GOOGLE_API_KEY && (
+
+ Tip: Provide a Google Places API key to enable location search suggestions.
+
+ )}
+
+ )}
+
+
+
+
+
+
+ Your quick actions
+
+
Jump back in
+
+
+
+ {secondaryActions.map((action) => (
+
+
+
+
+
+
+
{action.label}
+
{action.description}
+
+
+ {Array.isArray(action.details) && action.details.length > 0 && (
+
+ {action.details.map((detail) => (
+
+
+ {detail}
+
+ ))}
+
)}
-
{
- setShowLocationPicker(false);
- setGeoError("");
- setLocationSearchTerm(locationFilter?.label || "");
- }}
- className="px-4 py-2 rounded-xl bg-gray-900 text-white font-bold text-sm hover:bg-gray-700 transition-colors"
- >
- Close
-
+
+
+
+ {action.ctaLabel}
+
+
+ ))}
+
+
+
+
+
+
+
+ Personal schedule
+
+
+ {personalScheduleMatches.length > 0
+ ? "Coming up for you"
+ : "Save time slots"
+ }
+
- {geoError && (
-
{geoError}
+
goToInvites()}
+ className="text-sm font-semibold text-emerald-600 hover:text-emerald-700"
+ >
+ View invites
+
+
+
+ {personalScheduleMatches.length === 0 ? (
+
+ No upcoming matches yet. Create one or join an open match to fill your calendar.
+
+ ) : (
+
+ {personalScheduleMatches.map((entry) => (
+
+
+
+ {entry.membership === "hosted" ? "Hosting" : "Playing"}
+
+
{entry.formatLabel || "Match"}
+
+ {entry.startLabel && (
+
+
+ {entry.startLabel}
+
+ )}
+ {entry.locationLabel && (
+
+
+ {entry.locationLabel}
+
+ )}
+ {entry.relativeTime && (
+
+
+ {entry.relativeTime}
+
+ )}
+
+
+
+ handleViewDetails(entry.match.id)}
+ className="inline-flex items-center gap-2 rounded-2xl border border-emerald-300 px-4 py-2 text-sm font-semibold text-emerald-700 transition hover:bg-emerald-100"
+ >
+ View details
+
+
+ {entry.membership === "hosted" && (
+ openInviteScreen(entry.match.id)}
+ className="inline-flex items-center gap-2 rounded-2xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-100"
+ >
+ Manage invites
+
+ )}
+
+
+ ))}
+
)}
- {!import.meta.env.VITE_GOOGLE_API_KEY && (
-
- Tip: Provide a Google Places API key to enable location search suggestions.
+
+
+
+
+
+
+
+ Nearby highlights
+
Matches worth a look
+
+
{
+ if (typeof window !== "undefined") {
+ window.scrollTo({ top: 0, behavior: "smooth" });
+ }
+ }}
+ className="text-sm font-semibold text-emerald-600 hover:text-emerald-700"
+ >
+ Jump to listings
+
+
+
+ {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
+
+ )}
+ handleViewDetails(preview.match.id)}
+ className="inline-flex items-center gap-2 rounded-2xl border border-emerald-300 px-4 py-2 text-sm font-semibold text-emerald-700 transition hover:bg-emerald-100"
+ >
+ View details
+
+
+
+ ))
+ ) : (
+
+ We’ll highlight nearby matches here once you set a location or new events are posted.
+
)}
- )}
-
-
-
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) => (
-
setActiveFilter(filter.id)}
- className={`px-5 py-3 rounded-xl text-sm font-bold whitespace-nowrap transition-all flex items-center gap-2 ${
- activeFilter === filter.id
- ? "text-white shadow-lg scale-105"
- : "bg-gray-100 text-gray-700 hover:bg-gray-200"
- }`}
- style={
- activeFilter === filter.id
- ? {
- background:
- filter.color === "violet"
- ? "linear-gradient(135deg, rgb(139 92 246), rgb(124 58 237))"
- : filter.color === "green"
- ? "linear-gradient(135deg, rgb(34 197 94), rgb(16 185 129))"
- : filter.color === "blue"
- ? "linear-gradient(135deg, rgb(59 130 246), rgb(37 99 235))"
- : filter.color === "amber"
- ? "linear-gradient(135deg, rgb(245 158 11), rgb(217 119 6))"
- : filter.color === "slate"
- ? "linear-gradient(135deg, rgb(148 163 184), rgb(100 116 139))"
- : "linear-gradient(135deg, rgb(168 85 247), rgb(147 51 234))",
- }
- : {}
- }
- >
- {filter.icon}
- {filter.label}
- {filter.count > 0 && (
-
- {filter.count}
-
- )}
-
- ))}
+
+ refreshMatchesAndInvites()}
+ onViewAll={goToInvites}
+ pendingInviteCount={pendingInvites.length}
+ unreadUpdateCount={Number(notificationSummary.unread ?? 0)}
+ />
+
+ {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}
+
+ )}
+
+
+ handleViewDetails(match.id)}
+ className="inline-flex items-center gap-2 rounded-2xl border border-amber-300 px-4 py-2 text-sm font-semibold text-amber-800 transition hover:bg-amber-100"
+ >
+ View match
+
+ openInviteScreen(match.id)}
+ className="inline-flex items-center gap-2 rounded-2xl border border-amber-200 px-4 py-2 text-sm font-semibold text-amber-700 transition hover:bg-amber-100"
+ >
+ Manage invites
+
+
+
+ );
+ })}
+
+
+ )}
+
-
-
- {/* 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"
- />
-
+
+
+
+
+ {filterDefinitions.map((filter) => {
+ const isActive = activeFilter === filter.id;
+ return (
+ setActiveFilter(filter.id)}
+ className={`flex items-center gap-2 whitespace-nowrap rounded-full px-4 py-2 text-sm font-semibold transition ${
+ isActive
+ ? "text-white shadow-lg"
+ : "bg-slate-100 text-slate-600 hover:bg-slate-200"
+ }`}
+ style={isActive ? { backgroundImage: filter.gradient } : undefined}
+ >
+ {filter.icon}
+ {filter.label}
+ {getMatchCount(filter.id) > 0 && (
+
+ {getMatchCount(filter.id)}
+
+ )}
+
+ );
+ })}
+
+
+ setMatchSearch(event.target.value)}
+ 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"
+ />
+
+
+
- {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) => (
-
- ))}
-
+ {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."}
+
+
+ )}
- {matchPagination && !hasLocationFilter && (
-
-
setMatchPage((p) => Math.max(1, p - 1))}
- disabled={matchPage === 1}
- className="w-full rounded-lg border-2 border-gray-200 px-3 py-1.5 text-sm font-bold text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
- >
- Previous
-
-
- Page {matchPagination.page} of
- {" "}
- {Math.max(
- 1,
- Math.ceil(
- getMatchCount(activeFilter) /
- matchPagination.perPage
- )
- )}
-
-
setMatchPage((p) => p + 1)}
- disabled={
- matchPagination.page >=
- Math.ceil(
- getMatchCount(activeFilter) /
- matchPagination.perPage
- )
- }
- className="w-full rounded-lg border-2 border-gray-200 px-3 py-1.5 text-sm font-bold text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
- >
- Next
-
+ {matchPagination && !hasLocationFilter && (
+
+ setMatchPage((page) => Math.max(1, page - 1))}
+ disabled={matchPage === 1}
+ className="inline-flex w-full items-center justify-center gap-2 rounded-2xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
+ >
+
+ Previous
+
+
+ Page {matchPagination.page} of {Math.max(1, Math.ceil(getMatchCount(activeFilter) / matchPagination.perPage))}
+
+ setMatchPage((page) => page + 1)}
+ disabled={
+ matchPagination.page >=
+ Math.ceil(getMatchCount(activeFilter) / matchPagination.perPage)
+ }
+ className="inline-flex w-full items-center justify-center gap-2 rounded-2xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
+ >
+ Next
+
+
+
+ )}
- )}
-
- >
- ) : (
-
-
- Sign up or log in to view available matches.
-
-
setShowSignInModal(true)}
- className="bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-3 rounded-xl font-bold shadow-lg hover:shadow-xl hover:scale-105 transition-all"
- >
- Sign Up / Log In
-
- )}
-
- );
+
+ );
+ };
+
const MatchCard = ({ match }) => {
const isHosted = match.type === "hosted";