Client - add dropdown to select language
This commit is contained in:
		@@ -1,2 +0,0 @@
 | 
			
		||||
VUE_APP_I18N_LOCALE=en
 | 
			
		||||
VUE_APP_I18N_FALLBACK_LOCALE=en
 | 
			
		||||
							
								
								
									
										97
									
								
								fittrackee_client/src/components/Common/Dropdown.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								fittrackee_client/src/components/Common/Dropdown.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="dropdown-wrapper">
 | 
			
		||||
    <div class="dropdown-selected" @click="openDropdown">
 | 
			
		||||
      <slot></slot>
 | 
			
		||||
    </div>
 | 
			
		||||
    <ul class="dropdown-list" v-if="isOpen">
 | 
			
		||||
      <li
 | 
			
		||||
        class="dropdown-item"
 | 
			
		||||
        :class="{ selected: option.value === selected }"
 | 
			
		||||
        v-for="(option, index) in dropdownOptions"
 | 
			
		||||
        :key="index"
 | 
			
		||||
        @click="updateSelected(option)"
 | 
			
		||||
      >
 | 
			
		||||
        {{ option.label }}
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { PropType, defineComponent, ref } from 'vue'
 | 
			
		||||
  import { IDropdownOption, TDropdownOptions } from '@/types'
 | 
			
		||||
 | 
			
		||||
  export default defineComponent({
 | 
			
		||||
    name: 'Dropdown',
 | 
			
		||||
    props: {
 | 
			
		||||
      options: {
 | 
			
		||||
        type: Object as PropType<TDropdownOptions>,
 | 
			
		||||
        required: true,
 | 
			
		||||
      },
 | 
			
		||||
      selected: {
 | 
			
		||||
        type: String,
 | 
			
		||||
        required: true,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    emits: {
 | 
			
		||||
      selected: (option: IDropdownOption) => option,
 | 
			
		||||
    },
 | 
			
		||||
    setup(props, { emit }) {
 | 
			
		||||
      let isOpen = ref(false)
 | 
			
		||||
      let dropdownOptions = props.options.map((option) => option)
 | 
			
		||||
 | 
			
		||||
      function openDropdown() {
 | 
			
		||||
        isOpen.value = true
 | 
			
		||||
      }
 | 
			
		||||
      function updateSelected(option: IDropdownOption) {
 | 
			
		||||
        emit('selected', option)
 | 
			
		||||
        isOpen.value = false
 | 
			
		||||
      }
 | 
			
		||||
      function getSelectedLabel(selectedValue: string) {
 | 
			
		||||
        return props.options.filter(
 | 
			
		||||
          (option: IDropdownOption) => option.value === selectedValue
 | 
			
		||||
        )[0].label
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        dropdownOptions,
 | 
			
		||||
        updateSelected,
 | 
			
		||||
        getSelectedLabel,
 | 
			
		||||
        isOpen,
 | 
			
		||||
        openDropdown,
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
  .dropdown-list {
 | 
			
		||||
    list-style-type: none;
 | 
			
		||||
    background-color: #ffffff;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin: 5px 0;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    border: solid 1px lightgrey;
 | 
			
		||||
    box-shadow: 2px 2px 5px lightgrey;
 | 
			
		||||
 | 
			
		||||
    li {
 | 
			
		||||
      padding-top: 5px;
 | 
			
		||||
    }
 | 
			
		||||
    li:last-child {
 | 
			
		||||
      padding-bottom: 5px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .dropdown-item {
 | 
			
		||||
    cursor: default;
 | 
			
		||||
 | 
			
		||||
    &.selected {
 | 
			
		||||
      font-weight: bold;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.selected::after {
 | 
			
		||||
      content: ' ✔';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -2,14 +2,14 @@
 | 
			
		||||
  <div id="nav">
 | 
			
		||||
    <div class="container">
 | 
			
		||||
      <div class="nav-app-name">
 | 
			
		||||
        <span class="nav-item app-name">FitTrackee</span>
 | 
			
		||||
        <div class="nav-item app-name">FitTrackee</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="nav-icon-open" :class="{ 'menu-open': isMenuOpen }">
 | 
			
		||||
        <i class="fa fa-bars hamburger-icon" @click="openMenu()"></i>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="nav-items" :class="{ 'menu-open': isMenuOpen }">
 | 
			
		||||
        <div class="nav-items-close">
 | 
			
		||||
          <span class="app-name">FitTrackee</span>
 | 
			
		||||
          <div class="app-name">FitTrackee</div>
 | 
			
		||||
          <i
 | 
			
		||||
            class="fa fa-close close-icon nav-item"
 | 
			
		||||
            :class="{ 'menu-closed': !isMenuOpen }"
 | 
			
		||||
@@ -20,16 +20,25 @@
 | 
			
		||||
          <router-link class="nav-item" to="/">{{
 | 
			
		||||
            t('dashboard.DASHBOARD')
 | 
			
		||||
          }}</router-link>
 | 
			
		||||
          <span class="nav-item">{{ t('workouts.WORKOUTS') }}</span>
 | 
			
		||||
          <span class="nav-item">{{ t('statistics.STATISTICS') }}</span>
 | 
			
		||||
          <span class="nav-item">{{ t('administration.ADMIN') }}</span>
 | 
			
		||||
          <span class="nav-item">{{ t('workouts.ADD_WORKOUT') }}</span>
 | 
			
		||||
          <div class="nav-item">{{ t('workouts.WORKOUTS') }}</div>
 | 
			
		||||
          <div class="nav-item">{{ t('statistics.STATISTICS') }}</div>
 | 
			
		||||
          <div class="nav-item">{{ t('administration.ADMIN') }}</div>
 | 
			
		||||
          <div class="nav-item">{{ t('workouts.ADD_WORKOUT') }}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="nav-items-user-menu">
 | 
			
		||||
          <span class="nav-item">User</span>
 | 
			
		||||
          <span class="nav-item">{{ t('user.LOGOUT') }}</span>
 | 
			
		||||
          <span class="nav-item">{{ t('user.REGISTER') }}</span>
 | 
			
		||||
          <span class="nav-item">{{ t('user.LOGIN') }}</span>
 | 
			
		||||
          <div class="nav-item">User</div>
 | 
			
		||||
          <div class="nav-item">{{ t('user.LOGOUT') }}</div>
 | 
			
		||||
          <!--          <span class="nav-item">{{ t('user.REGISTER') }}</span>-->
 | 
			
		||||
          <!--          <span class="nav-item">{{ t('user.LOGIN') }}</span>-->
 | 
			
		||||
          <Dropdown
 | 
			
		||||
            v-if="availableLanguages && language"
 | 
			
		||||
            class="nav-item"
 | 
			
		||||
            :options="availableLanguages"
 | 
			
		||||
            :selected="language"
 | 
			
		||||
            @selected="updateLanguage"
 | 
			
		||||
          >
 | 
			
		||||
            <i class="fa fa-language"></i>
 | 
			
		||||
          </Dropdown>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -37,21 +46,45 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { defineComponent, ref } from 'vue'
 | 
			
		||||
  import { computed, defineComponent, ref } from 'vue'
 | 
			
		||||
  import { useI18n } from 'vue-i18n'
 | 
			
		||||
  import { useStore } from 'vuex'
 | 
			
		||||
 | 
			
		||||
  import { IDropdownOption } from '@/types'
 | 
			
		||||
  import Dropdown from '@/components/Common/Dropdown.vue'
 | 
			
		||||
 | 
			
		||||
  export default defineComponent({
 | 
			
		||||
    name: 'NavBar',
 | 
			
		||||
    components: {
 | 
			
		||||
      Dropdown,
 | 
			
		||||
    },
 | 
			
		||||
    setup() {
 | 
			
		||||
      const { t, locale, availableLocales } = useI18n()
 | 
			
		||||
      const store = useStore()
 | 
			
		||||
      const availableLanguages = availableLocales.map((l) => {
 | 
			
		||||
        return { label: l.toUpperCase(), value: l }
 | 
			
		||||
      })
 | 
			
		||||
      let isMenuOpen = ref(false)
 | 
			
		||||
      const { t } = useI18n()
 | 
			
		||||
      function openMenu() {
 | 
			
		||||
        isMenuOpen.value = true
 | 
			
		||||
      }
 | 
			
		||||
      function closeMenu() {
 | 
			
		||||
        isMenuOpen.value = false
 | 
			
		||||
      }
 | 
			
		||||
      return { isMenuOpen, openMenu, closeMenu, t }
 | 
			
		||||
      function updateLanguage(option: IDropdownOption) {
 | 
			
		||||
        locale.value = option.value.toString()
 | 
			
		||||
        store.commit('setLanguage', option.value)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        availableLanguages,
 | 
			
		||||
        isMenuOpen,
 | 
			
		||||
        language: computed(() => store.state.language),
 | 
			
		||||
        t,
 | 
			
		||||
        openMenu,
 | 
			
		||||
        closeMenu,
 | 
			
		||||
        updateLanguage,
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
</script>
 | 
			
		||||
@@ -74,7 +107,6 @@
 | 
			
		||||
      font-size: 1.2em;
 | 
			
		||||
      font-weight: bold;
 | 
			
		||||
      margin-right: 10px;
 | 
			
		||||
      line-height: 12px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .fa {
 | 
			
		||||
@@ -84,6 +116,7 @@
 | 
			
		||||
    .nav-icon-open {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .hamburger-icon,
 | 
			
		||||
    .close-icon {
 | 
			
		||||
      display: none;
 | 
			
		||||
@@ -99,8 +132,25 @@
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .nav-items-app-menu,
 | 
			
		||||
      .nav-items-user-menu {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .nav-item {
 | 
			
		||||
        padding: 10px 10px;
 | 
			
		||||
        padding: 0 10px;
 | 
			
		||||
 | 
			
		||||
        &.dropdown-wrapper {
 | 
			
		||||
          width: 60px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ::v-deep(.dropdown-list) {
 | 
			
		||||
          margin-left: -10px;
 | 
			
		||||
          padding-left: 10px;
 | 
			
		||||
          width: 75px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -155,9 +205,13 @@
 | 
			
		||||
          justify-content: space-between;
 | 
			
		||||
 | 
			
		||||
          .app-name {
 | 
			
		||||
            padding: 19px 25px;
 | 
			
		||||
            padding: 15px 25px;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .nav-item {
 | 
			
		||||
          padding: 7px 25px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,8 @@ function loadLocaleMessages(): LocaleMessages<VueMessageType> {
 | 
			
		||||
 | 
			
		||||
export default createI18n({
 | 
			
		||||
  legacy: false,
 | 
			
		||||
  locale: process.env.VUE_APP_I18N_LOCALE || 'en',
 | 
			
		||||
  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
 | 
			
		||||
  locale: 'en',
 | 
			
		||||
  fallbackLocale: 'en',
 | 
			
		||||
  globalInjection: true,
 | 
			
		||||
  messages: loadLocaleMessages(),
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "LANGUAGE": "Language",
 | 
			
		||||
  "LOGIN": "Login",
 | 
			
		||||
  "LOGOUT": "Logout",
 | 
			
		||||
  "REGISTER": "Register"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "LANGUAGE": "Langue",
 | 
			
		||||
  "LOGIN": "Se connecter",
 | 
			
		||||
  "LOGOUT": "Se déconnecter",
 | 
			
		||||
  "REGISTER": "S'inscrire"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,14 @@
 | 
			
		||||
import { createStore } from 'vuex'
 | 
			
		||||
 | 
			
		||||
export default createStore({
 | 
			
		||||
  state: {},
 | 
			
		||||
  mutations: {},
 | 
			
		||||
  state: {
 | 
			
		||||
    language: 'en',
 | 
			
		||||
  },
 | 
			
		||||
  mutations: {
 | 
			
		||||
    setLanguage(state, language: string) {
 | 
			
		||||
      state.language = language
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  actions: {},
 | 
			
		||||
  modules: {},
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								fittrackee_client/src/types/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								fittrackee_client/src/types/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
export interface IDropdownOption {
 | 
			
		||||
  value: string | number
 | 
			
		||||
  label: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type TDropdownOptions = IDropdownOption[]
 | 
			
		||||
		Reference in New Issue
	
	Block a user