Add code formatting with prettier and black

This commit is contained in:
Daniel Egger 2022-09-30 17:43:06 +02:00
parent 38c8d2120a
commit 827e7a0fc0
140 changed files with 4588 additions and 3050 deletions

3
.gitignore vendored
View File

@ -283,6 +283,9 @@ cypress/videos
cypress/screenshots cypress/screenshots
cypress/test-reports cypress/test-reports
git-crypt-encrypted-files-check.txt
/server/vbv_lernwelt/static/css/tailwind.css /server/vbv_lernwelt/static/css/tailwind.css
/server/vbv_lernwelt/static/vue/ /server/vbv_lernwelt/static/vue/
/server/vbv_lernwelt/templates/vue/index.html /server/vbv_lernwelt/templates/vue/index.html

View File

@ -1,6 +1,8 @@
# VBV Lernwelt # VBV Lernwelt
Project setup is based on [cookiecutter-django](https://github.com/cookiecutter/cookiecutter-django) project template. Project setup is based
on [cookiecutter-django](https://github.com/cookiecutter/cookiecutter-django) project
template.
## Run for development ## Run for development
@ -28,7 +30,6 @@ export IT_APP_ENVIRONMENT=development
See `.env_secrets/local_daniel.env` for more possible environment variables. See `.env_secrets/local_daniel.env` for more possible environment variables.
Especially set correct values for `POSTGRES_*` and `DATABASE_URL` Especially set correct values for `POSTGRES_*` and `DATABASE_URL`
### Server part ### Server part
Install python dependencies: Install python dependencies:
@ -37,7 +38,8 @@ Install python dependencies:
pip install -r server/requirements/requirements-dev.txt pip install -r server/requirements/requirements-dev.txt
``` ```
The "prepare_server.sh" script will create the database according to `POSTGRES_*` environment variables. The "prepare_server.sh" script will create the database according to `POSTGRES_*`
environment variables.
It will also setup the tables for django and run the django development server. It will also setup the tables for django and run the django development server.
```bash ```bash
@ -61,13 +63,32 @@ npm run dev
### General part ### General part
Cypress is installed for client and server, so there is this package.json on the project root directory Cypress is installed for client and server, so there is this package.json on the project
root directory
```bash ```bash
# in project root directory # in project root directory
npm install npm install
``` ```
### Git hooks
```bash
# install git hooks
ln -s ../../git-pre-commit.sh .git/hooks/pre-commit
ln -s ../../git-pre-push.sh .git/hooks/pre-push
```
### Actions on Save
You can enable some useful "Actions on Save" in your JetBrains IDE:
Preferences -> Tools -> Actions on Save
* Reformat Code
* Optimize Imports
* Run eslint --fix
* Run prettier
## Deployment to CapRover ## Deployment to CapRover

View File

@ -6,6 +6,19 @@ pipelines:
- step: - step:
name: python tests name: python tests
max-time: 15 max-time: 15
services:
- postgres
caches:
- vbvpip
script:
- source ./env/bitbucket/prepare_for_test.sh
- python -m venv vbvvenv
- source vbvvenv/bin/activate
- pip install -r server/requirements/requirements-dev.txt
- ./server/run_tests_coverage.sh
- step:
name: python linting
max-time: 15
services: services:
- postgres - postgres
caches: caches:
@ -17,8 +30,7 @@ pipelines:
- pip install -r server/requirements/requirements-dev.txt - pip install -r server/requirements/requirements-dev.txt
- git-crypt status -e | sort > git-crypt-encrypted-files-check.txt && diff git-crypt-encrypted-files.txt git-crypt-encrypted-files-check.txt - git-crypt status -e | sort > git-crypt-encrypted-files-check.txt && diff git-crypt-encrypted-files.txt git-crypt-encrypted-files-check.txt
- trufflehog --exclude_paths trufflehog-exclude-patterns.txt --allow trufflehog-allow.json --entropy=True --max_depth=100 . - trufflehog --exclude_paths trufflehog-exclude-patterns.txt --allow trufflehog-allow.json --entropy=True --max_depth=100 .
- ./server/run_tests_coverage.sh - ufmt check server
# - ./src/run_pylint.sh
- step: - step:
name: js tests name: js tests
max-time: 15 max-time: 15
@ -30,6 +42,18 @@ pipelines:
- pwd - pwd
- npm install - npm install
- npm test - npm test
- step:
name: js linting
max-time: 15
caches:
- node
- clientnode
script:
- cd client
- pwd
- npm install
- npm run prettier:check
- npm run lint
- step: - step:
name: cypress tests name: cypress tests
max-time: 45 max-time: 45

View File

@ -1,18 +1,21 @@
/* eslint-env node */ /* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution'); require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = { module.exports = {
'root': true, root: true,
'extends': [ extends: [
'plugin:vue/vue3-essential', "plugin:vue/vue3-recommended",
'eslint:recommended', "eslint:recommended",
'@vue/eslint-config-typescript/recommended', "@vue/eslint-config-typescript/recommended",
// "@vue/eslint-config-prettier" "@vue/eslint-config-prettier",
], ],
'env': { env: {
'vue/setup-compiler-macros': true "vue/setup-compiler-macros": true,
}, },
'rules': { ignorePatterns: ["versionize.js", "tailwind.config.js", "postcss.config.js"],
'@typescript-eslint/no-unused-vars': ['warn'], rules: {
} "@typescript-eslint/no-unused-vars": ["warn"],
} "@typescript-eslint/ban-ts-comment": ["warn"],
"prefer-const": ["warn"],
},
};

2
client/.prettierignore Normal file
View File

@ -0,0 +1,2 @@
dist
node_modules

View File

@ -1,6 +1,7 @@
{ {
"semi": false, "semi": true,
"singleQuote": true, "singleQuote": false,
"tabWidth": 2, "tabWidth": 2,
"printWidth": 120 "printWidth": 88,
"organizeImportsSkipDestructiveCodeActions": true
} }

View File

@ -1,3 +1,6 @@
{ {
"recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"] "recommendations": [
"johnsoncodehk.volar",
"johnsoncodehk.vscode-typescript-vue-plugin"
]
} }

View File

@ -11,4 +11,5 @@ VBV Frontend
`npm run dev` `npm run dev`
## Vue layouts ## Vue layouts
[How layouts are implemented](https://itnext.io/vue-tricks-smart-layouts-for-vuejs-5c61a472b69b) [How layouts are implemented](https://itnext.io/vue-tricks-smart-layouts-for-vuejs-5c61a472b69b)

View File

@ -6,7 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- workaround for vitejs bundling -> reference https:// --> <!-- workaround for vitejs bundling -> reference https:// -->
<link href="https://vbv-lernwelt.control.iterativ.ch/static/fonts/BuenosAires/stylesheet.css" rel="stylesheet"> <link
href="https://vbv-lernwelt.control.iterativ.ch/static/fonts/BuenosAires/stylesheet.css"
rel="stylesheet"
/>
<script defer src="/server/core/icons/"></script> <script defer src="/server/core/icons/"></script>
<!-- end workaround --> <!-- end workaround -->

View File

@ -9,6 +9,8 @@
"coverage": "vitest run --coverage", "coverage": "vitest run --coverage",
"typecheck": "vue-tsc --noEmit -p tsconfig.app.json --composite false", "typecheck": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"prettier": "prettier . --write",
"prettier:check": "prettier . --check",
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch" "tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch"
}, },
"dependencies": { "dependencies": {
@ -33,17 +35,20 @@
"@types/lodash": "^4.14.184", "@types/lodash": "^4.14.184",
"@types/node": "^18.7.14", "@types/node": "^18.7.14",
"@vitejs/plugin-vue": "^3.0.3", "@vitejs/plugin-vue": "^3.0.3",
"@volar/vue-typescript": "^0.40.13",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0", "@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.0.2", "@vue/test-utils": "^2.0.2",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.1.3",
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"eslint": "8.22.0", "eslint": "8.22.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-vue": "^9.4.0", "eslint-plugin-vue": "^9.4.0",
"jsdom": "^20.0.0", "jsdom": "^20.0.0",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"postcss-import": "^14.1.0", "postcss-import": "^14.1.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prettier-plugin-organize-imports": "^3.1.1",
"replace-in-file": "^6.3.5", "replace-in-file": "^6.3.5",
"sass": "^1.54.6", "sass": "^1.54.6",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",

View File

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@ -1,28 +1,26 @@
<template> <template>
<div id="app" class="flex flex-col min-h-screen"> <div id="app" class="flex flex-col min-h-screen">
<MainNavigationBar class="flex-none" /> <MainNavigationBar class="flex-none" />
<RouterView class="flex-auto" v-slot="{ Component }"> <RouterView v-slot="{ Component }" class="flex-auto">
<Transition mode="out-in" name="app"> <Transition mode="out-in" name="app">
<component :is="Component"></component> <component :is="Component"></component>
</Transition> </Transition>
</RouterView> </RouterView>
<Footer class="flex-none" /> <AppFooter class="flex-none" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel'; import * as log from "loglevel";
import MainNavigationBar from '@/components/MainNavigationBar.vue'; import MainNavigationBar from "@/components/MainNavigationBar.vue";
import Footer from '@/components/Footer.vue'; import { onMounted } from "vue";
import {onMounted} from 'vue';
log.debug('App created'); log.debug("App created");
onMounted(() => { onMounted(() => {
log.debug('App mounted'); log.debug("App mounted");
}); });
</script> </script>
<style lang="postcss" scoped> <style lang="postcss" scoped>
@ -35,5 +33,4 @@ onMounted(() => {
.app-leave-to { .app-leave-to {
opacity: 0; opacity: 0;
} }
</style> </style>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import * as log from "loglevel";
log.debug('Footer created') log.debug("AppFooter created");
</script> </script>
<template> <template>

View File

@ -1,91 +1,93 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import * as log from "loglevel";
import { onMounted, reactive } from 'vue' import IconLogout from "@/components/icons/IconLogout.vue";
import { useUserStore } from '@/stores/user' import IconSettings from "@/components/icons/IconSettings.vue";
import { useLearningPathStore } from '@/stores/learningPath' import MobileMenu from "@/components/MobileMenu.vue";
import { useRoute, useRouter } from 'vue-router' import ItDropdown from "@/components/ui/ItDropdown.vue";
import { useAppStore } from '@/stores/app' import { useAppStore } from "@/stores/app";
import IconLogout from '@/components/icons/IconLogout.vue' import { useLearningPathStore } from "@/stores/learningPath";
import IconSettings from '@/components/icons/IconSettings.vue' import { useUserStore } from "@/stores/user";
import ItDropdown from '@/components/ui/ItDropdown.vue' import { onMounted, reactive } from "vue";
import MobileMenu from '@/components/MobileMenu.vue' import { useRoute, useRouter } from "vue-router";
log.debug('MainNavigationBar created') log.debug("MainNavigationBar created");
const route = useRoute() const route = useRoute();
const router = useRouter() const router = useRouter();
const userStore = useUserStore() const userStore = useUserStore();
const appStore = useAppStore() const appStore = useAppStore();
const learningPathStore = useLearningPathStore() const learningPathStore = useLearningPathStore();
const state = reactive({ showMenu: false }) const state = reactive({ showMenu: false });
function toggleNav() { function toggleNav() {
state.showMenu = !state.showMenu state.showMenu = !state.showMenu;
} }
function isInRoutePath(checkPaths: string[]) { function isInRoutePath(checkPaths: string[]) {
return checkPaths.some((path) => route.path.startsWith(path)) return checkPaths.some((path) => route.path.startsWith(path));
} }
function inLearningPath() { function inLearningPath() {
return isInRoutePath(['/learn/']) return isInRoutePath(["/learn/"]);
} }
function getLearningPathStringProp(prop: 'title' | 'slug'): string { function getLearningPathStringProp(prop: "title" | "slug"): string {
return inLearningPath() && learningPathStore.learningPath ? learningPathStore.learningPath[prop] : '' return inLearningPath() && learningPathStore.learningPath
? learningPathStore.learningPath[prop]
: "";
} }
function learningPathName(): string { function learningPathName(): string {
return getLearningPathStringProp('title') return getLearningPathStringProp("title");
} }
function learninPathSlug(): string { function learninPathSlug(): string {
return getLearningPathStringProp('slug') return getLearningPathStringProp("slug");
} }
function handleDropdownSelect(data) { function handleDropdownSelect(data) {
log.debug('Selected action:', data.action) log.debug("Selected action:", data.action);
switch (data.action) { switch (data.action) {
case 'settings': case "settings":
router.push('/profile') router.push("/profile");
break break;
case 'logout': case "logout":
userStore.handleLogout() userStore.handleLogout();
break break;
default: default:
console.log('no action') console.log("no action");
} }
} }
function logout() { function logout() {
userStore.handleLogout() userStore.handleLogout();
} }
onMounted(() => { onMounted(() => {
log.debug('MainNavigationBar mounted') log.debug("MainNavigationBar mounted");
}) });
const profileDropdownData = [ const profileDropdownData = [
[ [
{ {
title: 'Kontoeinstellungen', title: "Kontoeinstellungen",
icon: IconSettings, icon: IconSettings,
data: { data: {
action: 'settings', action: "settings",
}, },
}, },
], ],
[ [
{ {
title: 'Abmelden', title: "Abmelden",
icon: IconLogout, icon: IconLogout,
data: { data: {
action: 'logout', action: "logout",
}, },
}, },
], ],
] ];
</script> </script>
<template> <template>
@ -108,7 +110,9 @@ const profileDropdownData = [
<it-icon-vbv class="h-8 w-16 mr-3 -mt-6 -ml-3" /> <it-icon-vbv class="h-8 w-16 mr-3 -mt-6 -ml-3" />
</a> </a>
<router-link to="/" class="flex"> <router-link to="/" class="flex">
<div class="text-white text-2xl pr-10 pl-3 ml-1 border-l border-white">myVBV</div> <div class="text-white text-2xl pr-10 pl-3 ml-1 border-l border-white">
myVBV
</div>
</router-link> </router-link>
</div> </div>
@ -122,7 +126,7 @@ const profileDropdownData = [
<it-icon-message class="w-8 h-8 mr-6" /> <it-icon-message class="w-8 h-8 mr-6" />
</router-link> </router-link>
<!-- Mobile menu button --> <!-- Mobile menu button -->
<div @click="toggleNav" class="flex"> <div class="flex" @click="toggleNav">
<button <button
type="button" type="button"
class="w-8 h-8 text-white hover:text-sky-500 focus:outline-none focus:text-sky-500" class="w-8 h-8 text-white hover:text-sky-500 focus:outline-none focus:text-sky-500"
@ -158,7 +162,11 @@ const profileDropdownData = [
</router-link> </router-link>
<div class="hidden lg:block flex-auto"></div> <div class="hidden lg:block flex-auto"></div>
<router-link to="/shop" class="nav-item" :class="{ 'nav-item--active': isInRoutePath(['/shop']) }"> <router-link
to="/shop"
class="nav-item"
:class="{ 'nav-item--active': isInRoutePath(['/shop']) }"
>
Shop Shop
</router-link> </router-link>
<router-link <router-link
@ -168,10 +176,14 @@ const profileDropdownData = [
> >
Mediathek Mediathek
</router-link> </router-link>
<router-link to="/messages" class="nav-item flex flex-row items-center" data-cy="messages-link"> <router-link
to="/messages"
class="nav-item flex flex-row items-center"
data-cy="messages-link"
>
<it-icon-message class="w-8 h-8 mr-6" /> <it-icon-message class="w-8 h-8 mr-6" />
</router-link> </router-link>
<div class="nav-item flex items-center" v-if="userStore.loggedIn"> <div v-if="userStore.loggedIn" class="nav-item flex items-center">
<ItDropdown <ItDropdown
:button-classes="[]" :button-classes="[]"
:list-items="profileDropdownData" :list-items="profileDropdownData"
@ -179,7 +191,11 @@ const profileDropdownData = [
@select="handleDropdownSelect" @select="handleDropdownSelect"
> >
<div v-if="userStore.avatar_url"> <div v-if="userStore.avatar_url">
<img class="inline-block h-8 w-8 rounded-full" :src="userStore.avatar_url" alt="" /> <img
class="inline-block h-8 w-8 rounded-full"
:src="userStore.avatar_url"
alt=""
/>
</div> </div>
<div v-else> <div v-else>
{{ userStore.getFullName }} {{ userStore.getFullName }}

View File

@ -1,80 +1,80 @@
<script setup lang="ts"> <script setup lang="ts">
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue' import IconLogout from "@/components/icons/IconLogout.vue";
import IconLogout from '@/components/icons/IconLogout.vue' import IconSettings from "@/components/icons/IconSettings.vue";
import IconSettings from '@/components/icons/IconSettings.vue' import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import {useRouter} from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter() const router = useRouter();
const props = defineProps<{ const props = defineProps<{
show: boolean, show: boolean;
userStore: object, userStore: object;
learningPathName: string, learningPathName: string;
learningPathSlug: string learningPathSlug: string;
}>() }>();
const emits = defineEmits(['closemodal']) const emits = defineEmits(["closemodal"]);
const clickLink = (to: string) => { const clickLink = (to: string) => {
router.push(to) router.push(to);
emits('closemodal') emits("closemodal");
} };
</script> </script>
<template> <template>
<ItFullScreenModal <ItFullScreenModal :show="show" @closemodal="$emit('closemodal')">
:show="show"
@closemodal="$emit('closemodal')"
>
<div>
<div> <div>
<div
v-if="userStore.loggedIn"
class="border-b border-gray-500 -mx-4 px-8 pb-4">
<div class="-ml-4 flex">
<div
v-if="userStore.avatar_url"
>
<img class="inline-block h-16 w-16 rounded-full"
:src="userStore.avatar_url"
alt=""/>
</div>
<div class="ml-6">
<h3>{{userStore.first_name}} {{userStore.last_name}}</h3>
<button
@click="clickLink('/profile')"
class="mt-2 inline-block flex items-center">
<IconSettings class="inline-block" /><span class="ml-3">Kontoeinstellungen</span>
</button>
</div>
</div>
</div>
<div> <div>
<div <div v-if="userStore.loggedIn" class="border-b border-gray-500 -mx-4 px-8 pb-4">
class="mt-6 pb-6 border-b border-gray-500" <div class="-ml-4 flex">
v-if="learningPathName"> <div v-if="userStore.avatar_url">
<h4 class="text-gray-900 text-sm">Kurs: {{learningPathName}}</h4> <img
<ul class="mt-6"> class="inline-block h-16 w-16 rounded-full"
<li><button @click="clickLink(`/learningpath/${learningPathSlug}`)">Lernpfad</button></li> :src="userStore.avatar_url"
<li class="mt-6">Kompetenzprofil</li> alt=""
</ul> />
</div>
<div class="ml-6">
<h3>{{ userStore.first_name }} {{ userStore.last_name }}</h3>
<button
class="mt-2 inline-block flex items-center"
@click="clickLink('/profile')"
>
<IconSettings class="inline-block" /><span class="ml-3"
>Kontoeinstellungen</span
>
</button>
</div>
</div>
</div> </div>
<div class="mt-6 pb-6 border-b border-gray-500"> <div>
<ul> <div v-if="learningPathName" class="mt-6 pb-6 border-b border-gray-500">
<li>Shop</li> <h4 class="text-gray-900 text-sm">Kurs: {{ learningPathName }}</h4>
<li class="mt-6">Mediathek</li> <ul class="mt-6">
</ul> <li>
<button @click="clickLink(`/learningpath/${learningPathSlug}`)">
Lernpfad
</button>
</li>
<li class="mt-6">Kompetenzprofil</li>
</ul>
</div>
<div class="mt-6 pb-6 border-b border-gray-500">
<ul>
<li>Shop</li>
<li class="mt-6">Mediathek</li>
</ul>
</div>
<button
v-if="userStore.loggedIn"
type="button"
class="mt-6 items-center flex"
@click="userStore.handleLogout()"
>
<IconLogout class="inline-block" /><span class="ml-1">Abmelden</span>
</button>
</div> </div>
<button
v-if="userStore.loggedIn"
type="button"
@click="userStore.handleLogout()"
class="mt-6 items-center flex">
<IconLogout class="inline-block" /><span class="ml-1">Abmelden</span>
</button>
</div> </div>
</div> </div>
</div>
</ItFullScreenModal> </ItFullScreenModal>
</template> </template>

View File

@ -1,18 +1,17 @@
import { describe, it } from 'vitest' import { createPinia, setActivePinia } from "pinia";
import MainNavigationBar from '../MainNavigationBar.vue' import { describe, it } from "vitest";
import { createPinia, setActivePinia } from 'pinia'
describe('MainNavigationBar', () => { describe("MainNavigationBar", () => {
beforeEach(() => { beforeEach(() => {
// creates a fresh pinia and make it active so it's automatically picked // creates a fresh pinia and make it active so it's automatically picked
// up by any useStore() call without having to pass it to it: // up by any useStore() call without having to pass it to it:
// `useStore(pinia)` // `useStore(pinia)`
setActivePinia(createPinia()) setActivePinia(createPinia());
}) });
it('renders properly', () => { it("renders properly", () => {
expect(42).toBe(42) expect(42).toBe(42);
// const wrapper = mount(MainNavigationBar, {}) // const wrapper = mount(MainNavigationBar, {})
// expect(wrapper.text()).toContain('myVBV') // expect(wrapper.text()).toContain('myVBV')
}) });
}) });

View File

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCircleStore } from "@/stores/circle";
import * as d3 from "d3"; import * as d3 from "d3";
import {computed, onMounted} from "vue"; import * as _ from "lodash";
import * as _ from 'lodash' import * as log from "loglevel";
import {useCircleStore} from '@/stores/circle'; import { computed, onMounted } from "vue";
import * as log from 'loglevel';
import colors from "@/colors.json"; import colors from "@/colors.json";
@ -11,65 +11,71 @@ const circleStore = useCircleStore();
function someFinished(learningSequence) { function someFinished(learningSequence) {
if (circleStore.circle) { if (circleStore.circle) {
return circleStore.circle.someFinishedInLearningSequence(learningSequence.translation_key); return circleStore.circle.someFinishedInLearningSequence(
learningSequence.translation_key
);
} }
return false; return false;
} }
function allFinished(learningSequence) { function allFinished(learningSequence) {
if (circleStore.circle) { if (circleStore.circle) {
return circleStore.circle.allFinishedInLearningSequence(learningSequence.translation_key); return circleStore.circle.allFinishedInLearningSequence(
learningSequence.translation_key
);
} }
return false; return false;
} }
onMounted(async () => { onMounted(async () => {
log.debug('CircleDiagram mounted'); log.debug("CircleDiagram mounted");
render(); render();
}); });
const pieData = computed(() => { const pieData = computed(() => {
const circle = circleStore.circle const circle = circleStore.circle;
console.log('initial of compute pie data ', circle) console.log("initial of compute pie data ", circle);
if (circle) { if (circle) {
console.log('initial of compute pie data ', circle) console.log("initial of compute pie data ", circle);
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1) const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1);
const pieGenerator = d3.pie() const pieGenerator = d3.pie();
let angles = pieGenerator(pieWeights) let angles = pieGenerator(pieWeights);
_.forEach(angles, (pie) => { _.forEach(angles, (pie) => {
const thisLearningSequence = circle.learningSequences[parseInt(pie.index)] const thisLearningSequence = circle.learningSequences[parseInt(pie.index)];
pie.title = thisLearningSequence.title pie.title = thisLearningSequence.title;
pie.icon = thisLearningSequence.icon pie.icon = thisLearningSequence.icon;
pie.startAngle = pie.startAngle + Math.PI pie.startAngle = pie.startAngle + Math.PI;
pie.endAngle = pie.endAngle + Math.PI pie.endAngle = pie.endAngle + Math.PI;
pie.arrowStartAngle = pie.endAngle + (pie.startAngle - pie.endAngle) / 2 pie.arrowStartAngle = pie.endAngle + (pie.startAngle - pie.endAngle) / 2;
pie.arrowEndAngle = pie.startAngle + (pie.startAngle - pie.endAngle) / 2 pie.arrowEndAngle = pie.startAngle + (pie.startAngle - pie.endAngle) / 2;
pie.translation_key = thisLearningSequence.translation_key pie.translation_key = thisLearningSequence.translation_key;
pie.slug = thisLearningSequence.slug pie.slug = thisLearningSequence.slug;
pie.someFinished = someFinished(thisLearningSequence) pie.someFinished = someFinished(thisLearningSequence);
pie.allFinished = allFinished(thisLearningSequence) pie.allFinished = allFinished(thisLearningSequence);
}) });
angles = angles.reverse() angles = angles.reverse();
return angles return angles;
} }
return {} return {};
}) });
const width = 450 const width = 450;
const height = 450 const height = 450;
const radius = Math.min(width, height) / 2.4 const radius = Math.min(width, height) / 2.4;
function render() { function render() {
const arrowStrokeWidth = 2 const arrowStrokeWidth = 2;
const svg = d3.select('.circle-visualization') const svg = d3
.attr('viewBox', `0 0 ${width} ${height}`) .select(".circle-visualization")
.attr("viewBox", `0 0 ${width} ${height}`);
// Append markter as definition to the svg // Append markter as definition to the svg
svg.append("svg:defs").append("svg:marker") svg
.append("svg:defs")
.append("svg:marker")
.attr("id", "triangle") .attr("id", "triangle")
.attr("refX", 11) .attr("refX", 11)
.attr("refY", 11) .attr("refY", 11)
@ -79,12 +85,13 @@ function render() {
.attr("orient", "auto") .attr("orient", "auto")
.append("path") .append("path")
.attr("d", "M -1 0 l 10 0 M 0 -1 l 0 10") .attr("d", "M -1 0 l 10 0 M 0 -1 l 0 10")
.attr('transform', 'rotate(-90, 10, 0)') .attr("transform", "rotate(-90, 10, 0)")
.attr('stroke-width', arrowStrokeWidth) .attr("stroke-width", arrowStrokeWidth)
.attr('stroke', colors.gray[500]) .attr("stroke", colors.gray[500]);
const g = svg.append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
const g = svg
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
function getColor(d) { function getColor(d) {
let color = colors.gray[300]; let color = colors.gray[300];
@ -94,7 +101,7 @@ function render() {
if (d.allFinished) { if (d.allFinished) {
color = colors.green[500]; color = colors.green[500];
} }
return color return color;
} }
function getHoverColor(d) { function getHoverColor(d) {
@ -105,7 +112,7 @@ function render() {
if (d.allFinished) { if (d.allFinished) {
color = colors.green[400]; color = colors.green[400];
} }
return color return color;
} }
// Generate the pie diagram wede // Generate the pie diagram wede
@ -113,84 +120,80 @@ function render() {
.arc() .arc()
.innerRadius(radius / 2.5) .innerRadius(radius / 2.5)
.padAngle(12 / 360) .padAngle(12 / 360)
.outerRadius(radius) .outerRadius(radius);
// Generate the arrows // Generate the arrows
const arrowRadius = radius * 1.1 const arrowRadius = radius * 1.1;
const learningSequences = g.selectAll('.learningSegmentArc').data(pieData.value).enter().append('g')
.attr('class', 'learningSegmentArc')
.attr('role', 'button')
.attr('fill', colors.gray[300])
const learningSequences = g
.selectAll(".learningSegmentArc")
.data(pieData.value)
.enter()
.append("g")
.attr("class", "learningSegmentArc")
.attr("role", "button")
.attr("fill", colors.gray[300]);
learningSequences learningSequences
.on('mouseover', function (d, i) { .on("mouseover", function (d, i) {
d3.select(this) d3.select(this)
.transition() .transition()
.duration('200') .duration("200")
.attr('fill', (d) => { .attr("fill", (d) => {
return getHoverColor(d) return getHoverColor(d);
}) });
}) })
.on('mouseout', function (d, i) { .on("mouseout", function (d, i) {
d3.select(this) d3.select(this)
.transition() .transition()
.duration('200') .duration("200")
.attr('fill', (d) => { .attr("fill", (d) => {
return getColor(d) return getColor(d);
}) });
}) })
.on('click', function (d, elm) { .on("click", function (d, elm) {
console.log('clicked on ', d, elm) console.log("clicked on ", d, elm);
document.getElementById(elm.slug)?.scrollIntoView({behavior: 'smooth'}) document.getElementById(elm.slug)?.scrollIntoView({ behavior: "smooth" });
}) });
learningSequences learningSequences
.transition() .transition()
.duration(1) .duration(1)
.attr('fill', (d) => { .attr("fill", (d) => {
return getColor(d) return getColor(d);
}) });
learningSequences.append('path').attr('d', wedgeGenerator)
learningSequences.append("path").attr("d", wedgeGenerator);
const learningSequenceText = learningSequences const learningSequenceText = learningSequences
.append('text') .append("text")
.attr('fill', colors.blue[900]) .attr("fill", colors.blue[900])
.style('font-size', '15px') .style("font-size", "15px")
.text((d) => { .text((d) => {
return d.title return d.title;
}) })
.attr("transform", function (d) { .attr("transform", function (d) {
let translate = wedgeGenerator.centroid(d) let translate = wedgeGenerator.centroid(d);
translate = [translate[0], translate[1] + 20] translate = [translate[0], translate[1] + 20];
return "translate(" + translate + ")"; return "translate(" + translate + ")";
}) })
.attr('class', 'circlesText text-xl font-bold') .attr("class", "circlesText text-xl font-bold")
.style('text-anchor', 'middle') .style("text-anchor", "middle");
const iconWidth = 25 const iconWidth = 25;
const learningSequenceIcon = learningSequences.append("svg:image") const learningSequenceIcon = learningSequences
.append("svg:image")
.attr("xlink:href", (d) => { .attr("xlink:href", (d) => {
return "/static/icons/" + d.icon.replace("it-", "") + ".svg" return "/static/icons/" + d.icon.replace("it-", "") + ".svg";
}) })
.attr("width", iconWidth) .attr("width", iconWidth)
.attr("height", iconWidth) .attr("height", iconWidth)
.attr("transform", function (d) { .attr("transform", function (d) {
let translate = wedgeGenerator.centroid(d) let translate = wedgeGenerator.centroid(d);
translate = [translate[0] - iconWidth / 2, translate[1] - iconWidth] translate = [translate[0] - iconWidth / 2, translate[1] - iconWidth];
return "translate(" + translate + ")"; return "translate(" + translate + ")";
}) });
// Create Arrows // Create Arrows
const arrow = d3 const arrow = d3
@ -198,20 +201,19 @@ function render() {
.innerRadius(arrowRadius) .innerRadius(arrowRadius)
.outerRadius(arrowRadius + arrowStrokeWidth) .outerRadius(arrowRadius + arrowStrokeWidth)
.padAngle(20 / 360) .padAngle(20 / 360)
.startAngle(d => { .startAngle((d) => {
return d.arrowStartAngle return d.arrowStartAngle;
}) })
.endAngle(d => { .endAngle((d) => {
return d.arrowEndAngle return d.arrowEndAngle;
}) });
const arrows = g const arrows = g
.selectAll('.arrow') .selectAll(".arrow")
.data(pieData.value) .data(pieData.value)
.join('g') .join("g")
.attr('class', 'arrow') .attr("class", "arrow")
.attr('marker-end', 'url(#triangle)') .attr("marker-end", "url(#triangle)");
// remove last arrow // remove last arrow
d3.selection.prototype.last = function () { d3.selection.prototype.last = function () {
@ -219,24 +221,34 @@ function render() {
return d3.select(this.nodes()[last]); return d3.select(this.nodes()[last]);
}; };
const all_arows = g.selectAll('.arrow') const all_arows = g.selectAll(".arrow");
all_arows.last().remove() all_arows.last().remove();
//Draw arrow paths //Draw arrow paths
arrows.append('path').attr('fill', colors.gray[500]).attr('d', arrow) arrows.append("path").attr("fill", colors.gray[500]).attr("d", arrow);
return svg return svg;
} }
</script> </script>
<template> <template>
<div class="svg-container h-full content-center"> <div class="svg-container h-full content-center">
<pre hidden>{{ pieData }}</pre> <pre hidden>{{ pieData }}</pre>
<pre hidden>{{ render() }}</pre> <pre hidden>{{ render() }}</pre>
<svg class="circle-visualization h-full"> <svg class="circle-visualization h-full">
<circle v-if="!circleStore.circle" :cx="width / 2" :cy="height / 2" :r="radius" :color="colors.gray[300]"/> <circle
<circle v-if="!circleStore.circle" :cx="width / 2" :cy="height / 2" :r="radius / 2.5" color="white"/> v-if="!circleStore.circle"
:cx="width / 2"
:cy="height / 2"
:r="radius"
:color="colors.gray[300]"
/>
<circle
v-if="!circleStore.circle"
:cx="width / 2"
:cy="height / 2"
:r="radius / 2.5"
color="white"
/>
</svg> </svg>
</div> </div>
</template> </template>

View File

@ -1,35 +1,44 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Circle } from '@/services/circle' import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue' import type { Circle } from "@/services/circle";
const props = defineProps<{ const props = defineProps<{
circle: Circle | undefined circle: Circle | undefined;
show: boolean show: boolean;
}>() }>();
// const emits = defineEmits(['closemodal']) // const emits = defineEmits(['closemodal'])
</script> </script>
<template> <template>
<ItFullScreenModal :show="show" @closemodal="$emit('closemodal')"> <ItFullScreenModal :show="show" @closemodal="$emit('closemodal')">
<div class="container-medium" v-if="circle"> <div v-if="circle" class="container-medium">
<h1 class="">Überblick: Circle "{{ circle.title }}"</h1> <h1 class="">Überblick: Circle "{{ circle.title }}"</h1>
<p class="mt-8 text-xl">Hier zeigen wir dir, was du in diesem Circle lernen wirst.</p> <p class="mt-8 text-xl">
Hier zeigen wir dir, was du in diesem Circle lernen wirst.
</p>
<div class="mt-8 p-4 border border-gray-500"> <div class="mt-8 p-4 border border-gray-500">
<h3>Du wirst in der Lage sein, ...</h3> <h3>Du wirst in der Lage sein, ...</h3>
<ul class="mt-4"> <ul class="mt-4">
<li class="text-xl flex items-center" v-for="goal in circle.goals" :key="goal.id"> <li
<it-icon-check class="mt-4 hidden lg:block w-12 h-12 text-sky-500 flex-none"></it-icon-check> v-for="goal in circle.goals"
:key="goal.id"
class="text-xl flex items-center"
>
<it-icon-check
class="mt-4 hidden lg:block w-12 h-12 text-sky-500 flex-none"
></it-icon-check>
<div class="mt-4">{{ goal.value }}</div> <div class="mt-4">{{ goal.value }}</div>
</li> </li>
</ul> </ul>
</div> </div>
<h3 class="mt-16"> <h3 class="mt-16">
Du wirst dein neu erworbenes Wissen auf folgenden berufstypischen Situation anwenden können: Du wirst dein neu erworbenes Wissen auf folgenden berufstypischen Situation
anwenden können:
</h3> </h3>
<ul class="grid grid-cols-1 lg:grid-cols-3 auto-rows-fr gap-6 mt-8"> <ul class="grid grid-cols-1 lg:grid-cols-3 auto-rows-fr gap-6 mt-8">

View File

@ -1,29 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import { useCircleStore } from "@/stores/circle";
import { computed } from 'vue' import type { LearningContent } from "@/types";
import type { LearningContent } from '@/types' import * as log from "loglevel";
import { useCircleStore } from '@/stores/circle' import { computed } from "vue";
log.debug('LearningContent.vue setup') log.debug("LearningContent.vue setup");
const circleStore = useCircleStore() const circleStore = useCircleStore();
const props = defineProps<{ const props = defineProps<{
learningContent: LearningContent learningContent: LearningContent;
}>() }>();
const block = computed(() => { const block = computed(() => {
if (props.learningContent?.contents?.length) { if (props.learningContent?.contents?.length) {
return props.learningContent.contents[0] return props.learningContent.contents[0];
} }
return undefined return undefined;
}) });
</script> </script>
<template> <template>
<div v-if="block"> <div v-if="block">
<nav class="px-4 lg:px-8 py-4 flex justify-between items-center border-b border-gray-500"> <nav
class="px-4 lg:px-8 py-4 flex justify-between items-center border-b border-gray-500"
>
<button <button
type="button" type="button"
class="btn-text inline-flex items-center px-3 py-2 font-normal" class="btn-text inline-flex items-center px-3 py-2 font-normal"
@ -34,7 +36,9 @@ const block = computed(() => {
<span class="hidden lg:inline">zurück zum Circle</span> <span class="hidden lg:inline">zurück zum Circle</span>
</button> </button>
<h1 class="text-xl hidden lg:block" data-cy="ln-title">{{ learningContent?.title }}</h1> <h1 class="text-xl hidden lg:block" data-cy="ln-title">
{{ learningContent?.title }}
</h1>
<button <button
type="button" type="button"
@ -64,7 +68,6 @@ const block = computed(() => {
> >
</iframe> </iframe>
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,23 +1,48 @@
<script setup lang="ts"> <script setup lang="ts">
import type { LearningContentType } from '@/types' import type { LearningContentType } from "@/types";
import { learningContentTypesToName } from '@/utils/typeMaps' import { learningContentTypesToName } from "@/utils/typeMaps";
const props = defineProps<{ const props = defineProps<{
learningContentType: LearningContentType learningContentType: LearningContentType;
}>() }>();
</script> </script>
<template> <template>
<div class="flex bg-gray-200 rounded-full px-2.5 py-0.5 gap-2 items-center w-min h-min"> <div
<it-icon-lc-assignment class="w-6 h-6" v-if="props.learningContentType === 'assignment'" /> class="flex bg-gray-200 rounded-full px-2.5 py-0.5 gap-2 items-center w-min h-min"
<it-icon-lc-exercise class="w-6 h-6" v-else-if="props.learningContentType === 'exercise'" /> >
<it-icon-lc-book class="w-6 h-6" v-else-if="props.learningContentType === 'book'" /> <it-icon-lc-assignment
<it-icon-lc-video class="w-6 h-6" v-else-if="props.learningContentType === 'video'" /> v-if="props.learningContentType === 'assignment'"
<it-icon-lc-media-library class="w-6 h-6" v-else-if="props.learningContentType === 'media_library'" /> class="w-6 h-6"
<it-icon-lc-test class="w-6 h-6" v-else-if="props.learningContentType === 'test'" /> />
<it-icon-lc-online-training class="w-6 h-6" v-else-if="props.learningContentType === 'online_training'" /> <it-icon-lc-exercise
<it-icon-lc-resource class="w-6 h-6" v-else-if="props.learningContentType === 'resource'" /> v-else-if="props.learningContentType === 'exercise'"
<it-icon-lc-document class="w-6 h-6" v-else-if="props.learningContentType === 'document'" /> class="w-6 h-6"
<p class="whitespace-nowrap">{{ learningContentTypesToName.get(props.learningContentType) }}</p> />
<it-icon-lc-book v-else-if="props.learningContentType === 'book'" class="w-6 h-6" />
<it-icon-lc-video
v-else-if="props.learningContentType === 'video'"
class="w-6 h-6"
/>
<it-icon-lc-media-library
v-else-if="props.learningContentType === 'media_library'"
class="w-6 h-6"
/>
<it-icon-lc-test v-else-if="props.learningContentType === 'test'" class="w-6 h-6" />
<it-icon-lc-online-training
v-else-if="props.learningContentType === 'online_training'"
class="w-6 h-6"
/>
<it-icon-lc-resource
v-else-if="props.learningContentType === 'resource'"
class="w-6 h-6"
/>
<it-icon-lc-document
v-else-if="props.learningContentType === 'document'"
class="w-6 h-6"
/>
<p class="whitespace-nowrap">
{{ learningContentTypesToName.get(props.learningContentType) }}
</p>
</div> </div>
</template> </template>

View File

@ -1,8 +1,8 @@
<script> <script>
import * as d3 from 'd3' import colors from "@/colors.json";
import { useLearningPathStore } from '@/stores/learningPath' import { useLearningPathStore } from "@/stores/learningPath";
import colors from '@/colors.json' import * as d3 from "d3";
import * as log from 'loglevel' import * as log from "loglevel";
export default { export default {
props: { props: {
@ -15,294 +15,334 @@ export default {
type: String, type: String,
}, },
}, },
setup() {
const learningPathStore = useLearningPathStore();
return { learningPathStore };
},
data() { data() {
return { return {
width: 1640, width: 1640,
height: 384, height: 384,
} };
},
setup() {
const learningPathStore = useLearningPathStore()
return { learningPathStore }
}, },
computed: { computed: {
viewBox() { viewBox() {
return `0 0 ${this.width} ${this.height}` return `0 0 ${this.width} ${this.height}`;
}, },
circles() { circles() {
function someFinished(circle, learningSequence) { function someFinished(circle, learningSequence) {
if (circle) { if (circle) {
return circle.someFinishedInLearningSequence(learningSequence.translation_key) return circle.someFinishedInLearningSequence(
learningSequence.translation_key
);
} }
return false return false;
} }
function allFinished(circle, learningSequence) { function allFinished(circle, learningSequence) {
if (circle) { if (circle) {
return circle.allFinishedInLearningSequence(learningSequence.translation_key) return circle.allFinishedInLearningSequence(learningSequence.translation_key);
} }
return false return false;
} }
if (this.learningPathStore.learningPath) { if (this.learningPathStore.learningPath) {
const internalCircles = [] const internalCircles = [];
this.learningPathStore.learningPath.circles.forEach((circle) => { this.learningPathStore.learningPath.circles.forEach((circle) => {
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1) const pieWeights = new Array(
const pieGenerator = d3.pie() Math.max(circle.learningSequences.length, 1)
const pieData = pieGenerator(pieWeights) ).fill(1);
const pieGenerator = d3.pie();
const pieData = pieGenerator(pieWeights);
pieData.forEach((pie) => { pieData.forEach((pie) => {
const thisLearningSequence = circle.learningSequences[parseInt(pie.index)] const thisLearningSequence = circle.learningSequences[parseInt(pie.index)];
pie.startAngle = pie.startAngle + Math.PI pie.startAngle = pie.startAngle + Math.PI;
pie.endAngle = pie.endAngle + Math.PI pie.endAngle = pie.endAngle + Math.PI;
pie.done = circle.someFinishedInLearningSequence(thisLearningSequence.translation_key) pie.done = circle.someFinishedInLearningSequence(
pie.someFinished = someFinished(circle, thisLearningSequence) thisLearningSequence.translation_key
pie.allFinished = allFinished(circle, thisLearningSequence) );
}) pie.someFinished = someFinished(circle, thisLearningSequence);
const newCircle = {} pie.allFinished = allFinished(circle, thisLearningSequence);
newCircle.pieData = pieData.reverse() });
newCircle.title = circle.title const newCircle = {};
newCircle.slug = circle.slug.replace(`${circle.parentLearningPath.slug}-circle-`, '') newCircle.pieData = pieData.reverse();
newCircle.id = circle.id newCircle.title = circle.title;
internalCircles.push(newCircle) newCircle.slug = circle.slug.replace(
}) `${circle.parentLearningPath.slug}-circle-`,
return internalCircles ""
);
newCircle.id = circle.id;
internalCircles.push(newCircle);
});
return internalCircles;
} }
return [] return [];
}, },
svg() { svg() {
return d3.select('#' + this.identifier) return d3.select("#" + this.identifier);
}, },
learningPath() { learningPath() {
return Object.assign({}, this.learningPathStore.learningPath) return Object.assign({}, this.learningPathStore.learningPath);
}, },
}, },
mounted() { mounted() {
log.debug('LearningPathDiagram mounted') log.debug("LearningPathDiagram mounted");
if (this.vertical) { if (this.vertical) {
this.width = Math.min(960, window.innerWidth - 32) this.width = Math.min(960, window.innerWidth - 32);
this.height = 860 this.height = 860;
} }
const circleWidth = this.vertical ? 60 : 200 const circleWidth = this.vertical ? 60 : 200;
const radius = (circleWidth * 0.8) / 2 const radius = (circleWidth * 0.8) / 2;
function getColor(d) { function getColor(d) {
let color = colors.gray[300] let color = colors.gray[300];
if (d.someFinished) { if (d.someFinished) {
color = colors.sky[500] color = colors.sky[500];
} }
if (d.allFinished) { if (d.allFinished) {
color = colors.green[500] color = colors.green[500];
} }
return color return color;
} }
function getHoverColor(d) { function getHoverColor(d) {
let color = colors.gray[200] let color = colors.gray[200];
if (d.someFinished) { if (d.someFinished) {
color = colors.sky[400] color = colors.sky[400];
} }
if (d.allFinished) { if (d.allFinished) {
color = colors.green[400] color = colors.green[400];
} }
return color return color;
} }
const vueRouter = this.$router const vueRouter = this.$router;
// Create append pie charts to the main svg // Create append pie charts to the main svg
const circle_groups = this.svg const circle_groups = this.svg
.selectAll('.circle') .selectAll(".circle")
.data(this.circles) .data(this.circles)
.enter() .enter()
.append('g') .append("g")
.attr('class', 'circle') .attr("class", "circle")
.attr('data-cy', (d) => { .attr("data-cy", (d) => {
if (this.vertical) { if (this.vertical) {
return `circle-${d.slug}-vertical` return `circle-${d.slug}-vertical`;
} else { } else {
return `circle-${d.slug}` return `circle-${d.slug}`;
} }
}) })
.on('mouseover', function (d, i) { .on("mouseover", function (d, i) {
d3.select(this) d3.select(this)
.selectAll('.learningSegmentArc') .selectAll(".learningSegmentArc")
.transition() .transition()
.duration(200) .duration(200)
.attr('fill', (d) => { .attr("fill", (d) => {
return getHoverColor(d) return getHoverColor(d);
}) });
}) })
.on('mouseout', function (d, i) { .on("mouseout", function (d, i) {
d3.select(this) d3.select(this)
.selectAll('.learningSegmentArc') .selectAll(".learningSegmentArc")
.transition() .transition()
.duration(200) .duration(200)
.attr('fill', (d) => { .attr("fill", (d) => {
return getColor(d) return getColor(d);
}) });
}) })
.on('click', (d, i) => { .on("click", (d, i) => {
vueRouter.push(`/learn/${this.learningPathStore.learningPath.slug}/${i.slug}`) vueRouter.push(`/learn/${this.learningPathStore.learningPath.slug}/${i.slug}`);
}) })
.attr('role', 'button') .attr("role", "button");
const arcGenerator = d3 const arcGenerator = d3
.arc() .arc()
.innerRadius(radius / 2) .innerRadius(radius / 2)
.padAngle(12 / 360) .padAngle(12 / 360)
.outerRadius(radius) .outerRadius(radius);
//Generate groups //Generate groups
const arcs = this.svg const arcs = this.svg
.selectAll('g') .selectAll("g")
.selectAll('.learningSegmentArc') .selectAll(".learningSegmentArc")
.data((d) => { .data((d) => {
return d.pieData return d.pieData;
}) })
.enter() .enter()
.append('g') .append("g")
.attr('class', 'learningSegmentArc') .attr("class", "learningSegmentArc")
.attr('fill', colors.gray[300]) .attr("fill", colors.gray[300]);
arcs arcs
.transition() .transition()
.duration(1000) .duration(1000)
.attr('fill', (d) => { .attr("fill", (d) => {
return getColor(d) return getColor(d);
}) });
//Draw arc paths //Draw arc paths
arcs.append('path').attr('d', arcGenerator) arcs.append("path").attr("d", arcGenerator);
const circlesText = circle_groups const circlesText = circle_groups
.append('text') .append("text")
.attr('fill', colors.blue[900]) .attr("fill", colors.blue[900])
.style('font-size', '18px') .style("font-size", "18px")
.style('overflow-wrap', 'break-word') .style("overflow-wrap", "break-word")
.text((d) => { .text((d) => {
if (!this.vertical) { if (!this.vertical) {
return d.title.replace('Prüfungsvorbereitung', 'Prüfungs- vorbereitung') return d.title.replace("Prüfungsvorbereitung", "Prüfungs- vorbereitung");
} }
return d.title return d.title;
}) });
const topicHeightOffset = 20 const topicHeightOffset = 20;
const topicHeight = 50 const topicHeight = 50;
const circleHeigth = circleWidth + 20 const circleHeigth = circleWidth + 20;
function getTopicHorizontalPosition(i, d, topics) { function getTopicHorizontalPosition(i, d, topics) {
let x = 0 let x = 0;
for (let index = 0; index < i; index++) { for (let index = 0; index < i; index++) {
x += circleWidth * topics[index].circles.length x += circleWidth * topics[index].circles.length;
} }
return x + 30 return x + 30;
} }
function getTopicVerticalPosition(i, d, topics) { function getTopicVerticalPosition(i, d, topics) {
let pos = topicHeightOffset let pos = topicHeightOffset;
for (let index = 0; index < i; index++) { for (let index = 0; index < i; index++) {
const topic = topics[index] const topic = topics[index];
if (topic.is_visible) { if (topic.is_visible) {
pos += topicHeight pos += topicHeight;
} }
pos += circleHeigth * topic.circles.length pos += circleHeigth * topic.circles.length;
} }
return pos + topicHeightOffset return pos + topicHeightOffset;
} }
function getCircleVerticalPostion(i, d, topics) { function getCircleVerticalPostion(i, d, topics) {
let y = circleHeigth / 2 + topicHeightOffset + 10 let y = circleHeigth / 2 + topicHeightOffset + 10;
for (let topic_index = 0; topic_index < topics.length; topic_index++) { for (let topic_index = 0; topic_index < topics.length; topic_index++) {
const topic = topics[topic_index] const topic = topics[topic_index];
if (topic.is_visible) { if (topic.is_visible) {
y += topicHeight y += topicHeight;
} }
for (let circle_index = 0; circle_index < topic.circles.length; circle_index++) { for (
const circle = topic.circles[circle_index] let circle_index = 0;
circle_index < topic.circles.length;
circle_index++
) {
const circle = topic.circles[circle_index];
if (circle.id === d.id) { if (circle.id === d.id) {
return y return y;
} }
y += circleHeigth y += circleHeigth;
} }
} }
} }
const topicGroups = this.svg.selectAll('.topic').data(this.learningPath.topics).enter().append('g') const topicGroups = this.svg
.selectAll(".topic")
.data(this.learningPath.topics)
.enter()
.append("g");
const topicLines = topicGroups.append('line').attr('class', 'stroke-gray-500').attr('stroke-width', 1) const topicLines = topicGroups
.append("line")
.attr("class", "stroke-gray-500")
.attr("stroke-width", 1);
const topicTitles = topicGroups const topicTitles = topicGroups
.append('text') .append("text")
.attr('fill', colors.blue[900]) .attr("fill", colors.blue[900])
.style('font-size', '16px') .style("font-size", "16px")
.text((d) => d.title) .text((d) => d.title);
// Calculate positions of objects // Calculate positions of objects
if (this.vertical) { if (this.vertical) {
const Circles_X = radius const Circles_X = radius;
const Topics_X = Circles_X - radius const Topics_X = Circles_X - radius;
circle_groups.attr('transform', (d, i) => { circle_groups.attr("transform", (d, i) => {
return 'translate(' + Circles_X + ',' + getCircleVerticalPostion(i, d, this.learningPath.topics) + ')' return (
}) "translate(" +
Circles_X +
"," +
getCircleVerticalPostion(i, d, this.learningPath.topics) +
")"
);
});
circlesText circlesText
.attr('y', 7) .attr("y", 7)
.attr('x', radius + 40) .attr("x", radius + 40)
.attr('class', 'circlesText text-xl font-bold block') .attr("class", "circlesText text-xl font-bold block");
topicGroups topicGroups
.attr('transform', (d, i) => { .attr("transform", (d, i) => {
return 'translate(' + Topics_X + ', ' + getTopicVerticalPosition(i, d, this.learningPath.topics) + ')' return (
}) "translate(" +
.attr('class', (d) => { Topics_X +
return 'topic '.concat(d.is_visible ? 'block' : 'hidden') ", " +
getTopicVerticalPosition(i, d, this.learningPath.topics) +
")"
);
}) })
.attr("class", (d) => {
return "topic ".concat(d.is_visible ? "block" : "hidden");
});
topicLines.transition().duration('1000').attr('x2', this.width) topicLines.transition().duration("1000").attr("x2", this.width);
topicTitles.attr('y', 30) topicTitles.attr("y", 30);
} else { } else {
circle_groups.attr('transform', (d, i) => { circle_groups.attr("transform", (d, i) => {
const x_coord = (i + 1) * circleWidth - radius const x_coord = (i + 1) * circleWidth - radius;
return 'translate(' + x_coord + ', 200)' return "translate(" + x_coord + ", 200)";
}) });
circlesText circlesText
.attr('y', radius + 30) .attr("y", radius + 30)
.style('text-anchor', 'middle') .style("text-anchor", "middle")
.call(wrap, circleWidth - 20) .call(wrap, circleWidth - 20)
.attr('class', 'circlesText text-xl font-bold hidden lg:block') .attr("class", "circlesText text-xl font-bold hidden lg:block");
topicGroups topicGroups
.attr('transform', (d, i) => { .attr("transform", (d, i) => {
return 'translate(' + getTopicHorizontalPosition(i, d, this.learningPathStore.learningPath.topics) + ',0)' return (
}) "translate(" +
.attr('class', (d) => { getTopicHorizontalPosition(
return 'topic '.concat(d.is_visible ? 'hidden lg:block' : 'hidden') i,
d,
this.learningPathStore.learningPath.topics
) +
",0)"
);
}) })
.attr("class", (d) => {
return "topic ".concat(d.is_visible ? "hidden lg:block" : "hidden");
});
topicLines topicLines
.attr('x1', -10) .attr("x1", -10)
.attr('y1', 0) .attr("y1", 0)
.attr('x2', -10) .attr("x2", -10)
.attr('y2', 0) .attr("y2", 0)
.transition() .transition()
.duration('1000') .duration("1000")
.attr('y2', 350) .attr("y2", 350);
topicTitles topicTitles
.attr('y', 20) .attr("y", 20)
.style('font-size', '18px') .style("font-size", "18px")
.call(wrap, circleWidth * 0.8) .call(wrap, circleWidth * 0.8)
.attr('class', 'topicTitles font-bold') .attr("class", "topicTitles font-bold");
} }
function wrap(text, width) { function wrap(text, width) {
@ -313,38 +353,42 @@ export default {
line = [], line = [],
lineNumber = 0, lineNumber = 0,
lineHeight = 1.1, // ems lineHeight = 1.1, // ems
y = text.attr('y'), y = text.attr("y"),
dy = 0, //parseFloat(text.attr('dy')), dy = 0, //parseFloat(text.attr('dy')),
tspan = text tspan = text
.text(null) .text(null)
.append('tspan') .append("tspan")
.attr('x', 0) .attr("x", 0)
.attr('y', y) .attr("y", y)
.attr('dy', dy + 'em') .attr("dy", dy + "em");
while ((word = words.pop())) { while ((word = words.pop())) {
line.push(word) line.push(word);
tspan.text(line.join(' ')) tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) { if (tspan.node().getComputedTextLength() > width) {
line.pop() line.pop();
tspan.text(line.join(' ')) tspan.text(line.join(" "));
line = [word] line = [word];
tspan = text tspan = text
.append('tspan') .append("tspan")
.attr('x', 0) .attr("x", 0)
.attr('y', y) .attr("y", y)
.attr('dy', ++lineNumber * lineHeight + dy + 'em') .attr("dy", ++lineNumber * lineHeight + dy + "em")
.text(word) .text(word);
} }
} }
}) });
} }
}, },
} };
</script> </script>
<template> <template>
<div class="svg-container h-full content-start"> <div class="svg-container h-full content-start">
<svg class="learning-path-visualization h-full" :viewBox="viewBox" :id="identifier"></svg> <svg
:id="identifier"
class="learning-path-visualization h-full"
:viewBox="viewBox"
></svg>
</div> </div>
</template> </template>

View File

@ -1,79 +1,90 @@
<script setup lang="ts"> <script setup lang="ts">
import ItCheckbox from '@/components/ui/ItCheckbox.vue' import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import type { CourseCompletionStatus, LearningContent, LearningSequence } from '@/types' import { useCircleStore } from "@/stores/circle";
import { useCircleStore } from '@/stores/circle' import type {
import { computed } from 'vue' CourseCompletionStatus,
import _ from 'lodash' LearningContent,
import { humanizeDuration } from '@/utils/humanizeDuration' LearningSequence,
import LearningContentBadge from './LearningContentTypeBadge.vue' } from "@/types";
import { humanizeDuration } from "@/utils/humanizeDuration";
import _ from "lodash";
import { computed } from "vue";
import LearningContentBadge from "./LearningContentTypeBadge.vue";
const props = defineProps<{ const props = defineProps<{
learningSequence: LearningSequence learningSequence: LearningSequence;
}>() }>();
const circleStore = useCircleStore() const circleStore = useCircleStore();
function toggleCompleted(learningContent: LearningContent) { function toggleCompleted(learningContent: LearningContent) {
let completionStatus: CourseCompletionStatus = 'success' let completionStatus: CourseCompletionStatus = "success";
if (learningContent.completion_status === 'success') { if (learningContent.completion_status === "success") {
completionStatus = 'fail' completionStatus = "fail";
} }
circleStore.markCompletion(learningContent, completionStatus) circleStore.markCompletion(learningContent, completionStatus);
} }
const someFinished = computed(() => { const someFinished = computed(() => {
if (props.learningSequence && circleStore.circle) { if (props.learningSequence && circleStore.circle) {
return circleStore.circle.someFinishedInLearningSequence(props.learningSequence.translation_key) return circleStore.circle.someFinishedInLearningSequence(
props.learningSequence.translation_key
);
} }
return false return false;
}) });
const allFinished = computed(() => { const allFinished = computed(() => {
if (props.learningSequence && circleStore.circle) { if (props.learningSequence && circleStore.circle) {
return circleStore.circle.allFinishedInLearningSequence(props.learningSequence.translation_key) return circleStore.circle.allFinishedInLearningSequence(
props.learningSequence.translation_key
);
} }
return false return false;
}) });
const continueTranslationKeyTuple = computed(() => { const continueTranslationKeyTuple = computed(() => {
if (props.learningSequence && circleStore.circle) { if (props.learningSequence && circleStore.circle) {
const lastFinished = _.findLast(circleStore.circle.flatLearningContents, (learningContent) => { const lastFinished = _.findLast(
return learningContent.completion_status === 'success' circleStore.circle.flatLearningContents,
}) (learningContent) => {
return learningContent.completion_status === "success";
}
);
if (!lastFinished) { if (!lastFinished) {
// must be the first // must be the first
return [circleStore.circle.flatLearningContents[0].translation_key, true] return [circleStore.circle.flatLearningContents[0].translation_key, true];
} }
if (lastFinished && lastFinished.nextLearningContent) { if (lastFinished && lastFinished.nextLearningContent) {
return [lastFinished.nextLearningContent.translation_key, false] return [lastFinished.nextLearningContent.translation_key, false];
} }
} }
return '' return "";
}) });
const learningSequenceBorderClass = computed(() => { const learningSequenceBorderClass = computed(() => {
let result: string[] = [] let result: string[] = [];
if (props.learningSequence && circleStore.circle) { if (props.learningSequence && circleStore.circle) {
if (allFinished.value) { if (allFinished.value) {
result = ['border-l-4', 'border-l-green-500'] result = ["border-l-4", "border-l-green-500"];
} else if (someFinished.value) { } else if (someFinished.value) {
result = ['border-l-4', 'border-l-sky-500'] result = ["border-l-4", "border-l-sky-500"];
} else { } else {
result = ['border-l-gray-500'] result = ["border-l-gray-500"];
} }
} }
return result return result;
}) });
</script> </script>
<template> <template>
<div class="mb-8 learning-sequence" :id="learningSequence.slug"> <div :id="learningSequence.slug" class="mb-8 learning-sequence">
<div class="flex items-center gap-4 mb-2 text-blue-900"> <div class="flex items-center gap-4 mb-2 text-blue-900">
<component :is="learningSequence.icon" /> <component :is="learningSequence.icon" />
<h3 class="text-xl font-semibold"> <h3 class="text-xl font-semibold">
@ -82,11 +93,20 @@ const learningSequenceBorderClass = computed(() => {
<div>{{ humanizeDuration(learningSequence.minutes) }}</div> <div>{{ humanizeDuration(learningSequence.minutes) }}</div>
</div> </div>
<div class="bg-white px-4 lg:px-6 border border-gray-500" :class="learningSequenceBorderClass"> <div
<div v-for="learningUnit in learningSequence.learningUnits" :key="learningUnit.id" class="pt-3 lg:pt-6"> class="bg-white px-4 lg:px-6 border border-gray-500"
<div class="pb-3 lg:pg-6 flex gap-4 text-blue-900" v-if="learningUnit.title"> :class="learningSequenceBorderClass"
>
<div
v-for="learningUnit in learningSequence.learningUnits"
:key="learningUnit.id"
class="pt-3 lg:pt-6"
>
<div v-if="learningUnit.title" class="pb-3 lg:pg-6 flex gap-4 text-blue-900">
<div class="font-semibold">{{ learningUnit.title }}</div> <div class="font-semibold">{{ learningUnit.title }}</div>
<div class="whitespace-nowrap">{{ humanizeDuration(learningUnit.minutes) }}</div> <div class="whitespace-nowrap">
{{ humanizeDuration(learningUnit.minutes) }}
</div>
</div> </div>
<div <div
@ -95,39 +115,54 @@ const learningSequenceBorderClass = computed(() => {
class="flex gap-4 pb-3 lg:pb-6" class="flex gap-4 pb-3 lg:pb-6"
> >
<ItCheckbox <ItCheckbox
:modelValue="learningContent.completion_status === 'success'" :model-value="learningContent.completion_status === 'success'"
:onToggle="() => toggleCompleted(learningContent)" :on-toggle="() => toggleCompleted(learningContent)"
:data-cy="`${learningContent.slug}`" :data-cy="`${learningContent.slug}`"
> >
<span class="flex flex-wrap gap-4 items-center"> <span class="flex flex-wrap gap-4 items-center">
<div <div
@click.stop="circleStore.openLearningContent(learningContent)"
class="cursor-pointer w-full sm:w-auto" class="cursor-pointer w-full sm:w-auto"
@click.stop="circleStore.openLearningContent(learningContent)"
> >
{{ learningContent.title }} {{ learningContent.title }}
</div> </div>
<div class="flex items-center gap-4 flex-col justify-between sm:flex-row sm:grow"> <div
class="flex items-center gap-4 flex-col justify-between sm:flex-row sm:grow"
>
<button <button
v-if="learningContent.translation_key === continueTranslationKeyTuple[0]" v-if="
learningContent.translation_key === continueTranslationKeyTuple[0]
"
class="btn-blue order-1 sm:order-none" class="btn-blue order-1 sm:order-none"
data-cy="ls-continue-button" data-cy="ls-continue-button"
@click.stop="circleStore.openLearningContent(learningContent)" @click.stop="circleStore.openLearningContent(learningContent)"
> >
<span v-if="continueTranslationKeyTuple[1]" class="whitespace-nowrap">Los geht's </span> <span v-if="continueTranslationKeyTuple[1]" class="whitespace-nowrap"
>Los geht's
</span>
<span v-else class="whitespace-nowrap"> Weiter geht's </span> <span v-else class="whitespace-nowrap"> Weiter geht's </span>
</button> </button>
<div class="hidden sm:block"></div> <div class="hidden sm:block"></div>
<div class="w-full sm:w-auto"> <div class="w-full sm:w-auto">
<LearningContentBadge :learningContentType="learningContent.contents[0].type" /> <LearningContentBadge
:learning-content-type="learningContent.contents[0].type"
/>
</div> </div>
</div> </div>
</span> </span>
</ItCheckbox> </ItCheckbox>
</div> </div>
<div v-if="learningUnit.id" class="hover:cursor-pointer" @click="circleStore.openSelfEvaluation(learningUnit)"> <div
<div v-if="circleStore.calcSelfEvaluationStatus(learningUnit)" class="flex items-center gap-4 pb-3 lg:pb-6"> v-if="learningUnit.id"
class="hover:cursor-pointer"
@click="circleStore.openSelfEvaluation(learningUnit)"
>
<div
v-if="circleStore.calcSelfEvaluationStatus(learningUnit)"
class="flex items-center gap-4 pb-3 lg:pb-6"
>
<it-icon-smiley-happy class="w-8 h-8 flex-none" /> <it-icon-smiley-happy class="w-8 h-8 flex-none" />
<div>Selbsteinschätzung: Ich kann das.</div> <div>Selbsteinschätzung: Ich kann das.</div>
</div> </div>

View File

@ -1,39 +1,41 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import { useCircleStore } from "@/stores/circle";
import { computed, reactive } from 'vue' import type { LearningUnit } from "@/types";
import { useCircleStore } from '@/stores/circle' import * as log from "loglevel";
import type { LearningUnit } from '@/types' import { computed, reactive } from "vue";
log.debug('LearningContent.vue setup') log.debug("LearningContent.vue setup");
const circleStore = useCircleStore() const circleStore = useCircleStore();
const state = reactive({ const state = reactive({
questionIndex: 0, questionIndex: 0,
}) });
const props = defineProps<{ const props = defineProps<{
learningUnit: LearningUnit learningUnit: LearningUnit;
}>() }>();
const questions = computed(() => props.learningUnit?.children) const questions = computed(() => props.learningUnit?.children);
const currentQuestion = computed(() => questions.value[state.questionIndex]) const currentQuestion = computed(() => questions.value[state.questionIndex]);
function handleContinue() { function handleContinue() {
log.debug('handleContinue') log.debug("handleContinue");
if (state.questionIndex + 1 < questions.value.length) { if (state.questionIndex + 1 < questions.value.length) {
log.debug('increment questionIndex', state.questionIndex) log.debug("increment questionIndex", state.questionIndex);
state.questionIndex += 1 state.questionIndex += 1;
} else { } else {
log.debug('continue to next learning content') log.debug("continue to next learning content");
circleStore.continueFromSelfEvaluation() circleStore.continueFromSelfEvaluation();
} }
} }
</script> </script>
<template> <template>
<div v-if="learningUnit"> <div v-if="learningUnit">
<nav class="px-4 lg:px-8 py-4 flex justify-between items-center border-b border-gray-500"> <nav
class="px-4 lg:px-8 py-4 flex justify-between items-center border-b border-gray-500"
>
<button <button
type="button" type="button"
class="btn-text inline-flex items-center px-3 py-2 font-normal" class="btn-text inline-flex items-center px-3 py-2 font-normal"
@ -49,11 +51,14 @@ function handleContinue() {
</nav> </nav>
<div class="mx-auto max-w-6xl px-4 lg:px-8 py-4"> <div class="mx-auto max-w-6xl px-4 lg:px-8 py-4">
<div class="mt-2 lg:mt-8 text-gray-700">Schritt {{ state.questionIndex + 1 }} von {{ questions.length }}</div> <div class="mt-2 lg:mt-8 text-gray-700">
Schritt {{ state.questionIndex + 1 }} von {{ questions.length }}
</div>
<p class="text-xl mt-4"> <p class="text-xl mt-4">
Überprüfe, ob du in der Lernheinheit Überprüfe, ob du in der Lernheinheit
<span class="font-bold">"{{ learningUnit.title }}"</span> alles verstanden hast.<br /> <span class="font-bold">"{{ learningUnit.title }}"</span> alles verstanden
hast.<br />
Lies die folgende Aussage und bewerte sie: Lies die folgende Aussage und bewerte sie:
</p> </p>
@ -62,32 +67,34 @@ function handleContinue() {
<div class="mt-4 lg:mt-8 flex flex-col lg:flex-row justify-between gap-6"> <div class="mt-4 lg:mt-8 flex flex-col lg:flex-row justify-between gap-6">
<button <button
@click="circleStore.markCompletion(currentQuestion, 'success')"
class="flex-1 inline-flex items-center text-left p-4 border" class="flex-1 inline-flex items-center text-left p-4 border"
:class="{ :class="{
'border-green-500': currentQuestion.completion_status === 'success', 'border-green-500': currentQuestion.completion_status === 'success',
'border-2': currentQuestion.completion_status === 'success', 'border-2': currentQuestion.completion_status === 'success',
'border-gray-500': currentQuestion.completion_status !== 'success', 'border-gray-500': currentQuestion.completion_status !== 'success',
}" }"
@click="circleStore.markCompletion(currentQuestion, 'success')"
> >
<it-icon-smiley-happy class="w-16 h-16 mr-4"></it-icon-smiley-happy> <it-icon-smiley-happy class="w-16 h-16 mr-4"></it-icon-smiley-happy>
<span class="font-bold text-xl"> Ja, ich kann das. </span> <span class="font-bold text-xl"> Ja, ich kann das. </span>
</button> </button>
<button <button
@click="circleStore.markCompletion(currentQuestion, 'fail')"
class="flex-1 inline-flex items-center text-left p-4 border" class="flex-1 inline-flex items-center text-left p-4 border"
:class="{ :class="{
'border-orange-500': currentQuestion.completion_status === 'fail', 'border-orange-500': currentQuestion.completion_status === 'fail',
'border-2': currentQuestion.completion_status === 'fail', 'border-2': currentQuestion.completion_status === 'fail',
'border-gray-500': currentQuestion.completion_status !== 'fail' 'border-gray-500': currentQuestion.completion_status !== 'fail',
}" }"
@click="circleStore.markCompletion(currentQuestion, 'fail')"
> >
<it-icon-smiley-thinking class="w-16 h-16 mr-4"></it-icon-smiley-thinking> <it-icon-smiley-thinking class="w-16 h-16 mr-4"></it-icon-smiley-thinking>
<span class="font-bold text-xl"> Das muss ich nochmals anschauen. </span> <span class="font-bold text-xl"> Das muss ich nochmals anschauen. </span>
</button> </button>
</div> </div>
<div class="mt-6 lg:mt-12">Schau dein Fortschritt in deinem Kompetenzprofil: Kompetenzprofil öffnen</div> <div class="mt-6 lg:mt-12">
Schau dein Fortschritt in deinem Kompetenzprofil: Kompetenzprofil öffnen
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,18 @@
<template> <template>
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
<path d="M26.3292 15.7779C26.3292 15.7625 26.3474 15.7474 26.3536 15.7321C26.3619 15.71 26.3691 15.6877 26.3749 15.6649V15.6222C26.39 15.5448 26.39 15.465 26.3749 15.3876V15.3449C26.3691 15.3221 26.3619 15.2998 26.3536 15.2777C26.3536 15.2623 26.3383 15.2472 26.3292 15.2319C26.32 15.2132 26.3098 15.1949 26.2985 15.1771L26.262 15.1313L26.2315 15.0918L20.9236 9.44226C20.8147 9.31721 20.6597 9.24193 20.494 9.23364C20.3283 9.22556 20.1665 9.2851 20.0457 9.39888C19.9249 9.51244 19.8558 9.67046 19.8541 9.83634C19.8522 10.002 19.9181 10.1615 20.0363 10.2776L24.3779 14.8906H7.47836C7.1417 14.8906 6.86865 15.1634 6.86865 15.5003C6.86865 15.8371 7.14172 16.11 7.47836 16.11H24.3779L20.0363 20.7229C19.8213 20.9698 19.8398 21.3426 20.0782 21.5672C20.3169 21.7916 20.6898 21.7877 20.9236 21.5583L26.2315 15.918L26.2619 15.8784L26.2985 15.8327C26.3098 15.8149 26.32 15.7966 26.3291 15.7779L26.3292 15.7779Z" fill="#0A0A0A"/> width="30"
<path d="M6.65855 27H11.5884C12.5588 27 13.4894 26.6146 14.1755 25.9286C14.8616 25.2423 15.247 24.3119 15.247 23.3414V21.2926C15.247 20.9559 14.9741 20.6829 14.6372 20.6829C14.3006 20.6829 14.0275 20.956 14.0275 21.2926V23.3506C14.0275 23.9975 13.7706 24.6179 13.3132 25.0753C12.8558 25.5328 12.2354 25.7897 11.5885 25.7897H6.65861C6.01166 25.7897 5.39134 25.5328 4.93386 25.0753C4.47642 24.6179 4.21952 23.9975 4.21952 23.3506V7.65855C4.21952 7.01161 4.47642 6.39129 4.93386 5.93381C5.39131 5.47636 6.01166 5.21946 6.65861 5.21946H11.5885C12.2354 5.21946 12.8557 5.47637 13.3132 5.93381C13.7706 6.39125 14.0275 7.01161 14.0275 7.65855V9.7074C14.0275 10.0441 14.3006 10.3171 14.6372 10.3171C14.9741 10.3171 15.247 10.044 15.247 9.7074V7.65855C15.247 6.68817 14.8616 5.75774 14.1755 5.07143C13.4894 4.38535 12.5588 4 11.5884 4H6.65855C5.68817 4 4.75774 4.38535 4.07143 5.07143C3.38535 5.75768 3 6.68811 3 7.65855V23.3413C3 24.3117 3.38535 25.2422 4.07143 25.9285C4.75768 26.6145 5.68811 26.9999 6.65855 26.9999V27Z" fill="#0A0A0A"/> height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M26.3292 15.7779C26.3292 15.7625 26.3474 15.7474 26.3536 15.7321C26.3619 15.71 26.3691 15.6877 26.3749 15.6649V15.6222C26.39 15.5448 26.39 15.465 26.3749 15.3876V15.3449C26.3691 15.3221 26.3619 15.2998 26.3536 15.2777C26.3536 15.2623 26.3383 15.2472 26.3292 15.2319C26.32 15.2132 26.3098 15.1949 26.2985 15.1771L26.262 15.1313L26.2315 15.0918L20.9236 9.44226C20.8147 9.31721 20.6597 9.24193 20.494 9.23364C20.3283 9.22556 20.1665 9.2851 20.0457 9.39888C19.9249 9.51244 19.8558 9.67046 19.8541 9.83634C19.8522 10.002 19.9181 10.1615 20.0363 10.2776L24.3779 14.8906H7.47836C7.1417 14.8906 6.86865 15.1634 6.86865 15.5003C6.86865 15.8371 7.14172 16.11 7.47836 16.11H24.3779L20.0363 20.7229C19.8213 20.9698 19.8398 21.3426 20.0782 21.5672C20.3169 21.7916 20.6898 21.7877 20.9236 21.5583L26.2315 15.918L26.2619 15.8784L26.2985 15.8327C26.3098 15.8149 26.32 15.7966 26.3291 15.7779L26.3292 15.7779Z"
fill="#0A0A0A"
/>
<path
d="M6.65855 27H11.5884C12.5588 27 13.4894 26.6146 14.1755 25.9286C14.8616 25.2423 15.247 24.3119 15.247 23.3414V21.2926C15.247 20.9559 14.9741 20.6829 14.6372 20.6829C14.3006 20.6829 14.0275 20.956 14.0275 21.2926V23.3506C14.0275 23.9975 13.7706 24.6179 13.3132 25.0753C12.8558 25.5328 12.2354 25.7897 11.5885 25.7897H6.65861C6.01166 25.7897 5.39134 25.5328 4.93386 25.0753C4.47642 24.6179 4.21952 23.9975 4.21952 23.3506V7.65855C4.21952 7.01161 4.47642 6.39129 4.93386 5.93381C5.39131 5.47636 6.01166 5.21946 6.65861 5.21946H11.5885C12.2354 5.21946 12.8557 5.47637 13.3132 5.93381C13.7706 6.39125 14.0275 7.01161 14.0275 7.65855V9.7074C14.0275 10.0441 14.3006 10.3171 14.6372 10.3171C14.9741 10.3171 15.247 10.044 15.247 9.7074V7.65855C15.247 6.68817 14.8616 5.75774 14.1755 5.07143C13.4894 4.38535 12.5588 4 11.5884 4H6.65855C5.68817 4 4.75774 4.38535 4.07143 5.07143C3.38535 5.75768 3 6.68811 3 7.65855V23.3413C3 24.3117 3.38535 25.2422 4.07143 25.9285C4.75768 26.6145 5.68811 26.9999 6.65855 26.9999V27Z"
fill="#0A0A0A"
/>
</svg> </svg>
</template> </template>

File diff suppressed because one or more lines are too long

View File

@ -2,21 +2,27 @@
<svg viewBox="0 0 39 35" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 39 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
d="M38.3507 28.111C38.3898 28.1686 38.4124 28.2358 38.4162 28.3053C38.4201 28.3748 38.4049 28.4441 38.3725 28.5057C38.34 28.5672 38.2914 28.6188 38.2319 28.655C38.1724 28.6911 38.1042 28.7104 38.0346 28.7108H36.3463C36.2839 28.7105 36.2224 28.6949 36.1673 28.6655C36.1122 28.636 36.0652 28.5936 36.0302 28.5419L32.0792 22.7066L28.1282 28.5419C28.0932 28.5936 28.0461 28.636 27.991 28.6654C27.9359 28.6949 27.8745 28.7104 27.812 28.7108H26.122C26.0523 28.7104 25.9841 28.6911 25.9246 28.655C25.8651 28.6188 25.8165 28.5672 25.7841 28.5057C25.7516 28.4441 25.7365 28.3748 25.7403 28.3053C25.7442 28.2358 25.7668 28.1686 25.8058 28.111L31.7648 19.312C31.8 19.26 31.8475 19.2175 31.9029 19.1881C31.9584 19.1586 32.0202 19.1432 32.083 19.1432C32.1458 19.1432 32.2076 19.1586 32.2631 19.1881C32.3186 19.2175 32.366 19.26 32.4012 19.312L38.3507 28.111Z" d="M38.3507 28.111C38.3898 28.1686 38.4124 28.2358 38.4162 28.3053C38.4201 28.3748 38.4049 28.4441 38.3725 28.5057C38.34 28.5672 38.2914 28.6188 38.2319 28.655C38.1724 28.6911 38.1042 28.7104 38.0346 28.7108H36.3463C36.2839 28.7105 36.2224 28.6949 36.1673 28.6655C36.1122 28.636 36.0652 28.5936 36.0302 28.5419L32.0792 22.7066L28.1282 28.5419C28.0932 28.5936 28.0461 28.636 27.991 28.6654C27.9359 28.6949 27.8745 28.7104 27.812 28.7108H26.122C26.0523 28.7104 25.9841 28.6911 25.9246 28.655C25.8651 28.6188 25.8165 28.5672 25.7841 28.5057C25.7516 28.4441 25.7365 28.3748 25.7403 28.3053C25.7442 28.2358 25.7668 28.1686 25.8058 28.111L31.7648 19.312C31.8 19.26 31.8475 19.2175 31.9029 19.1881C31.9584 19.1586 32.0202 19.1432 32.083 19.1432C32.1458 19.1432 32.2076 19.1586 32.2631 19.1881C32.3186 19.2175 32.366 19.26 32.4012 19.312L38.3507 28.111Z"
fill="#007AC3"/> fill="#007AC3"
/>
<path <path
d="M12.6181 28.111C12.6572 28.1686 12.6798 28.2358 12.6836 28.3053C12.6874 28.3748 12.6723 28.4441 12.6399 28.5057C12.6074 28.5672 12.5588 28.6188 12.4993 28.655C12.4398 28.6911 12.3716 28.7104 12.302 28.7108H10.6119C10.5495 28.7104 10.488 28.6949 10.4329 28.6654C10.3779 28.636 10.3308 28.5936 10.2958 28.5419L6.34521 22.7066L2.3942 28.5419C2.35919 28.5936 2.31211 28.636 2.25702 28.6654C2.20193 28.6949 2.1405 28.7104 2.07805 28.7108H0.383918C0.314304 28.7104 0.246102 28.6911 0.186595 28.655C0.127088 28.6188 0.0785123 28.5672 0.0460511 28.5057C0.01359 28.4441 -0.00153491 28.3748 0.00228956 28.3053C0.00611403 28.2358 0.0287451 28.1686 0.0677665 28.111L6.0268 19.312C6.06202 19.26 6.10943 19.2175 6.1649 19.1881C6.22037 19.1586 6.2822 19.1432 6.34498 19.1432C6.40777 19.1432 6.4696 19.1586 6.52506 19.1881C6.58053 19.2175 6.62795 19.26 6.66317 19.312L12.6181 28.111Z" d="M12.6181 28.111C12.6572 28.1686 12.6798 28.2358 12.6836 28.3053C12.6874 28.3748 12.6723 28.4441 12.6399 28.5057C12.6074 28.5672 12.5588 28.6188 12.4993 28.655C12.4398 28.6911 12.3716 28.7104 12.302 28.7108H10.6119C10.5495 28.7104 10.488 28.6949 10.4329 28.6654C10.3779 28.636 10.3308 28.5936 10.2958 28.5419L6.34521 22.7066L2.3942 28.5419C2.35919 28.5936 2.31211 28.636 2.25702 28.6654C2.20193 28.6949 2.1405 28.7104 2.07805 28.7108H0.383918C0.314304 28.7104 0.246102 28.6911 0.186595 28.655C0.127088 28.6188 0.0785123 28.5672 0.0460511 28.5057C0.01359 28.4441 -0.00153491 28.3748 0.00228956 28.3053C0.00611403 28.2358 0.0287451 28.1686 0.0677665 28.111L6.0268 19.312C6.06202 19.26 6.10943 19.2175 6.1649 19.1881C6.22037 19.1586 6.2822 19.1432 6.34498 19.1432C6.40777 19.1432 6.4696 19.1586 6.52506 19.1881C6.58053 19.2175 6.62795 19.26 6.66317 19.312L12.6181 28.111Z"
fill="white"/> fill="white"
/>
<path <path
d="M0.0660575 7.0891C0.027036 7.03145 0.00440495 6.96429 0.000580477 6.89478C-0.003244 6.82527 0.0118844 6.75603 0.0443455 6.69444C0.0768067 6.63286 0.125382 6.58125 0.184889 6.54512C0.244396 6.50899 0.312599 6.48971 0.382213 6.48932H2.07047C2.13293 6.48967 2.19436 6.50523 2.24944 6.53466C2.30453 6.56409 2.35161 6.60651 2.38662 6.65824L6.33718 12.4935L10.2882 6.65824C10.3232 6.60651 10.3703 6.56409 10.4254 6.53466C10.4805 6.50523 10.5419 6.48967 10.6043 6.48932H12.2944C12.364 6.48971 12.4322 6.50899 12.4917 6.54512C12.5512 6.58125 12.5998 6.63286 12.6323 6.69444C12.6647 6.75603 12.6799 6.82527 12.676 6.89478C12.6722 6.96429 12.6496 7.03145 12.6106 7.0891L6.65966 15.8876C6.62451 15.9397 6.57711 15.9824 6.52164 16.0118C6.46616 16.0413 6.40429 16.0568 6.34147 16.0568C6.27865 16.0568 6.21678 16.0413 6.16131 16.0118C6.10583 15.9824 6.05844 15.9397 6.02328 15.8876L0.0660575 7.0891Z" d="M0.0660575 7.0891C0.027036 7.03145 0.00440495 6.96429 0.000580477 6.89478C-0.003244 6.82527 0.0118844 6.75603 0.0443455 6.69444C0.0768067 6.63286 0.125382 6.58125 0.184889 6.54512C0.244396 6.50899 0.312599 6.48971 0.382213 6.48932H2.07047C2.13293 6.48967 2.19436 6.50523 2.24944 6.53466C2.30453 6.56409 2.35161 6.60651 2.38662 6.65824L6.33718 12.4935L10.2882 6.65824C10.3232 6.60651 10.3703 6.56409 10.4254 6.53466C10.4805 6.50523 10.5419 6.48967 10.6043 6.48932H12.2944C12.364 6.48971 12.4322 6.50899 12.4917 6.54512C12.5512 6.58125 12.5998 6.63286 12.6323 6.69444C12.6647 6.75603 12.6799 6.82527 12.676 6.89478C12.6722 6.96429 12.6496 7.03145 12.6106 7.0891L6.65966 15.8876C6.62451 15.9397 6.57711 15.9824 6.52164 16.0118C6.46616 16.0413 6.40429 16.0568 6.34147 16.0568C6.27865 16.0568 6.21678 16.0413 6.16131 16.0118C6.10583 15.9824 6.05844 15.9397 6.02328 15.8876L0.0660575 7.0891Z"
fill="#007AC3"/> fill="#007AC3"
/>
<path <path
d="M25.7982 7.0891C25.7592 7.03145 25.7366 6.96429 25.7328 6.89478C25.7289 6.82527 25.7441 6.75603 25.7765 6.69444C25.809 6.63286 25.8576 6.58125 25.9171 6.54512C25.9766 6.50899 26.0448 6.48971 26.1144 6.48932H27.8026C27.8651 6.48963 27.9265 6.50517 27.9816 6.53461C28.0367 6.56405 28.0838 6.60648 28.1188 6.65824L32.0698 12.4935L36.0208 6.65824C36.0558 6.60651 36.1029 6.56409 36.158 6.53466C36.2131 6.50523 36.2745 6.48967 36.337 6.48932H38.027C38.0966 6.48971 38.1648 6.50899 38.2244 6.54512C38.2839 6.58125 38.3324 6.63286 38.3649 6.69444C38.3974 6.75603 38.4125 6.82527 38.4087 6.89478C38.4048 6.96429 38.3822 7.03145 38.3432 7.0891L32.3841 15.8881C32.349 15.9402 32.3016 15.9828 32.2461 16.0123C32.1906 16.0418 32.1288 16.0572 32.066 16.0572C32.0031 16.0572 31.9413 16.0418 31.8858 16.0123C31.8303 15.9828 31.7829 15.9402 31.7478 15.8881L25.7982 7.0891Z" d="M25.7982 7.0891C25.7592 7.03145 25.7366 6.96429 25.7328 6.89478C25.7289 6.82527 25.7441 6.75603 25.7765 6.69444C25.809 6.63286 25.8576 6.58125 25.9171 6.54512C25.9766 6.50899 26.0448 6.48971 26.1144 6.48932H27.8026C27.8651 6.48963 27.9265 6.50517 27.9816 6.53461C28.0367 6.56405 28.0838 6.60648 28.1188 6.65824L32.0698 12.4935L36.0208 6.65824C36.0558 6.60651 36.1029 6.56409 36.158 6.53466C36.2131 6.50523 36.2745 6.48967 36.337 6.48932H38.027C38.0966 6.48971 38.1648 6.50899 38.2244 6.54512C38.2839 6.58125 38.3324 6.63286 38.3649 6.69444C38.3974 6.75603 38.4125 6.82527 38.4087 6.89478C38.4048 6.96429 38.3822 7.03145 38.3432 7.0891L32.3841 15.8881C32.349 15.9402 32.3016 15.9828 32.2461 16.0123C32.1906 16.0418 32.1288 16.0572 32.066 16.0572C32.0031 16.0572 31.9413 16.0418 31.8858 16.0123C31.8303 15.9828 31.7829 15.9402 31.7478 15.8881L25.7982 7.0891Z"
fill="white"/> fill="white"
/>
<path <path
d="M24.2526 20.5202C23.1808 19.5633 21.7844 19.0514 20.3481 19.0889C17.1866 19.0889 15.1465 21.3851 15.1465 24.9395V34.1161C15.1466 34.218 15.1872 34.3156 15.2592 34.3876C15.3313 34.4596 15.429 34.5 15.5308 34.5H17.1035C17.2053 34.5 17.303 34.4596 17.3751 34.3876C17.4472 34.3156 17.4877 34.218 17.4878 34.1161V28.5739H22.0228C22.1247 28.5738 22.2223 28.5333 22.2943 28.4612C22.3663 28.3891 22.4067 28.2914 22.4067 28.1896V26.7895C22.4067 26.6876 22.3663 26.5899 22.2943 26.5179C22.2223 26.4458 22.1247 26.4052 22.0228 26.4051H17.4874V24.8722C17.4874 22.5431 18.5262 21.2591 20.4163 21.2591C21.3288 21.2362 22.2134 21.5752 22.8769 22.2021L23.0395 22.3471C23.0789 22.3821 23.125 22.4086 23.1751 22.4251C23.2252 22.4415 23.2782 22.4474 23.3307 22.4425C23.3831 22.4376 23.434 22.4219 23.4802 22.3965C23.5264 22.3711 23.5668 22.3364 23.5991 22.2947L24.4545 21.1859C24.5125 21.1105 24.5406 21.0163 24.5331 20.9214C24.5257 20.8265 24.4834 20.7378 24.4143 20.6724C24.3343 20.596 24.263 20.5283 24.2526 20.5202Z" d="M24.2526 20.5202C23.1808 19.5633 21.7844 19.0514 20.3481 19.0889C17.1866 19.0889 15.1465 21.3851 15.1465 24.9395V34.1161C15.1466 34.218 15.1872 34.3156 15.2592 34.3876C15.3313 34.4596 15.429 34.5 15.5308 34.5H17.1035C17.2053 34.5 17.303 34.4596 17.3751 34.3876C17.4472 34.3156 17.4877 34.218 17.4878 34.1161V28.5739H22.0228C22.1247 28.5738 22.2223 28.5333 22.2943 28.4612C22.3663 28.3891 22.4067 28.2914 22.4067 28.1896V26.7895C22.4067 26.6876 22.3663 26.5899 22.2943 26.5179C22.2223 26.4458 22.1247 26.4052 22.0228 26.4051H17.4874V24.8722C17.4874 22.5431 18.5262 21.2591 20.4163 21.2591C21.3288 21.2362 22.2134 21.5752 22.8769 22.2021L23.0395 22.3471C23.0789 22.3821 23.125 22.4086 23.1751 22.4251C23.2252 22.4415 23.2782 22.4474 23.3307 22.4425C23.3831 22.4376 23.434 22.4219 23.4802 22.3965C23.5264 22.3711 23.5668 22.3364 23.5991 22.2947L24.4545 21.1859C24.5125 21.1105 24.5406 21.0163 24.5331 20.9214C24.5257 20.8265 24.4834 20.7378 24.4143 20.6724C24.3343 20.596 24.263 20.5283 24.2526 20.5202Z"
fill="white"/> fill="white"
/>
<path <path
d="M19.2443 6.13249C19.2444 6.08179 19.2546 6.03161 19.2742 5.98484C19.2938 5.93808 19.3224 5.89566 19.3585 5.86001C19.3945 5.82437 19.4373 5.79622 19.4843 5.77717C19.5313 5.75813 19.5816 5.74857 19.6323 5.74904C19.7226 5.74904 19.7989 5.74904 19.8129 5.7522C21.1089 5.881 22.3057 6.50307 23.1558 7.4897C24.0059 8.47632 24.4442 9.75198 24.38 11.0527C24.2917 12.3636 23.7038 13.5907 22.7374 14.4808C21.7709 15.3709 20.4998 15.8562 19.186 15.8366H15.5277C15.4258 15.8365 15.3282 15.7959 15.2562 15.7238C15.1842 15.6518 15.1438 15.5541 15.1438 15.4522V0.884351C15.1438 0.782493 15.1842 0.6848 15.2562 0.612733C15.3282 0.540666 15.4258 0.50012 15.5277 0.5H17.0976C17.1996 0.5 17.2973 0.540494 17.3694 0.612574C17.4415 0.684654 17.482 0.782414 17.482 0.884351V13.2405C17.482 13.3425 17.5225 13.4402 17.5946 13.5123C17.6666 13.5844 17.7644 13.6249 17.8663 13.6249H19.2326C19.9527 13.6327 20.6501 13.3736 21.1905 12.8975C21.7308 12.4215 22.0758 11.7622 22.1588 11.0469C22.2259 10.2951 21.9924 9.5474 21.5092 8.96758C21.0261 8.38775 20.3328 8.0231 19.5812 7.95353C19.4866 7.94417 19.3988 7.89972 19.3353 7.82892C19.2718 7.75812 19.2371 7.66611 19.238 7.57099L19.2443 6.13249Z" d="M19.2443 6.13249C19.2444 6.08179 19.2546 6.03161 19.2742 5.98484C19.2938 5.93808 19.3224 5.89566 19.3585 5.86001C19.3945 5.82437 19.4373 5.79622 19.4843 5.77717C19.5313 5.75813 19.5816 5.74857 19.6323 5.74904C19.7226 5.74904 19.7989 5.74904 19.8129 5.7522C21.1089 5.881 22.3057 6.50307 23.1558 7.4897C24.0059 8.47632 24.4442 9.75198 24.38 11.0527C24.2917 12.3636 23.7038 13.5907 22.7374 14.4808C21.7709 15.3709 20.4998 15.8562 19.186 15.8366H15.5277C15.4258 15.8365 15.3282 15.7959 15.2562 15.7238C15.1842 15.6518 15.1438 15.5541 15.1438 15.4522V0.884351C15.1438 0.782493 15.1842 0.6848 15.2562 0.612733C15.3282 0.540666 15.4258 0.50012 15.5277 0.5H17.0976C17.1996 0.5 17.2973 0.540494 17.3694 0.612574C17.4415 0.684654 17.482 0.782414 17.482 0.884351V13.2405C17.482 13.3425 17.5225 13.4402 17.5946 13.5123C17.6666 13.5844 17.7644 13.6249 17.8663 13.6249H19.2326C19.9527 13.6327 20.6501 13.3736 21.1905 12.8975C21.7308 12.4215 22.0758 11.7622 22.1588 11.0469C22.2259 10.2951 21.9924 9.5474 21.5092 8.96758C21.0261 8.38775 20.3328 8.0231 19.5812 7.95353C19.4866 7.94417 19.3988 7.89972 19.3353 7.82892C19.2718 7.75812 19.2371 7.66611 19.238 7.57099L19.2443 6.13249Z"
fill="white"/> fill="white"
/>
</svg> </svg>
</template> </template>

View File

@ -1,20 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import MediaLink from '@/components/mediaCenter/MediaLink.vue' import MediaLink from "@/components/mediaCenter/MediaLink.vue";
export interface Props { export interface Props {
title: string title: string;
description: string description: string;
linkText: string linkText: string;
url: string url: string;
icon: string icon: string;
openWindow?: boolean openWindow?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
icon: '', icon: "",
description: '', description: "",
openWindow: false, openWindow: false,
}) });
</script> </script>
<template> <template>

View File

@ -2,8 +2,8 @@
// https://router.vuejs.org/guide/advanced/extending-router-link.html // https://router.vuejs.org/guide/advanced/extending-router-link.html
// https://vueschool.io/articles/vuejs-tutorials/extending-vue-router-links-in-vue-3/ // https://vueschool.io/articles/vuejs-tutorials/extending-vue-router-links-in-vue-3/
import { RouterLink } from 'vue-router' import { computed } from "vue";
import { computed } from 'vue' import { RouterLink } from "vue-router";
const props = defineProps({ const props = defineProps({
...RouterLink.props, // @ts-ignore ...RouterLink.props, // @ts-ignore
@ -11,17 +11,29 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}) });
const isExternalLink = computed(() => typeof props.to === 'string' && props.to.startsWith('http')) const isExternalLink = computed(
() => typeof props.to === "string" && props.to.startsWith("http")
);
</script> </script>
<template> <template>
<div> <div>
<a v-if="isExternalLink" :target="props.blank ? '_blank' : '_self'" rel="noopener" :href="props.to"> <a
v-if="isExternalLink"
:target="props.blank ? '_blank' : '_self'"
rel="noopener"
:href="props.to"
>
<slot /> <slot />
</a> </a>
<router-link v-else :target="props.blank ? '_blank' : '_self'" rel="noopener" v-bind="props"> <router-link
v-else
:target="props.blank ? '_blank' : '_self'"
rel="noopener"
v-bind="props"
>
<slot /> <slot />
</router-link> </router-link>
</div> </div>

View File

@ -1,39 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
export interface Props { export interface Props {
title: string, title: string;
description: string, description: string;
call2Action: string, call2Action: string;
link: string, link: string;
icon: string icon: string;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
icon: '' icon: "",
}) });
</script> </script>
<template> <template>
<div class="bg-white p-8 flex justify-between"> <div class="bg-white p-8 flex justify-between">
<div> <div>
<h3 class="mb-4">{{title}}</h3> <h3 class="mb-4">{{ title }}</h3>
<p class="mb-4">{{description}}</p> <p class="mb-4">{{ description }}</p>
<router-link <router-link :to="link" class="inline-flex items-center font-normal">
:to="link" <span class="inline">{{ call2Action }}</span>
class="inline-flex items-center font-normal"
>
<span class="inline">{{call2Action}}</span>
<it-icon-arrow-right class="ml-1 h-5 w-5"></it-icon-arrow-right> <it-icon-arrow-right class="ml-1 h-5 w-5"></it-icon-arrow-right>
</router-link> </router-link>
</div> </div>
<div <div
v-if="icon" v-if="icon"
:class="[`bg-${icon}`]" :class="[`bg-${icon}`]"
class="bg-contain bg-no-repeat bg-right w-2/6 -mr-8"> class="bg-contain bg-no-repeat bg-right w-2/6 -mr-8"
</div> ></div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped></style>
</style>

View File

@ -1,16 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
interface Props { interface Props {
modelValue?: boolean modelValue?: boolean;
disabled?: boolean disabled?: boolean;
onToggle: () => void onToggle?: () => void;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
modelValue: false, modelValue: false,
disabled: false, disabled: false,
}) onToggle: () => {
// do nothing
},
});
defineEmits(['update:modelValue']) defineEmits(["update:modelValue"]);
</script> </script>
<template> <template>

View File

@ -1,27 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
import {Menu, MenuButton, MenuItems, MenuItem} from '@headlessui/vue'
const props = defineProps<{ const props = defineProps<{
buttonClasses: [string], buttonClasses: [string];
listItems: [ listItems: [[object]];
[object] align: "left" | "right";
], }>();
align: 'left' | 'right'
}>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'select', data: object): void (e: "select", data: object): void;
}>() }>();
</script> </script>
<template> <template>
<Menu as="div" class="relative inline-block text-left"> <Menu as="div" class="relative inline-block text-left">
<div> <div>
<MenuButton <MenuButton :class="buttonClasses">
:class="buttonClasses"
>
<slot></slot> <slot></slot>
</MenuButton> </MenuButton>
</div> </div>
@ -38,20 +32,17 @@ const emit = defineEmits<{
class="absolute mt-2 px-6 w-56 origin-top-right divide-y divide-gray-500 bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" class="absolute mt-2 px-6 w-56 origin-top-right divide-y divide-gray-500 bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
:class="[align === 'left' ? 'left-0' : 'right-0']" :class="[align === 'left' ? 'left-0' : 'right-0']"
> >
<div class="" v-for="section in listItems" :key="section"> <div v-for="section in listItems" :key="section" class="">
<div class="px-1 py-1" v-for="item in section" :key="item"> <div v-for="item in section" :key="item" class="px-1 py-1">
<MenuItem> <MenuItem>
<button <button
@click="$emit('select', item.data)"
class="text-black group flex w-full items-center px-0 py-2 text-sm" class="text-black group flex w-full items-center px-0 py-2 text-sm"
@click="$emit('select', item.data)"
> >
<span class="inline-block pr-2"> <span class="inline-block pr-2">
<component <component :is="item.icon" v-if="item.icon"></component>
v-if="item.icon"
:is="item.icon"
></component>
</span> </span>
{{item.title}} {{ item.title }}
</button> </button>
</MenuItem> </MenuItem>
</div> </div>

View File

@ -1,74 +1,91 @@
<script setup lang="ts"> <script setup lang="ts">
import { watch, onMounted, reactive, defineEmits, computed } from 'vue' import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "@headlessui/vue";
import {Listbox, ListboxButton, ListboxOption, ListboxOptions} from '@headlessui/vue'; import { computed, defineEmits } from "vue";
export interface DropdownSelectable { interface DropdownSelectable {
id: number|string, id: number | string;
name: string name: string;
} }
// https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding // https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding
export interface Props { interface Props {
modelValue: { modelValue: {
id: string|number id: string | number;
name: string name: string;
}, };
items: DropdownSelectable[] items: DropdownSelectable[];
} }
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', data: object): void (e: "update:modelValue", data: object): void;
}>() }>();
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
modelValue: { modelValue: () => {
id: -1, return {
name: '' id: -1,
name: "",
};
}, },
items: [], items: () => [],
}) });
const dropdownSelected = computed({ const dropdownSelected = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (val) => emit('update:modelValue', val), set: (val) => emit("update:modelValue", val),
}) });
</script> </script>
<template> <template>
<Listbox as="div" v-model="dropdownSelected"> <Listbox v-model="dropdownSelected" as="div">
<div class="mt-1 relative w-96"> <div class="mt-1 relative w-96">
<ListboxButton <ListboxButton
class="bg-white relative w-full border border-gray-500 pl-5 pr-10 py-3 text-left cursor-default font-bold"> class="bg-white relative w-full border border-gray-500 pl-5 pr-10 py-3 text-left cursor-default font-bold"
>
<span class="block truncate">{{ dropdownSelected.name }}</span> <span class="block truncate">{{ dropdownSelected.name }}</span>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <span
<it-icon-arrow-down class="h-5 w-5" aria-hidden="true"/> class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"
</span> >
<it-icon-arrow-down class="h-5 w-5" aria-hidden="true" />
</span>
</ListboxButton> </ListboxButton>
<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100" <transition
leave-to-class="opacity-0"> leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions <ListboxOptions
class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"> class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
<ListboxOption <ListboxOption
as="template"
v-for="item in items" v-for="item in items"
:key="item.id" :key="item.id"
v-slot="{ active, selected }"
as="template"
:value="item" :value="item"
v-slot="{ active, selected }"> >
<li <li
:class="[active ? 'text-white bg-blue-900' : 'text-black', 'cursor-default select-none relative py-2 pl-3 pr-9']"> :class="[
<span :class="[dropdownSelected ? 'font-semibold' : 'font-normal', 'block truncate']"> active ? 'text-white bg-blue-900' : 'text-black',
{{ item.name }} 'cursor-default select-none relative py-2 pl-3 pr-9',
</span> ]"
>
<span
:class="[
dropdownSelected ? 'font-semibold' : 'font-normal',
'block truncate',
]"
>
{{ item.name }}
</span>
<span v-if="dropdownSelected" <span
class="text-blue-900 absolute inset-y-0 right-0 flex items-center pr-4"> v-if="dropdownSelected"
<it-icon-check class="text-blue-900 absolute inset-y-0 right-0 flex items-center pr-4"
v-if="selected" >
class="h-5 w-5" <it-icon-check v-if="selected" class="h-5 w-5" aria-hidden="true" />
aria-hidden="true"/> </span>
</span>
</li> </li>
</ListboxOption> </ListboxOption>
</ListboxOptions> </ListboxOptions>

View File

@ -1,31 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
// inspiration https://vuejs.org/examples/#modal // inspiration https://vuejs.org/examples/#modal
import {onMounted, watch} from "vue"; import { onMounted, watch } from "vue";
const props = defineProps<{ const props = defineProps<{
show: boolean show: boolean;
}>() }>();
const emits = defineEmits(['closemodal']) const emits = defineEmits(["closemodal"]);
let appElement: HTMLElement | null = null; let appElement: HTMLElement | null = null;
watch(() => props.show, watch(
(isShown) => isShown && appElement ? appElement.classList.add('no-scroll') : null () => props.show,
) (isShown) => (isShown && appElement ? appElement.classList.add("no-scroll") : null)
);
onMounted(() => { onMounted(() => {
appElement = document.getElementById('app'); appElement = document.getElementById("app");
}) });
const closeModal = () => { const closeModal = () => {
if (appElement) { if (appElement) {
appElement.classList.remove('no-scroll') appElement.classList.remove("no-scroll");
} }
emits('closemodal') emits("closemodal");
} };
</script> </script>
<template> <template>
@ -33,7 +33,8 @@ const closeModal = () => {
<div <div
v-if="show" v-if="show"
data-cy="full-screen-modal" data-cy="full-screen-modal"
class="px-4 py-16 lg:px-16 lg:py-24 fixed top-0 overflow-y-scroll bg-white h-full w-full"> class="px-4 py-16 lg:px-16 lg:py-24 fixed top-0 overflow-y-scroll bg-white h-full w-full"
>
<button <button
type="button" type="button"
class="w-8 h-8 absolute right-4 top-4 cursor-pointer" class="w-8 h-8 absolute right-4 top-4 cursor-pointer"

View File

@ -1,16 +1,16 @@
import {getCookieValue} from '@/router/guards'; import { getCookieValue } from "@/router/guards";
class FetchError extends Error { class FetchError extends Error {
response: Response; response: Response;
constructor(response: Response, message = 'HTTP error ' + response.status) { constructor(response: Response, message = "HTTP error " + response.status) {
super(message); super(message);
this.response = response; this.response = response;
} }
} }
export const itFetch = (url: RequestInfo, options: RequestInit) => { export const itFetch = (url: RequestInfo, options: RequestInit) => {
return fetch(url, options).then(response => { return fetch(url, options).then((response) => {
if (!response.ok) { if (!response.ok) {
throw new FetchError(response); throw new FetchError(response);
} }
@ -19,33 +19,35 @@ export const itFetch = (url: RequestInfo, options: RequestInit) => {
}); });
}; };
export const itPost = ( export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {}) => {
url: RequestInfo,
data: unknown,
options: RequestInit = {},
) => {
options = Object.assign({}, options); options = Object.assign({}, options);
const headers = Object.assign({ const headers = Object.assign(
Accept: 'application/json', {
'Content-Type': 'application/json;charset=UTF-8', Accept: "application/json",
}, options?.headers); "Content-Type": "application/json;charset=UTF-8",
},
options?.headers
);
if (options?.headers) { if (options?.headers) {
delete options.headers; delete options.headers;
} }
options = Object.assign({ options = Object.assign(
method: 'POST', {
headers: headers, method: "POST",
body: JSON.stringify(data) headers: headers,
}, options); body: JSON.stringify(data),
},
options
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
options.headers['X-CSRFToken'] = getCookieValue('csrftoken'); options.headers["X-CSRFToken"] = getCookieValue("csrftoken");
if (options.method === 'GET') { if (options.method === "GET") {
delete options.body; delete options.body;
} }
@ -57,5 +59,5 @@ export const itPost = (
}; };
export const itGet = (url: RequestInfo) => { export const itGet = (url: RequestInfo) => {
return itPost(url, {}, {method: 'GET'}); return itPost(url, {}, { method: "GET" });
}; };

View File

@ -1,20 +1,20 @@
import { nextTick } from 'vue' import { nextTick } from "vue";
import { createI18n } from 'vue-i18n' import { createI18n } from "vue-i18n";
// https://vue-i18n.intlify.dev/guide/advanced/lazy.html // https://vue-i18n.intlify.dev/guide/advanced/lazy.html
export const SUPPORT_LOCALES = ['de', 'fr', 'it'] export const SUPPORT_LOCALES = ["de", "fr", "it"];
export function setupI18n(options = { locale: 'de' }) { export function setupI18n(options = { locale: "de" }) {
const i18n = createI18n(options) const i18n = createI18n(options);
setI18nLanguage(i18n, options.locale) setI18nLanguage(i18n, options.locale);
return i18n return i18n;
} }
export function setI18nLanguage(i18n: any, locale: string) { export function setI18nLanguage(i18n: any, locale: string) {
if (i18n.mode === 'legacy') { if (i18n.mode === "legacy") {
i18n.global.locale = locale i18n.global.locale = locale;
} else { } else {
i18n.global.locale.value = locale i18n.global.locale.value = locale;
} }
/** /**
* NOTE: * NOTE:
@ -23,17 +23,17 @@ export function setI18nLanguage(i18n: any, locale: string) {
* *
* axios.defaults.headers.common['Accept-Language'] = locale * axios.defaults.headers.common['Accept-Language'] = locale
*/ */
document.querySelector('html').setAttribute('lang', locale) document.querySelector("html").setAttribute("lang", locale);
} }
export async function loadLocaleMessages(i18n: any, locale: any) { export async function loadLocaleMessages(i18n: any, locale: any) {
// load locale messages with dynamic import // load locale messages with dynamic import
const messages = await import( const messages = await import(
/* webpackChunkName: "locale-[request]" */ `./locales/${locale}.json` /* webpackChunkName: "locale-[request]" */ `./locales/${locale}.json`
) );
// set locale and locale message // set locale and locale message
i18n.global.setLocaleMessage(locale, messages.default) i18n.global.setLocaleMessage(locale, messages.default);
return nextTick() return nextTick();
} }

