diff --git a/client/package-lock.json b/client/package-lock.json index 08d5fb5c..e8f9b7b7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,6 +13,7 @@ "@sentry/tracing": "^7.20.0", "@sentry/vue": "^7.20.0", "@urql/vue": "^1.0.2", + "@vueuse/core": "^9.13.0", "d3": "^7.6.1", "dayjs": "^1.11.7", "graphql": "^16.6.0", @@ -3365,6 +3366,11 @@ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", "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": { "version": "8.5.3", "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": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -14412,6 +14501,11 @@ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", "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": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -14738,6 +14832,46 @@ "dev": true, "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": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", diff --git a/client/package.json b/client/package.json index 92c91e05..33b1bf6e 100644 --- a/client/package.json +++ b/client/package.json @@ -21,6 +21,7 @@ "@sentry/tracing": "^7.20.0", "@sentry/vue": "^7.20.0", "@urql/vue": "^1.0.2", + "@vueuse/core": "^9.13.0", "d3": "^7.6.1", "dayjs": "^1.11.7", "graphql": "^16.6.0", diff --git a/client/src/components/AppFooter.vue b/client/src/components/AppFooter.vue index 4174bd72..de18ae8e 100644 --- a/client/src/components/AppFooter.vue +++ b/client/src/components/AppFooter.vue @@ -34,8 +34,8 @@ async function changeLocale(language: AvailableLanguages) { diff --git a/client/src/components/learningPath/page/LearningPathCircleColumn.vue b/client/src/components/learningPath/page/LearningPathCircleColumn.vue index f9927c38..0d101bca 100644 --- a/client/src/components/learningPath/page/LearningPathCircleColumn.vue +++ b/client/src/components/learningPath/page/LearningPathCircleColumn.vue @@ -6,6 +6,7 @@ import { calculateCircleSectorData } from "@/components/learningPath/page/utils" import type { Circle } from "@/services/circle"; import type { LearningPath } from "@/services/learningPath"; import type { Topic } from "@/types"; +import { onMounted, ref } from "vue"; const props = defineProps<{ learningPath: LearningPath | undefined; @@ -15,34 +16,54 @@ const props = defineProps<{ isLastCircle: boolean; isCurrentCircle: boolean; }>(); + +const circleElement = ref(null); + +onMounted(() => { + if (props.isCurrentCircle) { + setTimeout(() => { + circleElement?.value?.scrollIntoView({ + behavior: "smooth", + inline: "center", + block: "nearest", + }); + }, 400); + } +}); diff --git a/client/src/components/learningPath/page/LearningPathCircleListTile.vue b/client/src/components/learningPath/page/LearningPathCircleListTile.vue new file mode 100644 index 00000000..12f82377 --- /dev/null +++ b/client/src/components/learningPath/page/LearningPathCircleListTile.vue @@ -0,0 +1,51 @@ + + + diff --git a/client/src/components/learningPath/page/LearningPathListView.vue b/client/src/components/learningPath/page/LearningPathListView.vue index f37707da..b14a927e 100644 --- a/client/src/components/learningPath/page/LearningPathListView.vue +++ b/client/src/components/learningPath/page/LearningPathListView.vue @@ -1,33 +1,18 @@ diff --git a/client/src/components/learningPath/page/LearningPathPathView.vue b/client/src/components/learningPath/page/LearningPathPathView.vue index 79257442..1be9d796 100644 --- a/client/src/components/learningPath/page/LearningPathPathView.vue +++ b/client/src/components/learningPath/page/LearningPathPathView.vue @@ -4,29 +4,18 @@ import LearningPathCircleColumn from "@/components/learningPath/page/LearningPat 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"; +import { useScroll } from "@vueuse/core"; +import { 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 learnPathDiagram = ref(null); +const { x, arrivedState } = useScroll(learnPathDiagram, { behavior: "smooth" }); const isFirstCircle = (topicIndex: number, circleIndex: number) => topicIndex === 0 && circleIndex === 0; @@ -38,52 +27,26 @@ const isLastCircle = (topicIndex: number, circleIndex: number, numCircles: numbe 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" }); + x.value += offset; };