Added submission page and cleaned up time formatting.
This commit is contained in:
parent
7c638d066e
commit
8c2a84755d
|
@ -1,6 +1,7 @@
|
||||||
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||||
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.ExerciseSubmission;
|
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.ExerciseSubmission;
|
||||||
|
import nl.andrewlalis.gymboard_api.util.StandardDateFormatter;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ public record ExerciseSubmissionResponse(
|
||||||
public ExerciseSubmissionResponse(ExerciseSubmission submission) {
|
public ExerciseSubmissionResponse(ExerciseSubmission submission) {
|
||||||
this(
|
this(
|
||||||
submission.getId(),
|
submission.getId(),
|
||||||
submission.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
StandardDateFormatter.format(submission.getCreatedAt()),
|
||||||
new GymSimpleResponse(submission.getGym()),
|
new GymSimpleResponse(submission.getGym()),
|
||||||
new ExerciseResponse(submission.getExercise()),
|
new ExerciseResponse(submission.getExercise()),
|
||||||
submission.getVideoFileId(),
|
submission.getVideoFileId(),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||||
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||||
|
import nl.andrewlalis.gymboard_api.util.StandardDateFormatter;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ public record GymResponse (
|
||||||
gym.getCity().getCountry().getName(),
|
gym.getCity().getCountry().getName(),
|
||||||
gym.getCity().getShortName(),
|
gym.getCity().getShortName(),
|
||||||
gym.getCity().getName(),
|
gym.getCity().getName(),
|
||||||
gym.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
StandardDateFormatter.format(gym.getCreatedAt()),
|
||||||
gym.getShortName(),
|
gym.getShortName(),
|
||||||
gym.getDisplayName(),
|
gym.getDisplayName(),
|
||||||
gym.getWebsiteUrl(),
|
gym.getWebsiteUrl(),
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class User {
|
||||||
)
|
)
|
||||||
private Set<Role> roles;
|
private Set<Role> roles;
|
||||||
|
|
||||||
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, optional = false)
|
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, optional = false, fetch = FetchType.LAZY)
|
||||||
private UserPersonalDetails personalDetails;
|
private UserPersonalDetails personalDetails;
|
||||||
|
|
||||||
public User() {}
|
public User() {}
|
||||||
|
@ -49,6 +49,7 @@ public class User {
|
||||||
this.passwordHash = passwordHash;
|
this.passwordHash = passwordHash;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.roles = new HashSet<>();
|
this.roles = new HashSet<>();
|
||||||
|
this.personalDetails = new UserPersonalDetails(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
|
@ -86,4 +87,8 @@ public class User {
|
||||||
public Set<Role> getRoles() {
|
public Set<Role> getRoles() {
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserPersonalDetails getPersonalDetails() {
|
||||||
|
return personalDetails;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The API-standard formatter for date-time objects that are sent as responses
|
||||||
|
* where we need to enforce a specific format.
|
||||||
|
*/
|
||||||
|
public class StandardDateFormatter {
|
||||||
|
public static String format(LocalDateTime utcTimestamp) {
|
||||||
|
return utcTimestamp.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@
|
||||||
"@quasar/cli": "^2.0.0",
|
"@quasar/cli": "^2.0.0",
|
||||||
"@quasar/extras": "^1.0.0",
|
"@quasar/extras": "^1.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
"luxon": "^3.2.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"pinia": "^2.0.11",
|
"pinia": "^2.0.11",
|
||||||
"quasar": "^2.6.0",
|
"quasar": "^2.6.0",
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
"@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/leaflet": "^1.9.0",
|
||||||
|
"@types/luxon": "^3.2.0",
|
||||||
"@types/node": "^12.20.21",
|
"@types/node": "^12.20.21",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
"@typescript-eslint/parser": "^5.10.0",
|
"@typescript-eslint/parser": "^5.10.0",
|
||||||
|
@ -598,6 +600,12 @@
|
||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/luxon": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-lGmaGFoaXHuOLXFvuju2bfvZRqxAqkHPx9Y9IQdQABrinJJshJwfNCKV+u7rR3kJbiqfTF/NhOkcxxAFrObyaA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
|
||||||
|
@ -4237,6 +4245,14 @@
|
||||||
"yallist": "^2.0.0"
|
"yallist": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/luxon": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.25.9",
|
"version": "0.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
|
||||||
|
@ -6828,6 +6844,12 @@
|
||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/luxon": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-lGmaGFoaXHuOLXFvuju2bfvZRqxAqkHPx9Y9IQdQABrinJJshJwfNCKV+u7rR3kJbiqfTF/NhOkcxxAFrObyaA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/mime": {
|
"@types/mime": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
|
||||||
|
@ -9347,6 +9369,11 @@
|
||||||
"yallist": "^2.0.0"
|
"yallist": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"luxon": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg=="
|
||||||
|
},
|
||||||
"magic-string": {
|
"magic-string": {
|
||||||
"version": "0.25.9",
|
"version": "0.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"@quasar/cli": "^2.0.0",
|
"@quasar/cli": "^2.0.0",
|
||||||
"@quasar/extras": "^1.0.0",
|
"@quasar/extras": "^1.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
"luxon": "^3.2.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"pinia": "^2.0.11",
|
"pinia": "^2.0.11",
|
||||||
"quasar": "^2.6.0",
|
"quasar": "^2.6.0",
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
"@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/leaflet": "^1.9.0",
|
||||||
|
"@types/luxon": "^3.2.0",
|
||||||
"@types/node": "^12.20.21",
|
"@types/node": "^12.20.21",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
"@typescript-eslint/parser": "^5.10.0",
|
"@typescript-eslint/parser": "^5.10.0",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
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, parseSubmission } 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';
|
||||||
|
|
||||||
|
@ -55,7 +55,8 @@ class GymsModule {
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
`/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/recent-submissions`
|
`/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/recent-submissions`
|
||||||
);
|
);
|
||||||
return response.data;
|
const submissionObjects: Array<object> = response.data;
|
||||||
|
return submissionObjects.map(parseSubmission);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ExerciseSubmission } from 'src/api/main/submission';
|
import { ExerciseSubmission, parseSubmission } from 'src/api/main/submission';
|
||||||
import { getGymCompoundId, GymRoutable } from 'src/router/gym-routing';
|
import { getGymCompoundId, GymRoutable } from 'src/router/gym-routing';
|
||||||
import { api } from 'src/api/main/index';
|
import { api } from 'src/api/main/index';
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ class LeaderboardsModule {
|
||||||
if (params.size) requestParams.size = params.size;
|
if (params.size) requestParams.size = params.size;
|
||||||
|
|
||||||
const response = await api.get('/leaderboards', { params: requestParams });
|
const response = await api.get('/leaderboards', { params: requestParams });
|
||||||
return response.data.content;
|
return response.data.content.map(parseSubmission);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
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 } from 'src/api/main/index';
|
||||||
import { getGymCompoundId, GymRoutable } from 'src/router/gym-routing';
|
import { getGymCompoundId, GymRoutable } from 'src/router/gym-routing';
|
||||||
import { sleep } from 'src/utils';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data that's sent when creating a submission.
|
* The data that's sent when creating a submission.
|
||||||
|
@ -30,7 +30,7 @@ export class WeightUnitUtil {
|
||||||
|
|
||||||
export interface ExerciseSubmission {
|
export interface ExerciseSubmission {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: DateTime;
|
||||||
gym: SimpleGym;
|
gym: SimpleGym;
|
||||||
exercise: Exercise;
|
exercise: Exercise;
|
||||||
videoFileId: string;
|
videoFileId: string;
|
||||||
|
@ -41,12 +41,18 @@ export interface ExerciseSubmission {
|
||||||
reps: number;
|
reps: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseSubmission(data: any): ExerciseSubmission {
|
||||||
|
data.createdAt = DateTime.fromISO(data.createdAt);
|
||||||
|
console.log(data);
|
||||||
|
return data as ExerciseSubmission;
|
||||||
|
}
|
||||||
|
|
||||||
class SubmissionsModule {
|
class SubmissionsModule {
|
||||||
public async getSubmission(
|
public async getSubmission(
|
||||||
submissionId: string
|
submissionId: string
|
||||||
): Promise<ExerciseSubmission> {
|
): Promise<ExerciseSubmission> {
|
||||||
const response = await api.get(`/submissions/${submissionId}`);
|
const response = await api.get(`/submissions/${submissionId}`);
|
||||||
return response.data;
|
return parseSubmission(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createSubmission(
|
public async createSubmission(
|
||||||
|
@ -55,7 +61,7 @@ class SubmissionsModule {
|
||||||
): Promise<ExerciseSubmission> {
|
): Promise<ExerciseSubmission> {
|
||||||
const gymId = getGymCompoundId(gym);
|
const gymId = getGymCompoundId(gym);
|
||||||
const response = await api.post(`/gyms/${gymId}/submissions`, payload);
|
const response = await api.post(`/gyms/${gymId}/submissions`, payload);
|
||||||
return response.data;
|
return parseSubmission(response.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<q-expansion-item
|
<q-item clickable :to="'/submissions/' + submission.id">
|
||||||
expand-separator
|
<q-item-section>
|
||||||
:label="
|
<q-item-label>
|
||||||
submission.rawWeight +
|
{{ submission.rawWeight }} {{ WeightUnitUtil.toAbbreviation(submission.weightUnit) }}
|
||||||
' ' +
|
{{ submission.exercise.displayName }}
|
||||||
submission.weightUnit +
|
</q-item-label>
|
||||||
' x' +
|
<q-item-label caption>
|
||||||
submission.reps +
|
{{ submission.submitterName }}
|
||||||
' ' +
|
</q-item-label>
|
||||||
submission.exercise.displayName
|
</q-item-section>
|
||||||
"
|
<q-item-section side top>
|
||||||
:caption="submission.submitterName"
|
{{ submission.createdAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }}
|
||||||
>
|
</q-item-section>
|
||||||
<q-card>
|
</q-item>
|
||||||
<q-card-section class="text-center">
|
|
||||||
<video
|
|
||||||
:src="getFileUrl(submission.videoFileId)"
|
|
||||||
width="600"
|
|
||||||
loop
|
|
||||||
controls
|
|
||||||
autopictureinpicture
|
|
||||||
preload="metadata"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ExerciseSubmission } from 'src/api/main/submission';
|
import { ExerciseSubmission, WeightUnitUtil } from 'src/api/main/submission';
|
||||||
import api from 'src/api/main';
|
|
||||||
import { getFileUrl } from 'src/api/cdn';
|
import { getFileUrl } from 'src/api/cdn';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
submission: ExerciseSubmission;
|
submission: ExerciseSubmission;
|
||||||
|
|
|
@ -1,16 +1,38 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page>
|
<q-page>
|
||||||
<standard-centered-page v-if="submission">
|
<StandardCenteredPage v-if="submission">
|
||||||
<h3>Submission: {{ submission.id }}</h3>
|
<h3>
|
||||||
</standard-centered-page>
|
{{ submission.rawWeight }} {{ WeightUnitUtil.toAbbreviation(submission.weightUnit) }}
|
||||||
|
{{ submission.exercise.displayName }}
|
||||||
|
</h3>
|
||||||
|
<p>{{ submission.reps }} reps</p>
|
||||||
|
<p>by {{ submission.submitterName }}</p>
|
||||||
|
<p>At <router-link :to="getGymRoute(submission.gym)">{{ submission.gym.displayName }}</router-link></p>
|
||||||
|
<p>
|
||||||
|
{{ submission.createdAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }}
|
||||||
|
</p>
|
||||||
|
<video
|
||||||
|
:src="getFileUrl(submission.videoFileId)"
|
||||||
|
width="600"
|
||||||
|
loop
|
||||||
|
controls
|
||||||
|
autopictureinpicture
|
||||||
|
preload="metadata"
|
||||||
|
autoplay
|
||||||
|
/>
|
||||||
|
</StandardCenteredPage>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from 'src/api/main';
|
import api from 'src/api/main';
|
||||||
import { ExerciseSubmission } from 'src/api/main/submission';
|
import StandardCenteredPage from 'src/components/StandardCenteredPage.vue';
|
||||||
|
import { ExerciseSubmission, WeightUnitUtil } from 'src/api/main/submission';
|
||||||
import { onMounted, ref, Ref } from 'vue';
|
import { onMounted, ref, Ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { getFileUrl } from 'src/api/cdn';
|
||||||
|
import { getGymRoute } from 'src/router/gym-routing';
|
||||||
|
|
||||||
const submission: Ref<ExerciseSubmission | undefined> = ref();
|
const submission: Ref<ExerciseSubmission | undefined> = ref();
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
<div v-if="recentSubmissions.length > 0">
|
<div v-if="recentSubmissions.length > 0">
|
||||||
<h4 class="text-center">{{ $t('gymPage.homePage.recentLifts') }}</h4>
|
<h4 class="text-center">{{ $t('gymPage.homePage.recentLifts') }}</h4>
|
||||||
<q-list>
|
<q-list separator>
|
||||||
<ExerciseSubmissionListItem
|
<ExerciseSubmissionListItem
|
||||||
v-for="sub in recentSubmissions"
|
v-for="sub in recentSubmissions"
|
||||||
:submission="sub"
|
:submission="sub"
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
emit-value
|
emit-value
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-list>
|
<q-list separator>
|
||||||
<ExerciseSubmissionListItem
|
<ExerciseSubmissionListItem
|
||||||
v-for="sub in submissions"
|
v-for="sub in submissions"
|
||||||
:submission="sub"
|
:submission="sub"
|
||||||
|
|
Loading…
Reference in New Issue