Skip to content
This repository has been archived by the owner on Jan 31, 2024. It is now read-only.

Dennis de vries #1

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 15 additions & 33 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,16 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
parserOptions: {
parser: '@babel/eslint-parser',
requireConfigFile: false,
},
extends: ['airbnb-base', '@nuxtjs', 'plugin:nuxt/recommended', 'prettier'],
plugins: [],
// add your custom rules here
rules: {
'prettier/prettier': 0,
'no-console': 0,
'no-param-reassign': [
'error',
{
ignorePropertyModificationsFor: ['state'],
},
],
'no-shadow': [
'error',
{
allow: ['getters', 'state'],
},
],
'vue/no-v-html': 0,
'vue/html-indent': ['error', 4],
'vue/max-attributes-per-line': 0,
'vue/singleline-html-element-content-newline': 0,
},
};
root: true,
env: {
browser: true,
node: true
},
extends: [
'@nuxtjs/eslint-config-typescript',
'plugin:nuxt/recommended',
'prettier'
],
plugins: [
],
// add your custom rules here
rules: {}
}
39 changes: 39 additions & 0 deletions components/logo/23GLogo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<template>
<svg width="93px" height="50px" viewBox="0 0 93 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="logo" data-v-e1381740="" data-v-3a83f8ae=""><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" data-v-e1381740=""><g data-v-e1381740=""><path d="M24.9863487,0.0272861842 C11.1861842,0.0272861842 0,11.2136349 0,25.0136349 C0,38.8128125 11.1861842,49.9999836 24.9863487,49.9999836 C38.7858553,49.9999836 49.9726974,38.8128125 49.9726974,25.0136349 C49.9726974,11.2136349 38.7858553,0.0272861842 24.9863487,0.0272861842" class="logo-circle transition duration-300" data-v-e1381740=""></path></g> <g transform="translate(15.000000, 19.000000)" class="logo-stripes transition duration-300" data-v-e1381740=""><path d="M12.3021095,0.0661714105 C11.748681,-0.139124738 11.1434106,0.149556919 10.9442908,0.706887164 L7.06372666,11.556078 C6.86443853,12.1072443 7.14805382,12.7306665 7.69845263,12.9337368 C7.81930472,12.9780835 7.94318654,13 8.06504854,13 C8.49745666,13 8.89956759,12.7280982 9.05593472,12.2926786 L12.9373404,1.44451503 C13.1341038,0.892835135 12.8525083,0.269412885 12.3021095,0.0661714105" data-v-e1381740=""></path> <path d="M19.3021095,0.0661714105 C18.748681,-0.139124738 18.1434106,0.149556919 17.9442908,0.706887164 L14.0637267,11.556078 C13.8644385,12.1072443 14.1480538,12.7306665 14.6984526,12.9337368 C14.8193047,12.9780835 14.9431865,13 15.0650485,13 C15.4974567,13 15.8995676,12.7280982 16.0559347,12.2926786 L19.9373404,1.44451503 C20.1341038,0.892835135 19.8525083,0.269412885 19.3021095,0.0661714105" data-v-e1381740=""></path> <path d="M5.30210949,0.0661714105 C4.74868096,-0.139124738 4.14341058,0.149556919 3.94429077,0.706887164 L0.0637266601,11.556078 C-0.135561473,12.1072443 0.148053818,12.7306665 0.69845263,12.9337368 C0.819304724,12.9780835 0.943186537,13 1.06504854,13 C1.49745666,13 1.89956759,12.7280982 2.05593472,12.2926786 L5.93734041,1.44451503 C6.13410378,0.892835135 5.8525083,0.269412885 5.30210949,0.0661714105" data-v-e1381740=""></path></g> <g data-v-e1381740=""><path d="M60.4685864,20.1634121 C59.6633619,20.9379925 59.2344324,21.967112 59.1863095,23.251456 L61.677175,23.251456 C61.7010694,22.6659815 61.8883814,22.166838 62.2344324,21.7541966 C62.5816531,21.3415553 63.0536928,21.135149 63.6483792,21.135149 C64.1687089,21.135149 64.6068286,21.3131209 64.9593962,21.6694073 C65.3122981,22.0241521 65.4889161,22.4881809 65.4889161,23.0609798 C65.4889161,23.6834532 65.247967,24.3626242 64.7655676,25.1005481 C64.5409937,25.4431312 63.8345216,26.3254539 62.6441462,27.7495718 L59,32 L68,32 L68,29.8658787 L63.5180461,29.8658787 L65.6565111,27.4821857 C66.1150162,26.9739637 66.4684193,26.5642343 66.7160521,26.2531689 C66.9633508,25.9421035 67.2251866,25.4806441 67.4977164,24.8708462 C67.769912,24.2613909 67.905425,23.6509078 67.905425,23.0416238 C67.905425,21.94827 67.5186031,21.0015416 66.7451264,20.2010963 C65.9693105,19.3999657 64.9007464,19 63.5364264,19 C62.2969255,19 61.2746463,19.387804 60.4685864,20.1634121" fill="#FFFFFF" data-v-e1381740=""></path> <path d="M71.8072553,19.932605 C71.054261,20.5541135 70.62119,21.4182704 70.5080422,22.525589 L72.8052015,22.525589 C72.8307678,22.0153087 73.0092131,21.6172831 73.3414012,21.3309991 C73.6735893,21.0445439 74.0715931,20.902343 74.5371401,20.902343 C75.0389635,20.902343 75.4402495,21.0507042 75.7415163,21.3503357 C76.0450288,21.6477425 76.193762,22.0464525 76.193762,22.5433855 C76.193762,23.0161906 76.0502111,23.4178097 75.7612092,23.7477294 C75.4720345,24.0762801 75.0455278,24.2402132 74.4822073,24.2402132 L74.1410365,24.2402132 L74.1410365,25.994024 L74.4623417,25.994024 C75.1659309,25.994024 75.6877927,26.1868764 76.0343186,26.5727524 C76.3787716,26.9572594 76.553071,27.4126102 76.553071,27.9335001 C76.553071,28.468764 76.3787716,28.9318152 76.0343186,29.323167 C75.6877927,29.71469 75.1906334,29.9106226 74.5371401,29.9106226 C73.9485988,29.9106226 73.469405,29.750283 73.1061228,29.4261814 C72.7426679,29.1034487 72.5217274,28.673766 72.4474472,28.1396999 L70,28.1396999 C70.0993282,29.3084507 70.5705758,30.2432802 71.4114971,30.9464131 C72.2525912,31.6486903 73.2890595,32 74.5181382,32 C75.8113052,32 76.8821497,31.6461235 77.7282534,30.9368303 C78.5760845,30.2285639 79,29.2516388 79,28.0081085 C79,26.5534158 78.3592898,25.5653679 77.078906,25.042938 C78.0461036,24.5083586 78.5294434,23.6380413 78.5294434,22.4321574 C78.5294434,21.3867843 78.1732438,20.5541135 77.4642994,19.932605 C76.7550096,19.3112676 75.7990403,19 74.5934549,19 C73.4878887,19 72.560595,19.3112676 71.8072553,19.932605" fill="#FFFFFF" data-v-e1381740=""></path> <path d="M82.7409242,20.9024891 C81.5807423,22.1210199 81,23.6634769 81,25.5278067 C81,27.4927538 81.5859537,29.0896263 82.7573726,30.3208198 C83.8229355,31.4409577 85.2541087,32 87.0539866,32 C88.8177105,32 90.2190812,31.4525938 91.2603786,30.3584657 C92.4207233,29.1525977 93,27.4374827 93,25.210554 L93,25.0618525 L87.5687725,25.0618525 L87.5687725,27.150518 L90.6216598,27.150518 C90.4443102,27.9599058 90.0713714,28.5935554 89.5010518,29.0530071 C88.8753613,29.5501046 88.1011468,29.7989101 87.1787338,29.7989101 C86.1237565,29.7989101 85.249223,29.4450382 84.551062,28.7357544 C83.7690303,27.9405694 83.3791545,26.8890498 83.3791545,25.5842756 C83.3791545,24.2033539 83.775056,23.1090548 84.5697903,22.3018915 C85.2786999,21.5676245 86.1890615,21.2010899 87.3029925,21.2010899 C87.9166316,21.2010899 88.4737599,21.3309684 88.9709575,21.5929499 C89.5735224,21.9286833 90.0360318,22.4196206 90.3549026,23.0654197 L92.3611183,21.9286833 C91.7924272,20.8963289 91.0537151,20.1379342 90.142702,19.6524727 C89.3608333,19.2173198 88.4506345,19 87.4072199,19 C85.4796634,19 83.9248829,19.634163 82.7409242,20.9024891" fill="#FFFFFF" data-v-e1381740=""></path></g></g></svg>
</template>

