This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/components/home/navigation.vue

380 lines
7.1 KiB
Vue

<template>
<aside :class="{'is-active': menuActive}" class="namespace-container">
<nav class="menu">
<router-link :to="{name: 'home'}" class="logo">
<Logo width="164" height="48"/>
</router-link>
<ul class="menu-list">
<li
v-for="({title, to, shortcut, icon, }, index) in MAIN_NAV_ITEMS"
:key="index"
>
<router-link :to="to" v-shortcut="shortcut">
<icon class="icon" :icon="icon"/>
{{ title }}
</router-link>
</li>
</ul>
</nav>
<LoadingIndicator
class="namespaces-lists"
:isLoading="loading"
element="nav"
>
<NavigationNamespace
v-for="(n, nk) in namespaces"
:key="n.id"
:namespace="n"
:title="namespaceTitles[nk]"
:showList="listsVisible[n.id] ?? true"
/>
</LoadingIndicator>
<PoweredByLink class="navigation__powered-by-link " />
</aside>
</template>
<script setup lang="ts">
import {ref, computed, onBeforeMount} from 'vue'
import {useStore} from 'vuex'
import {useEventListener} from '@vueuse/core'
import {useI18n} from 'vue-i18n'
import Logo from '@/components/home/Logo.vue'
import PoweredByLink from '@/components/home/PoweredByLink.vue'
import NavigationNamespace from './NavigationNamespace.vue'
import {MENU_ACTIVE} from '@/store/mutation-types'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import NamespaceModel from '@/models/namespace'
const {t} = useI18n()
const MAIN_NAV_ITEMS = [
{
title: t('navigation.overview'),
icon: 'calendar',
to: { name: 'home'},
shortcut: 'g o',
},
{
title: t('navigation.upcoming'),
icon: ['far', 'calendar-alt'],
to: { name: 'tasks.range'},
shortcut: 'g u',
},
{
title: t('namespace.title'),
icon: 'layer-group',
to: { name: 'namespaces.index'},
shortcut: 'g n',
},
{
title: t('label.title'),
icon: 'tags',
to: { name: 'labels.index'},
shortcut: 'g a',
},
{
title: t('team.title'),
icon: 'users',
to: { name: 'teams.index'},
shortcut: 'g m',
},
]
const store = useStore()
const menuActive = computed(() => store.state.menuActive)
const loading = computed(() => store.state.loading && store.state.loadingModule === 'namespaces')
const namespaces = computed(() => {
return (store.state.namespaces.namespaces as NamespaceModel[]).filter(n => !n.isArchived)
})
const activeLists = computed(() => {
return namespaces.value.map(({lists}) => {
return lists?.filter(item => {
return typeof item !== 'undefined' && !item.isArchived
})
})
})
const namespaceTitles = computed(() => {
return namespaces.value.map((namespace) => getNamespaceTitle(namespace))
})
const namespaceListsCount = computed(() => {
return namespaces.value.map((_, index) => activeLists.value[index]?.length ?? 0)
})
function resize() {
// Hide the menu by default on mobile
store.commit(MENU_ACTIVE, window.innerWidth >= 770)
}
useEventListener('resize', resize)
resize()
function toggleLists(namespaceId: number) {
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId]
}
const listsVisible = ref<{ [id: NamespaceModel['id']]: boolean }>({})
// FIXME: async action will be unfinished when component mounts
onBeforeMount(async () => {
const namespaces = await store.dispatch('namespaces/loadNamespaces') as NamespaceModel[]
namespaces.forEach(n => {
if (typeof listsVisible.value[n.id] === 'undefined') {
listsVisible.value[n.id] = true
}
})
})
</script>
<style lang="scss" scoped>
$navbar-padding: 2rem;
$vikunja-nav-background: var(--site-background);
$vikunja-nav-color: var(--grey-700);
$vikunja-nav-selected-width: 0.4rem;
.navigation {
position: fixed;
top: $navbar-height;
bottom: 0;
left: 0;
transition: transform $transition-duration ease-in;
transform: translateX(-100%);
overflow-x: auto;
width: $navbar-width;
@media screen and (max-width: $tablet) {
top: 0;
width: 70vw;
z-index: 20;
}
&.is-active {
transform: translateX(0);
transition: transform $transition-duration ease-out;
}
}
.navigation {
background: $vikunja-nav-background;
color: $vikunja-nav-color;
display: flex;
flex-direction: column;
.menu {
.menu-label,
.menu-list span.list-menu-link,
.menu-list a {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
&:hover .favorite {
opacity: 1;
}
&:hover {
background: transparent;
}
}
.list-menu-title {
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.color-bubble {
height: 12px;
flex: 0 0 12px;
}
.favorite {
margin-left: .25rem;
transition: opacity $transition, color $transition;
opacity: 0;
&:hover {
color: var(--warning);
}
&.is-favorite {
opacity: 1;
color: var(--warning);
}
}
.menu-label {
.color-bubble {
width: 14px;
height: 14px;
flex-basis: auto;
}
.is-archived {
min-width: 85px;
}
}
.menu-list {
li {
height: 44px;
display: flex;
align-items: center;
&:hover {
background: var(--white);
}
:deep(.dropdown-trigger) {
opacity: 0;
padding: .5rem;
cursor: pointer;
transition: $transition;
}
&:hover :deep(.dropdown-trigger) {
opacity: 1;
}
}
.flip-list-move {
transition: transform $transition-duration;
}
.ghost {
background: var(--grey-200);
* {
opacity: 0;
}
}
span.list-menu-link, li > a {
padding: 0.75rem .5rem 0.75rem ($navbar-padding * 1.5 - 1.75rem);
transition: all 0.2s ease;
border-radius: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
border-left: $vikunja-nav-selected-width solid transparent;
.icon {
height: 1rem;
vertical-align: middle;
padding-right: 0.5rem;
&.handle {
opacity: 0;
transition: opacity $transition;
margin-right: .25rem;
cursor: grab;
}
}
&:hover .icon.handle {
opacity: 1;
}
&.router-link-exact-active {
color: var(--primary);
border-left: $vikunja-nav-selected-width solid var(--primary);
.icon {
color: var(--primary);
}
}
&:hover {
border-left: $vikunja-nav-selected-width solid var(--primary);
}
}
}
&.namespaces-lists {
padding-top: math.div($navbar-padding, 2);
}
.icon {
color: var(--grey-400) !important;
}
}
.logo {
display: block;
padding-left: 2rem;
margin-right: 1rem;
@media screen and (min-width: $tablet) {
display: none;
}
}
.top-menu {
margin-top: math.div($navbar-padding, 2);
.menu-list {
li {
font-weight: 500;
font-family: $vikunja-font;
}
span.list-menu-link, li > a {
padding-left: 2rem;
display: inline-block;
.icon {
padding-bottom: .25rem;
}
}
}
}
}
.list-setting-spacer {
width: 2.5rem;
flex-shrink: 0;
}
.namespaces-list.loader-container.is-loading {
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
}
a.dropdown-item:hover {
background: var(--dropdown-item-hover-background-color) !important;
}
.navigation__powered-by-link {
margin-top: auto;
color: var(--grey-300);
text-align: center;
display: block;
padding-top: 1rem;
padding-bottom: 1rem;
font-size: .8rem;
}
</style>