View File

@ -1,40 +1,38 @@
import { createApp, markRaw } from 'vue' import * as log from "loglevel";
import { createPinia } from 'pinia' import { createPinia } from "pinia";
import * as log from 'loglevel' import { createApp, markRaw } from "vue";
// import {setupI18n} from './i18n' // import {setupI18n} from './i18n'
import App from './App.vue' import App from "./App.vue";
import router from './router' import router from "./router";
import '../tailwind.css' import type { Router } from "vue-router";
import type { Router } from 'vue-router' import "../tailwind.css";
if (window.location.href.indexOf('localhost') >= 0) { if (window.location.href.indexOf("localhost") >= 0) {
log.setLevel('trace') log.setLevel("trace");
} else { } else {
log.setLevel('warn') log.setLevel("warn");
} }
// const i18n = setupI18n() // const i18n = setupI18n()
const app = createApp(App) const app = createApp(App);
// todo: define lang setup // todo: define lang setup
// await loadLocaleMessages(i18n, 'de') // await loadLocaleMessages(i18n, 'de')
app.use(router) app.use(router);
declare module 'pinia' { declare module "pinia" {
export interface PiniaCustomProperties { export interface PiniaCustomProperties {
router: Router router: Router;
} }
} }
const pinia = createPinia(); const pinia = createPinia();
pinia.use(({ store }) => { pinia.use(({ store }) => {
store.router = markRaw(router) store.router = markRaw(router);
}) });
app.use(pinia) app.use(pinia);
// app.use(i18n) // app.use(i18n)
app.mount("#app");
app.mount('#app')

