Browse Source

Feature: Mobile - Add bottom navigation design

Vladimir Sheremet 2 years ago
parent
commit
7b25771e39

+ 4 - 2
app/frontend/apps/mobile/App.vue

@@ -13,6 +13,7 @@ import useAppMaintenanceCheck from '@shared/composables/useAppMaintenanceCheck'
 import usePushMessages from '@shared/composables/usePushMessages'
 import useLocaleStore from '@shared/stores/locale'
 import useFormKitConfig from '@shared/composables/form/useFormKitConfig'
+import { useAppTheme } from '@shared/composables/useAppTheme'
 
 const router = useRouter()
 const route = useRoute()
@@ -29,6 +30,7 @@ onMounted(() => {
 
 useAppMaintenanceCheck()
 usePushMessages()
+useAppTheme()
 
 // Add a watcher for authenticated changes (e.g. login/logout in a other browser tab).
 authentication.$subscribe(async (mutation, state) => {
@@ -44,7 +46,7 @@ authentication.$subscribe(async (mutation, state) => {
     })
   } else if (!state.authenticated && session.id) {
     await authentication.clearAuthentication()
-    router.replace('login')
+    router.replace('/login')
   }
 })
 
@@ -58,7 +60,7 @@ watch(
       !session.hasPermission(['admin.maintenance', 'maintenance'])
     ) {
       await authentication.logout()
-      router.replace('login')
+      router.replace('/login')
     }
   },
 )

+ 33 - 0
app/frontend/apps/mobile/components/layout/LayoutBottomNavigation.vue

@@ -0,0 +1,33 @@
+<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
+<script setup lang="ts">
+import CommonAvatar from '@shared/components/CommonAvatar/CommonAvatar.vue'
+import useSessionStore from '@shared/stores/session'
+import { getInitials } from '@shared/utils/formatter'
+import { storeToRefs } from 'pinia'
+
+const { user } = storeToRefs(useSessionStore())
+</script>
+
+<template>
+  <footer
+    class="fixed bottom-0 z-10 flex h-14 w-full items-center border-t bg-black/30 text-center backdrop-blur-lg"
+  >
+    <CommonLink
+      link="/"
+      class="flex flex-1 justify-center"
+      exact-active-class="text-blue"
+    >
+      <CommonIcon name="home" size="small" active-class="text-blue" />
+    </CommonLink>
+    <CommonLink link="/notifications" class="flex flex-1 justify-center">
+      <CommonIcon name="bell" size="medium" />
+    </CommonLink>
+    <CommonLink link="/user" class="flex-1">
+      <!-- TODO use CommonUserAvatar with entity -->
+      <CommonAvatar
+        class="bg-red"
+        :initials="getInitials(user?.firstname, user?.lastname)"
+      />
+    </CommonLink>
+  </footer>
+</template>

+ 4 - 8
app/frontend/apps/mobile/components/layout/LayoutMain/LayoutMain.vue → app/frontend/apps/mobile/components/layout/LayoutMain.vue

@@ -3,7 +3,8 @@
 <script setup lang="ts">
 import { computed } from 'vue'
 import { useRoute } from 'vue-router'
-import TransitionViewNavigation from '../../transition/TransitionViewNavigation/TransitionViewNavigation.vue'
+import TransitionViewNavigation from '../transition/TransitionViewNavigation/TransitionViewNavigation.vue'
+import LayoutBottomNavigation from './LayoutBottomNavigation.vue'
 
 const route = useRoute()
 
