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;
|
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;
|
||||||
|
|
|
@ -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 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;
|
||||||
|
|
|
@ -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.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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 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;
|
|
@ -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 { 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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 { 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 &&
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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}`;
|
||||||
|
}
|
Loading…
Reference in New Issue