View File

@ -1,32 +1,37 @@
import type { NavigationGuardWithThis, RouteLocationNormalized } from 'vue-router' import { useUserStore } from "@/stores/user";
import { useUserStore } from '@/stores/user' import type { NavigationGuardWithThis, RouteLocationNormalized } from "vue-router";
export const updateLoggedIn: NavigationGuardWithThis<undefined> = (_to) => { export const updateLoggedIn: NavigationGuardWithThis<undefined> = (_to) => {
const loggedIn = getCookieValue('loginStatus') === 'true' const loggedIn = getCookieValue("loginStatus") === "true";
const userStore = useUserStore() const userStore = useUserStore();
userStore.$patch({ loggedIn }) userStore.$patch({ loggedIn });
if (loggedIn && !userStore.email) { if (loggedIn && !userStore.email) {
userStore.fetchUser() userStore.fetchUser();
} }
} };
export const redirectToLoginIfRequired: NavigationGuardWithThis<undefined> = (to, _from) => { export const redirectToLoginIfRequired: NavigationGuardWithThis<undefined> = (
const userStore = useUserStore() to,
if(loginRequired(to) && !userStore.loggedIn) { _from
return `/login?next=${to.fullPath}` ) => {
const userStore = useUserStore();
if (loginRequired(to) && !userStore.loggedIn) {
return `/login?next=${to.fullPath}`;
} }
} };
export const getCookieValue = (cookieName: string): string => { export const getCookieValue = (cookieName: string): string => {
// https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript // https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript
const cookieValue = document.cookie.match('(^|[^;]+)\\s*' + cookieName + '\\s*=\\s*([^;]+)') const cookieValue = document.cookie.match(
"(^|[^;]+)\\s*" + cookieName + "\\s*=\\s*([^;]+)"
);
if (!cookieValue) { if (!cookieValue) {
return '' return "";
} }
return cookieValue.pop() || ''; return cookieValue.pop() || "";
} };
const loginRequired = (to: RouteLocationNormalized) => { const loginRequired = (to: RouteLocationNormalized) => {
return !to.meta?.public return !to.meta?.public;
} };

