Skip to content
Merged
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
124 changes: 44 additions & 80 deletions src/api/offstylesApi.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { Style } from '@/types/Style';
import Api from './api';
import type { Time } from '@/types/Time';
import type { Time, TimesPage } from '@/types/Time';
import type { TimesFilter } from '@/types/TimesFilter';
import type { User } from '@/types/User';
import type { RecentModAction, ModerationTargetFilter, ModerationAction } from '@/types/moderation';

// Add new interfaces based on the API spec
export interface RankAwareRecord extends Time {
rank: number;
}

export interface WRAwareRecord extends Time {
wr_time: number;
}

export interface ReturnStyle {
name: string;
s_id: number;
Expand Down Expand Up @@ -89,74 +81,36 @@ export interface ModerationLogResponse {
class OffstylesApi extends Api {
static offstylesApiUrl = "/api";

// Fixed method signature to require style parameter
static async getTimesByMap(
mapName: string,
style: number = Style.normal,
steamid?: string,
limit: number = 50,
page: number = 1,
): Promise<RankAwareRecord[]> {
const params = new URLSearchParams({
map: mapName,
style: style.toString(),
limit: limit.toString(),
page: page.toString(),
});

if (steamid) {
params.append("steamid", steamid);
}

this.url = `${this.offstylesApiUrl}/map?${params.toString()}`;
return await this.fetchFromUrl();
}

static async getTimesByPlayer(
steamID: string,
map?: string,
style: number = Style.all,
limit: number = 50,
page: number = 1,
best: boolean = false,
): Promise<WRAwareRecord[]> {
const params = new URLSearchParams({
steamid: steamID,
limit: limit.toString(),
page: page.toString(),
best: best.toString(),
});
static async getTimes(filter: TimesFilter): Promise<TimesPage> {
const params = new URLSearchParams();

if (map) {
params.append("map", map);
if (filter.map) params.append("map", filter.map);
if (filter.steamid) params.append("steamid", filter.steamid);
if (filter.style !== undefined && filter.style !== Style.all) {
params.append("style", filter.style.toString());
}

if (style !== undefined && style !== Style.all) {
params.append("style", style.toString());
if (filter.sort) params.append("sort", filter.sort);
if (filter.best !== undefined) params.append("best", filter.best.toString());
if (filter.has_replay) params.append("has_replay", "true");
if (filter.invalidated !== undefined) {
params.append("invalidated", filter.invalidated.toString());
}
if (filter.wr !== undefined) params.append("wr", filter.wr.toString());
if (filter.recent) params.append("recent", "true");
params.append("page", filter.page.toString());
params.append("limit", filter.limit.toString());

this.url = `${this.offstylesApiUrl}/times?${params.toString()}`;
return await this.fetchFromUrl();
}

static async getRecentTimes(
style: number = Style.all,
limit: number = 15,
page: number = 1,
wr: boolean = true,
): Promise<WRAwareRecord[]> {
const params = new URLSearchParams({
limit: limit.toString(),
page: page.toString(),
wr: wr.toString(),
});

if (style !== undefined && style !== Style.all) {
params.append("style", style.toString());
const response = await fetch(`${this.offstylesApiUrl}/times?${params.toString()}`);
if (!response.ok) {
const errorText = await response.text();
try {
const error: JsonError = JSON.parse(errorText);
throw new Error(`${error.code}: ${error.reason}`);
} catch {
throw new Error(`${response.status}: ${response.statusText}`);
}
}

this.url = `${this.offstylesApiUrl}/recent?${params.toString()}`;
return await this.fetchFromUrl();
return await response.json();
}

// New methods based on the API spec
Expand All @@ -178,13 +132,23 @@ class OffstylesApi extends Api {
return await this.fetchFromUrl();
}

static async getSingleTime(id: string): Promise<WRAwareRecord> {
const params = new URLSearchParams({
id: id,
});

this.url = `${this.offstylesApiUrl}/time?${params.toString()}`;
return await this.fetchFromUrl();
static async getSingleTime(id: string): Promise<Time> {
const params = new URLSearchParams({ ids: id, limit: '1', page: '1' });
const response = await fetch(`${this.offstylesApiUrl}/times?${params.toString()}`);
if (!response.ok) {
const errorText = await response.text();
try {
const error: JsonError = JSON.parse(errorText);
throw new Error(`${error.code}: ${error.reason}`);
} catch {
throw new Error(`${response.status}: ${response.statusText}`);
}
}
const page: TimesPage = await response.json();
if (page.data.length === 0) {
throw new Error('404: Record not found');
}
return page.data[0];
}

static async getStyles(): Promise<ReturnStyle[]> {
Expand Down
8 changes: 6 additions & 2 deletions src/components/CheckboxInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
const props = defineProps<{
name: string,
label: string,
default?: boolean,
}>()
const currentInput : Ref<boolean> = ref(urlParams.getAsObject().wr ? urlParams.getAsObject().wr === 'true' : true);

const initial = urlParams.getAsObject()[props.name];
const currentInput : Ref<boolean> = ref(
initial !== undefined ? initial === 'true' : (props.default ?? false)
);

watch(currentInput, async() => {
emit('checkbox-Changed', props.name, currentInput.value);
});
Expand Down
10 changes: 5 additions & 5 deletions src/components/CustomDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@
import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/vue'
import IconCheck from './icons/IconCheck.vue';
import IconChevronUpDown from './icons/IconChevronUpDown.vue';
import { Style } from '@/types/Style';
import urlParams from '@/utils/urlParams';
const emit = defineEmits(['dropdown-Changed']);

const props = defineProps<{
options: (number)[],
options: (number)[],
name: string,
format: (value: number) => string,
default: number
}>()

const currentInput : Ref<number> = ref(urlParams.getAsObject().style ? Number(urlParams.getAsObject().style) : props.default);

const initial = urlParams.getAsObject()[props.name];
const currentInput : Ref<number> = ref(initial !== undefined ? Number(initial) : props.default);

watch(currentInput, async() => {
emit('dropdown-Changed', props.name, currentInput.value);
});
Expand Down
63 changes: 42 additions & 21 deletions src/components/MapDetails.vue
Original file line number Diff line number Diff line change
@@ -1,50 +1,71 @@
<script setup lang="ts">
import TimesList from './TimesList.vue';
import TimesFilterBar from './TimesFilterBar.vue';
import dateTimeFormats from '@/utils/dateTimeFormats';
import timeLinks from '@/utils/timeLinks';
import type { Time } from '@/types/Time';
import CustomDropdown from './CustomDropdown.vue';
import styleFormat from '@/utils/styleFormat';
import type { SortOrder } from '@/types/TimesFilter';
import { Style } from "@/types/Style";
import urlParams from '@/utils/urlParams';
import { useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { computed } from 'vue';
import TimesListPagination from './TimesListPagination.vue';

const route = useRoute();
const router = useRouter();

const emit = defineEmits(['updateMap']);

const props = defineProps<{
mapName : string,
mapTimes: Time[] | null,
isLoading: boolean
isLoading: boolean,
total: number,
}>()

const dropdownChanged = async (name : string, value : number)=>{
await router.replace({query:urlParams.update(name, value)});
emit('updateMap', props.mapName);
}
const paginationChanged = async (page: number)=>{
await router.replace({query:urlParams.update('page', page)});
emit('updateMap', props.mapName);
}

const mapStyleOptions = [Style.normal, Style.sideways, Style.wonly, Style.legit_scroll, Style.half_sideways, Style.a_d_only, Style.segmented];

const currentFilter = computed(() => {
const q = route.query as Record<string, string>;
return {
style: q.style ? Number(q.style) : Style.normal,
sort: (q.sort as SortOrder) || 'Fastest',
best: q.best !== undefined ? q.best === 'true' : true,
hasReplay: q.has_replay === 'true',
invalidated: q.invalidated !== undefined ? q.invalidated === 'true' : undefined,
};
});

const filterChanged = async (name: 'style' | 'sort' | 'best' | 'has_replay' | 'invalidated', value: string | number | boolean | undefined) => {
await router.replace({ query: urlParams.updateMany({ [name]: value }) });
};

const paginationChanged = async (page: number) => {
await router.replace({ query: urlParams.update('page', page) });
};
</script>


<template>
<div class="text-white w-full max-w-[800px] p-4 text-center flex flex-col justify-center rounded-lg mt-8">
<h1 class="text-2xl mb-3">{{ mapName }}</h1>
<div class="flex py-2">
<CustomDropdown :options="[Style.normal, Style.sideways, Style.wonly, Style.legit_scroll, Style.half_sideways, Style.a_d_only, Style.segmented]"
:name="'style'" :format="styleFormat.name" :default="Style.normal" @dropdown-Changed="dropdownChanged"></CustomDropdown>
</div>
<TimesFilterBar
:styleValue="currentFilter.style"
:sort="currentFilter.sort"
:best="currentFilter.best"
:hasReplay="currentFilter.hasReplay"
:invalidated="currentFilter.invalidated"
:styleOptions="mapStyleOptions"
@filter-Changed="filterChanged"
/>
<TimesList v-if="props.mapTimes" :times="props.mapTimes" :cols="[{
label: 'Player',
data: 'name',
placement: true,
width:'25%',
alignmentClasses: 'text-left',
link: timeLinks.playerLink
},
},
{
label: 'Server',
data: 'server',
Expand All @@ -67,7 +88,7 @@
}]"
@refresh-data="() => emit('updateMap', props.mapName)"
></TimesList>
<h1 v-else-if="!props.isLoading" class="text-gray-200 mt-3">No times found for selected map & style</h1>
<TimesListPagination :limitPerPage="50" :times="props.mapTimes" :isLoading = "props.isLoading" @pagination-changed="paginationChanged"></TimesListPagination>
<h1 v-else-if="!props.isLoading" class="text-gray-200 mt-3">No times found for selected map & filters</h1>
<TimesListPagination :limitPerPage="50" :isLoading="props.isLoading" :total="props.total" @pagination-changed="paginationChanged"></TimesListPagination>
</div>
</template>
</template>
Loading