Merged in feature/new-navigation-layout (pull request #414)
Feature/new navigation layout Approved-by: Elia Bieri
This commit is contained in:
commit
d361dabd16
|
|
@ -1,29 +1,16 @@
|
|||
<template>
|
||||
<AccountMenuContent
|
||||
:course-sessions="courseSessionsStore.allCurrentCourseSessions"
|
||||
:selected-course-session="courseSessionsStore.currentCourseSession?.id"
|
||||
:user="userStore"
|
||||
@logout="logout"
|
||||
@select-course-session="selectCourseSession"
|
||||
@close="emit('close')"
|
||||
/>
|
||||
<AccountMenuContent :user="userStore" @logout="logout" @close="emit('close')" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AccountMenuContent from "@/components/header/AccountMenuContent.vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { CourseSession } from "@/types";
|
||||
|
||||
const emit = defineEmits(["close"]);
|
||||
|
||||
const logout = () => {
|
||||
userStore.handleLogout();
|
||||
};
|
||||
const selectCourseSession = (courseSession: CourseSession) => {
|
||||
courseSessionsStore.switchCourseSessionById(courseSession.id);
|
||||
};
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const userStore = useUserStore();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,32 +1,28 @@
|
|||
<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 { CourseSession } from "@/types";
|
||||
import { useRouteLookups } from "@/utils/route";
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSessions: CourseSession[];
|
||||
defineProps<{
|
||||
user: User;
|
||||
selectedCourseSession?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["selectCourseSession", "logout", "close"]);
|
||||
const emit = defineEmits(["logout", "close"]);
|
||||
|
||||
const router = useRouter();
|
||||
const { inCourse } = useRouteLookups();
|
||||
|
||||
const showCourseSessionMenu = computed(() => inCourse() && props.courseSessions.length);
|
||||
|
||||
async function navigate(routeName: string) {
|
||||
await router.push({ name: routeName });
|
||||
emit("close");
|
||||
}
|
||||
|
||||
const settingsRoute = {
|
||||
name: SETTINGS_ROUTE,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-black">
|
||||
<div class="flex flex-col gap-4 text-black">
|
||||
<div class="border-b py-4">
|
||||
<div class="flex justify-start">
|
||||
<div v-if="user.avatar_url">
|
||||
|
|
@ -46,22 +42,19 @@ async function navigate(routeName: string) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showCourseSessionMenu" class="border-b py-4">
|
||||
<CourseSessionsMenu
|
||||
:items="courseSessions"
|
||||
:selected="selectedCourseSession"
|
||||
@select="emit('selectCourseSession', $event)"
|
||||
/>
|
||||
</div>
|
||||
<router-link class="flex items-center gap-2" :to="settingsRoute">
|
||||
<it-icon-settings />
|
||||
{{ $t("a.Einstellungen") }}
|
||||
</router-link>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="mt-6 flex items-center"
|
||||
class="flex items-center gap-2"
|
||||
data-cy="logout-button"
|
||||
@click="emit('logout')"
|
||||
>
|
||||
<it-icon-logout class="inline-block" />
|
||||
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span>
|
||||
<span>{{ $t("mainNavigation.logout") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ import {
|
|||
} from "@/utils/utils";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { computed } from "vue";
|
||||
import SelectedCourseSession from "./SelectedCourseSession.vue";
|
||||
|
||||
const { t } = useTranslation();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
|
||||
const { inCockpit, inCompetenceProfile, inLearningMentor, inLearningPath } =
|
||||
const { isInCockpit, inCompetenceProfile, inLearningMentor, inLearningPath } =
|
||||
useRouteLookups();
|
||||
const {
|
||||
hasCompetenceNaviMenu,
|
||||
|
|
@ -30,13 +31,18 @@ const mentorTabTitle = computed(() =>
|
|||
);
|
||||
</script>
|
||||
<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
|
||||
v-if="hasCockpitMenu"
|
||||
data-cy="navigation-cockpit-link"
|
||||
:to="getCockpitUrl(courseSessionsStore.currentCourseSession.course.slug)"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCockpit() }"
|
||||
class="nav-item-no-mobile"
|
||||
:class="{ 'nav-item--active': isInCockpit }"
|
||||
>
|
||||
{{ t("cockpit.title") }}
|
||||
</router-link>
|
||||
|
|
@ -46,7 +52,7 @@ const mentorTabTitle = computed(() =>
|
|||
data-cy="navigation-preview-link"
|
||||
:to="getLearningPathUrl(courseSessionsStore.currentCourseSession.course.slug)"
|
||||
target="_blank"
|
||||
class="nav-item"
|
||||
class="nav-item-no-mobile"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span>{{ t("a.Vorschau Teilnehmer") }}</span>
|
||||
|
|
@ -57,7 +63,7 @@ const mentorTabTitle = computed(() =>
|
|||
v-if="hasLearningPathMenu"
|
||||
data-cy="navigation-learning-path-link"
|
||||
:to="getLearningPathUrl(courseSessionsStore.currentCourseSession.course.slug)"
|
||||
class="nav-item"
|
||||
class="nav-item-no-mobile"
|
||||
:class="{ 'nav-item--active': inLearningPath() }"
|
||||
>
|
||||
{{ t("general.learningPath") }}
|
||||
|
|
@ -67,7 +73,7 @@ const mentorTabTitle = computed(() =>
|
|||
v-if="hasCompetenceNaviMenu"
|
||||
data-cy="navigation-competence-profile-link"
|
||||
:to="getCompetenceNaviUrl(courseSessionsStore.currentCourseSession.course.slug)"
|
||||
class="nav-item"
|
||||
class="nav-item-no-mobile"
|
||||
:class="{ 'nav-item--active': inCompetenceProfile() }"
|
||||
>
|
||||
{{ t("competences.title") }}
|
||||
|
|
@ -77,7 +83,7 @@ const mentorTabTitle = computed(() =>
|
|||
v-if="hasLearningMentor"
|
||||
data-cy="navigation-learning-mentor-link"
|
||||
:to="getLearningMentorUrl(courseSessionsStore.currentCourseSession.course.slug)"
|
||||
class="nav-item"
|
||||
class="nav-item-no-mobile"
|
||||
:class="{ 'nav-item--active': inLearningMentor() }"
|
||||
>
|
||||
{{ t(mentorTabTitle) }}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
import { useRouteLookups } from "@/utils/route";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
const { isInCourse } = useRouteLookups();
|
||||
|
||||
const { t } = useTranslation();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hidden flex-shrink-0 items-center lg:flex">
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-shrink-0 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">
|
||||
<it-icon-vbv class="-ml-3 -mt-6 mr-3 h-8 w-16" />
|
||||
</router-link>
|
||||
|
|
@ -15,6 +27,6 @@ const { t } = useTranslation();
|
|||
{{ t("general.title") }}
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -15,12 +15,7 @@ log.debug("MainNavigationBar created");
|
|||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const { inMediaLibrary, inAppointments } = useRouteLookups();
|
||||
const { hasMediaLibraryMenu, hasAppointmentsMenu, hasSessionTitle } =
|
||||
useNavigationAttributes();
|
||||
|
||||
const selectedCourseSessionTitle = computed(() => {
|
||||
return courseSessionsStore.currentCourseSession?.title;
|
||||
});
|
||||
const { hasMediaLibraryMenu, hasAppointmentsMenu } = useNavigationAttributes();
|
||||
|
||||
const appointmentsUrl = computed(() => {
|
||||
const currentCourseSession = courseSessionsStore.currentCourseSession;
|
||||
|
|
@ -40,7 +35,6 @@ onMounted(() => {
|
|||
<nav class="bg-blue-900 text-white">
|
||||
<div class="mx-auto px-4 lg:px-8">
|
||||
<div class="relative flex h-16 justify-between">
|
||||
<MobileMenuButton />
|
||||
<div class="flex flex-1 items-stretch justify-start">
|
||||
<HomeNavigation />
|
||||
<CourseSessionNavigation />
|
||||
|
|
@ -62,7 +56,7 @@ onMounted(() => {
|
|||
v-if="hasAppointmentsMenu"
|
||||
:to="appointmentsUrl"
|
||||
data-cy="all-duedates-link"
|
||||
class="nav-item"
|
||||
class="nav-item-no-mobile"
|
||||
:class="{ 'nav-item--active': inAppointments() }"
|
||||
>
|
||||
<it-icon-calendar-light class="h-8 w-8" />
|
||||
|
|
@ -71,22 +65,12 @@ onMounted(() => {
|
|||
<!-- Notification Bell & Menu -->
|
||||
<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">
|
||||
<ProfileMenuButton />
|
||||
</div>
|
||||
</div>
|
||||
<MobileMenuButton />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style lang="postcss"></style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
||||
import { useVVByLink } from "@/composables";
|
||||
import { SETTINGS_ROUTE } from "@/router/names";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { User } from "@/stores/user";
|
||||
import type { CourseSession } from "@/types";
|
||||
|
|
@ -49,6 +50,10 @@ const mentorTabTitle = computed(() =>
|
|||
? "a.Praxisbildner"
|
||||
: "a.Lernbegleitung"
|
||||
);
|
||||
|
||||
const settingsRoute = {
|
||||
name: SETTINGS_ROUTE,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -71,57 +76,64 @@ const mentorTabTitle = computed(() =>
|
|||
</div>
|
||||
<div>
|
||||
<div v-if="courseSession" class="mt-6 border-b">
|
||||
<h4 class="text-sm text-gray-900">{{ courseSession.course.title }}</h4>
|
||||
<ul class="mt-6">
|
||||
<h4 class="px-4 text-sm text-gray-900">{{ courseSession.course.title }}</h4>
|
||||
<ul class="mt-6 flex flex-col">
|
||||
<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"
|
||||
@click="clickLink(getCockpitUrl(courseSession.course.slug))"
|
||||
:to="getCockpitUrl(courseSession.course.slug)"
|
||||
@click="$emit('closemodal')"
|
||||
>
|
||||
{{ $t("cockpit.title") }}
|
||||
</button>
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="hasPreviewMenu" class="mb-6">
|
||||
<button
|
||||
<li v-if="hasPreviewMenu" class="mb-2 flex">
|
||||
<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"
|
||||
@click="clickLink(getLearningPathUrl(courseSession.course.slug))"
|
||||
:to="getLearningPathUrl(courseSession.course.slug)"
|
||||
@click="$emit('closemodal')"
|
||||
>
|
||||
{{ $t("a.Vorschau Teilnehmer") }}
|
||||
</button>
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="hasLearningPathMenu" class="mb-6">
|
||||
<button
|
||||
<li v-if="hasLearningPathMenu" class="mb-2 flex">
|
||||
<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"
|
||||
@click="clickLink(getLearningPathUrl(courseSession.course.slug))"
|
||||
:to="getLearningPathUrl(courseSession.course.slug)"
|
||||
@click="$emit('closemodal')"
|
||||
>
|
||||
{{ $t("general.learningPath") }}
|
||||
</button>
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="hasCompetenceNaviMenu" class="mb-6">
|
||||
<button
|
||||
<li v-if="hasCompetenceNaviMenu" class="mb-2 flex">
|
||||
<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"
|
||||
@click="clickLink(getCompetenceNaviUrl(courseSession.course.slug))"
|
||||
:to="getCompetenceNaviUrl(courseSession.course.slug)"
|
||||
@click="$emit('closemodal')"
|
||||
>
|
||||
{{ $t("competences.title") }}
|
||||
</button>
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="hasLearningMentor" class="mb-6">
|
||||
<button
|
||||
<li v-if="hasLearningMentor" class="mb-2 flex">
|
||||
<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"
|
||||
@click="clickLink(getLearningMentorUrl(courseSession.course.slug))"
|
||||
:to="getLearningMentorUrl(courseSession.course.slug)"
|
||||
@click="$emit('closemodal')"
|
||||
>
|
||||
{{ $t(mentorTabTitle) }}
|
||||
</button>
|
||||
</router-link>
|
||||
</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
|
||||
v-if="isVVLearningMentor(courseSessionsStore.currentCourseSession)"
|
||||
class="mb-6"
|
||||
|
|
@ -137,15 +149,47 @@ const mentorTabTitle = computed(() =>
|
|||
</div>
|
||||
<div class="mt-6 border-b">
|
||||
<ul>
|
||||
<li class="mb-6">
|
||||
<button data-cy="dashboard-link" @click="clickLink('/')">myVBV</button>
|
||||
<li v-if="courseSession && hasMediaLibraryMenu" class="mb-6 flex">
|
||||
<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>
|
||||
</ul>
|
||||
</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
|
||||
v-if="user?.loggedIn"
|
||||
type="button"
|
||||
class="mt-6 flex items-center"
|
||||
class="mt-6 flex items-center px-4 py-2"
|
||||
@click="$emit('logout')"
|
||||
>
|
||||
<it-icon-logout class="inline-block" />
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const showMenu = ref(false);
|
|||
@logout="userStore.handleLogout()"
|
||||
/>
|
||||
</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 -->
|
||||
<div data-cy="navigation-mobile-menu-button" class="flex" @click="showMenu = true">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -29,7 +29,11 @@ function popoverClick(event: Event) {
|
|||
<AccountMenu @close="showMenu = false" />
|
||||
</ItFullScreenModal>
|
||||
</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">
|
||||
<PopoverButton @click="popoverClick($event)">
|
||||
<div v-if="userStore.avatar_url">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -70,6 +70,7 @@
|
|||
"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 wurde mit dir geteilt": "Das wurde mit dir geteilt",
|
||||
"a.Dashboard": "Dashboard",
|
||||
"a.Datei auswählen": "Datei auswählen",
|
||||
"a.Datei hochladen": "Datei hochladen",
|
||||
"a.Datei kann nicht gespeichert werden.": "Datei kann nicht gespeichert werden.",
|
||||
|
|
@ -104,6 +105,7 @@
|
|||
"a.E-Mail Adresse": "E-Mail Adresse",
|
||||
"a.Einladung": "Einladung",
|
||||
"a.Einladung abschicken": "Einladung abschicken",
|
||||
"a.Einstellungen": "Einstellungen",
|
||||
"a.Elemente zu erledigen": "Elemente zu erledigen",
|
||||
"a.Email": "Email",
|
||||
"a.Entfernen": "Entfernen",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@
|
|||
"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 wurde mit dir geteilt": "Cela a été partagé avec toi",
|
||||
"a.Dashboard": "Dashboard",
|
||||
"a.Datei auswählen": "Sélectionner le fichier",
|
||||
"a.Datei hochladen": "Télécharger 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.Einladung": "Invitation",
|
||||
"a.Einladung abschicken": "Envoyer l'invitation",
|
||||
"a.Einstellungen": "Paramètres",
|
||||
"a.Elemente zu erledigen": "Eléments à faire",
|
||||
"a.Email": "Email",
|
||||
"a.Entfernen": "Supprimer",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@
|
|||
"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 wurde mit dir geteilt": "Questo è stato condiviso con te",
|
||||
"a.Dashboard": "Dashboard",
|
||||
"a.Datei auswählen": "Selezionare il file",
|
||||
"a.Datei hochladen": "Carica il file",
|
||||
"a.Datei kann nicht gespeichert werden.": "Impossibile salvare il file.",
|
||||
|
|
@ -104,6 +105,7 @@
|
|||
"a.E-Mail Adresse": "Indirizzo e-mail",
|
||||
"a.Einladung": "Invito",
|
||||
"a.Einladung abschicken": "Inviare l'invito",
|
||||
"a.Einstellungen": "Impostazioni",
|
||||
"a.Elemente zu erledigen": "Elementi da completare",
|
||||
"a.Email": "E-mail",
|
||||
"a.Entfernen": "Rimuovere",
|
||||
|
|
|
|||
|
|
@ -1,101 +1,78 @@
|
|||
<script setup lang="ts">
|
||||
import SubNavigation, { type SubNavEntry } from "@/components/header/SubNavigation.vue";
|
||||
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 { onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
log.debug("CompetenceParentPage created");
|
||||
|
||||
const props = defineProps<{
|
||||
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 { t } = useTranslation();
|
||||
|
||||
const currentCourseSession = useCurrentCourseSession();
|
||||
const hasEvaluationFeedback = useEvaluationWithFeedback().hasFeedback;
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<nav class="border-b bg-white px-4 lg:px-8">
|
||||
<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>
|
||||
<SubNavigation :items="items" />
|
||||
<main>
|
||||
<router-view></router-view>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ import {
|
|||
import { addToHistory, setLastNavigationWasPush } from "@/router/history";
|
||||
import { onboardingRedirect } from "@/router/onboarding";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import {
|
||||
CERTIFICATES_ROUTE,
|
||||
COMPETENCE_ROUTE,
|
||||
COMPETENCES_ROUTE,
|
||||
SELF_EVALUATION_ROUTE,
|
||||
SETTINGS_ROUTE,
|
||||
} from "./names";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
|
@ -107,10 +114,12 @@ const router = createRouter({
|
|||
{
|
||||
path: "",
|
||||
props: true,
|
||||
name: COMPETENCE_ROUTE,
|
||||
component: () => import("@/pages/competence/CompetenceIndexPage.vue"),
|
||||
},
|
||||
{
|
||||
path: "certificates",
|
||||
name: CERTIFICATES_ROUTE,
|
||||
props: true,
|
||||
component: () =>
|
||||
import("@/pages/competence/CompetenceCertificateListPage.vue"),
|
||||
|
|
@ -122,7 +131,7 @@ const router = createRouter({
|
|||
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
|
||||
},
|
||||
{
|
||||
name: "selfEvaluationAndFeedback",
|
||||
name: SELF_EVALUATION_ROUTE,
|
||||
path: "self-evaluation-and-feedback",
|
||||
props: true,
|
||||
component: () =>
|
||||
|
|
@ -130,6 +139,7 @@ const router = createRouter({
|
|||
},
|
||||
{
|
||||
path: "competences",
|
||||
name: COMPETENCES_ROUTE,
|
||||
props: true,
|
||||
component: () => import("@/pages/competence/ActionCompetenceListPage.vue"),
|
||||
},
|
||||
|
|
@ -393,6 +403,7 @@ const router = createRouter({
|
|||
},
|
||||
{
|
||||
path: "/settings",
|
||||
name: SETTINGS_ROUTE,
|
||||
component: () => import("@/pages/SettingsPage.vue"),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { computed } from "vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
import { useRouteLookups } from "./route";
|
||||
|
||||
export function useNavigationAttributes() {
|
||||
|
|
@ -77,3 +78,7 @@ export function useNavigationAttributes() {
|
|||
hasSessionTitle,
|
||||
};
|
||||
}
|
||||
|
||||
export const isExternalLink = (route: string | RouteLocationRaw) => {
|
||||
return typeof route === "string" && route.startsWith("https");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
export function useRouteLookups() {
|
||||
|
|
@ -7,11 +8,15 @@ export function useRouteLookups() {
|
|||
return route.path.startsWith("/course/");
|
||||
}
|
||||
|
||||
const isInCourse = computed(() => inCourse());
|
||||
|
||||
function inCockpit() {
|
||||
const regex = new RegExp("/course/[^/]+/cockpit($|/)");
|
||||
return regex.test(route.path);
|
||||
}
|
||||
|
||||
const isInCockpit = computed(() => inCockpit());
|
||||
|
||||
function inLearningPath() {
|
||||
const regex = new RegExp("/course/[^/]+/learn($|/)");
|
||||
return regex.test(route.path);
|
||||
|
|
@ -39,7 +44,9 @@ export function useRouteLookups() {
|
|||
|
||||
return {
|
||||
inMediaLibrary,
|
||||
isInCourse,
|
||||
inCockpit,
|
||||
isInCockpit,
|
||||
inLearningPath,
|
||||
inCompetenceProfile,
|
||||
inLearningMentor,
|
||||
|
|
|
|||
|
|
@ -176,8 +176,12 @@ textarea {
|
|||
@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 {
|
||||
@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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue