Merged in bugfix/new_learnpath_refactorings (pull request #36)

Minor refactorings in new learnpath

* Refactor media query watching into composable

* Refactor media query to use vueuse package

* Get rid of custom scroll boundary watch logic

* Use template ref instead of id

* Switch to typed learning path key

* Move to-component-scrolling into the components themselves

* Minor cleanup

* Fix minor issues


Approved-by: Daniel Egger
This commit is contained in:
Elia Bieri 2023-03-08 17:10:49 +00:00
parent ef6774f260
commit 6733e7a311
9 changed files with 274 additions and 141 deletions

134
client/package-lock.json generated
View File

@ -13,6 +13,7 @@
"@sentry/tracing": "^7.20.0", "@sentry/tracing": "^7.20.0",
"@sentry/vue": "^7.20.0", "@sentry/vue": "^7.20.0",
"@urql/vue": "^1.0.2", "@urql/vue": "^1.0.2",
"@vueuse/core": "^9.13.0",
"d3": "^7.6.1", "d3": "^7.6.1",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"graphql": "^16.6.0", "graphql": "^16.6.0",
@ -3365,6 +3366,11 @@
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
"dev": true "dev": true
}, },
"node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ=="
},
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@ -3822,6 +3828,89 @@
} }
} }
}, },
"node_modules/@vueuse/core": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"dependencies": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@webassemblyjs/ast": { "node_modules/@webassemblyjs/ast": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@ -14412,6 +14501,11 @@
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
"dev": true "dev": true
}, },
"@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ=="
},
"@types/ws": { "@types/ws": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@ -14738,6 +14832,46 @@
"dev": true, "dev": true,
"requires": {} "requires": {}
}, },
"@vueuse/core": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"requires": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
},
"dependencies": {
"vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"requires": {}
}
}
},
"@vueuse/metadata": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ=="
},
"@vueuse/shared": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"requires": {
"vue-demi": "*"
},
"dependencies": {
"vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"requires": {}
}
}
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",

View File

@ -21,6 +21,7 @@
"@sentry/tracing": "^7.20.0", "@sentry/tracing": "^7.20.0",
"@sentry/vue": "^7.20.0", "@sentry/vue": "^7.20.0",
"@urql/vue": "^1.0.2", "@urql/vue": "^1.0.2",
"@vueuse/core": "^9.13.0",
"d3": "^7.6.1", "d3": "^7.6.1",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"graphql": "^16.6.0", "graphql": "^16.6.0",

View File

