Compare commits

...

4 Commits

  1. 5
      client/services/socket.js
  2. 10
      server/routes/athletes.js
  3. 20
      server/schemas/joi.js
  4. 1
      web/src/assets/undraw_finish_line_katerina_limpitsouni_xy20.svg
  5. 87
      web/src/components/Sidebar.vue
  6. 2
      web/src/store/modules/athletes.ts
  7. 8
      web/src/store/modules/user.ts
  8. 68
      web/src/views/Athlete.vue
  9. 174
      web/src/views/Athletes.vue
  10. 74
      web/src/views/DashboardHome.vue
  11. 2
      web/src/views/Login.vue
  12. 30
      web/src/views/Profile.vue

5
client/services/socket.js

@ -6,6 +6,11 @@ const getMAC = require('getmac').default
const socket = io(server_url);
const mac = getMAC();
console.log(chalk.green(`
ID: ${mac}
Use this ID, to adopt the athlete on the user dashboard.
`))
socket.on('connect', () => {
console.log(chalk.green('Connected to server!'));

10
server/routes/athletes.js

@ -39,7 +39,15 @@ router.put('/api/athletes/:id',
const {name, _trainer} = req.body
const updateAthlete = {name, _trainer}
if (name || _trainer)
if (_trainer === '') {
updateAthlete._trainer = undefined
await Athlete.findByIdAndUpdate(req.params.id, updateAthlete, {}, (err, athlete) => {
if (err)
return res.status(400).json({errors: 'Something went wrong!'});
return res.send(athlete)
})
} else if (name || _trainer)
await Athlete.findByIdAndUpdate(req.params.id, updateAthlete, {}, (err, athlete) => {
if (err)
return res.status(400).json({errors: 'Something went wrong!'});

20
server/schemas/joi.js

@ -1,7 +1,7 @@
const {Joi} = require('celebrate');
const guid = {
params:{
params: {
userId: Joi.string().guid().required()
}
}
@ -14,14 +14,18 @@ const userAuthSchema = {
};
const userUpdateSchema = {
body: {
_id: Joi.string().required(),
username: Joi.string().required(),
email: Joi.any(),
password: Joi.string().allow(''),
newPassword: Joi.string().allow(''),
body: {
_id: Joi.string().required(),
username: Joi.string().required(),
__v: Joi.number().integer(),
email: Joi.string().email(),
registered: Joi.string(),
lastLogin: Joi.string(),
password: Joi.string().alphanum().allow(''),
newPassword: Joi.string().alphanum().allow(''),
}
}
};
;
const athleteUpdateSchema = {
body: {

1
web/src/assets/undraw_finish_line_katerina_limpitsouni_xy20.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

87
web/src/components/Sidebar.vue

@ -6,9 +6,10 @@
</p>
<ul class="menu-list">
<li>
<router-link :to="{ name: 'DashboardHome', params: { username: $route.params.username }}" :class="{
<router-link :class="{
'has-background-white':
$route.name==='DashboardHome' }">
$route.name==='DashboardHome' }"
:to="{ name: 'DashboardHome', params: { username: $route.params.username }}">
<a>
<span class=" icon-text
">
@ -21,9 +22,9 @@
</router-link>
</li>
<li>
<router-link :to="{ name: 'Profile', params: { username: $route.params.username }}" :class="{
<router-link :class="{
'has-background-white':
$route.name==='Profile' }">
$route.name==='Profile' }" :to="{ name: 'Profile', params: { username: $route.params.username }}">
<a>
<span class="icon-text">
<span class="icon">
@ -35,10 +36,10 @@
</router-link>
</li>
<li>
<router-link :to="{ name: 'Athletes', params: { username: $route.params.username }}"
:class="{
<router-link :class="{
'has-background-white':
$route.name==='Athletes' }">
$route.name==='Athletes' }"
:to="{ name: 'Athletes', params: { username: $route.params.username }}">
<a>
<span class="icon-text">
<span class="icon">
@ -50,72 +51,14 @@
</router-link>
</li>
</ul>
<p class="menu-label">
Athlete Data
</p>
<ul class="menu-list">
<li>
<router-link v-if="$store.getters.currentAthlete._id === ''" :to="{ name: 'Table', params: { id:
$store.getters.currentAthlete._id }}" :class="{
'has-background-white':
$route.name==='Table' }">
<a>
<span class="icon-text">
<span class="icon">
<i class="fas fa-table"/>
</span>
<span>&nbsp;Table</span>
</span>
</a>
</router-link>
<router-link v-else :to="{ name: 'Athletes', params: { id:
$route.params.username }}" :class="{
'disabled':
$store.getters.currentAthlete === '' }">
<a>
<span class="icon-text">
<span class="icon">
<i class="fas fa-table"/>
</span>
<span>&nbsp;Table</span>
</span>
</a>
</router-link>
</li>
<li>
<router-link v-if="$store.getters.currentAthlete === ''" :to="{ name: 'Chart', params: { id:
$store.getters.currentAthlete._id }}" :class="{
'has-background-white':
$route.name==='Chart' }">
<a>
<span class="icon-text">
<span class="icon">
<i class="fas fa-chart-area"/>
</span>
<span>&nbsp;Chart</span>
</span>
</a>
</router-link>
<router-link v-else :to="{ name: 'Athletes', params: { id: $route.params.username }}">
<a>
<span class="icon-text">
<span class="icon">
<i class="fas fa-chart-area"/>
</span>
<span>&nbsp;Charts</span>
</span>
</a>
</router-link>
</li>
</ul>
<p class="menu-label">
Help
</p>
<ul class="menu-list">
<ul id="help" class="menu-list">
<li>
<router-link :to="{ name: 'Guide'}" :class="{
<router-link :class="{
'has-background-white':
$route.name==='Guide' }">
$route.name==='Guide' }" :to="{ name: 'Guide'}">
<a>
<span class="icon-text">
<span class="icon">
@ -127,9 +70,9 @@
</router-link>
</li>
<li>
<router-link :to="{ name: 'Ticket'}" :class="{
<router-link :class="{
'has-background-white':
$route.name==='Ticket' }">
$route.name==='Ticket' }" :to="{ name: 'Ticket'}">
<a>
<span class="icon-text">
<span class="icon">
@ -141,9 +84,9 @@
</router-link>
</li>
<li>
<router-link :to="{ name: 'Docs'}" :class="{
<router-link :class="{
'has-background-white':
$route.name==='Docs' }">
$route.name==='Docs' }" :to="{ name: 'Docs'}">
<a href="" target="_blank">
<span class="icon-text">
<span class="icon mr-1">

