Added leaderboard page and api support.

This commit is contained in:
Andrew Lalis 2023-01-28 10:52:20 +01:00
parent 16a7f105f8
commit 34cd6cac2c
18 changed files with 788 additions and 535 deletions

View File

@ -1,9 +1,10 @@
package nl.andrewlalis.gymboard_api.controller; package nl.andrewlalis.gymboard_api.controller;
import nl.andrewlalis.gymboard_api.controller.dto.*; import nl.andrewlalis.gymboard_api.controller.dto.*;
import nl.andrewlalis.gymboard_api.service.ExerciseSubmissionService;
import nl.andrewlalis.gymboard_api.service.GymService; import nl.andrewlalis.gymboard_api.service.GymService;
import nl.andrewlalis.gymboard_api.service.LeaderboardService;
import nl.andrewlalis.gymboard_api.service.UploadService; import nl.andrewlalis.gymboard_api.service.UploadService;
import nl.andrewlalis.gymboard_api.service.submission.ExerciseSubmissionService;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -20,7 +21,9 @@ public class GymController {
private final UploadService uploadService; private final UploadService uploadService;
private final ExerciseSubmissionService submissionService; private final ExerciseSubmissionService submissionService;
public GymController(GymService gymService, UploadService uploadService, ExerciseSubmissionService submissionService) { public GymController(GymService gymService,
UploadService uploadService,
ExerciseSubmissionService submissionService) {
this.gymService = gymService; this.gymService = gymService;
this.uploadService = uploadService; this.uploadService = uploadService;
this.submissionService = submissionService; this.submissionService = submissionService;

View File

@ -0,0 +1,37 @@
package nl.andrewlalis.gymboard_api.controller;
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
import nl.andrewlalis.gymboard_api.service.LeaderboardService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
@RestController
@RequestMapping(path = "/leaderboards")
public class LeaderboardController {
private final LeaderboardService leaderboardService;
public LeaderboardController(LeaderboardService leaderboardService) {
this.leaderboardService = leaderboardService;
}
@GetMapping
public Page<ExerciseSubmissionResponse> getLeaderboard(
@RequestParam(name = "exercise") Optional<String> exerciseShortName,
@RequestParam(name = "gyms") Optional<String> gymCompoundIdsString,
@RequestParam(name = "t") Optional<String> timeframe,
Pageable pageable
) {
return leaderboardService.getTopSubmissions(
exerciseShortName,
gymCompoundIdsString,
timeframe,
pageable
);
}
}

View File

@ -2,7 +2,7 @@ package nl.andrewlalis.gymboard_api.controller;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse; import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
import nl.andrewlalis.gymboard_api.service.ExerciseSubmissionService; import nl.andrewlalis.gymboard_api.service.submission.ExerciseSubmissionService;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;

View File

@ -0,0 +1,33 @@
package nl.andrewlalis.gymboard_api.model;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Optional;
public enum LeaderboardTimeframe {
DAY(Duration.ofDays(1)),
WEEK(Duration.ofDays(7)),
MONTH(Duration.ofDays(30)),
YEAR(Duration.ofDays(365)),
ALL(Duration.ZERO);
private final Duration duration;
LeaderboardTimeframe(Duration duration) {
this.duration = duration;
}
public Optional<LocalDateTime> getCutoffTime(LocalDateTime now) {
if (this.duration.isZero()) return Optional.empty();
return Optional.of(now.minus(this.duration));
}
public static LeaderboardTimeframe parse(String s, LeaderboardTimeframe defaultValue) {
if (s == null || s.isBlank()) return defaultValue;
try {
return LeaderboardTimeframe.valueOf(s.toUpperCase());
} catch (IllegalArgumentException e) {
return defaultValue;
}
}
}

View File

@ -8,7 +8,7 @@ import nl.andrewlalis.gymboard_api.dao.GymRepository;
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository; import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository;
import nl.andrewlalis.gymboard_api.model.exercise.Exercise; import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission; import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
import nl.andrewlalis.gymboard_api.service.ExerciseSubmissionService; import nl.andrewlalis.gymboard_api.service.submission.ExerciseSubmissionService;
import nl.andrewlalis.gymboard_api.service.UploadService; import nl.andrewlalis.gymboard_api.service.UploadService;
import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord; import org.apache.commons.csv.CSVRecord;
@ -19,7 +19,6 @@ import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;

View File

@ -1,13 +1,12 @@
package nl.andrewlalis.gymboard_api.service; package nl.andrewlalis.gymboard_api.service;
import jakarta.persistence.criteria.Predicate;
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId; import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse; import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
import nl.andrewlalis.gymboard_api.controller.dto.GymResponse; import nl.andrewlalis.gymboard_api.controller.dto.GymResponse;
import nl.andrewlalis.gymboard_api.dao.GymRepository; import nl.andrewlalis.gymboard_api.dao.GymRepository;
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionRepository; import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionRepository;
import nl.andrewlalis.gymboard_api.model.Gym; import nl.andrewlalis.gymboard_api.model.Gym;
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission; import nl.andrewlalis.gymboard_api.util.PredicateBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
@ -16,7 +15,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
@Service @Service
@ -46,14 +44,10 @@ public class GymService {
query.orderBy(criteriaBuilder.desc(root.get("createdAt"))); query.orderBy(criteriaBuilder.desc(root.get("createdAt")));
query.distinct(true); query.distinct(true);
List<Predicate> predicates = new ArrayList<>(); return PredicateBuilder.and(criteriaBuilder)
predicates.add(criteriaBuilder.equal(root.get("gym"), gym)); .with(criteriaBuilder.equal(root.get("gym"), gym))
predicates.add(criteriaBuilder.or( .with(criteriaBuilder.isTrue(root.get("complete")))
criteriaBuilder.equal(root.get("status"), ExerciseSubmission.Status.COMPLETED), .build();
criteriaBuilder.equal(root.get("status"), ExerciseSubmission.Status.VERIFIED)
));
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
}, PageRequest.of(0, 10)) }, PageRequest.of(0, 10))
.map(ExerciseSubmissionResponse::new) .map(ExerciseSubmissionResponse::new)
.toList(); .toList();

