Merged in feature/new-navigation-layout (pull request #414)

Feature/new navigation layout

Approved-by: Elia Bieri
This commit is contained in:
Ramon Wenger 2024-11-14 08:57:21 +00:00
commit d361dabd16
20 changed files with 455 additions and 182 deletions

View File

@ -1,29 +1,16 @@
<template> <template>
<AccountMenuContent <AccountMenuContent :user="userStore" @logout="logout" @close="emit('close')" />
:course-sessions="courseSessionsStore.allCurrentCourseSessions"
:selected-course-session="courseSessionsStore.currentCourseSession?.id"
:user="userStore"
@logout="logout"
@select-course-session="selectCourseSession"
@close="emit('close')"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import AccountMenuContent from "@/components/header/AccountMenuContent.vue"; import AccountMenuContent from "@/components/header/AccountMenuContent.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { CourseSession } from "@/types";
const emit = defineEmits(["close"]); const emit = defineEmits(["close"]);
const logout = () => { const logout = () => {
userStore.handleLogout(); userStore.handleLogout();
}; };
const selectCourseSession = (courseSession: CourseSession) => {
courseSessionsStore.switchCourseSessionById(courseSession.id);
};
const courseSessionsStore = useCourseSessionsStore();
const userStore = useUserStore(); const userStore = useUserStore();
</script> </script>

View File

@ -1,32 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import CourseSessionsMenu from "@/components/header/CourseSessionsMenu.vue"; import { SETTINGS_ROUTE } from "@/router/names";
import type { User } from "@/stores/user"; import type { User } from "@/stores/user";
import type { CourseSession } from "@/types";
import { useRouteLookups } from "@/utils/route";
import { computed } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const props = defineProps<{ defineProps<{
courseSessions: CourseSession[];
user: User; user: User;
selectedCourseSession?: string;
}>(); }>();
const emit = defineEmits(["selectCourseSession", "logout", "close"]); const emit = defineEmits(["logout", "close"]);
const router = useRouter(); const router = useRouter();
const { inCourse } = useRouteLookups();
const showCourseSessionMenu = computed(() => inCourse() && props.courseSessions.length);
async function navigate(routeName: string) { async function navigate(routeName: string) {
await router.push({ name: routeName }); await router.push({ name: routeName });
emit("close"); emit("close");
} }
const settingsRoute = {
name: SETTINGS_ROUTE,
};
</script> </script>
<template> <template>
<div class="text-black"> <div class="flex flex-col gap-4 text-black">
<div class="border-b py-4"> <div class="border-b py-4">
<div class="flex justify-start"> <div class="flex justify-start">
<div v-if="user.avatar_url"> <div v-if="user.avatar_url">
@ -46,22 +42,19 @@ async function navigate(routeName: string) {
</div> </div>
</div> </div>
<div v-if="showCourseSessionMenu" class="border-b py-4"> <router-link class="flex items-center gap-2" :to="settingsRoute">
<CourseSessionsMenu <it-icon-settings />
:items="courseSessions" {{ $t("a.Einstellungen") }}
:selected="selectedCourseSession" </router-link>
@select="emit('selectCourseSession', $event)"
/>
</div>
<button <button
type="button" type="button"
class="mt-6 flex items-center" class="flex items-center gap-2"
data-cy="logout-button" data-cy="logout-button"
@click="emit('logout')" @click="emit('logout')"
> >
<it-icon-logout class="inline-block" /> <it-icon-logout class="inline-block" />
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span> <span>{{ $t("mainNavigation.logout") }}</span>
</button> </button>
</div> </div>
</template> </template>

View File

@ -10,11 +10,12 @@ import {
} from "@/utils/utils"; } from "@/utils/utils";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import { computed } from "vue"; import { computed } from "vue";
import SelectedCourseSession from "./SelectedCourseSession.vue";
const { t } = useTranslation(); const { t } = useTranslation();
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();
const { inCockpit, inCompetenceProfile, inLearningMentor, inLearningPath } = const { isInCockpit, inCompetenceProfile, inLearningMentor, inLearningPath } =
useRouteLookups(); useRouteLookups();
const { const {
hasCompetenceNaviMenu, hasCompetenceNaviMenu,
@ -30,13 +31,18 @@ const mentorTabTitle = computed(() =>
); );
</script> </script>
<template> <template>
<div v-if="courseSessionsStore.currentCourseSession" class="hidden space-x-8 lg:flex"> <div
v-if="courseSessionsStore.currentCourseSession"
class="flex space-x-8 px-2 lg:px-10"
>
<SelectedCourseSession />
<router-link <router-link
v-if="hasCockpitMenu" v-if="hasCockpitMenu"
data-cy="navigation-cockpit-link" data-cy="navigation-cockpit-link"
:to="getCockpitUrl(courseSessionsStore.currentCourseSession.course.slug)" :to="getCockpitUrl(courseSessionsStore.currentCourseSession.course.slug)"
class="nav-item" class="nav-item-no-mobile"
:class="{ 'nav-item--active': inCockpit() }" :class="{ 'nav-item--active': isInCockpit }"
> >
{{ t("cockpit.title") }} {{ t("cockpit.title") }}
</router-link> </router-link>
@ -46,7 +52,7 @@ const mentorTabTitle = computed(() =>
data-cy="navigation-preview-link" data-cy="navigation-preview-link"
:to="getLearningPathUrl(courseSessionsStore.currentCourseSession.course.slug)" :to="getLearningPathUrl(courseSessionsStore.currentCourseSession.course.slug)"
target="_blank" target="_blank"
class="nav-item" class="nav-item-no-mobile"
> >
<div class="flex items-center"> <div class="flex items-center">
<span>{{ t("a.Vorschau Teilnehmer") }}</span> <span>{{ t("a.Vorschau Teilnehmer") }}</span>
@ -57,7 +63,7 @@ const mentorTabTitle = computed(() =>
v-if="hasLearningPathMenu" v-if="hasLearningPathMenu"
data-cy="navigation-learning-path-link" data-cy="navigation-learning-path-link"
:to="getLearningPathUrl(courseSessionsStore.currentCourseSession.course.slug)" :to="getLearningPathUrl(courseSessionsStore.currentCourseSession.course.slug)"
class="nav-item" class="nav-item-no-mobile"
:class="{ 'nav-item--active': inLearningPath() }" :class="{ 'nav-item--active': inLearningPath() }"
> >
{{ t("general.learningPath") }} {{ t("general.learningPath") }}
@ -67,7 +73,7 @@ const mentorTabTitle = computed(() =>
v-if="hasCompetenceNaviMenu" v-if="hasCompetenceNaviMenu"
data-cy="navigation-competence-profile-link" data-cy="navigation-competence-profile-link"
:to="getCompetenceNaviUrl(courseSessionsStore.currentCourseSession.course.slug)" :to="getCompetenceNaviUrl(courseSessionsStore.currentCourseSession.course.slug)"
class="nav-item" class="nav-item-no-mobile"
:class="{ 'nav-item--active': inCompetenceProfile() }" :class="{ 'nav-item--active': inCompetenceProfile() }"
> >
{{ t("competences.title") }} {{ t("competences.title") }}
@ -77,7 +83,7 @@ const mentorTabTitle = computed(() =>
v-if="hasLearningMentor" v-if="hasLearningMentor"
data-cy="navigation-learning-mentor-link" data-cy="navigation-learning-mentor-link"
:to="getLearningMentorUrl(courseSessionsStore.currentCourseSession.course.slug)" :to="getLearningMentorUrl(courseSessionsStore.currentCourseSession.course.slug)"
class="nav-item" class="nav-item-no-mobile"
:class="{ 'nav-item--active': inLearningMentor() }" :class="{ 'nav-item--active': inLearningMentor() }"
> >
{{ t(mentorTabTitle) }} {{ t(mentorTabTitle) }}

View File

@ -1,12 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouteLookups } from "@/utils/route";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
const { isInCourse } = useRouteLookups();
const { t } = useTranslation(); const { t } = useTranslation();
</script> </script>
<template> <template>
<div class="hidden flex-shrink-0 items-center lg:flex"> <div class="flex flex-shrink-0 items-center">
<div class="flex items-center"> <template v-if="isInCourse">
<div class="flex h-full items-center border-r border-slate-500">
<router-link to="/" class="flex items-center pr-3">
<it-icon-arrow-left />
<span class="hidden text-slate-500 lg:inline">
{{ t("a.Dashboard") }}
</span>
</router-link>
</div>
</template>
<template v-else>
<router-link to="/" class="flex"> <router-link to="/" class="flex">
<it-icon-vbv class="-ml-3 -mt-6 mr-3 h-8 w-16" /> <it-icon-vbv class="-ml-3 -mt-6 mr-3 h-8 w-16" />
</router-link> </router-link>
@ -15,6 +27,6 @@ const { t } = useTranslation();
{{ t("general.title") }} {{ t("general.title") }}
</div> </div>
</router-link> </router-link>
</div> </template>
</div> </div>
</template> </template>

View File

@ -15,12 +15,7 @@ log.debug("MainNavigationBar created");
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();
const { inMediaLibrary, inAppointments } = useRouteLookups(); const { inMediaLibrary, inAppointments } = useRouteLookups();
const { hasMediaLibraryMenu, hasAppointmentsMenu, hasSessionTitle } = const { hasMediaLibraryMenu, hasAppointmentsMenu } = useNavigationAttributes();
useNavigationAttributes();
const selectedCourseSessionTitle = computed(() => {
return courseSessionsStore.currentCourseSession?.title;
});
const appointmentsUrl = computed(() => { const appointmentsUrl = computed(() => {
const currentCourseSession = courseSessionsStore.currentCourseSession; const currentCourseSession = courseSessionsStore.currentCourseSession;
@ -40,7 +35,6 @@ onMounted(() => {
<nav class="bg-blue-900 text-white"> <nav class="bg-blue-900 text-white">
<div class="mx-auto px-4 lg:px-8"> <div class="mx-auto px-4 lg:px-8">
<div class="relative flex h-16 justify-between"> <div class="relative flex h-16 justify-between">
<MobileMenuButton />
<div class="flex flex-1 items-stretch justify-start"> <div class="flex flex-1 items-stretch justify-start">
<HomeNavigation /> <HomeNavigation />
<CourseSessionNavigation /> <CourseSessionNavigation />
@ -62,7 +56,7 @@ onMounted(() => {
v-if="hasAppointmentsMenu" v-if="hasAppointmentsMenu"
:to="appointmentsUrl" :to="appointmentsUrl"
data-cy="all-duedates-link" data-cy="all-duedates-link"
class="nav-item" class="nav-item-no-mobile"
:class="{ 'nav-item--active': inAppointments() }" :class="{ 'nav-item--active': inAppointments() }"
> >
<it-icon-calendar-light class="h-8 w-8" /> <it-icon-calendar-light class="h-8 w-8" />
@ -71,22 +65,12 @@ onMounted(() => {
<!-- Notification Bell & Menu --> <!-- Notification Bell & Menu -->
<NotificationButton /> <NotificationButton />
<div
v-if="hasSessionTitle"
class="nav-item hidden items-center lg:inline-flex"
>
<div class="" data-cy="current-course-session-title">
{{ selectedCourseSessionTitle }}
</div>
</div>
<div class="nav-item"> <div class="nav-item">
<ProfileMenuButton /> <ProfileMenuButton />
</div> </div>
</div> </div>
<MobileMenuButton />
</div> </div>
</div> </div>
</nav> </nav>
</template> </template>
<style lang="postcss"></style>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue"; import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import { useVVByLink } from "@/composables"; import { useVVByLink } from "@/composables";
import { SETTINGS_ROUTE } from "@/router/names";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { User } from "@/stores/user"; import type { User } from "@/stores/user";
import type { CourseSession } from "@/types"; import type { CourseSession } from "@/types";
@ -49,6 +50,10 @@ const mentorTabTitle = computed(() =>
? "a.Praxisbildner" ? "a.Praxisbildner"
: "a.Lernbegleitung" : "a.Lernbegleitung"
); );
const settingsRoute = {
name: SETTINGS_ROUTE,
};
</script> </script>
<template> <template>
@ -71,57 +76,64 @@ const mentorTabTitle = computed(() =>
</div> </div>
<div> <div>
<div v-if="courseSession" class="mt-6 border-b"> <div v-if="courseSession" class="mt-6 border-b">
<h4 class="text-sm text-gray-900">{{ courseSession.course.title }}</h4> <h4 class="px-4 text-sm text-gray-900">{{ courseSession.course.title }}</h4>
<ul class="mt-6"> <ul class="mt-6 flex flex-col">
<li v-if="hasCockpitMenu" class="mb-6"> <li v-if="hasCockpitMenu" class="mb-6">
<button <router-link
class="w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-cockpit-link" data-cy="navigation-mobile-cockpit-link"
@click="clickLink(getCockpitUrl(courseSession.course.slug))" :to="getCockpitUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
> >
{{ $t("cockpit.title") }} {{ $t("cockpit.title") }}
</button> </router-link>
</li> </li>
<li v-if="hasPreviewMenu" class="mb-6"> <li v-if="hasPreviewMenu" class="mb-2 flex">
<button <router-link
class="w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-preview-link" data-cy="navigation-mobile-preview-link"
@click="clickLink(getLearningPathUrl(courseSession.course.slug))" :to="getLearningPathUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
> >
{{ $t("a.Vorschau Teilnehmer") }} {{ $t("a.Vorschau Teilnehmer") }}
</button> </router-link>
</li> </li>
<li v-if="hasLearningPathMenu" class="mb-6"> <li v-if="hasLearningPathMenu" class="mb-2 flex">
<button <router-link
class="w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-learning-path-link" data-cy="navigation-mobile-learning-path-link"
@click="clickLink(getLearningPathUrl(courseSession.course.slug))" :to="getLearningPathUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
> >
{{ $t("general.learningPath") }} {{ $t("general.learningPath") }}
</button> </router-link>
</li> </li>
<li v-if="hasCompetenceNaviMenu" class="mb-6"> <li v-if="hasCompetenceNaviMenu" class="mb-2 flex">
<button <router-link
class="w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-competence-profile-link" data-cy="navigation-mobile-competence-profile-link"
@click="clickLink(getCompetenceNaviUrl(courseSession.course.slug))" :to="getCompetenceNaviUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
> >
{{ $t("competences.title") }} {{ $t("competences.title") }}
</button> </router-link>
</li> </li>
<li v-if="hasLearningMentor" class="mb-6"> <li v-if="hasLearningMentor" class="mb-2 flex">
<button <router-link
class="w-full px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
data-cy="navigation-mobile-mentor-link" data-cy="navigation-mobile-mentor-link"
@click="clickLink(getLearningMentorUrl(courseSession.course.slug))" :to="getLearningMentorUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
> >
{{ $t(mentorTabTitle) }} {{ $t(mentorTabTitle) }}
</button> </router-link>
</li> </li>
<li v-if="hasMediaLibraryMenu" class="mb-6">
<button
data-cy="medialibrary-link"
@click="clickLink(getMediaCenterUrl(courseSession.course.slug))"
>
{{ $t("a.Mediathek") }}
</button>
</li>
<li <li
v-if="isVVLearningMentor(courseSessionsStore.currentCourseSession)" v-if="isVVLearningMentor(courseSessionsStore.currentCourseSession)"
class="mb-6" class="mb-6"
@ -137,15 +149,47 @@ const mentorTabTitle = computed(() =>
</div> </div>
<div class="mt-6 border-b"> <div class="mt-6 border-b">
<ul> <ul>
<li class="mb-6"> <li v-if="courseSession && hasMediaLibraryMenu" class="mb-6 flex">
<button data-cy="dashboard-link" @click="clickLink('/')">myVBV</button> <router-link
data-cy="medialibrary-link"
class="flex w-full items-center gap-2 px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
:to="getMediaCenterUrl(courseSession.course.slug)"
@click="$emit('closemodal')"
>
<it-icon-media-library />
{{ $t("a.Mediathek") }}
</router-link>
</li>
<li v-if="courseSession && hasMediaLibraryMenu" class="mb-6 flex">
<router-link
data-cy="calendar-link"
class="flex w-full items-center gap-2 px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
:to="'/'"
@click="$emit('closemodal')"
>
<!-- todo: correct route -->
<it-icon-calendar-light />
{{ $t("a.Termine") }}
</router-link>
</li> </li>
</ul> </ul>
</div> </div>
<router-link
:to="settingsRoute"
class="mt-6 flex w-full items-center gap-2 px-4 py-2"
active-class="bg-gray-200 text-blue-900 font-bold"
v-if="user?.loggedIn"
type="button"
>
<it-icon-settings />
{{ $t("a.Einstellungen") }}
</router-link>
<button <button
v-if="user?.loggedIn" v-if="user?.loggedIn"
type="button" type="button"
class="mt-6 flex items-center" class="mt-6 flex items-center px-4 py-2"
@click="$emit('logout')" @click="$emit('logout')"
> >
<it-icon-logout class="inline-block" /> <it-icon-logout class="inline-block" />

View File

@ -45,7 +45,7 @@ const showMenu = ref(false);
@logout="userStore.handleLogout()" @logout="userStore.handleLogout()"
/> />
</Teleport> </Teleport>
<div class="absolute inset-y-0 left-0 flex items-center lg:hidden"> <div class="inset-y-0 flex items-center lg:hidden">
<!-- Mobile menu button --> <!-- Mobile menu button -->
<div data-cy="navigation-mobile-menu-button" class="flex" @click="showMenu = true"> <div data-cy="navigation-mobile-menu-button" class="flex" @click="showMenu = true">
<button <button

View File

@ -29,7 +29,11 @@ function popoverClick(event: Event) {
<AccountMenu @close="showMenu = false" /> <AccountMenu @close="showMenu = false" />
</ItFullScreenModal> </ItFullScreenModal>
</Teleport> </Teleport>
<div v-if="userStore.loggedIn" class="flex items-center" data-cy="header-profile"> <div
v-if="userStore.loggedIn"
class="hidden items-center lg:flex"
data-cy="header-profile"
>
<Popover class="relative"> <Popover class="relative">
<PopoverButton @click="popoverClick($event)"> <PopoverButton @click="popoverClick($event)">
<div v-if="userStore.avatar_url"> <div v-if="userStore.avatar_url">

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { CourseSession } from "@/types";
import { useNavigationAttributes } from "@/utils/navigation";
import { useRouteLookups } from "@/utils/route";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
import { computed } from "vue";
import CourseSessionsMenu from "./CourseSessionsMenu.vue";
const { isInCourse } = useRouteLookups();
const courseSessionsStore = useCourseSessionsStore();
const { hasSessionTitle } = useNavigationAttributes();
const selectedCourseSessionTitle = computed(() => {
return courseSessionsStore.currentCourseSession?.title;
});
const selectedCourseSession = computed(() => {
return courseSessionsStore.currentCourseSession;
});
const selectCourseSession = (courseSession: CourseSession) => {
courseSessionsStore.switchCourseSessionById(courseSession.id);
};
const courseSessions = computed(() => {
return courseSessionsStore.allCourseSessions;
});
const showCourseSessionMenu = computed(
() => isInCourse.value && courseSessions.value.length
);
</script>
<template>
<div
v-if="hasSessionTitle"
class="nav-item-base inline-flex items-center lg:inline-flex"
>
<div data-cy="current-course-session-title" class="text-bold">
{{ selectedCourseSessionTitle }}
</div>
<Popover v-if="showCourseSessionMenu" class="relative">
<PopoverButton
class="group flex items-center rounded-md bg-transparent px-3 text-base focus:outline-none"
>
<it-icon-arrow-down class="h-6 w-6" />
</PopoverButton>
<PopoverPanel class="absolute left-0 z-10 mt-3 w-64 px-1 sm:px-0 lg:max-w-3xl">
<div
class="flex flex-col rounded-lg bg-white p-4 shadow-lg ring-1 ring-black/5"
>
<h3 class="fond-bold mb-2 text-base text-black">Durchführung</h3>
<CourseSessionsMenu
:items="courseSessions"
:selected="selectedCourseSession?.id"
@select="selectCourseSession"
/>
</div>
</PopoverPanel>
</Popover>
</div>
</template>

View File

@ -0,0 +1,49 @@
<script setup lang="ts">
import { computed } from "vue";
import { RouterLink } from "vue-router";
// https://router.vuejs.org/guide/advanced/extending-router-link
import { isExternalLink as isExternalLinkFn } from "@/utils/navigation";
defineOptions({
inheritAttrs: false,
});
const props = defineProps({
// @ts-expect-error the example above mentions needing @ts-ignore
...RouterLink.props,
});
const isExternalLink = computed(() => {
return isExternalLinkFn(props.to);
});
</script>
<template>
<a
v-if="isExternalLink"
class="flex items-center gap-2"
v-bind="$attrs"
:href="to"
target="_blank"
>
<slot />
<it-icon-external-link class="w-6" />
</a>
<!-- make `:to` explicit -->
<router-link
v-else
v-slot="{ isActive, href, navigate }"
v-bind="$props"
:to="$props.to"
custom
>
<a
v-bind="$attrs"
:class="isActive ? activeClass : ''"
:href="href"
@click="navigate"
>
<slot />
</a>
</router-link>
</template>

View File

@ -0,0 +1,116 @@
<script setup lang="ts">
import SubNavItem from "@/components/header/SubNavItem.vue";
import { isExternalLink } from "@/utils/navigation";
import { Listbox, ListboxOption, ListboxOptions } from "@headlessui/vue";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
export interface EntryRoute {
name: string;
}
export type EntryOrExternalRoute = EntryRoute | string;
export interface SubNavEntry {
id: number;
name: string;
route: EntryOrExternalRoute;
dataCy?: string;
}
export interface Props {
items: SubNavEntry[];
}
const props = defineProps<Props>();
const isCurrentRoute = (route: { name: string } | string) => {
return typeof route !== "string" && route?.name === router.currentRoute.value.name;
};
const currentRouteName = computed(() => {
return props.items.find((item) => isCurrentRoute(item.route))?.name || "";
});
const open = ref<boolean>(false);
const currentRoute = ref(props.items.find((item) => isCurrentRoute(item.route)));
const selectRoute = (current: SubNavEntry) => {
// we use this to mimic VueRouter's active flag
open.value = false;
currentRoute.value = current;
};
const internalLinks = computed(() => {
return props.items.filter((i) => !isExternalLink(i.route));
});
const externalLinks = computed(() => {
return props.items.filter((i) => isExternalLink(i.route));
});
</script>
<template>
<nav class="border-b bg-white px-4 lg:px-8">
<Listbox as="div" :model-value="currentRoute" by="id">
<div class="relative w-full py-2 lg:hidden">
<button
class="relative flex w-full cursor-default flex-row items-center border bg-white py-3 pl-5 pr-10 text-left"
@click="open = !open"
>
{{ currentRouteName }}
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<it-icon-arrow-down class="h-5 w-5" aria-hidden="true" />
</span>
</button>
<ListboxOptions
v-if="open"
class="absolute top-14 z-50 flex w-full cursor-default flex-col rounded-xl border-0 bg-white text-left shadow-lg"
static
>
<ListboxOption
v-for="item in items"
:key="item.id"
v-slot="{ selected }"
:value="item"
class="relative w-full border-b py-3 pl-10 pr-10 last:border-b-0"
>
<SubNavItem
:to="item.route"
class="flex items-center gap-2"
@click="selectRoute(item)"
>
<it-icon-check
v-if="selected"
class="absolute left-2 top-1/2 w-8 -translate-y-1/2"
/>
{{ item.name }}
</SubNavItem>
</ListboxOption>
</ListboxOptions>
</div>
</Listbox>
<div class="center hidden items-end justify-between lg:flex">
<ul class="flex flex-row gap-10">
<li
v-for="item in internalLinks"
:key="item.id"
class="border-t-2 border-t-transparent"
:class="{ 'border-b-2 border-b-blue-900': isCurrentRoute(item.route) }"
>
<SubNavItem :data-cy="item.dataCy" :to="item.route" class="block py-3">
{{ item.name }}
</SubNavItem>
</li>
</ul>
<ul class="flex flex-row gap-10">
<li
v-for="item in externalLinks"
:key="item.id"
class="border-b-2 border-t-2 border-b-transparent border-t-transparent"
>
<SubNavItem :data-cy="item.dataCy" :to="item.route" class="block py-3">
{{ item.name }}
</SubNavItem>
</li>
</ul>
</div>
</nav>
</template>

View File

@ -70,6 +70,7 @@
"a.Damit du myVBV nutzen kannst, brauchst du ein Konto.": "Damit du myVBV nutzen kannst, brauchst du ein Konto.", "a.Damit du myVBV nutzen kannst, brauchst du ein Konto.": "Damit du myVBV nutzen kannst, brauchst du ein Konto.",
"a.Das muss ich nochmals anschauen": "Das muss ich nochmals anschauen", "a.Das muss ich nochmals anschauen": "Das muss ich nochmals anschauen",
"a.Das wurde mit dir geteilt": "Das wurde mit dir geteilt", "a.Das wurde mit dir geteilt": "Das wurde mit dir geteilt",
"a.Dashboard": "Dashboard",
"a.Datei auswählen": "Datei auswählen", "a.Datei auswählen": "Datei auswählen",
"a.Datei hochladen": "Datei hochladen", "a.Datei hochladen": "Datei hochladen",
"a.Datei kann nicht gespeichert werden.": "Datei kann nicht gespeichert werden.", "a.Datei kann nicht gespeichert werden.": "Datei kann nicht gespeichert werden.",
@ -104,6 +105,7 @@
"a.E-Mail Adresse": "E-Mail Adresse", "a.E-Mail Adresse": "E-Mail Adresse",
"a.Einladung": "Einladung", "a.Einladung": "Einladung",
"a.Einladung abschicken": "Einladung abschicken", "a.Einladung abschicken": "Einladung abschicken",
"a.Einstellungen": "Einstellungen",
"a.Elemente zu erledigen": "Elemente zu erledigen", "a.Elemente zu erledigen": "Elemente zu erledigen",
"a.Email": "Email", "a.Email": "Email",
"a.Entfernen": "Entfernen", "a.Entfernen": "Entfernen",

View File

@ -70,6 +70,7 @@
"a.Damit du myVBV nutzen kannst, brauchst du ein Konto.": "Pour utiliser myVBV, vous devez créer un compte.", "a.Damit du myVBV nutzen kannst, brauchst du ein Konto.": "Pour utiliser myVBV, vous devez créer un compte.",
"a.Das muss ich nochmals anschauen": "Il faut que je regarde cela encore une fois de plus près", "a.Das muss ich nochmals anschauen": "Il faut que je regarde cela encore une fois de plus près",
"a.Das wurde mit dir geteilt": "Cela a été partagé avec toi", "a.Das wurde mit dir geteilt": "Cela a été partagé avec toi",
"a.Dashboard": "Dashboard",
"a.Datei auswählen": "Sélectionner le fichier", "a.Datei auswählen": "Sélectionner le fichier",
"a.Datei hochladen": "Télécharger le fichier", "a.Datei hochladen": "Télécharger le fichier",
"a.Datei kann nicht gespeichert werden.": "Impossible d'enregistrer le fichier.", "a.Datei kann nicht gespeichert werden.": "Impossible d'enregistrer le fichier.",
@ -104,6 +105,7 @@
"a.E-Mail Adresse": "Adresse e-mail", "a.E-Mail Adresse": "Adresse e-mail",
"a.Einladung": "Invitation", "a.Einladung": "Invitation",
"a.Einladung abschicken": "Envoyer l'invitation", "a.Einladung abschicken": "Envoyer l'invitation",
"a.Einstellungen": "Paramètres",
"a.Elemente zu erledigen": "Eléments à faire", "a.Elemente zu erledigen": "Eléments à faire",
"a.Email": "Email", "a.Email": "Email",
"a.Entfernen": "Supprimer", "a.Entfernen": "Supprimer",

View File

@ -70,6 +70,7 @@
"a.Damit du myVBV nutzen kannst, brauchst du ein Konto.": "Per utilizzare myVBV, hai bisogno di un account.", "a.Damit du myVBV nutzen kannst, brauchst du ein Konto.": "Per utilizzare myVBV, hai bisogno di un account.",
"a.Das muss ich nochmals anschauen": "Devo riguardarlo ancora una volta", "a.Das muss ich nochmals anschauen": "Devo riguardarlo ancora una volta",
"a.Das wurde mit dir geteilt": "Questo è stato condiviso con te", "a.Das wurde mit dir geteilt": "Questo è stato condiviso con te",
"a.Dashboard": "Dashboard",
"a.Datei auswählen": "Selezionare il file", "a.Datei auswählen": "Selezionare il file",
"a.Datei hochladen": "Carica il file", "a.Datei hochladen": "Carica il file",
"a.Datei kann nicht gespeichert werden.": "Impossibile salvare il file.", "a.Datei kann nicht gespeichert werden.": "Impossibile salvare il file.",
@ -104,6 +105,7 @@
"a.E-Mail Adresse": "Indirizzo e-mail", "a.E-Mail Adresse": "Indirizzo e-mail",
"a.Einladung": "Invito", "a.Einladung": "Invito",
"a.Einladung abschicken": "Inviare l'invito", "a.Einladung abschicken": "Inviare l'invito",
"a.Einstellungen": "Impostazioni",
"a.Elemente zu erledigen": "Elementi da completare", "a.Elemente zu erledigen": "Elementi da completare",
"a.Email": "E-mail", "a.Email": "E-mail",
"a.Entfernen": "Rimuovere", "a.Entfernen": "Rimuovere",

View File

@ -1,101 +1,78 @@
<script setup lang="ts"> <script setup lang="ts">
import SubNavigation, { type SubNavEntry } from "@/components/header/SubNavigation.vue";
import { useCurrentCourseSession, useEvaluationWithFeedback } from "@/composables"; import { useCurrentCourseSession, useEvaluationWithFeedback } from "@/composables";
import {
CERTIFICATES_ROUTE,
COMPETENCE_ROUTE,
COMPETENCES_ROUTE,
SELF_EVALUATION_ROUTE,
} from "@/router/names";
import { useTranslation } from "i18next-vue";
import * as log from "loglevel"; import * as log from "loglevel";
import { onMounted } from "vue"; import { onMounted } from "vue";
import { useRoute } from "vue-router";
log.debug("CompetenceParentPage created"); log.debug("CompetenceParentPage created");
const props = defineProps<{ const { t } = useTranslation();
courseSlug: string;
}>();
const route = useRoute();
function routeInOverview() {
return route.path.endsWith("/competence");
}
function routeInCompetenceCertificate() {
return route.path.includes("/certificate");
}
function routeInActionCompetences() {
return route.path.endsWith("/competences");
}
function routeInSelfEvaluationAndFeedback() {
return route.path.endsWith("/self-evaluation-and-feedback");
}
const currentCourseSession = useCurrentCourseSession(); const currentCourseSession = useCurrentCourseSession();
const hasEvaluationFeedback = useEvaluationWithFeedback().hasFeedback; const hasEvaluationFeedback = useEvaluationWithFeedback().hasFeedback;
onMounted(async () => { onMounted(async () => {
log.debug("CompetenceParentPage mounted", props.courseSlug); log.debug("CompetenceParentPage mounted");
}); });
const competenceRoute = {
name: COMPETENCE_ROUTE,
};
const certificatesRoute = {
name: CERTIFICATES_ROUTE,
};
const selfEvaluationRoute = {
name: SELF_EVALUATION_ROUTE,
};
const competencesRoute = {
name: COMPETENCES_ROUTE,
};
// todo: replace this menu with a real one before going live
const items: SubNavEntry[] = [
{ id: 0, name: t("a.Übersicht"), route: competenceRoute },
...(currentCourseSession.value.course.configuration.enable_competence_certificates
? [
{
id: 1,
name: t("a.Kompetenznachweise"),
route: certificatesRoute,
},
]
: []),
{
id: 2,
name: hasEvaluationFeedback.value
? t("a.Selbst- und Fremdeinschätzungen")
: t("a.Selbsteinschätzungen"),
dataCy: "self-evaluation-and-feedback-navigation-link",
route: selfEvaluationRoute,
},
{ id: 3, name: t("a.Handlungskompetenzen"), route: competencesRoute },
{
id: 4,
name: "MS Teams",
route: "https://iterativ.ch",
},
{
id: 5,
name: "Vorschau Teilnehmer",
route: "https://iterativ.ch",
},
];
</script> </script>
<template> <template>
<div class="bg-gray-200"> <div class="bg-gray-200">
<nav class="border-b bg-white px-4 lg:px-8"> <SubNavigation :items="items" />
<ul class="flex flex-col lg:flex-row">
<li
class="border-t-2 border-t-transparent"
:class="{ 'border-b-2 border-b-blue-900': routeInOverview() }"
>
<router-link :to="`/course/${courseSlug}/competence`" class="block py-3">
{{ $t("a.Übersicht") }}
</router-link>
</li>
<li
v-if="
currentCourseSession.course.configuration.enable_competence_certificates
"
class="border-t-2 border-t-transparent lg:ml-12"
:class="{ 'border-b-2 border-b-blue-900': routeInCompetenceCertificate() }"
>
<router-link
:to="`/course/${courseSlug}/competence/certificates`"
class="block py-3"
>
{{ $t("a.Kompetenznachweise") }}
</router-link>
</li>
<li
class="border-t-2 border-t-transparent lg:ml-12"
:class="{
'border-b-2 border-b-blue-900': routeInSelfEvaluationAndFeedback(),
}"
>
<router-link
:to="`/course/${courseSlug}/competence/self-evaluation-and-feedback`"
class="block py-3"
data-cy="self-evaluation-and-feedback-navigation-link"
>
{{
hasEvaluationFeedback
? $t("a.Selbst- und Fremdeinschätzungen")
: $t("a.Selbsteinschätzungen")
}}
</router-link>
</li>
<li
class="border-t-2 border-t-transparent lg:ml-12"
:class="{ 'border-b-2 border-b-blue-900': routeInActionCompetences() }"
>
<router-link
:to="`/course/${courseSlug}/competence/competences`"
class="block py-3"
>
{{ $t("a.Handlungskompetenzen") }}
</router-link>
</li>
<!-- Add similar logic for other `li` items as you expand the list -->
<li class="ml-6 inline-block lg:ml-12"></li>
</ul>
</nav>
<main> <main>
<router-view></router-view> <router-view></router-view>
</main> </main>

View File

@ -13,6 +13,13 @@ import {
import { addToHistory, setLastNavigationWasPush } from "@/router/history"; import { addToHistory, setLastNavigationWasPush } from "@/router/history";
import { onboardingRedirect } from "@/router/onboarding"; import { onboardingRedirect } from "@/router/onboarding";
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import {
CERTIFICATES_ROUTE,
COMPETENCE_ROUTE,
COMPETENCES_ROUTE,
SELF_EVALUATION_ROUTE,
SETTINGS_ROUTE,
} from "./names";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -107,10 +114,12 @@ const router = createRouter({
{ {
path: "", path: "",
props: true, props: true,
name: COMPETENCE_ROUTE,
component: () => import("@/pages/competence/CompetenceIndexPage.vue"), component: () => import("@/pages/competence/CompetenceIndexPage.vue"),
}, },
{ {
path: "certificates", path: "certificates",
name: CERTIFICATES_ROUTE,
props: true, props: true,
component: () => component: () =>
import("@/pages/competence/CompetenceCertificateListPage.vue"), import("@/pages/competence/CompetenceCertificateListPage.vue"),
@ -122,7 +131,7 @@ const router = createRouter({
import("@/pages/competence/CompetenceCertificateDetailPage.vue"), import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
}, },
{ {
name: "selfEvaluationAndFeedback", name: SELF_EVALUATION_ROUTE,
path: "self-evaluation-and-feedback", path: "self-evaluation-and-feedback",
props: true, props: true,
component: () => component: () =>
@ -130,6 +139,7 @@ const router = createRouter({
}, },
{ {
path: "competences", path: "competences",
name: COMPETENCES_ROUTE,
props: true, props: true,
component: () => import("@/pages/competence/ActionCompetenceListPage.vue"), component: () => import("@/pages/competence/ActionCompetenceListPage.vue"),
}, },
@ -393,6 +403,7 @@ const router = createRouter({
}, },
{ {
path: "/settings", path: "/settings",
name: SETTINGS_ROUTE,
component: () => import("@/pages/SettingsPage.vue"), component: () => import("@/pages/SettingsPage.vue"),
}, },
{ {

View File

@ -0,0 +1,5 @@
export const COMPETENCE_ROUTE = "competence";
export const CERTIFICATES_ROUTE = "certificates";
export const SELF_EVALUATION_ROUTE = "selfEvaluationAndFeedback";
export const COMPETENCES_ROUTE = "competences";
export const SETTINGS_ROUTE = "settings";

View File

@ -1,6 +1,7 @@
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { computed } from "vue"; import { computed } from "vue";
import type { RouteLocationRaw } from "vue-router";
import { useRouteLookups } from "./route"; import { useRouteLookups } from "./route";
export function useNavigationAttributes() { export function useNavigationAttributes() {
@ -77,3 +78,7 @@ export function useNavigationAttributes() {
hasSessionTitle, hasSessionTitle,
}; };
} }
export const isExternalLink = (route: string | RouteLocationRaw) => {
return typeof route === "string" && route.startsWith("https");
};

View File

@ -1,3 +1,4 @@
import { computed } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
export function useRouteLookups() { export function useRouteLookups() {
@ -7,11 +8,15 @@ export function useRouteLookups() {
return route.path.startsWith("/course/"); return route.path.startsWith("/course/");
} }
const isInCourse = computed(() => inCourse());
function inCockpit() { function inCockpit() {
const regex = new RegExp("/course/[^/]+/cockpit($|/)"); const regex = new RegExp("/course/[^/]+/cockpit($|/)");
return regex.test(route.path); return regex.test(route.path);
} }
const isInCockpit = computed(() => inCockpit());
function inLearningPath() { function inLearningPath() {
const regex = new RegExp("/course/[^/]+/learn($|/)"); const regex = new RegExp("/course/[^/]+/learn($|/)");
return regex.test(route.path); return regex.test(route.path);
@ -39,7 +44,9 @@ export function useRouteLookups() {
return { return {
inMediaLibrary, inMediaLibrary,
isInCourse,
inCockpit, inCockpit,
isInCockpit,
inLearningPath, inLearningPath,
inCompetenceProfile, inCompetenceProfile,
inLearningMentor, inLearningMentor,

View File

@ -176,8 +176,12 @@ textarea {
@apply rounded-full bg-blue-900 px-4 py-2 font-semibold text-white; @apply rounded-full bg-blue-900 px-4 py-2 font-semibold text-white;
} }
.nav-item-base {
@apply inline-flex items-center border-b-4 border-transparent px-1 pt-1 text-white;
}
.nav-item { .nav-item {
@apply inline-flex items-center border-b-4 border-transparent px-1 pt-1 text-white hover:text-sky-500; @apply nav-item-base hover:text-sky-500;
} }
.nav-item-no-mobile { .nav-item-no-mobile {