speedclimbing/website

View on GitHub
src/utils/api.ts

Summary

Maintainability
A
0 mins
Test Coverage
import type { Fetch } from 'types/Fetch';
import { API_URL } from './constants';
import type { Filter, FilterOption } from 'components/shared/inputs/SelectFilter.svelte';
import type { LeagueGroup } from 'types/LeagueGroup';
import type { Season } from 'types/Season';
import type { Nation } from 'types/Nation';

const paramsToUrlSearchParams = (
    params: URLSearchParams | Record<string, string | undefined>
): URLSearchParams => {
    if (params instanceof URLSearchParams) {
        return params;
    }

    Object.keys(params).forEach((key) => {
        if (params[key] === undefined || params[key] === '') delete params[key];
    });
    return new URLSearchParams(params as Record<string, string>);
};

export const fetchEndpoint = async <T>(
    fetch: Fetch,
    platform: Readonly<App.Platform> | undefined,
    path: string,
    params?: URLSearchParams | Record<string, string | undefined>
): Promise<T> => {
    if (params) {
        path += `?${paramsToUrlSearchParams(params).toString()}`;
    }

    let headers: Record<string, string> = {
        Accept: 'application/json'
    };

    if (platform?.env?.API_TOKEN) {
        headers['Authorization'] = `Bearer ${platform.env.API_TOKEN}`;
    } else if (process.env.API_TOKEN) {
        headers['Authorization'] = `Bearer ${process.env.API_TOKEN}`;
    }

    const response = await fetch(`${API_URL}/${path}`, {
        headers: headers
    });

    return response.json();
};

export const getAvailableFilters = async (
    fetch: Fetch,
    platform: Readonly<App.Platform> | undefined,
    filters: string[]
): Promise<Filter[]> => {
    return await Promise.all(
        filters.map((f) => getFilter(fetch, platform, f)).filter((f) => f !== undefined)
    );
};

type OptionsFilterCallback = (
    filter: Filter,
    params: Record<string, string>
) => { required: boolean; applicableOptions: FilterOption[] };

export const getApplicableFiltersAndParams = (
    filters: Filter[],
    url: URL,
    getApplicableFilterOptions: OptionsFilterCallback
): { applicableFilters: Filter[]; params: Record<string, string> } => {
    let params: Record<string, string> = {};
    let applicableFilters: Filter[] = [];

    for (const availableFilter of filters) {
        const { applicableFilter, value } = processFilter(
            availableFilter,
            params,
            url,
            getApplicableFilterOptions
        );

        if (!applicableFilter) {
            continue;
        }

        applicableFilters.push(applicableFilter);

        if (value) {
            params[applicableFilter.name] = value;
        }
    }

    return { applicableFilters, params };
};

const processFilter = (
    availableFilter: Filter,
    params: Record<string, string>,
    url: URL,
    getApplicableFilterOptions: OptionsFilterCallback
): {
    applicableFilter?: Filter;
    value?: string;
} => {
    const { required, applicableOptions } = getApplicableFilterOptions(availableFilter, params);

    if (applicableOptions.length === 0) {
        return {};
    }

    const applicableFilter = {
        options: applicableOptions,
        defaultText: required ? undefined : availableFilter.defaultText,
        name: availableFilter.name,
        optgroup: availableFilter.optgroup
    };

    const value = parameterFromList(
        url.searchParams.get(availableFilter.name),
        applicableOptions.map((o) => o.value),
        required
    );

    return { applicableFilter, value };
};

const parameterFromList = (
    value: string | null,
    list: string[],
    required: boolean
): string | undefined => {
    if (!value || !list.includes(value)) {
        return required ? list[0] : undefined;
    }

    return encodeURIComponent(value);
};

export const filterByOptgroup = (
    options: FilterOption[],
    optgroup: string | undefined
): FilterOption[] => {
    return options.filter((o) => !optgroup || o.optgroup === optgroup);
};

const entityFilter = {
    name: 'entity',
    options: [
        { name: 'Athletes', value: 'athlete' },
        { name: 'Competitions', value: 'competition' },
        { name: 'Naions', value: 'nation' }
    ]
};

const subjectFilter = {
    name: 'subject',
    options: [
        { name: 'Average rank', value: 'avg_rank' },
        { name: 'Points', value: 'points' },
        { name: 'Time', value: 'time' },
        { name: 'Final entry time', value: 'fet' },
        { name: 'Points and medals', value: 'points_and_medals' }
    ]
};

const genderFilter = {
    name: 'gender',
    defaultText: 'Male and Female',
    options: ['Male', 'Female'].map((v) => ({ name: v, value: v }))
};

const leagueGroupFilter = async (fetch: Fetch, platform: Readonly<App.Platform> | undefined) => ({
    name: 'league_group',
    defaultText: 'All leagues',
    optgroup: {
        defaultText: 'World wide'
    },
    options: (await fetchEndpoint<LeagueGroup[]>(fetch, platform, `/league_group`)).map(
        (league_grouop) => ({
            name: league_grouop.name,
            value: league_grouop.id,
            optgroup: league_grouop.continent
        })
    )
});

const yearFilter = async (fetch: Fetch, platform: Readonly<App.Platform> | undefined) => ({
    name: 'year',
    defaultText: 'All years',
    options: (await fetchEndpoint<Season[]>(fetch, platform, `/season`)).map((s) => ({
        name: s.year.toString(),
        value: s.year.toString()
    }))
});

const continentFilter = {
    name: 'continent',
    defaultText: 'All continents',
    options: ['Africa', 'Asia', 'Europe', 'Oceania', 'PanAmerica'].map((v) => ({
        name: v,
        value: v
    }))
};

const nationFilter = async (fetch: Fetch, platform: Readonly<App.Platform> | undefined) => ({
    name: 'nation_code_ioc',
    defaultText: 'All nations',
    optgroup: {},
    options: (await fetchEndpoint<Nation[]>(fetch, platform, '/nation')).map((nation) => ({
        name: nation.name,
        value: nation.code_ioc,
        optgroup: nation.continent
    }))
});

const getFilter = async (
    fetch: Fetch,
    platform: Readonly<App.Platform> | undefined,
    name: string
): Promise<Filter> => {
    switch (name) {
        case 'entity':
            return entityFilter;
        case 'subject':
            return subjectFilter;
        case 'gender':
            return genderFilter;
        case 'league_group':
            return await leagueGroupFilter(fetch, platform);
        case 'year':
            return await yearFilter(fetch, platform);
        case 'continent':
            return continentFilter;
        case 'nation_code_ioc':
            return await nationFilter(fetch, platform);
        default:
            throw new Error(`Unknown filter ${name}`);
    }
};