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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/api/matches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { request } from "./http";
import type { MatchesResponse } from "../types/match";

const normalizeEndpoint = (value: string) => `/${value.replace(/^\/+/, "")}`;

const matchesEndpointCandidates = (() => {
const primary = import.meta.env.VITE_PLAYER_MATCHES_ENDPOINT ?? "/player/matches";
const fallbacks = (import.meta.env.VITE_PLAYER_MATCHES_FALLBACK_ENDPOINTS ?? "")
.split(",")
.map((item) => item.trim())
.filter(Boolean);

const defaults = ["/player/upcoming_matches", "/matches", "/player/matches/list"];

const normalized = [primary, ...fallbacks, ...defaults].map(normalizeEndpoint);

return Array.from(new Set(normalized));
})();
const MATCHES_METHOD = (import.meta.env.VITE_PLAYER_MATCHES_METHOD ?? "POST").toUpperCase();

export interface GetMatchesParams {
token?: string | null;
perPage?: number;
page?: number;
signal?: AbortSignal;
/**
* Optional payload forwarded to the API for filtering/sorting server-side.
*/
filters?: Record<string, unknown>;
}

const isAuthError = (error: unknown) => {
const status = (error as { status?: number })?.status;
return status === 401 || status === 403;
};

export const getBrowseMatches = async ({
token,
perPage = 20,
page = 1,
signal,
filters = {},
}: GetMatchesParams = {}) => {
const requestOptions = {
token: token ?? undefined,
query: {
perPage,
page,
},
signal,
} satisfies Omit<GetMatchesParams, "filters" | "body"> & {
query: { perPage: number; page: number };
};

let lastError: unknown;

for (const endpoint of matchesEndpointCandidates) {
const methodsToTry = MATCHES_METHOD === "GET" ? ["GET"] : [MATCHES_METHOD, "GET"];

for (const method of methodsToTry) {
try {
return await request<MatchesResponse>(endpoint, {
...requestOptions,
method,
body: method === "GET" ? undefined : filters,
});
} catch (error) {
if (isAuthError(error)) {
throw error;
}
lastError = error;
}
}
}

throw lastError ?? new Error("Unable to load matches.");
};

export const extractMatches = (payload: MatchesResponse): Record<string, unknown>[] => {
if (Array.isArray(payload)) return payload;
if (Array.isArray(payload?.data)) return payload.data;
if (Array.isArray(payload?.results)) return payload.results;
if (Array.isArray(payload?.items)) return payload.items;
if (Array.isArray(payload?.matches)) return payload.matches;
return [];
};
61 changes: 0 additions & 61 deletions src/data/mockMatches.ts

This file was deleted.

45 changes: 45 additions & 0 deletions src/pages/BrowseMatchesPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,51 @@
gap: 20px;
}

.matches-state {
grid-column: 1 / -1;
background: #ffffff;
border: 1px solid rgba(148, 163, 184, 0.45);
border-radius: 18px;
padding: 24px;
text-align: center;
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.05);
}

.matches-state__title {
margin: 0 0 8px;
font-weight: 700;
color: var(--matches-text);
}

.matches-state__subtitle {
margin: 0;
color: var(--matches-muted);
font-size: 14px;
}

.matches-state__action {
margin-top: 12px;
padding: 10px 18px;
border-radius: 12px;
border: 1px solid rgba(15, 23, 42, 0.08);
background: linear-gradient(135deg, #22c55e, #16a34a);
color: #ffffff;
font-weight: 700;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.matches-state__action:hover,
.matches-state__action:focus-visible {
outline: none;
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
}

.matches-state--error {
border-color: rgba(239, 68, 68, 0.4);
}

.match-card {
background: var(--matches-surface);
border-radius: 20px;
Expand Down
Loading