View File

@ -0,0 +1,86 @@
package nl.andrewlalis.gymboard_api.service;
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
import nl.andrewlalis.gymboard_api.dao.GymRepository;
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository;
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionRepository;
import nl.andrewlalis.gymboard_api.model.Gym;
import nl.andrewlalis.gymboard_api.model.LeaderboardTimeframe;
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
import nl.andrewlalis.gymboard_api.util.PredicateBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
/**
* This service is responsible for the various methods of fetching submissions
* for a gym's leaderboard pages.
*/
@Service
public class LeaderboardService {
private final ExerciseSubmissionRepository submissionRepository;
private final ExerciseRepository exerciseRepository;
private final GymRepository gymRepository;
public LeaderboardService(ExerciseSubmissionRepository submissionRepository, ExerciseRepository exerciseRepository, GymRepository gymRepository) {
this.submissionRepository = submissionRepository;
this.exerciseRepository = exerciseRepository;
this.gymRepository = gymRepository;
}
@Transactional(readOnly = true)
public Page<ExerciseSubmissionResponse> getTopSubmissions(
Optional<String> exerciseShortName,
Optional<String> gymCompoundIdsString,
Optional<String> optionalTimeframe,
Pageable pageable
) {
Optional<LocalDateTime> cutoffTime = optionalTimeframe.flatMap(s ->
LeaderboardTimeframe.parse(s, LeaderboardTimeframe.DAY)
.getCutoffTime(LocalDateTime.now())
);
Optional<Exercise> optionalExercise = exerciseShortName.flatMap(exerciseRepository::findById);
List<Gym> gyms = gymCompoundIdsString.map(this::parseGymCompoundIdsString).orElse(Collections.emptyList());
return submissionRepository.findAll((root, query, criteriaBuilder) -> {
query.distinct(true);
query.orderBy(criteriaBuilder.desc(root.get("metricWeight")));
PredicateBuilder pb = PredicateBuilder.and(criteriaBuilder);
cutoffTime.ifPresent(time -> pb.with(criteriaBuilder.greaterThan(root.get("createdAt"), time)));
optionalExercise.ifPresent(exercise -> pb.with(criteriaBuilder.equal(root.get("exercise"), exercise)));
if (!gyms.isEmpty()) {
PredicateBuilder or = PredicateBuilder.or(criteriaBuilder);
for (Gym gym : gyms) {
or.with(criteriaBuilder.equal(root.get("gym"), gym));
}
pb.with(or.build());
}
return pb.build();
}, pageable).map(ExerciseSubmissionResponse::new);
}
private List<Gym> parseGymCompoundIdsString(String s) {
if (s == null || s.isBlank()) return Collections.emptyList();
String[] ids = s.split(",");
List<Gym> gyms = new ArrayList<>(ids.length);
for (String compoundId : ids) {
try {
CompoundGymId id = CompoundGymId.parse(compoundId);
gymRepository.findByCompoundId(id).ifPresent(gyms::add);
} catch (ResponseStatusException ignored) {}
}
return gyms;
}
}

