Fixed the exercise submission front-end again.

This commit is contained in:
Andrew Lalis 2023-04-07 12:17:40 +02:00
parent 2881dc5376
commit 4a6d33ffa4
3 changed files with 63 additions and 79 deletions

View File

@ -8,6 +8,7 @@ const api = axios.create({
}); });
export enum VideoProcessingStatus { export enum VideoProcessingStatus {
NOT_STARTED = 'NOT_STARTED',
WAITING = 'WAITING', WAITING = 'WAITING',
IN_PROGRESS = 'IN_PROGRESS', IN_PROGRESS = 'IN_PROGRESS',
COMPLETED = 'COMPLETED', COMPLETED = 'COMPLETED',
@ -18,24 +19,21 @@ export interface FileMetadata {
filename: string; filename: string;
mimeType: string; mimeType: string;
size: number; size: number;
uploadedAt: string; createdAt: string;
availableForDownload: boolean;
} }
export async function uploadVideoToCDN(file: File): Promise<string> { export async function uploadVideoToCDN(file: File): Promise<number> {
const response = await api.post('/uploads/video', file, { const response = await api.post('/uploads/video', file, {
headers: { headers: {
'Content-Type': file.type, 'Content-Type': file.type,
}, },
}); });
return response.data.id; return response.data.taskId;
} }
export async function getVideoProcessingStatus( export async function getVideoProcessingStatus(taskId: number): Promise<VideoProcessingStatus | null> {
id: string
): Promise<VideoProcessingStatus | null> {
try { try {
const response = await api.get(`/uploads/video/${id}/status`); const response = await api.get(`/uploads/video/${taskId}/status`);
return response.data.status; return response.data.status;
} catch (error: any) { } catch (error: any) {
if (error.response && error.response.status === 404) { if (error.response && error.response.status === 404) {
@ -45,16 +43,14 @@ export async function getVideoProcessingStatus(
} }
} }
export async function waitUntilVideoProcessingComplete( export async function waitUntilVideoProcessingComplete(taskId: number): Promise<VideoProcessingStatus> {
id: string
): Promise<VideoProcessingStatus> {
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 status = await getVideoProcessingStatus(id); const status = await getVideoProcessingStatus(taskId);
failureCount = 0; failureCount = 0;
if ( if (
status === VideoProcessingStatus.COMPLETED || status === VideoProcessingStatus.COMPLETED ||

View File

@ -51,8 +51,8 @@ export default {
upload: 'Video File to Upload', upload: 'Video File to Upload',
submit: 'Submit', submit: 'Submit',
submitUploading: 'Uploading video...', submitUploading: 'Uploading video...',
submitUploadFailed: 'Video upload failed.',
submitCreatingSubmission: 'Creating submission...', submitCreatingSubmission: 'Creating submission...',
submitVideoProcessing: 'Processing...',
submitComplete: 'Submission complete!', submitComplete: 'Submission complete!',
submitFailed: 'Submission processing failed. Please try again later.', submitFailed: 'Submission processing failed. Please try again later.',
}, },

View File

@ -48,7 +48,7 @@ A high-level overview of the submission process is as follows:
</div> </div>
<div class="row"> <div class="row">
<q-input <q-input
v-model="submissionModel.date" v-model="submissionModel.performedAt"
type="date" type="date"
:label="$t('gymPage.submitPage.date')" :label="$t('gymPage.submitPage.date')"
class="col-12" class="col-12"
@ -91,28 +91,22 @@ A high-level overview of the submission process is as follows:
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, Ref } from 'vue'; import {onMounted, ref, Ref} from 'vue';
import { getGymFromRoute } from 'src/router/gym-routing'; import {getGymFromRoute} from 'src/router/gym-routing';
import SlimForm from 'components/SlimForm.vue'; import SlimForm from 'components/SlimForm.vue';
import api from 'src/api/main'; 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 { useRoute, useRouter } from 'vue-router'; import {useRoute, useRouter} from 'vue-router';
import { showApiErrorToast, sleep } from 'src/utils'; import {showApiErrorToast, showWarningToast, sleep} from 'src/utils';
import { import {uploadVideoToCDN,} from 'src/api/cdn';
uploadVideoToCDN, import {useAuthStore} from 'stores/auth-store';
VideoProcessingStatus, import {useI18n} from 'vue-i18n';
waitUntilVideoProcessingComplete,
} from 'src/api/cdn';
import { useAuthStore } from 'stores/auth-store';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
const authStore = useAuthStore(); const authStore = useAuthStore();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const i18n = useI18n(); const i18n = useI18n();
const quasar = useQuasar();
interface Option { interface Option {
value: string; value: string;
@ -127,8 +121,8 @@ let submissionModel = ref({
weight: 100, weight: 100,
weightUnit: 'Kg', weightUnit: 'Kg',
reps: 1, reps: 1,
videoFileId: '', performedAt: new Date().toLocaleDateString('en-CA'),
date: new Date().toLocaleDateString('en-CA'), taskId: -1
}); });
const selectedVideoFile: Ref<File | undefined> = ref<File>(); const selectedVideoFile: Ref<File | undefined> = ref<File>();
const weightUnits = ['KG', 'LBS']; const weightUnits = ['KG', 'LBS'];
@ -169,59 +163,53 @@ async function onSubmitted() {
if (!selectedVideoFile.value || !gym.value) return; if (!selectedVideoFile.value || !gym.value) return;
submitting.value = true; submitting.value = true;
if (await uploadVideo()) {
await createSubmission();
}
submitting.value = false;
}
/**
* Uploads the selected video and returns true if successful.
*/
async function uploadVideo(): Promise<boolean> {
if (!selectedVideoFile.value) return false;
try { try {
// 1. Upload the video to the CDN. // 1. Upload the video to the CDN.
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitUploading'); submitButtonLabel.value = i18n.t('gymPage.submitPage.submitUploading');
await sleep(1000); await sleep(1000);
submissionModel.value.videoFileId = await uploadVideoToCDN( submissionModel.value.taskId = await uploadVideoToCDN(selectedVideoFile.value);
selectedVideoFile.value return true;
); } catch (error) {
// 2. Wait for the video to be processed.
submitButtonLabel.value = i18n.t(
'gymPage.submitPage.submitVideoProcessing'
);
const processingStatus = await waitUntilVideoProcessingComplete(
submissionModel.value.videoFileId
);
// 3. If successful upload, create the submission.
if (processingStatus === VideoProcessingStatus.COMPLETED) {
try {
submitButtonLabel.value = i18n.t(
'gymPage.submitPage.submitCreatingSubmission'
);
await sleep(1000);
const submission = await api.gyms.submissions.createSubmission(
gym.value,
submissionModel.value,
authStore
);
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitComplete');
await sleep(2000);
await router.push(`/submissions/${submission.id}`);
} catch (error: any) {
if (error.response && error.response.status === 400) {
quasar.notify({
message: error.response.data.message,
type: 'warning',
position: 'top',
});
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitFailed');
await sleep(3000);
} else {
showApiErrorToast(error);
}
}
// Otherwise, report the failed submission and give up.
} else {
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitFailed');
await sleep(3000);
}
} catch (error: any) {
showApiErrorToast(error); showApiErrorToast(error);
} finally { submitButtonLabel.value = i18n.t('gymPage.submitPage.submitUploadFailed');
submitting.value = false; await sleep(1000);
selectedVideoFile.value = undefined;
submitButtonLabel.value = i18n.t('gymPage.submitPage.submit');
return false;
}
}
/**
* Tries to create a new submission, and if successful, redirects the user to it.
*/
async function createSubmission() {
if (!gym.value) return;
try {
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitCreatingSubmission');
await sleep(1000);
const submission = await api.gyms.submissions.createSubmission(gym.value, submissionModel.value, authStore);
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitComplete');
await sleep(2000);
await router.push(`/submissions/${submission.id}`);
} catch (error: any) {
if (error.response && error.response.status === 400) {
showWarningToast(error.response.data.message);
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitFailed');
} else {
showApiErrorToast(error);
}
await sleep(3000);
submitButtonLabel.value = i18n.t('gymPage.submitPage.submit'); submitButtonLabel.value = i18n.t('gymPage.submitPage.submit');
} }
} }