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
2 changes: 1 addition & 1 deletion src/api/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export const API_BASE_URL =
import.meta.env.VITE_API_URL ||
DEFAULT_API_BASE_URL;

export const DEFAULT_AUTH_SCHEME = "token";
export const DEFAULT_AUTH_SCHEME = "Bearer";
14 changes: 13 additions & 1 deletion src/api/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { API_BASE_URL, DEFAULT_AUTH_SCHEME } from "./config";
import { getStoredAuthToken, normalizeAuthToken } from "../services/authToken";

export type AuthScheme = "token" | "Bearer" | (string & {});

Expand All @@ -9,6 +10,10 @@ export interface RequestQuery {
export interface RequestOptions<TBody = unknown> {
method?: string;
token?: string;
/**
* Alias for `token` used by legacy callers; if provided, `token` takes precedence.
*/
authToken?: string;
authScheme?: AuthScheme;
headers?: Record<string, string>;
query?: RequestQuery;
Expand Down Expand Up @@ -41,6 +46,8 @@ const buildQueryString = (query?: RequestQuery) => {

const normalizeAuthHeader = (token?: string, scheme: AuthScheme = DEFAULT_AUTH_SCHEME) => {
if (!token) return undefined;
const normalized = normalizeAuthToken(token, { preferScheme: scheme, defaultScheme: scheme });
if (normalized) return normalized;
if (/^\s*([A-Za-z]+)\s+/i.test(token)) {
// Token already includes scheme
return token;
Expand All @@ -61,6 +68,7 @@ export async function request<TResponse = unknown, TBody = unknown>(
{
method = "GET",
token,
authToken,
authScheme = DEFAULT_AUTH_SCHEME,
headers = {},
query,
Expand All @@ -74,7 +82,11 @@ export async function request<TResponse = unknown, TBody = unknown>(
? path
: `${API_BASE_URL}${path.startsWith("/") ? "" : "/"}${path.replace(/^\//, "")}`;
const queryString = buildQueryString(query);
const authHeader = normalizeAuthHeader(token, authScheme);
const storedToken =
token ??
authToken ??
getStoredAuthToken({ preferScheme: authScheme, defaultScheme: authScheme });
const authHeader = normalizeAuthHeader(storedToken, authScheme);

const finalHeaders: Record<string, string> = {
Accept: "application/json",
Expand Down
154 changes: 150 additions & 4 deletions src/api/matches.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { request, type RequestQuery } from "./http";
import { getStoredAuthToken, normalizeAuthToken } from "../services/authToken";

export type TruthyLike = boolean | string | number;

Expand Down Expand Up @@ -184,6 +185,13 @@ const deriveShareLink = (source: unknown): string | undefined => {
return shareCandidate;
};

const resolveAuthToken = (token?: string | null) => {
const normalized = normalizeAuthToken(token ?? getStoredAuthToken({ preferScheme: "Bearer" }), {
preferScheme: "Bearer",
});
return normalized ?? undefined;
};

const persistCreatedMatch = (response: unknown) => {
const match = extractMatchDetail(response) as Record<string, unknown> | undefined;
const matchId = deriveMatchId(match ?? response);
Expand Down Expand Up @@ -1242,7 +1250,7 @@ export const listMatches = async ({ token, signal, ...params }: ListMatchesParam
const query = buildMatchesQuery(params);
const response = await request<unknown>("/matches", {
query,
token: token ?? undefined,
token: resolveAuthToken(token),
signal,
});

Expand All @@ -1254,14 +1262,152 @@ export const listMatches = async ({ token, signal, ...params }: ListMatchesParam

export const getMatchById = async (
id: string | number,
{ token, signal, ...params }: Omit<ListMatchesParams, "page" | "perPage"> = {},
{
token,
signal,
includeHidden = false,
include_hidden,
...params
}: Omit<ListMatchesParams, "page" | "perPage"> = {},
) => {
const query = buildMatchesQuery(params);
const query = buildMatchesQuery({ includeHidden, include_hidden, ...params });
const response = await request<unknown>(`/matches/${id}`, {
query,
token: token ?? undefined,
token: resolveAuthToken(token),
signal,
});
return response;
};

export const updateMatch = async (
id: string | number,
{
startDate,
startTime,
locationText,
matchFormat,
skillLevel,
playerLimit,
notes,
token,
signal,
}: {
startDate?: string;
startTime?: string;
locationText?: string;
matchFormat?: string | null;
skillLevel?: string | number | null;
playerLimit?: number | null;
notes?: string | null;
token?: string | null;
signal?: AbortSignal;
} = {},
) => {
const startIso = startDate && startTime ? toIsoString(`${startDate}T${startTime}`) : undefined;
const payload: Record<string, unknown> = {};

if (startIso) {
payload.start_date_time = startIso;
payload.dateTime = startIso;
}

if (locationText !== undefined) {
payload.location_text = locationText;
payload.location = locationText;
}

if (matchFormat !== undefined) {
payload.match_format = matchFormat;
payload.format = matchFormat;
}

if (skillLevel !== undefined) {
payload.skill_level_min = skillLevel;
payload.skillLevel = skillLevel;
}

if (playerLimit !== undefined) {
payload.player_limit = playerLimit;
payload.playerCount = playerLimit;
}

if (notes !== undefined) {
payload.notes = notes;
}

return request(`/matches/${id}`, {
method: "PUT",
token: resolveAuthToken(token),
authScheme: "Bearer",
signal,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
};

export const joinMatch = async (
id: string | number,
{ token, signal }: { token?: string | null; signal?: AbortSignal } = {},
) =>
request(`/matches/${id}/join`, {
method: "POST",
token: resolveAuthToken(token),
authScheme: "Bearer",
signal,
});

export const leaveMatch = async (
id: string | number,
{ token, signal }: { token?: string | null; signal?: AbortSignal } = {},
) =>
request(`/matches/${id}/leave`, {
method: "POST",
token: resolveAuthToken(token),
authScheme: "Bearer",
signal,
});

export const removeMatchParticipant = async (
matchId: string | number,
playerId: string | number,
{ token, signal }: { token?: string | null; signal?: AbortSignal } = {},
) =>
request(`/matches/${matchId}/participants/${playerId}`, {
method: "DELETE",
token: resolveAuthToken(token),
authScheme: "Bearer",
signal,
});

export const sendMatchInvites = async (
matchId: string | number,
{
playerIds = [],
phoneNumbers = [],
token,
signal,
}: { playerIds?: Array<string | number>; phoneNumbers?: string[]; token?: string | null; signal?: AbortSignal } = {},
) =>
request(`/matches/${matchId}/invites`, {
method: "POST",
token: resolveAuthToken(token),
authScheme: "Bearer",
signal,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
playerIds,
phoneNumbers,
}),
});

export const getMatchShareLink = async (
id: string | number,
{ token, signal }: { token?: string | null; signal?: AbortSignal } = {},
) =>
request(`/matches/${id}/share-link`, {
method: "GET",
token: resolveAuthToken(token),
authScheme: "Bearer",
signal,
});

Loading