@@ -14,18 +15,13 @@ const showBottomNavigation = computed(() => {
 
 <template>
   <div class="flex h-full flex-col overflow-hidden">
-    <main class="flex-1 overflow-y-scroll">
+    <main class="overflow-y-scroll" :class="{ 'pb-14': showBottomNavigation }">
       <router-view v-slot="{ Component }">
         <TransitionViewNavigation>
           <component :is="Component" />
         </TransitionViewNavigation>
       </router-view>
     </main>
-    <footer
-      v-if="showBottomNavigation"
-      class="w-full border-t bg-black p-4 text-center"
-    >
-      Bottom-Navigation
-    </footer>
+    <LayoutBottomNavigation v-if="showBottomNavigation" />
   </div>
 </template>

+ 28 - 0
app/frontend/apps/mobile/components/layout/__tests__/LayoutBottomNavigation.spec.ts

@@ -0,0 +1,28 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import useSessionStore from '@shared/stores/session'
+import { UserData } from '@shared/types/store'
+import { renderComponent } from '@tests/support/components'
+import { flushPromises } from '@vue/test-utils'
+import LayoutBottomNavigation from '../LayoutBottomNavigation.vue'
+
+describe('bottom navigation in layout', () => {
+  it('renders navigation', async () => {
+    const view = renderComponent(LayoutBottomNavigation, {
+      store: true,
+      router: true,
+    })
+    const store = useSessionStore()
+
+    store.user = {
+      firstname: 'User',
+      lastname: 'Test',
+    } as UserData
+
+    await flushPromises()
+
+    expect(view.getIconByName('home')).toBeInTheDocument()
+    expect(view.getIconByName('bell')).toBeInTheDocument()
+    expect(view.getByText('UT')).toBeInTheDocument()
+  })
+})

+ 1 - 1
app/frontend/apps/mobile/components/transition/TransitionViewNavigation/TransitionViewNavigation.vue

@@ -7,7 +7,7 @@ const { viewTransition } = useViewTransition()
 </script>
 
 <template>
-  <main class="grid min-h-screen flex-1 overflow-hidden">
+  <main class="grid flex-1 overflow-hidden">
     <transition class="z-10 flex-auto" :name="viewTransition">
       <slot></slot>
     </transition>

+ 1 - 1
app/frontend/apps/mobile/router/index.ts

@@ -4,7 +4,7 @@ import type { App } from 'vue'
 import type { RouteRecordRaw } from 'vue-router'
 import mainInitializeRouter from '@shared/router'
 import type { InitializeAppRouter, RoutesModule } from '@shared/types/router'
-import LayoutMain from '@mobile/components/layout/LayoutMain/LayoutMain.vue'
+import LayoutMain from '@mobile/components/layout/LayoutMain.vue'
 import transitionViewGuard from './guards/before/viewTransition'
 
 const routeModules: Record<string, RoutesModule> = import.meta.globEager(

+ 16 - 0
app/frontend/shared/composables/useAppTheme.ts

@@ -0,0 +1,16 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+// TODO add light color, if we have light theme
+// TODO toggle color, if we have light theme
+export function useAppTheme() {
+  const meta =
+    document.head.querySelector('meta[name="theme-color"]') ||
+    document.createElement('meta')
+
+  meta.setAttribute('name', 'theme-color')
+  meta.setAttribute('content', '#191919')
+
+  if (!document.head.contains(meta)) {
+    document.head.appendChild(meta)
+  }
+}

+ 3 - 3
app/frontend/shared/utils/formatter.ts

@@ -42,9 +42,9 @@ export function order(
  * @param email - user's email address
  */
 export function getInitials(
-  firstname?: string,
-  lastname?: string,
-  email?: string,
+  firstname?: Maybe<string>,
+  lastname?: Maybe<string>,
+  email?: Maybe<string>,
 ) {
   if (firstname && lastname) {
     return firstname[0] + lastname[0]

+ 4 - 0
public/assets/images/icons/bell.svg

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M22.3346 14.6667V19.1389L24.4319 23.3334H8.23737L10.3346 19.1389V14.6667C10.3346 11.2576 12.0767 8.83042 14.7957 8.18619L14.8248 8.1793L14.8464 8.1735L14.8546 8.17145C14.8672 8.16834 14.8915 8.16247 14.9263 8.15472C14.9962 8.13915 15.1065 8.11638 15.2471 8.09305C15.533 8.04559 15.9197 8 16.3346 8C16.7496 8 17.1362 8.04559 17.4222 8.09305C17.5628 8.11638 17.673 8.13915 17.7429 8.15472C17.7777 8.16247 17.8021 8.16834 17.8147 8.17145L17.8229 8.1735L17.8437 8.1791L17.872 8.18583C20.5804 8.82979 22.3346 11.2727 22.3346 14.6667ZM27.0013 25.3334V24.0001L24.3346 18.6667V14.6667C24.3346 10.5734 22.148 7.14674 18.3346 6.24007C18.3346 6.24007 17.4413 6 16.3346 6C15.228 6 14.3346 6.24007 14.3346 6.24007C10.508 7.14674 8.33464 10.5601 8.33464 14.6667V18.6667L5.66797 24.0001V25.3334H12.668C12.668 27.3491 14.2991 29 16.3346 29C18.3536 29 20.0013 27.3523 20.0013 25.3334H27.0013ZM18.0013 25.3334H14.668C14.668 26.251 15.4102 27 16.3346 27C17.249 27 18.0013 26.2478 18.0013 25.3334Z" fill="currentColor"/>
+</svg>

+ 4 - 0
public/assets/images/icons/home.svg

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="22" height="23" viewBox="0 0 22 23" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.33398 14.6668C6.33398 13.5623 7.22941 12.6668 8.33398 12.6668H13.6673C14.7719 12.6668 15.6673 13.5623 15.6673 14.6668V20.6668H19.6673V8.58997L11.0007 2.45108L2.33398 8.58997V20.6668H6.33398V14.6668ZM8.33398 22.6668H0.333984V8.24523C0.333984 7.81276 0.543729 7.40716 0.896629 7.15719L10.23 0.546082C10.6917 0.219014 11.3096 0.219015 11.7713 0.546082L21.1047 7.15719C21.4576 7.40716 21.6673 7.81276 21.6673 8.24523V22.6668H13.6673V14.6668H8.33398V22.6668Z" fill="currentColor"/>
+</svg>