<script>
export default {
name: 'Logo23GLogo'
}
</script>

<style scoped>
.logo {
width: auto;
height: 40px;
}

@media (min-width: 768px) {
.logo {
width: auto;
height: 48px;
}
}

.logo-circle {
fill: #e51c2a;
}

.logo-stripes {
fill: #fff;
}

.logo.inverted .logo-circle {
fill: #fff;
}

.logo.inverted .logo-stripes {
fill: #e51c2a;
}
</style>
202 changes: 202 additions & 0 deletions components/timing/Form.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<template>
<v-form ref="Form" @submit.prevent="() => {}">
<v-subheader>
New laptime
</v-subheader>

<v-row class="px-4">
<!-- Laptime -->
<v-col cols="12">
<v-text-field
v-model.number="form.laptime"
type="number"
label="Laptime (sec) *"
outlined
dense
persistent-placeholder
placeholder="123,1234"
hide-details="auto"
:min="0.0000"
:error-messages="laptimeErrors"
@change="() => $v.form.laptime.$touch()"
/>
</v-col>

<!-- User -->
<v-col cols="12">
<v-select
v-model="form.userId"
:items="userOptions"
outlined
dense
hide-selected
clearable
persistent-placeholder
label="User *"
hide-details="auto"
:error-messages="userIdErrors"
@change="() => $v.form.userId.$touch()"
/>
</v-col>

