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>
<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>

View File

@ -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>

View File

@ -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) }}

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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

View File

@ -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">

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.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",

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.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",

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.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",

View File

@ -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>

View File

@ -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"),
},
{

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 { 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");
};

View File

@ -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,

View File

@ -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 {