View File

@ -1,4 +1,4 @@
package nl.andrewlalis.gymboard_api.service; package nl.andrewlalis.gymboard_api.service.submission;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId; import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
@ -14,7 +14,6 @@ import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission; import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionTempFile; import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionTempFile;
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionVideoFile; import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionVideoFile;
import nl.andrewlalis.gymboard_api.service.submission.SubmissionProcessingService;
import nl.andrewlalis.gymboard_api.util.ULID; import nl.andrewlalis.gymboard_api.util.ULID;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;

View File

@ -0,0 +1,45 @@
package nl.andrewlalis.gymboard_api.util;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
public class PredicateBuilder {
private enum Type {
AND,
OR
}
private final Type type;
private final CriteriaBuilder criteriaBuilder;
private final List<Predicate> predicates;
public PredicateBuilder(Type type, CriteriaBuilder cb) {
this.type = type;
this.criteriaBuilder = cb;
this.predicates = new ArrayList<>();
}
public PredicateBuilder with(Predicate predicate) {
this.predicates.add(predicate);
return this;
}
public Predicate build() {
Predicate[] predicatesArray = predicates.toArray(new Predicate[0]);
return switch (type) {
case OR -> this.criteriaBuilder.or(predicatesArray);
case AND -> this.criteriaBuilder.and(predicatesArray);
};
}
public static PredicateBuilder and(CriteriaBuilder cb) {
return new PredicateBuilder(Type.AND, cb);
}
public static PredicateBuilder or(CriteriaBuilder cb) {
return new PredicateBuilder(Type.OR, cb);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
import { GeoPoint } from 'src/api/main/models'; import { GeoPoint } from 'src/api/main/models';
import SubmissionsModule, {ExerciseSubmission} from 'src/api/main/submission'; import SubmissionsModule, {ExerciseSubmission} from 'src/api/main/submission';
import { api } from 'src/api/main/index'; import { api } from 'src/api/main/index';
import {GymRoutable} from "src/router/gym-routing"; import {GymRoutable} from 'src/router/gym-routing';
export interface Gym { export interface Gym {
countryCode: string; countryCode: string;

View File

@ -1,6 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import GymsModule from 'src/api/main/gyms'; import GymsModule from 'src/api/main/gyms';
import ExercisesModule from 'src/api/main/exercises'; import ExercisesModule from 'src/api/main/exercises';
import LeaderboardsModule from 'src/api/main/leaderboards';
export const BASE_URL = 'http://localhost:8080'; export const BASE_URL = 'http://localhost:8080';
@ -12,5 +13,6 @@ export const api = axios.create({
class GymboardApi { class GymboardApi {
public readonly gyms = new GymsModule(); public readonly gyms = new GymsModule();
public readonly exercises = new ExercisesModule(); public readonly exercises = new ExercisesModule();
public readonly leaderboards = new LeaderboardsModule();
} }
export default new GymboardApi(); export default new GymboardApi();

View File

@ -0,0 +1,51 @@
import { ExerciseSubmission } from 'src/api/main/submission';
import { getGymCompoundId, GymRoutable } from 'src/router/gym-routing';
import { api } from 'src/api/main/index';
export enum LeaderboardTimeframe {
DAY = "DAY",
WEEK = "WEEK",
MONTH = "MONTH",
YEAR = "YEAR",
ALL = "ALL"
}
export interface LeaderboardParams {
exerciseShortName?: string;
gyms?: Array<GymRoutable>;
timeframe?: LeaderboardTimeframe;
page?: number;
size?: number;
}
interface RequestParams {
exercise?: string;
gyms?: string;
t?: string;
page?: number;
size?: number;
}
class LeaderboardsModule {
public async getLeaderboard(params: LeaderboardParams): Promise<Array<ExerciseSubmission>> {
const requestParams: RequestParams = {};
if (params.exerciseShortName) {
requestParams.exercise = params.exerciseShortName;
}
if (params.gyms) {
requestParams.gyms = params.gyms
.map(gym => getGymCompoundId(gym))
.join(',');
}
if (params.timeframe) {
requestParams.t = params.timeframe;
}
if (params.page) requestParams.page = params.page;
if (params.size) requestParams.size = params.size;
const response = await api.get('/leaderboards', { params: requestParams });
return response.data.content;
}
}
export default LeaderboardsModule;

View File

@ -1,7 +1,7 @@
import { SimpleGym } from 'src/api/main/gyms'; import { SimpleGym } from 'src/api/main/gyms';
import { Exercise } from 'src/api/main/exercises'; import { Exercise } from 'src/api/main/exercises';
import {api, BASE_URL} from 'src/api/main/index'; import {api, BASE_URL} from 'src/api/main/index';
import { GymRoutable } from 'src/router/gym-routing'; import { getGymCompoundId, GymRoutable } from 'src/router/gym-routing';
import { sleep } from 'src/utils'; import { sleep } from 'src/utils';
/** /**
@ -17,7 +17,7 @@ export interface ExerciseSubmissionPayload {
} }
export interface ExerciseSubmission { export interface ExerciseSubmission {
id: number; id: string;
createdAt: string; createdAt: string;
gym: SimpleGym; gym: SimpleGym;
exercise: Exercise; exercise: Exercise;
@ -38,12 +38,9 @@ export enum ExerciseSubmissionStatus {
} }
class SubmissionsModule { class SubmissionsModule {
public async getSubmission( public async getSubmission(submissionId: string): Promise<ExerciseSubmission> {
gym: GymRoutable,
submissionId: number
): Promise<ExerciseSubmission> {
const response = await api.get( const response = await api.get(
`/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/submissions/${submissionId}` `/submissions/${submissionId}`
); );
return response.data; return response.data;
} }
@ -55,15 +52,16 @@ class SubmissionsModule {
) { ) {
return null; return null;
} }
return BASE_URL + `/gyms/${submission.gym.countryCode}_${submission.gym.cityShortName}_${submission.gym.shortName}/submissions/${submission.id}/video` return BASE_URL + `/submissions/${submission.id}/video`
} }
public async createSubmission( public async createSubmission(
gym: GymRoutable, gym: GymRoutable,
payload: ExerciseSubmissionPayload payload: ExerciseSubmissionPayload
): Promise<ExerciseSubmission> { ): Promise<ExerciseSubmission> {
const gymId = getGymCompoundId(gym);
const response = await api.post( const response = await api.post(
`/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/submissions`, `/gyms/${gymId}/submissions`,
payload payload
); );
return response.data; return response.data;
@ -72,8 +70,9 @@ class SubmissionsModule {
public async uploadVideoFile(gym: GymRoutable, file: File): Promise<number> { public async uploadVideoFile(gym: GymRoutable, file: File): Promise<number> {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
const gymId = getGymCompoundId(gym);
const response = await api.post( const response = await api.post(
`/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/submissions/upload`, `/gyms/${gymId}/submissions/upload`,
formData, formData,
{ {
headers: { 'Content-Type': 'multipart/form-data' }, headers: { 'Content-Type': 'multipart/form-data' },
@ -84,20 +83,16 @@ class SubmissionsModule {
/** /**
* Asynchronous method that waits until a submission is done processing. * Asynchronous method that waits until a submission is done processing.
* @param gym The gym that the submission is for.
* @param submissionId The submission's id. * @param submissionId The submission's id.
*/ */
public async waitUntilSubmissionProcessed( public async waitUntilSubmissionProcessed(submissionId: string): Promise<ExerciseSubmission> {
gym: GymRoutable,
submissionId: number
): Promise<ExerciseSubmission> {
let failureCount = 0; let failureCount = 0;
let attemptCount = 0; let attemptCount = 0;
while (failureCount < 5 && attemptCount < 60) { while (failureCount < 5 && attemptCount < 60) {
await sleep(1000); await sleep(1000);
attemptCount++; attemptCount++;
try { try {
const response = await this.getSubmission(gym, submissionId); const response = await this.getSubmission(submissionId);
failureCount = 0; failureCount = 0;
if ( if (
response.status !== ExerciseSubmissionStatus.WAITING && response.status !== ExerciseSubmissionStatus.WAITING &&

View File

@ -4,7 +4,7 @@
<div class="col-xs-12 col-md-6 q-pt-md"> <div class="col-xs-12 col-md-6 q-pt-md">
<p>{{ $t('gymPage.homePage.overview') }}</p> <p>{{ $t('gymPage.homePage.overview') }}</p>
<ul> <ul>
<li>Website: <a :href="gym.websiteUrl">{{ gym.websiteUrl }}</a></li> <li v-if="gym.websiteUrl">Website: <a :href="gym.websiteUrl" target="_blank">{{ gym.websiteUrl }}</a></li>
<li>Address: <em>{{ gym.streetAddress }}</em></li> <li>Address: <em>{{ gym.streetAddress }}</em></li>
<li>City: <em>{{ gym.cityName }}</em></li> <li>City: <em>{{ gym.cityName }}</em></li>
<li>Country: <em>{{ gym.countryName }}</em></li> <li>Country: <em>{{ gym.countryName }}</em></li>

View File

@ -1,10 +1,91 @@
<template> <template>
<q-page> <q-page>
<h3>Leaderboards</h3> <div class="q-ma-md row justify-end q-gutter-sm">
<p>Some text here.</p> <q-spinner
color="primary"
size="3em"
v-if="loadingIndicatorActive"
/>
<q-select
v-model="selectedExercise"
:options="exerciseOptions"
:disable="loadingIndicatorActive"
map-options
emit-value
/>
<q-select
v-model="selectedTimeframe"
:options="timeframeOptions"
:disable="loadingIndicatorActive"
map-options
emit-value
/>
</div>
<q-list>
<ExerciseSubmissionListItem v-for="sub in submissions" :submission="sub" :key="sub.id"/>
</q-list>
</q-page> </q-page>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import api from 'src/api/main';
import { Exercise } from 'src/api/main/exercises';
import { Gym } from 'src/api/main/gyms';
import { LeaderboardTimeframe } from 'src/api/main/leaderboards';
import { ExerciseSubmission } from 'src/api/main/submission';
import ExerciseSubmissionListItem from 'src/components/ExerciseSubmissionListItem.vue';
import { getGymFromRoute } from 'src/router/gym-routing';
import { sleep } from 'src/utils';
import { onMounted, ref, Ref, watch, computed } from 'vue';
const submissions: Ref<Array<ExerciseSubmission>> = ref([]);
const gym: Ref<Gym | undefined> = ref();
const exercises: Ref<Array<Exercise>> = ref([]);
const exerciseOptions = computed(() => {
let options = exercises.value.map(exercise => {
return {
value: exercise.shortName,
label: exercise.displayName
};
});
options.push({ value: '', label: 'Any' });
return options;
});
const selectedExercise: Ref<string> = ref('');
const timeframeOptions = [
{ value: LeaderboardTimeframe.DAY, label: 'Day' },
{ value: LeaderboardTimeframe.WEEK, label: 'Week' },
{ value: LeaderboardTimeframe.MONTH, label: 'Month' },
{ value: LeaderboardTimeframe.YEAR, label: 'Year' },
{ value: LeaderboardTimeframe.ALL, label: 'All' },
];
const selectedTimeframe: Ref<LeaderboardTimeframe> = ref(LeaderboardTimeframe.DAY);
const loadingIndicatorActive = ref(false);
onMounted(async () => {
gym.value = await getGymFromRoute();
exercises.value = await api.exercises.getExercises();
doSearch();
});
async function doSearch() {
submissions.value = [];
if (gym.value) {
loadingIndicatorActive.value = true;
await sleep(500);
submissions.value = await api.leaderboards.getLeaderboard({
timeframe: selectedTimeframe.value,
gyms: [gym.value],
exerciseShortName: selectedExercise.value
});
loadingIndicatorActive.value = false;
}
}
watch([selectedTimeframe, selectedExercise], doSearch);
</script>
<style scoped></style> <style scoped></style>

View File

@ -12,6 +12,7 @@ A high-level overview of the submission process is as follows:
--> -->
<template> <template>
<q-page v-if="gym"> <q-page v-if="gym">
<!-- The below form contains the fields that will become part of the submission. -->
<q-form @submit="onSubmitted"> <q-form @submit="onSubmitted">
<SlimForm> <SlimForm>
<div class="row"> <div class="row">
@ -82,6 +83,9 @@ A high-level overview of the submission process is as follows:
:disable="!submitButtonEnabled()" :disable="!submitButtonEnabled()"
/> />
</div> </div>
<div class="row text-center" v-if="infoMessage">
<p>{{ infoMessage }}</p>
</div>
</SlimForm> </SlimForm>
</q-form> </q-form>
</q-page> </q-page>
@ -95,6 +99,7 @@ import api from 'src/api/main';
import { Gym } from 'src/api/main/gyms'; import { Gym } from 'src/api/main/gyms';
import { Exercise } from 'src/api/main/exercises'; import { Exercise } from 'src/api/main/exercises';
import {useRouter} from 'vue-router'; import {useRouter} from 'vue-router';
import { sleep } from 'src/utils';
interface Option { interface Option {
value: string; value: string;
@ -120,8 +125,8 @@ const selectedVideoFile: Ref<File | undefined> = ref<File>();
const weightUnits = ['KG', 'LBS']; const weightUnits = ['KG', 'LBS'];
const submitting = ref(false); const submitting = ref(false);
const infoMessage: Ref<string | undefined> = ref();
// TODO: Make it possible to pass the gym to this via props instead.
onMounted(async () => { onMounted(async () => {
try { try {
gym.value = await getGymFromRoute(); gym.value = await getGymFromRoute();
@ -139,7 +144,7 @@ onMounted(async () => {
}); });
function submitButtonEnabled() { function submitButtonEnabled() {
return selectedVideoFile.value !== undefined && validateForm(); return selectedVideoFile.value !== undefined && !submitting.value && validateForm();
} }
function validateForm() { function validateForm() {
@ -149,22 +154,29 @@ function validateForm() {
async function onSubmitted() { async function onSubmitted() {
if (!selectedVideoFile.value || !gym.value) throw new Error('Invalid state.'); if (!selectedVideoFile.value || !gym.value) throw new Error('Invalid state.');
submitting.value = true; submitting.value = true;
submissionModel.value.videoId = await api.gyms.submissions.uploadVideoFile( try {
gym.value, infoMessage.value = 'Uploading video...';
selectedVideoFile.value await sleep(1000);
); submissionModel.value.videoId = await api.gyms.submissions.uploadVideoFile(
const submission = await api.gyms.submissions.createSubmission(
gym.value,
submissionModel.value
);
const completedSubmission =
await api.gyms.submissions.waitUntilSubmissionProcessed(
gym.value, gym.value,
submission.id selectedVideoFile.value
); );
console.log(completedSubmission); infoMessage.value = 'Creating submission...';
submitting.value = false; await sleep(1000);
await router.push(getGymRoute(gym.value)); const submission = await api.gyms.submissions.createSubmission(
gym.value,
submissionModel.value
);
infoMessage.value = 'Submission processing...';
const completedSubmission =
await api.gyms.submissions.waitUntilSubmissionProcessed(submission.id);
console.log(completedSubmission);
infoMessage.value = 'Submission complete!';
await router.push(getGymRoute(gym.value));
} finally {
submitting.value = false;
}
} }
</script> </script>

View File

@ -30,3 +30,12 @@ export async function getGymFromRoute(): Promise<Gym> {
route.params.gymShortName as string route.params.gymShortName as string
); );
} }
/**
* Gets the compound id for a gym; that is, the universally unique identifier
* that can be used for certain API requests.
* @param gym The gym to get the compound id for.
*/
export function getGymCompoundId(gym: GymRoutable): string {
return `${gym.countryCode}_${gym.cityShortName}_${gym.shortName}`;
}