Updated front-end to follow updated APIs.

This commit is contained in:
Andrew Lalis 2023-02-03 22:12:26 +01:00
parent 67504d0883
commit 1134eef3ad
23 changed files with 134 additions and 186 deletions

View File

@ -44,9 +44,9 @@ public class GymService {
query.orderBy(criteriaBuilder.desc(root.get("createdAt")));
query.distinct(true);
// TODO: Filter to only verified submissions.
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)

View File

@ -111,9 +111,7 @@ module.exports = configure(function (ctx) {
// directives: [],
// Quasar plugins
plugins: [
'Notify'
],
plugins: ['Notify'],
},
// animations: 'all', // --- includes all animations

View File

@ -41,30 +41,49 @@ class AuthModule {
clearTimeout(this.tokenRefreshTimer);
}
public async register(payload: UserCreationPayload) {
public async register(payload: UserCreationPayload): Promise<User> {
const response = await api.post('/auth/register', payload);
console.log(response);
}
public async activateUser(code: string): Promise<User> {
const response = await api.post('/auth/activate', {code: code});
return response.data;
}
private async fetchNewToken(credentials: TokenCredentials): Promise<string> {
public async activateUser(code: string): Promise<User> {
const response = await api.post('/auth/activate', { code: code });
return response.data;
}
public async fetchNewToken(credentials: TokenCredentials): Promise<string> {
const response = await api.post('/auth/token', credentials);
return response.data.token;
}
private async refreshToken(authStore: AuthStoreType) {
public async refreshToken(authStore: AuthStoreType) {
const response = await api.get('/auth/token', authStore.axiosConfig);
authStore.token = response.data.token;
}
private async fetchMyUser(authStore: AuthStoreType): Promise<User> {
public async fetchMyUser(authStore: AuthStoreType): Promise<User> {
const response = await api.get('/auth/me', authStore.axiosConfig);
return response.data;
}
public async updatePassword(newPassword: string, authStore: AuthStoreType) {
await api.post(
'/auth/me/password',
{ newPassword: newPassword },
authStore.axiosConfig
);
}
public async generatePasswordResetCode(email: string) {
await api.get('/auth/reset-password', { params: { email: email } });
}
public async resetPassword(resetCode: string, newPassword: string) {
await api.post('/auth/reset-password', {
code: resetCode,
newPassword: newPassword,
});
}
}
export default AuthModule;

View File

@ -13,7 +13,7 @@ export interface ExerciseSubmissionPayload {
weight: number;
weightUnit: string;
reps: number;
videoId: number;
videoFileId: string;
}
export interface ExerciseSubmission {
@ -21,7 +21,7 @@ export interface ExerciseSubmission {
createdAt: string;
gym: SimpleGym;
exercise: Exercise;
status: ExerciseSubmissionStatus;
videoFileId: string;
submitterName: string;
rawWeight: number;
weightUnit: string;
@ -29,14 +29,6 @@ export interface ExerciseSubmission {
reps: number;
}
export enum ExerciseSubmissionStatus {
WAITING = 'WAITING',
PROCESSING = 'PROCESSING',
FAILED = 'FAILED',
COMPLETED = 'COMPLETED',
VERIFIED = 'VERIFIED',
}
class SubmissionsModule {
public async getSubmission(
submissionId: string
@ -45,16 +37,6 @@ class SubmissionsModule {
return response.data;
}
public getSubmissionVideoUrl(submission: ExerciseSubmission): string | null {
if (
submission.status !== ExerciseSubmissionStatus.COMPLETED &&
submission.status !== ExerciseSubmissionStatus.VERIFIED
) {
return null;
}
return BASE_URL + `/submissions/${submission.id}/video`;
}
public async createSubmission(
gym: GymRoutable,
payload: ExerciseSubmissionPayload
@ -63,49 +45,6 @@ class SubmissionsModule {
const response = await api.post(`/gyms/${gymId}/submissions`, payload);
return response.data;
}
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/${gymId}/submissions/upload`,
formData,
{
headers: { 'Content-Type': 'multipart/form-data' },
}
);
return response.data.id as number;
}
/**
* Asynchronous method that waits until a submission is done processing.
* @param submissionId The submission's id.
*/
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(submissionId);
failureCount = 0;
if (
response.status !== ExerciseSubmissionStatus.WAITING &&
response.status !== ExerciseSubmissionStatus.PROCESSING
) {
return response;
}
} catch (error) {
console.log(error);
failureCount++;
}
}
throw new Error('Failed to wait for submission to complete.');
}
}
export default SubmissionsModule;

View File

@ -1,5 +1,5 @@
import {boot} from 'quasar/wrappers';
import {createI18n} from 'vue-i18n';
import { boot } from 'quasar/wrappers';
import { createI18n } from 'vue-i18n';
import messages from 'src/i18n';

View File

@ -29,7 +29,7 @@
<script setup lang="ts">
import { useAuthStore } from 'stores/auth-store';
import api from 'src/api/main';
import {useRoute, useRouter} from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
const authStore = useAuthStore();
const route = useRoute();
@ -39,8 +39,8 @@ async function goToLoginPage() {
await router.push({
path: '/login',
query: {
next: encodeURIComponent(route.path)
}
next: encodeURIComponent(route.path),
},
});
}
</script>

View File

@ -15,7 +15,7 @@
<q-card>
<q-card-section class="text-center">
<video
:src="api.gyms.submissions.getSubmissionVideoUrl(submission)"
:src="getFileUrl(submission.videoFileId)"
width="600"
loop
controls
@ -30,6 +30,7 @@
<script setup lang="ts">
import { ExerciseSubmission } from 'src/api/main/submission';
import api from 'src/api/main';
import { getFileUrl } from 'src/api/cdn';
interface Props {
submission: ExerciseSubmission;

View File

@ -25,7 +25,7 @@ const i18n = useI18n({ useScope: 'global' });
const localeOptions = [
{ value: 'en-US', label: 'English' },
{ value: 'nl-NL', label: 'Nederlands' },
{ value: 'de', label: 'Deutsch' }
{ value: 'de', label: 'Deutsch' },
];
</script>

View File

@ -1,28 +1,27 @@
export default {
mainLayout: {
language: 'Sprache',
pages: 'Seiten',
mainLayout: {
language: 'Sprache',
pages: 'Seiten',
},
indexPage: {
searchHint: 'Suche nach einem Gym',
},
gymPage: {
home: 'Home',
submit: 'Einreichen',
leaderboard: 'Bestenliste',
homePage: {
overview: 'Überblick über dieses Fitnessstudio:',
recentLifts: 'Letzten Aufzüge',
},
indexPage: {
searchHint: 'Suche nach einem Gym',
},
gymPage: {
home: 'Home',
submitPage: {
name: 'Dein Name',
exercise: 'Übung',
weight: 'Gewicht',
reps: 'Wiederholungen',
date: 'Datum',
upload: 'Videodatei zum Hochladen',
submit: 'Einreichen',
leaderboard: 'Bestenliste',
homePage: {
overview: 'Überblick über dieses Fitnessstudio:',
recentLifts: 'Letzten Aufzüge'
},
submitPage: {
name: 'Dein Name',
exercise: 'Übung',
weight: 'Gewicht',
reps: 'Wiederholungen',
date: 'Datum',
upload: 'Videodatei zum Hochladen',
submit: 'Einreichen',
},
},
};
},
};

View File

@ -9,14 +9,14 @@ export default {
email: 'Email',
password: 'Password',
register: 'Register',
error: 'An error occurred.'
error: 'An error occurred.',
},
loginPage: {
title: 'Login to Gymboard',
email: 'Email',
password: 'Password',
logIn: 'Log in',
createAccount: 'Create an account'
createAccount: 'Create an account',
},
indexPage: {
searchHint: 'Search for a Gym',
@ -41,6 +41,6 @@ export default {
},
accountMenuItem: {
logIn: 'Login',
logOut: 'Log out'
}
logOut: 'Log out',
},
};

View File

@ -5,5 +5,5 @@ import de from './de';
export default {
'en-US': enUS,
'nl-NL': nlNL,
'de': de,
de: de,
};

View File

@ -9,14 +9,14 @@ export default {
email: 'E-mail',
password: 'Wachtwoord',
register: 'Registreren',
error: 'Er is een fout opgetreden.'
error: 'Er is een fout opgetreden.',
},
loginPage: {
title: 'Inloggen bij Gymboard',
email: 'E-mail',
password: 'Wachtwoord',
logIn: 'Inloggen',
createAccount: 'Account aanmaken'
createAccount: 'Account aanmaken',
},
indexPage: {
searchHint: 'Zoek een sportschool',
@ -41,6 +41,6 @@ export default {
},
accountMenuItem: {
logIn: 'Inloggen',
logOut: 'Uitloggen'
}
logOut: 'Uitloggen',
},
};

View File

@ -2,7 +2,10 @@
<StandardCenteredPage>
<h3 class="text-center">About Gymboard</h3>
<p>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Error fugit quia laboriosam eaque? Deserunt, accusantium dicta assumenda debitis incidunt eius provident magnam, est quasi officia voluptas, nam neque omnis reiciendis.
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Error fugit quia
laboriosam eaque? Deserunt, accusantium dicta assumenda debitis incidunt
eius provident magnam, est quasi officia voluptas, nam neque omnis
reiciendis.
</p>
</StandardCenteredPage>
</template>

View File

@ -1,19 +1,11 @@
<template>
<div
class="fullscreen text-center q-pa-md flex flex-center"
>
<div class="fullscreen text-center q-pa-md flex flex-center">
<div>
<div style="font-size: 30vh">404</div>
<div class="text-h2" style="opacity: 0.4">Page not found.</div>
<q-btn
class="q-mt-xl"
unelevated
to="/"
label="Go Home"
no-caps
/>
<q-btn class="q-mt-xl" unelevated to="/" label="Go Home" no-caps />
</div>
</div>
</template>

View File

@ -3,8 +3,8 @@
<StandardCenteredPage>
<h3>Testing Page</h3>
<p>
Use this page to test new functionality, before adding it to the main app.
This page should be hidden on production.
Use this page to test new functionality, before adding it to the main
app. This page should be hidden on production.
</p>
<div style="border: 3px solid red">
<h4>Auth Test</h4>

View File

@ -6,11 +6,11 @@
<script setup lang="ts">
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
import {onMounted, ref} from 'vue';
import {useRoute, useRouter} from 'vue-router';
import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import api from 'src/api/main';
import {sleep} from 'src/utils';
import {useI18n} from 'vue-i18n';
import { sleep } from 'src/utils';
import { useI18n } from 'vue-i18n';
const router = useRouter();
const route = useRoute();
@ -35,6 +35,4 @@ onMounted(async () => {
});
</script>
<style scoped>
</style>
<style scoped></style>

View File

@ -28,11 +28,20 @@
</q-input>
</div>
<div class="row">
<q-btn type="submit" :label="$t('loginPage.logIn')" color="primary" class="q-mt-md col-12" no-caps/>
<q-btn
type="submit"
:label="$t('loginPage.logIn')"
color="primary"
class="q-mt-md col-12"
no-caps
/>
</div>
<div class="row">
<router-link
:to="{ path: '/register', query: route.query.next ? { next: route.query.next } : {} }"
:to="{
path: '/register',
query: route.query.next ? { next: route.query.next } : {},
}"
class="q-mt-md text-primary text-center col-12"
>
{{ $t('loginPage.createAccount') }}
@ -46,10 +55,10 @@
<script setup lang="ts">
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
import SlimForm from 'components/SlimForm.vue';
import {ref} from 'vue';
import { ref } from 'vue';
import api from 'src/api/main';
import {useAuthStore} from 'stores/auth-store';
import {useRoute, useRouter} from 'vue-router';
import { useAuthStore } from 'stores/auth-store';
import { useRoute, useRouter } from 'vue-router';
const authStore = useAuthStore();
const router = useRouter();
@ -57,14 +66,16 @@ const route = useRoute();
const loginModel = ref({
email: '',
password: ''
password: '',
});
const passwordVisible = ref(false);
async function tryLogin() {
try {
await api.auth.login(authStore, loginModel.value);
const dest = route.query.next ? decodeURIComponent(route.query.next as string) : '/';
const dest = route.query.next
? decodeURIComponent(route.query.next as string)
: '/';
await router.push(dest);
} catch (error) {
console.error(error);
@ -77,6 +88,4 @@ function resetLogin() {
}
</script>
<style scoped>
</style>
<style scoped></style>

View File

@ -53,17 +53,17 @@
import SlimForm from 'components/SlimForm.vue';
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
import api from 'src/api/main';
import {useRouter} from 'vue-router';
import {ref} from 'vue';
import {useQuasar} from 'quasar';
import {useI18n} from 'vue-i18n';
import { useRouter } from 'vue-router';
import { ref } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
const router = useRouter();
const registerModel = ref({
name: '',
email: '',
password: ''
password: '',
});
const passwordVisible = ref(false);
@ -77,7 +77,7 @@ async function tryRegister() {
} catch (error) {
quasar.notify({
message: t('registerPage.error'),
type: 'negative'
type: 'negative',
});
}
}
@ -89,6 +89,4 @@ function resetForm() {
}
</script>
<style scoped>
</style>
<style scoped></style>

View File

@ -10,6 +10,4 @@
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
</script>
<style scoped>
</style>
<style scoped></style>

View File

@ -69,7 +69,6 @@ onMounted(async () => {
function initMap() {
if (!gym.value) return;
const g: Gym = gym.value;
console.log(mapContainer);
const tiles = new TileLayer(TILE_URL, {
attribution: ATTRIBUTION,

View File

@ -100,6 +100,7 @@ import { Gym } from 'src/api/main/gyms';
import { Exercise } from 'src/api/main/exercises';
import { useRouter } from 'vue-router';
import { sleep } from 'src/utils';
import { uploadVideoToCDN, VideoProcessingStatus, waitUntilVideoProcessingComplete } from 'src/api/cdn';
interface Option {
value: string;
@ -117,7 +118,7 @@ let submissionModel = ref({
weight: 100,
weightUnit: 'Kg',
reps: 1,
videoId: -1,
videoFileId: '',
videoFile: null,
date: new Date().toLocaleDateString('en-CA'),
});
@ -159,10 +160,7 @@ async function onSubmitted() {
try {
infoMessage.value = 'Uploading video...';
await sleep(1000);
submissionModel.value.videoId = await api.gyms.submissions.uploadVideoFile(
gym.value,
selectedVideoFile.value
);
submissionModel.value.videoFileId = await uploadVideoToCDN(selectedVideoFile.value);
infoMessage.value = 'Creating submission...';
await sleep(1000);
const submission = await api.gyms.submissions.createSubmission(
@ -170,11 +168,14 @@ async function onSubmitted() {
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));
const finalStatus = await waitUntilVideoProcessingComplete(submission.videoFileId);
if (finalStatus === VideoProcessingStatus.COMPLETED) {
infoMessage.value = 'Submission complete!';
await sleep(1000);
await router.push(getGymRoute(gym.value));
} else {
infoMessage.value = 'Submission processing failed. Please try again later.';
}
} finally {
submitting.value = false;
}

View File

@ -8,9 +8,9 @@ import GymHomePage from 'pages/gym/GymHomePage.vue';
import GymLeaderboardsPage from 'pages/gym/GymLeaderboardsPage.vue';
import TestingPage from 'pages/TestingPage.vue';
import LoginPage from 'pages/auth/LoginPage.vue';
import RegisterPage from "pages/auth/RegisterPage.vue";
import RegistrationSuccessPage from "pages/auth/RegistrationSuccessPage.vue";
import ActivationPage from "pages/auth/ActivationPage.vue";
import RegisterPage from 'pages/auth/RegisterPage.vue';
import RegistrationSuccessPage from 'pages/auth/RegistrationSuccessPage.vue';
import ActivationPage from 'pages/auth/ActivationPage.vue';
const routes: RouteRecordRaw[] = [
// Auth-related pages, which live outside the main layout.
@ -35,7 +35,7 @@ const routes: RouteRecordRaw[] = [
{ path: 'leaderboard', component: GymLeaderboardsPage },
],
},
{ path: 'about', component: AboutPage }
{ path: 'about', component: AboutPage },
],
},

View File

@ -4,11 +4,12 @@ import nl.andrewlalis.gymboardcdn.util.ULID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
@Configuration
@EnableScheduling
@ -18,24 +19,17 @@ public class Config {
@Value("${app.api-origin}")
private String apiOrigin;
/**
* Defines the CORS configuration for this API, which is to say that we
* allow cross-origin requests ONLY from the web app for the vast majority
* of endpoints.
* @return The CORS configuration source.
*/
@Bean
@Order(1)
public CorsConfigurationSource corsConfigurationSource() {
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern(webOrigin);
config.addAllowedOriginPattern(apiOrigin);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH"));
source.registerCorsConfiguration("/**", config);
return source;
return new CorsFilter(source);
}
@Bean