Merged in feature/VBV-260-implement-new-learnpath (pull request #28)

Feature/VBV-260 implement new learnpath

* Fix linting problems

* Fix cypress tests

* Change various texts to gray-800

* Rework progress bar to show in progress circles

* Scroll to initial circle

* Make list view circles clickable

* Minor rework actions

* Rework

* Fix LearningPathScrollButton alignment

* Additional fix for LearningPathScrollButton alignment
This commit is contained in:
Elia Bieri 2023-03-02 16:58:16 +00:00
parent 39bc70eff0
commit a7dcb86cfe
18 changed files with 666 additions and 115 deletions

View File

@ -0,0 +1,12 @@
<template>
<p class="mb-4 font-bold">Nächste Termine</p>
<ul>
<li class="border-b border-t py-3">
<p class="pr-12">24. November 2022, 11 Uhr - Austausch mit Trainer</p>
</li>
<li class="border-b py-3">
<p class="pr-12">4. Dezember 2022, 10.30 Uhr - Vernetzen - Live Session</p>
</li>
</ul>
<p class="pt-2 underline">Alle Termine anzeigen</p>
</template>

View File

@ -51,9 +51,17 @@ const pieData = computed(() => {
.reverse() as CircleSector[];
});
const width = 450;
const height = 450;
const radius = Math.min(width, height) / 2.4;
const width = 100;
const height = 100;
const radius = 50;
const svgId = computed(() => {
// Generate a random id for the svg element like 'circle-visualization-ziurxxmp'
return `circle-visualization-${Math.random()
.toString(36)
.replace(/[^a-z]+/g, "")
.substring(2, 10)}`;
});
function getColor(sector: CircleSector) {
let color = colors.gray[300];
@ -67,7 +75,7 @@ function getColor(sector: CircleSector) {
}
function render() {
const svg = d3.select(".circle-visualization");
const svg = d3.select("#" + svgId.value);
// Clean svg before adding new stuff.
svg.selectAll("*").remove();
@ -124,10 +132,10 @@ function render() {
</script>
<template>
<div class="svg-container h-full content-center">
<div class="svg-container content-center">
<pre hidden>{{ pieData }}</pre>
<pre hidden>{{ render() }}</pre>
<svg class="circle-visualization h-full">
<svg :id="svgId" class="h-full">
<circle :cx="width / 2" :cy="height / 2" :r="radius" :color="colors.gray[300]" />
<circle :cx="width / 2" :cy="height / 2" :r="radius / 2.5" color="white" />
</svg>

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import type { CircleSectorData } from "@/components/learningPath/page/LearningPathCircle.vue";
import LearningPathCircle from "@/components/learningPath/page/LearningPathCircle.vue";
import LearningPathContinueButton from "@/components/learningPath/page/LearningPathContinueButton.vue";
import { calculateCircleSectorData } from "@/components/learningPath/page/utils";
import type { Circle } from "@/services/circle";
import type { LearningPath } from "@/services/learningPath";
import type { Topic } from "@/types";
const props = defineProps<{
learningPath: LearningPath | undefined;
circle: Circle;
topic: Topic;
isFirstCircle: boolean;
isLastCircle: boolean;
isCurrentCircle: boolean;
}>();
</script>
<template>
<div class="flex flex-row items-center pb-2">
<div class="w-12">
<hr v-if="!props.isFirstCircle" class="h-[1.6px] w-full border-0 bg-gray-500" />
</div>
<LearningPathCircle
:sectors="calculateCircleSectorData(circle)"
class="h-24 w-24 snap-center"
></LearningPathCircle>
<div class="w-12">
<hr v-if="!props.isLastCircle" class="h-[1.6px] w-full border-0 bg-gray-500" />
</div>
</div>
<span v-if="props.isCurrentCircle" class="text-sm text-gray-800">
{{ $t("learningPathPage.currentCircle") }}
</span>
<div class="w-36 text-center text-lg font-bold text-blue-900">
{{ props.circle.title }}
</div>
<div v-if="props.isCurrentCircle" class="whitespace-nowrap">
<LearningPathContinueButton
:has-progress="!props.learningPath?.continueData?.has_no_progress"
:url="props.learningPath?.continueData?.url"
></LearningPathContinueButton>
</div>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
interface Props {
hasProgress: boolean;
url: string;
}
const props = withDefaults(defineProps<Props>(), {
hasProgress: false,
url: "",
});
</script>
<template>
<router-link
class="btn-blue mt-2 pl-6"
:to="props.url"
data-cy="lp-continue-button"
translate
>
<span>
{{ props.hasProgress ? $t("general.nextStep") : $t("general.start") }}
</span>
</router-link>
</template>

View File

@ -0,0 +1,61 @@
<script setup lang="ts">
import type { CircleSectorData } from "@/components/learningPath/page/LearningPathCircle.vue";
import LearningPathCircle from "@/components/learningPath/page/LearningPathCircle.vue";
import LearningPathContinueButton from "@/components/learningPath/page/LearningPathContinueButton.vue";
import { calculateCircleSectorData } from "@/components/learningPath/page/utils";
import type { Circle } from "@/services/circle";
import type { LearningPath } from "@/services/learningPath";
import { computed, onMounted } from "vue";
const props = defineProps<{
learningPath: LearningPath | undefined;
}>();
onMounted(() => {
const initialCircle = props.learningPath?.nextLearningContent?.parentCircle;
if (initialCircle) {
scrollToCircle(initialCircle);
}
});
const topics = computed(() => props.learningPath?.topics ?? []);
const shouldContinueHere = (circle: Circle) =>
props.learningPath?.nextLearningContent?.parentCircle === circle;
const scrollToCircle = (circle: Circle) => {
const id = `circle-${circle.slug}`;
const el = document.getElementById(id);
el?.scrollIntoView({ behavior: "smooth", inline: "center", block: "end" });
};
</script>
<template>
<div v-for="topic in topics" :key="topic.title">
<div class="pb-2 font-bold text-gray-700">
{{ topic.title }}
</div>
<router-link
v-for="circle in topic.circles"
:id="`circle-${circle.slug}`"
:key="circle.id"
:to="circle.frontend_url"
class="flex flex-row items-center pb-2"
>
<LearningPathCircle
:sectors="calculateCircleSectorData(circle)"
class="h-12 w-12"
></LearningPathCircle>
<div class="pl-3 font-bold text-blue-900">
{{ circle.title }}
</div>
<div v-if="shouldContinueHere(circle)" class="whitespace-nowrap pl-4">
<LearningPathContinueButton
:has-progress="!props.learningPath?.continueData?.has_no_progress"
:url="props.learningPath?.continueData?.url"
></LearningPathContinueButton>
</div>
</router-link>
</div>
</template>

View File

@ -0,0 +1,130 @@
<script setup lang="ts">
import type { CircleSectorData } from "@/components/learningPath/page/LearningPathCircle.vue";
import LearningPathCircleColumn from "@/components/learningPath/page/LearningPathCircleColumn.vue";
import LearningPathScrollButton from "@/components/learningPath/page/LearningPathScrollButton.vue";
import type { Circle } from "@/services/circle";
import type { LearningPath } from "@/services/learningPath";
import { onMounted, ref } from "vue";
const props = defineProps<{
learningPath: LearningPath | undefined;
useMobileLayout: boolean;
}>();
// Scroll bounds
const isAtLeftEnd = ref(true);
const isAtRightEnd = ref(false);
// Constants
const scrollIncrement = 600;
const learnPathDiagramId = "learnpath-diagram";
onMounted(async () => {
watchScrollBounds();
const initialCircle = props.learningPath?.nextLearningContent?.parentCircle;
if (initialCircle) {
scrollToCircle(initialCircle);
}
});
const isFirstCircle = (topicIndex: number, circleIndex: number) =>
topicIndex === 0 && circleIndex === 0;
const isLastCircle = (topicIndex: number, circleIndex: number, numCircles: number) =>
topicIndex === (props.learningPath?.topics ?? []).length - 1 &&
circleIndex === numCircles - 1;
const isCurrentCircle = (circle: Circle) =>
props.learningPath?.nextLearningContent?.parentCircle === circle;
const watchScrollBounds = () => {
// We need to monitor the scroll bounds of the diagram to hide the scroll buttons when we're at the end
const el = document.getElementById(learnPathDiagramId);
el?.addEventListener("scroll", () => recalculateScrollBounds());
};
const recalculateScrollBounds = () => {
const el = document.getElementById(learnPathDiagramId);
const scrollLeft = el?.scrollLeft ?? 0;
const scrollWidth = el?.scrollWidth ?? 0;
isAtLeftEnd.value = scrollLeft === 0;
isAtRightEnd.value = scrollLeft > scrollWidth - (el?.clientWidth ?? 0) - 1;
};
const scrollRight = () => scrollLearnPathDiagram(scrollIncrement);
const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
const scrollLearnPathDiagram = (offset: number) => {
const el = document.getElementById(learnPathDiagramId);
const newScrollOffset = (el?.scrollLeft ?? 0) + offset;
el?.scroll({
left: newScrollOffset,
top: 0,
behavior: "smooth",
});
};
const scrollToCircle = (circle: Circle) => {
const id = `circle-${circle.slug}`;
const el = document.getElementById(id);
el?.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" });
};
</script>
<template>
<div class="relative flex flex-row items-center">
<LearningPathScrollButton
v-show="!isAtLeftEnd"
direction="left"
:hidden="props.useMobileLayout"
@click="scrollLeft"
></LearningPathScrollButton>
<div
:id="learnPathDiagramId"
class="flex h-96 snap-x flex-row overflow-auto py-5 sm:overflow-hidden sm:py-10"
>
<div
v-for="(topic, topicIndex) in props.learningPath?.topics ?? []"
:key="topic.title"
class="border-l border-gray-500"
:class="topicIndex == 0 ? 'ml-6 sm:ml-12' : ''"
>
<p
:id="`topic-${topic.slug}`"
class="inline-block h-12 self-start px-4 font-bold text-gray-800"
>
{{ topic.title }}
</p>
<div class="flex flex-row pt-6">
<router-link
v-for="(circle, circleIndex) in topic.circles"
:id="`circle-${circle.slug}`"
:key="circle.id"
:to="circle.frontend_url"
:data-cy="`circle-${circle.title}`"
class="flex flex-col items-center pb-6"
>
<LearningPathCircleColumn
:learning-path="learningPath"
:circle="circle"
:topic="topic"
:is-first-circle="isFirstCircle(topicIndex, circleIndex)"
:is-last-circle="
isLastCircle(topicIndex, circleIndex, topic.circles.length)
"
:is-current-circle="isCurrentCircle(circle)"
></LearningPathCircleColumn>
</router-link>
</div>
</div>
</div>
<LearningPathScrollButton
v-show="!isAtRightEnd"
direction="right"
:hidden="props.useMobileLayout"
@click="scrollRight"
></LearningPathScrollButton>
</div>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{ allCount: number; inProgressCount: number }>();
const progress = computed(() => {
if (props.allCount === 0) {
return 0;
}
return (props.inProgressCount / props.allCount) * 100;
});
</script>
<template>
<div>
<span class="relative inline-block h-2 w-full bg-white">
<div
v-if="progress > 0"
class="absolute h-full bg-green-500"
:style="{ width: `${progress}%` }"
></div>
</span>
<p>
{{
$t("learningPathPage.progressText", {
inProgressCount: props.inProgressCount,
allCount: props.allCount,
})
}}
</p>
</div>
</template>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
export type Direction = "right" | "left";
const emit = defineEmits(["click"]);
const props = defineProps<{
direction: Direction;
}>();
</script>
<template>
<transition
leave-active-class="transition ease-out duration-600"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
enter-active-class="transition ease-in duration-600"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
>
<button
class="absolute h-full w-32 from-white"
:class="
props.direction === 'right'
? 'right-0 bg-gradient-to-l'
: 'left-0 bg-gradient-to-r'
"
@click="emit('click')"
>
<div
class="flex flex-col"
:class="props.direction === 'right' ? 'items-end' : 'items-start'"
>
<component
:is="`it-icon-arrow-${props.direction}`"
class="mb-16 h-20 w-20 text-gray-600"
></component>
</div>
</button>
</transition>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import type { Topic } from "@/types";
const props = defineProps<{
topics: Topic[];
}>();
const scrollToTopic = (topic: Topic) => {
const id = `topic-${topic.slug}`;
const el = document.getElementById(id);
el?.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" });
};
</script>
<template>
<div class="text-sm text-gray-800">
<span>{{ $t("learningPathPage.topics") }}</span>
<button
v-for="topic in props.topics"
:key="topic.slug"
class="pl-2 underline sm:pl-4"
@click="scrollToTopic(topic)"
>
{{ topic.title }}
</button>
</div>
</template>

View File

@ -0,0 +1,52 @@
<script setup lang="ts">
import ItToggleSwitch from "@/components/ui/ItToggleSwitch.vue";
import { computed, ref } from "vue";
export type ViewType = "path" | "list";
const props = defineProps<{
initialView: ViewType;
}>();
const emit = defineEmits<{
(event: "selectView", value: ViewType): void;
}>();
const selectedView = ref<ViewType>(props.initialView);
const activeStyle = "text-black";
const inactiveStyle = "text-gray-700";
const pathViewLabelStyle = computed(() =>
selectedView.value == "path" ? activeStyle : inactiveStyle
);
const listViewLabelStyle = computed(() =>
selectedView.value == "list" ? activeStyle : inactiveStyle
);
const onToggle = () => {
if (selectedView.value == "path") {
selectedView.value = "list";
} else {
selectedView.value = "path";
}
emit("selectView", selectedView.value);
};
</script>
<template>
<div class="flex flex-row items-center space-x-2 text-sm">
<span :class="pathViewLabelStyle">
{{ $t("learningPathPage.pathView") }}
</span>
<ItToggleSwitch
data-cy="view-switch"
:initially-switched-left="selectedView == 'list'"
@click="onToggle"
></ItToggleSwitch>
<span :class="listViewLabelStyle">
{{ $t("learningPathPage.listView") }}
</span>
</div>
</template>

View File

@ -0,0 +1,20 @@
import type {
CircleSectorData,
CircleSectorProgress,
} from "@/components/learningPath/page/LearningPathCircle.vue";
import type { Circle } from "@/services/circle";
export function calculateCircleSectorData(circle: Circle): CircleSectorData[] {
const sectors = circle.learningSequences.map((ls) => {
let progress: CircleSectorProgress = "none";
if (circle.allFinishedInLearningSequence(ls.translation_key)) {
progress = "finished";
} else if (circle.someFinishedInLearningSequence(ls.translation_key)) {
progress = "in_progress";
}
return {
progress: progress,
};
});
return sectors;
}

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import { Switch } from "@headlessui/vue";
import { ref } from "vue";
interface Props {
initiallySwitchedLeft: boolean;
}
const props = withDefaults(defineProps<Props>(), {
initiallySwitchedLeft: false,
});
const isSwitchedLeft = ref(props.initiallySwitchedLeft);
</script>
<template>
<Switch
v-model="isSwitchedLeft"
class="relative inline-flex h-5 w-11 min-w-[44px] items-center rounded-full bg-gray-200"
>
<span
:class="isSwitchedLeft ? 'translate-x-6' : 'translate-x-0'"
class="inline-block h-5 w-5 transform rounded-full bg-black transition"
/>
</Switch>
</template>

View File

@ -126,9 +126,14 @@
"completeAndContinue": "Als erledigt markieren"
},
"learningPathPage": {
"currentCircle": "Aktueller Circle",
"listView": "Listenansicht",
"nextStep": "Nächster Schritt",
"pathView": "Pfadansicht",
"progressText": "Du hast { inProgressCount } von { allCount } Circles bearbeitet",
"showListView": "Listenansicht anzeigen",
"welcomeBack": "Willkommen zurück, {name}"
"topics": "Themen:",
"welcomeBack": "Hallo { name }! Willkommen zurück in deinem Lehrgang:"
},
"mainNavigation": {
"logout": "Abmelden",

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import IconLogout from "@/components/icons/IconLogout.vue";
import IconSettings from "@/components/icons/IconSettings.vue";
import LearningPathCircle from "@/components/learningPath/LearningPathCircle.vue";
import LearningPathCircle from "@/components/learningPath/page/LearningPathCircle.vue";
import HorizontalBarChart from "@/components/ui/HorizontalBarChart.vue";
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import ItCheckboxGroup from "@/components/ui/ItCheckboxGroup.vue";
@ -9,6 +9,7 @@ import ItDropdown from "@/components/ui/ItDropdown.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
import ItTextarea from "@/components/ui/ItTextarea.vue";
import ItToggleSwitch from "@/components/ui/ItToggleSwitch.vue";
import RatingScale from "@/components/ui/RatingScale.vue";
import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
import logger from "loglevel";
@ -517,6 +518,10 @@ function log(data: any) {
},
]"
></LearningPathCircle>
<h2 class="mt-8 mb-8">ItToggleSwitch</h2>
<ItToggleSwitch></ItToggleSwitch>
</main>
</template>

View File

@ -1,15 +1,16 @@
<script setup lang="ts">
import * as log from "loglevel";
import LearningPathAppointmentsMock from "@/components/learningPath/page/LearningPathAppointmentsMock.vue";
import LearningPathListView from "@/components/learningPath/page/LearningPathListView.vue";
import LearningPathPathView from "@/components/learningPath/page/LearningPathPathView.vue";
import CircleProgress from "@/components/learningPath/page/LearningPathProgress.vue";
import LearningPathTopics from "@/components/learningPath/page/LearningPathTopics.vue";
import type { ViewType } from "@/components/learningPath/page/LearningPathViewSwitch.vue";
import LearningPathViewSwitch from "@/components/learningPath/page/LearningPathViewSwitch.vue";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import { computed, onMounted } from "vue";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import LearningPathViewVertical from "@/components/learningPath/LearningPathViewVertical.vue";
import type { LearningPath } from "@/services/learningPath";
log.debug("LearningPathView created");
import { computed, onMounted, ref } from "vue";
const props = defineProps<{
courseSlug: string;
@ -18,108 +19,136 @@ const props = defineProps<{
const learningPathStore = useLearningPathStore();
const userStore = useUserStore();
const learningPath = computed(() => {
if (userStore.loggedIn && learningPathStore.learningPaths.size > 0) {
const learningPathKey = `${props.courseSlug}-lp-${userStore.id}`;
return learningPathStore.learningPaths.get(learningPathKey);
}
return undefined;
});
// Layout state
const useMobileLayout = ref(false);
const selectedView = ref<ViewType>(
(window.localStorage.getItem("learningPathView") as ViewType) || "path"
);
onMounted(async () => {
log.debug("LearningPathView mounted");
log.debug("LearningPathPage mounted");
try {
await learningPathStore.loadLearningPath(props.courseSlug + "-lp");
} catch (error) {
log.error(error);
}
// Detect if we're on a mobile layout
watchLayoutChanges();
});
const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
if (learningPath?.nextLearningContent) {
const circle = learningPath.nextLearningContent.parentCircle;
const url =
learningPath.nextLearningContent.parentLearningSequence?.frontend_url ||
circle.frontend_url;
const isFirst =
learningPath.nextLearningContent.translation_key ===
learningPath.circles[0].flatLearningContents[0].translation_key;
return [url, isFirst];
const learningPath = computed(() => {
if (userStore.loggedIn && learningPathStore.learningPaths.size > 0) {
const learningPathKey = `${props.courseSlug}-lp-${userStore.id}`;
return learningPathStore.learningPaths.get(learningPathKey);
}
return undefined;
});
return ["", false];
const circlesCount = computed(() => {
if (learningPath.value) {
return learningPath.value.circles.length;
}
return 0;
});
const inProgressCirclesCount = computed(() => {
if (learningPath.value) {
return learningPath.value.circles.filter(
(circle) =>
circle.learningSequences.filter((ls) =>
circle.someFinishedInLearningSequence(ls.translation_key)
).length
).length;
}
return 0;
});
const watchLayoutChanges = () => {
const mobileLayoutQuery = window.matchMedia("(min-width: 640px)");
useMobileLayout.value = !mobileLayoutQuery.matches;
mobileLayoutQuery.onchange = (event) => {
useMobileLayout.value = !event.matches;
};
};
const changeViewType = (viewType: ViewType) => {
selectedView.value = viewType;
window.localStorage.setItem("learningPathView", viewType);
};
</script>
<template>
<div v-if="learningPath" class="bg-gray-200">
<Teleport to="body">
<LearningPathViewVertical
:show="learningPathStore.page === 'OVERVIEW'"
:learning-path="learningPath"
@closemodal="learningPathStore.page = 'INDEX'"
/>
</Teleport>
<div class="learningpath flex flex-col">
<div class="flex h-max flex-col">
<div class="flex flex-col bg-white lg:py-8">
<div class="flex justify-end lg:p-4">
<button
class="btn-text inline-flex items-center px-3 lg:py-2"
data-cy="show-list-view"
@click="learningPathStore.page = 'OVERVIEW'"
>
<it-icon-list />
{{ $t("learningPathPage.showListView") }}
</button>
</div>
<LearningPathDiagram
class="mx-auto max-h-[90px] w-full max-w-[1920px] px-4 lg:max-h-[380px]"
diagram-type="horizontal"
:learning-path="learningPath"
></LearningPathDiagram>
<div class="flex flex-col">
<!-- Top -->
<div class="flex flex-row justify-between space-x-8 bg-gray-200 p-6 sm:p-12">
<!-- Left -->
<div class="flex flex-col justify-between">
<div>
<p class="font-bold">
{{ $t("learningPathPage.welcomeBack", { name: userStore.first_name }) }}
</p>
<h2 data-cy="learning-path-title">
{{ learningPath?.title }}
</h2>
</div>
<div class="container-large pt-0 lg:pt-4">
<h1 data-cy="learning-path-title" class="mt-6 mb-6 lg:mt-12">
{{ learningPath.title }}
</h1>
<div
class="flex flex-col justify-start divide-y divide-gray-500 bg-white p-4 lg:mb-16 lg:flex-row lg:divide-y-0 lg:divide-x"
>
<div class="flex-auto p-2 lg:p-8">
<h2>
{{ $t("learningPathPage.welcomeBack", { name: userStore.first_name }) }}
</h2>
<p class="mt-4 text-xl"></p>
</div>
<div v-if="learningPath.nextLearningContent" class="flex-2 p-4 lg:p-8">
{{ $t("learningPathPage.nextStep") }}
<h3>
{{ learningPath.nextLearningContent.parentCircle.title }}:
{{ learningPath.nextLearningContent.parentLearningSequence?.title }}
</h3>
<router-link
class="btn-blue mt-4"
:to="createContinueUrl(learningPath)[0]"
data-cy="lp-continue-button"
translate
>
<span v-if="createContinueUrl(learningPath)[1]">
{{ $t("general.start") }}
</span>
<span v-else>{{ $t("general.nextStep") }}</span>
</router-link>
</div>
</div>
<CircleProgress
:all-count="circlesCount"
:in-progress-count="inProgressCirclesCount"
></CircleProgress>
</div>
<!-- Right -->
<div v-if="!useMobileLayout" class="max-w-md">
<LearningPathAppointmentsMock></LearningPathAppointmentsMock>
</div>
</div>
<!-- Bottom -->
<div class="bg-white">
<div class="flex flex-col justify-between px-6 sm:flex-row sm:px-12">
<!-- Topics -->
<div
v-if="selectedView == 'path'"
class="order-2 pb-8 sm:order-1 sm:pb-0 sm:pt-4"
>
<LearningPathTopics :topics="learningPath?.topics ?? []"></LearningPathTopics>
</div>
<div class="topic"></div>
<div v-else class="flex-grow"></div>
<!-- View switch -->
<LearningPathViewSwitch
class="order-1 py-8 sm:order-2 sm:py-0 sm:pt-4"
:initial-view="selectedView"
@select-view="changeViewType($event)"
></LearningPathViewSwitch>
</div>
<!-- Path view -->
<div v-if="selectedView == 'path'" class="flex flex-col" data-cy="lp-path-view">
<LearningPathPathView
:learning-path="learningPath"
:use-mobile-layout="useMobileLayout"
></LearningPathPathView>
</div>
<!-- List view -->
<div
v-if="selectedView == 'list'"
class="flex flex-col pl-6 sm:pl-24"
data-cy="lp-list-view"
>
<LearningPathListView :learning-path="learningPath"></LearningPathListView>
</div>
<div
v-if="useMobileLayout"
class="p-6"
:class="useMobileLayout ? 'bg-gray-200' : ''"
>
<LearningPathAppointmentsMock></LearningPathAppointmentsMock>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@ -222,6 +222,12 @@ export class Circle implements WagtailCircle {
return false;
}
public isComplete(): boolean {
return this.learningSequences.every((ls) =>
this.allFinishedInLearningSequence(ls.translation_key)
);
}
public parseCompletionData(completionData: CourseCompletion[]) {
this.flatChildren.forEach((page) => {
const pageIndex = completionData.findIndex((e) => {

View File

@ -10,6 +10,11 @@ import type {
WagtailLearningPath,
} from "@/types";
export interface ContinueData {
url: string;
has_no_progress: boolean;
}
function getLastCompleted(courseId: number, completionData: CourseCompletion[]) {
return orderBy(completionData, ["updated_at"], "desc").find((c: CourseCompletion) => {
return (
@ -125,4 +130,25 @@ export class LearningPath implements WagtailLearningPath {
}
}
}
public get continueData(): ContinueData {
if (this.nextLearningContent) {
const circle = this.nextLearningContent.parentCircle;
const url =
this.nextLearningContent.parentLearningSequence?.frontend_url ||
circle.frontend_url;
const isFirst =
this.nextLearningContent.translation_key ===
this.circles[0].flatLearningContents[0].translation_key;
return {
url,
has_no_progress: isFirst,
};
}
return {
url: "",
has_no_progress: true,
};
}
}

View File

@ -1,4 +1,4 @@
import {login} from "./helpers";
import { login } from "./helpers";
describe("learningPath page", () => {
beforeEach(() => {
@ -19,7 +19,7 @@ describe("learningPath page", () => {
login("admin", "test");
cy.visit("/course/versicherungsvermittler-in-alt/learn");
cy.get('[data-cy="circle-analyse"]').click({force: true});
cy.get('[data-cy="circle-Analyse"]').click({ force: true });
cy.url().should(
"include",
@ -28,20 +28,15 @@ describe("learningPath page", () => {
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
});
it("open listView and click on circle will open circle", () => {
it("switch between list and path view", () => {
login("admin", "test");
cy.visit("/course/versicherungsvermittler-in-alt/learn");
cy.get('[data-cy="show-list-view"]').click();
cy.get('[data-cy="full-screen-modal"]').should("be.visible");
cy.get('[data-cy="circle-analyse-vertical"]').click({force: true});
cy.url().should(
"include",
"/course/versicherungsvermittler-in-alt/learn/analyse"
);
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
cy.get('[data-cy="lp-path-view"]').should("be.visible");
cy.get('[data-cy="view-switch"]').click();
cy.get('[data-cy="lp-list-view"]').should("be.visible");
cy.get('[data-cy="view-switch"]').click();
cy.get('[data-cy="lp-path-view"]').should("be.visible");
});
it("weiter gehts button will open next circle", () => {
@ -49,21 +44,25 @@ describe("learningPath page", () => {
cy.visit("/course/versicherungsvermittler-in-alt/learn");
// first click will open first circle
cy.get('[data-cy="lp-continue-button"]').should("contain", "Los geht's");
cy.get('[data-cy="lp-continue-button"]').click();
cy.get('[data-cy="lp-continue-button"]')
.filter(":visible")
.should("contain", "Los geht's")
.click();
cy.get('[data-cy="circle-title"]').should("contain", "Basis");
cy.get('[data-cy="back-to-learning-path-button"]').click();
// mark a learning content in second circle
cy.get('[data-cy="circle-analyse"]').click({force: true});
cy.get('[data-cy="circle-Analyse"]').click({ force: true });
cy.get(
'[data-cy="versicherungsvermittler-in-alt-lp-circle-analyse-lc-fachcheck-fahrzeug-checkbox"] > .cy-checkbox'
).click();
cy.get('[data-cy="back-to-learning-path-button"]').click();
// click on continue should go to unit-test-circle
cy.get('[data-cy="lp-continue-button"]').should("contain", "Weiter geht's");
cy.get('[data-cy="lp-continue-button"]').click();
cy.get('[data-cy="lp-continue-button"]')
.filter(":visible")
.should("contain", "Weiter geht's")
.click();
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
});
});