2
web/src/store/modules/athletes.ts

@ -8,7 +8,7 @@ export interface AthleteInterface {
id: string,
socketID: string,
name: string,
_trainer: UserInterface
_trainer: string
}
@Module

8
web/src/store/modules/user.ts

@ -52,6 +52,11 @@ export default class User extends VuexModule {
this.err = err
}
@Mutation
private update_user(user: UserInterface) {
this.user = user
}
@Action
private login(user: UserInterface) {
return new Promise((resolve, reject) => {
@ -132,7 +137,8 @@ export default class User extends VuexModule {
data: {...user}
})
.then((resp: AxiosResponse) => {
this.context.commit('auth_success', resp.data);
console.log(resp.data)
this.context.commit('update_user', resp.data);
resolve(resp)
})
.catch((err: Error) => {

68
web/src/views/Athlete.vue

@ -1,5 +1,30 @@
<template>
<h1 class="title is-2">/athletes/{{ athlete.name }}</h1>
<div class="tile mb-5">
<article class="tile is-child box">
<nav class="level">
<div class="level-left">
<div class="level-item ">
<div class="title">Actions</div>
</div>
</div>
<div class="level-right">
<div class="level-item has-text-centered">
<router-link :to="{ name: 'Table', params: { id: athlete._id }}" @click="saveAthlete(athlete)">
<span class="icon is-medium has-background-primary has-text-white mr-1">
<i class="fa fa-lg fa-table"></i>
</span>
</router-link>
<router-link :to="{ name: 'Chart', params: { id: athlete._id }}" @click="saveAthlete(athlete)">
<span class="icon is-medium has-background-primary has-text-white mr-1">
<i class="fa fa-lg fa-chart-area"></i>
</span>
</router-link>
</div>
</div>
</nav>
</article>
</div>
<div class="tile is-ancestor">
<div class="tile is-6 is-vertical is-parent">
<form @submit="update">
@ -11,10 +36,6 @@
<input v-model="athlete.name" class="input" type="text">
</div>
</div>
<div class="field">
<label class="label">ID</label>
<p>{{ athlete.id }}</p>
</div>
<div class="field">
<label class="label">Client Status</label>
<p v-if="athlete.socketID">Online</p>
@ -50,12 +71,25 @@
<div class="tile is-parent">
<div class="tile is-child box">
<p class="title">Trainer Status</p>
<div v-if="!athlete._trainer" class="notification is-warning is-light mt-5">
<ul>
<p>It seems that this athlete has no trainer attached to him!</p>
<p>By adopting an athlete you can edit his personal details and view his performance stats.</p>
</ul>
<button class="button is-large is-rounded is-primary is-light" @click="update">Adopt</button>
<div v-if="!athlete._trainer">
<div class="notification is-primary is-light mt-5">
<ul>
<p>It seems that this athlete has no trainer attached to him!</p>
<p>By adopting an athlete you can edit his personal details and view his performance stats.</p>
<p>Enter the client id displayed in the clients terminal to adopt him!</p>
</ul>
</div>
<div class="field">
<label class="label">Athlete ID</label>
<div class="control">
<input v-model="clientID" class="input" type="text">
</div>
</div>
<div class="field">
<p class="control">
<button class="button is-medium is-rounded is-primary" @click="update('adopt')">Adopt</button>
</p>
</div>
</div>
<div v-else>
<div class="field">
@ -88,6 +122,7 @@ export default class Athlete extends Vue {
private trainerLogin = '';
private msgError = ''
private msgSuccess = ''
private clientID = ''
mounted() {
this.athlete = this.$store.getters.currentAthlete
@ -106,9 +141,20 @@ export default class Athlete extends Vue {
}
}
private update() {
private update(action: string) {
if (action === 'adopt' && this.clientID === '') {
this.msgError = 'Please give a valid athlete ID!';
return;
}
if (this.clientID != '' && this.clientID != this.athlete.id) {
this.msgError = 'Athlete ID does not match with current athlete!';
return;
}
const user = {_trainer: this.$store.getters.currentUser._id}
Object.assign(this.athlete, user)
this.$store.dispatch('updateAthlete', this.athlete)
.then((res: any) => {
this.msgSuccess = 'Athlete updated'

174
web/src/views/Athletes.vue

@ -1,73 +1,156 @@
<template>
<h1 class="title is-2">/athletes</h1>
<div class="tile">
<table
class="table
<div class="notification is-link is-light">
Your adopted athletes are displayed on this table. Choose an athlete and view his details, data.
</div>
<div class="section ">
<div class="table-container">
<table
class="table
is-striped
is-large
is-hoverable
is-fullwidth
has-text-left"
>
<thead>
<tr>
<th><span class="icon mr-1"><i class="fa fa-id-card"></i></span>
Athlete id
</th>
<th><span class="icon mr-1"><i class="fas fa-running"></i></span>
Name
</th>
<th>Trainer</th>
<th>Actions</th>
</tr>
</thead>
<tr v-for="(athlete, index) in athletes" :key="index">
<td class="is-family-monospace">
<span>{{ athlete.id }}</span>
</td>
<td>
<span>{{ athlete.name }}</span>
</td>
<td v-if="athlete._trainer">
<span class="icon mr-1"><i class="fa fa-check"></i></span>
</td>
<td v-else>
<span class="icon mr-1"><i class="fa fa-times"></i></span>
</td>
<td>
<router-link :to="{ name: 'Athlete', params: { id: athlete._id }}" @click="saveAthlete(athlete)">
has-text-centered">
<thead>
<tr>
<th><span class="icon mr-1"><i class="fa fa-plug"></i></span>
Athlete status
</th>
<th><span class="icon mr-1"><i class="fas fa-running"></i></span>
Name
</th>
<th><span class="icon mr-1"><i class="fas fa-id-badge"></i></span>
ID
</th>
<th>Actions</th>
</tr>
</thead>
<tr v-for="(athlete, index) in myAthletes" :key="index">
<td class="is-family-monospace" v-if="athlete.name">
<span v-if="athlete.socketID">Online</span>
<span v-else>Offline</span>
</td>
<td v-if="athlete.name">
<span>{{ athlete.name }}</span>
</td>
<td v-if="athlete.name">
<span>{{ athlete.id }}</span>
</td>
<td v-if="athlete.name">
<router-link :to="{ name: 'Table', params: { id: athlete._id }}" @click="saveAthlete(athlete)">
<span class="icon is-medium has-background-primary has-text-white mr-1">
<i class="fa fa-lg fa-table"></i>
</span>
</router-link>
<router-link :to="{ name: 'Chart', params: { id: athlete._id }}" @click="saveAthlete(athlete)">
<span class="icon is-medium has-background-primary has-text-white mr-1">
<i class="fa fa-lg fa-chart-area"></i>
</span>
</router-link>
<router-link :to="{ name: 'Athlete', params: { id: athlete._id }}" @click="saveAthlete(athlete)">
<span class="icon is-medium has-background-warning has-text-white mr-1">
<i class="fa fa-lg fa-user-edit"></i>
</span>
</router-link>
<router-link :to="{ name: 'Table', params: { id: athlete._id }}" @click="saveAthlete(athlete)">
</router-link>
<a @click="removeMyAthlete(athlete, index)">
<span class="icon is-medium has-background-danger has-text-white mr-1">
<i class="fa fa-lg fa-user-times"></i>
</span>
</a>
</td>
</tr>
</table>
</div>
</div>
<div class="section is-medium">
<div class="container">
<h3 class="title is-4">Search all athletes</h3>
<p class="subtitle">All athletes are displayed on this table.</p>
<div class="control">
<input class="input is-focused" placeholder="Search Athletes" type="text">
</div>
</div>
<div class="table-container">
<table
class="table
is-striped
is-large
is-hoverable
is-fullwidth
has-text-centered">
<thead>
<tr>
<th><span class="icon mr-1"><i class="fa fa-plug"></i></span>
Athlete status
</th>
<th><span class="icon mr-1"><i class="fas fa-running"></i></span>
Name
</th>
<th>Trainer</th>
<th>Actions</th>
</tr>
</thead>
<tr v-for="(athlete, index) in athletes" :key="index">
<td class="is-family-monospace">
<span v-if="athlete.socketID">Online</span>
<span v-else>Offline</span>
</td>
<td>
<span>{{ athlete.name }}</span>
</td>
<td v-if="athlete._trainer">
<span class="icon mr-1"><i class="fa fa-check"></i></span>
</td>
<td v-else>
<span class="icon mr-1"><i class="fa fa-times"></i></span>
</td>
<td>
<router-link :to="{ name: 'Table', params: { id: athlete._id }}" @click="saveAthlete(athlete)">
<span class="icon is-medium has-background-primary has-text-white mr-1">
<i class="fa fa-lg fa-table"></i>
</span>
</router-link>
<router-link :to="{ name: 'Chart', params: { id: athlete._id }}" @click="saveAthlete(athlete)">
</router-link>
<router-link :to="{ name: 'Chart', params: { id: athlete._id }}" @click="saveAthlete(athlete)">
<span class="icon is-medium has-background-primary has-text-white mr-1">
<i class="fa fa-lg fa-chart-area"></i>
</span>
</router-link>
</td>
</tr>
</table>
</router-link>
<router-link :to="{ name: 'Athlete', params: { id: athlete._id }}" @click="saveAthlete(athlete)">
<span class="icon is-medium has-background-warning has-text-white mr-1">
<i class="fa fa-lg fa-user-edit"></i>
</span>
</router-link>
</td>
</tr>
</table>
</div>
</div>
</template>
<script lang="ts">
import {Vue} from "vue-class-component";
import {AthleteInterface} from "@/store/modules/athletes";
import {UserInterface} from "@/store/modules/user";
export default class Athletes extends Vue {
private athletes = [<AthleteInterface>{}]
private myAthletes = [<AthleteInterface>{}]
private user = <UserInterface>{}
private msg = ''
mounted() {
this.myAthletes = [<AthleteInterface>{}]
this.$store.dispatch('getAthletes')
.then((res: any) => this.athletes = res.data)
.then((res: any) => {
this.athletes = res.data;
this.user = this.$store.getters.currentUser
for (const athlete of this.athletes) {
if (athlete._trainer === this.user._id)
this.myAthletes.push(athlete)
}
})
.catch((err: any) => this.msg = err.response.data.errors.message || err.message || 'Something went wrong!')
}
@ -75,6 +158,13 @@ export default class Athletes extends Vue {
this.$store.dispatch('saveAthlete', athlete)
}
private removeMyAthlete(athlete: AthleteInterface, index: number) {
athlete._trainer = ''
this.$store.dispatch('updateAthlete', athlete)
.then(() => this.myAthletes.splice(index, 1))
}
}
</script>

74
web/src/views/DashboardHome.vue

@ -1,7 +1,7 @@
<template>
<h1 class="title is-2">/home</h1>
<div class="tile">
<article class="tile is-child notification has-background-primary-dark has-text-white">
<div class="tile mb-5">
<article class="tile is-child box">
<nav class="level">
<div class="level-left">
<div class="level-item ">
@ -17,34 +17,58 @@
</div>
</div>
</nav>
<nav class="level">
<div class="level-item has-text-centered">
<div>
<p class="heading">Your Athletes</p>
<p class="title">3,456</p>
</div>
</article>
</div>
<div class="tile is-ancestor">
<div class="tile is-vertical is-8">
<div class="tile">
<div class="tile is-parent is-vertical">
<article class="tile is-child box">
<p class="title">System status</p>
<p class="subtitle">All systems operational</p>
</article>
<article class="tile is-child box">
<p class="title">...tiles</p>
<p class="subtitle">Bottom tile</p>
</article>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Live Athletes</p>
<p class="title">123</p>
</div>
<div class="tile is-parent">
<article class="tile is-child box">
<figure class="image is-4by3">
<img src="../assets/undraw_finish_line_katerina_limpitsouni_xy20.svg">
</figure>
</article>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Server</p>
<p class="title" v-if="this.$store.getters.currentStatus">Connected</p>
<p class="title" v-else>Disconnected</p>
</div>
<div class="tile is-parent">
<article class="tile is-child box">
<p class="title">Wide tile</p>
<p class="subtitle">Aligned with the right tile</p>
<div class="content">
<!-- Content -->
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Likes</p>
<p class="title">789</p>
</article>
</div>
</div>
<div class="tile is-parent">
<article class="tile is-child box">
<div class="content">
<p class="title">Tall tile</p>
<p class="subtitle">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Assumenda aut culpa, deserunt
distinctio illo ipsa itaque laborum magnam minima minus molestiae nisi odio provident quas repellendus,
similique velit veniam voluptatem?</p>
<p class="subtitle">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Assumenda aut culpa, deserunt
distinctio illo ipsa itaque laborum magnam minima minus molestiae nisi odio provident quas repellendus,
similique velit veniam voluptatem?</p>
<p class="subtitle">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Assumenda aut culpa, deserunt
distinctio illo ipsa itaque laborum magnam minima minus molestiae nisi odio provident quas repellendus,
similique velit veniam voluptatem?</p>
<div class="content">
<!-- Content -->
</div>
</div>
</nav>
</article>
</article>
</div>
</div>
</template>

2
web/src/views/Login.vue

@ -73,7 +73,7 @@ export default class Login extends Vue {
this.$store.dispatch('login', user)
.then(() => this.$router.push({name: 'Dashboard', params: {username: this.username}}))
.catch(() => {
this.msg = this.$store.getters.getErr.response.data.errors.message || 'Something went wrong!'
this.msg = this.$store.getters.getErrUser.response.data.errors.message || 'Something went wrong!'
})
}
}

30
web/src/views/Profile.vue

@ -1,6 +1,6 @@
<template>
<h1 class="title is-2">/profile</h1>
<form @submit="update">
<form @submit.prevent="update">
<div class="tile is-ancestor">
<div class="tile is-4 is-vertical is-parent">
<div class="tile is-child box">
@ -31,25 +31,25 @@
<p class="title">Change Password</p>
<div class="field">
<p class="control has-icons-left">
<input v-model="user.currentPassword" class="input" placeholder="Current Password" type="password">
<input v-model="currentPassword" class="input" placeholder="Current Password" type="password">
<span class="icon is-small is-left"><i class="fas fa-lock"></i></span>
</p>
</div>
<div class="field">
<p class="control has-icons-left">
<input v-model="user.newPassword" class="input" placeholder="New Password" type="password">
<input v-model="newPassword" class="input" placeholder="New Password" type="password">
<span class="icon is-small is-left"><i class="fas fa-lock"></i></span>
</p>
</div>
<div class="field">
<p class="control has-icons-left">
<input v-model="user.repeatNewPassword" class="input" placeholder="Confirm Password" type="password">
<input v-model="repeatNewPassword" class="input" placeholder="Confirm Password" type="password">
<span class="icon is-small is-left"><i class="fas fa-lock"></i></span>
</p>
</div>
<div class="field">
<p class="control">
<button class="button is-primary is-rounded" type="submit">
<button class="button is-primary is-rounded">
Update
</button>
</p>
@ -66,7 +66,7 @@
<li>Has 12 Characters, Minimum</li>
<li>Includes Numbers, Symbols, Capital Letters, and Lower-Case Letters</li>
<li>Isnt a Dictionary Word or Combination of Dictionary Words</li>
<li>Doesnt Rely on Obvious Substitutions</li>
<li>Doesn't Rely on Obvious Substitutions</li>
</ul>
</div>
</div>
@ -87,30 +87,26 @@ interface UpdatedUser extends UserInterface {
export default class Profile extends Vue {
private user = <UpdatedUser>{};
private date = '';
private currentPassword = '';
private newPassword = '';
private repeatNewPassword = '';
private msgSuccess = '';
private msgError = '';
created() {
mounted() {
this.user = this.$store.getters.currentUser
this.date = new Date(this.user.registered).toLocaleString()
}
private update() {
if (this.user.newPassword != this.user.repeatNewPassword) {
if (this.newPassword != this.repeatNewPassword) {
this.msgError = 'Passwords do not match!'
return
}
let user = {
_id: this.user._id,
username: this.user.username,
email: this.user.email,
password: this.user.password,
newPassword: this.user.newPassword,
repeatNewPassword: this.user.repeatNewPassword
}
Object.assign(this.user, {password: this.currentPassword, newPassword: this.newPassword})
this.$store.dispatch('updateUser', user)
this.$store.dispatch('updateUser', this.user)
.then(() => this.msgSuccess = 'User updated!')
.catch(() => this.msgError = this.$store.getters.getErrUser.response.data.errors.message ||
'Something went wrong!')

Loading…
Cancel
Save