parent
d1aa06dd95
commit
e0433753ea
|
@ -1,5 +1,6 @@
|
||||||
package nl.andrewlalis.gymboard_api.controller;
|
package nl.andrewlalis.gymboard_api.controller;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
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.ExerciseSubmissionService;
|
||||||
import nl.andrewlalis.gymboard_api.service.GymService;
|
import nl.andrewlalis.gymboard_api.service.GymService;
|
||||||
|
@ -8,6 +9,8 @@ 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;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller for accessing a particular gym.
|
* Controller for accessing a particular gym.
|
||||||
*/
|
*/
|
||||||
|
@ -29,6 +32,11 @@ public class GymController {
|
||||||
return gymService.getGym(CompoundGymId.parse(compoundId));
|
return gymService.getGym(CompoundGymId.parse(compoundId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/recent-submissions")
|
||||||
|
public List<ExerciseSubmissionResponse> getRecentSubmissions(@PathVariable String compoundId) {
|
||||||
|
return gymService.getRecentSubmissions(CompoundGymId.parse(compoundId));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(path = "/submissions")
|
@PostMapping(path = "/submissions")
|
||||||
public ExerciseSubmissionResponse createSubmission(
|
public ExerciseSubmissionResponse createSubmission(
|
||||||
@PathVariable String compoundId,
|
@PathVariable String compoundId,
|
||||||
|
@ -38,10 +46,7 @@ public class GymController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/submissions/{submissionId}")
|
@GetMapping(path = "/submissions/{submissionId}")
|
||||||
public ExerciseSubmissionResponse getSubmission(
|
public ExerciseSubmissionResponse getSubmission(@PathVariable String compoundId, @PathVariable long submissionId) {
|
||||||
@PathVariable String compoundId,
|
|
||||||
@PathVariable long submissionId
|
|
||||||
) {
|
|
||||||
return submissionService.getSubmission(CompoundGymId.parse(compoundId), submissionId);
|
return submissionService.getSubmission(CompoundGymId.parse(compoundId), submissionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,4 +57,13 @@ public class GymController {
|
||||||
) {
|
) {
|
||||||
return uploadService.handleSubmissionUpload(CompoundGymId.parse(compoundId), file);
|
return uploadService.handleSubmissionUpload(CompoundGymId.parse(compoundId), file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/submissions/{submissionId}/video")
|
||||||
|
public void getSubmissionVideo(
|
||||||
|
@PathVariable String compoundId,
|
||||||
|
@PathVariable long submissionId,
|
||||||
|
HttpServletResponse response
|
||||||
|
) {
|
||||||
|
submissionService.streamVideo(CompoundGymId.parse(compoundId), submissionId, response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,12 @@ package nl.andrewlalis.gymboard_api.dao.exercise;
|
||||||
|
|
||||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface ExerciseSubmissionRepository extends JpaRepository<ExerciseSubmission, Long> {
|
public interface ExerciseSubmissionRepository extends JpaRepository<ExerciseSubmission, Long>, JpaSpecificationExecutor<ExerciseSubmission> {
|
||||||
List<ExerciseSubmission> findAllByStatus(ExerciseSubmission.Status status);
|
List<ExerciseSubmission> findAllByStatus(ExerciseSubmission.Status status);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package nl.andrewlalis.gymboard_api.service;
|
package nl.andrewlalis.gymboard_api.service;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
|
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
|
||||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionPayload;
|
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionPayload;
|
||||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
|
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
|
||||||
|
@ -32,6 +33,7 @@ import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -78,6 +80,33 @@ public class ExerciseSubmissionService {
|
||||||
return new ExerciseSubmissionResponse(submission);
|
return new ExerciseSubmissionResponse(submission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public void streamVideo(CompoundGymId compoundId, long submissionId, HttpServletResponse response) {
|
||||||
|
// TODO: Make a faster way to stream videos, should be one DB call instead of this mess.
|
||||||
|
Gym gym = gymRepository.findByCompoundId(compoundId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
ExerciseSubmission submission = exerciseSubmissionRepository.findById(submissionId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (!submission.getGym().getId().equals(gym.getId())) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
Set<ExerciseSubmission.Status> validStatuses = Set.of(
|
||||||
|
ExerciseSubmission.Status.COMPLETED,
|
||||||
|
ExerciseSubmission.Status.VERIFIED
|
||||||
|
);
|
||||||
|
if (!validStatuses.contains(submission.getStatus())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
ExerciseSubmissionVideoFile videoFile = submissionVideoFileRepository.findBySubmission(submission)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
response.setContentType(videoFile.getFile().getMimeType());
|
||||||
|
response.setContentLengthLong(videoFile.getFile().getSize());
|
||||||
|
try {
|
||||||
|
response.getOutputStream().write(videoFile.getFile().getContent());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to write submission video file to response.", e);
|
||||||
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the creation of a new exercise submission. This involves a few steps:
|
* Handles the creation of a new exercise submission. This involves a few steps:
|
||||||
* <ol>
|
* <ol>
|
||||||
|
|
|
@ -1,24 +1,34 @@
|
||||||
package nl.andrewlalis.gymboard_api.service;
|
package nl.andrewlalis.gymboard_api.service;
|
||||||
|
|
||||||
import nl.andrewlalis.gymboard_api.controller.dto.GymResponse;
|
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.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.model.Gym;
|
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||||
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||||
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.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
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;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class GymService {
|
public class GymService {
|
||||||
private static final Logger log = LoggerFactory.getLogger(GymService.class);
|
private static final Logger log = LoggerFactory.getLogger(GymService.class);
|
||||||
|
|
||||||
private final GymRepository gymRepository;
|
private final GymRepository gymRepository;
|
||||||
|
private final ExerciseSubmissionRepository submissionRepository;
|
||||||
|
|
||||||
public GymService(GymRepository gymRepository) {
|
public GymService(GymRepository gymRepository, ExerciseSubmissionRepository submissionRepository) {
|
||||||
this.gymRepository = gymRepository;
|
this.gymRepository = gymRepository;
|
||||||
|
this.submissionRepository = submissionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
|
@ -28,5 +38,24 @@ public class GymService {
|
||||||
return new GymResponse(gym);
|
return new GymResponse(gym);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<ExerciseSubmissionResponse> getRecentSubmissions(CompoundGymId id) {
|
||||||
|
Gym gym = gymRepository.findByCompoundId(id)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
return submissionRepository.findAll((root, query, criteriaBuilder) -> {
|
||||||
|
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]));
|
||||||
|
}, PageRequest.of(0, 10))
|
||||||
|
.map(ExerciseSubmissionResponse::new)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -11,25 +11,28 @@
|
||||||
"test": "echo \"No test specified\" && exit 0"
|
"test": "echo \"No test specified\" && exit 0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
"@quasar/cli": "^2.0.0",
|
||||||
"vue-i18n": "^9.2.2",
|
|
||||||
"pinia": "^2.0.11",
|
|
||||||
"@quasar/extras": "^1.0.0",
|
"@quasar/extras": "^1.0.0",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"pinia": "^2.0.11",
|
||||||
"quasar": "^2.6.0",
|
"quasar": "^2.6.0",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-router": "^4.0.0"
|
"vue-router": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
|
||||||
"@typescript-eslint/parser": "^5.10.0",
|
|
||||||
"eslint": "^8.10.0",
|
|
||||||
"eslint-plugin-vue": "^9.0.0",
|
|
||||||
"eslint-config-prettier": "^8.1.0",
|
|
||||||
"prettier": "^2.5.1",
|
|
||||||
"@types/node": "^12.20.21",
|
|
||||||
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
|
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
|
||||||
"@quasar/app-vite": "^1.0.0",
|
"@quasar/app-vite": "^1.0.0",
|
||||||
|
"@types/leaflet": "^1.9.0",
|
||||||
|
"@types/node": "^12.20.21",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
|
"@typescript-eslint/parser": "^5.10.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
|
"eslint": "^8.10.0",
|
||||||
|
"eslint-config-prettier": "^8.1.0",
|
||||||
|
"eslint-plugin-vue": "^9.0.0",
|
||||||
|
"leaflet": "^1.9.3",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
"typescript": "^4.5.4"
|
"typescript": "^4.5.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -1,6 +1,7 @@
|
||||||
import { GeoPoint } from 'src/api/main/models';
|
import { GeoPoint } from 'src/api/main/models';
|
||||||
import SubmissionsModule 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";
|
||||||
|
|
||||||
export interface Gym {
|
export interface Gym {
|
||||||
countryCode: string;
|
countryCode: string;
|
||||||
|
@ -47,6 +48,13 @@ class GymsModule {
|
||||||
streetAddress: d.streetAddress,
|
streetAddress: d.streetAddress,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getRecentSubmissions(gym: GymRoutable): Promise<Array<ExerciseSubmission>> {
|
||||||
|
const response = await api.get(
|
||||||
|
`/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/recent-submissions`
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GymsModule;
|
export default GymsModule;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import axios, { AxiosInstance } 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 { GymRoutable } from 'src/router/gym-routing';
|
|
||||||
|
|
||||||
export const BASE_URL = 'http://localhost:8080';
|
export const BASE_URL = 'http://localhost:8080';
|
||||||
|
|
||||||
|
@ -10,39 +9,8 @@ export const api = axios.create({
|
||||||
baseURL: BASE_URL,
|
baseURL: BASE_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* The base class for all API modules.
|
|
||||||
*/
|
|
||||||
export abstract class ApiModule {
|
|
||||||
protected api: AxiosInstance;
|
|
||||||
|
|
||||||
protected constructor(api: AxiosInstance) {
|
|
||||||
this.api = api;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GymboardApi {
|
class GymboardApi {
|
||||||
public readonly gyms = new GymsModule();
|
public readonly gyms = new GymsModule();
|
||||||
public readonly exercises = new ExercisesModule();
|
public readonly exercises = new ExercisesModule();
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the URL for uploading a video file when creating an exercise submission
|
|
||||||
* for a gym.
|
|
||||||
* @param gym The gym that the submission is for.
|
|
||||||
*/
|
|
||||||
public getUploadUrl(gym: GymRoutable) {
|
|
||||||
return (
|
|
||||||
BASE_URL +
|
|
||||||
`/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/submissions/upload`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the URL at which the raw file data for the given file id can be streamed.
|
|
||||||
* @param fileId The file id.
|
|
||||||
*/
|
|
||||||
public getFileUrl(fileId: number) {
|
|
||||||
return BASE_URL + `/files/${fileId}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export default new GymboardApi();
|
export default new GymboardApi();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 } from 'src/api/main/index';
|
import {api, BASE_URL} from 'src/api/main/index';
|
||||||
import { GymRoutable } from 'src/router/gym-routing';
|
import { GymRoutable } from 'src/router/gym-routing';
|
||||||
import { sleep } from 'src/utils';
|
import { sleep } from 'src/utils';
|
||||||
|
|
||||||
|
@ -23,7 +23,9 @@ export interface ExerciseSubmission {
|
||||||
exercise: Exercise;
|
exercise: Exercise;
|
||||||
status: ExerciseSubmissionStatus;
|
status: ExerciseSubmissionStatus;
|
||||||
submitterName: string;
|
submitterName: string;
|
||||||
weight: number;
|
rawWeight: number;
|
||||||
|
weightUnit: string;
|
||||||
|
metricWeight: number;
|
||||||
reps: number;
|
reps: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +48,16 @@ class SubmissionsModule {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSubmissionVideoUrl(submission: ExerciseSubmission): string | null {
|
||||||
|
if (
|
||||||
|
submission.status !== ExerciseSubmissionStatus.COMPLETED &&
|
||||||
|
submission.status !== ExerciseSubmissionStatus.VERIFIED
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return BASE_URL + `/gyms/${submission.gym.countryCode}_${submission.gym.cityShortName}_${submission.gym.shortName}/submissions/${submission.id}/video`
|
||||||
|
}
|
||||||
|
|
||||||
public async createSubmission(
|
public async createSubmission(
|
||||||
gym: GymRoutable,
|
gym: GymRoutable,
|
||||||
payload: ExerciseSubmissionPayload
|
payload: ExerciseSubmissionPayload
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
expand-separator
|
||||||
|
:label="submission.rawWeight + ' ' + submission.weightUnit + ' x' + submission.reps + ' ' + submission.exercise.displayName"
|
||||||
|
:caption="submission.submitterName"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="text-center">
|
||||||
|
<video
|
||||||
|
:src="api.gyms.submissions.getSubmissionVideoUrl(submission)"
|
||||||
|
width="600"
|
||||||
|
loop
|
||||||
|
controls
|
||||||
|
autopictureinpicture
|
||||||
|
preload="metadata"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ExerciseSubmission} from 'src/api/main/submission';
|
||||||
|
import api from 'src/api/main';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
submission: ExerciseSubmission
|
||||||
|
}
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -10,7 +10,12 @@ export default {
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
submit: 'Submit',
|
submit: 'Submit',
|
||||||
leaderboard: 'Leaderboard',
|
leaderboard: 'Leaderboard',
|
||||||
|
homePage: {
|
||||||
|
overview: 'Overview of this gym:',
|
||||||
|
recentLifts: 'Recent Lifts'
|
||||||
|
},
|
||||||
submitPage: {
|
submitPage: {
|
||||||
|
name: 'Your Name',
|
||||||
exercise: 'Exercise',
|
exercise: 'Exercise',
|
||||||
weight: 'Weight',
|
weight: 'Weight',
|
||||||
reps: 'Repetitions',
|
reps: 'Repetitions',
|
||||||
|
|
|
@ -10,7 +10,12 @@ export default {
|
||||||
home: 'Thuis',
|
home: 'Thuis',
|
||||||
submit: 'Indienen',
|
submit: 'Indienen',
|
||||||
leaderboard: 'Scorebord',
|
leaderboard: 'Scorebord',
|
||||||
|
homePage: {
|
||||||
|
overview: 'Overzicht van dit sportschool:',
|
||||||
|
recentLifts: 'Recente liften'
|
||||||
|
},
|
||||||
submitPage: {
|
submitPage: {
|
||||||
|
name: 'Jouw naam',
|
||||||
exercise: 'Oefening',
|
exercise: 'Oefening',
|
||||||
weight: 'Gewicht',
|
weight: 'Gewicht',
|
||||||
reps: 'Repetities',
|
reps: 'Repetities',
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'leaflet';
|
||||||
|
|
||||||
|
export {};
|
|
@ -1,12 +1,74 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page>
|
<q-page v-if="gym">
|
||||||
<h3>Gym Home Page</h3>
|
<div class="row">
|
||||||
<p>Maybe put an image of the gym here?</p>
|
<div class="col-xs-12 col-md-6 q-pt-md">
|
||||||
<p>Put a description of the gym here?</p>
|
<p>{{ $t('gymPage.homePage.overview') }}</p>
|
||||||
<p>Maybe show a snapshot of some recent lifts?</p>
|
<ul>
|
||||||
|
<li>Website: <a :href="gym.websiteUrl">{{ 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>
|
||||||
|
<li>Registered at: <em>{{ gym.createdAt }}</em></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<div ref="mapContainer" style="height: 300px; width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="recentSubmissions.length > 0">
|
||||||
|
<h4 class="text-center">{{ $t('gymPage.homePage.recentLifts') }}</h4>
|
||||||
|
<q-list>
|
||||||
|
<ExerciseSubmissionListItem v-for="sub in recentSubmissions" :submission="sub" :key="sub.id"/>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import {nextTick, onMounted, ref, Ref} from 'vue';
|
||||||
|
import {ExerciseSubmission} from 'src/api/main/submission';
|
||||||
|
import api from 'src/api/main';
|
||||||
|
import {getGymFromRoute} from 'src/router/gym-routing';
|
||||||
|
import ExerciseSubmissionListItem from 'components/ExerciseSubmissionListItem.vue';
|
||||||
|
import {Gym} from 'src/api/main/gyms';
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import {Map, Marker, TileLayer} from 'leaflet';
|
||||||
|
|
||||||
|
const recentSubmissions: Ref<Array<ExerciseSubmission>> = ref([]);
|
||||||
|
const gym: Ref<Gym | undefined> = ref();
|
||||||
|
|
||||||
|
const TILE_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||||
|
const ATTRIBUTION = '© <a href="https://www.openstreetmap.org/copyright">OSM</a>';
|
||||||
|
const map: Ref<Map | undefined> = ref();
|
||||||
|
const mapContainer = ref();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
gym.value = await getGymFromRoute();
|
||||||
|
// We need to wait one tick for the main page to be loaded as a consequence of the gym being loaded.
|
||||||
|
await nextTick(() => initMap());
|
||||||
|
recentSubmissions.value = await api.gyms.getRecentSubmissions(gym.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
function initMap() {
|
||||||
|
if (!gym.value) return;
|
||||||
|
const g: Gym = gym.value;
|
||||||
|
console.log(mapContainer);
|
||||||
|
|
||||||
|
const tiles = new TileLayer(TILE_URL, { attribution: ATTRIBUTION, maxZoom: 19 });
|
||||||
|
const marker = new Marker([g.location.latitude, g.location.longitude], {
|
||||||
|
title: g.displayName,
|
||||||
|
alt: g.displayName
|
||||||
|
});
|
||||||
|
map.value = new Map(mapContainer.value, {})
|
||||||
|
.setView([g.location.latitude, g.location.longitude], 16);
|
||||||
|
|
||||||
|
tiles.addTo(map.value);
|
||||||
|
marker.addTo(map.value);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
map.value?.invalidateSize();
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
Loading…
Reference in New Issue