Client - add Sport administration

This commit is contained in:
Sam 2021-10-27 18:51:59 +02:00
parent 95dec79814
commit e4434acc94
14 changed files with 342 additions and 113 deletions

View File

@ -23,7 +23,11 @@
}}
</span>
</dd>
<dt>{{ capitalize($t('workouts.SPORT', 0)) }}</dt>
<dt>
<router-link to="/admin/SPORTS">
{{ capitalize($t('workouts.SPORT', 0)) }}
</router-link>
</dt>
<dd>
{{ $t('admin.ENABLE_DISABLE_SPORTS') }}
</dd>

View File

@ -0,0 +1,171 @@
<template>
<div id="admin-sports" class="admin-card">
<Card>
<template #title>{{ $t('admin.SPORTS.TITLE') }}</template>
<template #content>
<button class="top-button" @click.prevent="$router.push('/admin')">
{{ $t('admin.BACK_TO_ADMIN') }}
</button>
<div class="responsive-table">
<table>
<thead>
<tr>
<th>#</th>
<th>{{ $t('admin.SPORTS.TABLE.IMAGE') }}</th>
<th class="text-left">
{{ $t('admin.SPORTS.TABLE.LABEL') }}
</th>
<th>{{ $t('admin.SPORTS.TABLE.ACTIVE') }}</th>
<th class="text-left sport-action">
{{ $t('admin.SPORTS.TABLE.ACTION') }}
</th>
<th />
</tr>
</thead>
<tbody>
<tr v-for="sport in translatedSports" :key="sport.id">
<td class="center-text">
<span class="cell-heading">id</span>
{{ sport.id }}
</td>
<td>
<span class="cell-heading">
{{ $t('admin.SPORTS.TABLE.IMAGE') }}
</span>
<SportImage
:title="sport.translatedLabel"
:sport-label="sport.label"
/>
</td>
<td class="sport-label">
<span class="cell-heading">
{{ $t('admin.SPORTS.TABLE.LABEL') }}
</span>
{{ sport.translatedLabel }}
</td>
<td class="center-text">
<span class="cell-heading">
{{ $t('admin.SPORTS.TABLE.ACTIVE') }}
</span>
<i
:class="`fa fa${sport.is_active ? '-check' : ''}-square-o`"
aria-hidden="true"
/>
</td>
<td class="sport-action">
<span class="cell-heading">
{{ $t('admin.SPORTS.TABLE.ACTION') }}
</span>
<div class="action-button">
<button
:class="{ danger: sport.is_active }"
@click="updateSportStatus(sport.id, !sport.is_active)"
>
{{ $t(`buttons.${sport.is_active ? 'DIS' : 'EN'}ABLE`) }}
</button>
<span v-if="sport.has_workouts" class="has-workouts">
<i class="fa fa-warning" aria-hidden="true" />
{{ $t('admin.SPORTS.TABLE.HAS_WORKOUTS') }}
</span>
</div>
</td>
</tr>
</tbody>
</table>
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
<button @click.prevent="$router.push('/admin')">
{{ $t('admin.BACK_TO_ADMIN') }}
</button>
</div>
</template>
</Card>
</div>
</template>
<script lang="ts">
import { ComputedRef, computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import { ROOT_STORE, SPORTS_STORE } from '@/store/constants'
import { ITranslatedSport } from '@/types/sports'
import { useStore } from '@/use/useStore'
import { translateSports } from '@/utils/sports'
export default defineComponent({
name: 'AdminSports',
setup() {
const { t } = useI18n()
const store = useStore()
const translatedSports: ComputedRef<ITranslatedSport[]> = computed(() =>
translateSports(store.getters[SPORTS_STORE.GETTERS.SPORTS], t)
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
function updateSportStatus(id: number, isActive: boolean) {
store.dispatch(SPORTS_STORE.ACTIONS.UPDATE_SPORTS, {
id,
isActive,
})
}
return { errorMessages, translatedSports, updateSportStatus }
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base.scss';
#admin-sports {
table {
td {
font-size: 1.1em;
}
}
.center-text {
text-align: center;
}
.sport-img {
height: 35px;
width: 35px;
margin: 0 auto;
}
.has-workouts {
font-size: 0.95em;
font-style: italic;
padding: 0 $default-padding;
}
.text-left {
text-align: left;
}
.sport-action {
padding-left: $default-padding * 4;
}
.action-button {
display: block;
}
.top-button {
display: none;
}
@media screen and (max-width: $small-limit) {
.sport-action {
padding-left: $default-padding;
}
.has-workouts {
padding-top: $default-padding * 0.5;
}
.action-button {
display: flex;
align-items: center;
flex-wrap: wrap;
flex-direction: column;
}
.top-button {
display: block;
margin-bottom: $default-margin * 2;
}
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="workouts-list">
<div class="box" :class="{ 'empty-table': workouts.length === 0 }">
<div class="workouts-table">
<div class="workouts-table responsive-table">
<table>
<thead>
<tr>
@ -210,122 +210,35 @@
}
}
.workouts-table {
margin-bottom: 15px;
/* responsive table, adapted from: */
/* https://uglyduck.ca/making-tables-responsive-with-minimal-css/ */
table {
width: 100%;
padding: $default-padding;
font-size: 0.9em;
border-collapse: collapse;
thead th {
vertical-align: center;
padding: $default-padding;
border-bottom: 2px solid var(--card-border-color);
.sport-col {
padding-right: 0;
}
.workout-title {
max-width: 90px;
position: relative;
.fa-map-o {
font-size: 0.75em;
}
tbody {
font-size: 0.95em;
td {
padding: $default-padding;
border-bottom: 1px solid var(--card-border-color);
}
tr:last-child td {
border: none;
}
}
.sport-col {
padding-right: 0;
}
.workout-title {
max-width: 90px;
position: relative;
.fa-map-o {
font-size: 0.75em;
}
.static-map {
display: none;
box-shadow: 3px 3px 3px 1px lightgrey;
}
}
.workout-title:hover .static-map {
display: block;
}
.cell-heading {
background: var(--cell-heading-bg-color);
color: var(--cell-heading-color);
.static-map {
display: none;
font-size: 10px;
font-weight: bold;
padding: 5px;
position: absolute;
text-transform: uppercase;
top: 0;
left: 0;
}
.sport-img {
height: 20px;
width: 20px;
box-shadow: 3px 3px 3px 1px lightgrey;
}
}
.workout-title:hover .static-map {
display: block;
}
.sport-img {
height: 20px;
width: 20px;
}
@media screen and (max-width: $small-limit) {
table {
thead {
left: -9999px;
position: absolute;
visibility: hidden;
}
tr {
border-bottom: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 40px;
}
td {
border: 1px solid var(--card-border-color);
margin: 0 -1px -1px 0;
padding-top: 25px !important;
position: relative;
text-align: center;
width: 45%;
}
tbody {
tr:last-child td {
border: 1px solid var(--card-border-color);
}
}
.sport-col {
display: flex;
justify-content: center;
padding: $default-padding;
}
.cell-heading {
display: flex;
}
.workout-title {
max-width: initial;
}
.sport-col {
display: flex;
justify-content: center;
padding: $default-padding;
}
}
@media screen and (max-width: $x-small-limit) {
table {
td {
width: 100%;
}
.workout-title {
max-width: initial;
}
}
}

View File

@ -15,6 +15,16 @@
"ENABLE_DISABLE_SPORTS": "Enable/disable sports.",
"REGISTRATION_DISABLED": "Registration is currently disabled.",
"REGISTRATION_ENABLED": "Registration is currently enabled.",
"SPORTS": {
"TABLE": {
"ACTION": "Action",
"ACTIVE": "Active",
"HAS_WORKOUTS": "workouts exist",
"IMAGE": "Image",
"LABEL": "Label"
},
"TITLE": "Sports administration"
},
"UPDATE_APPLICATION_DESCRIPTION": "Update application configuration (maximum number of registered users, maximum files size).",
"USER": "user | users"
}

View File

@ -1,7 +1,9 @@
{
"CANCEL": "Cancel",
"DELETE_MY_ACCOUNT": "Delete my account",
"DISABLE": "Disable",
"EDIT": "Edit",
"ENABLE": "Enable",
"FILTER": "Filter",
"LOGIN": "Log in",
"NO": "No",

View File

@ -15,6 +15,16 @@
"ENABLE_DISABLE_SPORTS": "Activer/désactiver des sports.",
"REGISTRATION_DISABLED": "Les inscriptions sont actuellement désactivées.",
"REGISTRATION_ENABLED": "Les inscriptions sont actuellement activées.",
"SPORTS": {
"TABLE": {
"ACTION": "Action",
"ACTIVE": "Actif",
"HAS_WORKOUTS": "des séances existent",
"IMAGE": "Image",
"LABEL": "Label"
},
"TITLE": "Administration - Sports"
},
"UPDATE_APPLICATION_DESCRIPTION": "Configurer l'application (nombre maximum d'utilisateurs inscrits, taille maximale des fichers).",
"USER": "utilisateur | utilisateurs"
}

View File

@ -1,7 +1,9 @@
{
"CANCEL": "Annuler",
"DELETE_MY_ACCOUNT": "Supprimer mon compte",
"DISABLE": "Désactiver",
"EDIT": "Modifier",
"ENABLE": "Activer",
"FILTER": "Filtrer",
"LOGIN": "Se connecter",
"NO": "Non",

View File

@ -2,6 +2,7 @@ import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import AdminApplication from '@/components/Administration/AdminApplication.vue'
import AdminMenu from '@/components/Administration/AdminMenu.vue'
import AdminSports from '@/components/Administration/AdminSports.vue'
import Profile from '@/components/User/ProfileDisplay/index.vue'
import UserInfos from '@/components/User/ProfileDisplay/UserInfos.vue'
import UserPreferences from '@/components/User/ProfileDisplay/UserPreferences.vue'
@ -186,6 +187,11 @@ const routes: Array<RouteRecordRaw> = [
component: AdminApplication,
props: { edition: true },
},
{
path: 'sports',
name: 'SportsAdministration',
component: AdminSports,
},
],
},
{

View File

@ -231,3 +231,83 @@ button {
}
}
}
.responsive-table {
margin-bottom: 15px;
/* responsive table, adapted from: */
/* https://uglyduck.ca/making-tables-responsive-with-minimal-css/ */
table {
width: 100%;
padding: $default-padding;
font-size: 0.9em;
border-collapse: collapse;
thead th {
vertical-align: center;
padding: $default-padding;
border-bottom: 2px solid var(--card-border-color);
}
tbody {
font-size: 0.95em;
td {
padding: $default-padding;
border-bottom: 1px solid var(--card-border-color);
}
tr:last-child td {
border: none;
}
}
.cell-heading {
background: var(--cell-heading-bg-color);
color: var(--cell-heading-color);
display: none;
font-size: 10px;
font-weight: bold;
padding: 5px;
position: absolute;
text-transform: uppercase;
top: 0;
left: 0;
}
}
@media screen and (max-width: $small-limit) {
table {
thead {
left: -9999px;
position: absolute;
visibility: hidden;
}
tr {
border-bottom: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 40px;
}
td {
border: 1px solid var(--card-border-color);
margin: 0 -1px -1px 0;
padding-top: 25px !important;
position: relative;
text-align: center;
width: 45%;
}
tbody {
tr:last-child td {
border: 1px solid var(--card-border-color);
}
}
.cell-heading {
display: flex;
}
}
}
@media screen and (max-width: $x-small-limit) {
table {
td {
width: 100%;
}
}
}
}

View File

@ -4,6 +4,7 @@ import authApi from '@/api/authApi'
import { ROOT_STORE, SPORTS_STORE } from '@/store/constants'
import { IRootState } from '@/store/modules/root/types'
import { ISportsActions, ISportsState } from '@/store/modules/sports/types'
import { ISportPayload } from '@/types/sports'
import { handleError } from '@/utils'
export const actions: ActionTree<ISportsState, IRootState> & ISportsActions = {
@ -25,4 +26,20 @@ export const actions: ActionTree<ISportsState, IRootState> & ISportsActions = {
})
.catch((error) => handleError(context, error))
},
[SPORTS_STORE.ACTIONS.UPDATE_SPORTS](
context: ActionContext<ISportsState, IRootState>,
payload: ISportPayload
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
authApi
.patch(`sports/${payload.id}`, { is_active: payload.isActive })
.then((res) => {
if (res.data.status === 'success') {
context.dispatch(SPORTS_STORE.ACTIONS.GET_SPORTS)
} else {
handleError(context, null)
}
})
.catch((error) => handleError(context, error))
},
}

View File

@ -1,5 +1,6 @@
export enum SportsActions {
GET_SPORTS = 'GET_SPORTS',
UPDATE_SPORTS = 'UPDATE_SPORTS',
}
export enum SportsGetters {

View File

@ -7,7 +7,7 @@ import {
import { SPORTS_STORE } from '@/store/constants'
import { IRootState } from '@/store/modules/root/types'
import { ISport } from '@/types/sports'
import { ISport, ISportPayload } from '@/types/sports'
export interface ISportsState {
sports: ISport[]
@ -17,6 +17,10 @@ export interface ISportsActions {
[SPORTS_STORE.ACTIONS.GET_SPORTS](
context: ActionContext<ISportsState, IRootState>
): void
[SPORTS_STORE.ACTIONS.UPDATE_SPORTS](
context: ActionContext<ISportsState, IRootState>,
payload: ISportPayload
): void
}
export interface ISportsGetters {

View File

@ -5,6 +5,12 @@ export interface ISport {
is_active: boolean
label: string
}
export interface ITranslatedSport extends ISport {
translatedLabel: string
}
export interface ISportPayload {
id: number
isActive: boolean
}

View File

@ -7,6 +7,7 @@
:appStatistics="appStatistics"
/>
<NotFound v-else />
<div id="bottom" />
</div>
</div>
</template>
@ -64,6 +65,8 @@
#admin {
.admin-card {
width: 100%;
margin-bottom: 55px;
::v-deep(.card) {
.admin-form {
display: flex;