<!-- Submit -->
<v-col cols="12">
<v-btn
type="submit"
small
depressed
color="primary"
:loading="loading"
:disabled="$v.$invalid"
@click.stop="onSubmit"
>
Submit
</v-btn>
</v-col>

<v-col v-if="error" cols="12">
<small class="error--text" v-text="error" />
</v-col>

<v-col v-if="success" cols="12">
<small class="success--text" v-text="success" />
</v-col>
</v-row>
</v-form>
</template>

<script lang="ts">
import { defineComponent, PropType } from '@nuxtjs/composition-api'
import { required } from 'vuelidate/lib/validators'
import { IForm, ITiming, IUser, TUserList } from '~/types'

export default defineComponent({
name: 'TimingForm',
props: {
timing: {
type: Object as PropType<ITiming>,
default: undefined,
},
},
validations: {
form: {
laptime: {
required,
},
userId: {
required,
},
datetime: {
required,
},
},
},
data: () => ({
error: undefined as string | undefined,
success: undefined as string | undefined,
form: {
id: undefined,
laptime: undefined,
userId: undefined,
datetime: undefined,
} as IForm,
}),
computed: {
loading(): boolean {
return this.$store.state.loading
},
users(): TUserList {
return this.$store.state.users.users
},
userOptions(): Array<{ text: string, value: IUser['id'] }> {
// Map text and value properties or users for use in v-select
return this.users.map((user: IUser) => {
let fullname = user.first_name

if (user.last_name_prefix)
fullname = `${fullname} ${user.last_name_prefix}`

fullname = `${fullname} ${user.last_name}`

return {
text: fullname,
value: user.id,
}
})
},
laptimeErrors() {
// Error messages for laptime input
const errors: Array<string> = []

if (!this.$v.form.laptime?.$dirty) return errors

!this.$v.form.laptime?.required && errors.push('Laptime is required.')

return errors
},
userIdErrors() {
// Error messages for user input
const errors: Array<string> = []

if (!this.$v.form.userId?.$dirty) return errors

!this.$v.form.userId?.required && errors.push('User is required.')

return errors
},
},
async beforeMount() {
await this.$store.dispatch('users/fetchUsers')
},
mounted() {
// Init form for edition
this.form = { ...this.form, ...this.timing }

// Set time to now if we're creating a new timing
if (!this.timing)
this.form.datetime = new Date().toISOString()
},
methods: {
resetForm() {
this.error = undefined
this.success = undefined

this.form = {
id: undefined,
laptime: undefined,
userId: undefined,
}

this.$v.$reset()
},
async onSubmit() {
// Unset error and success messages
this.error = undefined
this.success = undefined

// If invalid, do nothing
if (this.$v.$invalid) return

// Prepare the payload
const payload: IForm = { ...this.form }

// Set time to now if we're creating a new timing
if (!this.timing)
payload.datetime = new Date().toISOString()

// Are we updating or adding a timing?
const promise = this.timing ? 'timings/updateTiming' : 'timings/addTiming'

// Add/update
try {
await this.$store.dispatch(promise, payload)
this.success = 'Success!'
return true
} catch (error) {
this.error = error.message
return false
}
},
},
})
</script>
93 changes: 93 additions & 0 deletions components/timing/Leaderboard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<template>
<!--
There is a bug in the combination of SSR and v-data-table which causes
an error stating that the virtual DOM does not match server-rendered content.
It can be resolved, but not in the time that is left for the assignment.
-->
<client-only>
<v-card>
<v-card-title>
<v-toolbar-title>
Leaderboard
</v-toolbar-title>

