Added leaderboard page and api support.
This commit is contained in:
parent
16a7f105f8
commit
34cd6cac2c
|
@ -1,9 +1,10 @@
|
|||
package nl.andrewlalis.gymboard_api.controller;
|
||||
|
||||
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.LeaderboardService;
|
||||
import nl.andrewlalis.gymboard_api.service.UploadService;
|
||||
import nl.andrewlalis.gymboard_api.service.submission.ExerciseSubmissionService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
@ -20,7 +21,9 @@ public class GymController {
|
|||
private final UploadService uploadService;
|
||||
private final ExerciseSubmissionService submissionService;
|
||||
|
||||
public GymController(GymService gymService, UploadService uploadService, ExerciseSubmissionService submissionService) {
|
||||
public GymController(GymService gymService,
|
||||
UploadService uploadService,
|
||||
ExerciseSubmissionService submissionService) {
|
||||
this.gymService = gymService;
|
||||
this.uploadService = uploadService;
|
||||
this.submissionService = submissionService;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ package nl.andrewlalis.gymboard_api.controller;
|
|||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import nl.andrewlalis.gymboard_api.dao.GymRepository;
|
|||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
|
||||
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 org.apache.commons.csv.CSVFormat;
|
||||
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.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
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.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.GymResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.GymRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionRepository;
|
||||
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.LoggerFactory;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
|
@ -16,7 +15,6 @@ import org.springframework.stereotype.Service;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
|
@ -46,14 +44,10 @@ public class GymService {
|
|||
query.orderBy(criteriaBuilder.desc(root.get("createdAt")));
|
||||
query.distinct(true);
|
||||
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
predicates.add(criteriaBuilder.equal(root.get("gym"), gym));
|
||||
predicates.add(criteriaBuilder.or(
|
||||
criteriaBuilder.equal(root.get("status"), ExerciseSubmission.Status.COMPLETED),
|
||||
criteriaBuilder.equal(root.get("status"), ExerciseSubmission.Status.VERIFIED)
|
||||
));
|
||||
|
||||
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
|
||||
return PredicateBuilder.and(criteriaBuilder)
|
||||
.with(criteriaBuilder.equal(root.get("gym"), gym))
|
||||
.with(criteriaBuilder.isTrue(root.get("complete")))
|
||||
.build();
|
||||
}, PageRequest.of(0, 10))
|
||||
.map(ExerciseSubmissionResponse::new)
|
||||
.toList();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.service;
|
||||
package nl.andrewlalis.gymboard_api.service.submission;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
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.ExerciseSubmissionTempFile;
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionVideoFile;
|
||||
import nl.andrewlalis.gymboard_api.service.submission.SubmissionProcessingService;
|
||||
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
|
@ -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
|
@ -1,7 +1,7 @@
|
|||
import { GeoPoint } from 'src/api/main/models';
|
||||
import SubmissionsModule, {ExerciseSubmission} from 'src/api/main/submission';
|
||||
import { api } from 'src/api/main/index';
|
||||
import {GymRoutable} from "src/router/gym-routing";
|
||||
import {GymRoutable} from 'src/router/gym-routing';
|
||||
|
||||
export interface Gym {
|
||||
countryCode: string;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import axios from 'axios';
|
||||
import GymsModule from 'src/api/main/gyms';
|
||||
import ExercisesModule from 'src/api/main/exercises';
|
||||
import LeaderboardsModule from 'src/api/main/leaderboards';
|
||||
|
||||
export const BASE_URL = 'http://localhost:8080';
|
||||
|
||||
|
@ -12,5 +13,6 @@ export const api = axios.create({
|
|||
class GymboardApi {
|
||||
public readonly gyms = new GymsModule();
|
||||
public readonly exercises = new ExercisesModule();
|
||||
public readonly leaderboards = new LeaderboardsModule();
|
||||
}
|
||||
export default new GymboardApi();
|
||||
|
|
|
@ -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;
|
|
@ -1,7 +1,7 @@
|
|||
import { SimpleGym } from 'src/api/main/gyms';
|
||||
import { Exercise } from 'src/api/main/exercises';
|
||||
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';
|
||||
|
||||
/**
|
||||
|
@ -17,7 +17,7 @@ export interface ExerciseSubmissionPayload {
|
|||
}
|
||||
|
||||
export interface ExerciseSubmission {
|
||||
id: number;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
gym: SimpleGym;
|
||||
exercise: Exercise;
|
||||
|
@ -38,12 +38,9 @@ export enum ExerciseSubmissionStatus {
|
|||
}
|
||||
|
||||
class SubmissionsModule {
|
||||
public async getSubmission(
|
||||
gym: GymRoutable,
|
||||
submissionId: number
|
||||
): Promise<ExerciseSubmission> {
|
||||
public async getSubmission(submissionId: string): Promise<ExerciseSubmission> {
|
||||
const response = await api.get(
|
||||
`/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/submissions/${submissionId}`
|
||||
`/submissions/${submissionId}`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
@ -55,15 +52,16 @@ class SubmissionsModule {
|
|||
) {
|
||||
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(
|
||||
gym: GymRoutable,
|
||||
payload: ExerciseSubmissionPayload
|
||||
): Promise<ExerciseSubmission> {
|
||||
const gymId = getGymCompoundId(gym);
|
||||
const response = await api.post(
|
||||
`/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/submissions`,
|
||||
`/gyms/${gymId}/submissions`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
|
@ -72,8 +70,9 @@ class SubmissionsModule {
|
|||
public async uploadVideoFile(gym: GymRoutable, file: File): Promise<number> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const gymId = getGymCompoundId(gym);
|
||||
const response = await api.post(
|
||||
`/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/submissions/upload`,
|
||||
`/gyms/${gymId}/submissions/upload`,
|
||||
formData,
|
||||
{
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
|
@ -84,20 +83,16 @@ class SubmissionsModule {
|
|||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public async waitUntilSubmissionProcessed(
|
||||
gym: GymRoutable,
|
||||
submissionId: number
|
||||
): Promise<ExerciseSubmission> {
|
||||
public async waitUntilSubmissionProcessed(submissionId: string): Promise<ExerciseSubmission> {
|
||||
let failureCount = 0;
|
||||
let attemptCount = 0;
|
||||
while (failureCount < 5 && attemptCount < 60) {
|
||||
await sleep(1000);
|
||||
attemptCount++;
|
||||
try {
|
||||
const response = await this.getSubmission(gym, submissionId);
|
||||
const response = await this.getSubmission(submissionId);
|
||||
failureCount = 0;
|
||||
if (
|
||||
response.status !== ExerciseSubmissionStatus.WAITING &&
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="col-xs-12 col-md-6 q-pt-md">
|
||||
<p>{{ $t('gymPage.homePage.overview') }}</p>
|
||||
<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>City: <em>{{ gym.cityName }}</em></li>
|
||||
<li>Country: <em>{{ gym.countryName }}</em></li>
|
||||
|
|
|
@ -1,10 +1,91 @@
|
|||
<template>
|
||||
<q-page>
|
||||
<h3>Leaderboards</h3>
|
||||
<p>Some text here.</p>
|
||||
<div class="q-ma-md row justify-end q-gutter-sm">
|
||||
<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>
|
||||
</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>
|
||||
|
|
|
@ -12,6 +12,7 @@ A high-level overview of the submission process is as follows:
|
|||
-->
|
||||
<template>
|
||||
<q-page v-if="gym">
|
||||
<!-- The below form contains the fields that will become part of the submission. -->
|
||||
<q-form @submit="onSubmitted">
|
||||
<SlimForm>
|
||||
<div class="row">
|
||||
|
@ -82,6 +83,9 @@ A high-level overview of the submission process is as follows:
|
|||
:disable="!submitButtonEnabled()"
|
||||
/>
|
||||
</div>
|
||||
<div class="row text-center" v-if="infoMessage">
|
||||
<p>{{ infoMessage }}</p>
|
||||
</div>
|
||||
</SlimForm>
|
||||
</q-form>
|
||||
</q-page>
|
||||
|
@ -95,6 +99,7 @@ import api from 'src/api/main';
|
|||
import { Gym } from 'src/api/main/gyms';
|
||||
import { Exercise } from 'src/api/main/exercises';
|
||||
import {useRouter} from 'vue-router';
|
||||
import { sleep } from 'src/utils';
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
|
@ -120,8 +125,8 @@ const selectedVideoFile: Ref<File | undefined> = ref<File>();
|
|||
const weightUnits = ['KG', 'LBS'];
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
gym.value = await getGymFromRoute();
|
||||
|
@ -139,7 +144,7 @@ onMounted(async () => {
|
|||
});
|
||||
|
||||
function submitButtonEnabled() {
|
||||
return selectedVideoFile.value !== undefined && validateForm();
|
||||
return selectedVideoFile.value !== undefined && !submitting.value && validateForm();
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
|
@ -149,22 +154,29 @@ function validateForm() {
|
|||
async function onSubmitted() {
|
||||
if (!selectedVideoFile.value || !gym.value) throw new Error('Invalid state.');
|
||||
submitting.value = true;
|
||||
submissionModel.value.videoId = await api.gyms.submissions.uploadVideoFile(
|
||||
gym.value,
|
||||
selectedVideoFile.value
|
||||
);
|
||||
const submission = await api.gyms.submissions.createSubmission(
|
||||
gym.value,
|
||||
submissionModel.value
|
||||
);
|
||||
const completedSubmission =
|
||||
await api.gyms.submissions.waitUntilSubmissionProcessed(
|
||||
try {
|
||||
infoMessage.value = 'Uploading video...';
|
||||
await sleep(1000);
|
||||
submissionModel.value.videoId = await api.gyms.submissions.uploadVideoFile(
|
||||
gym.value,
|
||||
submission.id
|
||||
selectedVideoFile.value
|
||||
);
|
||||
console.log(completedSubmission);
|
||||
submitting.value = false;
|
||||
await router.push(getGymRoute(gym.value));
|
||||
infoMessage.value = 'Creating submission...';
|
||||
await sleep(1000);
|
||||
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>
|
||||
|
||||
|
|
|
@ -30,3 +30,12 @@ export async function getGymFromRoute(): Promise<Gym> {
|
|||
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}`;
|
||||
}
|
Loading…
Reference in New Issue