View File

@ -1,14 +1,14 @@
import { createRouter, createWebHistory } from 'vue-router' import { redirectToLoginIfRequired, updateLoggedIn } from "@/router/guards";
import CockpitView from '@/views/CockpitView.vue' import { useAppStore } from "@/stores/app";
import LoginView from '@/views/LoginView.vue' import CockpitView from "@/views/CockpitView.vue";
import { redirectToLoginIfRequired, updateLoggedIn } from '@/router/guards' import LoginView from "@/views/LoginView.vue";
import { useAppStore } from '@/stores/app' import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
path: '/login', path: "/login",
component: LoginView, component: LoginView,
meta: { meta: {
// no login required -> so `public === true` // no login required -> so `public === true`
@ -16,86 +16,86 @@ const router = createRouter({
}, },
}, },
{ {
path: '/', path: "/",
name: 'home', name: "home",
component: CockpitView, component: CockpitView,
}, },
{ {
path: '/shop', path: "/shop",
component: () => import('@/views/ShopView.vue'), component: () => import("@/views/ShopView.vue"),
}, },
{ {
path: '/mediacenter/:mediaCenterPageSlug', path: "/mediacenter/:mediaCenterPageSlug",
props: true, props: true,
component: () => import('@/views/MediaCenterView.vue'), component: () => import("@/views/MediaCenterView.vue"),
children: [ children: [
{ {
path: 'overview', path: "overview",
component: () => import('@/views/MediaCenterMainView.vue'), component: () => import("@/views/MediaCenterMainView.vue"),
}, },
{ {
path: 'handlungsfelder/:mediaCategorySlug', path: "handlungsfelder/:mediaCategorySlug",
props: true, props: true,
component: () => import('@/views/MediaCategoryDetailView.vue'), component: () => import("@/views/MediaCategoryDetailView.vue"),
}, },
{ {
path: 'handlungsfelder', path: "handlungsfelder",
component: () => import('@/views/MediaCenterCategoryOverview.vue'), component: () => import("@/views/MediaCenterCategoryOverview.vue"),
}, },
{ {
path: 'handlungsfeldlist', path: "handlungsfeldlist",
component: () => import('@/views/MediaList.vue'), component: () => import("@/views/MediaList.vue"),
}, },
], ],
}, },
{ {
path: '/messages', path: "/messages",
component: () => import('@/views/MessagesView.vue'), component: () => import("@/views/MessagesView.vue"),
}, },
{ {
path: '/profile', path: "/profile",
component: () => import('@/views/ProfileView.vue'), component: () => import("@/views/ProfileView.vue"),
}, },
{ {
path: '/learn/:learningPathSlug', path: "/learn/:learningPathSlug",
component: () => import('../views/LearningPathView.vue'), component: () => import("../views/LearningPathView.vue"),
props: true, props: true,
}, },
{ {
path: '/learn/:learningPathSlug/:circleSlug', path: "/learn/:learningPathSlug/:circleSlug",
component: () => import('../views/CircleView.vue'), component: () => import("../views/CircleView.vue"),
props: true, props: true,
}, },
{ {
path: '/learn/:learningPathSlug/:circleSlug/evaluate/:learningUnitSlug', path: "/learn/:learningPathSlug/:circleSlug/evaluate/:learningUnitSlug",
component: () => import('../views/LearningUnitSelfEvaluationView.vue'), component: () => import("../views/LearningUnitSelfEvaluationView.vue"),
props: true, props: true,
}, },
{ {
path: '/learn/:learningPathSlug/:circleSlug/:contentSlug', path: "/learn/:learningPathSlug/:circleSlug/:contentSlug",
component: () => import('../views/LearningContentView.vue'), component: () => import("../views/LearningContentView.vue"),
props: true, props: true,
}, },
{ {
path: '/styleguide', path: "/styleguide",
component: () => import('../views/StyleGuideView.vue'), component: () => import("../views/StyleGuideView.vue"),
meta: { meta: {
public: true, public: true,
}, },
}, },
{ {
path: '/:pathMatch(.*)*', path: "/:pathMatch(.*)*",
component: () => import('../views/404View.vue'), component: () => import("../views/404View.vue"),
}, },
], ],
}) });
router.beforeEach(updateLoggedIn) router.beforeEach(updateLoggedIn);
router.beforeEach(redirectToLoginIfRequired) router.beforeEach(redirectToLoginIfRequired);
router.afterEach((to, from) => { router.afterEach((to, from) => {
const appStore = useAppStore(); const appStore = useAppStore();
appStore.routingFinished = true; appStore.routingFinished = true;
}); });
export default router export default router;

View File

@ -1,12 +1,14 @@
import { describe, it } from 'vitest' import { describe, it } from "vitest";
import data from './learning_path_json.json' import { Circle } from "../circle";
import { Circle } from '../circle' import data from "./learning_path_json.json";
describe('Circle.parseJson', () => { describe("Circle.parseJson", () => {
it('can parse circle from api response', () => { it("can parse circle from api response", () => {
const cirleData = data.children.find((c) => c.slug === 'test-lehrgang-lp-circle-analyse') const cirleData = data.children.find(
const circle = Circle.fromJson(cirleData, undefined) (c) => c.slug === "test-lehrgang-lp-circle-analyse"
expect(circle.learningSequences.length).toBe(3) );
expect(circle.flatLearningContents.length).toBe(7) const circle = Circle.fromJson(cirleData, undefined);
}) expect(circle.learningSequences.length).toBe(3);
}) expect(circle.flatLearningContents.length).toBe(7);
});
});

View File

@ -1,15 +1,15 @@
import { describe, it } from 'vitest' import { describe, it } from "vitest";
import data from './learning_path_json.json' import { LearningPath } from "../learningPath";
import { LearningPath } from '../learningPath' import data from "./learning_path_json.json";
describe('LearningPath.parseJson', () => { describe("LearningPath.parseJson", () => {
it('can parse learning sequences from api response', () => { it("can parse learning sequences from api response", () => {
const learningPath = LearningPath.fromJson(data, []) const learningPath = LearningPath.fromJson(data, []);
expect(learningPath.circles.length).toBe(2) expect(learningPath.circles.length).toBe(2);
expect(learningPath.circles[0].title).toBe('Basis') expect(learningPath.circles[0].title).toBe("Basis");
expect(learningPath.circles[1].title).toBe('Analyse') expect(learningPath.circles[1].title).toBe("Analyse");
expect(learningPath.topics.length).toBe(2) expect(learningPath.topics.length).toBe(2);
}) });
}) });

View File

@ -1,344 +1,344 @@
{ {
"id": 372, "id": 372,
"title": "Test Lernpfad", "title": "Test Lernpfad",
"slug": "test-lehrgang-lp", "slug": "test-lehrgang-lp",
"type": "learnpath.LearningPath", "type": "learnpath.LearningPath",
"translation_key": "42e559ca-970f-4a08-9e5e-63860585ee1e", "translation_key": "42e559ca-970f-4a08-9e5e-63860585ee1e",
"children": [ "children": [
{
"id": 373,
"title": "Basis",
"slug": "test-lehrgang-lp-topic-basis",
"type": "learnpath.Topic",
"translation_key": "d68c1544-cf22-4a59-a81c-8cb977440cd0",
"is_visible": false
},
{
"id": 374,
"title": "Basis",
"slug": "test-lehrgang-lp-circle-basis",
"type": "learnpath.Circle",
"translation_key": "ec62a2af-6f74-4031-b971-c3287bbbc573",
"children": [
{ {
"id": 373, "id": 375,
"title": "Basis", "title": "Starten",
"slug": "test-lehrgang-lp-topic-basis", "slug": "test-lehrgang-lp-circle-basis-ls-starten",
"type": "learnpath.Topic", "type": "learnpath.LearningSequence",
"translation_key": "d68c1544-cf22-4a59-a81c-8cb977440cd0", "translation_key": "c5fdada9-036d-4516-a50f-6656a1c6b009",
"is_visible": false "icon": "it-icon-ls-start"
}, },
{ {
"id": 374, "id": 376,
"title": "Basis", "title": "Einf\u00fchrung",
"slug": "test-lehrgang-lp-circle-basis", "slug": "test-lehrgang-lp-circle-basis-lc-einf\u00fchrung",
"type": "learnpath.Circle", "type": "learnpath.LearningContent",
"translation_key": "ec62a2af-6f74-4031-b971-c3287bbbc573", "translation_key": "01de5131-28ce-4b1f-805f-8643384bfd6b",
"children": [ "minutes": 15,
{ "contents": [
"id": 375, {
"title": "Starten", "type": "document",
"slug": "test-lehrgang-lp-circle-basis-ls-starten", "value": {
"type": "learnpath.LearningSequence", "description": "Beispiel Dokument",
"translation_key": "c5fdada9-036d-4516-a50f-6656a1c6b009", "url": null
"icon": "it-icon-ls-start" },
}, "id": "bd05f721-3e9d-4a11-8fe2-7c04e2365f52"
{ }
"id": 376, ]
"title": "Einf\u00fchrung",
"slug": "test-lehrgang-lp-circle-basis-lc-einf\u00fchrung",
"type": "learnpath.LearningContent",
"translation_key": "01de5131-28ce-4b1f-805f-8643384bfd6b",
"minutes": 15,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "bd05f721-3e9d-4a11-8fe2-7c04e2365f52"
}
]
},
{
"id": 377,
"title": "Beenden",
"slug": "test-lehrgang-lp-circle-basis-ls-beenden",
"type": "learnpath.LearningSequence",
"translation_key": "128c0162-025f-41be-9842-60016a77cdbc",
"icon": "it-icon-ls-end"
},
{
"id": 378,
"title": "Jetzt kann es losgehen!",
"slug": "test-lehrgang-lp-circle-basis-lc-jetzt-kann-es-losgehen",
"type": "learnpath.LearningContent",
"translation_key": "271896b9-6082-4fd4-9d70-6093ec9cc6ea",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "204fc13b-a9ae-40de-8e09-f1e922c4fdd9"
}
]
}
],
"description": "Basis",
"job_situations": [],
"goals": [],
"experts": []
}, },
{ {
"id": 379, "id": 377,
"title": "Beraten der Kunden", "title": "Beenden",
"slug": "test-lehrgang-lp-topic-beraten-der-kunden", "slug": "test-lehrgang-lp-circle-basis-ls-beenden",
"type": "learnpath.Topic", "type": "learnpath.LearningSequence",
"translation_key": "91918780-75f8-4db3-8fb8-91b63f08b9b9", "translation_key": "128c0162-025f-41be-9842-60016a77cdbc",
"is_visible": true "icon": "it-icon-ls-end"
}, },
{ {
"id": 380, "id": 378,
"title": "Analyse", "title": "Jetzt kann es losgehen!",
"slug": "test-lehrgang-lp-circle-analyse", "slug": "test-lehrgang-lp-circle-basis-lc-jetzt-kann-es-losgehen",
"type": "learnpath.Circle", "type": "learnpath.LearningContent",
"translation_key": "50f11be3-a56d-412d-be25-3d272fb5df40", "translation_key": "271896b9-6082-4fd4-9d70-6093ec9cc6ea",
"children": [ "minutes": 30,
{ "contents": [
"id": 381, {
"title": "Starten", "type": "document",
"slug": "test-lehrgang-lp-circle-analyse-ls-starten", "value": {
"type": "learnpath.LearningSequence", "description": "Beispiel Dokument",
"translation_key": "07ac0eb9-3671-4b62-8053-1d0c43a1f0fb", "url": null
"icon": "it-icon-ls-start" },
}, "id": "204fc13b-a9ae-40de-8e09-f1e922c4fdd9"
{ }
"id": 382, ]
"title": "Einleitung Circle \"Analyse\"",
"slug": "test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse",
"type": "learnpath.LearningContent",
"translation_key": "00ed0ab2-fdb0-4ee6-a7d2-42a219b849a8",
"minutes": 15,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "892a9a4a-8e1e-4f7e-8c35-9bf3bbe5371b"
}
]
},
{
"id": 383,
"title": "Beobachten",
"slug": "test-lehrgang-lp-circle-analyse-ls-beobachten",
"type": "learnpath.LearningSequence",
"translation_key": "4cb08bc2-d101-43cc-b006-8f2bbb1a0579",
"icon": "it-icon-ls-watch"
},
{
"id": 384,
"title": "Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lu-fahrzeug",
"type": "learnpath.LearningUnit",
"translation_key": "8f4afa40-c27e-48f7-a2d7-0e713479a55e",
"course_category": {
"id": 15,
"title": "Fahrzeug",
"general": false
},
"children": [
{
"id": 397,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "e9d49552-7d18-418a-94b6-ebb4ee6bf187",
"competence_id": "Y1.3"
},
{
"id": 398,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die IST-Situation des Kunden mit der geeigneten Gespr\u00e4chs-/Fragetechnik zu erfassen.",
"slug": "test-lehrgang-competence-crit-y21-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "5f257b35-c6ca-49e4-9401-a5d02d53926d",
"competence_id": "Y2.1"
}
]
},
{
"id": 385,
"title": "Rafael Fasel wechselt sein Auto",
"slug": "test-lehrgang-lp-circle-analyse-lc-rafael-fasel-wechselt-sein-auto",
"type": "learnpath.LearningContent",
"translation_key": "fda4f870-9307-414d-b07f-eea607a9afb7",
"minutes": 30,
"contents": [
{
"type": "online_training",
"value": {
"description": "In diesem Online-Training lernst du, wie du den Kundenbedarf ermittelst.",
"url": ""
},
"id": "700a0f64-0892-4fa5-9e08-3bd34e99edeb"
}
]
},
{
"id": 386,
"title": "Fachcheck Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug",
"type": "learnpath.LearningContent",
"translation_key": "dce0847f-4593-4bba-bd0c-a09c71eb0344",
"minutes": 30,
"contents": [
{
"type": "test",
"value": {
"description": "Beispiel Test",
"url": null
},
"id": "9f674aaa-ebf0-4a01-adcc-c0c46394fb10"
}
]
},
{
"id": 387,
"title": "Reisen",
"slug": "test-lehrgang-lp-circle-analyse-lu-reisen",
"type": "learnpath.LearningUnit",
"translation_key": "c3f6d33f-8dbc-4d88-9a81-3c602c4f9cc8",
"course_category": {
"id": 16,
"title": "Reisen",
"general": false
},
"children": [
{
"id": 399,
"title": "Innerhalb des Handlungsfelds \u00abReisen\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-reisen",
"type": "competence.PerformanceCriteria",
"translation_key": "1e488b69-8a3e-4acc-9547-48c103e0d038",
"competence_id": "Y1.3"
}
]
},
{
"id": 388,
"title": "Reiseversicherung",
"slug": "test-lehrgang-lp-circle-analyse-lc-reiseversicherung",
"type": "learnpath.LearningContent",
"translation_key": "ff513aae-efe1-4974-b67f-7a292b8aef86",
"minutes": 240,
"contents": [
{
"type": "exercise",
"value": {
"description": "Beispiel \u00dcbung",
"url": null
},
"id": "f35f213e-1a33-49fe-97c5-26e15161719f"
}
]
},
{
"id": 389,
"title": "Emma und Ayla campen durch Amerika",
"slug": "test-lehrgang-lp-circle-analyse-lc-emma-und-ayla-campen-durch-amerika",
"type": "learnpath.LearningContent",
"translation_key": "a77b0f9d-9a70-47bd-8e62-7580d70a4306",
"minutes": 120,
"contents": [
{
"type": "exercise",
"value": {
"description": "Beispiel \u00dcbung",
"url": "/static/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html"
},
"id": "60f087ff-fa3a-4da2-820f-4fcdf449f70d"
}
]
},
{
"id": 390,
"title": "Beenden",
"slug": "test-lehrgang-lp-circle-analyse-ls-beenden",
"type": "learnpath.LearningSequence",
"translation_key": "06f1e998-b827-41cc-8129-d72d731719c1",
"icon": "it-icon-ls-end"
},
{
"id": 391,
"title": "Kompetenzprofil anschauen",
"slug": "test-lehrgang-lp-circle-analyse-lc-kompetenzprofil-anschauen",
"type": "learnpath.LearningContent",
"translation_key": "6cc47dc1-a74f-4cbf-afa6-23885891c82f",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "3f685055-4e3e-4ca9-93af-bac19236931d"
}
]
},
{
"id": 392,
"title": "Circle \"Analyse\" abschliessen",
"slug": "test-lehrgang-lp-circle-analyse-lc-circle-analyse-abschliessen",
"type": "learnpath.LearningContent",
"translation_key": "9b32e2cd-1368-4885-a79b-906b45ba04bc",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "650b7b15-b522-4df7-ac5b-6a654f12334f"
}
]
}
],
"description": "Unit-Test Circle",
"job_situations": [
{
"type": "job_situation",
"value": "Autoversicherung",
"id": "c5a6b365-0a18-47d5-b6e1-6cb8b8ec7d35"
},
{
"type": "job_situation",
"value": "Autokauf",
"id": "e969d2a2-b383-482c-a721-88552af086a6"
}
],
"goals": [
{
"type": "goal",
"value": "... die heutige Versicherungssituation von Privat- oder Gesch\u00e4ftskunden einzusch\u00e4tzen.",
"id": "d9ad8aed-d7d6-42c7-b6d4-65102c8ddf10"
},
{
"type": "goal",
"value": "... deinem Kunden seine optimale L\u00f6sung aufzuzeigen",
"id": "2506950c-45cb-474f-acb9-45e83e9ebe1b"
}
],
"experts": [
{
"type": "person",
"value": {
"first_name": "Patrizia",
"last_name": "Huggel",
"email": "patrizia.huggel@example.com",
"photo": null,
"biography": ""
},
"id": "b7b0ff2e-f840-4d74-99c1-c7a5ee6dc14e"
}
]
} }
], ],
"course": { "description": "Basis",
"id": -1, "job_situations": [],
"title": "Test Lerngang", "goals": [],
"category_name": "Handlungsfeld" "experts": []
},
{
"id": 379,
"title": "Beraten der Kunden",
"slug": "test-lehrgang-lp-topic-beraten-der-kunden",
"type": "learnpath.Topic",
"translation_key": "91918780-75f8-4db3-8fb8-91b63f08b9b9",
"is_visible": true
},
{
"id": 380,
"title": "Analyse",
"slug": "test-lehrgang-lp-circle-analyse",
"type": "learnpath.Circle",
"translation_key": "50f11be3-a56d-412d-be25-3d272fb5df40",
"children": [
{
"id": 381,
"title": "Starten",
"slug": "test-lehrgang-lp-circle-analyse-ls-starten",
"type": "learnpath.LearningSequence",
"translation_key": "07ac0eb9-3671-4b62-8053-1d0c43a1f0fb",
"icon": "it-icon-ls-start"
},
{
"id": 382,
"title": "Einleitung Circle \"Analyse\"",
"slug": "test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse",
"type": "learnpath.LearningContent",
"translation_key": "00ed0ab2-fdb0-4ee6-a7d2-42a219b849a8",
"minutes": 15,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "892a9a4a-8e1e-4f7e-8c35-9bf3bbe5371b"
}
]
},
{
"id": 383,
"title": "Beobachten",
"slug": "test-lehrgang-lp-circle-analyse-ls-beobachten",
"type": "learnpath.LearningSequence",
"translation_key": "4cb08bc2-d101-43cc-b006-8f2bbb1a0579",
"icon": "it-icon-ls-watch"
},
{
"id": 384,
"title": "Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lu-fahrzeug",
"type": "learnpath.LearningUnit",
"translation_key": "8f4afa40-c27e-48f7-a2d7-0e713479a55e",
"course_category": {
"id": 15,
"title": "Fahrzeug",
"general": false
},
"children": [
{
"id": 397,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "e9d49552-7d18-418a-94b6-ebb4ee6bf187",
"competence_id": "Y1.3"
},
{
"id": 398,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die IST-Situation des Kunden mit der geeigneten Gespr\u00e4chs-/Fragetechnik zu erfassen.",
"slug": "test-lehrgang-competence-crit-y21-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "5f257b35-c6ca-49e4-9401-a5d02d53926d",
"competence_id": "Y2.1"
}
]
},
{
"id": 385,
"title": "Rafael Fasel wechselt sein Auto",
"slug": "test-lehrgang-lp-circle-analyse-lc-rafael-fasel-wechselt-sein-auto",
"type": "learnpath.LearningContent",
"translation_key": "fda4f870-9307-414d-b07f-eea607a9afb7",
"minutes": 30,
"contents": [
{
"type": "online_training",
"value": {
"description": "In diesem Online-Training lernst du, wie du den Kundenbedarf ermittelst.",
"url": ""
},
"id": "700a0f64-0892-4fa5-9e08-3bd34e99edeb"
}
]
},
{
"id": 386,
"title": "Fachcheck Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug",
"type": "learnpath.LearningContent",
"translation_key": "dce0847f-4593-4bba-bd0c-a09c71eb0344",
"minutes": 30,
"contents": [
{
"type": "test",
"value": {
"description": "Beispiel Test",
"url": null
},
"id": "9f674aaa-ebf0-4a01-adcc-c0c46394fb10"
}
]
},
{
"id": 387,
"title": "Reisen",
"slug": "test-lehrgang-lp-circle-analyse-lu-reisen",
"type": "learnpath.LearningUnit",
"translation_key": "c3f6d33f-8dbc-4d88-9a81-3c602c4f9cc8",
"course_category": {
"id": 16,
"title": "Reisen",
"general": false
},
"children": [
{
"id": 399,
"title": "Innerhalb des Handlungsfelds \u00abReisen\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-reisen",
"type": "competence.PerformanceCriteria",
"translation_key": "1e488b69-8a3e-4acc-9547-48c103e0d038",
"competence_id": "Y1.3"
}
]
},
{
"id": 388,
"title": "Reiseversicherung",
"slug": "test-lehrgang-lp-circle-analyse-lc-reiseversicherung",
"type": "learnpath.LearningContent",
"translation_key": "ff513aae-efe1-4974-b67f-7a292b8aef86",
"minutes": 240,
"contents": [
{
"type": "exercise",
"value": {
"description": "Beispiel \u00dcbung",
"url": null
},
"id": "f35f213e-1a33-49fe-97c5-26e15161719f"
}
]
},
{
"id": 389,
"title": "Emma und Ayla campen durch Amerika",
"slug": "test-lehrgang-lp-circle-analyse-lc-emma-und-ayla-campen-durch-amerika",
"type": "learnpath.LearningContent",
"translation_key": "a77b0f9d-9a70-47bd-8e62-7580d70a4306",
"minutes": 120,
"contents": [
{
"type": "exercise",
"value": {
"description": "Beispiel \u00dcbung",
"url": "/static/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html"
},
"id": "60f087ff-fa3a-4da2-820f-4fcdf449f70d"
}
]
},
{
"id": 390,
"title": "Beenden",
"slug": "test-lehrgang-lp-circle-analyse-ls-beenden",
"type": "learnpath.LearningSequence",
"translation_key": "06f1e998-b827-41cc-8129-d72d731719c1",
"icon": "it-icon-ls-end"
},
{
"id": 391,
"title": "Kompetenzprofil anschauen",
"slug": "test-lehrgang-lp-circle-analyse-lc-kompetenzprofil-anschauen",
"type": "learnpath.LearningContent",
"translation_key": "6cc47dc1-a74f-4cbf-afa6-23885891c82f",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "3f685055-4e3e-4ca9-93af-bac19236931d"
}
]
},
{
"id": 392,
"title": "Circle \"Analyse\" abschliessen",
"slug": "test-lehrgang-lp-circle-analyse-lc-circle-analyse-abschliessen",
"type": "learnpath.LearningContent",
"translation_key": "9b32e2cd-1368-4885-a79b-906b45ba04bc",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "650b7b15-b522-4df7-ac5b-6a654f12334f"
}
]
}
],
"description": "Unit-Test Circle",
"job_situations": [
{
"type": "job_situation",
"value": "Autoversicherung",
"id": "c5a6b365-0a18-47d5-b6e1-6cb8b8ec7d35"
},
{
"type": "job_situation",
"value": "Autokauf",
"id": "e969d2a2-b383-482c-a721-88552af086a6"
}
],
"goals": [
{
"type": "goal",
"value": "... die heutige Versicherungssituation von Privat- oder Gesch\u00e4ftskunden einzusch\u00e4tzen.",
"id": "d9ad8aed-d7d6-42c7-b6d4-65102c8ddf10"
},
{
"type": "goal",
"value": "... deinem Kunden seine optimale L\u00f6sung aufzuzeigen",
"id": "2506950c-45cb-474f-acb9-45e83e9ebe1b"
}
],
"experts": [
{
"type": "person",
"value": {
"first_name": "Patrizia",
"last_name": "Huggel",
"email": "patrizia.huggel@example.com",
"photo": null,
"biography": ""
},
"id": "b7b0ff2e-f840-4d74-99c1-c7a5ee6dc14e"
}
]
} }
} ],
"course": {
"id": -1,
"title": "Test Lerngang",
"category_name": "Handlungsfeld"
}
}

View File

