Added submission page and cleaned up time formatting.

This commit is contained in:
Andrew Lalis 2023-02-05 12:48:30 +01:00
parent 7c638d066e
commit 8c2a84755d
13 changed files with 114 additions and 46 deletions

View File

@ -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(),

View File

@ -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(),

View File

@ -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;
}
} }

View File

@ -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);
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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);
} }
} }

View File

@ -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);
} }
} }

View File

@ -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);
} }
} }

View File

@ -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 }}&nbsp;{{ 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;

View File

@ -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 }}&nbsp;{{ 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();

View File

@ -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"

View File

@ -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"