@ -34,8 +34,8 @@ async function changeLocale(language: AvailableLanguages) {
<MenuItem v-for="locale in SUPPORT_LOCALES" :key="locale" class="py-1"> <MenuItem v-for="locale in SUPPORT_LOCALES" :key="locale" class="py-1">
<button <button
class="" class=""
@click="changeLocale(locale)"
:data-cy="`language-selector-${locale}`" :data-cy="`language-selector-${locale}`"
@click="changeLocale(locale)"
> >
<span class="ml-2 inline">{{ $t(`language.${locale}`) }}</span> <span class="ml-2 inline">{{ $t(`language.${locale}`) }}</span>
</button> </button>

View File

@ -6,6 +6,7 @@ import { calculateCircleSectorData } from "@/components/learningPath/page/utils"
import type { Circle } from "@/services/circle"; import type { Circle } from "@/services/circle";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import type { Topic } from "@/types"; import type { Topic } from "@/types";
import { onMounted, ref } from "vue";
const props = defineProps<{ const props = defineProps<{
learningPath: LearningPath | undefined; learningPath: LearningPath | undefined;
@ -15,34 +16,54 @@ const props = defineProps<{
isLastCircle: boolean; isLastCircle: boolean;
isCurrentCircle: boolean; isCurrentCircle: boolean;
}>(); }>();
const circleElement = ref<HTMLElement | null>(null);
onMounted(() => {
if (props.isCurrentCircle) {
setTimeout(() => {
circleElement?.value?.scrollIntoView({
behavior: "smooth",
inline: "center",
block: "nearest",
});
}, 400);
}
});
</script> </script>
<template> <template>
<div class="flex flex-row items-center pb-2"> <router-link
<div class="w-12"> :to="props.circle.frontend_url"
<hr v-if="!props.isFirstCircle" class="h-[1.6px] w-full border-0 bg-gray-500" /> :data-cy="`circle-${props.circle.title}`"
class="flex flex-col items-center pb-6"
>
<div ref="circleElement" 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> </div>
<LearningPathCircle
:sectors="calculateCircleSectorData(circle)" <span v-if="props.isCurrentCircle" class="text-sm text-gray-800">
class="h-24 w-24 snap-center" {{ $t("learningPathPage.currentCircle") }}
></LearningPathCircle> </span>
<div class="w-12">
<hr v-if="!props.isLastCircle" class="h-[1.6px] w-full border-0 bg-gray-500" /> <div class="w-36 text-center text-lg font-bold text-blue-900">
{{ props.circle.title }}
</div> </div>
</div>
<span v-if="props.isCurrentCircle" class="text-sm text-gray-800"> <div v-if="props.isCurrentCircle" class="whitespace-nowrap">
{{ $t("learningPathPage.currentCircle") }} <LearningPathContinueButton
</span> :has-progress="!props.learningPath?.continueData?.has_no_progress"
:url="props.learningPath?.continueData?.url"
<div class="w-36 text-center text-lg font-bold text-blue-900"> ></LearningPathContinueButton>
{{ props.circle.title }} </div>
</div> </router-link>
<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> </template>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
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 { onMounted, ref } from "vue";
const props = defineProps<{
learningPath: LearningPath | undefined;
circle: Circle;
isCurrentCircle: boolean;
}>();
const circleElement = ref<HTMLElement | null>(null);
onMounted(() => {
if (props.isCurrentCircle) {
setTimeout(() => {
circleElement?.value?.scrollIntoView({
behavior: "smooth",
inline: "nearest",
block: "center",
});
}, 400);
}
});
</script>
<template>
<router-link
:key="props.circle.id"
:to="props.circle.frontend_url"
class="flex flex-row items-center pb-2"
>
<LearningPathCircle
:sectors="calculateCircleSectorData(circle)"
class="h-12 w-12"
></LearningPathCircle>
<div ref="circleElement" class="pl-3 font-bold text-blue-900">
{{ props.circle.title }}
</div>
<div v-if="isCurrentCircle" class="whitespace-nowrap pl-4">
<LearningPathContinueButton
:has-progress="!props.learningPath?.continueData?.has_no_progress"
:url="props.learningPath?.continueData?.url"
></LearningPathContinueButton>
</div>
</router-link>
</template>

View File

@ -1,33 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CircleSectorData } from "@/components/learningPath/page/LearningPathCircle.vue"; import type { CircleSectorData } from "@/components/learningPath/page/LearningPathCircle.vue";
import LearningPathCircle from "@/components/learningPath/page/LearningPathCircle.vue"; import LearningPathCircleListTile from "@/components/learningPath/page/LearningPathCircleListTile.vue";
import LearningPathContinueButton from "@/components/learningPath/page/LearningPathContinueButton.vue";
import { calculateCircleSectorData } from "@/components/learningPath/page/utils";
import type { Circle } from "@/services/circle"; import type { Circle } from "@/services/circle";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import { computed, onMounted } from "vue"; import { computed } from "vue";
const props = defineProps<{ const props = defineProps<{
learningPath: LearningPath | undefined; learningPath: LearningPath | undefined;
}>(); }>();
onMounted(() => {
const initialCircle = props.learningPath?.nextLearningContent?.parentCircle;
if (initialCircle) {
scrollToCircle(initialCircle);
}
});
const topics = computed(() => props.learningPath?.topics ?? []); const topics = computed(() => props.learningPath?.topics ?? []);
const shouldContinueHere = (circle: Circle) => const isCurrentCircle = (circle: Circle) =>
props.learningPath?.nextLearningContent?.parentCircle === 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> </script>
<template> <template>
@ -35,27 +20,12 @@ const scrollToCircle = (circle: Circle) => {
<div class="pb-2 font-bold text-gray-700"> <div class="pb-2 font-bold text-gray-700">
{{ topic.title }} {{ topic.title }}
</div> </div>
<router-link <LearningPathCircleListTile
v-for="circle in topic.circles" v-for="circle in topic.circles"
:id="`circle-${circle.slug}`"
:key="circle.id" :key="circle.id"
:to="circle.frontend_url" :learning-path="learningPath"
class="flex flex-row items-center pb-2" :circle="circle"
> :is-current-circle="isCurrentCircle(circle)"
<LearningPathCircle ></LearningPathCircleListTile>
: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> </div>
</template> </template>

View File

@ -4,29 +4,18 @@ import LearningPathCircleColumn from "@/components/learningPath/page/LearningPat
import LearningPathScrollButton from "@/components/learningPath/page/LearningPathScrollButton.vue"; import LearningPathScrollButton from "@/components/learningPath/page/LearningPathScrollButton.vue";
import type { Circle } from "@/services/circle"; import type { Circle } from "@/services/circle";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import { onMounted, ref } from "vue"; import { useScroll } from "@vueuse/core";
import { ref } from "vue";
const props = defineProps<{ const props = defineProps<{
learningPath: LearningPath | undefined; learningPath: LearningPath | undefined;
useMobileLayout: boolean; useMobileLayout: boolean;
}>(); }>();
// Scroll bounds
const isAtLeftEnd = ref(true);
const isAtRightEnd = ref(false);
// Constants
const scrollIncrement = 600; const scrollIncrement = 600;
const learnPathDiagramId = "learnpath-diagram";
onMounted(async () => { const learnPathDiagram = ref<HTMLElement | null>(null);
watchScrollBounds(); const { x, arrivedState } = useScroll(learnPathDiagram, { behavior: "smooth" });
const initialCircle = props.learningPath?.nextLearningContent?.parentCircle;
if (initialCircle) {
scrollToCircle(initialCircle);
}
});
const isFirstCircle = (topicIndex: number, circleIndex: number) => const isFirstCircle = (topicIndex: number, circleIndex: number) =>
topicIndex === 0 && circleIndex === 0; topicIndex === 0 && circleIndex === 0;
@ -38,51 +27,25 @@ const isLastCircle = (topicIndex: number, circleIndex: number, numCircles: numbe
const isCurrentCircle = (circle: Circle) => const isCurrentCircle = (circle: Circle) =>
props.learningPath?.nextLearningContent?.parentCircle === 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 scrollRight = () => scrollLearnPathDiagram(scrollIncrement);
const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement); const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
const scrollLearnPathDiagram = (offset: number) => { const scrollLearnPathDiagram = (offset: number) => {
const el = document.getElementById(learnPathDiagramId); x.value += offset;
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> </script>
<template> <template>
<div class="relative flex flex-row items-center"> <div class="relative flex flex-row items-center">
<LearningPathScrollButton <LearningPathScrollButton
v-show="!isAtLeftEnd" v-show="!arrivedState.left"
direction="left" direction="left"
:hidden="props.useMobileLayout" :hidden="props.useMobileLayout"
@click="scrollLeft" @click="scrollLeft"
></LearningPathScrollButton> ></LearningPathScrollButton>
<div <div
:id="learnPathDiagramId" ref="learnPathDiagram"
class="flex h-96 snap-x flex-row overflow-auto py-5 sm:overflow-hidden sm:py-10" class="flex h-96 snap-x flex-row overflow-auto py-5 sm:overflow-hidden sm:py-10"
> >
<div <div
@ -98,30 +61,23 @@ const scrollToCircle = (circle: Circle) => {
{{ topic.title }} {{ topic.title }}
</p> </p>
<div class="flex flex-row pt-6"> <div class="flex flex-row pt-6">
<router-link <LearningPathCircleColumn
v-for="(circle, circleIndex) in topic.circles" v-for="(circle, circleIndex) in topic.circles"
:id="`circle-${circle.slug}`"
:key="circle.id" :key="circle.id"
:to="circle.frontend_url" :learning-path="learningPath"
:data-cy="`circle-${circle.title}`" :circle="circle"
class="flex flex-col items-center pb-6" :topic="topic"
> :is-first-circle="isFirstCircle(topicIndex, circleIndex)"
<LearningPathCircleColumn :is-last-circle="
:learning-path="learningPath" isLastCircle(topicIndex, circleIndex, topic.circles.length)
:circle="circle" "
:topic="topic" :is-current-circle="isCurrentCircle(circle)"
:is-first-circle="isFirstCircle(topicIndex, circleIndex)" ></LearningPathCircleColumn>
:is-last-circle="
isLastCircle(topicIndex, circleIndex, topic.circles.length)
"
:is-current-circle="isCurrentCircle(circle)"
></LearningPathCircleColumn>
</router-link>
</div> </div>
</div> </div>
</div> </div>
<LearningPathScrollButton <LearningPathScrollButton
v-show="!isAtRightEnd" v-show="!arrivedState.right"
direction="right" direction="right"
:hidden="props.useMobileLayout" :hidden="props.useMobileLayout"
@click="scrollRight" @click="scrollRight"

View File

@ -10,17 +10,19 @@ import type { ViewType } from "@/components/learningPath/page/LearningPathViewSw
import LearningPathViewSwitch from "@/components/learningPath/page/LearningPathViewSwitch.vue"; import LearningPathViewSwitch from "@/components/learningPath/page/LearningPathViewSwitch.vue";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
}>(); }>();
const breakpoints = useBreakpoints(breakpointsTailwind);
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();
const userStore = useUserStore(); const userStore = useUserStore();
// Layout state // Layout state
const useMobileLayout = ref(false); const useMobileLayout = breakpoints.smaller("sm");
const selectedView = ref<ViewType>( const selectedView = ref<ViewType>(
(window.localStorage.getItem("learningPathView") as ViewType) || "path" (window.localStorage.getItem("learningPathView") as ViewType) || "path"
); );
@ -33,9 +35,6 @@ onMounted(async () => {
} catch (error) { } catch (error) {
log.error(error); log.error(error);
} }
// Detect if we're on a mobile layout
watchLayoutChanges();
}); });
const learningPath = computed(() => { const learningPath = computed(() => {
@ -65,14 +64,6 @@ const inProgressCirclesCount = computed(() => {
return 0; 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) => { const changeViewType = (viewType: ViewType) => {
selectedView.value = viewType; selectedView.value = viewType;
window.localStorage.setItem("learningPathView", viewType); window.localStorage.setItem("learningPathView", viewType);

View File

@ -11,20 +11,29 @@ export type LearningPathStoreState = {
page: "INDEX" | "OVERVIEW"; page: "INDEX" | "OVERVIEW";
}; };
type LearningPathKey = string;
function getLearningPathKey(
slug: string,
userId: string | number | undefined
): LearningPathKey {
return `${slug}-${userId}`;
}
export const useLearningPathStore = defineStore({ export const useLearningPathStore = defineStore({
id: "learningPath", id: "learningPath",
state: () => { state: () => {
return { return {
learningPaths: new Map<string, LearningPath>(), learningPaths: new Map<LearningPathKey, LearningPath>(),
page: "INDEX", page: "INDEX",
loading: false, loading: false,
} as LearningPathStoreState; } as LearningPathStoreState;
}, },
getters: { getters: {
learningPathForUser: (state) => { learningPathForUser: (state) => {
return (courseSlug: string, userId: number | string) => { return (courseSlug: string, userId: string | number | undefined) => {
if (state.learningPaths.size > 0) { if (state.learningPaths.size > 0) {
const learningPathKey = `${courseSlug}-lp-${userId}`; const learningPathKey = getLearningPathKey(courseSlug, userId);
return state.learningPaths.get(learningPathKey); return state.learningPaths.get(learningPathKey);
} }
@ -44,7 +53,7 @@ export const useLearningPathStore = defineStore({
userId = userStore.id; userId = userStore.id;
} }
const key = `${slug}-${userId}`; const key = getLearningPathKey(slug, userId);
if (this.learningPaths.has(key) && !reload) { if (this.learningPaths.has(key) && !reload) {
return this.learningPaths.get(key); return this.learningPaths.get(key);