<v-spacer />

<v-text-field
v-model="search"
append-icon="mdi-magnify"
label="Search by user"
single-line
hide-details
/>
</v-card-title>

<v-data-table
:items="items"
:headers="headers"
:search="search"
must-sort
sort-by="laptime"
/>
</v-card>
</client-only>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import { DataTableHeader } from 'vuetify'
import { ITiming, TTimingList, TUserList } from '~/types'

export default defineComponent({
name: 'TimingLeaderboard',
data: () => ({
search: '',
headers: [
{ text: 'Laptime (sec)', value: 'laptime', sortable: true },
{ text: 'User', value: 'user', filterable: true },
{ text: 'Datetime', value: 'datetime', sortable: true },
] as Array<DataTableHeader>
}),
computed: {
timings(): TTimingList {
return this.$store.state.timings.timings
},
users(): TUserList {
return this.$store.state.users.users
},
items(): { [key: number]: any } {
const userTimings: { [key: number]: any } = {}

// Map the fastest lap per user
for (const user of this.users) {
// Find all laps made by the current user and sort by most recent
const laps = this.timings.filter((timing: ITiming) => timing.userId === user.id)
.map((timing: ITiming) => timing)
.sort((a, b) => a.datetime > b.datetime ? -1 : 1)
if (!laps.length) continue

// Get the fastest lap the current user made
const fastestLap = laps.reduce((res, obj) => (obj.laptime < res.laptime) ? obj : res)

// Create a string of the user's full name
let fullname = user.first_name

if (user.last_name_prefix)
fullname = `${fullname} ${user.last_name_prefix}`

fullname = `${fullname} ${user.last_name}`

userTimings[user.id] = {
...fastestLap,
user: fullname,
}
}

return Object.values(userTimings)
},
},
async beforeMount() {
await this.$store.dispatch('users/fetchUsers')
},
})
</script>
Loading