@ -1,3 +1,4 @@
import type { LearningPath } from "@/services/learningPath";
import type { import type {
CircleChild, CircleChild,
CircleGoal, CircleGoal,
@ -9,34 +10,38 @@ import type {
LearningSequence, LearningSequence,
LearningUnit, LearningUnit,
LearningUnitQuestion, LearningUnitQuestion,
} from '@/types' } from "@/types";
import type { LearningPath } from '@/services/learningPath'
function _createEmptyLearningUnit(parentLearningSequence: LearningSequence): LearningUnit { function _createEmptyLearningUnit(
parentLearningSequence: LearningSequence
): LearningUnit {
return { return {
id: 0, id: 0,
title: '', title: "",
slug: '', slug: "",
translation_key: '', translation_key: "",
type: 'learnpath.LearningUnit', type: "learnpath.LearningUnit",
learningContents: [], learningContents: [],
minutes: 0, minutes: 0,
parentLearningSequence: parentLearningSequence, parentLearningSequence: parentLearningSequence,
children: [], children: [],
last: true, last: true,
completion_status: 'unknown', completion_status: "unknown",
} };
} }
export function parseLearningSequences (circle: Circle, children: CircleChild[]): LearningSequence[] { export function parseLearningSequences(
let learningSequence:LearningSequence | undefined; circle: Circle,
let learningUnit:LearningUnit | undefined; children: CircleChild[]
let learningContent:LearningContent | undefined; ): LearningSequence[] {
let learningSequence: LearningSequence | undefined;
let learningUnit: LearningUnit | undefined;
let learningContent: LearningContent | undefined;
let previousLearningContent: LearningContent | undefined; let previousLearningContent: LearningContent | undefined;
const result:LearningSequence[] = []; const result: LearningSequence[] = [];
children.forEach((child) => { children.forEach((child) => {
if (child.type === 'learnpath.LearningSequence') { if (child.type === "learnpath.LearningSequence") {
if (learningSequence) { if (learningSequence) {
if (learningUnit) { if (learningUnit) {
learningUnit.last = true; learningUnit.last = true;
@ -44,13 +49,13 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
} }
result.push(learningSequence); result.push(learningSequence);
} }
learningSequence = Object.assign(child, {learningUnits: []}); learningSequence = Object.assign(child, { learningUnits: [] });
// initialize empty learning unit if there will not come a learning unit next // initialize empty learning unit if there will not come a learning unit next
learningUnit = _createEmptyLearningUnit(learningSequence); learningUnit = _createEmptyLearningUnit(learningSequence);
} else if (child.type === 'learnpath.LearningUnit') { } else if (child.type === "learnpath.LearningUnit") {
if (!learningSequence) { if (!learningSequence) {
throw new Error('LearningUnit found before LearningSequence'); throw new Error("LearningUnit found before LearningSequence");
} }
if (learningUnit && learningUnit.learningContents.length) { if (learningUnit && learningUnit.learningContents.length) {
@ -64,11 +69,11 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
c.parentLearningUnit = learningUnit; c.parentLearningUnit = learningUnit;
c.parentLearningSequence = learningSequence; c.parentLearningSequence = learningSequence;
return c; return c;
}) }),
}); });
} else if (child.type === 'learnpath.LearningContent') { } else if (child.type === "learnpath.LearningContent") {
if (!learningUnit) { if (!learningUnit) {
throw new Error('LearningContent found before LearningUnit'); throw new Error("LearningContent found before LearningUnit");
} }
previousLearningContent = learningContent; previousLearningContent = learningContent;
@ -93,7 +98,9 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
(learningSequence as LearningSequence).learningUnits.push(learningUnit); (learningSequence as LearningSequence).learningUnits.push(learningUnit);
result.push(learningSequence); result.push(learningSequence);
} else { } else {
throw new Error('Finished with LearningContent but there is no LearningSequence and LearningUnit'); throw new Error(
"Finished with LearningContent but there is no LearningSequence and LearningUnit"
);
} }
// sum minutes // sum minutes
@ -112,9 +119,9 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
} }
export class Circle implements CourseWagtailPage { export class Circle implements CourseWagtailPage {
readonly type = 'learnpath.Circle'; readonly type = "learnpath.Circle";
readonly learningSequences: LearningSequence[]; readonly learningSequences: LearningSequence[];
completion_status: CourseCompletionStatus = 'unknown' completion_status: CourseCompletionStatus = "unknown";
nextCircle?: Circle; nextCircle?: Circle;
previousCircle?: Circle; previousCircle?: Circle;
@ -128,7 +135,7 @@ export class Circle implements CourseWagtailPage {
public children: CircleChild[], public children: CircleChild[],
public goals: CircleGoal[], public goals: CircleGoal[],
public job_situations: CircleJobSituation[], public job_situations: CircleJobSituation[],
public readonly parentLearningPath?: LearningPath, public readonly parentLearningPath?: LearningPath
) { ) {
this.learningSequences = parseLearningSequences(this, this.children); this.learningSequences = parseLearningSequences(this, this.children);
} }
@ -144,8 +151,8 @@ export class Circle implements CourseWagtailPage {
json.children, json.children,
json.goals, json.goals,
json.job_situations, json.job_situations,
learningPath, learningPath
) );
} }
public get flatChildren(): (LearningContent | LearningUnitQuestion)[] { public get flatChildren(): (LearningContent | LearningUnitQuestion)[] {
@ -154,7 +161,7 @@ export class Circle implements CourseWagtailPage {
learningSequence.learningUnits.forEach((learningUnit) => { learningSequence.learningUnits.forEach((learningUnit) => {
learningUnit.children.forEach((learningUnitQuestion) => { learningUnit.children.forEach((learningUnitQuestion) => {
result.push(learningUnitQuestion); result.push(learningUnitQuestion);
}) });
learningUnit.learningContents.forEach((learningContent) => { learningUnit.learningContents.forEach((learningContent) => {
result.push(learningContent); result.push(learningContent);
}); });
@ -187,9 +194,14 @@ export class Circle implements CourseWagtailPage {
public someFinishedInLearningSequence(translationKey: string): boolean { public someFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) { if (translationKey) {
return this.flatChildren.filter((lc) => { return (
return lc.completion_status === 'success' && lc.parentLearningSequence?.translation_key === translationKey; this.flatChildren.filter((lc) => {
}).length > 0; return (
lc.completion_status === "success" &&
lc.parentLearningSequence?.translation_key === translationKey
);
}).length > 0
);
} }
return false; return false;
@ -197,14 +209,17 @@ export class Circle implements CourseWagtailPage {
public allFinishedInLearningSequence(translationKey: string): boolean { public allFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) { if (translationKey) {
const finishedContents = this.flatChildren.filter((lc) => { const finishedContents = this.flatChildren.filter((lc) => {
return lc.completion_status === 'success' && lc.parentLearningSequence?.translation_key === translationKey; return (
lc.completion_status === "success" &&
lc.parentLearningSequence?.translation_key === translationKey
);
}).length; }).length;
const totalContents = this.flatChildren.filter((lc) => { const totalContents = this.flatChildren.filter((lc) => {
return lc.parentLearningSequence?.translation_key === translationKey; return lc.parentLearningSequence?.translation_key === translationKey;
}).length; }).length;
return finishedContents === totalContents return finishedContents === totalContents;
} }
return false; return false;
@ -218,7 +233,7 @@ export class Circle implements CourseWagtailPage {
if (pageIndex >= 0) { if (pageIndex >= 0) {
page.completion_status = completionData[pageIndex].completion_status; page.completion_status = completionData[pageIndex].completion_status;
} else { } else {
page.completion_status = 'unknown'; page.completion_status = "unknown";
} }
}); });
@ -228,7 +243,7 @@ export class Circle implements CourseWagtailPage {
} }
public getUrl(): string { public getUrl(): string {
const shortSlug = this.slug.replace(`${this.parentLearningPath?.slug}-circle-`, '') const shortSlug = this.slug.replace(`${this.parentLearningPath?.slug}-circle-`, "");
return `/learn/${this.parentLearningPath?.slug}/${shortSlug}`; return `/learn/${this.parentLearningPath?.slug}/${shortSlug}`;
} }
} }

View File

@ -1,5 +1,6 @@
import * as _ from 'lodash' import * as _ from "lodash";
import { Circle } from "@/services/circle";
import type { import type {
CourseCompletion, CourseCompletion,
CourseCompletionStatus, CourseCompletionStatus,
@ -7,24 +8,37 @@ import type {
LearningContent, LearningContent,
LearningPathChild, LearningPathChild,
Topic, Topic,
} from '@/types' } from "@/types";
import { Circle } from '@/services/circle'
function getLastCompleted(courseId: number, completionData: CourseCompletion[]) { function getLastCompleted(courseId: number, completionData: CourseCompletion[]) {
return _.orderBy(completionData, ['updated_at'], 'desc').find((c: CourseCompletion) => { return _.orderBy(completionData, ["updated_at"], "desc").find(
return c.completion_status === 'success' && c.course === courseId && c.page_type === 'learnpath.LearningContent' (c: CourseCompletion) => {
}) return (
c.completion_status === "success" &&
c.course === courseId &&
c.page_type === "learnpath.LearningContent"
);
}
);
} }
export class LearningPath implements CourseWagtailPage { export class LearningPath implements CourseWagtailPage {
readonly type = 'learnpath.LearningPath' readonly type = "learnpath.LearningPath";
public topics: Topic[] public topics: Topic[];
public circles: Circle[] public circles: Circle[];
public nextLearningContent?: LearningContent public nextLearningContent?: LearningContent;
readonly completion_status: CourseCompletionStatus = 'unknown' readonly completion_status: CourseCompletionStatus = "unknown";
public static fromJson(json: any, completionData: CourseCompletion[]): LearningPath { public static fromJson(json: any, completionData: CourseCompletion[]): LearningPath {
return new LearningPath(json.id, json.slug, json.title, json.translation_key, json.course.id, json.children, completionData) return new LearningPath(
json.id,
json.slug,
json.title,
json.translation_key,
json.course.id,
json.children,
completionData
);
} }
constructor( constructor(
@ -37,70 +51,75 @@ export class LearningPath implements CourseWagtailPage {
completionData?: CourseCompletion[] completionData?: CourseCompletion[]
) { ) {
// parse children // parse children
this.topics = [] this.topics = [];
this.circles = [] this.circles = [];
let topic: Topic | undefined let topic: Topic | undefined;
this.children.forEach((page) => { this.children.forEach((page) => {
if (page.type === 'learnpath.Topic') { if (page.type === "learnpath.Topic") {
if (topic) { if (topic) {
this.topics.push(topic) this.topics.push(topic);
} }
topic = Object.assign(page, { circles: [] }) topic = Object.assign(page, { circles: [] });
} }
if (page.type === 'learnpath.Circle') { if (page.type === "learnpath.Circle") {
const circle = Circle.fromJson(page, this) const circle = Circle.fromJson(page, this);
if (completionData) { if (completionData) {
circle.parseCompletionData(completionData) circle.parseCompletionData(completionData);
} }
if (topic) { if (topic) {
topic.circles.push(circle) topic.circles.push(circle);
} }
circle.previousCircle = this.circles[this.circles.length - 1] circle.previousCircle = this.circles[this.circles.length - 1];
if (circle.previousCircle) { if (circle.previousCircle) {
circle.previousCircle.nextCircle = circle circle.previousCircle.nextCircle = circle;
} }
this.circles.push(circle) this.circles.push(circle);
} }
}) });
if (topic) { if (topic) {
this.topics.push(topic) this.topics.push(topic);
} }
if (completionData) { if (completionData) {
this.calcNextLearningContent(completionData) this.calcNextLearningContent(completionData);
} }
} }
public calcNextLearningContent(completionData: CourseCompletion[]): void { public calcNextLearningContent(completionData: CourseCompletion[]): void {
this.nextLearningContent = undefined this.nextLearningContent = undefined;
const lastCompletedLearningContent = getLastCompleted(this.courseId, completionData) const lastCompletedLearningContent = getLastCompleted(
this.courseId,
completionData
);
if (lastCompletedLearningContent) { if (lastCompletedLearningContent) {
const lastCircle = this.circles.find( const lastCircle = this.circles.find((circle) => {
(circle) => { return circle.flatLearningContents.find(
return circle.flatLearningContents.find((learningContent) => learningContent.translation_key === lastCompletedLearningContent.page_key) (learningContent) =>
} learningContent.translation_key === lastCompletedLearningContent.page_key
) );
});
if (lastCircle) { if (lastCircle) {
const lastLearningContent = lastCircle.flatLearningContents.find( const lastLearningContent = lastCircle.flatLearningContents.find(
(learningContent) => learningContent.translation_key === lastCompletedLearningContent.page_key (learningContent) =>
) learningContent.translation_key === lastCompletedLearningContent.page_key
);
if (lastLearningContent && lastLearningContent.nextLearningContent) { if (lastLearningContent && lastLearningContent.nextLearningContent) {
this.nextLearningContent = lastLearningContent.nextLearningContent this.nextLearningContent = lastLearningContent.nextLearningContent;
} else { } else {
if (lastCircle.nextCircle) { if (lastCircle.nextCircle) {
this.nextLearningContent = lastCircle.nextCircle.flatLearningContents[0] this.nextLearningContent = lastCircle.nextCircle.flatLearningContents[0];
} }
} }
} }
} else { } else {
if (this.circles[0]) { if (this.circles[0]) {
this.nextLearningContent = this.circles[0].flatLearningContents[0] this.nextLearningContent = this.circles[0].flatLearningContents[0];
} }
} }
} }

View File

@ -1,38 +1,37 @@
import { defineStore } from 'pinia' import { defineStore } from "pinia";
export type AppState = { export type AppState = {
userLoaded: boolean userLoaded: boolean;
routingFinished: boolean routingFinished: boolean;
showMainNavigationBar: boolean showMainNavigationBar: boolean;
} };
const showMainNavigationBarInitialState = () => { const showMainNavigationBarInitialState = () => {
let path = window.location.pathname; let path = window.location.pathname;
// remove dangling slash // remove dangling slash
if (path.endsWith('/')) { if (path.endsWith("/")) {
path = path.slice(0, -1); path = path.slice(0, -1);
} }
const numberOfSlashes = (path.match(/\//g) || []).length; const numberOfSlashes = (path.match(/\//g) || []).length;
// it should hide main navigation bar when on learning content page // it should hide main navigation bar when on learning content page
if (path.startsWith('/learn/') && numberOfSlashes >= 4) { if (path.startsWith("/learn/") && numberOfSlashes >= 4) {
return false return false;
} }
return true; return true;
} };
export const useAppStore = defineStore({ export const useAppStore = defineStore({
id: 'app', id: "app",
state: () => ({ state: () =>
showMainNavigationBar: showMainNavigationBarInitialState(), ({
userLoaded: false, showMainNavigationBar: showMainNavigationBarInitialState(),
routingFinished: false, userLoaded: false,
} as AppState), routingFinished: false,
getters: { } as AppState),
}, getters: {},
actions: { actions: {},
} });
})

View File

@ -1,27 +1,31 @@
import * as log from 'loglevel' import * as log from "loglevel";
import { defineStore } from 'pinia' import { defineStore } from "pinia";
import type { CourseCompletionStatus, LearningContent, LearningUnit, LearningUnitQuestion } from '@/types' import { itPost } from "@/fetchHelpers";
import type { Circle } from '@/services/circle' import type { Circle } from "@/services/circle";
import { itPost } from '@/fetchHelpers' import { useLearningPathStore } from "@/stores/learningPath";
import { useLearningPathStore } from '@/stores/learningPath' import type {
CourseCompletionStatus,
LearningContent,
LearningUnit,
LearningUnitQuestion,
} from "@/types";
export type CircleStoreState = { export type CircleStoreState = {
circle: Circle | undefined circle: Circle | undefined;
page: 'INDEX' | 'OVERVIEW' page: "INDEX" | "OVERVIEW";
} };
export const useCircleStore = defineStore({ export const useCircleStore = defineStore({
id: 'circle', id: "circle",
state: () => { state: () => {
return { return {
circle: undefined, circle: undefined,
page: 'INDEX', page: "INDEX",
} as CircleStoreState; } as CircleStoreState;
}, },
getters: { getters: {},
},
actions: { actions: {
async loadCircle(learningPathSlug: string, circleSlug: string): Promise<Circle> { async loadCircle(learningPathSlug: string, circleSlug: string): Promise<Circle> {
this.circle = undefined; this.circle = undefined;
@ -37,9 +41,13 @@ export const useCircleStore = defineStore({
throw `No circle found with slug: ${circleSlug}`; throw `No circle found with slug: ${circleSlug}`;
} }
return this.circle return this.circle;
}, },
async loadLearningContent(learningPathSlug: string, circleSlug: string, learningContentSlug: string) { async loadLearningContent(
learningPathSlug: string,
circleSlug: string,
learningContentSlug: string
) {
const circle = await this.loadCircle(learningPathSlug, circleSlug); const circle = await this.loadCircle(learningPathSlug, circleSlug);
const result = circle.flatLearningContents.find((learningContent) => { const result = circle.flatLearningContents.find((learningContent) => {
return learningContent.slug.endsWith(learningContentSlug); return learningContent.slug.endsWith(learningContentSlug);
@ -49,24 +57,31 @@ export const useCircleStore = defineStore({
throw `No learning content found with slug: ${learningContentSlug}`; throw `No learning content found with slug: ${learningContentSlug}`;
} }
return result return result;
}, },
async loadSelfEvaluation(learningPathSlug: string, circleSlug: string, learningUnitSlug: string) { async loadSelfEvaluation(
learningPathSlug: string,
circleSlug: string,
learningUnitSlug: string
) {
const circle = await this.loadCircle(learningPathSlug, circleSlug); const circle = await this.loadCircle(learningPathSlug, circleSlug);
const learningUnit = circle.flatLearningUnits.find((child) => { const learningUnit = circle.flatLearningUnits.find((child) => {
return child.slug.endsWith(learningUnitSlug) return child.slug.endsWith(learningUnitSlug);
}); });
if (!learningUnit) { if (!learningUnit) {
throw `No self evaluation found with slug: ${learningUnitSlug}`; throw `No self evaluation found with slug: ${learningUnitSlug}`;
} }
return learningUnit return learningUnit;
}, },
async markCompletion(page: LearningContent | LearningUnitQuestion, completion_status:CourseCompletionStatus='success') { async markCompletion(
page: LearningContent | LearningUnitQuestion,
completion_status: CourseCompletionStatus = "success"
) {
try { try {
page.completion_status = completion_status; page.completion_status = completion_status;
const completionData = await itPost('/api/course/completion/mark/', { const completionData = await itPost("/api/course/completion/mark/", {
page_key: page.translation_key, page_key: page.translation_key,
completion_status: page.completion_status, completion_status: page.completion_status,
}); });
@ -75,37 +90,37 @@ export const useCircleStore = defineStore({
} }
} catch (error) { } catch (error) {
log.error(error); log.error(error);
return error return error;
} }
}, },
openLearningContent(learningContent: LearningContent) { openLearningContent(learningContent: LearningContent) {
const shortSlug = learningContent.slug.replace(`${this.circle?.slug}-lc-`, ''); const shortSlug = learningContent.slug.replace(`${this.circle?.slug}-lc-`, "");
this.router.push({ this.router.push({
path: `${this.circle?.getUrl()}/${shortSlug}`, path: `${this.circle?.getUrl()}/${shortSlug}`,
}); });
}, },
closeLearningContent() { closeLearningContent() {
this.router.push({ this.router.push({
path: `${this.circle?.getUrl()}` path: `${this.circle?.getUrl()}`,
}); });
}, },
openSelfEvaluation(learningUnit: LearningUnit) { openSelfEvaluation(learningUnit: LearningUnit) {
const shortSlug = learningUnit.slug.replace(`${this.circle?.slug}-lu-`, ''); const shortSlug = learningUnit.slug.replace(`${this.circle?.slug}-lu-`, "");
this.router.push({ this.router.push({
path: `${this.circle?.getUrl()}/evaluate/${shortSlug}`, path: `${this.circle?.getUrl()}/evaluate/${shortSlug}`,
}); });
}, },
closeSelfEvaluation() { closeSelfEvaluation() {
this.router.push({ this.router.push({
path: `${this.circle?.getUrl()}` path: `${this.circle?.getUrl()}`,
}); });
}, },
calcSelfEvaluationStatus(learningUnit: LearningUnit) { calcSelfEvaluationStatus(learningUnit: LearningUnit) {
if (learningUnit.children.length > 0) { if (learningUnit.children.length > 0) {
if (learningUnit.children.every((q) => q.completion_status === 'success')) { if (learningUnit.children.every((q) => q.completion_status === "success")) {
return true; return true;
} }
if (learningUnit.children.some((q) => q.completion_status === 'fail')) { if (learningUnit.children.some((q) => q.completion_status === "fail")) {
return false; return false;
} }
} }
@ -113,14 +128,15 @@ export const useCircleStore = defineStore({
}, },
continueFromLearningContent(currentLearningContent: LearningContent) { continueFromLearningContent(currentLearningContent: LearningContent) {
if (currentLearningContent) { if (currentLearningContent) {
this.markCompletion(currentLearningContent, 'success'); this.markCompletion(currentLearningContent, "success");
const nextLearningContent = currentLearningContent.nextLearningContent; const nextLearningContent = currentLearningContent.nextLearningContent;
const currentParent = currentLearningContent.parentLearningUnit; const currentParent = currentLearningContent.parentLearningUnit;
const nextParent = nextLearningContent?.parentLearningUnit; const nextParent = nextLearningContent?.parentLearningUnit;
if ( if (
currentParent && currentParent.id && currentParent &&
currentParent.id &&
currentParent.id !== nextParent?.id && currentParent.id !== nextParent?.id &&
currentParent.children.length > 0 currentParent.children.length > 0
) { ) {
@ -130,7 +146,8 @@ export const useCircleStore = defineStore({
} else if (currentLearningContent.nextLearningContent) { } else if (currentLearningContent.nextLearningContent) {
if ( if (
currentLearningContent.parentLearningSequence && currentLearningContent.parentLearningSequence &&
currentLearningContent.parentLearningSequence.id === nextLearningContent?.parentLearningSequence?.id currentLearningContent.parentLearningSequence.id ===
nextLearningContent?.parentLearningSequence?.id
) { ) {
this.openLearningContent(currentLearningContent.nextLearningContent); this.openLearningContent(currentLearningContent.nextLearningContent);
} else { } else {
@ -140,11 +157,11 @@ export const useCircleStore = defineStore({
this.closeLearningContent(); this.closeLearningContent();
} }
} else { } else {
log.error('currentLearningContent is undefined'); log.error("currentLearningContent is undefined");
} }
}, },
continueFromSelfEvaluation() { continueFromSelfEvaluation() {
this.closeSelfEvaluation() this.closeSelfEvaluation();
// if (this.currentSelfEvaluation) { // if (this.currentSelfEvaluation) {
// const nextContent = this.currentSelfEvaluation.learningContents[this.currentSelfEvaluation.learningContents.length - 1].nextLearningContent; // const nextContent = this.currentSelfEvaluation.learningContents[this.currentSelfEvaluation.learningContents.length - 1].nextLearningContent;
// //
@ -160,6 +177,6 @@ export const useCircleStore = defineStore({
// } else { // } else {
// log.error('currentSelfEvaluation is undefined'); // log.error('currentSelfEvaluation is undefined');
// } // }
} },
} },
}) });

View File

@ -1,18 +1,18 @@
import { defineStore } from 'pinia' import { itGet } from "@/fetchHelpers";
import { itGet } from '@/fetchHelpers' import { LearningPath } from "@/services/learningPath";
import { LearningPath } from '@/services/learningPath' import { defineStore } from "pinia";
export type LearningPathStoreState = { export type LearningPathStoreState = {
learningPath: LearningPath | undefined learningPath: LearningPath | undefined;
page: 'INDEX' | 'OVERVIEW' page: "INDEX" | "OVERVIEW";
} };
export const useLearningPathStore = defineStore({ export const useLearningPathStore = defineStore({
id: 'learningPath', id: "learningPath",
state: () => { state: () => {
return { return {
learningPath: undefined, learningPath: undefined,
page: 'INDEX', page: "INDEX",
} as LearningPathStoreState; } as LearningPathStoreState;
}, },
getters: {}, getters: {},
@ -22,7 +22,9 @@ export const useLearningPathStore = defineStore({
return this.learningPath; return this.learningPath;
} }
const learningPathData = await itGet(`/api/course/page/${slug}/`); const learningPathData = await itGet(`/api/course/page/${slug}/`);
const completionData = await itGet(`/api/course/completion/${learningPathData.course.id}/`); const completionData = await itGet(
`/api/course/completion/${learningPathData.course.id}/`
);
if (!learningPathData) { if (!learningPathData) {
throw `No learning path found with: ${slug}`; throw `No learning path found with: ${slug}`;
@ -31,5 +33,5 @@ export const useLearningPathStore = defineStore({
this.learningPath = LearningPath.fromJson(learningPathData, completionData); this.learningPath = LearningPath.fromJson(learningPathData, completionData);
return this.learningPath; return this.learningPath;
}, },
} },
}) });

View File

@ -1,39 +1,39 @@
import { defineStore } from 'pinia' import { itGet } from "@/fetchHelpers";
import { itGet } from '@/fetchHelpers' import type { MediaLibraryPage } from "@/types";
import type { MediaLibraryPage } from '@/types' import { defineStore } from "pinia";
export type MediaCenterStoreState = { export type MediaCenterStoreState = {
mediaCenterPage: MediaLibraryPage | undefined mediaCenterPage: MediaLibraryPage | undefined;
selectedLearningPath: { id: number; name: string } selectedLearningPath: { id: number; name: string };
availableLearningPaths: { id: number; name: string }[] availableLearningPaths: { id: number; name: string }[];
} };
export const useMediaCenterStore = defineStore({ export const useMediaCenterStore = defineStore({
id: 'mediaCenter', id: "mediaCenter",
state: () => { state: () => {
return { return {
mediaCenterPage: undefined, mediaCenterPage: undefined,
selectedLearningPath: { id: 1, name: 'Alle Lehrgänge' }, selectedLearningPath: { id: 1, name: "Alle Lehrgänge" },
availableLearningPaths: [ availableLearningPaths: [
{ id: 1, name: 'Alle Lehrgänge' }, { id: 1, name: "Alle Lehrgänge" },
{ id: 2, name: 'Versicherungsvermittler/in' }, { id: 2, name: "Versicherungsvermittler/in" },
], ],
} as MediaCenterStoreState } as MediaCenterStoreState;
}, },
getters: {}, getters: {},
actions: { actions: {
async loadMediaCenterPage(slug: string, reload = false) { async loadMediaCenterPage(slug: string, reload = false) {
if (this.mediaCenterPage && !reload) { if (this.mediaCenterPage && !reload) {
return this.mediaCenterPage return this.mediaCenterPage;
} }
const mediaCenterPageData = await itGet(`/api/course/page/${slug}/`) const mediaCenterPageData = await itGet(`/api/course/page/${slug}/`);
if (!mediaCenterPageData) { if (!mediaCenterPageData) {
throw `No mediaCenterPageData found with: ${slug}` throw `No mediaCenterPageData found with: ${slug}`;
} }
this.mediaCenterPage = mediaCenterPageData this.mediaCenterPage = mediaCenterPageData;
return this.mediaCenterPage return this.mediaCenterPage;
}, },
}, },
}) });

View File

@ -1,70 +1,73 @@
import * as log from "loglevel"; import * as log from "loglevel";
import { defineStore } from "pinia";
import { itGet, itPost } from "@/fetchHelpers"; import { itGet, itPost } from "@/fetchHelpers";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
import { defineStore } from "pinia";
// typed state https://stackoverflow.com/questions/71012513/when-using-pinia-and-typescript-how-do-you-use-an-action-to-set-the-state // typed state https://stackoverflow.com/questions/71012513/when-using-pinia-and-typescript-how-do-you-use-an-action-to-set-the-state
export type UserState = { export type UserState = {
first_name: string, first_name: string;
last_name: string, last_name: string;
email: string; email: string;
username: string, username: string;
avatar_url: string, avatar_url: string;
loggedIn: boolean; loggedIn: boolean;
} };
const initialUserState: UserState = { const initialUserState: UserState = {
email: '', email: "",
first_name: '', first_name: "",
last_name: '', last_name: "",
username: '', username: "",
avatar_url: '', avatar_url: "",
loggedIn: false loggedIn: false,
} };
export const useUserStore = defineStore({ export const useUserStore = defineStore({
id: 'user', id: "user",
state: () => (initialUserState as UserState), state: () => initialUserState as UserState,
getters: { getters: {
getFullName(): string { getFullName(): string {
return `${this.first_name} ${this.last_name}`.trim(); return `${this.first_name} ${this.last_name}`.trim();
}, },
}, },
actions: { actions: {
handleLogin(username: string, password: string, next='/') { handleLogin(username: string, password: string, next = "/") {
if (username && password) { if (username && password) {
itPost('/api/core/login/', { itPost("/api/core/login/", {
username, username,
password, password,
}).then((data) => { })
this.$state = data; .then((data) => {
this.loggedIn = true; this.$state = data;
log.debug(`redirect to ${next}`); this.loggedIn = true;
window.location.href = next; log.debug(`redirect to ${next}`);
}).catch(() => { window.location.href = next;
this.loggedIn = false; })
alert('Login failed'); .catch(() => {
}); this.loggedIn = false;
alert("Login failed");
});
} }
}, },
handleLogout() { handleLogout() {
itPost('/api/core/logout/', {}) itPost("/api/core/logout/", {}).then((data) => {
.then(data => { Object.assign(this, initialUserState);
Object.assign(this, initialUserState); window.location.href = "/";
window.location.href = '/'; });
})
}, },
fetchUser() { fetchUser() {
const appStore = useAppStore(); const appStore = useAppStore();
itGet('/api/core/me/').then((data) => { itGet("/api/core/me/")
this.$state = data; .then((data) => {
this.loggedIn = true; this.$state = data;
appStore.userLoaded = true; this.loggedIn = true;
}).catch(() => { appStore.userLoaded = true;
this.loggedIn = false; })
appStore.userLoaded = true; .catch(() => {
}) this.loggedIn = false;
} appStore.userLoaded = true;
} });
}) },
},
});

View File

@ -1,116 +1,115 @@
import type { Circle } from '@/services/circle' import type { Circle } from "@/services/circle";
export type CourseCompletionStatus = 'unknown' | 'fail' | 'success' export type CourseCompletionStatus = "unknown" | "fail" | "success";
export type LearningContentType = export type LearningContentType =
| 'assignment' | "assignment"
| 'book' | "book"
| 'document' | "document"
| 'exercise' | "exercise"
| 'media_library' | "media_library"
| 'online_training' | "online_training"
| 'resource' | "resource"
| 'test' | "test"
| 'video' | "video";
export interface LearningContentBlock { export interface LearningContentBlock {
type: LearningContentType type: LearningContentType;
value: { value: {
description: string description: string;
} };
id: string id: string;
} }
export interface AssignmentBlock { export interface AssignmentBlock {
type: 'assignment'; type: "assignment";
value: { value: {
description: string; description: string;
url: string; url: string;
}, };
id: string; id: string;
} }
export interface BookBlock { export interface BookBlock {
type: 'book'; type: "book";
value: { value: {
description: string; description: string;
url: string; url: string;
}, };
id: string; id: string;
} }
export interface DocumentBlock { export interface DocumentBlock {
type: 'document'; type: "document";
value: { value: {
description: string; description: string;
url: string; url: string;
}, };
id: string; id: string;
} }
export interface ExerciseBlock { export interface ExerciseBlock {
type: 'exercise'; type: "exercise";
value: { value: {
description: string; description: string;
url: string; url: string;
}, };
id: string; id: string;
} }
export interface MediaLibraryBlock { export interface MediaLibraryBlock {
type: 'media_library'; type: "media_library";
value: { value: {
description: string; description: string;
url: string; url: string;
}, };
id: string; id: string;
} }
export interface OnlineTrainingBlock { export interface OnlineTrainingBlock {
type: 'online_training'; type: "online_training";
value: { value: {
description: string; description: string;
url: string; url: string;
}, };
id: string; id: string;
} }
export interface ResourceBlock { export interface ResourceBlock {
type: 'resource'; type: "resource";
value: { value: {
description: string; description: string;
url: string; url: string;
}, };
id: string; id: string;
} }
export interface TestBlock { export interface TestBlock {
type: 'test'; type: "test";
value: { value: {
description: string; description: string;
url: string; url: string;
}, };
id: string; id: string;
} }
export interface VideoBlock { export interface VideoBlock {
type: 'video'; type: "video";
value: { value: {
description: string; description: string;
url: string; url: string;
}, };
id: string; id: string;
} }
export interface CircleGoal { export interface CircleGoal {
type: 'goal'; type: "goal";
value: string; value: string;
id: string; id: string;
} }
export interface CircleJobSituation { export interface CircleJobSituation {
type: 'job_situation'; type: "job_situation";
value: string; value: string;
id: string; id: string;
} }
@ -124,9 +123,19 @@ export interface CourseWagtailPage {
} }
export interface LearningContent extends CourseWagtailPage { export interface LearningContent extends CourseWagtailPage {
type: 'learnpath.LearningContent'; type: "learnpath.LearningContent";
minutes: number; minutes: number;
contents: (AssignmentBlock | BookBlock | DocumentBlock | ExerciseBlock | MediaLibraryBlock | OnlineTrainingBlock | ResourceBlock | TestBlock | VideoBlock)[]; contents: (
| AssignmentBlock
| BookBlock
| DocumentBlock
| ExerciseBlock
| MediaLibraryBlock
| OnlineTrainingBlock
| ResourceBlock
| TestBlock
| VideoBlock
)[];
parentCircle: Circle; parentCircle: Circle;
parentLearningSequence?: LearningSequence; parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit; parentLearningUnit?: LearningUnit;
@ -135,13 +144,13 @@ export interface LearningContent extends CourseWagtailPage {
} }
export interface LearningUnitQuestion extends CourseWagtailPage { export interface LearningUnitQuestion extends CourseWagtailPage {
type: 'learnpath.LearningUnitQuestion'; type: "learnpath.LearningUnitQuestion";
parentLearningSequence?: LearningSequence; parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit; parentLearningUnit?: LearningUnit;
} }
export interface LearningUnit extends CourseWagtailPage { export interface LearningUnit extends CourseWagtailPage {
type: 'learnpath.LearningUnit'; type: "learnpath.LearningUnit";
learningContents: LearningContent[]; learningContents: LearningContent[];
minutes: number; minutes: number;
parentLearningSequence?: LearningSequence; parentLearningSequence?: LearningSequence;
@ -150,22 +159,26 @@ export interface LearningUnit extends CourseWagtailPage {
} }
export interface LearningSequence extends CourseWagtailPage { export interface LearningSequence extends CourseWagtailPage {
type: 'learnpath.LearningSequence'; type: "learnpath.LearningSequence";
icon: string; icon: string;
learningUnits: LearningUnit[]; learningUnits: LearningUnit[];
minutes: number; minutes: number;
} }
export type CircleChild = LearningContent | LearningUnit | LearningSequence | LearningUnitQuestion; export type CircleChild =
| LearningContent
| LearningUnit
| LearningSequence
| LearningUnitQuestion;
export interface WagtailCircle extends CourseWagtailPage { export interface WagtailCircle extends CourseWagtailPage {
type: 'learnpath.Circle'; type: "learnpath.Circle";
children: CircleChild[]; children: CircleChild[];
description: string; description: string;
} }
export interface Topic extends CourseWagtailPage { export interface Topic extends CourseWagtailPage {
type: 'learnpath.Topic'; type: "learnpath.Topic";
is_visible: boolean; is_visible: boolean;
circles: Circle[]; circles: Circle[];
} }
@ -186,17 +199,16 @@ export interface CourseCompletion {
} }
export interface CircleDiagramData { export interface CircleDiagramData {
index: number index: number;
title: string title: string;
icon: string icon: string;
startAngle: number startAngle: number;
endAngle: number endAngle: number;
arrowStartAngle: number arrowStartAngle: number;
arrowEndAngle: number arrowEndAngle: number;
done: boolean done: boolean;
} }
export interface Course { export interface Course {
id: number; id: number;
name: string; name: string;
@ -223,7 +235,7 @@ export interface MediaLink {
description: string; description: string;
link_display_text: string; link_display_text: string;
url: string; url: string;
} };
} }
export interface MediaContentCollection { export interface MediaContentCollection {
@ -231,26 +243,26 @@ export interface MediaContentCollection {
value: { value: {
title: string; title: string;
contents: (MediaDocument | MediaLink)[]; contents: (MediaDocument | MediaLink)[];
} };
} }
export interface MediaCategoryPage extends CourseWagtailPage { export interface MediaCategoryPage extends CourseWagtailPage {
type: 'media_library.MediaCategoryPage'; type: "media_library.MediaCategoryPage";
overview_icon: string; overview_icon: string;
introduction_text: string; introduction_text: string;
description_title: string; description_title: string;
description_text: string; description_text: string;
items: { items: {
type: 'item'; type: "item";
value: string; value: string;
id: string; id: string;
} };
course_category: CourseCategory; course_category: CourseCategory;
body: MediaContentCollection[]; body: MediaContentCollection[];
} }
export interface MediaLibraryPage extends CourseWagtailPage { export interface MediaLibraryPage extends CourseWagtailPage {
type: 'media_library.MediaLibraryPage'; type: "media_library.MediaLibraryPage";
course: Course; course: Course;
children: MediaCategoryPage[]; children: MediaCategoryPage[];
} }

View File

@ -1,13 +1,13 @@
import { expect, test } from 'vitest' import { expect, test } from "vitest";
import { humanizeDuration } from '../humanizeDuration' import { humanizeDuration } from "../humanizeDuration";
test('format duration for humans', () => { test("format duration for humans", () => {
expect(humanizeDuration(1)).toBe('1 Minute') expect(humanizeDuration(1)).toBe("1 Minute");
expect(humanizeDuration(15)).toBe('15 Minuten') expect(humanizeDuration(15)).toBe("15 Minuten");
expect(humanizeDuration(42)).toBe('45 Minuten') expect(humanizeDuration(42)).toBe("45 Minuten");
expect(humanizeDuration(60)).toBe('1 Stunde') expect(humanizeDuration(60)).toBe("1 Stunde");
expect(humanizeDuration(122)).toBe('2 Stunden') expect(humanizeDuration(122)).toBe("2 Stunden");
expect(humanizeDuration(120)).toBe('2 Stunden') expect(humanizeDuration(120)).toBe("2 Stunden");
expect(humanizeDuration(132)).toBe('2 Stunden 15 Minuten') expect(humanizeDuration(132)).toBe("2 Stunden 15 Minuten");
expect(humanizeDuration(632)).toBe('10 Stunden') expect(humanizeDuration(632)).toBe("10 Stunden");
}) });

View File

@ -1,29 +1,30 @@
function pluralize(text: string, count: number) { function pluralize(text: string, count: number) {
if (count === 1) { if (count === 1) {
return text; return text;
} }
return text + 'n'; return text + "n";
} }
export function humanizeDuration(minutes: number) { export function humanizeDuration(minutes: number) {
const hours = Math.floor(minutes / 60) const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60 const remainingMinutes = minutes % 60;
if (hours === 0 && minutes < 16) { if (hours === 0 && minutes < 16) {
return pluralize(`${remainingMinutes} Minute`, remainingMinutes) return pluralize(`${remainingMinutes} Minute`, remainingMinutes);
} }
// Remaining minutes are rounded to 15 mins // Remaining minutes are rounded to 15 mins
const roundToMinutes = 15 const roundToMinutes = 15;
const roundedMinutes = Math.round((minutes % 60) / roundToMinutes) * roundToMinutes const roundedMinutes = Math.round((minutes % 60) / roundToMinutes) * roundToMinutes;
const hoursString = hours > 0 ? pluralize(`${hours} Stunde`, hours) : '' const hoursString = hours > 0 ? pluralize(`${hours} Stunde`, hours) : "";
const showMinutesUpToHours = 10 const showMinutesUpToHours = 10;
const minutesString = roundedMinutes > 0 && hours < showMinutesUpToHours const minutesString =
? pluralize(`${roundedMinutes} Minute`, roundedMinutes) : '' roundedMinutes > 0 && hours < showMinutesUpToHours
? pluralize(`${roundedMinutes} Minute`, roundedMinutes)
: "";
const delimiter = hoursString && minutesString ? ' ' : '' const delimiter = hoursString && minutesString ? " " : "";
return `${hoursString}${delimiter}${minutesString}` return `${hoursString}${delimiter}${minutesString}`;
} }

View File

@ -1,13 +1,13 @@
import type { LearningContentType } from '@/types'; import type { LearningContentType } from "@/types";
export const learningContentTypesToName = new Map<LearningContentType, string>([ export const learningContentTypesToName = new Map<LearningContentType, string>([
['assignment', 'Auftrag'], ["assignment", "Auftrag"],
['book', 'Buch'], ["book", "Buch"],
['document', 'Dokument'], ["document", "Dokument"],
['exercise', 'Übung'], ["exercise", "Übung"],
['media_library', 'Mediathek'], ["media_library", "Mediathek"],
['online_training', 'Online-Training'], ["online_training", "Online-Training"],
['video', 'Video'], ["video", "Video"],
['test', 'Test'], ["test", "Test"],
['resource', 'Hilfsmittel'], ["resource", "Hilfsmittel"],
]); ]);

View File

@ -1,18 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
let url = document.location.href; let url = document.location.href;
if (url.charAt(url.length - 1) !== '/') { if (url.charAt(url.length - 1) !== "/") {
url += '/'; url += "/";
} }
</script> </script>
<template> <template>
<main class="px-4 py-8"> <main class="px-4 py-8">
<h1>404 - Not Found as Vue view...</h1> <h1>404 - Not Found as Vue view...</h1>
<div class="text-xl mt-8">Add trailing slash for django view?</div> <div class="text-xl mt-8">Add trailing slash for django view?</div>
<div class="mt-8 text-xl">Try this: <a class="link" :href="url">{{ url }}</a></div> <div class="mt-8 text-xl">
Try this: <a class="link" :href="url">{{ url }}</a>
</div>
</main> </main>
</template> </template>
<style scoped> <style scoped></style>
</style>

View File

@ -1,57 +1,59 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import CircleDiagram from "@/components/circle/CircleDiagram.vue";
import LearningSequence from '@/components/circle/LearningSequence.vue' import CircleOverview from "@/components/circle/CircleOverview.vue";
import CircleOverview from '@/components/circle/CircleOverview.vue' import LearningSequence from "@/components/circle/LearningSequence.vue";
import CircleDiagram from '@/components/circle/CircleDiagram.vue' import * as log from "loglevel";
import { computed, onMounted } from 'vue' import { useAppStore } from "@/stores/app";
import { useCircleStore } from '@/stores/circle' import { useCircleStore } from "@/stores/circle";
import { useAppStore } from '@/stores/app' import { humanizeDuration } from "@/utils/humanizeDuration";
import { useRoute } from 'vue-router' import _ from "lodash";
import _ from 'lodash' import { computed, onMounted } from "vue";
import { humanizeDuration } from '@/utils/humanizeDuration' import { useRoute } from "vue-router";
const route = useRoute() const route = useRoute();
log.debug('CircleView.vue created', route) log.debug("CircleView.vue created", route);
const props = defineProps<{ const props = defineProps<{
learningPathSlug: string learningPathSlug: string;
circleSlug: string circleSlug: string;
}>() }>();
const appStore = useAppStore() const appStore = useAppStore();
appStore.showMainNavigationBar = true appStore.showMainNavigationBar = true;
const circleStore = useCircleStore() const circleStore = useCircleStore();
const duration = computed(() => { const duration = computed(() => {
if (circleStore.circle) { if (circleStore.circle) {
const minutes = _.sumBy(circleStore.circle.learningSequences, 'minutes') const minutes = _.sumBy(circleStore.circle.learningSequences, "minutes");
return humanizeDuration(minutes) return humanizeDuration(minutes);
} }
return '' return "";
}) });
onMounted(async () => { onMounted(async () => {
log.debug('CircleView.vue mounted', props.learningPathSlug, props.circleSlug) log.debug("CircleView.vue mounted", props.learningPathSlug, props.circleSlug);
try { try {
await circleStore.loadCircle(props.learningPathSlug, props.circleSlug) await circleStore.loadCircle(props.learningPathSlug, props.circleSlug);
if (route.hash.startsWith('#ls-')) { if (route.hash.startsWith("#ls-")) {
const hashLearningSequence = circleStore.circle?.learningSequences.find((ls) => { const hashLearningSequence = circleStore.circle?.learningSequences.find((ls) => {
return ls.slug.endsWith(route.hash.replace('#', '')) return ls.slug.endsWith(route.hash.replace("#", ""));
}) });
if (hashLearningSequence) { if (hashLearningSequence) {
document.getElementById(hashLearningSequence.slug)?.scrollIntoView({ behavior: 'smooth' }) document
.getElementById(hashLearningSequence.slug)
?.scrollIntoView({ behavior: "smooth" });
} }
} }
} catch (error) { } catch (error) {
log.error(error) log.error(error);
} }
}) });
</script> </script>
<template> <template>
@ -89,7 +91,10 @@ onMounted(async () => {
</div> </div>
<div class="border-t-2 border-gray-500 mt-4 lg:hidden"> <div class="border-t-2 border-gray-500 mt-4 lg:hidden">
<div class="mt-4 inline-flex items-center" @click="circleStore.page = 'OVERVIEW'"> <div
class="mt-4 inline-flex items-center"
@click="circleStore.page = 'OVERVIEW'"
>
<it-icon-info class="mr-2" /> <it-icon-info class="mr-2" />
Das lernst du in diesem Circle Das lernst du in diesem Circle
</div> </div>
@ -106,7 +111,10 @@ onMounted(async () => {
{{ circleStore.circle?.description }} {{ circleStore.circle?.description }}
</div> </div>
<button class="btn-primary mt-4 text-xl" @click="circleStore.page = 'OVERVIEW'"> <button
class="btn-primary mt-4 text-xl"
@click="circleStore.page = 'OVERVIEW'"
>
Erfahre mehr dazu Erfahre mehr dazu
</button> </button>
</div> </div>
@ -116,17 +124,22 @@ onMounted(async () => {
<div class="leading-relaxed mt-4"> <div class="leading-relaxed mt-4">
Tausche dich mit der Fachexpertin aus für den Circle Analyse aus. Tausche dich mit der Fachexpertin aus für den Circle Analyse aus.
</div> </div>
<button class="btn-secondary mt-4 text-xl">Fachexpertin kontaktieren</button> <button class="btn-secondary mt-4 text-xl">
Fachexpertin kontaktieren
</button>
</div> </div>
</div> </div>
</div> </div>
<div class="flex-auto bg-gray-200 px-4 py-8 lg:px-24"> <div class="flex-auto bg-gray-200 px-4 py-8 lg:px-24">
<div <div
v-for="learningSequence in circleStore.circle?.learningSequences || []" v-for="learningSequence in circleStore.circle?.learningSequences ||
[]"
:key="learningSequence.translation_key" :key="learningSequence.translation_key"
> >
<LearningSequence :learning-sequence="learningSequence"></LearningSequence> <LearningSequence
:learning-sequence="learningSequence"
></LearningSequence>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import { useUserStore } from "@/stores/user";
import { useUserStore } from '@/stores/user' import * as log from "loglevel";
log.debug('CockpitView created') log.debug("CockpitView created");
const userStore = useUserStore() const userStore = useUserStore();
</script> </script>
<template> <template>
@ -16,7 +16,9 @@ const userStore = useUserStore()
<div class="mt-8 p-8 break-words bg-white max-w-xl"> <div class="mt-8 p-8 break-words bg-white max-w-xl">
<h3>Versicherungsvermittler/in</h3> <h3>Versicherungsvermittler/in</h3>
<div class="mt-4"> <div class="mt-4">
<router-link class="btn-blue" to="/learn/versicherungsvermittlerin-lp"> Weiter geht's </router-link> <router-link class="btn-blue" to="/learn/versicherungsvermittlerin-lp">
Weiter geht's
</router-link>
</div> </div>
</div> </div>
</main> </main>

View File

@ -1,8 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router'; import { useRouter } from "vue-router";
const router = useRouter()
const router = useRouter();
</script> </script>
<template> <template>
@ -10,9 +9,9 @@ const router = useRouter()
<div class="px-16 fixed top-0 overflow-y-scroll bg-white h-full w-full"> <div class="px-16 fixed top-0 overflow-y-scroll bg-white h-full w-full">
<div class="-mx-16 pt-4 pb-24 px-16 mb-20 bg-gray-200"> <div class="-mx-16 pt-4 pb-24 px-16 mb-20 bg-gray-200">
<nav> <nav>
<a <a class="block my-9 cursor-pointer flex items-center" @click="router.go(-1)"
class="block my-9 cursor-pointer flex items-center" ><it-icon-arrow-left /><span>zurück</span></a
@click="router.go(-1)"><it-icon-arrow-left /><span>zurück</span></a> >
</nav> </nav>
<slot name="header"></slot> <slot name="header"></slot>
</div> </div>
@ -23,7 +22,7 @@ const router = useRouter()
<style scoped> <style scoped>
.it-icon-hf { .it-icon-hf {
color: blue color: blue;
} }
.it-icon-hf > * { .it-icon-hf > * {
@apply m-auto; @apply m-auto;

View File

@ -1,25 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import LearningContent from "@/components/circle/LearningContent.vue";
import { onMounted, reactive, watch } from 'vue' import { useAppStore } from "@/stores/app";
import { useCircleStore } from '@/stores/circle' import { useCircleStore } from "@/stores/circle";
import { useAppStore } from '@/stores/app' import type { LearningContent as LearningContentType } from "@/types";
import LearningContent from '@/components/circle/LearningContent.vue' import * as log from "loglevel";
import type { LearningContent as LearningContentType } from '@/types' import { onMounted, reactive, watch } from "vue";
log.debug('LearningContentView created') log.debug("LearningContentView created");
const props = defineProps<{ const props = defineProps<{
learningPathSlug: string learningPathSlug: string;
circleSlug: string circleSlug: string;
contentSlug: string contentSlug: string;
}>() }>();
const state: { learningContent?: LearningContentType } = reactive({}) const state: { learningContent?: LearningContentType } = reactive({});
const appStore = useAppStore() const appStore = useAppStore();
appStore.showMainNavigationBar = false appStore.showMainNavigationBar = false;
const circleStore = useCircleStore() const circleStore = useCircleStore();
const loadLearningContent = async () => { const loadLearningContent = async () => {
try { try {
@ -27,33 +27,41 @@ const loadLearningContent = async () => {
props.learningPathSlug, props.learningPathSlug,
props.circleSlug, props.circleSlug,
props.contentSlug props.contentSlug
) );
} catch (error) { } catch (error) {
log.error(error) log.error(error);
} }
} };
watch( watch(
() => props.contentSlug, () => props.contentSlug,
async () => { async () => {
log.debug( log.debug(
'LearningContentView props.contentSlug changed', "LearningContentView props.contentSlug changed",
props.learningPathSlug, props.learningPathSlug,
props.circleSlug, props.circleSlug,
props.contentSlug props.contentSlug
) );
await loadLearningContent() await loadLearningContent();
} }
) );
onMounted(async () => { onMounted(async () => {
log.debug('LearningContentView mounted', props.learningPathSlug, props.circleSlug, props.contentSlug) log.debug(
await loadLearningContent() "LearningContentView mounted",
}) props.learningPathSlug,
props.circleSlug,
props.contentSlug
);
await loadLearningContent();
});
</script> </script>
<template> <template>
<LearningContent v-if="state.learningContent" :learning-content="state.learningContent" /> <LearningContent
v-if="state.learningContent"
:learning-content="state.learningContent"
/>
</template> </template>
<style lang="postcss" scoped></style> <style lang="postcss" scoped></style>

View File

@ -1,55 +1,59 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import * as log from "loglevel";
import { onMounted } from 'vue' import { useLearningPathStore } from "@/stores/learningPath";
import { useLearningPathStore } from '@/stores/learningPath' import { useUserStore } from "@/stores/user";
import { useUserStore } from '@/stores/user' import { onMounted } from "vue";
import LearningPathDiagram from '@/components/circle/LearningPathDiagram.vue' import LearningPathDiagram from "@/components/circle/LearningPathDiagram.vue";
import LearningPathViewVertical from '@/views/LearningPathViewVertical.vue' import type { LearningPath } from "@/services/learningPath";
import type { LearningPath } from '@/services/learningPath' import LearningPathViewVertical from "@/views/LearningPathViewVertical.vue";
log.debug('LearningPathView created') log.debug("LearningPathView created");
const props = defineProps<{ const props = defineProps<{
learningPathSlug: string learningPathSlug: string;
}>() }>();
const learningPathStore = useLearningPathStore() const learningPathStore = useLearningPathStore();
const userStore = useUserStore() const userStore = useUserStore();
onMounted(async () => { onMounted(async () => {
log.debug('LearningPathView mounted') log.debug("LearningPathView mounted");
try { try {
await learningPathStore.loadLearningPath(props.learningPathSlug) await learningPathStore.loadLearningPath(props.learningPathSlug);
} catch (error) { } catch (error) {
log.error(error) log.error(error);
} }
}) });
const createContinueUrl = (learningPath: LearningPath): [string, boolean] => { const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
if (learningPath.nextLearningContent) { if (learningPath.nextLearningContent) {
const circle = learningPath.nextLearningContent.parentCircle const circle = learningPath.nextLearningContent.parentCircle;
const lsShortSlug = learningPath.nextLearningContent.parentLearningSequence?.slug.replace(`${circle.slug}-`, '') const lsShortSlug =
const url = `/learn/${learningPath.slug}/${learningPath.nextLearningContent.parentCircle.slug}#${lsShortSlug}` learningPath.nextLearningContent.parentLearningSequence?.slug.replace(
`${circle.slug}-`,
""
);
const url = `/learn/${learningPath.slug}/${learningPath.nextLearningContent.parentCircle.slug}#${lsShortSlug}`;
const isFirst = const isFirst =
learningPath.nextLearningContent.translation_key === learningPath.nextLearningContent.translation_key ===
learningPath.circles[0].flatLearningContents[0].translation_key learningPath.circles[0].flatLearningContents[0].translation_key;
return [url, isFirst] return [url, isFirst];
} }
return ['', false] return ["", false];
} };
</script> </script>
<template> <template>
<div class="bg-gray-200" v-if="learningPathStore.learningPath"> <div v-if="learningPathStore.learningPath" class="bg-gray-200">
<Teleport to="body"> <Teleport to="body">
<LearningPathViewVertical <LearningPathViewVertical
:show="learningPathStore.page === 'OVERVIEW'" :show="learningPathStore.page === 'OVERVIEW'"
:learning-path-slug="props.learningPathSlug"
@closemodal="learningPathStore.page = 'INDEX'" @closemodal="learningPathStore.page = 'INDEX'"
v-bind:learning-path-slug="props.learningPathSlug"
/> />
</Teleport> </Teleport>
@ -57,7 +61,11 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
<div class="flex flex-col h-max"> <div class="flex flex-col h-max">
<div class="bg-white py-8 flex flex-col"> <div class="bg-white py-8 flex flex-col">
<div class="flex justify-end p-3"> <div class="flex justify-end p-3">
<button class="flex items-center" @click="learningPathStore.page = 'OVERVIEW'" data-cy="show-list-view"> <button
class="flex items-center"
data-cy="show-list-view"
@click="learningPathStore.page = 'OVERVIEW'"
>
<it-icon-list /> <it-icon-list />
Listenansicht anzeigen Listenansicht anzeigen
</button> </button>
@ -65,7 +73,7 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
<LearningPathDiagram <LearningPathDiagram
class="max-w-[1680px] w-full" class="max-w-[1680px] w-full"
identifier="mainVisualization" identifier="mainVisualization"
v-bind:vertical="false" :vertical="false"
></LearningPathDiagram> ></LearningPathDiagram>
</div> </div>
@ -80,11 +88,19 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
<h2>Willkommmen zurück, {{ userStore.first_name }}</h2> <h2>Willkommmen zurück, {{ userStore.first_name }}</h2>
<p class="mt-4 text-xl"></p> <p class="mt-4 text-xl"></p>
</div> </div>
<div class="p-4 lg:p-8 flex-2" v-if="learningPathStore.learningPath.nextLearningContent"> <div
v-if="learningPathStore.learningPath.nextLearningContent"
class="p-4 lg:p-8 flex-2"
>
Nächster Schritt Nächster Schritt
<h3> <h3>
{{ learningPathStore.learningPath.nextLearningContent.parentCircle.title }}: {{
{{ learningPathStore.learningPath.nextLearningContent.parentLearningSequence?.title }} learningPathStore.learningPath.nextLearningContent.parentCircle.title
}}:
{{
learningPathStore.learningPath.nextLearningContent
.parentLearningSequence?.title
}}
</h3> </h3>
<router-link <router-link
class="mt-4 btn-blue" class="mt-4 btn-blue"
@ -92,7 +108,9 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
data-cy="lp-continue-button" data-cy="lp-continue-button"
translate translate
> >
<span v-if="createContinueUrl(learningPathStore.learningPath)[1]"> Los geht's </span> <span v-if="createContinueUrl(learningPathStore.learningPath)[1]">
Los geht's
</span>
<span v-else>Weiter geht's</span> <span v-else>Weiter geht's</span>
</router-link> </router-link>
</div> </div>

View File

@ -1,33 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import { useLearningPathStore } from "@/stores/learningPath";
import { useLearningPathStore } from '@/stores/learningPath' import { useUserStore } from "@/stores/user";
import { useUserStore } from '@/stores/user' import * as log from "loglevel";
import LearningPathDiagram from '@/components/circle/LearningPathDiagram.vue' import LearningPathDiagram from "@/components/circle/LearningPathDiagram.vue";
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue' import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
log.debug('LearningPathView created') log.debug("LearningPathView created");
const props = defineProps<{ const props = defineProps<{
learningPathSlug: string learningPathSlug: string;
show: boolean show: boolean;
}>() }>();
const learningPathStore = useLearningPathStore() const learningPathStore = useLearningPathStore();
const userStore = useUserStore() const userStore = useUserStore();
const emits = defineEmits(['closemodal']) const emits = defineEmits(["closemodal"]);
</script> </script>
<template> <template>
<ItFullScreenModal :show="show" @closemodal="$emit('closemodal')"> <ItFullScreenModal :show="show" @closemodal="$emit('closemodal')">
<div class="container-medium" v-if="learningPathStore.learningPath"> <div v-if="learningPathStore.learningPath" class="container-medium">
<h1>{{ learningPathStore.learningPath.title }}</h1> <h1>{{ learningPathStore.learningPath.title }}</h1>
<div class="learningpath flex flex-col"> <div class="learningpath flex flex-col">
<div class="flex flex-col h-max"> <div class="flex flex-col h-max">
<LearningPathDiagram <LearningPathDiagram
class="w-full" class="w-full"
identifier="verticalVisualization" identifier="verticalVisualization"
v-bind:vertical="true" :vertical="true"
></LearningPathDiagram> ></LearningPathDiagram>
</div> </div>
</div> </div>

View File

@ -1,41 +1,46 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import * as log from "loglevel";
import SelfEvaluation from '@/components/circle/SelfEvaluation.vue' import SelfEvaluation from "@/components/circle/SelfEvaluation.vue";
import { onMounted, reactive } from 'vue' import { useAppStore } from "@/stores/app";
import { useAppStore } from '@/stores/app' import { useCircleStore } from "@/stores/circle";
import { useCircleStore } from '@/stores/circle' import type { LearningUnit } from "@/types";
import type { LearningUnit } from '@/types' import { onMounted, reactive } from "vue";
log.debug('LearningUnitSelfEvaluationView created') log.debug("LearningUnitSelfEvaluationView created");
const props = defineProps<{ const props = defineProps<{
learningPathSlug: string learningPathSlug: string;
circleSlug: string circleSlug: string;
learningUnitSlug: string learningUnitSlug: string;
}>() }>();
const appStore = useAppStore() const appStore = useAppStore();
appStore.showMainNavigationBar = false appStore.showMainNavigationBar = false;
const circleStore = useCircleStore() const circleStore = useCircleStore();
const state: { learningUnit?: LearningUnit } = reactive({}) const state: { learningUnit?: LearningUnit } = reactive({});
onMounted(async () => { onMounted(async () => {
log.debug('LearningUnitSelfEvaluationView mounted', props.learningPathSlug, props.circleSlug, props.learningUnitSlug) log.debug(
"LearningUnitSelfEvaluationView mounted",
props.learningPathSlug,
props.circleSlug,
props.learningUnitSlug
);
try { try {
state.learningUnit = await circleStore.loadSelfEvaluation( state.learningUnit = await circleStore.loadSelfEvaluation(
props.learningPathSlug, props.learningPathSlug,
props.circleSlug, props.circleSlug,
props.learningUnitSlug props.learningUnitSlug
) );
} catch (error) { } catch (error) {
log.error(error) log.error(error);
} }
}) });
</script> </script>
<template> <template>

View File

@ -1,34 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import { useUserStore } from "@/stores/user";
import { reactive } from 'vue' import * as log from "loglevel";
import { useUserStore } from '@/stores/user' import { reactive } from "vue";
import { useRoute } from 'vue-router' import { useRoute } from "vue-router";
const route = useRoute() const route = useRoute();
log.debug('LoginView.vue created') log.debug("LoginView.vue created");
log.debug(route.query) log.debug(route.query);
const state = reactive({ const state = reactive({
username: '', username: "",
password: '', password: "",
}) });
const userStore = useUserStore() const userStore = useUserStore();
</script> </script>
<template> <template>
<main class="px-8 py-8"> <main class="px-8 py-8">
<h1>Login</h1> <h1>Login</h1>
<form @submit.prevent="userStore.handleLogin(state.username, state.password, route.query.next)"> <form
@submit.prevent="
userStore.handleLogin(state.username, state.password, route.query.next)
"
>
<div class="mt-8 mb-4"> <div class="mt-8 mb-4">
<label class="block mb-1" for="email">Username</label> <label class="block mb-1" for="email">Username</label>
<input <input
id="username" id="username"
v-model="state.username"
type="text" type="text"
name="username" name="username"
v-model="state.username"
class="py-2 px-3 border border-gray-500 mt-1 block w-96" class="py-2 px-3 border border-gray-500 mt-1 block w-96"
/> />
</div> </div>
@ -36,9 +40,9 @@ const userStore = useUserStore()
<label class="block mb-1" for="password">Password</label> <label class="block mb-1" for="password">Password</label>
<input <input
id="password" id="password"
v-model="state.password"
type="password" type="password"
name="password" name="password"
v-model="state.password"
class="py-2 px-3 border border-gray-500 mt-1 block w-96" class="py-2 px-3 border border-gray-500 mt-1 block w-96"
/> />
</div> </div>

View File

@ -1,195 +1,205 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import LinkCard from "@/components/mediaCenter/LinkCard.vue";
import LinkCard from '@/components/mediaCenter/LinkCard.vue' import MediaLink from "@/components/mediaCenter/MediaLink.vue";
import HandlungsfeldLayout from '@/views/HandlungsfeldLayout.vue' import { useMediaCenterStore } from "@/stores/mediaCenter";
import MediaLink from '@/components/mediaCenter/MediaLink.vue' import HandlungsfeldLayout from "@/views/HandlungsfeldLayout.vue";
import { useMediaCenterStore } from '@/stores/mediaCenter' import * as log from "loglevel";
import { computed } from 'vue' import { computed } from "vue";
const field = { const field = {
title: 'Fahrzeug', title: "Fahrzeug",
description: description:
'Das Auto ist für viele der grösste Stolz! Es birgt aber auch ein grosses Gefahrenpotenzial. Dabei geht es bei den heutigen Fahrzeugpreisen und Reparaturkosten rasch um namhafte Summen, die der Fahrzeugbesitzer und die Fahrzeugbesitzerin in einem grösseren Schadenfall oft nur schwer selbst aufbringen kann.', "Das Auto ist für viele der grösste Stolz! Es birgt aber auch ein grosses Gefahrenpotenzial. Dabei geht es bei den heutigen Fahrzeugpreisen und Reparaturkosten rasch um namhafte Summen, die der Fahrzeugbesitzer und die Fahrzeugbesitzerin in einem grösseren Schadenfall oft nur schwer selbst aufbringen kann.",
icon: '/static/icons/demo/icon-hf-fahrzeug-big.svg', icon: "/static/icons/demo/icon-hf-fahrzeug-big.svg",
summary: { summary: {
text: 'In diesem berufstypischem Handlungsfeld lernst du alles rund um Motorfahrzeugversicherungen, wie man sein Auto optimal schützen kann, wie du vorgehst bei einem Fahrzeugwechsel, welche Aspekte du bei einer Offerte beachten musst und wie du dem Kunden die Lösung präsentierst.', text: "In diesem berufstypischem Handlungsfeld lernst du alles rund um Motorfahrzeugversicherungen, wie man sein Auto optimal schützen kann, wie du vorgehst bei einem Fahrzeugwechsel, welche Aspekte du bei einer Offerte beachten musst und wie du dem Kunden die Lösung präsentierst.",
items: ['Motorfahrzeughaftpflichtversicherung', 'Motorfahrzeugkaskoversicherung', 'Insassenunfallversicherung'], items: [
"Motorfahrzeughaftpflichtversicherung",
"Motorfahrzeugkaskoversicherung",
"Insassenunfallversicherung",
],
}, },
items: [ items: [
{ {
title: 'Lernmedien', title: "Lernmedien",
type: 'learnmedia', type: "learnmedia",
moreLink: '', moreLink: "",
items: [ items: [
{ {
title: 'Die Motorfahrzeughaftpflicht', title: "Die Motorfahrzeughaftpflicht",
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16', description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
iconUrl: '/static/icons/demo/icon-hf-book.png', iconUrl: "/static/icons/demo/icon-hf-book.png",
linkText: 'PDF anzeigen', linkText: "PDF anzeigen",
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf', link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true, openWindow: true,
}, },
{ {
title: 'Die Motorfahrzeughaftpflicht', title: "Die Motorfahrzeughaftpflicht",
iconUrl: '/static/icons/demo/icon-hf-book.png', iconUrl: "/static/icons/demo/icon-hf-book.png",
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16', description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: 'PDF anzeigen', linkText: "PDF anzeigen",
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf', link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true, openWindow: true,
}, },
{ {
title: 'Die Motorfahrzeughaftpflicht', title: "Die Motorfahrzeughaftpflicht",
iconUrl: '/static/icons/demo/icon-hf-book.png', iconUrl: "/static/icons/demo/icon-hf-book.png",
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16', description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: 'PDF anzeigen', linkText: "PDF anzeigen",
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf', link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true, openWindow: true,
}, },
{ {
title: 'Die Motorfahrzeughaftpflicht', title: "Die Motorfahrzeughaftpflicht",
iconUrl: '/static/icons/demo/icon-hf-book.png', iconUrl: "/static/icons/demo/icon-hf-book.png",
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16', description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: 'PDF anzeigen', linkText: "PDF anzeigen",
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf', link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true, openWindow: true,
}, },
{ {
title: 'Die Motorfahrzeughaftpflicht', title: "Die Motorfahrzeughaftpflicht",
iconUrl: '/static/icons/demo/icon-hf-book.png', iconUrl: "/static/icons/demo/icon-hf-book.png",
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16', description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: 'PDF anzeigen', linkText: "PDF anzeigen",
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf', link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true, openWindow: true,
}, },
], ],
}, },
{ {
title: 'Links', title: "Links",
type: 'externalLinks', type: "externalLinks",
moreLink: '', moreLink: "",
items: [ items: [
{ {
title: 'Nationales Versicherungsbüro', title: "Nationales Versicherungsbüro",
iconUrl: '', iconUrl: "",
description: '', description: "",
linkText: 'Link öffnen', linkText: "Link öffnen",
link: 'https://www.nbi-ngf.ch/h', link: "https://www.nbi-ngf.ch/h",
openWindow: true, openWindow: true,
}, },
{ {
title: 'Adressen der Strassenverkehrsämter', title: "Adressen der Strassenverkehrsämter",
iconUrl: '', iconUrl: "",
description: '', description: "",
linkText: 'Link öffnen', linkText: "Link öffnen",
link: 'https://asa.ch/strassenverkehrsaemter/adressen/', link: "https://asa.ch/strassenverkehrsaemter/adressen/",
openWindow: true, openWindow: true,
}, },
], ],
}, },
{ {
title: 'Verankerung im Lernpfad', title: "Verankerung im Lernpfad",
type: 'internalLinks', type: "internalLinks",
moreLink: '', moreLink: "",
items: [ items: [
{ {
title: 'Circle: Einstieg Lernsequenz: Anwenden', title: "Circle: Einstieg Lernsequenz: Anwenden",
iconUrl: '', iconUrl: "",
description: '', description: "",
linkText: 'Lerineinheit anzeigen', linkText: "Lerineinheit anzeigen",
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse', link: "http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse",
openWindow: false, openWindow: false,
}, },
{ {
title: 'Circle: Einstieg Lernsequenz: Anwenden', title: "Circle: Einstieg Lernsequenz: Anwenden",
iconUrl: '', iconUrl: "",
description: '', description: "",
linkText: 'Lerineinheit anzeigen', linkText: "Lerineinheit anzeigen",
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse', link: "http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse",
openWindow: false, openWindow: false,
}, },
{ {
title: 'Circle: Einstieg Lernsequenz: Anwenden', title: "Circle: Einstieg Lernsequenz: Anwenden",
iconUrl: '', iconUrl: "",
description: '', description: "",
linkText: 'Lerineinheit anzeigen', linkText: "Lerineinheit anzeigen",
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse', link: "http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse",
openWindow: false, openWindow: false,
}, },
{ {
title: 'Circle: Einstieg Lernsequenz: Anwenden', title: "Circle: Einstieg Lernsequenz: Anwenden",
iconUrl: '', iconUrl: "",
description: '', description: "",
linkText: 'Lerineinheit anzeigen', linkText: "Lerineinheit anzeigen",
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse', link: "http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse",
openWindow: false, openWindow: false,
}, },
], ],
}, },
{ {
title: 'Querverweise', title: "Querverweise",
type: 'realtiveLinks', type: "realtiveLinks",
moreLink: '', moreLink: "",
items: [ items: [
{ {
title: 'Rechtsstreigkeiten', title: "Rechtsstreigkeiten",
iconUrl: '/static/icons/demo/icon-hf-einkommenssicherung.svg', iconUrl: "/static/icons/demo/icon-hf-einkommenssicherung.svg",
description: 'Lernmedium: Verkehrsrechtsschutz Buch «Sach- und Vermögensversicherungen/Kapitel 12.3»', description:
linkText: 'Handlungsfeldanzeigen', "Lernmedium: Verkehrsrechtsschutz Buch «Sach- und Vermögensversicherungen/Kapitel 12.3»",
link: 'http://localhost:8000/mediacenter/handlungsfeld', linkText: "Handlungsfeldanzeigen",
link: "http://localhost:8000/mediacenter/handlungsfeld",
openWindow: false, openWindow: false,
}, },
{ {
title: 'Rechtsstreigkeiten', title: "Rechtsstreigkeiten",
iconUrl: '/static/icons/demo/icon-hf-einkommenssicherung.svg', iconUrl: "/static/icons/demo/icon-hf-einkommenssicherung.svg",
description: 'Lernmedium: Verkehrsrechtsschutz Buch «Sach- und Vermögensversicherungen/Kapitel 12.3»', description:
linkText: 'Handlungsfeldanzeigen', "Lernmedium: Verkehrsrechtsschutz Buch «Sach- und Vermögensversicherungen/Kapitel 12.3»",
link: 'http://localhost:8000/mediacenter/handlungsfeld', linkText: "Handlungsfeldanzeigen",
link: "http://localhost:8000/mediacenter/handlungsfeld",
openWindow: false, openWindow: false,
}, },
], ],
}, },
], ],
} };
const props = defineProps<{ const props = defineProps<{
mediaCategorySlug: string mediaCategorySlug: string;
}>() }>();
log.debug('MediaCategoryDetailView created', props.mediaCategorySlug) log.debug("MediaCategoryDetailView created", props.mediaCategorySlug);
const mediaStore = useMediaCenterStore() const mediaStore = useMediaCenterStore();
const mediaCategory = computed(() => { const mediaCategory = computed(() => {
return mediaStore.mediaCenterPage?.children.find((category) => category.slug === props.mediaCategorySlug) return mediaStore.mediaCenterPage?.children.find(
}) (category) => category.slug === props.mediaCategorySlug
);
});
const maxCardItems = 4 const maxCardItems = 4;
const maxListItems = 6 const maxListItems = 6;
const displayAsCard = (itemType: string): boolean => { const displayAsCard = (itemType: string): boolean => {
return itemType === 'learnmedia' || itemType === 'realtiveLinks' return itemType === "learnmedia" || itemType === "realtiveLinks";
} };
const hasMoreItems = (items: object[], maxItems: number): boolean => { const hasMoreItems = (items: object[], maxItems: number): boolean => {
return items.length > maxItems return items.length > maxItems;
} };
const getMaxDisplayItems = (items: object[], maxItems: number) => { const getMaxDisplayItems = (items: object[], maxItems: number) => {
return items.slice(0, maxItems) return items.slice(0, maxItems);
} };
const getMaxDisplayItemsForType = (itemType: string, items: object[]) => { const getMaxDisplayItemsForType = (itemType: string, items: object[]) => {
return displayAsCard(itemType) ? getMaxDisplayItems(items, maxCardItems) : getMaxDisplayItems(items, maxListItems) return displayAsCard(itemType)
} ? getMaxDisplayItems(items, maxCardItems)
: getMaxDisplayItems(items, maxListItems);
};
const hasMoreItemsForType = (itemType: string, items: object[]) => { const hasMoreItemsForType = (itemType: string, items: object[]) => {
const maxItems = displayAsCard(itemType) ? maxCardItems : maxListItems const maxItems = displayAsCard(itemType) ? maxCardItems : maxListItems;
return hasMoreItems(items, maxItems) return hasMoreItems(items, maxItems);
} };
</script> </script>
<template> <template>
<Teleport to="body" v-if="mediaStore.mediaCenterPage && mediaCategory"> <Teleport v-if="mediaStore.mediaCenterPage && mediaCategory" to="body">
<HandlungsfeldLayout> <HandlungsfeldLayout>
<template #header> <template #header>
<div class="flex justify-between"> <div class="flex justify-between">
@ -206,13 +216,19 @@ const hasMoreItemsForType = (itemType: string, items: object[]) => {
<h2 class="mb-4">{{ mediaCategory.description_title }}</h2> <h2 class="mb-4">{{ mediaCategory.description_title }}</h2>
<p class="mb-4 lg:w-2/3">{{ mediaCategory.description_text }}</p> <p class="mb-4 lg:w-2/3">{{ mediaCategory.description_text }}</p>
<ul> <ul>
<li v-for="item in mediaCategory.items" :key="item" class="mb-2 h-10 leading-10 flex items-center"> <li
<span class="text-sky-500 bg-[url('/static/icons/icon-check.svg')] bg-no-repeat h-10 w-10 mr-2"></span> v-for="item in mediaCategory.items"
:key="item"
class="mb-2 h-10 leading-10 flex items-center"
>
<span
class="text-sky-500 bg-[url('/static/icons/icon-check.svg')] bg-no-repeat h-10 w-10 mr-2"
></span>
{{ item.value }} {{ item.value }}
</li> </li>
</ul> </ul>
</section> </section>
<section class="mb-20" v-for="item in field.items" :key="item.title"> <section v-for="item in field.items" :key="item.title" class="mb-20">
<h2 class="mb-4">{{ item.title }}</h2> <h2 class="mb-4">{{ item.title }}</h2>
<ul <ul
:class="{ :class="{
@ -221,7 +237,10 @@ const hasMoreItemsForType = (itemType: string, items: object[]) => {
'mb-6': hasMoreItemsForType(item.type, item.items), 'mb-6': hasMoreItemsForType(item.type, item.items),
}" }"
> >
<li v-for="subItem in getMaxDisplayItemsForType(item.type, item.items)" :key="subItem.link"> <li
v-for="subItem in getMaxDisplayItemsForType(item.type, item.items)"
:key="subItem.link"
>
<LinkCard <LinkCard
v-if="displayAsCard(item.type)" v-if="displayAsCard(item.type)"
:title="subItem.title" :title="subItem.title"
@ -233,9 +252,12 @@ const hasMoreItemsForType = (itemType: string, items: object[]) => {
/> />
<div v-else class="flex items-center justify-between border-b py-4"> <div v-else class="flex items-center justify-between border-b py-4">
<h4 class="text-bold">{{ subItem.title }}</h4> <h4 class="text-bold">{{ subItem.title }}</h4>
<media-link :blank="subItem.openWindow" :to="subItem.link" class="link">{{ <media-link
subItem.linkText :blank="subItem.openWindow"
}}</media-link> :to="subItem.link"
class="link"
>{{ subItem.linkText }}</media-link
>
</div> </div>
</li> </li>
</ul> </ul>

View File

@ -1,69 +1,69 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import { useMediaCenterStore } from "@/stores/mediaCenter";
import { ref, watch } from 'vue' import * as log from "loglevel";
import { useMediaCenterStore } from '@/stores/mediaCenter' import { ref, watch } from "vue";
log.debug('HandlungsfelderOverview created') log.debug("HandlungsfelderOverview created");
const fields = [ const fields = [
{ {
name: 'Fahrzeug', name: "Fahrzeug",
icon: 'icon-hf-fahrzeug', icon: "icon-hf-fahrzeug",
}, },
{ {
name: 'Reisen', name: "Reisen",
icon: 'icon-hf-reisen', icon: "icon-hf-reisen",
}, },
{ {
name: 'Einkommenssicherung', name: "Einkommenssicherung",
icon: 'icon-hf-einkommenssicherung', icon: "icon-hf-einkommenssicherung",
}, },
{ {
name: 'Gesundheit', name: "Gesundheit",
icon: 'icon-hf-fahrzeug', icon: "icon-hf-fahrzeug",
}, },
{ {
name: 'Haushalt', name: "Haushalt",
icon: 'icon-hf-reisen', icon: "icon-hf-reisen",
}, },
{ {
name: 'Sparen', name: "Sparen",
icon: 'icon-hf-einkommenssicherung', icon: "icon-hf-einkommenssicherung",
}, },
{ {
name: 'Pensionierung', name: "Pensionierung",
icon: 'icon-hf-fahrzeug', icon: "icon-hf-fahrzeug",
}, },
{ {
name: 'KMU', name: "KMU",
icon: 'icon-hf-reisen', icon: "icon-hf-reisen",
}, },
{ {
name: 'Wohneigentum', name: "Wohneigentum",
icon: 'icon-hf-einkommenssicherung', icon: "icon-hf-einkommenssicherung",
}, },
{ {
name: 'Rechtsstreitigkeiten', name: "Rechtsstreitigkeiten",
icon: 'icon-hf-fahrzeug', icon: "icon-hf-fahrzeug",
}, },
{ {
name: 'Erben / Vererben', name: "Erben / Vererben",
icon: 'icon-hf-reisen', icon: "icon-hf-reisen",
}, },
{ {
name: 'Selbstständigkeit', name: "Selbstständigkeit",
icon: 'icon-hf-einkommenssicherung', icon: "icon-hf-einkommenssicherung",
}, },
] ];
const mediaStore = useMediaCenterStore() const mediaStore = useMediaCenterStore();
const dropdownSelected = ref(mediaStore.selectedLearningPath) const dropdownSelected = ref(mediaStore.selectedLearningPath);
watch(dropdownSelected, (newValue) => watch(dropdownSelected, (newValue) =>
mediaStore.$patch({ mediaStore.$patch({
selectedLearningPath: newValue, selectedLearningPath: newValue,
}) })
) );
</script> </script>
<template> <template>
@ -74,8 +74,14 @@ watch(dropdownSelected, (newValue) =>
</div> </div>
<div v-if="mediaStore.mediaCenterPage"> <div v-if="mediaStore.mediaCenterPage">
<ul class="grid gap-5 grid-cols-1 lg:grid-cols-4"> <ul class="grid gap-5 grid-cols-1 lg:grid-cols-4">
<li class="bg-white p-4" v-for="cat in mediaStore.mediaCenterPage.children" :key="cat.id"> <li
<router-link :to="`/mediacenter/${mediaStore.mediaCenterPage.slug}/handlungsfelder/${cat.slug}`"> v-for="cat in mediaStore.mediaCenterPage.children"
:key="cat.id"
class="bg-white p-4"
>
<router-link
:to="`/mediacenter/${mediaStore.mediaCenterPage.slug}/handlungsfelder/${cat.slug}`"
>
<img class="m-auto" :src="`/static/icons/demo/${cat.overview_icon}.svg`" /> <img class="m-auto" :src="`/static/icons/demo/${cat.overview_icon}.svg`" />
<h3 class="text-base text-center">{{ cat.title }}</h3> <h3 class="text-base text-center">{{ cat.title }}</h3>
</router-link> </router-link>

View File

@ -1,19 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import OverviewCard from "@/components/mediaCenter/OverviewCard.vue";
import OverviewCard from '@/components/mediaCenter/OverviewCard.vue' import { useMediaCenterStore } from "@/stores/mediaCenter";
import { ref, watch } from 'vue' import * as log from "loglevel";
import { useMediaCenterStore } from '@/stores/mediaCenter' import { ref, watch } from "vue";
log.debug('MediaMainView created') log.debug("MediaMainView created");
const mediaStore = useMediaCenterStore() const mediaStore = useMediaCenterStore();
const dropdownSelected = ref(mediaStore.selectedLearningPath) const dropdownSelected = ref(mediaStore.selectedLearningPath);
watch(dropdownSelected, (newValue) => watch(dropdownSelected, (newValue) =>
mediaStore.$patch({ mediaStore.$patch({
selectedLearningPath: newValue, selectedLearningPath: newValue,
}) })
) );
</script> </script>
<template> <template>

View File

@ -1,25 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel' import { useMediaCenterStore } from "@/stores/mediaCenter";
import { onMounted } from 'vue' import * as log from "loglevel";
import { useMediaCenterStore } from '@/stores/mediaCenter' import { onMounted } from "vue";
log.debug('MediaCenterView created') log.debug("MediaCenterView created");
const props = defineProps<{ const props = defineProps<{
mediaCenterPageSlug: string mediaCenterPageSlug: string;
}>() }>();
const mediaCenterStore = useMediaCenterStore() const mediaCenterStore = useMediaCenterStore();
onMounted(async () => { onMounted(async () => {
log.debug('MediaCenterView mounted', props.mediaCenterPageSlug) log.debug("MediaCenterView mounted", props.mediaCenterPageSlug);
try { try {
await mediaCenterStore.loadMediaCenterPage(props.mediaCenterPageSlug) await mediaCenterStore.loadMediaCenterPage(props.mediaCenterPageSlug);
} catch (error) { } catch (error) {
log.error(error) log.error(error);
} }
}) });
</script> </script>
<template> <template>
@ -30,7 +30,9 @@ onMounted(async () => {
<li class="ml-10">Handlungsfelder</li> <li class="ml-10">Handlungsfelder</li>
<li class="ml-10">Allgemeines zu Versicherungen</li> <li class="ml-10">Allgemeines zu Versicherungen</li>
<li class="ml-10">Lernmedien</li> <li class="ml-10">Lernmedien</li>
<li class="ml-10"><a href="https://www.vbv.ch/de/der-vbv/lernen-lehren/lexikon">Lexikon</a></li> <li class="ml-10">
<a href="https://www.vbv.ch/de/der-vbv/lernen-lehren/lexikon">Lexikon</a>
</li>
</ul> </ul>
</nav> </nav>
<main class="px-8 py-8"> <main class="px-8 py-8">

View File

@ -1,52 +1,52 @@
<script setup lang="ts"> <script setup lang="ts">
import HandlungsfeldLayout from '@/views/HandlungsfeldLayout.vue' import MediaLink from "@/components/mediaCenter/MediaLink.vue";
import MediaLink from '@/components/mediaCenter/MediaLink.vue' import HandlungsfeldLayout from "@/views/HandlungsfeldLayout.vue";
const data = { const data = {
title: 'Fahrzeug: Lernmedien', title: "Fahrzeug: Lernmedien",
items: [ items: [
{ {
title: 'Die Motorfahrzeughaftpflicht', title: "Die Motorfahrzeughaftpflicht",
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16', description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
iconUrl: '/static/icons/demo/icon-hf-book.png', iconUrl: "/static/icons/demo/icon-hf-book.png",
linkText: 'PDF anzeigen', linkText: "PDF anzeigen",
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf', link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true, openWindow: true,
}, },
{ {
title: 'Die Motorfahrzeughaftpflicht', title: "Die Motorfahrzeughaftpflicht",
iconUrl: '/static/icons/demo/icon-hf-book.png', iconUrl: "/static/icons/demo/icon-hf-book.png",
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16', description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: 'PDF anzeigen', linkText: "PDF anzeigen",
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf', link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true, openWindow: true,
}, },
{ {
title: 'Die Motorfahrzeughaftpflicht', title: "Die Motorfahrzeughaftpflicht",
iconUrl: '/static/icons/demo/icon-hf-book.png', iconUrl: "/static/icons/demo/icon-hf-book.png",
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16', description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: 'PDF anzeigen', linkText: "PDF anzeigen",
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf', link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true, openWindow: true,
}, },
{ {
title: 'Die Motorfahrzeughaftpflicht', title: "Die Motorfahrzeughaftpflicht",
iconUrl: '/static/icons/demo/icon-hf-book.png', iconUrl: "/static/icons/demo/icon-hf-book.png",
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16', description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: 'PDF anzeigen', linkText: "PDF anzeigen",
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf', link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true, openWindow: true,
}, },
{ {
title: 'Die Motorfahrzeughaftpflicht', title: "Die Motorfahrzeughaftpflicht",
iconUrl: '/static/icons/demo/icon-hf-book.png', iconUrl: "/static/icons/demo/icon-hf-book.png",
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16', description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: 'PDF anzeigen', linkText: "PDF anzeigen",
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf', link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true, openWindow: true,
}, },
], ],
} };
</script> </script>
<template> <template>
@ -58,7 +58,11 @@ const data = {
<template #body> <template #body>
<section class="mb-20"> <section class="mb-20">
<ul class="border-t"> <ul class="border-t">
<li v-for="item in data.items" :key="item.link" class="flex items-center justify-between border-b py-4"> <li
v-for="item in data.items"
:key="item.link"
class="flex items-center justify-between border-b py-4"
>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div v-if="item.iconUrl"> <div v-if="item.iconUrl">
<img class="mr-6 max-h-[70px]" :src="item.iconUrl" /> <img class="mr-6 max-h-[70px]" :src="item.iconUrl" />
@ -69,7 +73,9 @@ const data = {
</div> </div>
</div> </div>
<div class=""> <div class="">
<media-link :to="item.link" :blank="item.openWindow" class="link">{{ item.linkText }}</media-link> <media-link :to="item.link" :blank="item.openWindow" class="link">{{
item.linkText
}}</media-link>
</div> </div>
</li> </li>
</ul> </ul>

View File

@ -1,8 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel'; import * as log from "loglevel";
log.debug('MessagesView created');
log.debug("MessagesView created");
</script> </script>
<template> <template>
@ -11,5 +10,4 @@ log.debug('MessagesView created');
</main> </main>
</template> </template>
<style scoped> <style scoped></style>
</style>

View File

@ -1,8 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel'; import * as log from "loglevel";
log.debug('ProfileView created');
log.debug("ProfileView created");
</script> </script>
<template> <template>
@ -11,5 +10,4 @@ log.debug('ProfileView created');
</main> </main>
</template> </template>
<style scoped> <style scoped></style>
</style>

View File

@ -1,8 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel'; import * as log from "loglevel";
log.debug('ShopView created');
log.debug("ShopView created");
</script> </script>
<template> <template>
@ -11,5 +10,4 @@ log.debug('ShopView created');
</main> </main>
</template> </template>
<style scoped> <style scoped></style>
</style>

View File

@ -1,40 +1,40 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive } from 'vue' import IconLogout from "@/components/icons/IconLogout.vue";
import ItCheckbox from '@/components/ui/ItCheckbox.vue' import IconSettings from "@/components/icons/IconSettings.vue";
import ItDropdown from '@/components/ui/ItDropdown.vue' import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import IconLogout from '@/components/icons/IconLogout.vue' import ItDropdown from "@/components/ui/ItDropdown.vue";
import IconSettings from '@/components/icons/IconSettings.vue' import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import ItDropdownSelect from '@/components/ui/ItDropdownSelect.vue' import { reactive } from "vue";
const state = reactive({ const state = reactive({
checkboxValue: true, checkboxValue: true,
dropdownValues: [ dropdownValues: [
{ id: 1, name: 'Wade Cooper' }, { id: 1, name: "Wade Cooper" },
{ id: 2, name: 'Arlene Mccoy' }, { id: 2, name: "Arlene Mccoy" },
{ id: 3, name: 'Devon Webb' }, { id: 3, name: "Devon Webb" },
{ id: 4, name: 'Tom Cook' }, { id: 4, name: "Tom Cook" },
{ id: 5, name: 'Tanya Fox' }, { id: 5, name: "Tanya Fox" },
{ id: 6, name: 'Hellen Schmidt' }, { id: 6, name: "Hellen Schmidt" },
{ id: 7, name: 'Caroline Schultz' }, { id: 7, name: "Caroline Schultz" },
{ id: 8, name: 'Mason Heaney' }, { id: 8, name: "Mason Heaney" },
{ id: 9, name: 'Claudie Smitham' }, { id: 9, name: "Claudie Smitham" },
{ id: 10, name: 'Emil Schaefer' }, { id: 10, name: "Emil Schaefer" },
], ],
dropdownSelected: { dropdownSelected: {
id: -1, id: -1,
name: 'Select a name', name: "Select a name",
}, },
}) });
const dropdownData = [ const dropdownData = [
[ [
{ {
title: 'Option 1', title: "Option 1",
icon: IconLogout, icon: IconLogout,
data: {}, data: {},
}, },
{ {
title: 'Option 2', title: "Option 2",
icon: null, icon: null,
data: { data: {
test: 12, test: 12,
@ -43,27 +43,37 @@ const dropdownData = [
], ],
[ [
{ {
title: 'Option 3', title: "Option 3",
icon: IconSettings, icon: IconSettings,
data: { data: {
amount: 34, amount: 34,
}, },
}, },
], ],
] ];
// TODO: die CSS-Klasse für die Farben wird hier in der StyleGuideView.vue generiert. // TODO: die CSS-Klasse für die Farben wird hier in der StyleGuideView.vue generiert.
// deshalb muss man diese CSS-Klassen in tailwind.config.js "safelist"en, wenn diese sonst // deshalb muss man diese CSS-Klassen in tailwind.config.js "safelist"en, wenn diese sonst
// noch nirgendwo verwendet werden. // noch nirgendwo verwendet werden.
const colors = ['blue', 'sky', 'green', 'red', 'orange', 'yellow', 'stone', 'gray', 'slate'] const colors = [
const colorValues = [200, 300, 400, 500, 600, 700, 800, 900] "blue",
"sky",
"green",
"red",
"orange",
"yellow",
"stone",
"gray",
"slate",
];
const colorValues = [200, 300, 400, 500, 600, 700, 800, 900];
function colorBgClass(color: string, value: number) { function colorBgClass(color: string, value: number) {
return `bg-${color}-${value}` return `bg-${color}-${value}`;
} }
function log(data: any) { function log(data: any) {
console.log(data) console.log(data);
} }
</script> </script>
@ -76,8 +86,8 @@ function log(data: any) {
</div> </div>
<p class="mt-8 text-xl"> <p class="mt-8 text-xl">
The icons are defined as Web Components, so they can also be used in the backend. Use them like The icons are defined as Web Components, so they can also be used in the backend.
&lt;it-icon-message/&gt; Use them like &lt;it-icon-message/&gt;
</p> </p>
<div class="mt-8 mb-8 flex flex-col gap-4 flex-wrap lg:flex-row"> <div class="mt-8 mb-8 flex flex-col gap-4 flex-wrap lg:flex-row">
@ -266,12 +276,17 @@ function log(data: any) {
<table class="text-gray-700"> <table class="text-gray-700">
<tr class="h-12 md:h-18 lg:h-24"> <tr class="h-12 md:h-18 lg:h-24">
<td></td> <td></td>
<td class="text-center" v-for="value in colorValues">{{ value }}</td> <td v-for="value in colorValues" :key="value" class="text-center">
{{ value }}
</td>
</tr> </tr>
<tr v-for="color in colors" class="h-12 md:h-18 lg:h-24"> <tr v-for="color in colors" :key="color" class="h-12 md:h-18 lg:h-24">
<td>{{ color }}</td> <td>{{ color }}</td>
<td v-for="value in colorValues" class="px-2"> <td v-for="value in colorValues" :key="value" class="px-2">
<div class="w-8 h-8 md:w-12 md:h-12 lg:w-16 lg:h-16 rounded-full" :class="[colorBgClass(color, value)]"></div> <div
class="w-8 h-8 md:w-12 md:h-12 lg:w-16 lg:h-16 rounded-full"
:class="[colorBgClass(color, value)]"
></div>
</td> </td>
</tr> </tr>
</table> </table>
@ -313,8 +328,13 @@ function log(data: any) {
<button disabled class="btn-blue">Blue disabled</button> <button disabled class="btn-blue">Blue disabled</button>
</div> </div>
<div class="flex flex-col gap-4 flex-wrap lg:flex-row content-center lg:justify-start mb-16"> <div
<button type="button" class="btn-primary inline-flex items-center p-3 rounded-full"> class="flex flex-col gap-4 flex-wrap lg:flex-row content-center lg:justify-start mb-16"
>
<button
type="button"
class="btn-primary inline-flex items-center p-3 rounded-full"
>
<it-icon-message class="h-5 w-5"></it-icon-message> <it-icon-message class="h-5 w-5"></it-icon-message>
</button> </button>
@ -336,19 +356,26 @@ function log(data: any) {
<h2 class="mt-8 mb-8">Dropdown (Work-in-progress)</h2> <h2 class="mt-8 mb-8">Dropdown (Work-in-progress)</h2>
<ItDropdownSelect v-model="state.dropdownSelected" :items="state.dropdownValues"> </ItDropdownSelect> <ItDropdownSelect v-model="state.dropdownSelected" :items="state.dropdownValues">
</ItDropdownSelect>
{{ state.dropdownSelected }} {{ state.dropdownSelected }}
<h2 class="mt-8 mb-8">Checkbox</h2> <h2 class="mt-8 mb-8">Checkbox</h2>
<ItCheckbox :disabled="false" class="" v-model="state.checkboxValue">Label</ItCheckbox> <ItCheckbox v-model="state.checkboxValue" :disabled="false" class=""
>Label</ItCheckbox
>
<ItCheckbox disabled class="mt-4">Disabled</ItCheckbox> <ItCheckbox disabled class="mt-4">Disabled</ItCheckbox>
<h2 class="mt-8 mb-8">Dropdown</h2> <h2 class="mt-8 mb-8">Dropdown</h2>
<div class="h-60"> <div class="h-60">
<ItDropdown :button-classes="['btn-primary']" :list-items="dropdownData" :align="'left'" @select="log" <ItDropdown
:button-classes="['btn-primary']"
:list-items="dropdownData"
:align="'left'"
@select="log"
>Click Me</ItDropdown >Click Me</ItDropdown
> >
</div> </div>

View File

@ -1,40 +1,40 @@
const colors = require('./src/colors.json'); const colors = require("./src/colors.json");
module.exports = { module.exports = {
content: [ content: [
'./index.html', "./index.html",
'./src/**/*.{vue,js,ts,jsx,tsx}', "./src/**/*.{vue,js,ts,jsx,tsx}",
// TODO: wenn man den server-pfad auch angibt wird Tailwind langsamer?! (Startzeit erhöht sich stark...) // TODO: wenn man den server-pfad auch angibt wird Tailwind langsamer?! (Startzeit erhöht sich stark...)
// '../server/vbv_lernwelt/**/*.{html,js,py}', // '../server/vbv_lernwelt/**/*.{html,js,py}',
], ],
theme: { theme: {
fontFamily: { fontFamily: {
sans: ['Buenos Aires', 'sans-serif'], sans: ["Buenos Aires", "sans-serif"],
}, },
extend: { extend: {
spacing: { spacing: {
'128': '32rem', 128: "32rem",
}, },
maxWidth: { maxWidth: {
'8xl': '88rem', "8xl": "88rem",
'9xl': '96rem', "9xl": "96rem",
}, },
backgroundImage: { backgroundImage: {
'handlungsfelder-overview': "url('/static/icons/icon-handlungsfelder-overview.svg')", "handlungsfelder-overview":
'lernmedien-overview': "url('/static/icons/icon-lernmedien-overview.svg')", "url('/static/icons/icon-handlungsfelder-overview.svg')",
} "lernmedien-overview": "url('/static/icons/icon-lernmedien-overview.svg')",
},
}, },
colors: colors, colors: colors,
}, },
safelist: [ safelist: [
{ pattern: /bg-(blue|sky|green|red|orange|yellow|stone|gray|slate)-(200|300|400|500|600|700|800|900)/, }, {
'it-icon', pattern:
'bg-handlungsfelder-overview', /bg-(blue|sky|green|red|orange|yellow|stone|gray|slate)-(200|300|400|500|600|700|800|900)/,
'bg-lernmedien-overview', },
"it-icon",
"bg-handlungsfelder-overview",
"bg-lernmedien-overview",
], ],
plugins: [ plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
require('@tailwindcss/typography'), };
require('@tailwindcss/forms'),
],
}

View File

@ -3,7 +3,7 @@
@tailwind utilities; @tailwind utilities;
html { html {
@apply text-black @apply text-black;
} }
body { body {
@ -11,94 +11,93 @@ body {
} }
svg { svg {
@apply fill-current @apply fill-current;
} }
@layer base { @layer base {
.it-icon { .it-icon {
@apply w-8 h-8 inline-block @apply w-8 h-8 inline-block;
} }
h1 { h1 {
@apply text-4xl md:text-5xl xl:text-7xl font-bold @apply text-4xl md:text-5xl xl:text-7xl font-bold;
} }
.heading-1 { .heading-1 {
@apply text-4xl md:text-5xl xl:text-7xl font-bold @apply text-4xl md:text-5xl xl:text-7xl font-bold;
} }
h2 { h2 {
@apply text-2xl md:text-3xl xl:text-4xl font-bold @apply text-2xl md:text-3xl xl:text-4xl font-bold;
} }
.heading-2 { .heading-2 {
@apply text-2xl md:text-3xl xl:text-4xl font-bold @apply text-2xl md:text-3xl xl:text-4xl font-bold;
} }
h3 { h3 {
@apply text-xl xl:text-2xl font-bold @apply text-xl xl:text-2xl font-bold;
} }
.heading-3 { .heading-3 {
@apply text-xl xl:text-2xl font-bold @apply text-xl xl:text-2xl font-bold;
} }
.link { .link {
@apply underline underline-offset-2 @apply underline underline-offset-2;
} }
.link-large { .link-large {
@apply text-lg underline xl:text-xl @apply text-lg underline xl:text-xl;
} }
.text-large { .text-large {
@apply text-lg xl:text-xl @apply text-lg xl:text-xl;
} }
.text-bold { .text-bold {
@apply text-base font-bold @apply text-base font-bold;
} }
.container-medium { .container-medium {
@apply mx-auto max-w-5xl px-4 lg:px-8 py-4 @apply mx-auto max-w-5xl px-4 lg:px-8 py-4;
} }
.container-large { .container-large {
@apply mx-auto max-w-9xl px-4 lg:px-8 py-4 @apply mx-auto max-w-9xl px-4 lg:px-8 py-4;
} }
} }
@layer components { @layer components {
.circle-title { .circle-title {
@apply text-9xl font-bold @apply text-9xl font-bold;
} }
.btn-primary { .btn-primary {
@apply font-semibold py-2 px-4 align-middle inline-block @apply font-semibold py-2 px-4 align-middle inline-block
bg-blue-900 text-white border-2 border-blue-900 bg-blue-900 text-white border-2 border-blue-900
hover:bg-blue-700 hover:border-blue-700 hover:bg-blue-700 hover:border-blue-700
disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50 disabled:cursor-not-allowed;
} }
.btn-secondary { .btn-secondary {
@apply font-semibold py-2 px-4 align-middle inline-block @apply font-semibold py-2 px-4 align-middle inline-block
bg-white text-blue-900 border-2 border-blue-900 bg-white text-blue-900 border-2 border-blue-900
hover:bg-gray-200 hover:bg-gray-200
disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50 disabled:cursor-not-allowed;
} }
.btn-blue { .btn-blue {
@apply font-semibold py-2 px-4 align-middle inline-block @apply font-semibold py-2 px-4 align-middle inline-block
bg-sky-500 text-blue-900 border-2 border-sky-500 bg-sky-500 text-blue-900 border-2 border-sky-500
hover:bg-sky-400 hover:border-sky-400 hover:bg-sky-400 hover:border-sky-400
disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50 disabled:cursor-not-allowed;
} }
.btn-text { .btn-text {
@apply font-semibold py-2 px-4 align-middle inline-block @apply font-semibold py-2 px-4 align-middle inline-block
hover:text-gray-700 hover:text-gray-700
disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50 disabled:cursor-not-allowed;
} }
} }
@ -108,4 +107,3 @@ svg {
overflow: hidden; overflow: hidden;
} }
} }

View File

@ -4,10 +4,6 @@
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"lib": [], "lib": [],
"types": [ "types": ["node", "jsdom", "vitest/globals"]
"node",
"jsdom",
"vitest/globals"
]
} }
} }

View File

@ -1,12 +1,13 @@
const replace = require("replace-in-file"); const replace = require("replace-in-file");
const gitHash = require('child_process') const gitHash = require("child_process")
.execSync("git rev-parse --short HEAD") .execSync("git rev-parse --short HEAD")
.toString().trim() .toString()
.trim();
const options = { const options = {
files: "dist/static/vue/*.js", files: "dist/static/vue/*.js",
from: /VBV_VERSION_BUILD_NUMBER_VBV/g, from: /VBV_VERSION_BUILD_NUMBER_VBV/g,
to: new Date().toISOString().replace("T", " ").substring(0, 19) + ' ' + gitHash, to: new Date().toISOString().replace("T", " ").substring(0, 19) + " " + gitHash,
}; };
replace(options, (error, results) => { replace(options, (error, results) => {

View File

@ -1,20 +1,20 @@
import { fileURLToPath, URL } from "url"; import { fileURLToPath, URL } from "url";
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import { defineConfig, loadEnv } from "vite";
// import vueI18n from '@intlify/vite-plugin-vue-i18n' // import vueI18n from '@intlify/vite-plugin-vue-i18n'
import alias from "@rollup/plugin-alias"; import alias from "@rollup/plugin-alias";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) } process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
return { return {
plugins: [ plugins: [
vue({ vue({
template: { template: {
compilerOptions: { compilerOptions: {
// treat all tags which start with '<it-' as custom elements // treat all tags which start with '<it-' as custom elements
isCustomElement: (tag) => tag.startsWith('it-'), isCustomElement: (tag) => tag.startsWith("it-"),
}, },
}, },
}), }),
@ -35,19 +35,19 @@ export default defineConfig(({ mode }) => {
], ],
server: { server: {
port: 5173, port: 5173,
hmr: { port: 5173 } hmr: { port: 5173 },
}, },
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), "@": fileURLToPath(new URL("./src", import.meta.url)),
}, },
}, },
build: { build: {
assetsDir: 'static/vue', assetsDir: "static/vue",
}, },
test: { test: {
globals: true, globals: true,
environment: 'jsdom', environment: "jsdom",
}, },
} };
}) });

10
format_code.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
# script should fail when any process returns non zero code
set -e
echo 'format client code'
npm run prettier
echo 'format python code'
ufmt format server

13
git-pre-commit.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
# script should fail when any process returns non zero code
set -e
echo 'prettier:check'
(cd client && npm run prettier:check)
echo 'lint'
(cd client && npm run lint)
echo 'python ufmt check'
ufmt check server

10
git-pre-push.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
# script should fail when any process returns non zero code
set -e
echo 'check git-crypt files diff'
git-crypt status -e | sort > git-crypt-encrypted-files-check.txt && diff git-crypt-encrypted-files.txt git-crypt-encrypted-files-check.txt
echo 'check for secrets with truffleHog'
trufflehog --exclude_paths trufflehog-exclude-patterns.txt --allow trufflehog-allow.json --max_depth=3 .

View File

@ -5,7 +5,8 @@
"build": "npm install --prefix client && npm run build --prefix client && npm run build:tailwind --prefix client", "build": "npm install --prefix client && npm run build --prefix client && npm run build:tailwind --prefix client",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:ci": "cypress run" "cypress:ci": "cypress run",
"prettier": "npm run prettier --prefix client"
}, },
"devDependencies": { "devDependencies": {
"cypress": "^10.6.0" "cypress": "^10.6.0"

View File

@ -78,26 +78,24 @@ THIRD_PARTY_APPS = [
"rest_framework.authtoken", "rest_framework.authtoken",
"corsheaders", "corsheaders",
"drf_spectacular", "drf_spectacular",
"wagtail.contrib.forms",
'wagtail.contrib.forms', "wagtail.contrib.redirects",
'wagtail.contrib.redirects', "wagtail.contrib.styleguide",
'wagtail.contrib.styleguide', "wagtail.embeds",
'wagtail.embeds', "wagtail.sites",
'wagtail.sites', "wagtail.users",
'wagtail.users', "wagtail.snippets",
'wagtail.snippets', "wagtail.documents",
'wagtail.documents', "wagtail.images",
'wagtail.images', "wagtail.search",
'wagtail.search', "wagtail.admin",
'wagtail.admin', "wagtail",
'wagtail',
# 'wagtail.locales', # 'wagtail.locales',
"wagtail_localize", "wagtail_localize",
"wagtail_localize.locales", "wagtail_localize.locales",
'wagtail.api.v2', "wagtail.api.v2",
"modelcluster",
'modelcluster', "taggit",
'taggit',
] ]
LOCAL_APPS = [ LOCAL_APPS = [
@ -199,32 +197,32 @@ MEDIA_ROOT = str(APPS_DIR / "media")
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
IT_SERVE_VUE = env.bool("IT_SERVE_VUE", DEBUG) IT_SERVE_VUE = env.bool("IT_SERVE_VUE", DEBUG)
IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", 'http://localhost:5173') IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", "http://localhost:5173")
# WAGTAIL # WAGTAIL
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
WAGTAIL_SITE_NAME = 'VBV Lernwelt' WAGTAIL_SITE_NAME = "VBV Lernwelt"
WAGTAIL_I18N_ENABLED = True WAGTAIL_I18N_ENABLED = True
LANGUAGES = [ LANGUAGES = [
('en-US', "English (American)"), ("en-US", "English (American)"),
('fr-CH', "Swiss French"), ("fr-CH", "Swiss French"),
('de-CH', "Swiss German"), ("de-CH", "Swiss German"),
('it-CH', "Swiss Italian") ("it-CH", "Swiss Italian"),
] ]
WAGTAILDOCS_DOCUMENT_MODEL = 'media_library.LibraryDocument' WAGTAILDOCS_DOCUMENT_MODEL = "media_library.LibraryDocument"
WAGTAIL_CONTENT_LANGUAGES = [ WAGTAIL_CONTENT_LANGUAGES = [
('fr-CH', "Swiss French"), ("fr-CH", "Swiss French"),
('de-CH', "Swiss German"), ("de-CH", "Swiss German"),
('it-CH', "Swiss Italian") ("it-CH", "Swiss Italian"),
] ]
WAGTAILSEARCH_BACKENDS = { WAGTAILSEARCH_BACKENDS = {
'default': { "default": {
'BACKEND': 'wagtail.search.backends.database', "BACKEND": "wagtail.search.backends.database",
} }
} }
@ -456,13 +454,13 @@ CORS_URLS_REGEX = r"^/api/.*$"
CSP_DEFAULT_SRC = [ CSP_DEFAULT_SRC = [
"'self'", "'self'",
"'unsafe-inline'", "'unsafe-inline'",
'ws://localhost:5173', "ws://localhost:5173",
'ws://127.0.0.1:5173', "ws://127.0.0.1:5173",
'localhost:8000', "localhost:8000",
'localhost:8001', "localhost:8001",
'blob:', "blob:",
'data:', "data:",
'http://*' "http://*",
] ]
CSP_FRAME_ANCESTORS = ("'self'",) CSP_FRAME_ANCESTORS = ("'self'",)
@ -498,7 +496,10 @@ ALLOWED_HOSTS = env.list(
# CACHES # CACHES
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": env("IT_DJANGO_CACHE_BACKEND", default="django.core.cache.backends.db.DatabaseCache"), "BACKEND": env(
"IT_DJANGO_CACHE_BACKEND",
default="django.core.cache.backends.db.DatabaseCache",
),
"LOCATION": env("IT_DJANGO_CACHE_LOCATION", default="django_cache_table"), "LOCATION": env("IT_DJANGO_CACHE_LOCATION", default="django_cache_table"),
}, },
} }
@ -524,9 +525,7 @@ CACHES["api_page_cache"] = {
IT_OAUTH_TENANT_ID = env.str("IT_OAUTH_TENANT_ID", default=None) IT_OAUTH_TENANT_ID = env.str("IT_OAUTH_TENANT_ID", default=None)
if IT_OAUTH_TENANT_ID: if IT_OAUTH_TENANT_ID:
IT_OAUTH_AUTHORIZE_PARAMS = { IT_OAUTH_AUTHORIZE_PARAMS = {"tenant_id": IT_OAUTH_TENANT_ID}
'tenant_id': IT_OAUTH_TENANT_ID
}
else: else:
IT_OAUTH_AUTHORIZE_PARAMS = {} IT_OAUTH_AUTHORIZE_PARAMS = {}
@ -538,14 +537,22 @@ OAUTH = {
# "authorize_url": env("IT_OAUTH_AUTHORIZE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/auth"), # "authorize_url": env("IT_OAUTH_AUTHORIZE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/auth"),
"authorize_params": IT_OAUTH_AUTHORIZE_PARAMS, "authorize_params": IT_OAUTH_AUTHORIZE_PARAMS,
"access_token_params": IT_OAUTH_AUTHORIZE_PARAMS, "access_token_params": IT_OAUTH_AUTHORIZE_PARAMS,
"api_base_url": env("IT_OAUTH_API_BASE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/"), "api_base_url": env(
"local_redirect_uri": env("IT_OAUTH_LOCAL_DIRECT_URI", default="http://localhost:8000/sso/callback/"), "IT_OAUTH_API_BASE_URL",
"server_metadata_url": env("IT_OAUTH_SERVER_METADATA_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/.well-known/openid-configuration"), default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/",
),
"local_redirect_uri": env(
"IT_OAUTH_LOCAL_DIRECT_URI", default="http://localhost:8000/sso/callback/"
),
"server_metadata_url": env(
"IT_OAUTH_SERVER_METADATA_URL",
default="https://sso.test.b.lernetz.host/auth/realms/vbv/.well-known/openid-configuration",
),
"client_kwargs": { "client_kwargs": {
'scope': env("IT_OAUTH_SCOPE", default=''), "scope": env("IT_OAUTH_SCOPE", default=""),
'token_endpoint_auth_method': 'client_secret_post', "token_endpoint_auth_method": "client_secret_post",
'token_placement': 'body', "token_placement": "body",
} },
} }
if APP_ENVIRONMENT == "development": if APP_ENVIRONMENT == "development":
@ -555,7 +562,7 @@ if APP_ENVIRONMENT == "development":
# django-debug-toolbar # django-debug-toolbar
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites
#INSTALLED_APPS += ["debug_toolbar"] # noqa F405 # INSTALLED_APPS += ["debug_toolbar"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware
# MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405 # MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config # https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config
@ -582,7 +589,9 @@ if APP_ENVIRONMENT == "development":
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
INSTALLED_APPS += ["django_extensions"] # noqa F405 INSTALLED_APPS += ["django_extensions"] # noqa F405
if APP_ENVIRONMENT in ["production", "caprover"] or APP_ENVIRONMENT.startswith("caprover"): if APP_ENVIRONMENT in ["production", "caprover"] or APP_ENVIRONMENT.startswith(
"caprover"
):
# SECURITY # SECURITY
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header # https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
@ -632,7 +641,7 @@ if APP_ENVIRONMENT in ["production", "caprover"] or APP_ENVIRONMENT.startswith("
# ADMIN # ADMIN
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Django Admin URL regex. # Django Admin URL regex.
ADMIN_URL = env("IT_DJANGO_ADMIN_URL", 'admin/') ADMIN_URL = env("IT_DJANGO_ADMIN_URL", "admin/")
# Anymail # Anymail
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -1,7 +1,7 @@
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position # pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
import os import os
os.environ['IT_APP_ENVIRONMENT'] = 'development' os.environ["IT_APP_ENVIRONMENT"] = "development"
from .base import * # noqa from .base import * # noqa

View File

@ -1,14 +1,14 @@
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position # pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
import os import os
os.environ['IT_APP_ENVIRONMENT'] = 'development' os.environ["IT_APP_ENVIRONMENT"] = "development"
from .base import * # noqa from .base import * # noqa
# GENERAL # GENERAL
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
DATABASES['default']['NAME'] = 'vbv_lernwelt_cypress' DATABASES["default"]["NAME"] = "vbv_lernwelt_cypress"
# EMAIL # EMAIL
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -6,16 +6,27 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path, re_path from django.urls import include, path, re_path
from django.views import defaults as default_views from django.views import defaults as default_views
from ratelimit.exceptions import Ratelimited from ratelimit.exceptions import Ratelimited
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.core.views import ( from vbv_lernwelt.core.views import (
rate_limit_exceeded_view, check_rate_limit,
cypress_reset_view,
generate_web_component_icons,
me_user_view,
permission_denied_view, permission_denied_view,
check_rate_limit, cypress_reset_view, vue_home, vue_login, me_user_view, vue_logout, generate_web_component_icons, ) rate_limit_exceeded_view,
from vbv_lernwelt.course.views import page_api_view, request_course_completion, mark_course_completion vue_home,
vue_login,
vue_logout,
)
from vbv_lernwelt.course.views import (
mark_course_completion,
page_api_view,
request_course_completion,
)
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
def raise_example_error(request): def raise_example_error(request):
@ -106,4 +117,4 @@ if settings.DEBUG:
# serve everything else via the vue app # serve everything else via the vue app
urlpatterns += [re_path(r'^(?!.*(server/|api/|sso/)).*$', vue_home, name='home')] urlpatterns += [re_path(r"^(?!.*(server/|api/|sso/)).*$", vue_home, name="home")]

View File

@ -1,4 +1,4 @@
from django.test import TestCase, override_settings from django.test import override_settings, TestCase
class RateLimitTest(TestCase): class RateLimitTest(TestCase):

View File

@ -19,9 +19,10 @@ djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-
flake8 # https://github.com/PyCQA/flake8 flake8 # https://github.com/PyCQA/flake8
flake8-isort # https://github.com/gforcada/flake8-isort flake8-isort # https://github.com/gforcada/flake8-isort
coverage # https://github.com/nedbat/coveragepy coverage # https://github.com/nedbat/coveragepy
black # https://github.com/psf/black black>=22.8.0 # https://github.com/psf/black
pylint-django # https://github.com/PyCQA/pylint-django pylint-django # https://github.com/PyCQA/pylint-django
pre-commit # https://github.com/pre-commit/pre-commit pre-commit # https://github.com/pre-commit/pre-commit
ufmt
# Django # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -28,14 +28,17 @@ attrs==21.4.0
# via # via
# jsonschema # jsonschema
# pytest # pytest
# usort
authlib==1.0.0 authlib==1.0.0
# via -r requirements.in # via -r requirements.in
backcall==0.2.0 backcall==0.2.0
# via ipython # via ipython
beautifulsoup4==4.9.3 beautifulsoup4==4.9.3
# via wagtail # via wagtail
black==22.3.0 black==22.8.0
# via -r requirements-dev.in # via
# -r requirements-dev.in
# ufmt
certifi==2021.10.8 certifi==2021.10.8
# via # via
# requests # requests
@ -52,7 +55,10 @@ click==8.1.1
# via # via
# black # black
# django-click # django-click
# moreorless
# pip-tools # pip-tools
# ufmt
# usort
# uvicorn # uvicorn
concurrent-log-handler==0.9.20 concurrent-log-handler==0.9.20
# via -r requirements.in # via -r requirements.in
@ -86,7 +92,6 @@ django==3.2.13
# django-debug-toolbar # django-debug-toolbar
# django-extensions # django-extensions
# django-filter # django-filter
# django-htmx
# django-model-utils # django-model-utils
# django-modelcluster # django-modelcluster
# django-permissionedforms # django-permissionedforms
@ -212,8 +217,14 @@ l18n==2021.3
# via wagtail # via wagtail
lazy-object-proxy==1.7.1 lazy-object-proxy==1.7.1
# via astroid # via astroid
libcst==0.4.7
# via
# ufmt
# usort
markupsafe==2.1.1 markupsafe==2.1.1
# via jinja2 # via
# jinja2
# werkzeug
marshmallow==3.15.0 marshmallow==3.15.0
# via environs # via environs
matplotlib-inline==0.1.3 matplotlib-inline==0.1.3
@ -222,6 +233,10 @@ mccabe==0.6.1
# via # via
# flake8 # flake8
# pylint # pylint
moreorless==0.4.0
# via
# ufmt
# usort
mypy==0.942 mypy==0.942
# via # via
# -r requirements-dev.in # -r requirements-dev.in
@ -231,6 +246,7 @@ mypy-extensions==0.4.3
# via # via
# black # black
# mypy # mypy
# typing-inspect
nodeenv==1.6.0 nodeenv==1.6.0
# via pre-commit # via pre-commit
openpyxl==3.0.9 openpyxl==3.0.9
@ -244,7 +260,9 @@ packaging==21.3
parso==0.8.3 parso==0.8.3
# via jedi # via jedi
pathspec==0.9.0 pathspec==0.9.0
# via black # via
# black
# trailrunner
pep517==0.12.0 pep517==0.12.0
# via pip-tools # via pip-tools
pexpect==4.8.0 pexpect==4.8.0
@ -329,6 +347,7 @@ pytz==2022.1
pyyaml==6.0 pyyaml==6.0
# via # via
# drf-spectacular # drf-spectacular
# libcst
# pre-commit # pre-commit
# uvicorn # uvicorn
redis==4.2.1 redis==4.2.1
@ -362,6 +381,8 @@ sqlparse==0.4.2
# django-debug-toolbar # django-debug-toolbar
stack-data==0.2.0 stack-data==0.2.0
# via ipython # via ipython
stdlibs==2022.6.8
# via usort
structlog==21.5.0 structlog==21.5.0
# via -r requirements.in # via -r requirements.in
tablib[xls,xlsx]==3.2.1 tablib[xls,xlsx]==3.2.1
@ -378,6 +399,7 @@ toml==0.10.2
# via # via
# ipdb # ipdb
# pre-commit # pre-commit
# usort
tomli==2.0.1 tomli==2.0.1
# via # via
# black # black
@ -386,6 +408,12 @@ tomli==2.0.1
# pep517 # pep517
# pylint # pylint
# pytest # pytest
tomlkit==0.11.5
# via ufmt
trailrunner==1.2.1
# via
# ufmt
# usort
traitlets==5.1.1 traitlets==5.1.1
# via # via
# ipython # ipython
@ -403,8 +431,15 @@ typing-extensions==4.1.1
# django-stubs # django-stubs
# django-stubs-ext # django-stubs-ext
# djangorestframework-stubs # djangorestframework-stubs
# libcst
# mypy # mypy
# typing-inspect
# ufmt
# wagtail-localize # wagtail-localize
typing-inspect==0.8.0
# via libcst
ufmt==2.0.1
# via -r requirements-dev.in
uritemplate==4.1.1 uritemplate==4.1.1
# via # via
# coreapi # coreapi
@ -413,6 +448,8 @@ urllib3==1.26.9
# via # via
# requests # requests
# sentry-sdk # sentry-sdk
usort==1.0.5
# via ufmt
uvicorn[standard]==0.17.6 uvicorn[standard]==0.17.6
# via -r requirements.in # via -r requirements.in
uvloop==0.16.0 uvloop==0.16.0

View File

@ -2,5 +2,5 @@ from django.apps import AppConfig
class MediaLibraryConfig(AppConfig): class MediaLibraryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = "django.db.models.BigAutoField"
name = 'vbv_lernwelt.competence' name = "vbv_lernwelt.competence"

View File

@ -1,8 +1,11 @@
from vbv_lernwelt.competence.factories import CompetenceProfilePageFactory, PerformanceCriteriaFactory, \ from vbv_lernwelt.competence.factories import (
CompetencePageFactory CompetencePageFactory,
CompetenceProfilePageFactory,
PerformanceCriteriaFactory,
)
from vbv_lernwelt.competence.models import CompetencePage from vbv_lernwelt.competence.models import CompetencePage
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
from vbv_lernwelt.course.models import CoursePage, Course from vbv_lernwelt.course.models import Course, CoursePage
from vbv_lernwelt.learnpath.models import LearningUnit from vbv_lernwelt.learnpath.models import LearningUnit
@ -11,135 +14,143 @@ def create_default_competence_profile():
course_page = CoursePage.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID) course_page = CoursePage.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
competence_profile_page = CompetenceProfilePageFactory( competence_profile_page = CompetenceProfilePageFactory(
title='Kompetenzprofil', title="Kompetenzprofil",
parent=course_page, parent=course_page,
) )
competences = [{ competences = [
'competence_id': 'A1', {
'title': 'Weiterempfehlung für Neukunden generieren', "competence_id": "A1",
'items': [ "title": "Weiterempfehlung für Neukunden generieren",
'Verhandlungsgeschick', "items": [
'Überzeugtes Auftreten', "Verhandlungsgeschick",
], "Überzeugtes Auftreten",
}, { ],
'competence_id': 'A2', },
'title': 'Kundengespräche vereinbaren', {
'items': [ "competence_id": "A2",
'Gesprächsführung / Fragetechniken', "title": "Kundengespräche vereinbaren",
'Selbstorganisation', "items": [
'Arbeitstechniken', "Gesprächsführung / Fragetechniken",
'Psychologische Kenntnisse / Kommunikations-psychologie', "Selbstorganisation",
], "Arbeitstechniken",
}, { "Psychologische Kenntnisse / Kommunikations-psychologie",
'competence_id': 'A3', ],
'title': 'Auftritt in den sozialen Medien zeitgemäss halten', },
'items': [ {
'Gesetzliche und Compliance-Anforderungen der Versicherer', "competence_id": "A3",
'Datenschutzgesetz', "title": "Auftritt in den sozialen Medien zeitgemäss halten",
'Kommunikation in den sozialen Medien', "items": [
] "Gesetzliche und Compliance-Anforderungen der Versicherer",
}, { "Datenschutzgesetz",
'competence_id': 'A4', "Kommunikation in den sozialen Medien",
'title': 'Kundendaten erfassen', ],
'items': [] },
}, { {"competence_id": "A4", "title": "Kundendaten erfassen", "items": []},
'competence_id': 'B1', {
'title': 'Wünsche, Ziele und Bedürfnisse der Kunden im Gespräch ermitteln', "competence_id": "B1",
'items': [ "title": "Wünsche, Ziele und Bedürfnisse der Kunden im Gespräch ermitteln",
'Gesprächsführung', "items": [
'Fragetechniken', "Gesprächsführung",
'Kundenpsychologie', "Fragetechniken",
] "Kundenpsychologie",
}, { ],
'competence_id': 'B2', },
'title': 'Analyse des Kundenbedarfs und des Kundenbedürfnisses durchführen', {
'items': [ "competence_id": "B2",
'Fragetechniken', "title": "Analyse des Kundenbedarfs und des Kundenbedürfnisses durchführen",
'Visuelle Hilfsmittel / Visualisierungstechniken', "items": [
] "Fragetechniken",
}, { "Visuelle Hilfsmittel / Visualisierungstechniken",
'competence_id': 'B3', ],
'title': 'Individuelle Lösungsvorschläge erarbeiten', },
'items': [ {
'Fundierte Produktekenntnisse', "competence_id": "B3",
'Regulatorische Vorschriften', "title": "Individuelle Lösungsvorschläge erarbeiten",
] "items": [
}, { "Fundierte Produktekenntnisse",
'competence_id': 'B4', "Regulatorische Vorschriften",
'title': 'Lösungsvorschläge präsentieren und umsetzen', ],
'items': [ },
'Verhandlungsstrategien', {
'Fundierte Produktkenntnisse', "competence_id": "B4",
'Visuelle Hilfsmittel / Visualisierungstechniken', "title": "Lösungsvorschläge präsentieren und umsetzen",
] "items": [
}, { "Verhandlungsstrategien",
'competence_id': 'C1', "Fundierte Produktkenntnisse",
'title': 'Cross- und Upselling; bestehende fremdverwaltete Versicherungspolicen prüfen und in das Portfolio aufnehmen', "Visuelle Hilfsmittel / Visualisierungstechniken",
'items': [ ],
'Produktkenntnisse', },
'Gesprächsführung', {
'Kommunikation', "competence_id": "C1",
'Fragetechnik', "title": "Cross- und Upselling; bestehende fremdverwaltete Versicherungspolicen prüfen und in das Portfolio aufnehmen",
'Verhandlungsgeschick', "items": [
'Vertragsrecht', "Produktkenntnisse",
'Regulatorische Vorgaben', "Gesprächsführung",
'UVG, BVG, KVG, VVG', "Kommunikation",
] "Fragetechnik",
}, { "Verhandlungsgeschick",
'competence_id': 'C2', "Vertragsrecht",
'title': 'Änderungswünsche entgegennehmen und bestehende Verträge anpassen', "Regulatorische Vorgaben",
'items': [ "UVG, BVG, KVG, VVG",
'Produktkenntnisse', ],
'Gesprächsführung', },
'Kommunikation', {
'Fragetechnik', "competence_id": "C2",
'Verhandlungsgeschick', "title": "Änderungswünsche entgegennehmen und bestehende Verträge anpassen",
'Vertragsrecht', "items": [
'Regulatorische Vorgaben', "Produktkenntnisse",
'UVG, BVG, KVG, VVG', "Gesprächsführung",
] "Kommunikation",
}, { "Fragetechnik",
'competence_id': 'C3', "Verhandlungsgeschick",
'title': 'Kunden im Schadenfall unterstützen', "Vertragsrecht",
'items': [] "Regulatorische Vorgaben",
}, { "UVG, BVG, KVG, VVG",
'competence_id': 'C4', ],
'title': 'Bestehende Kunden pflegen', },
'items': [] {
}, { "competence_id": "C3",
'competence_id': 'C5', "title": "Kunden im Schadenfall unterstützen",
'title': 'Versicherungsanträge nachbearbeiten', "items": [],
'items': [] },
}] {"competence_id": "C4", "title": "Bestehende Kunden pflegen", "items": []},
{
"competence_id": "C5",
"title": "Versicherungsanträge nachbearbeiten",
"items": [],
},
]
for c in competences: for c in competences:
CompetencePageFactory( CompetencePageFactory(
parent=competence_profile_page, parent=competence_profile_page,
competence_id=c['competence_id'], competence_id=c["competence_id"],
title=c['title'], title=c["title"],
items=[ items=[("item", i) for i in c["items"]],
('item', i) for i in c['items']
]
) )
PerformanceCriteriaFactory( PerformanceCriteriaFactory(
parent=CompetencePage.objects.get(competence_id='B1'), parent=CompetencePage.objects.get(competence_id="B1"),
competence_id='B1.3', competence_id="B1.3",
title='Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Ziele und Pläne des Kunden zu ergründen (SOLL).', title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Ziele und Pläne des Kunden zu ergründen (SOLL).",
learning_unit=LearningUnit.objects.get(slug='versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug'), learning_unit=LearningUnit.objects.get(
slug="versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug"
),
) )
PerformanceCriteriaFactory( PerformanceCriteriaFactory(
parent=CompetencePage.objects.get(competence_id='B2'), parent=CompetencePage.objects.get(competence_id="B2"),
competence_id='B2.1', competence_id="B2.1",
title='Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die IST-Situation des Kunden mit der geeigneten Gesprächs-/Fragetechnik zu erfassen.', title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die IST-Situation des Kunden mit der geeigneten Gesprächs-/Fragetechnik zu erfassen.",
learning_unit=LearningUnit.objects.get(slug='versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug'), learning_unit=LearningUnit.objects.get(
slug="versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug"
),
) )
PerformanceCriteriaFactory( PerformanceCriteriaFactory(
parent=CompetencePage.objects.get(competence_id='B2'), parent=CompetencePage.objects.get(competence_id="B2"),
competence_id='B2.2', competence_id="B2.2",
title='Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Risiken aufzuzeigen.', title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Risiken aufzuzeigen.",
learning_unit=LearningUnit.objects.get(slug='versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug'), learning_unit=LearningUnit.objects.get(
slug="versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug"
),
) )

View File

@ -1,26 +1,32 @@
import wagtail_factories import wagtail_factories
from vbv_lernwelt.competence.models import CompetenceProfilePage, PerformanceCriteria, CompetencePage from vbv_lernwelt.competence.models import (
CompetencePage,
CompetenceProfilePage,
PerformanceCriteria,
)
class CompetenceProfilePageFactory(wagtail_factories.PageFactory): class CompetenceProfilePageFactory(wagtail_factories.PageFactory):
title = 'Kompetenzprofil' title = "Kompetenzprofil"
class Meta: class Meta:
model = CompetenceProfilePage model = CompetenceProfilePage
class CompetencePageFactory(wagtail_factories.PageFactory): class CompetencePageFactory(wagtail_factories.PageFactory):
competence_id = 'A1' competence_id = "A1"
title = 'Weiterempfehlung für Neukunden generieren' title = "Weiterempfehlung für Neukunden generieren"
class Meta: class Meta:
model = CompetencePage model = CompetencePage
class PerformanceCriteriaFactory(wagtail_factories.PageFactory): class PerformanceCriteriaFactory(wagtail_factories.PageFactory):
competence_id = 'A1.1' competence_id = "A1.1"
title = 'Bestehende Kunden so zu beraten, dass sie von diesen weiterempfohlen werden' title = (
"Bestehende Kunden so zu beraten, dass sie von diesen weiterempfohlen werden"
)
class Meta: class Meta:
model = PerformanceCriteria model = PerformanceCriteria

View File

@ -1,9 +1,9 @@
# Generated by Django 3.2.13 on 2022-09-28 12:51 # Generated by Django 3.2.13 on 2022-09-28 12:51
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import wagtail.blocks import wagtail.blocks
import wagtail.fields import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -11,41 +11,76 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('wagtailcore', '0069_log_entry_jsonfield'), ("wagtailcore", "0069_log_entry_jsonfield"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='CompetencePage', name="CompetencePage",
fields=[ fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), (
('competence_id', models.TextField(default='A1')), "page_ptr",
('items', wagtail.fields.StreamField([('item', wagtail.blocks.TextBlock())], use_json_field=True)), models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
("competence_id", models.TextField(default="A1")),
(
"items",
wagtail.fields.StreamField(
[("item", wagtail.blocks.TextBlock())], use_json_field=True
),
),
], ],
options={ options={
'abstract': False, "abstract": False,
}, },
bases=('wagtailcore.page',), bases=("wagtailcore.page",),
), ),
migrations.CreateModel( migrations.CreateModel(
name='CompetenceProfilePage', name="CompetenceProfilePage",
fields=[ fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), (
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
], ],
options={ options={
'abstract': False, "abstract": False,
}, },
bases=('wagtailcore.page',), bases=("wagtailcore.page",),
), ),
migrations.CreateModel( migrations.CreateModel(
name='PerformanceCriteria', name="PerformanceCriteria",
fields=[ fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), (
('competence_id', models.TextField(default='A1.1')), "page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
("competence_id", models.TextField(default="A1.1")),
], ],
options={ options={
'abstract': False, "abstract": False,
}, },
bases=('wagtailcore.page',), bases=("wagtailcore.page",),
), ),
] ]

View File

@ -1,7 +1,7 @@
# Generated by Django 3.2.13 on 2022-09-28 12:51 # Generated by Django 3.2.13 on 2022-09-28 12:51
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -9,14 +9,19 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('learnpath', '0001_initial'), ("learnpath", "0001_initial"),
('competence', '0001_initial'), ("competence", "0001_initial"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='performancecriteria', model_name="performancecriteria",
name='learning_unit', name="learning_unit",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='learnpath.learningunit'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="learnpath.learningunit",
),
), ),
] ]

View File

@ -10,86 +10,117 @@ from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
class CompetenceProfilePage(Page): class CompetenceProfilePage(Page):
parent_page_types = ['course.CoursePage'] parent_page_types = ["course.CoursePage"]
subpage_types = ['competence.CompetencePage'] subpage_types = ["competence.CompetencePage"]
content_panels = [ content_panels = [
FieldPanel('title', classname="full title"), FieldPanel("title", classname="full title"),
] ]
def full_clean(self, *args, **kwargs): def full_clean(self, *args, **kwargs):
self.slug = find_available_slug(slugify(f"{self.get_parent().slug}-competence", allow_unicode=True)) self.slug = find_available_slug(
slugify(f"{self.get_parent().slug}-competence", allow_unicode=True)
)
super(CompetenceProfilePage, self).full_clean(*args, **kwargs) super(CompetenceProfilePage, self).full_clean(*args, **kwargs)
@classmethod @classmethod
def get_serializer_class(cls): def get_serializer_class(cls):
return get_it_serializer_class( return get_it_serializer_class(
cls, [ cls,
'id', 'title', 'slug', 'type', 'translation_key', [
'course', "id",
'children', "title",
] "slug",
"type",
"translation_key",
"course",
"children",
],
) )
class CompetencePage(Page): class CompetencePage(Page):
parent_page_types = ['competence.CompetenceProfilePage'] parent_page_types = ["competence.CompetenceProfilePage"]
subpage_types = ['competence.PerformanceCriteria'] subpage_types = ["competence.PerformanceCriteria"]
competence_id = models.TextField(default='A1') competence_id = models.TextField(default="A1")
items = StreamField([ items = StreamField(
('item', blocks.TextBlock()), [
], use_json_field=True) ("item", blocks.TextBlock()),
],
use_json_field=True,
)
content_panels = [ content_panels = [
FieldPanel('title'), FieldPanel("title"),
FieldPanel('competence_id'), FieldPanel("competence_id"),
] ]
def full_clean(self, *args, **kwargs): def full_clean(self, *args, **kwargs):
self.slug = find_available_slug(slugify(f"{self.get_parent().slug}-competence-{self.competence_id}", allow_unicode=True)) self.slug = find_available_slug(
slugify(
f"{self.get_parent().slug}-competence-{self.competence_id}",
allow_unicode=True,
)
)
super(CompetencePage, self).full_clean(*args, **kwargs) super(CompetencePage, self).full_clean(*args, **kwargs)
@classmethod @classmethod
def get_serializer_class(cls): def get_serializer_class(cls):
return get_it_serializer_class( return get_it_serializer_class(
cls, [ cls,
'id', 'title', 'slug', 'type', 'translation_key', [
'children', "id",
] "title",
"slug",
"type",
"translation_key",
"children",
],
) )
class PerformanceCriteria(Page): class PerformanceCriteria(Page):
parent_page_types = ['competence.CompetenceProfilePage'] parent_page_types = ["competence.CompetenceProfilePage"]
competence_id = models.TextField(default='A1.1') competence_id = models.TextField(default="A1.1")
learning_unit = models.ForeignKey( learning_unit = models.ForeignKey(
'learnpath.LearningUnit', "learnpath.LearningUnit",
null=True, null=True,
blank=True, blank=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
content_panels = [ content_panels = [
FieldPanel('title'), FieldPanel("title"),
FieldPanel('competence_id'), FieldPanel("competence_id"),
FieldPanel('learning_unit'), FieldPanel("learning_unit"),
] ]
def full_clean(self, *args, **kwargs): def full_clean(self, *args, **kwargs):
profile_parent = self.get_ancestors().exact_type(CompetenceProfilePage).last() profile_parent = self.get_ancestors().exact_type(CompetenceProfilePage).last()
if self.learning_unit and self.learning_unit.course_category: if self.learning_unit and self.learning_unit.course_category:
self.slug = find_available_slug(slugify(f"{profile_parent.slug}-crit-{self.competence_id}-{self.learning_unit.course_category.title}", allow_unicode=True)) self.slug = find_available_slug(
slugify(
f"{profile_parent.slug}-crit-{self.competence_id}-{self.learning_unit.course_category.title}",
allow_unicode=True,
)
)
else: else:
self.slug = find_available_slug(slugify(f"{profile_parent.slug}-crit-{self.competence_id}", allow_unicode=True)) self.slug = find_available_slug(
slugify(
f"{profile_parent.slug}-crit-{self.competence_id}",
allow_unicode=True,
)
)
super(PerformanceCriteria, self).full_clean(*args, **kwargs) super(PerformanceCriteria, self).full_clean(*args, **kwargs)
@classmethod @classmethod
def get_serializer_class(cls): def get_serializer_class(cls):
from vbv_lernwelt.competence.serializers import PerformanceCriteriaSerializer from vbv_lernwelt.competence.serializers import PerformanceCriteriaSerializer
return PerformanceCriteriaSerializer return PerformanceCriteriaSerializer
def get_admin_display_title(self): def get_admin_display_title(self):
if self.learning_unit and self.learning_unit.course_category: if self.learning_unit and self.learning_unit.course_category:
return f'{self.competence_id} ({self.learning_unit.course_category.title}) {self.draft_title[:30]}' return f"{self.competence_id} ({self.learning_unit.course_category.title}) {self.draft_title[:30]}"
else: else:
return f'{self.competence_id} {self.draft_title[:30]}' return f"{self.competence_id} {self.draft_title[:30]}"

View File

@ -6,18 +6,37 @@ from vbv_lernwelt.learnpath.models import LearningUnit
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
class PerformanceCriteriaSerializer(get_it_serializer_class(PerformanceCriteria, [ class PerformanceCriteriaSerializer(
'id', 'title', 'slug', 'type', 'translation_key', get_it_serializer_class(
'competence_id', 'learning_unit', 'circle', 'course_category', PerformanceCriteria,
])): [
"id",
"title",
"slug",
"type",
"translation_key",
"competence_id",
"learning_unit",
"circle",
"course_category",
],
)
):
learning_unit = serializers.SerializerMethodField() learning_unit = serializers.SerializerMethodField()
circle = serializers.SerializerMethodField() circle = serializers.SerializerMethodField()
course_category = serializers.SerializerMethodField() course_category = serializers.SerializerMethodField()
def get_learning_unit(self, obj): def get_learning_unit(self, obj):
learning_unit_serializer = get_it_serializer_class(LearningUnit, [ learning_unit_serializer = get_it_serializer_class(
'id', 'title', 'slug', 'type', 'translation_key', LearningUnit,
]) [
"id",
"title",
"slug",
"type",
"translation_key",
],
)
return learning_unit_serializer(obj.learning_unit).data return learning_unit_serializer(obj.learning_unit).data
def get_circle(self, obj): def get_circle(self, obj):
@ -29,7 +48,17 @@ class PerformanceCriteriaSerializer(get_it_serializer_class(PerformanceCriteria,
return None return None
class PerformanceCriteriaLearningPathSerializer(get_it_serializer_class(PerformanceCriteria, [ class PerformanceCriteriaLearningPathSerializer(
'id', 'title', 'slug', 'type', 'translation_key', 'competence_id', get_it_serializer_class(
])): PerformanceCriteria,
[
"id",
"title",
"slug",
"type",
"translation_key",
"competence_id",
],
)
):
pass pass

View File

@ -10,16 +10,19 @@ class CompetenceAPITestCase(APITestCase):
def setUp(self) -> None: def setUp(self) -> None:
create_default_users() create_default_users()
create_test_course() create_test_course()
self.user = User.objects.get(username='student') self.user = User.objects.get(username="student")
self.client.login(username='student', password='test') self.client.login(username="student", password="test")
def test_get_learnpathPage(self): def test_get_learnpathPage(self):
slug = 'test-lehrgang-competence' slug = "test-lehrgang-competence"
competence_profile = CompetenceProfilePage.objects.get(slug=slug) competence_profile = CompetenceProfilePage.objects.get(slug=slug)
response = self.client.get(f'/api/course/page/{slug}/') response = self.client.get(f"/api/course/page/{slug}/")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = response.json() data = response.json()
self.assertEqual(competence_profile.title, data['title']) self.assertEqual(competence_profile.title, data["title"])
self.assertEqual('Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Ziele und Pläne des Kunden zu ergründen (SOLL).', data['children'][1]['children'][0]['title']) self.assertEqual(
"Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Ziele und Pläne des Kunden zu ergründen (SOLL).",
data["children"][1]["children"][0]["title"],
)

View File

@ -1,6 +1,5 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import admin as auth_admin from django.contrib.auth import admin as auth_admin, get_user_model
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
User = get_user_model() User = get_user_model()

View File

@ -6,15 +6,21 @@ from vbv_lernwelt.core.models import User
def create_default_users(user_model=User, group_model=Group, default_password=None): def create_default_users(user_model=User, group_model=Group, default_password=None):
if default_password is None: if default_password is None:
default_password = 'test' default_password = "test"
admin_group, created = group_model.objects.get_or_create(name='admin_group') admin_group, created = group_model.objects.get_or_create(name="admin_group")
_content_creator_grop, _created = group_model.objects.get_or_create(name='content_creator_grop') _content_creator_grop, _created = group_model.objects.get_or_create(
student_group, created = group_model.objects.get_or_create(name='student_group') name="content_creator_grop"
)
student_group, created = group_model.objects.get_or_create(name="student_group")
def _create_student_user(email, first_name, last_name, avatar_url='', password=default_password): def _create_student_user(
email, first_name, last_name, avatar_url="", password=default_password
):
student_user, created = _get_or_create_user( student_user, created = _get_or_create_user(
user_model=user_model, username=email, password=password, user_model=user_model,
username=email,
password=password,
) )
student_user.first_name = first_name student_user.first_name = first_name
student_user.last_name = last_name student_user.last_name = last_name
@ -22,7 +28,7 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
student_user.groups.add(student_group) student_user.groups.add(student_group)
student_user.save() student_user.save()
def _create_admin_user(email, first_name, last_name, avatar_url=''): def _create_admin_user(email, first_name, last_name, avatar_url=""):
admin_user, created = _get_or_create_user( admin_user, created = _get_or_create_user(
user_model=user_model, username=email, password=default_password user_model=user_model, username=email, password=default_password
) )
@ -35,68 +41,68 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
admin_user.save() admin_user.save()
_create_admin_user( _create_admin_user(
email='info@iterativ.ch', email="info@iterativ.ch",
first_name='Info', first_name="Info",
last_name='Iterativ', last_name="Iterativ",
avatar_url='/static/avatars/avatar_iterativ.png' avatar_url="/static/avatars/avatar_iterativ.png",
) )
_create_admin_user( _create_admin_user(
email='admin', email="admin",
first_name='Peter', first_name="Peter",
last_name='Adminson', last_name="Adminson",
avatar_url='/static/avatars/avatar_iterativ.png' avatar_url="/static/avatars/avatar_iterativ.png",
) )
_create_student_user( _create_student_user(
email='student', email="student",
first_name='Student', first_name="Student",
last_name='Meier', last_name="Meier",
avatar_url='/static/avatars/avatar_iterativ.png' avatar_url="/static/avatars/avatar_iterativ.png",
) )
_create_student_user( _create_student_user(
email='daniel.egger@iterativ.ch', email="daniel.egger@iterativ.ch",
first_name='Daniel', first_name="Daniel",
last_name='Egger', last_name="Egger",
avatar_url='/static/avatars/avatar_iterativ.png' avatar_url="/static/avatars/avatar_iterativ.png",
) )
_create_student_user( _create_student_user(
email='axel.manderbach@lernetz.ch', email="axel.manderbach@lernetz.ch",
first_name='Axel', first_name="Axel",
last_name='Manderbach', last_name="Manderbach",
avatar_url='/static/avatars/avatar_axel.jpg' avatar_url="/static/avatars/avatar_axel.jpg",
) )
_create_student_user( _create_student_user(
email='christoph.bosshard@vbv-afa.ch', email="christoph.bosshard@vbv-afa.ch",
first_name='Christoph', first_name="Christoph",
last_name='Bosshard', last_name="Bosshard",
avatar_url='/static/avatars/avatar_christoph.png', avatar_url="/static/avatars/avatar_christoph.png",
password='myvbv1234' password="myvbv1234",
) )
_create_student_user( _create_student_user(
email='alexandra.vangelista@lernetz.ch', email="alexandra.vangelista@lernetz.ch",
first_name='Alexandra', first_name="Alexandra",
last_name='Vangelista', last_name="Vangelista",
avatar_url='/static/avatars/avatar_alexandra.png', avatar_url="/static/avatars/avatar_alexandra.png",
password='myvbv1234' password="myvbv1234",
) )
_create_student_user( _create_student_user(
email='chantal.rosenberg@vbv-afa.ch', email="chantal.rosenberg@vbv-afa.ch",
first_name='Chantal', first_name="Chantal",
last_name='Rosenberg', last_name="Rosenberg",
avatar_url='/static/avatars/avatar_chantal.png', avatar_url="/static/avatars/avatar_chantal.png",
password='myvbv1234' password="myvbv1234",
) )
def _get_or_create_user(user_model, *args, **kwargs): def _get_or_create_user(user_model, *args, **kwargs):
username = kwargs.get('username', None) username = kwargs.get("username", None)
password = kwargs.get('password', None) password = kwargs.get("password", None)
created = False created = False
user = user_model.objects.filter(username=username).first() user = user_model.objects.filter(username=username).first()

View File

@ -1,15 +1,15 @@
import djclick as click import djclick as click
from django.conf import settings from django.conf import settings
from django.core.management import call_command from django.core.management import call_command
from django.db import transaction, connection from django.db import connection, transaction
def reset_schema(db_config_user): def reset_schema(db_config_user):
sql_list = ( sql_list = (
'DROP SCHEMA public CASCADE', "DROP SCHEMA public CASCADE",
f'CREATE SCHEMA public AUTHORIZATION {db_config_user}', f"CREATE SCHEMA public AUTHORIZATION {db_config_user}",
'GRANT ALL ON SCHEMA public TO postgres', "GRANT ALL ON SCHEMA public TO postgres",
'GRANT ALL ON SCHEMA public TO public', "GRANT ALL ON SCHEMA public TO public",
"COMMENT ON SCHEMA public IS 'standard public schema';", "COMMENT ON SCHEMA public IS 'standard public schema';",
) )
@ -21,11 +21,11 @@ def reset_schema(db_config_user):
@click.command() @click.command()
def command(): def command():
user = settings.DATABASES['default']['USER'] user = settings.DATABASES["default"]["USER"]
print(user) print(user)
reset_schema(db_config_user=user) reset_schema(db_config_user=user)
call_command('createcachetable') call_command("createcachetable")
call_command('migrate') call_command("migrate")
call_command('create_default_users') call_command("create_default_users")
call_command('create_default_learning_path') call_command("create_default_learning_path")

View File

@ -3,13 +3,17 @@ from django.contrib.auth.models import AbstractUser
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
def create_or_update_by_email(self, email: str, first_name: str, last_name: str, username: str) -> tuple[ def create_or_update_by_email(
AbstractUser, bool]: self, email: str, first_name: str, last_name: str, username: str
) -> tuple[AbstractUser, bool]:
# create or sync user with OpenID Data # create or sync user with OpenID Data
user, created = self.model.objects.get_or_create(email=email, defaults={ user, created = self.model.objects.get_or_create(
"first_name": first_name, email=email,
"last_name": last_name, defaults={
"username": username "first_name": first_name,
}) "last_name": last_name,
"username": username,
},
)
return user, created return user, created

View File

@ -55,13 +55,15 @@ class UserLoggedInCookieMiddleWare(MiddlewareMixin):
If the user is not authenticated and the cookie remains, delete it If the user is not authenticated and the cookie remains, delete it
""" """
cookie_name = 'loginStatus' cookie_name = "loginStatus"
def process_response(self, request, response): def process_response(self, request, response):
# if user and no cookie, set cookie # if user and no cookie, set cookie
if request.user.is_authenticated and not request.COOKIES.get(self.cookie_name): if request.user.is_authenticated and not request.COOKIES.get(self.cookie_name):
response.set_cookie(self.cookie_name, 'true') response.set_cookie(self.cookie_name, "true")
elif not request.user.is_authenticated and request.COOKIES.get(self.cookie_name): elif not request.user.is_authenticated and request.COOKIES.get(
self.cookie_name
):
# else if no user and cookie remove user cookie, logout # else if no user and cookie remove user cookie, logout
response.delete_cookie(self.cookie_name) response.delete_cookie(self.cookie_name)
return response return response

View File

@ -2,8 +2,8 @@
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

Some files were not shown because too many files have changed in this diff Show More