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:
parent
ef6774f260
commit
6733e7a311
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue