diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/GymController.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/GymController.java index a827ceb..4522018 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/GymController.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/GymController.java @@ -8,12 +8,11 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; - /** * Controller for accessing a particular gym. */ @RestController +@RequestMapping(path = "/gyms/{compoundId}") public class GymController { private final GymService gymService; private final UploadService uploadService; @@ -25,35 +24,32 @@ public class GymController { this.submissionService = submissionService; } - @GetMapping(path = "/gyms/{countryCode}/{cityCode}/{gymName}") - public GymResponse getGym( - @PathVariable String countryCode, - @PathVariable String cityCode, - @PathVariable String gymName - ) { - return gymService.getGym(new RawGymId(countryCode, cityCode, gymName)); + @GetMapping + public GymResponse getGym(@PathVariable String compoundId) { + return gymService.getGym(CompoundGymId.parse(compoundId)); } - @PostMapping(path = "/gyms/{countryCode}/{cityCode}/{gymName}/submissions") + @PostMapping(path = "/submissions") public ExerciseSubmissionResponse createSubmission( - @PathVariable String countryCode, - @PathVariable String cityCode, - @PathVariable String gymName, + @PathVariable String compoundId, @RequestBody ExerciseSubmissionPayload payload ) { - return submissionService.createSubmission(new RawGymId(countryCode, cityCode, gymName), payload); + return submissionService.createSubmission(CompoundGymId.parse(compoundId), payload); } - @PostMapping( - path = "/gyms/{countryCode}/{cityCode}/{gymName}/submissions/upload", - consumes = MediaType.MULTIPART_FORM_DATA_VALUE - ) + @GetMapping(path = "/submissions/{submissionId}") + public ExerciseSubmissionResponse getSubmission( + @PathVariable String compoundId, + @PathVariable long submissionId + ) { + return submissionService.getSubmission(CompoundGymId.parse(compoundId), submissionId); + } + + @PostMapping(path = "/submissions/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public UploadedFileResponse uploadVideo( - @PathVariable String countryCode, - @PathVariable String cityCode, - @PathVariable String gymName, + @PathVariable String compoundId, @RequestParam MultipartFile file ) { - return uploadService.handleSubmissionUpload(new RawGymId(countryCode, cityCode, gymName), file); + return uploadService.handleSubmissionUpload(CompoundGymId.parse(compoundId), file); } } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/CompoundGymId.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/CompoundGymId.java new file mode 100644 index 0000000..6fe8ee8 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/CompoundGymId.java @@ -0,0 +1,28 @@ +package nl.andrewlalis.gymboard_api.controller.dto; + +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +public record CompoundGymId(String country, String city, String gym) { + /** + * Parses a compound gym id from a string expression. + *

+ * For example, `nl_groningen_trainmore-munnekeholm`. + *

+ * @param idStr The id string. + * @return The compound gym id. + * @throws ResponseStatusException A not found exception is thrown if the id + * string is invalid. + */ + public static CompoundGymId parse(String idStr) throws ResponseStatusException { + if (idStr == null || idStr.isBlank()) throw new ResponseStatusException(HttpStatus.NOT_FOUND); + String[] parts = idStr.strip().toLowerCase().split("_"); + if (parts.length != 3) throw new ResponseStatusException(HttpStatus.NOT_FOUND); + + return new CompoundGymId( + parts[0], + parts[1], + parts[2] + ); + } +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/ExerciseSubmissionPayload.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/ExerciseSubmissionPayload.java index 9760f29..2d8ae27 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/ExerciseSubmissionPayload.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/ExerciseSubmissionPayload.java @@ -4,6 +4,7 @@ public record ExerciseSubmissionPayload( String name, String exerciseShortName, float weight, + String weightUnit, int reps, long videoId ) {} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/ExerciseSubmissionResponse.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/ExerciseSubmissionResponse.java index e1c7452..2abe6ac 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/ExerciseSubmissionResponse.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/ExerciseSubmissionResponse.java @@ -11,7 +11,9 @@ public record ExerciseSubmissionResponse( ExerciseResponse exercise, String status, String submitterName, - double weight, + double rawWeight, + String weightUnit, + double metricWeight, int reps ) { public ExerciseSubmissionResponse(ExerciseSubmission submission) { @@ -22,7 +24,9 @@ public record ExerciseSubmissionResponse( new ExerciseResponse(submission.getExercise()), submission.getStatus().name(), submission.getSubmitterName(), - submission.getWeight().doubleValue(), + submission.getRawWeight().doubleValue(), + submission.getWeightUnit().name(), + submission.getMetricWeight().doubleValue(), submission.getReps() ); } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/RawGymId.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/RawGymId.java deleted file mode 100644 index 0b22f07..0000000 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/dto/RawGymId.java +++ /dev/null @@ -1,3 +0,0 @@ -package nl.andrewlalis.gymboard_api.controller.dto; - -public record RawGymId(String countryCode, String cityCode, String gymName) {} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/GymRepository.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/GymRepository.java index 961f9ce..ae164c1 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/GymRepository.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/GymRepository.java @@ -1,5 +1,6 @@ package nl.andrewlalis.gymboard_api.dao; +import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId; import nl.andrewlalis.gymboard_api.model.Gym; import nl.andrewlalis.gymboard_api.model.GymId; import org.springframework.data.jpa.repository.JpaRepository; @@ -12,8 +13,14 @@ import java.util.Optional; @Repository public interface GymRepository extends JpaRepository, JpaSpecificationExecutor { @Query("SELECT g FROM Gym g " + - "WHERE g.id.shortName = :gym AND " + - "g.id.city.id.shortName = :city AND " + - "g.id.city.id.country.code = :country") - Optional findByRawId(String gym, String city, String country); + "WHERE g.id.shortName = :#{#id.gym()} AND " + + "g.id.city.id.shortName = :#{#id.city()} AND " + + "g.id.city.id.country.code = :#{#id.country()}") + Optional findByCompoundId(CompoundGymId id); + + @Query("SELECT COUNT(g) > 0 FROM Gym g " + + "WHERE g.id.shortName = :#{#id.gym()} AND " + + "g.id.city.id.shortName = :#{#id.city()} AND " + + "g.id.city.id.country.code = :#{#id.country()}") + boolean existsByCompoundId(CompoundGymId id); } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/exercise/ExerciseSubmissionTempFileRepository.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/exercise/ExerciseSubmissionTempFileRepository.java index 8f58403..12d2948 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/exercise/ExerciseSubmissionTempFileRepository.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/exercise/ExerciseSubmissionTempFileRepository.java @@ -13,4 +13,5 @@ import java.util.Optional; public interface ExerciseSubmissionTempFileRepository extends JpaRepository { List findAllByCreatedAtBefore(LocalDateTime timestamp); Optional findBySubmission(ExerciseSubmission submission); + boolean existsByPath(String path); } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/exercise/ExerciseSubmission.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/exercise/ExerciseSubmission.java index 73f51f4..e047f09 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/exercise/ExerciseSubmission.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/model/exercise/ExerciseSubmission.java @@ -28,6 +28,11 @@ public class ExerciseSubmission { VERIFIED } + public enum WeightUnit { + KG, + LBS + } + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -49,18 +54,27 @@ public class ExerciseSubmission { private String submitterName; @Column(nullable = false, precision = 7, scale = 2) - private BigDecimal weight; + private BigDecimal rawWeight; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private WeightUnit weightUnit; + + @Column(nullable = false, precision = 7, scale = 2) + private BigDecimal metricWeight; @Column(nullable = false) private int reps; public ExerciseSubmission() {} - public ExerciseSubmission(Gym gym, Exercise exercise, String submitterName, BigDecimal weight, int reps) { + public ExerciseSubmission(Gym gym, Exercise exercise, String submitterName, BigDecimal rawWeight, WeightUnit unit, BigDecimal metricWeight, int reps) { this.gym = gym; this.exercise = exercise; this.submitterName = submitterName; - this.weight = weight; + this.rawWeight = rawWeight; + this.weightUnit = unit; + this.metricWeight = metricWeight; this.reps = reps; this.status = Status.WAITING; } @@ -93,8 +107,16 @@ public class ExerciseSubmission { return submitterName; } - public BigDecimal getWeight() { - return weight; + public BigDecimal getRawWeight() { + return rawWeight; + } + + public WeightUnit getWeightUnit() { + return weightUnit; + } + + public BigDecimal getMetricWeight() { + return metricWeight; } public int getReps() { diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/ExerciseSubmissionService.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/ExerciseSubmissionService.java index b3c3f34..0df8637 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/ExerciseSubmissionService.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/ExerciseSubmissionService.java @@ -1,8 +1,8 @@ package nl.andrewlalis.gymboard_api.service; +import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId; import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionPayload; import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse; -import nl.andrewlalis.gymboard_api.controller.dto.RawGymId; import nl.andrewlalis.gymboard_api.dao.GymRepository; import nl.andrewlalis.gymboard_api.dao.StoredFileRepository; import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository; @@ -30,6 +30,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.time.Instant; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -63,6 +64,15 @@ public class ExerciseSubmissionService { this.submissionVideoFileRepository = submissionVideoFileRepository; } + @Transactional(readOnly = true) + public ExerciseSubmissionResponse getSubmission(CompoundGymId id, long submissionId) { + Gym gym = gymRepository.findByCompoundId(id) + .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); + return new ExerciseSubmissionResponse(submission); + } /** * Handles the creation of a new exercise submission. This involves a few steps: @@ -79,8 +89,8 @@ public class ExerciseSubmissionService { * @return The saved submission, which will be in the PROCESSING state at first. */ @Transactional - public ExerciseSubmissionResponse createSubmission(RawGymId id, ExerciseSubmissionPayload payload) { - Gym gym = gymRepository.findByRawId(id.gymName(), id.cityCode(), id.countryCode()) + public ExerciseSubmissionResponse createSubmission(CompoundGymId id, ExerciseSubmissionPayload payload) { + Gym gym = gymRepository.findByCompoundId(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); Exercise exercise = exerciseRepository.findById(payload.exerciseShortName()) .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid exercise.")); @@ -90,11 +100,20 @@ public class ExerciseSubmissionService { // TODO: Validate the submission data. // Create the submission. + BigDecimal rawWeight = BigDecimal.valueOf(payload.weight()); + ExerciseSubmission.WeightUnit unit = ExerciseSubmission.WeightUnit.valueOf(payload.weightUnit().toUpperCase()); + BigDecimal metricWeight = BigDecimal.valueOf(payload.weight()); + if (unit == ExerciseSubmission.WeightUnit.LBS) { + metricWeight = metricWeight.multiply(new BigDecimal("0.45359237")); + } + ExerciseSubmission submission = exerciseSubmissionRepository.save(new ExerciseSubmission( gym, exercise, payload.name(), - BigDecimal.valueOf(payload.weight()), + rawWeight, + unit, + metricWeight, payload.reps() )); // Then link it to the temporary video file so the async task can find it. @@ -164,7 +183,7 @@ public class ExerciseSubmissionService { } // Now we can try to process the video file into a compressed format that can be stored in the DB. - Path dir = tempFilePath.getParent(); + Path dir = UploadService.SUBMISSION_TEMP_FILE_DIR; String tempFileName = tempFilePath.getFileName().toString(); String tempFileBaseName = tempFileName.substring(0, tempFileName.length() - ".tmp".length()); Path outFilePath = dir.resolve(tempFileBaseName + "-out.mp4"); @@ -255,4 +274,35 @@ public class ExerciseSubmissionService { Files.deleteIfExists(tmpStdout); Files.deleteIfExists(tmpStderr); } + + @Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES) + public void removeOldUploadedFiles() { + // First remove any temp files older than 10 minutes. + LocalDateTime cutoff = LocalDateTime.now().minusMinutes(10); + var tempFiles = tempFileRepository.findAllByCreatedAtBefore(cutoff); + for (var file : tempFiles) { + try { + Files.deleteIfExists(Path.of(file.getPath())); + tempFileRepository.delete(file); + log.info("Removed temporary submission file {} at {}.", file.getId(), file.getPath()); + } catch (IOException e) { + log.error(String.format("Could not delete submission temp file %d at %s.", file.getId(), file.getPath()), e); + } + } + + // Then remove any files in the directory which don't correspond to a valid file in the db. + try (var s = Files.list(UploadService.SUBMISSION_TEMP_FILE_DIR)) { + for (var path : s.toList()) { + if (!tempFileRepository.existsByPath(path.toString())) { + try { + Files.delete(path); + } catch (IOException e) { + log.error("Couldn't delete orphan temp file: " + path, e); + } + } + } + } catch (IOException e) { + log.error("Couldn't get list of temp files.", e); + } + } } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/GymService.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/GymService.java index 9a494cc..eb12b3e 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/GymService.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/GymService.java @@ -1,7 +1,7 @@ package nl.andrewlalis.gymboard_api.service; import nl.andrewlalis.gymboard_api.controller.dto.GymResponse; -import nl.andrewlalis.gymboard_api.controller.dto.RawGymId; +import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId; import nl.andrewlalis.gymboard_api.dao.GymRepository; import nl.andrewlalis.gymboard_api.model.Gym; import org.slf4j.Logger; @@ -22,8 +22,8 @@ public class GymService { } @Transactional(readOnly = true) - public GymResponse getGym(RawGymId id) { - Gym gym = gymRepository.findByRawId(id.gymName(), id.cityCode(), id.countryCode()) + public GymResponse getGym(CompoundGymId id) { + Gym gym = gymRepository.findByCompoundId(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); return new GymResponse(gym); } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/UploadService.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/UploadService.java index 9472b36..6095139 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/UploadService.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/UploadService.java @@ -1,6 +1,6 @@ package nl.andrewlalis.gymboard_api.service; -import nl.andrewlalis.gymboard_api.controller.dto.RawGymId; +import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId; import nl.andrewlalis.gymboard_api.controller.dto.UploadedFileResponse; import nl.andrewlalis.gymboard_api.dao.GymRepository; import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionTempFileRepository; @@ -21,6 +21,7 @@ import java.nio.file.Path; */ @Service public class UploadService { + public static final Path SUBMISSION_TEMP_FILE_DIR = Path.of("exercise_submission_temp_files"); private static final String[] ALLOWED_VIDEO_TYPES = { "video/mp4" }; @@ -46,8 +47,8 @@ public class UploadService { * the user's submission. */ @Transactional - public UploadedFileResponse handleSubmissionUpload(RawGymId gymId, MultipartFile multipartFile) { - Gym gym = gymRepository.findByRawId(gymId.gymName(), gymId.cityCode(), gymId.countryCode()) + public UploadedFileResponse handleSubmissionUpload(CompoundGymId gymId, MultipartFile multipartFile) { + Gym gym = gymRepository.findByCompoundId(gymId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); // TODO: Check that user is allowed to upload. boolean fileTypeAcceptable = false; @@ -61,11 +62,10 @@ public class UploadService { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid content type."); } try { - Path tempFileDir = Path.of("exercise_submission_temp_files"); - if (!Files.exists(tempFileDir)) { - Files.createDirectory(tempFileDir); + if (!Files.exists(SUBMISSION_TEMP_FILE_DIR)) { + Files.createDirectory(SUBMISSION_TEMP_FILE_DIR); } - Path tempFilePath = Files.createTempFile(tempFileDir, null, null); + Path tempFilePath = Files.createTempFile(SUBMISSION_TEMP_FILE_DIR, null, null); multipartFile.transferTo(tempFilePath); ExerciseSubmissionTempFile tempFileEntity = tempFileRepository.save(new ExerciseSubmissionTempFile(tempFilePath.toString())); return new UploadedFileResponse(tempFileEntity.getId()); diff --git a/gymboard-app/.eslintrc.js b/gymboard-app/.eslintrc.js index 796b2e8..2f905fc 100644 --- a/gymboard-app/.eslintrc.js +++ b/gymboard-app/.eslintrc.js @@ -9,14 +9,14 @@ module.exports = { // `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted parserOptions: { parser: require.resolve('@typescript-eslint/parser'), - extraFileExtensions: [ '.vue' ] + extraFileExtensions: ['.vue'], }, env: { browser: true, es2021: true, node: true, - 'vue/setup-compiler-macros': true + 'vue/setup-compiler-macros': true, }, // Rules order is important, please avoid shuffling them @@ -37,7 +37,7 @@ module.exports = { // https://github.com/prettier/eslint-config-prettier#installation // usage with Prettier, provided by 'eslint-config-prettier'. - 'prettier' + 'prettier', ], plugins: [ @@ -46,12 +46,11 @@ module.exports = { // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files // required to lint *.vue files - 'vue' - + 'vue', + // https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674 // Prettier has not been included as plugin to avoid performance impact // add it as an extension for your IDE - ], globals: { @@ -64,12 +63,11 @@ module.exports = { __QUASAR_SSR_PWA__: 'readonly', process: 'readonly', Capacitor: 'readonly', - chrome: 'readonly' + chrome: 'readonly', }, // add your custom rules here rules: { - 'prefer-promise-reject-errors': 'off', quotes: ['warn', 'single', { avoidEscape: true }], @@ -85,6 +83,6 @@ module.exports = { 'no-unused-vars': 'off', // allow debugger during development only - 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' - } -} + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', + }, +}; diff --git a/gymboard-app/.vscode/extensions.json b/gymboard-app/.vscode/extensions.json index fe38802..b6a2eb9 100644 --- a/gymboard-app/.vscode/extensions.json +++ b/gymboard-app/.vscode/extensions.json @@ -12,4 +12,4 @@ "dbaeumer.jshint", "ms-vscode.vscode-typescript-tslint-plugin" ] -} \ No newline at end of file +} diff --git a/gymboard-app/.vscode/settings.json b/gymboard-app/.vscode/settings.json index 746cf57..fa9e6d6 100644 --- a/gymboard-app/.vscode/settings.json +++ b/gymboard-app/.vscode/settings.json @@ -3,14 +3,7 @@ "editor.guides.bracketPairs": true, "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.codeActionsOnSave": [ - "source.fixAll.eslint" - ], - "eslint.validate": [ - "javascript", - "javascriptreact", - "typescript", - "vue" - ], + "editor.codeActionsOnSave": ["source.fixAll.eslint"], + "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], "typescript.tsdk": "node_modules/typescript/lib" -} \ No newline at end of file +} diff --git a/gymboard-app/README.md b/gymboard-app/README.md index 610cdbe..84dec1e 100644 --- a/gymboard-app/README.md +++ b/gymboard-app/README.md @@ -3,6 +3,7 @@ Web app for Gymboard ## Install the dependencies + ```bash yarn # or @@ -10,32 +11,33 @@ npm install ``` ### Start the app in development mode (hot-code reloading, error reporting, etc.) + ```bash quasar dev ``` - ### Lint the files + ```bash yarn lint # or npm run lint ``` - ### Format the files + ```bash yarn format # or npm run format ``` - - ### Build the app for production + ```bash quasar build ``` ### Customize the configuration + See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js). diff --git a/gymboard-app/index.html b/gymboard-app/index.html index 3c8c78f..36059a4 100644 --- a/gymboard-app/index.html +++ b/gymboard-app/index.html @@ -3,17 +3,40 @@ <%= productName %> - - - - - + + + + + - - - - - + + + + + diff --git a/gymboard-app/package.json b/gymboard-app/package.json index fd818de..6514923 100644 --- a/gymboard-app/package.json +++ b/gymboard-app/package.json @@ -37,4 +37,4 @@ "npm": ">= 6.13.4", "yarn": ">= 1.21.1" } -} \ No newline at end of file +} diff --git a/gymboard-app/postcss.config.js b/gymboard-app/postcss.config.js index 94b7b1c..8bfb534 100644 --- a/gymboard-app/postcss.config.js +++ b/gymboard-app/postcss.config.js @@ -13,9 +13,9 @@ module.exports = { 'last 4 Android versions', 'last 4 ChromeAndroid versions', 'last 4 FirefoxAndroid versions', - 'last 4 iOS versions' - ] - }) + 'last 4 iOS versions', + ], + }), // https://github.com/elchininet/postcss-rtlcss // If you want to support RTL css, then @@ -23,5 +23,5 @@ module.exports = { // 2. optionally set quasar.config.js > framework > lang to an RTL language // 3. uncomment the following line: // require('postcss-rtlcss') - ] -} + ], +}; diff --git a/gymboard-app/quasar.config.js b/gymboard-app/quasar.config.js index 59ee271..bd2275d 100644 --- a/gymboard-app/quasar.config.js +++ b/gymboard-app/quasar.config.js @@ -8,7 +8,6 @@ // Configuration for your app // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js - const { configure } = require('quasar/wrappers'); const path = require('path'); const { withCtx } = require('vue'); @@ -21,7 +20,7 @@ module.exports = configure(function (ctx) { // exclude = [], // rawOptions = {}, warnings: true, - errors: true + errors: true, }, // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature @@ -30,15 +29,10 @@ module.exports = configure(function (ctx) { // app boot file (/src/boot) // --> boot files are part of "main.js" // https://v2.quasar.dev/quasar-cli-vite/boot-files - boot: [ - 'i18n', - 'axios', - ], + boot: ['i18n', 'axios'], // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css - css: [ - 'app.scss' - ], + css: ['app.scss'], // https://github.com/quasarframework/quasar/tree/dev/extras extras: [ @@ -57,8 +51,8 @@ module.exports = configure(function (ctx) { // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build build: { target: { - browser: [ 'es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1' ], - node: 'node16' + browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'], + node: 'node16', }, vueRouterMode: 'hash', // available values: 'hash', 'history' @@ -71,7 +65,7 @@ module.exports = configure(function (ctx) { // publicPath: '/', // analyze: true, env: { - API: ctx.dev ? 'http://localhost:8080' : 'https://api.gymboard.com' + API: ctx.dev ? 'http://localhost:8080' : 'https://api.gymboard.com', }, // rawDefine: {} // ignorePublicFolder: true, @@ -83,20 +77,23 @@ module.exports = configure(function (ctx) { // viteVuePluginOptions: {}, vitePlugins: [ - ['@intlify/vite-plugin-vue-i18n', { - // if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false` - // compositionOnly: false, + [ + '@intlify/vite-plugin-vue-i18n', + { + // if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false` + // compositionOnly: false, - // you need to set i18n resource including paths ! - include: path.resolve(__dirname, './src/i18n/**') - }] - ] + // you need to set i18n resource including paths ! + include: path.resolve(__dirname, './src/i18n/**'), + }, + ], + ], }, // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer devServer: { // https: true - open: true // opens browser window automatically + open: true, // opens browser window automatically }, // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework @@ -114,7 +111,7 @@ module.exports = configure(function (ctx) { // directives: [], // Quasar plugins - plugins: [] + plugins: [], }, // animations: 'all', // --- includes all animations @@ -136,7 +133,7 @@ module.exports = configure(function (ctx) { // https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr ssr: { // ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name! - // will mess up SSR + // will mess up SSR // extendSSRWebserverConf (esbuildConf) {}, // extendPackageJson (json) {}, @@ -147,11 +144,11 @@ module.exports = configure(function (ctx) { // manualPostHydrationTrigger: true, prodPort: 3000, // The default port that the production server should use - // (gets superseded if process.env.PORT is specified at runtime) + // (gets superseded if process.env.PORT is specified at runtime) middlewares: [ - 'render' // keep this as last one - ] + 'render', // keep this as last one + ], }, // https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa @@ -175,7 +172,7 @@ module.exports = configure(function (ctx) { // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor capacitor: { - hideSplashscreen: true + hideSplashscreen: true, }, // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron @@ -189,13 +186,11 @@ module.exports = configure(function (ctx) { packager: { // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options - // OS X / Mac App Store // appBundleId: '', // appCategoryType: '', // osxSign: '', // protocol: 'myapp://path', - // Windows only // win32metadata: { ... } }, @@ -203,18 +198,16 @@ module.exports = configure(function (ctx) { builder: { // https://www.electron.build/configuration/configuration - appId: 'gymboard-app' - } + appId: 'gymboard-app', + }, }, // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex bex: { - contentScripts: [ - 'my-content-script' - ], + contentScripts: ['my-content-script'], // extendBexScriptsConf (esbuildConf) {} // extendBexManifestJson (json) {} - } - } + }, + }; }); diff --git a/gymboard-app/src/App.vue b/gymboard-app/src/App.vue index ddfb546..a14ca21 100644 --- a/gymboard-app/src/App.vue +++ b/gymboard-app/src/App.vue @@ -6,6 +6,6 @@ import { defineComponent } from 'vue'; export default defineComponent({ - name: 'App' + name: 'App', }); diff --git a/gymboard-app/src/api/gymboard-api.ts b/gymboard-app/src/api/gymboard-api.ts deleted file mode 100644 index 9685057..0000000 --- a/gymboard-app/src/api/gymboard-api.ts +++ /dev/null @@ -1,119 +0,0 @@ -import axios from 'axios'; - -export const BASE_URL = 'http://localhost:8080'; - -// TODO: Figure out how to get the base URL from environment. -const api = axios.create({ - baseURL: BASE_URL -}); -api.defaults.headers.post['Content-Type'] = 'application/json'; - -export interface Exercise { - shortName: string, - displayName: string -} - -export interface GeoPoint { - latitude: number, - longitude: number -} - -export interface ExerciseSubmissionPayload { - name: string, - exerciseShortName: string, - weight: number, - reps: number, - videoId: number -} - -export interface ExerciseSubmission { - id: number, - createdAt: string, - gym: SimpleGym, - exercise: Exercise, - status: ExerciseSubmissionStatus, - submitterName: string, - weight: number, - reps: number -} - -export enum ExerciseSubmissionStatus { - WAITING = 'WAITING', - PROCESSING = 'PROCESSING', - FAILED = 'FAILED', - COMPLETED = 'COMPLETED', - VERIFIED = 'VERIFIED' -} - -export interface Gym { - countryCode: string, - countryName: string, - cityShortName: string, - cityName: string, - createdAt: Date, - shortName: string, - displayName: string, - websiteUrl: string | null, - location: GeoPoint, - streetAddress: string -} - -export interface SimpleGym { - countryCode: string, - cityShortName: string, - shortName: string, - displayName: string -} - -/** - * 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. - */ -export function getUploadUrl(gym: Gym) { - 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. - */ -export function getFileUrl(fileId: number) { - return BASE_URL + `/files/${fileId}`; -} - -export async function getExercises(): Promise> { - const response = await api.get('/exercises'); - return response.data; -} - -export async function getGym( - countryCode: string, - cityShortName: string, - gymShortName: string -): Promise { - const response = await api.get( - `/gyms/${countryCode}/${cityShortName}/${gymShortName}` - ); - const d = response.data; - return { - countryCode: d.countryCode, - countryName: d.countryName, - cityShortName: d.cityShortName, - cityName: d.cityName, - createdAt: new Date(d.createdAt), - shortName: d.shortName, - displayName: d.displayName, - websiteUrl: d.websiteUrl, - location: d.location, - streetAddress: d.streetAddress, - }; -} - -export async function createSubmission(gym: Gym, payload: ExerciseSubmissionPayload): Promise { - const response = await api.post( - `/gyms/${gym.countryCode}/${gym.cityShortName}/${gym.shortName}/submissions`, - payload - ); - return response.data; -} diff --git a/gymboard-app/src/api/main/exercises.ts b/gymboard-app/src/api/main/exercises.ts new file mode 100644 index 0000000..162e055 --- /dev/null +++ b/gymboard-app/src/api/main/exercises.ts @@ -0,0 +1,15 @@ +import { api } from 'src/api/main/index'; + +export interface Exercise { + shortName: string; + displayName: string; +} + +class ExercisesModule { + public async getExercises(): Promise> { + const response = await api.get('/exercises'); + return response.data; + } +} + +export default ExercisesModule; diff --git a/gymboard-app/src/api/main/gyms.ts b/gymboard-app/src/api/main/gyms.ts new file mode 100644 index 0000000..fc36b40 --- /dev/null +++ b/gymboard-app/src/api/main/gyms.ts @@ -0,0 +1,52 @@ +import { GeoPoint } from 'src/api/main/models'; +import SubmissionsModule from 'src/api/main/submission'; +import { api } from 'src/api/main/index'; + +export interface Gym { + countryCode: string; + countryName: string; + cityShortName: string; + cityName: string; + createdAt: Date; + shortName: string; + displayName: string; + websiteUrl: string | null; + location: GeoPoint; + streetAddress: string; +} + +export interface SimpleGym { + countryCode: string; + cityShortName: string; + shortName: string; + displayName: string; +} + +class GymsModule { + public readonly submissions: SubmissionsModule = new SubmissionsModule(); + + public async getGym( + countryCode: string, + cityShortName: string, + gymShortName: string + ) { + const response = await api.get( + `/gyms/${countryCode}_${cityShortName}_${gymShortName}` + ); + const d = response.data; + return { + countryCode: d.countryCode, + countryName: d.countryName, + cityShortName: d.cityShortName, + cityName: d.cityName, + createdAt: new Date(d.createdAt), + shortName: d.shortName, + displayName: d.displayName, + websiteUrl: d.websiteUrl, + location: d.location, + streetAddress: d.streetAddress, + }; + } +} + +export default GymsModule; diff --git a/gymboard-app/src/api/main/index.ts b/gymboard-app/src/api/main/index.ts new file mode 100644 index 0000000..cc4f453 --- /dev/null +++ b/gymboard-app/src/api/main/index.ts @@ -0,0 +1,48 @@ +import axios, { AxiosInstance } from 'axios'; +import GymsModule from 'src/api/main/gyms'; +import ExercisesModule from 'src/api/main/exercises'; +import { GymRoutable } from 'src/router/gym-routing'; + +export const BASE_URL = 'http://localhost:8080'; + +// TODO: Figure out how to get the base URL from environment. +export const api = axios.create({ + 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 { + public readonly gyms = new GymsModule(); + 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(); diff --git a/gymboard-app/src/api/main/models.ts b/gymboard-app/src/api/main/models.ts new file mode 100644 index 0000000..d922f0d --- /dev/null +++ b/gymboard-app/src/api/main/models.ts @@ -0,0 +1,4 @@ +export interface GeoPoint { + latitude: number; + longitude: number; +} diff --git a/gymboard-app/src/api/main/submission.ts b/gymboard-app/src/api/main/submission.ts new file mode 100644 index 0000000..7bfdbe2 --- /dev/null +++ b/gymboard-app/src/api/main/submission.ts @@ -0,0 +1,105 @@ +import { SimpleGym } from 'src/api/main/gyms'; +import { Exercise } from 'src/api/main/exercises'; +import { api } from 'src/api/main/index'; +import { GymRoutable } from 'src/router/gym-routing'; +import { sleep } from 'src/utils'; + +/** + * The data that's sent when creating a submission. + */ +export interface ExerciseSubmissionPayload { + name: string; + exerciseShortName: string; + weight: number; + weightUnit: string; + reps: number; + videoId: number; +} + +export interface ExerciseSubmission { + id: number; + createdAt: string; + gym: SimpleGym; + exercise: Exercise; + status: ExerciseSubmissionStatus; + submitterName: string; + weight: number; + reps: number; +} + +export enum ExerciseSubmissionStatus { + WAITING = 'WAITING', + PROCESSING = 'PROCESSING', + FAILED = 'FAILED', + COMPLETED = 'COMPLETED', + VERIFIED = 'VERIFIED', +} + +class SubmissionsModule { + public async getSubmission( + gym: GymRoutable, + submissionId: number + ): Promise { + const response = await api.get( + `/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/submissions/${submissionId}` + ); + return response.data; + } + + public async createSubmission( + gym: GymRoutable, + payload: ExerciseSubmissionPayload + ): Promise { + const response = await api.post( + `/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/submissions`, + payload + ); + return response.data; + } + + public async uploadVideoFile(gym: GymRoutable, file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + const response = await api.post( + `/gyms/${gym.countryCode}_${gym.cityShortName}_${gym.shortName}/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 gym The gym that the submission is for. + * @param submissionId The submission's id. + */ + public async waitUntilSubmissionProcessed( + gym: GymRoutable, + submissionId: number + ): Promise { + let failureCount = 0; + let attemptCount = 0; + while (failureCount < 5 && attemptCount < 60) { + await sleep(1000); + attemptCount++; + try { + const response = await this.getSubmission(gym, 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; diff --git a/gymboard-app/src/api/search/index.ts b/gymboard-app/src/api/search/index.ts index 90b1105..78dedb0 100644 --- a/gymboard-app/src/api/search/index.ts +++ b/gymboard-app/src/api/search/index.ts @@ -1,15 +1,17 @@ import axios from 'axios'; -import {GymSearchResult} from 'src/api/search/models'; +import { GymSearchResult } from 'src/api/search/models'; const api = axios.create({ - baseURL: 'http://localhost:8081' + baseURL: 'http://localhost:8081', }); /** * Searches for gyms using the given query, and eventually returns results. * @param query The query to use. */ -export async function searchGyms(query: string): Promise> { +export async function searchGyms( + query: string +): Promise> { const response = await api.get('/search/gyms?q=' + query); return response.data; } diff --git a/gymboard-app/src/api/search/models.ts b/gymboard-app/src/api/search/models.ts index e073e76..f06810b 100644 --- a/gymboard-app/src/api/search/models.ts +++ b/gymboard-app/src/api/search/models.ts @@ -1,12 +1,12 @@ export interface GymSearchResult { - compoundId: string, - shortName: string, - displayName: string, - cityShortName: string, - cityName: string, - countryCode: string, - countryName: string, - streetAddress: string, - latitude: number, - longitude: number + compoundId: string; + shortName: string; + displayName: string; + cityShortName: string; + cityName: string; + countryCode: string; + countryName: string; + streetAddress: string; + latitude: number; + longitude: number; } diff --git a/gymboard-app/src/boot/i18n.ts b/gymboard-app/src/boot/i18n.ts index 5189708..6a844a1 100644 --- a/gymboard-app/src/boot/i18n.ts +++ b/gymboard-app/src/boot/i18n.ts @@ -5,7 +5,7 @@ import messages from 'src/i18n'; export type MessageLanguages = keyof typeof messages; // Type-define 'en-US' as the master schema for the resource -export type MessageSchema = typeof messages['en-US']; +export type MessageSchema = (typeof messages)['en-US']; // See https://vue-i18n.intlify.dev/guide/advanced/typescript.html#global-resource-schema-type-definition /* eslint-disable @typescript-eslint/no-empty-interface */ diff --git a/gymboard-app/src/components/EssentialLink.vue b/gymboard-app/src/components/EssentialLink.vue index 431b56f..d309c7a 100644 --- a/gymboard-app/src/components/EssentialLink.vue +++ b/gymboard-app/src/components/EssentialLink.vue @@ -1,14 +1,6 @@ - + diff --git a/gymboard-app/src/components/models.ts b/gymboard-app/src/components/models.ts deleted file mode 100644 index 6945920..0000000 --- a/gymboard-app/src/components/models.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Todo { - id: number; - content: string; -} - -export interface Meta { - totalCount: number; -} diff --git a/gymboard-app/src/css/quasar.variables.scss b/gymboard-app/src/css/quasar.variables.scss index 9acbbcf..6cea171 100644 --- a/gymboard-app/src/css/quasar.variables.scss +++ b/gymboard-app/src/css/quasar.variables.scss @@ -12,14 +12,14 @@ // to match your app's branding. // Tip: Use the "Theme Builder" on Quasar's documentation website. -$primary : #171717; -$secondary : #575757; -$accent : #b60600; +$primary: #171717; +$secondary: #575757; +$accent: #b60600; -$dark : #1D1D1D; -$dark-page : #121212; +$dark: #1d1d1d; +$dark-page: #121212; -$positive : #41b65c; -$negative : #c0293b; -$info : #8bcfdd; -$warning : #eccb70; +$positive: #41b65c; +$negative: #c0293b; +$info: #8bcfdd; +$warning: #eccb70; diff --git a/gymboard-app/src/i18n/en-US/index.ts b/gymboard-app/src/i18n/en-US/index.ts index 9126988..2b6fa0c 100644 --- a/gymboard-app/src/i18n/en-US/index.ts +++ b/gymboard-app/src/i18n/en-US/index.ts @@ -1,10 +1,10 @@ export default { mainLayout: { language: 'Language', - pages: 'Pages' + pages: 'Pages', }, indexPage: { - searchHint: 'Search for a Gym' + searchHint: 'Search for a Gym', }, gymPage: { home: 'Home', @@ -15,7 +15,8 @@ export default { weight: 'Weight', reps: 'Repetitions', date: 'Date', - submit: 'Submit' - } - } + upload: 'Video File to Upload', + submit: 'Submit', + }, + }, }; diff --git a/gymboard-app/src/i18n/index.ts b/gymboard-app/src/i18n/index.ts index 965bfc2..1c28ddf 100644 --- a/gymboard-app/src/i18n/index.ts +++ b/gymboard-app/src/i18n/index.ts @@ -3,5 +3,5 @@ import nlNL from './nl-NL'; export default { 'en-US': enUS, - 'nl-NL': nlNL + 'nl-NL': nlNL, }; diff --git a/gymboard-app/src/i18n/nl-NL/index.ts b/gymboard-app/src/i18n/nl-NL/index.ts index b6dce4f..e673316 100644 --- a/gymboard-app/src/i18n/nl-NL/index.ts +++ b/gymboard-app/src/i18n/nl-NL/index.ts @@ -1,10 +1,10 @@ export default { mainLayout: { language: 'Taal', - pages: 'Pagina\'s' + pages: "Pagina's", }, indexPage: { - searchHint: 'Zoek een sportschool' + searchHint: 'Zoek een sportschool', }, gymPage: { home: 'Thuis', @@ -15,7 +15,8 @@ export default { weight: 'Gewicht', reps: 'Repetities', date: 'Datum', - submit: 'Sturen' - } - } -} + upload: 'Videobestand om te uploaden', + submit: 'Sturen', + }, + }, +}; diff --git a/gymboard-app/src/layouts/MainLayout.vue b/gymboard-app/src/layouts/MainLayout.vue index 12072f3..6864950 100644 --- a/gymboard-app/src/layouts/MainLayout.vue +++ b/gymboard-app/src/layouts/MainLayout.vue @@ -12,7 +12,9 @@ /> - Gymboard + Gymboard - + - + {{ $t('mainLayout.pages') }} Gyms @@ -58,17 +54,17 @@ diff --git a/gymboard-app/src/pages/ErrorNotFound.vue b/gymboard-app/src/pages/ErrorNotFound.vue index 9ca9745..e93c6a3 100644 --- a/gymboard-app/src/pages/ErrorNotFound.vue +++ b/gymboard-app/src/pages/ErrorNotFound.vue @@ -1,13 +1,11 @@ - + + - + diff --git a/gymboard-app/src/pages/gym/GymLeaderboardsPage.vue b/gymboard-app/src/pages/gym/GymLeaderboardsPage.vue index 404e098..7c22e9c 100644 --- a/gymboard-app/src/pages/gym/GymLeaderboardsPage.vue +++ b/gymboard-app/src/pages/gym/GymLeaderboardsPage.vue @@ -1,15 +1,10 @@ - + - + diff --git a/gymboard-app/src/pages/gym/GymPage.vue b/gymboard-app/src/pages/gym/GymPage.vue index ddef734..f8b3b69 100644 --- a/gymboard-app/src/pages/gym/GymPage.vue +++ b/gymboard-app/src/pages/gym/GymPage.vue @@ -18,16 +18,16 @@ :color="leaderboardPageSelected ? 'primary' : 'secondary'" /> - + diff --git a/gymboard-app/src/pages/gym/GymSubmissionPage.vue b/gymboard-app/src/pages/gym/GymSubmissionPage.vue index e3101b0..dbcbce8 100644 --- a/gymboard-app/src/pages/gym/GymSubmissionPage.vue +++ b/gymboard-app/src/pages/gym/GymSubmissionPage.vue @@ -8,7 +8,7 @@ A high-level overview of the submission process is as follows: 2. The user submits their lift's JSON data, including the `videoId`. 3. The API responds (if the data is valid) with the created submission, with the status WAITING. 4. Eventually the API will process the submission and status will change to either COMPLETED or FAILED. - +5. We wait on the submission page until the submission is done processing, then show a message and navigate to the submission page. --> - + diff --git a/gymboard-app/src/router/gym-routing.ts b/gymboard-app/src/router/gym-routing.ts index c478ea0..5babc1e 100644 --- a/gymboard-app/src/router/gym-routing.ts +++ b/gymboard-app/src/router/gym-routing.ts @@ -1,5 +1,6 @@ -import {useRoute} from 'vue-router'; -import {getGym, Gym} from 'src/api/gymboard-api'; +import { useRoute } from 'vue-router'; +import { Gym } from 'src/api/main/gyms'; +import api from 'src/api/main'; /** * Any object that contains the properties needed to identify a single gym. @@ -7,7 +8,7 @@ import {getGym, Gym} from 'src/api/gymboard-api'; export interface GymRoutable { countryCode: string; cityShortName: string; - shortName: string + shortName: string; } /** @@ -23,7 +24,7 @@ export function getGymRoute(gym: GymRoutable): string { */ export async function getGymFromRoute(): Promise { const route = useRoute(); - return await getGym( + return await api.gyms.getGym( route.params.countryCode as string, route.params.cityShortName as string, route.params.gymShortName as string diff --git a/gymboard-app/src/router/index.ts b/gymboard-app/src/router/index.ts index 4531114..56c2735 100644 --- a/gymboard-app/src/router/index.ts +++ b/gymboard-app/src/router/index.ts @@ -20,7 +20,9 @@ import routes from './routes'; export default route(function (/* { store, ssrContext } */) { const createHistory = process.env.SERVER ? createMemoryHistory - : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory); + : process.env.VUE_ROUTER_MODE === 'history' + ? createWebHistory + : createWebHashHistory; const Router = createRouter({ scrollBehavior: () => ({ left: 0, top: 0 }), diff --git a/gymboard-app/src/router/routes.ts b/gymboard-app/src/router/routes.ts index 8fcb6fc..b77e582 100644 --- a/gymboard-app/src/router/routes.ts +++ b/gymboard-app/src/router/routes.ts @@ -18,9 +18,9 @@ const routes: RouteRecordRaw[] = [ children: [ { path: '', component: GymHomePage }, { path: 'submit', component: GymSubmissionPage }, - { path: 'leaderboard', component: GymLeaderboardsPage } - ] - } + { path: 'leaderboard', component: GymLeaderboardsPage }, + ], + }, ], }, diff --git a/gymboard-app/src/stores/index.ts b/gymboard-app/src/stores/index.ts index d30b7cf..d7b189d 100644 --- a/gymboard-app/src/stores/index.ts +++ b/gymboard-app/src/stores/index.ts @@ -1,5 +1,5 @@ -import { store } from 'quasar/wrappers' -import { createPinia } from 'pinia' +import { store } from 'quasar/wrappers'; +import { createPinia } from 'pinia'; import { Router } from 'vue-router'; /* @@ -23,10 +23,10 @@ declare module 'pinia' { */ export default store((/* { ssrContext } */) => { - const pinia = createPinia() + const pinia = createPinia(); // You can add Pinia plugins here // pinia.use(SomePiniaPlugin) - return pinia -}) + return pinia; +}); diff --git a/gymboard-app/src/stores/store-flag.d.ts b/gymboard-app/src/stores/store-flag.d.ts index 7677175..b6ca7c8 100644 --- a/gymboard-app/src/stores/store-flag.d.ts +++ b/gymboard-app/src/stores/store-flag.d.ts @@ -1,9 +1,9 @@ /* eslint-disable */ // THIS FEATURE-FLAG FILE IS AUTOGENERATED, // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING -import "quasar/dist/types/feature-flag"; +import 'quasar/dist/types/feature-flag'; -declare module "quasar/dist/types/feature-flag" { +declare module 'quasar/dist/types/feature-flag' { interface QuasarFeatureFlags { store: true; } diff --git a/gymboard-app/src/utils.ts b/gymboard-app/src/utils.ts new file mode 100644 index 0000000..2bab3c8 --- /dev/null +++ b/gymboard-app/src/utils.ts @@ -0,0 +1 @@ +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); diff --git a/gymboard-app/tsconfig.json b/gymboard-app/tsconfig.json index ee0d9cf..bd323f0 100644 --- a/gymboard-app/tsconfig.json +++ b/gymboard-app/tsconfig.json @@ -3,4 +3,4 @@ "compilerOptions": { "baseUrl": "." } -} \ No newline at end of file +}