From 397db98cad37a7db625a3a4ec222c3fad516000b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20P=C4=99dzich?= Date: Thu, 6 Jul 2023 09:55:39 +0200 Subject: [PATCH] Publish the final devlog for RaceMash --- ...ivia-stops-and-ranking-page-in-racemash.md | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 src/content/blog/progress-bar-trivia-stops-and-ranking-page-in-racemash.md diff --git a/src/content/blog/progress-bar-trivia-stops-and-ranking-page-in-racemash.md b/src/content/blog/progress-bar-trivia-stops-and-ranking-page-in-racemash.md new file mode 100644 index 0000000..759121b --- /dev/null +++ b/src/content/blog/progress-bar-trivia-stops-and-ranking-page-in-racemash.md @@ -0,0 +1,263 @@ +--- +title: progress bar, trivia stops, and ranking page in racemash +description: Find out how I've implemented +pubDate: 2023-07-06T07:55:12.475Z +draft: true +categories: + - dev diary +tags: + - formulaone + - racemash + - vue + - vuetify +--- + +Hey! + +I know it's been a couple weeks since my last RaceMash post, although I have anything but abandonned this project. Better yet, I actually released it yesterday. You can [go to racemash.netlify.app](https://racemash.netlify.app) to check out the app in action, or [browse its source code on GitHub](https://github.com/maciejpedzich/racemash). + +Side note: I've actually opted not to use the `racemash.com` domain name, because I now consider this project a small but not borderline basic demo, rather than a product of sorts, so I didn't really feel like setting up and annually renewing a dedicated domain name for it. + +But this was supposed to be a post describing the implementation of the last two core features I was yet to implement at the time of publishing of my last post, so let's just jump into it already. + +## Adding a progress bar + +Vuetify had already got me covered with [an appropriate component](https://vuetifyjs.com/en/components/progress-linear/). I just needed to supply it with a percentage value, which in my case was 100 times the ratio of the votes submitted by the user so far to all the possible votes that can be casted. + +As I estabilished in [the first post of this series](/racemash-my-appwrite-x-hashnode-hackathon-project/#how-is-this-app-supposed-to-work), the number of all votes a user can submit given a set of `n` unique images equals _n choose 2_, which can be simplified to `(n * (n - 1)) / 2`. I made use of Vue 3's `computed` to keep the percentage up-to-date as the number of votes submitted by the user changed. + +This is how I modified my `useVote.ts` file: + +```ts +// ... + +const NUM_POSSIBLE_VOTES = (db.photos.length * (db.photos.length - 1)) / 2; + +const completionPercentage = computed( + () => (db.votes.length / NUM_POSSIBLE_VOTES) * 100 +); + +// ... + +export function useVote() { + return { + // ... + completionPercentage, + // ... + }; +} +``` + +All that was left to do was placing Vuetify's progress bar component, plugging `completionPercentage` computed property into the `model-value` prop, applying some styling touch-ups and having the bar display 2 decimal places, because it ensures constant value updates as one vote fills up the progress bar by about `0.36` percentage points. + +```vue + + + +``` + +## Handling trivia milestones and 100% completion + +With a progress bar out of the way, it was time for me to implement trivia pit stops for submitting 25%, 50%, and 75% of all votes, as well as a congratulations message with a link to the ranking page for reaching the highly coveted 100%. First I had to go back to the `useVote.ts` file to add the following `computed` properties, but also a `shownFactIndexes` field that's an initially empty array of numbers: + +```ts +const userSubmittedAllVotes = computed( + () => completionPercentage.value === 100 +); + +const userReachedTriviaMilestone = computed(() => + [25, 50, 75].includes(completionPercentage.value) +); +``` + +Then I created a `funFacts.json` file that was essentially an array of strings, where each one contained a fun fact. After that I came back to my vote view to implement picking a random fun fact whenever a user reached one of the milestones. + +I added two `ref`s: `funFactToShow` and `canShowFunFact`, which are initally set to be an empty string and `false` respecitvely. Then I placed a `watch` for the `userReachedTriviaMilestone` computed property. If its new value is `false`, I return immediately. Otherwise, I grab a list of indexes of facts that are yet to be shown, pick a random one and use it to get the fun fact to display to finally add it to `shownFactIndexes` array and toggle the `canShowFunFact` flag. + +Here's how a human known as Mac turned this _prompt_ into code: + +```ts +// ... + +import funFacts from '@/funFacts.json'; +import { randomNumber } from '@/utils/randomNumber'; + +const funFactToShow = ref(''); +const canShowFunFact = ref(false); + +const { + // ... + userReachedTriviaMilestone, + shownFactIndexes, + // ... +} = useVote(); + +// ... + +watch(userReachedTriviaMilestone, (valueIsTrue) => { + if (!valueIsTrue) return; + + const availableFunFactIndexes = [...Array(funFacts.length).keys()].filter( + (index) => !shownFactIndexes.value.includes(index) + ); + const funFactIndex = randomNumber(0, availableFunFactIndexes.length - 1); + + funFactToShow.value = funFacts[funFactIndex]; + shownFactIndexes.value.push(funFactIndex); + canShowFunFact.value = true; +}); +``` + +I also had to modify the template to add a couple `v-if`s to toggle between the 100% congratulations message, fun fact pit stop, and the actual voting section. + +```html +
+
+ + + +
+
+``` + +## Ranking page + +Here comes the final feature to implement - the ranking page you can enter after submitting all votes. I wanted to sort the images by their ratings descendingly and display them in a responsive grid, where each image has a caption in the bottom left corner with the photo's position in the ranking as well as the aforementioned rating. I took advantage of Vuetify's card and grid components, but also the `useDisplay` composable to achieve this. + +```vue + + + +``` + +Now I just needed to add an appropriate route record and a route guard to prevent the user from entering `/ranking` before casting all votes: + +```ts +import { + NavigationGuardNext, + RouteLocationNormalized, + createRouter, + createWebHistory +} from 'vue-router'; + +// ... + +import { useVote } from '@/composables/useVote'; + +const { userSubmittedAllVotes } = useVote(); + +const routes = [ + // ... + { + path: '/ranking', + name: 'Ranking', + component: () => import('../views/Ranking.vue'), + beforeEnter: ( + _to: RouteLocationNormalized, + _from: RouteLocationNormalized, + next: NavigationGuardNext + ) => { + if (userSubmittedAllVotes.value) { + return next(); + } else { + return next('/'); + } + } + } +]; +``` + +And that was it! RaceMash was, at long last, ready to deploy. + +## Wrapping up + +Thank you so much for reading in once again. For my next solo project, I'll stick to the topic of Formula One, but this time from a dataviz standpoint. I'm really looking forward to developing this one, as I should be done collecting all the aforementioned data this weekend. So stay tuned for my next post and take care!