Add code formatting with prettier and black
This commit is contained in:
parent
38c8d2120a
commit
827e7a0fc0
|
|
@ -283,6 +283,9 @@ cypress/videos
|
|||
cypress/screenshots
|
||||
cypress/test-reports
|
||||
|
||||
git-crypt-encrypted-files-check.txt
|
||||
|
||||
|
||||
/server/vbv_lernwelt/static/css/tailwind.css
|
||||
/server/vbv_lernwelt/static/vue/
|
||||
/server/vbv_lernwelt/templates/vue/index.html
|
||||
|
|
|
|||
29
README.md
29
README.md
|
|
@ -1,6 +1,8 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -28,7 +30,6 @@ export IT_APP_ENVIRONMENT=development
|
|||
See `.env_secrets/local_daniel.env` for more possible environment variables.
|
||||
Especially set correct values for `POSTGRES_*` and `DATABASE_URL`
|
||||
|
||||
|
||||
### Server part
|
||||
|
||||
Install python dependencies:
|
||||
|
|
@ -37,7 +38,8 @@ Install python dependencies:
|
|||
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.
|
||||
|
||||
```bash
|
||||
|
|
@ -61,13 +63,32 @@ npm run dev
|
|||
|
||||
### 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
|
||||
# in project root directory
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,19 @@ pipelines:
|
|||
- step:
|
||||
name: python tests
|
||||
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:
|
||||
- postgres
|
||||
caches:
|
||||
|
|
@ -17,8 +30,7 @@ pipelines:
|
|||
- 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
|
||||
- trufflehog --exclude_paths trufflehog-exclude-patterns.txt --allow trufflehog-allow.json --entropy=True --max_depth=100 .
|
||||
- ./server/run_tests_coverage.sh
|
||||
# - ./src/run_pylint.sh
|
||||
- ufmt check server
|
||||
- step:
|
||||
name: js tests
|
||||
max-time: 15
|
||||
|
|
@ -30,6 +42,18 @@ pipelines:
|
|||
- pwd
|
||||
- npm install
|
||||
- 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:
|
||||
name: cypress tests
|
||||
max-time: 45
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution');
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
'root': true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
// "@vue/eslint-config-prettier"
|
||||
root: true,
|
||||
extends: [
|
||||
"plugin:vue/vue3-recommended",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript/recommended",
|
||||
"@vue/eslint-config-prettier",
|
||||
],
|
||||
'env': {
|
||||
'vue/setup-compiler-macros': true
|
||||
env: {
|
||||
"vue/setup-compiler-macros": true,
|
||||
},
|
||||
'rules': {
|
||||
'@typescript-eslint/no-unused-vars': ['warn'],
|
||||
}
|
||||
}
|
||||
ignorePatterns: ["versionize.js", "tailwind.config.js", "postcss.config.js"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": ["warn"],
|
||||
"@typescript-eslint/ban-ts-comment": ["warn"],
|
||||
"prefer-const": ["warn"],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
dist
|
||||
node_modules
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 88,
|
||||
"organizeImportsSkipDestructiveCodeActions": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
{
|
||||
"recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"]
|
||||
"recommendations": [
|
||||
"johnsoncodehk.volar",
|
||||
"johnsoncodehk.vscode-typescript-vue-plugin"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ VBV Frontend
|
|||
`npm run dev`
|
||||
|
||||
## Vue layouts
|
||||
|
||||
[How layouts are implemented](https://itnext.io/vue-tricks-smart-layouts-for-vuejs-5c61a472b69b)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- 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>
|
||||
<!-- end workaround -->
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
"coverage": "vitest run --coverage",
|
||||
"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",
|
||||
"prettier": "prettier . --write",
|
||||
"prettier:check": "prettier . --check",
|
||||
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -33,17 +35,20 @@
|
|||
"@types/lodash": "^4.14.184",
|
||||
"@types/node": "^18.7.14",
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"@volar/vue-typescript": "^0.40.13",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/test-utils": "^2.0.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-vue": "^9.4.0",
|
||||
"jsdom": "^20.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-import": "^14.1.0",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.1.1",
|
||||
"replace-in-file": "^6.3.5",
|
||||
"sass": "^1.54.6",
|
||||
"sass-loader": "^12.6.0",
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ module.exports = {
|
|||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,28 +1,26 @@
|
|||
<template>
|
||||
<div id="app" class="flex flex-col min-h-screen">
|
||||
<MainNavigationBar class="flex-none" />
|
||||
<RouterView class="flex-auto" v-slot="{ Component }">
|
||||
<RouterView v-slot="{ Component }" class="flex-auto">
|
||||
<Transition mode="out-in" name="app">
|
||||
<component :is="Component"></component>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
<Footer class="flex-none" />
|
||||
<AppFooter class="flex-none" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as log from 'loglevel';
|
||||
import * as log from "loglevel";
|
||||
|
||||
import MainNavigationBar from '@/components/MainNavigationBar.vue';
|
||||
import Footer from '@/components/Footer.vue';
|
||||
import {onMounted} from 'vue';
|
||||
import MainNavigationBar from "@/components/MainNavigationBar.vue";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
log.debug('App created');
|
||||
log.debug("App created");
|
||||
|
||||
onMounted(() => {
|
||||
log.debug('App mounted');
|
||||
log.debug("App mounted");
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
|
@ -35,5 +33,4 @@ onMounted(() => {
|
|||
.app-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import * as log from "loglevel";
|
||||
|
||||
log.debug('Footer created')
|
||||
log.debug("AppFooter created");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -1,91 +1,93 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import * as log from "loglevel";
|
||||
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useLearningPathStore } from '@/stores/learningPath'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import IconLogout from '@/components/icons/IconLogout.vue'
|
||||
import IconSettings from '@/components/icons/IconSettings.vue'
|
||||
import ItDropdown from '@/components/ui/ItDropdown.vue'
|
||||
import MobileMenu from '@/components/MobileMenu.vue'
|
||||
import IconLogout from "@/components/icons/IconLogout.vue";
|
||||
import IconSettings from "@/components/icons/IconSettings.vue";
|
||||
import MobileMenu from "@/components/MobileMenu.vue";
|
||||
import ItDropdown from "@/components/ui/ItDropdown.vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { onMounted, reactive } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
log.debug('MainNavigationBar created')
|
||||
log.debug("MainNavigationBar created");
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
const learningPathStore = useLearningPathStore()
|
||||
const state = reactive({ showMenu: false })
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const appStore = useAppStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const state = reactive({ showMenu: false });
|
||||
|
||||
function toggleNav() {
|
||||
state.showMenu = !state.showMenu
|
||||
state.showMenu = !state.showMenu;
|
||||
}
|
||||
|
||||
function isInRoutePath(checkPaths: string[]) {
|
||||
return checkPaths.some((path) => route.path.startsWith(path))
|
||||
return checkPaths.some((path) => route.path.startsWith(path));
|
||||
}
|
||||
|
||||
function inLearningPath() {
|
||||
return isInRoutePath(['/learn/'])
|
||||
return isInRoutePath(["/learn/"]);
|
||||
}
|
||||
|
||||
function getLearningPathStringProp(prop: 'title' | 'slug'): string {
|
||||
return inLearningPath() && learningPathStore.learningPath ? learningPathStore.learningPath[prop] : ''
|
||||
function getLearningPathStringProp(prop: "title" | "slug"): string {
|
||||
return inLearningPath() && learningPathStore.learningPath
|
||||
? learningPathStore.learningPath[prop]
|
||||
: "";
|
||||
}
|
||||
|
||||
function learningPathName(): string {
|
||||
return getLearningPathStringProp('title')
|
||||
return getLearningPathStringProp("title");
|
||||
}
|
||||
|
||||
function learninPathSlug(): string {
|
||||
return getLearningPathStringProp('slug')
|
||||
return getLearningPathStringProp("slug");
|
||||
}
|
||||
|
||||
function handleDropdownSelect(data) {
|
||||
log.debug('Selected action:', data.action)
|
||||
log.debug("Selected action:", data.action);
|
||||
switch (data.action) {
|
||||
case 'settings':
|
||||
router.push('/profile')
|
||||
break
|
||||
case 'logout':
|
||||
userStore.handleLogout()
|
||||
break
|
||||
case "settings":
|
||||
router.push("/profile");
|
||||
break;
|
||||
case "logout":
|
||||
userStore.handleLogout();
|
||||
break;
|
||||
default:
|
||||
console.log('no action')
|
||||
console.log("no action");
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
userStore.handleLogout()
|
||||
userStore.handleLogout();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
log.debug('MainNavigationBar mounted')
|
||||
})
|
||||
log.debug("MainNavigationBar mounted");
|
||||
});
|
||||
|
||||
const profileDropdownData = [
|
||||
[
|
||||
{
|
||||
title: 'Kontoeinstellungen',
|
||||
title: "Kontoeinstellungen",
|
||||
icon: IconSettings,
|
||||
data: {
|
||||
action: 'settings',
|
||||
action: "settings",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Abmelden',
|
||||
title: "Abmelden",
|
||||
icon: IconLogout,
|
||||
data: {
|
||||
action: 'logout',
|
||||
action: "logout",
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -108,7 +110,9 @@ const profileDropdownData = [
|
|||
<it-icon-vbv class="h-8 w-16 mr-3 -mt-6 -ml-3" />
|
||||
</a>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -122,7 +126,7 @@ const profileDropdownData = [
|
|||
<it-icon-message class="w-8 h-8 mr-6" />
|
||||
</router-link>
|
||||
<!-- Mobile menu button -->
|
||||
<div @click="toggleNav" class="flex">
|
||||
<div class="flex" @click="toggleNav">
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
|
||||
<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
|
||||
</router-link>
|
||||
<router-link
|
||||
|
|
@ -168,10 +176,14 @@ const profileDropdownData = [
|
|||
>
|
||||
Mediathek
|
||||
</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" />
|
||||
</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
|
||||
:button-classes="[]"
|
||||
:list-items="profileDropdownData"
|
||||
|
|
@ -179,7 +191,11 @@ const profileDropdownData = [
|
|||
@select="handleDropdownSelect"
|
||||
>
|
||||
<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 v-else>
|
||||
{{ userStore.getFullName }}
|
||||
|
|
|
|||
|
|
@ -1,80 +1,80 @@
|
|||
<script setup lang="ts">
|
||||
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue'
|
||||
import IconLogout from '@/components/icons/IconLogout.vue'
|
||||
import IconSettings from '@/components/icons/IconSettings.vue'
|
||||
import {useRouter} from "vue-router";
|
||||
import IconLogout from "@/components/icons/IconLogout.vue";
|
||||
import IconSettings from "@/components/icons/IconSettings.vue";
|
||||
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean,
|
||||
userStore: object,
|
||||
learningPathName: string,
|
||||
learningPathSlug: string
|
||||
}>()
|
||||
show: boolean;
|
||||
userStore: object;
|
||||
learningPathName: string;
|
||||
learningPathSlug: string;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['closemodal'])
|
||||
const emits = defineEmits(["closemodal"]);
|
||||
|
||||
const clickLink = (to: string) => {
|
||||
router.push(to)
|
||||
emits('closemodal')
|
||||
}
|
||||
|
||||
router.push(to);
|
||||
emits("closemodal");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ItFullScreenModal
|
||||
:show="show"
|
||||
@closemodal="$emit('closemodal')"
|
||||
>
|
||||
<div>
|
||||
<ItFullScreenModal :show="show" @closemodal="$emit('closemodal')">
|
||||
<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
|
||||
class="mt-6 pb-6 border-b border-gray-500"
|
||||
v-if="learningPathName">
|
||||
<h4 class="text-gray-900 text-sm">Kurs: {{learningPathName}}</h4>
|
||||
<ul class="mt-6">
|
||||
<li><button @click="clickLink(`/learningpath/${learningPathSlug}`)">Lernpfad</button></li>
|
||||
<li class="mt-6">Kompetenzprofil</li>
|
||||
</ul>
|
||||
<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
|
||||
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 class="mt-6 pb-6 border-b border-gray-500">
|
||||
<ul>
|
||||
<li>Shop</li>
|
||||
<li class="mt-6">Mediathek</li>
|
||||
</ul>
|
||||
<div>
|
||||
<div v-if="learningPathName" class="mt-6 pb-6 border-b border-gray-500">
|
||||
<h4 class="text-gray-900 text-sm">Kurs: {{ learningPathName }}</h4>
|
||||
<ul class="mt-6">
|
||||
<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>
|
||||
<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>
|
||||
</ItFullScreenModal>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import { describe, it } from 'vitest'
|
||||
import MainNavigationBar from '../MainNavigationBar.vue'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
describe('MainNavigationBar', () => {
|
||||
describe("MainNavigationBar", () => {
|
||||
beforeEach(() => {
|
||||
// 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:
|
||||
// `useStore(pinia)`
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it('renders properly', () => {
|
||||
expect(42).toBe(42)
|
||||
it("renders properly", () => {
|
||||
expect(42).toBe(42);
|
||||
// const wrapper = mount(MainNavigationBar, {})
|
||||
// expect(wrapper.text()).toContain('myVBV')
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import * as d3 from "d3";
|
||||
import {computed, onMounted} from "vue";
|
||||
import * as _ from 'lodash'
|
||||
import {useCircleStore} from '@/stores/circle';
|
||||
import * as log from 'loglevel';
|
||||
import * as _ from "lodash";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
|
||||
import colors from "@/colors.json";
|
||||
|
||||
|
|
@ -11,65 +11,71 @@ const circleStore = useCircleStore();
|
|||
|
||||
function someFinished(learningSequence) {
|
||||
if (circleStore.circle) {
|
||||
return circleStore.circle.someFinishedInLearningSequence(learningSequence.translation_key);
|
||||
return circleStore.circle.someFinishedInLearningSequence(
|
||||
learningSequence.translation_key
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function allFinished(learningSequence) {
|
||||
if (circleStore.circle) {
|
||||
return circleStore.circle.allFinishedInLearningSequence(learningSequence.translation_key);
|
||||
return circleStore.circle.allFinishedInLearningSequence(
|
||||
learningSequence.translation_key
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug('CircleDiagram mounted');
|
||||
log.debug("CircleDiagram mounted");
|
||||
render();
|
||||
});
|
||||
|
||||
|
||||
const pieData = computed(() => {
|
||||
const circle = circleStore.circle
|
||||
console.log('initial of compute pie data ', circle)
|
||||
const circle = circleStore.circle;
|
||||
console.log("initial of compute pie data ", 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 pieGenerator = d3.pie()
|
||||
let angles = pieGenerator(pieWeights)
|
||||
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1);
|
||||
const pieGenerator = d3.pie();
|
||||
let angles = pieGenerator(pieWeights);
|
||||
_.forEach(angles, (pie) => {
|
||||
const thisLearningSequence = circle.learningSequences[parseInt(pie.index)]
|
||||
pie.title = thisLearningSequence.title
|
||||
pie.icon = thisLearningSequence.icon
|
||||
pie.startAngle = pie.startAngle + Math.PI
|
||||
pie.endAngle = pie.endAngle + Math.PI
|
||||
pie.arrowStartAngle = pie.endAngle + (pie.startAngle - pie.endAngle) / 2
|
||||
pie.arrowEndAngle = pie.startAngle + (pie.startAngle - pie.endAngle) / 2
|
||||
pie.translation_key = thisLearningSequence.translation_key
|
||||
pie.slug = thisLearningSequence.slug
|
||||
pie.someFinished = someFinished(thisLearningSequence)
|
||||
pie.allFinished = allFinished(thisLearningSequence)
|
||||
})
|
||||
angles = angles.reverse()
|
||||
return angles
|
||||
const thisLearningSequence = circle.learningSequences[parseInt(pie.index)];
|
||||
pie.title = thisLearningSequence.title;
|
||||
pie.icon = thisLearningSequence.icon;
|
||||
pie.startAngle = pie.startAngle + Math.PI;
|
||||
pie.endAngle = pie.endAngle + Math.PI;
|
||||
pie.arrowStartAngle = pie.endAngle + (pie.startAngle - pie.endAngle) / 2;
|
||||
pie.arrowEndAngle = pie.startAngle + (pie.startAngle - pie.endAngle) / 2;
|
||||
pie.translation_key = thisLearningSequence.translation_key;
|
||||
pie.slug = thisLearningSequence.slug;
|
||||
pie.someFinished = someFinished(thisLearningSequence);
|
||||
pie.allFinished = allFinished(thisLearningSequence);
|
||||
});
|
||||
angles = angles.reverse();
|
||||
return angles;
|
||||
}
|
||||
return {}
|
||||
})
|
||||
return {};
|
||||
});
|
||||
|
||||
const width = 450
|
||||
const height = 450
|
||||
const radius = Math.min(width, height) / 2.4
|
||||
const width = 450;
|
||||
const height = 450;
|
||||
const radius = Math.min(width, height) / 2.4;
|
||||
|
||||
function render() {
|
||||
const arrowStrokeWidth = 2
|
||||
const arrowStrokeWidth = 2;
|
||||
|
||||
const svg = d3.select('.circle-visualization')
|
||||
.attr('viewBox', `0 0 ${width} ${height}`)
|
||||
const svg = d3
|
||||
.select(".circle-visualization")
|
||||
.attr("viewBox", `0 0 ${width} ${height}`);
|
||||
|
||||
// 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("refX", 11)
|
||||
.attr("refY", 11)
|
||||
|
|
@ -79,12 +85,13 @@ function render() {
|
|||
.attr("orient", "auto")
|
||||
.append("path")
|
||||
.attr("d", "M -1 0 l 10 0 M 0 -1 l 0 10")
|
||||
.attr('transform', 'rotate(-90, 10, 0)')
|
||||
.attr('stroke-width', arrowStrokeWidth)
|
||||
.attr('stroke', colors.gray[500])
|
||||
|
||||
const g = svg.append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
|
||||
.attr("transform", "rotate(-90, 10, 0)")
|
||||
.attr("stroke-width", arrowStrokeWidth)
|
||||
.attr("stroke", colors.gray[500]);
|
||||
|
||||
const g = svg
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
|
||||
|
||||
function getColor(d) {
|
||||
let color = colors.gray[300];
|
||||
|
|
@ -94,7 +101,7 @@ function render() {
|
|||
if (d.allFinished) {
|
||||
color = colors.green[500];
|
||||
}
|
||||
return color
|
||||
return color;
|
||||
}
|
||||
|
||||
function getHoverColor(d) {
|
||||
|
|
@ -105,7 +112,7 @@ function render() {
|
|||
if (d.allFinished) {
|
||||
color = colors.green[400];
|
||||
}
|
||||
return color
|
||||
return color;
|
||||
}
|
||||
|
||||
// Generate the pie diagram wede
|
||||
|
|
@ -113,84 +120,80 @@ function render() {
|
|||
.arc()
|
||||
.innerRadius(radius / 2.5)
|
||||
.padAngle(12 / 360)
|
||||
.outerRadius(radius)
|
||||
|
||||
.outerRadius(radius);
|
||||
|
||||
// Generate the arrows
|
||||
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 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]);
|
||||
|
||||
learningSequences
|
||||
.on('mouseover', function (d, i) {
|
||||
.on("mouseover", function (d, i) {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration('200')
|
||||
.attr('fill', (d) => {
|
||||
return getHoverColor(d)
|
||||
})
|
||||
|
||||
|
||||
.duration("200")
|
||||
.attr("fill", (d) => {
|
||||
return getHoverColor(d);
|
||||
});
|
||||
})
|
||||
.on('mouseout', function (d, i) {
|
||||
.on("mouseout", function (d, i) {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration('200')
|
||||
.attr('fill', (d) => {
|
||||
return getColor(d)
|
||||
})
|
||||
.duration("200")
|
||||
.attr("fill", (d) => {
|
||||
return getColor(d);
|
||||
});
|
||||
})
|
||||
.on('click', function (d, elm) {
|
||||
console.log('clicked on ', d, elm)
|
||||
document.getElementById(elm.slug)?.scrollIntoView({behavior: 'smooth'})
|
||||
})
|
||||
|
||||
.on("click", function (d, elm) {
|
||||
console.log("clicked on ", d, elm);
|
||||
document.getElementById(elm.slug)?.scrollIntoView({ behavior: "smooth" });
|
||||
});
|
||||
|
||||
learningSequences
|
||||
.transition()
|
||||
.duration(1)
|
||||
.attr('fill', (d) => {
|
||||
return getColor(d)
|
||||
})
|
||||
|
||||
|
||||
learningSequences.append('path').attr('d', wedgeGenerator)
|
||||
.attr("fill", (d) => {
|
||||
return getColor(d);
|
||||
});
|
||||
|
||||
learningSequences.append("path").attr("d", wedgeGenerator);
|
||||
|
||||
const learningSequenceText = learningSequences
|
||||
.append('text')
|
||||
.attr('fill', colors.blue[900])
|
||||
.style('font-size', '15px')
|
||||
.append("text")
|
||||
.attr("fill", colors.blue[900])
|
||||
.style("font-size", "15px")
|
||||
.text((d) => {
|
||||
return d.title
|
||||
return d.title;
|
||||
})
|
||||
.attr("transform", function (d) {
|
||||
let translate = wedgeGenerator.centroid(d)
|
||||
translate = [translate[0], translate[1] + 20]
|
||||
let translate = wedgeGenerator.centroid(d);
|
||||
translate = [translate[0], translate[1] + 20];
|
||||
return "translate(" + translate + ")";
|
||||
})
|
||||
.attr('class', 'circlesText text-xl font-bold')
|
||||
.style('text-anchor', 'middle')
|
||||
.attr("class", "circlesText text-xl font-bold")
|
||||
.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) => {
|
||||
return "/static/icons/" + d.icon.replace("it-", "") + ".svg"
|
||||
return "/static/icons/" + d.icon.replace("it-", "") + ".svg";
|
||||
})
|
||||
.attr("width", iconWidth)
|
||||
.attr("height", iconWidth)
|
||||
.attr("transform", function (d) {
|
||||
let translate = wedgeGenerator.centroid(d)
|
||||
translate = [translate[0] - iconWidth / 2, translate[1] - iconWidth]
|
||||
let translate = wedgeGenerator.centroid(d);
|
||||
translate = [translate[0] - iconWidth / 2, translate[1] - iconWidth];
|
||||
return "translate(" + translate + ")";
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
// Create Arrows
|
||||
const arrow = d3
|
||||
|
|
@ -198,20 +201,19 @@ function render() {
|
|||
.innerRadius(arrowRadius)
|
||||
.outerRadius(arrowRadius + arrowStrokeWidth)
|
||||
.padAngle(20 / 360)
|
||||
.startAngle(d => {
|
||||
return d.arrowStartAngle
|
||||
.startAngle((d) => {
|
||||
return d.arrowStartAngle;
|
||||
})
|
||||
.endAngle(d => {
|
||||
return d.arrowEndAngle
|
||||
})
|
||||
|
||||
.endAngle((d) => {
|
||||
return d.arrowEndAngle;
|
||||
});
|
||||
|
||||
const arrows = g
|
||||
.selectAll('.arrow')
|
||||
.selectAll(".arrow")
|
||||
.data(pieData.value)
|
||||
.join('g')
|
||||
.attr('class', 'arrow')
|
||||
.attr('marker-end', 'url(#triangle)')
|
||||
.join("g")
|
||||
.attr("class", "arrow")
|
||||
.attr("marker-end", "url(#triangle)");
|
||||
|
||||
// remove last arrow
|
||||
d3.selection.prototype.last = function () {
|
||||
|
|
@ -219,24 +221,34 @@ function render() {
|
|||
return d3.select(this.nodes()[last]);
|
||||
};
|
||||
|
||||
const all_arows = g.selectAll('.arrow')
|
||||
all_arows.last().remove()
|
||||
const all_arows = g.selectAll(".arrow");
|
||||
all_arows.last().remove();
|
||||
|
||||
//Draw arrow paths
|
||||
arrows.append('path').attr('fill', colors.gray[500]).attr('d', arrow)
|
||||
return svg
|
||||
arrows.append("path").attr("fill", colors.gray[500]).attr("d", arrow);
|
||||
return svg;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="svg-container h-full content-center">
|
||||
<pre hidden>{{ pieData }}</pre>
|
||||
<pre hidden>{{ render() }}</pre>
|
||||
<svg class="circle-visualization h-full">
|
||||
<circle 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"/>
|
||||
<circle
|
||||
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>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,35 +1,44 @@
|
|||
<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<{
|
||||
circle: Circle | undefined
|
||||
show: boolean
|
||||
}>()
|
||||
circle: Circle | undefined;
|
||||
show: boolean;
|
||||
}>();
|
||||
|
||||
// const emits = defineEmits(['closemodal'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<h3>Du wirst in der Lage sein, ...</h3>
|
||||
|
||||
<ul class="mt-4">
|
||||
<li class="text-xl flex items-center" v-for="goal in circle.goals" :key="goal.id">
|
||||
<it-icon-check class="mt-4 hidden lg:block w-12 h-12 text-sky-500 flex-none"></it-icon-check>
|
||||
<li
|
||||
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>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<ul class="grid grid-cols-1 lg:grid-cols-3 auto-rows-fr gap-6 mt-8">
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import { computed } from 'vue'
|
||||
import type { LearningContent } from '@/types'
|
||||
import { useCircleStore } from '@/stores/circle'
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningContent } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
|
||||
log.debug('LearningContent.vue setup')
|
||||
log.debug("LearningContent.vue setup");
|
||||
|
||||
const circleStore = useCircleStore()
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const props = defineProps<{
|
||||
learningContent: LearningContent
|
||||
}>()
|
||||
learningContent: LearningContent;
|
||||
}>();
|
||||
|
||||
const block = computed(() => {
|
||||
if (props.learningContent?.contents?.length) {
|
||||
return props.learningContent.contents[0]
|
||||
return props.learningContent.contents[0];
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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
|
||||
type="button"
|
||||
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>
|
||||
</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
|
||||
type="button"
|
||||
|
|
@ -64,7 +68,6 @@ const block = computed(() => {
|
|||
>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,48 @@
|
|||
<script setup lang="ts">
|
||||
import type { LearningContentType } from '@/types'
|
||||
import { learningContentTypesToName } from '@/utils/typeMaps'
|
||||
import type { LearningContentType } from "@/types";
|
||||
import { learningContentTypesToName } from "@/utils/typeMaps";
|
||||
|
||||
const props = defineProps<{
|
||||
learningContentType: LearningContentType
|
||||
}>()
|
||||
learningContentType: LearningContentType;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex bg-gray-200 rounded-full px-2.5 py-0.5 gap-2 items-center w-min h-min">
|
||||
<it-icon-lc-assignment class="w-6 h-6" v-if="props.learningContentType === 'assignment'" />
|
||||
<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-video class="w-6 h-6" v-else-if="props.learningContentType === 'video'" />
|
||||
<it-icon-lc-media-library class="w-6 h-6" v-else-if="props.learningContentType === 'media_library'" />
|
||||
<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-resource class="w-6 h-6" v-else-if="props.learningContentType === 'resource'" />
|
||||
<it-icon-lc-document class="w-6 h-6" v-else-if="props.learningContentType === 'document'" />
|
||||
<p class="whitespace-nowrap">{{ learningContentTypesToName.get(props.learningContentType) }}</p>
|
||||
<div
|
||||
class="flex bg-gray-200 rounded-full px-2.5 py-0.5 gap-2 items-center w-min h-min"
|
||||
>
|
||||
<it-icon-lc-assignment
|
||||
v-if="props.learningContentType === 'assignment'"
|
||||
class="w-6 h-6"
|
||||
/>
|
||||
<it-icon-lc-exercise
|
||||
v-else-if="props.learningContentType === 'exercise'"
|
||||
class="w-6 h-6"
|
||||
/>
|
||||
<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>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import * as d3 from 'd3'
|
||||
import { useLearningPathStore } from '@/stores/learningPath'
|
||||
import colors from '@/colors.json'
|
||||
import * as log from 'loglevel'
|
||||
import colors from "@/colors.json";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import * as d3 from "d3";
|
||||
import * as log from "loglevel";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
|
@ -15,294 +15,334 @@ export default {
|
|||
type: String,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const learningPathStore = useLearningPathStore();
|
||||
return { learningPathStore };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: 1640,
|
||||
height: 384,
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const learningPathStore = useLearningPathStore()
|
||||
return { learningPathStore }
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
viewBox() {
|
||||
return `0 0 ${this.width} ${this.height}`
|
||||
return `0 0 ${this.width} ${this.height}`;
|
||||
},
|
||||
circles() {
|
||||
function someFinished(circle, learningSequence) {
|
||||
if (circle) {
|
||||
return circle.someFinishedInLearningSequence(learningSequence.translation_key)
|
||||
return circle.someFinishedInLearningSequence(
|
||||
learningSequence.translation_key
|
||||
);
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
function allFinished(circle, learningSequence) {
|
||||
if (circle) {
|
||||
return circle.allFinishedInLearningSequence(learningSequence.translation_key)
|
||||
return circle.allFinishedInLearningSequence(learningSequence.translation_key);
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.learningPathStore.learningPath) {
|
||||
const internalCircles = []
|
||||
const internalCircles = [];
|
||||
this.learningPathStore.learningPath.circles.forEach((circle) => {
|
||||
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1)
|
||||
const pieGenerator = d3.pie()
|
||||
const pieData = pieGenerator(pieWeights)
|
||||
const pieWeights = new Array(
|
||||
Math.max(circle.learningSequences.length, 1)
|
||||
).fill(1);
|
||||
const pieGenerator = d3.pie();
|
||||
const pieData = pieGenerator(pieWeights);
|
||||
pieData.forEach((pie) => {
|
||||
const thisLearningSequence = circle.learningSequences[parseInt(pie.index)]
|
||||
pie.startAngle = pie.startAngle + Math.PI
|
||||
pie.endAngle = pie.endAngle + Math.PI
|
||||
pie.done = circle.someFinishedInLearningSequence(thisLearningSequence.translation_key)
|
||||
pie.someFinished = someFinished(circle, thisLearningSequence)
|
||||
pie.allFinished = allFinished(circle, thisLearningSequence)
|
||||
})
|
||||
const newCircle = {}
|
||||
newCircle.pieData = pieData.reverse()
|
||||
newCircle.title = circle.title
|
||||
newCircle.slug = circle.slug.replace(`${circle.parentLearningPath.slug}-circle-`, '')
|
||||
newCircle.id = circle.id
|
||||
internalCircles.push(newCircle)
|
||||
})
|
||||
return internalCircles
|
||||
const thisLearningSequence = circle.learningSequences[parseInt(pie.index)];
|
||||
pie.startAngle = pie.startAngle + Math.PI;
|
||||
pie.endAngle = pie.endAngle + Math.PI;
|
||||
pie.done = circle.someFinishedInLearningSequence(
|
||||
thisLearningSequence.translation_key
|
||||
);
|
||||
pie.someFinished = someFinished(circle, thisLearningSequence);
|
||||
pie.allFinished = allFinished(circle, thisLearningSequence);
|
||||
});
|
||||
const newCircle = {};
|
||||
newCircle.pieData = pieData.reverse();
|
||||
newCircle.title = circle.title;
|
||||
newCircle.slug = circle.slug.replace(
|
||||
`${circle.parentLearningPath.slug}-circle-`,
|
||||
""
|
||||
);
|
||||
newCircle.id = circle.id;
|
||||
internalCircles.push(newCircle);
|
||||
});
|
||||
return internalCircles;
|
||||
}
|
||||
return []
|
||||
return [];
|
||||
},
|
||||
svg() {
|
||||
return d3.select('#' + this.identifier)
|
||||
return d3.select("#" + this.identifier);
|
||||
},
|
||||
|
||||
learningPath() {
|
||||
return Object.assign({}, this.learningPathStore.learningPath)
|
||||
return Object.assign({}, this.learningPathStore.learningPath);
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
log.debug('LearningPathDiagram mounted')
|
||||
log.debug("LearningPathDiagram mounted");
|
||||
|
||||
if (this.vertical) {
|
||||
this.width = Math.min(960, window.innerWidth - 32)
|
||||
this.height = 860
|
||||
this.width = Math.min(960, window.innerWidth - 32);
|
||||
this.height = 860;
|
||||
}
|
||||
|
||||
const circleWidth = this.vertical ? 60 : 200
|
||||
const radius = (circleWidth * 0.8) / 2
|
||||
const circleWidth = this.vertical ? 60 : 200;
|
||||
const radius = (circleWidth * 0.8) / 2;
|
||||
|
||||
function getColor(d) {
|
||||
let color = colors.gray[300]
|
||||
let color = colors.gray[300];
|
||||
if (d.someFinished) {
|
||||
color = colors.sky[500]
|
||||
color = colors.sky[500];
|
||||
}
|
||||
if (d.allFinished) {
|
||||
color = colors.green[500]
|
||||
color = colors.green[500];
|
||||
}
|
||||
return color
|
||||
return color;
|
||||
}
|
||||
|
||||
function getHoverColor(d) {
|
||||
let color = colors.gray[200]
|
||||
let color = colors.gray[200];
|
||||
if (d.someFinished) {
|
||||
color = colors.sky[400]
|
||||
color = colors.sky[400];
|
||||
}
|
||||
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
|
||||
const circle_groups = this.svg
|
||||
.selectAll('.circle')
|
||||
.selectAll(".circle")
|
||||
.data(this.circles)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'circle')
|
||||
.attr('data-cy', (d) => {
|
||||
.append("g")
|
||||
.attr("class", "circle")
|
||||
.attr("data-cy", (d) => {
|
||||
if (this.vertical) {
|
||||
return `circle-${d.slug}-vertical`
|
||||
return `circle-${d.slug}-vertical`;
|
||||
} else {
|
||||
return `circle-${d.slug}`
|
||||
return `circle-${d.slug}`;
|
||||
}
|
||||
})
|
||||
.on('mouseover', function (d, i) {
|
||||
.on("mouseover", function (d, i) {
|
||||
d3.select(this)
|
||||
.selectAll('.learningSegmentArc')
|
||||
.selectAll(".learningSegmentArc")
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('fill', (d) => {
|
||||
return getHoverColor(d)
|
||||
})
|
||||
.attr("fill", (d) => {
|
||||
return getHoverColor(d);
|
||||
});
|
||||
})
|
||||
.on('mouseout', function (d, i) {
|
||||
.on("mouseout", function (d, i) {
|
||||
d3.select(this)
|
||||
.selectAll('.learningSegmentArc')
|
||||
.selectAll(".learningSegmentArc")
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('fill', (d) => {
|
||||
return getColor(d)
|
||||
})
|
||||
.attr("fill", (d) => {
|
||||
return getColor(d);
|
||||
});
|
||||
})
|
||||
.on('click', (d, i) => {
|
||||
vueRouter.push(`/learn/${this.learningPathStore.learningPath.slug}/${i.slug}`)
|
||||
.on("click", (d, i) => {
|
||||
vueRouter.push(`/learn/${this.learningPathStore.learningPath.slug}/${i.slug}`);
|
||||
})
|
||||
|
||||
.attr('role', 'button')
|
||||
.attr("role", "button");
|
||||
|
||||
const arcGenerator = d3
|
||||
.arc()
|
||||
.innerRadius(radius / 2)
|
||||
.padAngle(12 / 360)
|
||||
.outerRadius(radius)
|
||||
.outerRadius(radius);
|
||||
|
||||
//Generate groups
|
||||
const arcs = this.svg
|
||||
.selectAll('g')
|
||||
.selectAll('.learningSegmentArc')
|
||||
.selectAll("g")
|
||||
.selectAll(".learningSegmentArc")
|
||||
.data((d) => {
|
||||
return d.pieData
|
||||
return d.pieData;
|
||||
})
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'learningSegmentArc')
|
||||
.attr('fill', colors.gray[300])
|
||||
.append("g")
|
||||
.attr("class", "learningSegmentArc")
|
||||
.attr("fill", colors.gray[300]);
|
||||
|
||||
arcs
|
||||
.transition()
|
||||
.duration(1000)
|
||||
.attr('fill', (d) => {
|
||||
return getColor(d)
|
||||
})
|
||||
.attr("fill", (d) => {
|
||||
return getColor(d);
|
||||
});
|
||||
|
||||
//Draw arc paths
|
||||
arcs.append('path').attr('d', arcGenerator)
|
||||
arcs.append("path").attr("d", arcGenerator);
|
||||
|
||||
const circlesText = circle_groups
|
||||
.append('text')
|
||||
.attr('fill', colors.blue[900])
|
||||
.style('font-size', '18px')
|
||||
.style('overflow-wrap', 'break-word')
|
||||
.append("text")
|
||||
.attr("fill", colors.blue[900])
|
||||
.style("font-size", "18px")
|
||||
.style("overflow-wrap", "break-word")
|
||||
.text((d) => {
|
||||
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 topicHeight = 50
|
||||
const circleHeigth = circleWidth + 20
|
||||
const topicHeightOffset = 20;
|
||||
const topicHeight = 50;
|
||||
const circleHeigth = circleWidth + 20;
|
||||
|
||||
function getTopicHorizontalPosition(i, d, topics) {
|
||||
let x = 0
|
||||
let x = 0;
|
||||
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) {
|
||||
let pos = topicHeightOffset
|
||||
let pos = topicHeightOffset;
|
||||
|
||||
for (let index = 0; index < i; index++) {
|
||||
const topic = topics[index]
|
||||
const topic = topics[index];
|
||||
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) {
|
||||
let y = circleHeigth / 2 + topicHeightOffset + 10
|
||||
let y = circleHeigth / 2 + topicHeightOffset + 10;
|
||||
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) {
|
||||
y += topicHeight
|
||||
y += topicHeight;
|
||||
}
|
||||
for (let circle_index = 0; circle_index < topic.circles.length; circle_index++) {
|
||||
const circle = topic.circles[circle_index]
|
||||
for (
|
||||
let circle_index = 0;
|
||||
circle_index < topic.circles.length;
|
||||
circle_index++
|
||||
) {
|
||||
const circle = topic.circles[circle_index];
|
||||
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
|
||||
.append('text')
|
||||
.attr('fill', colors.blue[900])
|
||||
.style('font-size', '16px')
|
||||
.text((d) => d.title)
|
||||
.append("text")
|
||||
.attr("fill", colors.blue[900])
|
||||
.style("font-size", "16px")
|
||||
.text((d) => d.title);
|
||||
|
||||
// Calculate positions of objects
|
||||
|
||||
if (this.vertical) {
|
||||
const Circles_X = radius
|
||||
const Topics_X = Circles_X - radius
|
||||
const Circles_X = radius;
|
||||
const Topics_X = Circles_X - radius;
|
||||
|
||||
circle_groups.attr('transform', (d, i) => {
|
||||
return 'translate(' + Circles_X + ',' + getCircleVerticalPostion(i, d, this.learningPath.topics) + ')'
|
||||
})
|
||||
circle_groups.attr("transform", (d, i) => {
|
||||
return (
|
||||
"translate(" +
|
||||
Circles_X +
|
||||
"," +
|
||||
getCircleVerticalPostion(i, d, this.learningPath.topics) +
|
||||
")"
|
||||
);
|
||||
});
|
||||
|
||||
circlesText
|
||||
.attr('y', 7)
|
||||
.attr('x', radius + 40)
|
||||
.attr('class', 'circlesText text-xl font-bold block')
|
||||
.attr("y", 7)
|
||||
.attr("x", radius + 40)
|
||||
.attr("class", "circlesText text-xl font-bold block");
|
||||
|
||||
topicGroups
|
||||
.attr('transform', (d, i) => {
|
||||
return 'translate(' + Topics_X + ', ' + getTopicVerticalPosition(i, d, this.learningPath.topics) + ')'
|
||||
})
|
||||
.attr('class', (d) => {
|
||||
return 'topic '.concat(d.is_visible ? 'block' : 'hidden')
|
||||
.attr("transform", (d, i) => {
|
||||
return (
|
||||
"translate(" +
|
||||
Topics_X +
|
||||
", " +
|
||||
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 {
|
||||
circle_groups.attr('transform', (d, i) => {
|
||||
const x_coord = (i + 1) * circleWidth - radius
|
||||
return 'translate(' + x_coord + ', 200)'
|
||||
})
|
||||
circle_groups.attr("transform", (d, i) => {
|
||||
const x_coord = (i + 1) * circleWidth - radius;
|
||||
return "translate(" + x_coord + ", 200)";
|
||||
});
|
||||
|
||||
circlesText
|
||||
.attr('y', radius + 30)
|
||||
.style('text-anchor', 'middle')
|
||||
.attr("y", radius + 30)
|
||||
.style("text-anchor", "middle")
|
||||
.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
|
||||
.attr('transform', (d, i) => {
|
||||
return 'translate(' + getTopicHorizontalPosition(i, d, this.learningPathStore.learningPath.topics) + ',0)'
|
||||
})
|
||||
.attr('class', (d) => {
|
||||
return 'topic '.concat(d.is_visible ? 'hidden lg:block' : 'hidden')
|
||||
.attr("transform", (d, i) => {
|
||||
return (
|
||||
"translate(" +
|
||||
getTopicHorizontalPosition(
|
||||
i,
|
||||
d,
|
||||
this.learningPathStore.learningPath.topics
|
||||
) +
|
||||
",0)"
|
||||
);
|
||||
})
|
||||
.attr("class", (d) => {
|
||||
return "topic ".concat(d.is_visible ? "hidden lg:block" : "hidden");
|
||||
});
|
||||
|
||||
topicLines
|
||||
.attr('x1', -10)
|
||||
.attr('y1', 0)
|
||||
.attr('x2', -10)
|
||||
.attr('y2', 0)
|
||||
.attr("x1", -10)
|
||||
.attr("y1", 0)
|
||||
.attr("x2", -10)
|
||||
.attr("y2", 0)
|
||||
.transition()
|
||||
.duration('1000')
|
||||
.attr('y2', 350)
|
||||
.duration("1000")
|
||||
.attr("y2", 350);
|
||||
|
||||
topicTitles
|
||||
.attr('y', 20)
|
||||
.style('font-size', '18px')
|
||||
.attr("y", 20)
|
||||
.style("font-size", "18px")
|
||||
.call(wrap, circleWidth * 0.8)
|
||||
.attr('class', 'topicTitles font-bold')
|
||||
.attr("class", "topicTitles font-bold");
|
||||
}
|
||||
|
||||
function wrap(text, width) {
|
||||
|
|
@ -313,38 +353,42 @@ export default {
|
|||
line = [],
|
||||
lineNumber = 0,
|
||||
lineHeight = 1.1, // ems
|
||||
y = text.attr('y'),
|
||||
y = text.attr("y"),
|
||||
dy = 0, //parseFloat(text.attr('dy')),
|
||||
tspan = text
|
||||
.text(null)
|
||||
.append('tspan')
|
||||
.attr('x', 0)
|
||||
.attr('y', y)
|
||||
.attr('dy', dy + 'em')
|
||||
.append("tspan")
|
||||
.attr("x", 0)
|
||||
.attr("y", y)
|
||||
.attr("dy", dy + "em");
|
||||
while ((word = words.pop())) {
|
||||
line.push(word)
|
||||
tspan.text(line.join(' '))
|
||||
line.push(word);
|
||||
tspan.text(line.join(" "));
|
||||
if (tspan.node().getComputedTextLength() > width) {
|
||||
line.pop()
|
||||
tspan.text(line.join(' '))
|
||||
line = [word]
|
||||
line.pop();
|
||||
tspan.text(line.join(" "));
|
||||
line = [word];
|
||||
|
||||
tspan = text
|
||||
.append('tspan')
|
||||
.attr('x', 0)
|
||||
.attr('y', y)
|
||||
.attr('dy', ++lineNumber * lineHeight + dy + 'em')
|
||||
.text(word)
|
||||
.append("tspan")
|
||||
.attr("x", 0)
|
||||
.attr("y", y)
|
||||
.attr("dy", ++lineNumber * lineHeight + dy + "em")
|
||||
.text(word);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,79 +1,90 @@
|
|||
<script setup lang="ts">
|
||||
import ItCheckbox from '@/components/ui/ItCheckbox.vue'
|
||||
import type { CourseCompletionStatus, LearningContent, LearningSequence } from '@/types'
|
||||
import { useCircleStore } from '@/stores/circle'
|
||||
import { computed } from 'vue'
|
||||
import _ from 'lodash'
|
||||
import { humanizeDuration } from '@/utils/humanizeDuration'
|
||||
import LearningContentBadge from './LearningContentTypeBadge.vue'
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type {
|
||||
CourseCompletionStatus,
|
||||
LearningContent,
|
||||
LearningSequence,
|
||||
} from "@/types";
|
||||
import { humanizeDuration } from "@/utils/humanizeDuration";
|
||||
import _ from "lodash";
|
||||
import { computed } from "vue";
|
||||
import LearningContentBadge from "./LearningContentTypeBadge.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
learningSequence: LearningSequence
|
||||
}>()
|
||||
learningSequence: LearningSequence;
|
||||
}>();
|
||||
|
||||
const circleStore = useCircleStore()
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
function toggleCompleted(learningContent: LearningContent) {
|
||||
let completionStatus: CourseCompletionStatus = 'success'
|
||||
if (learningContent.completion_status === 'success') {
|
||||
completionStatus = 'fail'
|
||||
let completionStatus: CourseCompletionStatus = "success";
|
||||
if (learningContent.completion_status === "success") {
|
||||
completionStatus = "fail";
|
||||
}
|
||||
circleStore.markCompletion(learningContent, completionStatus)
|
||||
circleStore.markCompletion(learningContent, completionStatus);
|
||||
}
|
||||
|
||||
const someFinished = computed(() => {
|
||||
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(() => {
|
||||
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(() => {
|
||||
if (props.learningSequence && circleStore.circle) {
|
||||
const lastFinished = _.findLast(circleStore.circle.flatLearningContents, (learningContent) => {
|
||||
return learningContent.completion_status === 'success'
|
||||
})
|
||||
const lastFinished = _.findLast(
|
||||
circleStore.circle.flatLearningContents,
|
||||
(learningContent) => {
|
||||
return learningContent.completion_status === "success";
|
||||
}
|
||||
);
|
||||
|
||||
if (!lastFinished) {
|
||||
// must be the first
|
||||
return [circleStore.circle.flatLearningContents[0].translation_key, true]
|
||||
return [circleStore.circle.flatLearningContents[0].translation_key, true];
|
||||
}
|
||||
|
||||
if (lastFinished && lastFinished.nextLearningContent) {
|
||||
return [lastFinished.nextLearningContent.translation_key, false]
|
||||
return [lastFinished.nextLearningContent.translation_key, false];
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
return "";
|
||||
});
|
||||
|
||||
const learningSequenceBorderClass = computed(() => {
|
||||
let result: string[] = []
|
||||
let result: string[] = [];
|
||||
if (props.learningSequence && circleStore.circle) {
|
||||
if (allFinished.value) {
|
||||
result = ['border-l-4', 'border-l-green-500']
|
||||
result = ["border-l-4", "border-l-green-500"];
|
||||
} else if (someFinished.value) {
|
||||
result = ['border-l-4', 'border-l-sky-500']
|
||||
result = ["border-l-4", "border-l-sky-500"];
|
||||
} else {
|
||||
result = ['border-l-gray-500']
|
||||
result = ["border-l-gray-500"];
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
return result;
|
||||
});
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<component :is="learningSequence.icon" />
|
||||
<h3 class="text-xl font-semibold">
|
||||
|
|
@ -82,11 +93,20 @@ const learningSequenceBorderClass = computed(() => {
|
|||
<div>{{ humanizeDuration(learningSequence.minutes) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white px-4 lg:px-6 border border-gray-500" :class="learningSequenceBorderClass">
|
||||
<div v-for="learningUnit in learningSequence.learningUnits" :key="learningUnit.id" class="pt-3 lg:pt-6">
|
||||
<div class="pb-3 lg:pg-6 flex gap-4 text-blue-900" v-if="learningUnit.title">
|
||||
<div
|
||||
class="bg-white px-4 lg:px-6 border border-gray-500"
|
||||
: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="whitespace-nowrap">{{ humanizeDuration(learningUnit.minutes) }}</div>
|
||||
<div class="whitespace-nowrap">
|
||||
{{ humanizeDuration(learningUnit.minutes) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -95,39 +115,54 @@ const learningSequenceBorderClass = computed(() => {
|
|||
class="flex gap-4 pb-3 lg:pb-6"
|
||||
>
|
||||
<ItCheckbox
|
||||
:modelValue="learningContent.completion_status === 'success'"
|
||||
:onToggle="() => toggleCompleted(learningContent)"
|
||||
:model-value="learningContent.completion_status === 'success'"
|
||||
:on-toggle="() => toggleCompleted(learningContent)"
|
||||
:data-cy="`${learningContent.slug}`"
|
||||
>
|
||||
<span class="flex flex-wrap gap-4 items-center">
|
||||
<div
|
||||
@click.stop="circleStore.openLearningContent(learningContent)"
|
||||
class="cursor-pointer w-full sm:w-auto"
|
||||
@click.stop="circleStore.openLearningContent(learningContent)"
|
||||
>
|
||||
{{ learningContent.title }}
|
||||
</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
|
||||
v-if="learningContent.translation_key === continueTranslationKeyTuple[0]"
|
||||
v-if="
|
||||
learningContent.translation_key === continueTranslationKeyTuple[0]
|
||||
"
|
||||
class="btn-blue order-1 sm:order-none"
|
||||
data-cy="ls-continue-button"
|
||||
@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>
|
||||
</button>
|
||||
<div class="hidden sm:block"></div>
|
||||
<div class="w-full sm:w-auto">
|
||||
<LearningContentBadge :learningContentType="learningContent.contents[0].type" />
|
||||
<LearningContentBadge
|
||||
:learning-content-type="learningContent.contents[0].type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</ItCheckbox>
|
||||
</div>
|
||||
|
||||
<div 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">
|
||||
<div
|
||||
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" />
|
||||
<div>Selbsteinschätzung: Ich kann das.</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,39 +1,41 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import { computed, reactive } from 'vue'
|
||||
import { useCircleStore } from '@/stores/circle'
|
||||
import type { LearningUnit } from '@/types'
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningUnit } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
import { computed, reactive } from "vue";
|
||||
|
||||
log.debug('LearningContent.vue setup')
|
||||
log.debug("LearningContent.vue setup");
|
||||
|
||||
const circleStore = useCircleStore()
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const state = reactive({
|
||||
questionIndex: 0,
|
||||
})
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
learningUnit: LearningUnit
|
||||
}>()
|
||||
learningUnit: LearningUnit;
|
||||
}>();
|
||||
|
||||
const questions = computed(() => props.learningUnit?.children)
|
||||
const currentQuestion = computed(() => questions.value[state.questionIndex])
|
||||
const questions = computed(() => props.learningUnit?.children);
|
||||
const currentQuestion = computed(() => questions.value[state.questionIndex]);
|
||||
|
||||
function handleContinue() {
|
||||
log.debug('handleContinue')
|
||||
log.debug("handleContinue");
|
||||
if (state.questionIndex + 1 < questions.value.length) {
|
||||
log.debug('increment questionIndex', state.questionIndex)
|
||||
state.questionIndex += 1
|
||||
log.debug("increment questionIndex", state.questionIndex);
|
||||
state.questionIndex += 1;
|
||||
} else {
|
||||
log.debug('continue to next learning content')
|
||||
circleStore.continueFromSelfEvaluation()
|
||||
log.debug("continue to next learning content");
|
||||
circleStore.continueFromSelfEvaluation();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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
|
||||
type="button"
|
||||
class="btn-text inline-flex items-center px-3 py-2 font-normal"
|
||||
|
|
@ -49,11 +51,14 @@ function handleContinue() {
|
|||
</nav>
|
||||
|
||||
<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">
|
||||
Ü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:
|
||||
</p>
|
||||
|
||||
|
|
@ -62,32 +67,34 @@ function handleContinue() {
|
|||
|
||||
<div class="mt-4 lg:mt-8 flex flex-col lg:flex-row justify-between gap-6">
|
||||
<button
|
||||
@click="circleStore.markCompletion(currentQuestion, 'success')"
|
||||
class="flex-1 inline-flex items-center text-left p-4 border"
|
||||
:class="{
|
||||
'border-green-500': currentQuestion.completion_status === 'success',
|
||||
'border-2': 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>
|
||||
<span class="font-bold text-xl"> Ja, ich kann das. </span>
|
||||
</button>
|
||||
<button
|
||||
@click="circleStore.markCompletion(currentQuestion, 'fail')"
|
||||
class="flex-1 inline-flex items-center text-left p-4 border"
|
||||
:class="{
|
||||
'border-orange-500': 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>
|
||||
<span class="font-bold text-xl"> Das muss ich nochmals anschauen. </span>
|
||||
</button>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
<template>
|
||||
<svg width="30" 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
|
||||
width="30"
|
||||
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>
|
||||
</template>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -2,21 +2,27 @@
|
|||
<svg viewBox="0 0 39 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<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"
|
||||
fill="#007AC3"/>
|
||||
fill="#007AC3"
|
||||
/>
|
||||
<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"
|
||||
fill="white"/>
|
||||
fill="white"
|
||||
/>
|
||||
<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"
|
||||
fill="#007AC3"/>
|
||||
fill="#007AC3"
|
||||
/>
|
||||
<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"
|
||||
fill="white"/>
|
||||
fill="white"
|
||||
/>
|
||||
<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"
|
||||
fill="white"/>
|
||||
fill="white"
|
||||
/>
|
||||
<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"
|
||||
fill="white"/>
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import MediaLink from '@/components/mediaCenter/MediaLink.vue'
|
||||
import MediaLink from "@/components/mediaCenter/MediaLink.vue";
|
||||
|
||||
export interface Props {
|
||||
title: string
|
||||
description: string
|
||||
linkText: string
|
||||
url: string
|
||||
icon: string
|
||||
openWindow?: boolean
|
||||
title: string;
|
||||
description: string;
|
||||
linkText: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
openWindow?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
icon: '',
|
||||
description: '',
|
||||
icon: "",
|
||||
description: "",
|
||||
openWindow: false,
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
// https://router.vuejs.org/guide/advanced/extending-router-link.html
|
||||
// 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({
|
||||
...RouterLink.props, // @ts-ignore
|
||||
|
|
@ -11,17 +11,29 @@ const props = defineProps({
|
|||
type: Boolean,
|
||||
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>
|
||||
|
||||
<template>
|
||||
<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 />
|
||||
</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 />
|
||||
</router-link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,39 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
export interface Props {
|
||||
title: string,
|
||||
description: string,
|
||||
call2Action: string,
|
||||
link: string,
|
||||
icon: string
|
||||
title: string;
|
||||
description: string;
|
||||
call2Action: string;
|
||||
link: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
icon: ''
|
||||
})
|
||||
|
||||
icon: "",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white p-8 flex justify-between">
|
||||
<div>
|
||||
<h3 class="mb-4">{{title}}</h3>
|
||||
<p class="mb-4">{{description}}</p>
|
||||
<router-link
|
||||
:to="link"
|
||||
class="inline-flex items-center font-normal"
|
||||
>
|
||||
<span class="inline">{{call2Action}}</span>
|
||||
<h3 class="mb-4">{{ title }}</h3>
|
||||
<p class="mb-4">{{ description }}</p>
|
||||
<router-link :to="link" 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>
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-if="icon"
|
||||
:class="[`bg-${icon}`]"
|
||||
class="bg-contain bg-no-repeat bg-right w-2/6 -mr-8">
|
||||
</div>
|
||||
class="bg-contain bg-no-repeat bg-right w-2/6 -mr-8"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue?: boolean
|
||||
disabled?: boolean
|
||||
onToggle: () => void
|
||||
modelValue?: boolean;
|
||||
disabled?: boolean;
|
||||
onToggle?: () => void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
disabled: false,
|
||||
})
|
||||
onToggle: () => {
|
||||
// do nothing
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
defineEmits(["update:modelValue"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import {Menu, MenuButton, MenuItems, MenuItem} from '@headlessui/vue'
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
|
||||
|
||||
const props = defineProps<{
|
||||
buttonClasses: [string],
|
||||
listItems: [
|
||||
[object]
|
||||
],
|
||||
align: 'left' | 'right'
|
||||
}>()
|
||||
buttonClasses: [string];
|
||||
listItems: [[object]];
|
||||
align: "left" | "right";
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', data: object): void
|
||||
}>()
|
||||
|
||||
(e: "select", data: object): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Menu as="div" class="relative inline-block text-left">
|
||||
<div>
|
||||
<MenuButton
|
||||
:class="buttonClasses"
|
||||
>
|
||||
<MenuButton :class="buttonClasses">
|
||||
<slot></slot>
|
||||
</MenuButton>
|
||||
</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="[align === 'left' ? 'left-0' : 'right-0']"
|
||||
>
|
||||
<div class="" v-for="section in listItems" :key="section">
|
||||
<div class="px-1 py-1" v-for="item in section" :key="item">
|
||||
<div v-for="section in listItems" :key="section" class="">
|
||||
<div v-for="item in section" :key="item" class="px-1 py-1">
|
||||
<MenuItem>
|
||||
<button
|
||||
@click="$emit('select', item.data)"
|
||||
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">
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
></component>
|
||||
<component :is="item.icon" v-if="item.icon"></component>
|
||||
</span>
|
||||
{{item.title}}
|
||||
{{ item.title }}
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,74 +1,91 @@
|
|||
<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 {
|
||||
id: number|string,
|
||||
name: string
|
||||
interface DropdownSelectable {
|
||||
id: number | string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding
|
||||
export interface Props {
|
||||
interface Props {
|
||||
modelValue: {
|
||||
id: string|number
|
||||
name: string
|
||||
},
|
||||
items: DropdownSelectable[]
|
||||
id: string | number;
|
||||
name: string;
|
||||
};
|
||||
items: DropdownSelectable[];
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', data: object): void
|
||||
}>()
|
||||
(e: "update:modelValue", data: object): void;
|
||||
}>();
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: {
|
||||
id: -1,
|
||||
name: ''
|
||||
modelValue: () => {
|
||||
return {
|
||||
id: -1,
|
||||
name: "",
|
||||
};
|
||||
},
|
||||
items: [],
|
||||
})
|
||||
items: () => [],
|
||||
});
|
||||
|
||||
const dropdownSelected = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
})
|
||||
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit("update:modelValue", val),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Listbox as="div" v-model="dropdownSelected">
|
||||
<Listbox v-model="dropdownSelected" as="div">
|
||||
<div class="mt-1 relative w-96">
|
||||
<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="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<it-icon-arrow-down class="h-5 w-5" aria-hidden="true"/>
|
||||
</span>
|
||||
<span
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"
|
||||
>
|
||||
<it-icon-arrow-down class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0">
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<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
|
||||
as="template"
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
v-slot="{ active, selected }"
|
||||
as="template"
|
||||
:value="item"
|
||||
v-slot="{ active, selected }">
|
||||
>
|
||||
<li
|
||||
:class="[active ? 'text-white bg-blue-900' : 'text-black', 'cursor-default select-none relative py-2 pl-3 pr-9']">
|
||||
<span :class="[dropdownSelected ? 'font-semibold' : 'font-normal', 'block truncate']">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
:class="[
|
||||
active ? 'text-white bg-blue-900' : 'text-black',
|
||||
'cursor-default select-none relative py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
dropdownSelected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
|
||||
<span v-if="dropdownSelected"
|
||||
class="text-blue-900 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||
<it-icon-check
|
||||
v-if="selected"
|
||||
class="h-5 w-5"
|
||||
aria-hidden="true"/>
|
||||
</span>
|
||||
<span
|
||||
v-if="dropdownSelected"
|
||||
class="text-blue-900 absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
>
|
||||
<it-icon-check v-if="selected" class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
// inspiration https://vuejs.org/examples/#modal
|
||||
|
||||
import {onMounted, watch} from "vue";
|
||||
import { onMounted, watch } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
show: boolean;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['closemodal'])
|
||||
const emits = defineEmits(["closemodal"]);
|
||||
|
||||
let appElement: HTMLElement | null = null;
|
||||
|
||||
watch(() => props.show,
|
||||
(isShown) => isShown && appElement ? appElement.classList.add('no-scroll') : null
|
||||
)
|
||||
watch(
|
||||
() => props.show,
|
||||
(isShown) => (isShown && appElement ? appElement.classList.add("no-scroll") : null)
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
appElement = document.getElementById('app');
|
||||
})
|
||||
appElement = document.getElementById("app");
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
if (appElement) {
|
||||
appElement.classList.remove('no-scroll')
|
||||
appElement.classList.remove("no-scroll");
|
||||
}
|
||||
emits('closemodal')
|
||||
}
|
||||
|
||||
emits("closemodal");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -33,7 +33,8 @@ const closeModal = () => {
|
|||
<div
|
||||
v-if="show"
|
||||
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
|
||||
type="button"
|
||||
class="w-8 h-8 absolute right-4 top-4 cursor-pointer"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import {getCookieValue} from '@/router/guards';
|
||||
import { getCookieValue } from "@/router/guards";
|
||||
|
||||
class FetchError extends Error {
|
||||
response: Response;
|
||||
|
||||
constructor(response: Response, message = 'HTTP error ' + response.status) {
|
||||
constructor(response: Response, message = "HTTP error " + response.status) {
|
||||
super(message);
|
||||
this.response = response;
|
||||
}
|
||||
}
|
||||
|
||||
export const itFetch = (url: RequestInfo, options: RequestInit) => {
|
||||
return fetch(url, options).then(response => {
|
||||
return fetch(url, options).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new FetchError(response);
|
||||
}
|
||||
|
|
@ -19,33 +19,35 @@ export const itFetch = (url: RequestInfo, options: RequestInit) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const itPost = (
|
||||
url: RequestInfo,
|
||||
data: unknown,
|
||||
options: RequestInit = {},
|
||||
) => {
|
||||
export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {}) => {
|
||||
options = Object.assign({}, options);
|
||||
|
||||
const headers = Object.assign({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
}, options?.headers);
|
||||
const headers = Object.assign(
|
||||
{
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
},
|
||||
options?.headers
|
||||
);
|
||||
|
||||
if (options?.headers) {
|
||||
delete options.headers;
|
||||
}
|
||||
|
||||
options = Object.assign({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(data)
|
||||
}, options);
|
||||
options = Object.assign(
|
||||
{
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @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;
|
||||
}
|
||||
|
||||
|
|
@ -57,5 +59,5 @@ export const itPost = (
|
|||
};
|
||||
|
||||
export const itGet = (url: RequestInfo) => {
|
||||
return itPost(url, {}, {method: 'GET'});
|
||||
return itPost(url, {}, { method: "GET" });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { nextTick } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
|
||||
// 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' }) {
|
||||
const i18n = createI18n(options)
|
||||
setI18nLanguage(i18n, options.locale)
|
||||
return i18n
|
||||
export function setupI18n(options = { locale: "de" }) {
|
||||
const i18n = createI18n(options);
|
||||
setI18nLanguage(i18n, options.locale);
|
||||
return i18n;
|
||||
}
|
||||
|
||||
export function setI18nLanguage(i18n: any, locale: string) {
|
||||
if (i18n.mode === 'legacy') {
|
||||
i18n.global.locale = locale
|
||||
if (i18n.mode === "legacy") {
|
||||
i18n.global.locale = locale;
|
||||
} else {
|
||||
i18n.global.locale.value = locale
|
||||
i18n.global.locale.value = locale;
|
||||
}
|
||||
/**
|
||||
* NOTE:
|
||||
|
|
@ -23,17 +23,17 @@ export function setI18nLanguage(i18n: any, locale: string) {
|
|||
*
|
||||
* 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) {
|
||||
// load locale messages with dynamic import
|
||||
const messages = await import(
|
||||
/* webpackChunkName: "locale-[request]" */ `./locales/${locale}.json`
|
||||
)
|
||||
);
|
||||
|
||||
// set locale and locale message
|
||||
i18n.global.setLocaleMessage(locale, messages.default)
|
||||
i18n.global.setLocaleMessage(locale, messages.default);
|
||||
|
||||
return nextTick()
|
||||
return nextTick();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,38 @@
|
|||
import { createApp, markRaw } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import * as log from 'loglevel'
|
||||
import * as log from "loglevel";
|
||||
import { createPinia } from "pinia";
|
||||
import { createApp, markRaw } from "vue";
|
||||
|
||||
// import {setupI18n} from './i18n'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import App from "./App.vue";
|
||||
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) {
|
||||
log.setLevel('trace')
|
||||
if (window.location.href.indexOf("localhost") >= 0) {
|
||||
log.setLevel("trace");
|
||||
} else {
|
||||
log.setLevel('warn')
|
||||
log.setLevel("warn");
|
||||
}
|
||||
|
||||
|
||||
// const i18n = setupI18n()
|
||||
const app = createApp(App)
|
||||
const app = createApp(App);
|
||||
|
||||
// todo: define lang setup
|
||||
// await loadLocaleMessages(i18n, 'de')
|
||||
|
||||
app.use(router)
|
||||
app.use(router);
|
||||
|
||||
declare module 'pinia' {
|
||||
declare module "pinia" {
|
||||
export interface PiniaCustomProperties {
|
||||
router: Router
|
||||
router: Router;
|
||||
}
|
||||
}
|
||||
const pinia = createPinia();
|
||||
pinia.use(({ store }) => {
|
||||
store.router = markRaw(router)
|
||||
})
|
||||
app.use(pinia)
|
||||
store.router = markRaw(router);
|
||||
});
|
||||
app.use(pinia);
|
||||
// app.use(i18n)
|
||||
|
||||
|
||||
app.mount('#app')
|
||||
app.mount("#app");
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
const loggedIn = getCookieValue('loginStatus') === 'true'
|
||||
const userStore = useUserStore()
|
||||
const loggedIn = getCookieValue("loginStatus") === "true";
|
||||
const userStore = useUserStore();
|
||||
|
||||
userStore.$patch({ loggedIn })
|
||||
userStore.$patch({ loggedIn });
|
||||
if (loggedIn && !userStore.email) {
|
||||
userStore.fetchUser()
|
||||
userStore.fetchUser();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const redirectToLoginIfRequired: NavigationGuardWithThis<undefined> = (to, _from) => {
|
||||
const userStore = useUserStore()
|
||||
if(loginRequired(to) && !userStore.loggedIn) {
|
||||
return `/login?next=${to.fullPath}`
|
||||
export const redirectToLoginIfRequired: NavigationGuardWithThis<undefined> = (
|
||||
to,
|
||||
_from
|
||||
) => {
|
||||
const userStore = useUserStore();
|
||||
if (loginRequired(to) && !userStore.loggedIn) {
|
||||
return `/login?next=${to.fullPath}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getCookieValue = (cookieName: string): string => {
|
||||
// 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) {
|
||||
return ''
|
||||
return "";
|
||||
}
|
||||
return cookieValue.pop() || '';
|
||||
}
|
||||
return cookieValue.pop() || "";
|
||||
};
|
||||
|
||||
const loginRequired = (to: RouteLocationNormalized) => {
|
||||
return !to.meta?.public
|
||||
}
|
||||
return !to.meta?.public;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import CockpitView from '@/views/CockpitView.vue'
|
||||
import LoginView from '@/views/LoginView.vue'
|
||||
import { redirectToLoginIfRequired, updateLoggedIn } from '@/router/guards'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { redirectToLoginIfRequired, updateLoggedIn } from "@/router/guards";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import CockpitView from "@/views/CockpitView.vue";
|
||||
import LoginView from "@/views/LoginView.vue";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
path: "/login",
|
||||
component: LoginView,
|
||||
meta: {
|
||||
// no login required -> so `public === true`
|
||||
|
|
@ -16,86 +16,86 @@ const router = createRouter({
|
|||
},
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: CockpitView,
|
||||
},
|
||||
{
|
||||
path: '/shop',
|
||||
component: () => import('@/views/ShopView.vue'),
|
||||
path: "/shop",
|
||||
component: () => import("@/views/ShopView.vue"),
|
||||
},
|
||||
{
|
||||
path: '/mediacenter/:mediaCenterPageSlug',
|
||||
path: "/mediacenter/:mediaCenterPageSlug",
|
||||
props: true,
|
||||
component: () => import('@/views/MediaCenterView.vue'),
|
||||
component: () => import("@/views/MediaCenterView.vue"),
|
||||
children: [
|
||||
{
|
||||
path: 'overview',
|
||||
component: () => import('@/views/MediaCenterMainView.vue'),
|
||||
path: "overview",
|
||||
component: () => import("@/views/MediaCenterMainView.vue"),
|
||||
},
|
||||
{
|
||||
path: 'handlungsfelder/:mediaCategorySlug',
|
||||
path: "handlungsfelder/:mediaCategorySlug",
|
||||
props: true,
|
||||
component: () => import('@/views/MediaCategoryDetailView.vue'),
|
||||
component: () => import("@/views/MediaCategoryDetailView.vue"),
|
||||
},
|
||||
{
|
||||
path: 'handlungsfelder',
|
||||
component: () => import('@/views/MediaCenterCategoryOverview.vue'),
|
||||
path: "handlungsfelder",
|
||||
component: () => import("@/views/MediaCenterCategoryOverview.vue"),
|
||||
},
|
||||
{
|
||||
path: 'handlungsfeldlist',
|
||||
component: () => import('@/views/MediaList.vue'),
|
||||
path: "handlungsfeldlist",
|
||||
component: () => import("@/views/MediaList.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/messages',
|
||||
component: () => import('@/views/MessagesView.vue'),
|
||||
path: "/messages",
|
||||
component: () => import("@/views/MessagesView.vue"),
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
component: () => import('@/views/ProfileView.vue'),
|
||||
path: "/profile",
|
||||
component: () => import("@/views/ProfileView.vue"),
|
||||
},
|
||||
{
|
||||
path: '/learn/:learningPathSlug',
|
||||
component: () => import('../views/LearningPathView.vue'),
|
||||
path: "/learn/:learningPathSlug",
|
||||
component: () => import("../views/LearningPathView.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/learn/:learningPathSlug/:circleSlug',
|
||||
component: () => import('../views/CircleView.vue'),
|
||||
path: "/learn/:learningPathSlug/:circleSlug",
|
||||
component: () => import("../views/CircleView.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/learn/:learningPathSlug/:circleSlug/evaluate/:learningUnitSlug',
|
||||
component: () => import('../views/LearningUnitSelfEvaluationView.vue'),
|
||||
path: "/learn/:learningPathSlug/:circleSlug/evaluate/:learningUnitSlug",
|
||||
component: () => import("../views/LearningUnitSelfEvaluationView.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/learn/:learningPathSlug/:circleSlug/:contentSlug',
|
||||
component: () => import('../views/LearningContentView.vue'),
|
||||
path: "/learn/:learningPathSlug/:circleSlug/:contentSlug",
|
||||
component: () => import("../views/LearningContentView.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/styleguide',
|
||||
component: () => import('../views/StyleGuideView.vue'),
|
||||
path: "/styleguide",
|
||||
component: () => import("../views/StyleGuideView.vue"),
|
||||
meta: {
|
||||
public: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: () => import('../views/404View.vue'),
|
||||
path: "/:pathMatch(.*)*",
|
||||
component: () => import("../views/404View.vue"),
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
router.beforeEach(updateLoggedIn)
|
||||
router.beforeEach(redirectToLoginIfRequired)
|
||||
router.beforeEach(updateLoggedIn);
|
||||
router.beforeEach(redirectToLoginIfRequired);
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
const appStore = useAppStore();
|
||||
appStore.routingFinished = true;
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { describe, it } from 'vitest'
|
||||
import data from './learning_path_json.json'
|
||||
import { Circle } from '../circle'
|
||||
import { describe, it } from "vitest";
|
||||
import { Circle } from "../circle";
|
||||
import data from "./learning_path_json.json";
|
||||
|
||||
describe('Circle.parseJson', () => {
|
||||
it('can parse circle from api response', () => {
|
||||
const cirleData = data.children.find((c) => c.slug === 'test-lehrgang-lp-circle-analyse')
|
||||
const circle = Circle.fromJson(cirleData, undefined)
|
||||
expect(circle.learningSequences.length).toBe(3)
|
||||
expect(circle.flatLearningContents.length).toBe(7)
|
||||
})
|
||||
})
|
||||
describe("Circle.parseJson", () => {
|
||||
it("can parse circle from api response", () => {
|
||||
const cirleData = data.children.find(
|
||||
(c) => c.slug === "test-lehrgang-lp-circle-analyse"
|
||||
);
|
||||
const circle = Circle.fromJson(cirleData, undefined);
|
||||
expect(circle.learningSequences.length).toBe(3);
|
||||
expect(circle.flatLearningContents.length).toBe(7);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { describe, it } from 'vitest'
|
||||
import data from './learning_path_json.json'
|
||||
import { LearningPath } from '../learningPath'
|
||||
import { describe, it } from "vitest";
|
||||
import { LearningPath } from "../learningPath";
|
||||
import data from "./learning_path_json.json";
|
||||
|
||||
describe('LearningPath.parseJson', () => {
|
||||
it('can parse learning sequences from api response', () => {
|
||||
const learningPath = LearningPath.fromJson(data, [])
|
||||
describe("LearningPath.parseJson", () => {
|
||||
it("can parse learning sequences from api response", () => {
|
||||
const learningPath = LearningPath.fromJson(data, []);
|
||||
|
||||
expect(learningPath.circles.length).toBe(2)
|
||||
expect(learningPath.circles[0].title).toBe('Basis')
|
||||
expect(learningPath.circles[1].title).toBe('Analyse')
|
||||
expect(learningPath.circles.length).toBe(2);
|
||||
expect(learningPath.circles[0].title).toBe("Basis");
|
||||
expect(learningPath.circles[1].title).toBe("Analyse");
|
||||
|
||||
expect(learningPath.topics.length).toBe(2)
|
||||
})
|
||||
})
|
||||
expect(learningPath.topics.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,344 +1,344 @@
|
|||
{
|
||||
"id": 372,
|
||||
"title": "Test Lernpfad",
|
||||
"slug": "test-lehrgang-lp",
|
||||
"type": "learnpath.LearningPath",
|
||||
"translation_key": "42e559ca-970f-4a08-9e5e-63860585ee1e",
|
||||
"children": [
|
||||
"id": 372,
|
||||
"title": "Test Lernpfad",
|
||||
"slug": "test-lehrgang-lp",
|
||||
"type": "learnpath.LearningPath",
|
||||
"translation_key": "42e559ca-970f-4a08-9e5e-63860585ee1e",
|
||||
"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,
|
||||
"title": "Basis",
|
||||
"slug": "test-lehrgang-lp-topic-basis",
|
||||
"type": "learnpath.Topic",
|
||||
"translation_key": "d68c1544-cf22-4a59-a81c-8cb977440cd0",
|
||||
"is_visible": false
|
||||
"id": 375,
|
||||
"title": "Starten",
|
||||
"slug": "test-lehrgang-lp-circle-basis-ls-starten",
|
||||
"type": "learnpath.LearningSequence",
|
||||
"translation_key": "c5fdada9-036d-4516-a50f-6656a1c6b009",
|
||||
"icon": "it-icon-ls-start"
|
||||
},
|
||||
{
|
||||
"id": 374,
|
||||
"title": "Basis",
|
||||
"slug": "test-lehrgang-lp-circle-basis",
|
||||
"type": "learnpath.Circle",
|
||||
"translation_key": "ec62a2af-6f74-4031-b971-c3287bbbc573",
|
||||
"children": [
|
||||
{
|
||||
"id": 375,
|
||||
"title": "Starten",
|
||||
"slug": "test-lehrgang-lp-circle-basis-ls-starten",
|
||||
"type": "learnpath.LearningSequence",
|
||||
"translation_key": "c5fdada9-036d-4516-a50f-6656a1c6b009",
|
||||
"icon": "it-icon-ls-start"
|
||||
},
|
||||
{
|
||||
"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": 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": 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": 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": 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"
|
||||
}
|
||||
]
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"course": {
|
||||
"id": -1,
|
||||
"title": "Test Lerngang",
|
||||
"category_name": "Handlungsfeld"
|
||||
],
|
||||
"description": "Basis",
|
||||
"job_situations": [],
|
||||
"goals": [],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { LearningPath } from "@/services/learningPath";
|
||||
import type {
|
||||
CircleChild,
|
||||
CircleGoal,
|
||||
|
|
@ -9,34 +10,38 @@ import type {
|
|||
LearningSequence,
|
||||
LearningUnit,
|
||||
LearningUnitQuestion,
|
||||
} from '@/types'
|
||||
import type { LearningPath } from '@/services/learningPath'
|
||||
} from "@/types";
|
||||
|
||||
function _createEmptyLearningUnit(parentLearningSequence: LearningSequence): LearningUnit {
|
||||
function _createEmptyLearningUnit(
|
||||
parentLearningSequence: LearningSequence
|
||||
): LearningUnit {
|
||||
return {
|
||||
id: 0,
|
||||
title: '',
|
||||
slug: '',
|
||||
translation_key: '',
|
||||
type: 'learnpath.LearningUnit',
|
||||
title: "",
|
||||
slug: "",
|
||||
translation_key: "",
|
||||
type: "learnpath.LearningUnit",
|
||||
learningContents: [],
|
||||
minutes: 0,
|
||||
parentLearningSequence: parentLearningSequence,
|
||||
children: [],
|
||||
last: true,
|
||||
completion_status: 'unknown',
|
||||
}
|
||||
completion_status: "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
export function parseLearningSequences (circle: Circle, children: CircleChild[]): LearningSequence[] {
|
||||
let learningSequence:LearningSequence | undefined;
|
||||
let learningUnit:LearningUnit | undefined;
|
||||
let learningContent:LearningContent | undefined;
|
||||
export function parseLearningSequences(
|
||||
circle: Circle,
|
||||
children: CircleChild[]
|
||||
): LearningSequence[] {
|
||||
let learningSequence: LearningSequence | undefined;
|
||||
let learningUnit: LearningUnit | undefined;
|
||||
let learningContent: LearningContent | undefined;
|
||||
let previousLearningContent: LearningContent | undefined;
|
||||
const result:LearningSequence[] = [];
|
||||
const result: LearningSequence[] = [];
|
||||
|
||||
children.forEach((child) => {
|
||||
if (child.type === 'learnpath.LearningSequence') {
|
||||
if (child.type === "learnpath.LearningSequence") {
|
||||
if (learningSequence) {
|
||||
if (learningUnit) {
|
||||
learningUnit.last = true;
|
||||
|
|
@ -44,13 +49,13 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
|
|||
}
|
||||
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
|
||||
learningUnit = _createEmptyLearningUnit(learningSequence);
|
||||
} else if (child.type === 'learnpath.LearningUnit') {
|
||||
} else if (child.type === "learnpath.LearningUnit") {
|
||||
if (!learningSequence) {
|
||||
throw new Error('LearningUnit found before LearningSequence');
|
||||
throw new Error("LearningUnit found before LearningSequence");
|
||||
}
|
||||
|
||||
if (learningUnit && learningUnit.learningContents.length) {
|
||||
|
|
@ -64,11 +69,11 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
|
|||
c.parentLearningUnit = learningUnit;
|
||||
c.parentLearningSequence = learningSequence;
|
||||
return c;
|
||||
})
|
||||
}),
|
||||
});
|
||||
} else if (child.type === 'learnpath.LearningContent') {
|
||||
} else if (child.type === "learnpath.LearningContent") {
|
||||
if (!learningUnit) {
|
||||
throw new Error('LearningContent found before LearningUnit');
|
||||
throw new Error("LearningContent found before LearningUnit");
|
||||
}
|
||||
previousLearningContent = learningContent;
|
||||
|
||||
|
|
@ -93,7 +98,9 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
|
|||
(learningSequence as LearningSequence).learningUnits.push(learningUnit);
|
||||
result.push(learningSequence);
|
||||
} 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
|
||||
|
|
@ -112,9 +119,9 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
|
|||
}
|
||||
|
||||
export class Circle implements CourseWagtailPage {
|
||||
readonly type = 'learnpath.Circle';
|
||||
readonly type = "learnpath.Circle";
|
||||
readonly learningSequences: LearningSequence[];
|
||||
completion_status: CourseCompletionStatus = 'unknown'
|
||||
completion_status: CourseCompletionStatus = "unknown";
|
||||
|
||||
nextCircle?: Circle;
|
||||
previousCircle?: Circle;
|
||||
|
|
@ -128,7 +135,7 @@ export class Circle implements CourseWagtailPage {
|
|||
public children: CircleChild[],
|
||||
public goals: CircleGoal[],
|
||||
public job_situations: CircleJobSituation[],
|
||||
public readonly parentLearningPath?: LearningPath,
|
||||
public readonly parentLearningPath?: LearningPath
|
||||
) {
|
||||
this.learningSequences = parseLearningSequences(this, this.children);
|
||||
}
|
||||
|
|
@ -144,8 +151,8 @@ export class Circle implements CourseWagtailPage {
|
|||
json.children,
|
||||
json.goals,
|
||||
json.job_situations,
|
||||
learningPath,
|
||||
)
|
||||
learningPath
|
||||
);
|
||||
}
|
||||
|
||||
public get flatChildren(): (LearningContent | LearningUnitQuestion)[] {
|
||||
|
|
@ -154,7 +161,7 @@ export class Circle implements CourseWagtailPage {
|
|||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.children.forEach((learningUnitQuestion) => {
|
||||
result.push(learningUnitQuestion);
|
||||
})
|
||||
});
|
||||
learningUnit.learningContents.forEach((learningContent) => {
|
||||
result.push(learningContent);
|
||||
});
|
||||
|
|
@ -187,9 +194,14 @@ export class Circle implements CourseWagtailPage {
|
|||
|
||||
public someFinishedInLearningSequence(translationKey: string): boolean {
|
||||
if (translationKey) {
|
||||
return this.flatChildren.filter((lc) => {
|
||||
return lc.completion_status === 'success' && lc.parentLearningSequence?.translation_key === translationKey;
|
||||
}).length > 0;
|
||||
return (
|
||||
this.flatChildren.filter((lc) => {
|
||||
return (
|
||||
lc.completion_status === "success" &&
|
||||
lc.parentLearningSequence?.translation_key === translationKey
|
||||
);
|
||||
}).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -197,14 +209,17 @@ export class Circle implements CourseWagtailPage {
|
|||
|
||||
public allFinishedInLearningSequence(translationKey: string): boolean {
|
||||
if (translationKey) {
|
||||
const finishedContents = this.flatChildren.filter((lc) => {
|
||||
return lc.completion_status === 'success' && lc.parentLearningSequence?.translation_key === translationKey;
|
||||
const finishedContents = this.flatChildren.filter((lc) => {
|
||||
return (
|
||||
lc.completion_status === "success" &&
|
||||
lc.parentLearningSequence?.translation_key === translationKey
|
||||
);
|
||||
}).length;
|
||||
|
||||
const totalContents = this.flatChildren.filter((lc) => {
|
||||
const totalContents = this.flatChildren.filter((lc) => {
|
||||
return lc.parentLearningSequence?.translation_key === translationKey;
|
||||
}).length;
|
||||
return finishedContents === totalContents
|
||||
return finishedContents === totalContents;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -218,7 +233,7 @@ export class Circle implements CourseWagtailPage {
|
|||
if (pageIndex >= 0) {
|
||||
page.completion_status = completionData[pageIndex].completion_status;
|
||||
} else {
|
||||
page.completion_status = 'unknown';
|
||||
page.completion_status = "unknown";
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -228,7 +243,7 @@ export class Circle implements CourseWagtailPage {
|
|||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as _ from 'lodash'
|
||||
import * as _ from "lodash";
|
||||
|
||||
import { Circle } from "@/services/circle";
|
||||
import type {
|
||||
CourseCompletion,
|
||||
CourseCompletionStatus,
|
||||
|
|
@ -7,24 +8,37 @@ import type {
|
|||
LearningContent,
|
||||
LearningPathChild,
|
||||
Topic,
|
||||
} from '@/types'
|
||||
import { Circle } from '@/services/circle'
|
||||
} from "@/types";
|
||||
|
||||
function getLastCompleted(courseId: number, completionData: CourseCompletion[]) {
|
||||
return _.orderBy(completionData, ['updated_at'], 'desc').find((c: CourseCompletion) => {
|
||||
return c.completion_status === 'success' && c.course === courseId && c.page_type === 'learnpath.LearningContent'
|
||||
})
|
||||
return _.orderBy(completionData, ["updated_at"], "desc").find(
|
||||
(c: CourseCompletion) => {
|
||||
return (
|
||||
c.completion_status === "success" &&
|
||||
c.course === courseId &&
|
||||
c.page_type === "learnpath.LearningContent"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export class LearningPath implements CourseWagtailPage {
|
||||
readonly type = 'learnpath.LearningPath'
|
||||
public topics: Topic[]
|
||||
public circles: Circle[]
|
||||
public nextLearningContent?: LearningContent
|
||||
readonly completion_status: CourseCompletionStatus = 'unknown'
|
||||
readonly type = "learnpath.LearningPath";
|
||||
public topics: Topic[];
|
||||
public circles: Circle[];
|
||||
public nextLearningContent?: LearningContent;
|
||||
readonly completion_status: CourseCompletionStatus = "unknown";
|
||||
|
||||
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(
|
||||
|
|
@ -37,70 +51,75 @@ export class LearningPath implements CourseWagtailPage {
|
|||
completionData?: CourseCompletion[]
|
||||
) {
|
||||
// parse children
|
||||
this.topics = []
|
||||
this.circles = []
|
||||
this.topics = [];
|
||||
this.circles = [];
|
||||
|
||||
let topic: Topic | undefined
|
||||
let topic: Topic | undefined;
|
||||
|
||||
this.children.forEach((page) => {
|
||||
if (page.type === 'learnpath.Topic') {
|
||||
if (page.type === "learnpath.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') {
|
||||
const circle = Circle.fromJson(page, this)
|
||||
if (page.type === "learnpath.Circle") {
|
||||
const circle = Circle.fromJson(page, this);
|
||||
if (completionData) {
|
||||
circle.parseCompletionData(completionData)
|
||||
circle.parseCompletionData(completionData);
|
||||
}
|
||||
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) {
|
||||
circle.previousCircle.nextCircle = circle
|
||||
circle.previousCircle.nextCircle = circle;
|
||||
}
|
||||
this.circles.push(circle)
|
||||
this.circles.push(circle);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (topic) {
|
||||
this.topics.push(topic)
|
||||
this.topics.push(topic);
|
||||
}
|
||||
|
||||
if (completionData) {
|
||||
this.calcNextLearningContent(completionData)
|
||||
this.calcNextLearningContent(completionData);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const lastCircle = this.circles.find(
|
||||
(circle) => {
|
||||
return circle.flatLearningContents.find((learningContent) => learningContent.translation_key === lastCompletedLearningContent.page_key)
|
||||
}
|
||||
)
|
||||
const lastCircle = this.circles.find((circle) => {
|
||||
return circle.flatLearningContents.find(
|
||||
(learningContent) =>
|
||||
learningContent.translation_key === lastCompletedLearningContent.page_key
|
||||
);
|
||||
});
|
||||
if (lastCircle) {
|
||||
const lastLearningContent = lastCircle.flatLearningContents.find(
|
||||
(learningContent) => learningContent.translation_key === lastCompletedLearningContent.page_key
|
||||
)
|
||||
(learningContent) =>
|
||||
learningContent.translation_key === lastCompletedLearningContent.page_key
|
||||
);
|
||||
if (lastLearningContent && lastLearningContent.nextLearningContent) {
|
||||
this.nextLearningContent = lastLearningContent.nextLearningContent
|
||||
this.nextLearningContent = lastLearningContent.nextLearningContent;
|
||||
} else {
|
||||
if (lastCircle.nextCircle) {
|
||||
this.nextLearningContent = lastCircle.nextCircle.flatLearningContents[0]
|
||||
this.nextLearningContent = lastCircle.nextCircle.flatLearningContents[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.circles[0]) {
|
||||
this.nextLearningContent = this.circles[0].flatLearningContents[0]
|
||||
this.nextLearningContent = this.circles[0].flatLearningContents[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,37 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export type AppState = {
|
||||
userLoaded: boolean
|
||||
routingFinished: boolean
|
||||
showMainNavigationBar: boolean
|
||||
}
|
||||
userLoaded: boolean;
|
||||
routingFinished: boolean;
|
||||
showMainNavigationBar: boolean;
|
||||
};
|
||||
|
||||
const showMainNavigationBarInitialState = () => {
|
||||
let path = window.location.pathname;
|
||||
|
||||
// remove dangling slash
|
||||
if (path.endsWith('/')) {
|
||||
if (path.endsWith("/")) {
|
||||
path = path.slice(0, -1);
|
||||
}
|
||||
|
||||
const numberOfSlashes = (path.match(/\//g) || []).length;
|
||||
|
||||
// it should hide main navigation bar when on learning content page
|
||||
if (path.startsWith('/learn/') && numberOfSlashes >= 4) {
|
||||
return false
|
||||
if (path.startsWith("/learn/") && numberOfSlashes >= 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const useAppStore = defineStore({
|
||||
id: 'app',
|
||||
state: () => ({
|
||||
showMainNavigationBar: showMainNavigationBarInitialState(),
|
||||
userLoaded: false,
|
||||
routingFinished: false,
|
||||
} as AppState),
|
||||
getters: {
|
||||
},
|
||||
actions: {
|
||||
}
|
||||
})
|
||||
id: "app",
|
||||
state: () =>
|
||||
({
|
||||
showMainNavigationBar: showMainNavigationBarInitialState(),
|
||||
userLoaded: false,
|
||||
routingFinished: false,
|
||||
} as AppState),
|
||||
getters: {},
|
||||
actions: {},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 type { Circle } from '@/services/circle'
|
||||
import { itPost } from '@/fetchHelpers'
|
||||
import { useLearningPathStore } from '@/stores/learningPath'
|
||||
import { itPost } from "@/fetchHelpers";
|
||||
import type { Circle } from "@/services/circle";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import type {
|
||||
CourseCompletionStatus,
|
||||
LearningContent,
|
||||
LearningUnit,
|
||||
LearningUnitQuestion,
|
||||
} from "@/types";
|
||||
|
||||
export type CircleStoreState = {
|
||||
circle: Circle | undefined
|
||||
page: 'INDEX' | 'OVERVIEW'
|
||||
}
|
||||
circle: Circle | undefined;
|
||||
page: "INDEX" | "OVERVIEW";
|
||||
};
|
||||
|
||||
export const useCircleStore = defineStore({
|
||||
id: 'circle',
|
||||
id: "circle",
|
||||
state: () => {
|
||||
return {
|
||||
circle: undefined,
|
||||
page: 'INDEX',
|
||||
page: "INDEX",
|
||||
} as CircleStoreState;
|
||||
},
|
||||
getters: {
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
async loadCircle(learningPathSlug: string, circleSlug: string): Promise<Circle> {
|
||||
this.circle = undefined;
|
||||
|
|
@ -37,9 +41,13 @@ export const useCircleStore = defineStore({
|
|||
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 result = circle.flatLearningContents.find((learningContent) => {
|
||||
return learningContent.slug.endsWith(learningContentSlug);
|
||||
|
|
@ -49,24 +57,31 @@ export const useCircleStore = defineStore({
|
|||
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 learningUnit = circle.flatLearningUnits.find((child) => {
|
||||
return child.slug.endsWith(learningUnitSlug)
|
||||
return child.slug.endsWith(learningUnitSlug);
|
||||
});
|
||||
|
||||
if (!learningUnit) {
|
||||
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 {
|
||||
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,
|
||||
completion_status: page.completion_status,
|
||||
});
|
||||
|
|
@ -75,37 +90,37 @@ export const useCircleStore = defineStore({
|
|||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return error
|
||||
return error;
|
||||
}
|
||||
},
|
||||
openLearningContent(learningContent: LearningContent) {
|
||||
const shortSlug = learningContent.slug.replace(`${this.circle?.slug}-lc-`, '');
|
||||
const shortSlug = learningContent.slug.replace(`${this.circle?.slug}-lc-`, "");
|
||||
this.router.push({
|
||||
path: `${this.circle?.getUrl()}/${shortSlug}`,
|
||||
});
|
||||
},
|
||||
closeLearningContent() {
|
||||
this.router.push({
|
||||
path: `${this.circle?.getUrl()}`
|
||||
path: `${this.circle?.getUrl()}`,
|
||||
});
|
||||
},
|
||||
openSelfEvaluation(learningUnit: LearningUnit) {
|
||||
const shortSlug = learningUnit.slug.replace(`${this.circle?.slug}-lu-`, '');
|
||||
const shortSlug = learningUnit.slug.replace(`${this.circle?.slug}-lu-`, "");
|
||||
this.router.push({
|
||||
path: `${this.circle?.getUrl()}/evaluate/${shortSlug}`,
|
||||
});
|
||||
},
|
||||
closeSelfEvaluation() {
|
||||
this.router.push({
|
||||
path: `${this.circle?.getUrl()}`
|
||||
path: `${this.circle?.getUrl()}`,
|
||||
});
|
||||
},
|
||||
calcSelfEvaluationStatus(learningUnit: LearningUnit) {
|
||||
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;
|
||||
}
|
||||
if (learningUnit.children.some((q) => q.completion_status === 'fail')) {
|
||||
if (learningUnit.children.some((q) => q.completion_status === "fail")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -113,14 +128,15 @@ export const useCircleStore = defineStore({
|
|||
},
|
||||
continueFromLearningContent(currentLearningContent: LearningContent) {
|
||||
if (currentLearningContent) {
|
||||
this.markCompletion(currentLearningContent, 'success');
|
||||
this.markCompletion(currentLearningContent, "success");
|
||||
|
||||
const nextLearningContent = currentLearningContent.nextLearningContent;
|
||||
const currentParent = currentLearningContent.parentLearningUnit;
|
||||
const nextParent = nextLearningContent?.parentLearningUnit;
|
||||
|
||||
if (
|
||||
currentParent && currentParent.id &&
|
||||
currentParent &&
|
||||
currentParent.id &&
|
||||
currentParent.id !== nextParent?.id &&
|
||||
currentParent.children.length > 0
|
||||
) {
|
||||
|
|
@ -130,7 +146,8 @@ export const useCircleStore = defineStore({
|
|||
} else if (currentLearningContent.nextLearningContent) {
|
||||
if (
|
||||
currentLearningContent.parentLearningSequence &&
|
||||
currentLearningContent.parentLearningSequence.id === nextLearningContent?.parentLearningSequence?.id
|
||||
currentLearningContent.parentLearningSequence.id ===
|
||||
nextLearningContent?.parentLearningSequence?.id
|
||||
) {
|
||||
this.openLearningContent(currentLearningContent.nextLearningContent);
|
||||
} else {
|
||||
|
|
@ -140,11 +157,11 @@ export const useCircleStore = defineStore({
|
|||
this.closeLearningContent();
|
||||
}
|
||||
} else {
|
||||
log.error('currentLearningContent is undefined');
|
||||
log.error("currentLearningContent is undefined");
|
||||
}
|
||||
},
|
||||
continueFromSelfEvaluation() {
|
||||
this.closeSelfEvaluation()
|
||||
this.closeSelfEvaluation();
|
||||
// if (this.currentSelfEvaluation) {
|
||||
// const nextContent = this.currentSelfEvaluation.learningContents[this.currentSelfEvaluation.learningContents.length - 1].nextLearningContent;
|
||||
//
|
||||
|
|
@ -160,6 +177,6 @@ export const useCircleStore = defineStore({
|
|||
// } else {
|
||||
// log.error('currentSelfEvaluation is undefined');
|
||||
// }
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { itGet } from '@/fetchHelpers'
|
||||
import { LearningPath } from '@/services/learningPath'
|
||||
import { itGet } from "@/fetchHelpers";
|
||||
import { LearningPath } from "@/services/learningPath";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export type LearningPathStoreState = {
|
||||
learningPath: LearningPath | undefined
|
||||
page: 'INDEX' | 'OVERVIEW'
|
||||
}
|
||||
learningPath: LearningPath | undefined;
|
||||
page: "INDEX" | "OVERVIEW";
|
||||
};
|
||||
|
||||
export const useLearningPathStore = defineStore({
|
||||
id: 'learningPath',
|
||||
id: "learningPath",
|
||||
state: () => {
|
||||
return {
|
||||
learningPath: undefined,
|
||||
page: 'INDEX',
|
||||
page: "INDEX",
|
||||
} as LearningPathStoreState;
|
||||
},
|
||||
getters: {},
|
||||
|
|
@ -22,7 +22,9 @@ export const useLearningPathStore = defineStore({
|
|||
return this.learningPath;
|
||||
}
|
||||
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) {
|
||||
throw `No learning path found with: ${slug}`;
|
||||
|
|
@ -31,5 +33,5 @@ export const useLearningPathStore = defineStore({
|
|||
this.learningPath = LearningPath.fromJson(learningPathData, completionData);
|
||||
return this.learningPath;
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,39 +1,39 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { itGet } from '@/fetchHelpers'
|
||||
import type { MediaLibraryPage } from '@/types'
|
||||
import { itGet } from "@/fetchHelpers";
|
||||
import type { MediaLibraryPage } from "@/types";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export type MediaCenterStoreState = {
|
||||
mediaCenterPage: MediaLibraryPage | undefined
|
||||
selectedLearningPath: { id: number; name: string }
|
||||
availableLearningPaths: { id: number; name: string }[]
|
||||
}
|
||||
mediaCenterPage: MediaLibraryPage | undefined;
|
||||
selectedLearningPath: { id: number; name: string };
|
||||
availableLearningPaths: { id: number; name: string }[];
|
||||
};
|
||||
|
||||
export const useMediaCenterStore = defineStore({
|
||||
id: 'mediaCenter',
|
||||
id: "mediaCenter",
|
||||
state: () => {
|
||||
return {
|
||||
mediaCenterPage: undefined,
|
||||
selectedLearningPath: { id: 1, name: 'Alle Lehrgänge' },
|
||||
selectedLearningPath: { id: 1, name: "Alle Lehrgänge" },
|
||||
availableLearningPaths: [
|
||||
{ id: 1, name: 'Alle Lehrgänge' },
|
||||
{ id: 2, name: 'Versicherungsvermittler/in' },
|
||||
{ id: 1, name: "Alle Lehrgänge" },
|
||||
{ id: 2, name: "Versicherungsvermittler/in" },
|
||||
],
|
||||
} as MediaCenterStoreState
|
||||
} as MediaCenterStoreState;
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
async loadMediaCenterPage(slug: string, reload = false) {
|
||||
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) {
|
||||
throw `No mediaCenterPageData found with: ${slug}`
|
||||
throw `No mediaCenterPageData found with: ${slug}`;
|
||||
}
|
||||
|
||||
this.mediaCenterPage = mediaCenterPageData
|
||||
return this.mediaCenterPage
|
||||
this.mediaCenterPage = mediaCenterPageData;
|
||||
return this.mediaCenterPage;
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,70 +1,73 @@
|
|||
import * as log from "loglevel";
|
||||
|
||||
import { defineStore } from "pinia";
|
||||
import { itGet, itPost } from "@/fetchHelpers";
|
||||
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
|
||||
|
||||
export type UserState = {
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
username: string,
|
||||
avatar_url: string,
|
||||
username: string;
|
||||
avatar_url: string;
|
||||
loggedIn: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
const initialUserState: UserState = {
|
||||
email: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
username: '',
|
||||
avatar_url: '',
|
||||
loggedIn: false
|
||||
}
|
||||
email: "",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
username: "",
|
||||
avatar_url: "",
|
||||
loggedIn: false,
|
||||
};
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: 'user',
|
||||
state: () => (initialUserState as UserState),
|
||||
id: "user",
|
||||
state: () => initialUserState as UserState,
|
||||
getters: {
|
||||
getFullName(): string {
|
||||
return `${this.first_name} ${this.last_name}`.trim();
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
handleLogin(username: string, password: string, next='/') {
|
||||
handleLogin(username: string, password: string, next = "/") {
|
||||
if (username && password) {
|
||||
itPost('/api/core/login/', {
|
||||
itPost("/api/core/login/", {
|
||||
username,
|
||||
password,
|
||||
}).then((data) => {
|
||||
this.$state = data;
|
||||
this.loggedIn = true;
|
||||
log.debug(`redirect to ${next}`);
|
||||
window.location.href = next;
|
||||
}).catch(() => {
|
||||
this.loggedIn = false;
|
||||
alert('Login failed');
|
||||
});
|
||||
})
|
||||
.then((data) => {
|
||||
this.$state = data;
|
||||
this.loggedIn = true;
|
||||
log.debug(`redirect to ${next}`);
|
||||
window.location.href = next;
|
||||
})
|
||||
.catch(() => {
|
||||
this.loggedIn = false;
|
||||
alert("Login failed");
|
||||
});
|
||||
}
|
||||
},
|
||||
handleLogout() {
|
||||
itPost('/api/core/logout/', {})
|
||||
.then(data => {
|
||||
Object.assign(this, initialUserState);
|
||||
window.location.href = '/';
|
||||
})
|
||||
itPost("/api/core/logout/", {}).then((data) => {
|
||||
Object.assign(this, initialUserState);
|
||||
window.location.href = "/";
|
||||
});
|
||||
},
|
||||
fetchUser() {
|
||||
const appStore = useAppStore();
|
||||
itGet('/api/core/me/').then((data) => {
|
||||
this.$state = data;
|
||||
this.loggedIn = true;
|
||||
appStore.userLoaded = true;
|
||||
}).catch(() => {
|
||||
this.loggedIn = false;
|
||||
appStore.userLoaded = true;
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
itGet("/api/core/me/")
|
||||
.then((data) => {
|
||||
this.$state = data;
|
||||
this.loggedIn = true;
|
||||
appStore.userLoaded = true;
|
||||
})
|
||||
.catch(() => {
|
||||
this.loggedIn = false;
|
||||
appStore.userLoaded = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
| 'assignment'
|
||||
| 'book'
|
||||
| 'document'
|
||||
| 'exercise'
|
||||
| 'media_library'
|
||||
| 'online_training'
|
||||
| 'resource'
|
||||
| 'test'
|
||||
| 'video'
|
||||
| "assignment"
|
||||
| "book"
|
||||
| "document"
|
||||
| "exercise"
|
||||
| "media_library"
|
||||
| "online_training"
|
||||
| "resource"
|
||||
| "test"
|
||||
| "video";
|
||||
|
||||
export interface LearningContentBlock {
|
||||
type: LearningContentType
|
||||
type: LearningContentType;
|
||||
value: {
|
||||
description: string
|
||||
}
|
||||
id: string
|
||||
description: string;
|
||||
};
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface AssignmentBlock {
|
||||
type: 'assignment';
|
||||
type: "assignment";
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
};
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface BookBlock {
|
||||
type: 'book';
|
||||
type: "book";
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
};
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface DocumentBlock {
|
||||
type: 'document';
|
||||
type: "document";
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
};
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ExerciseBlock {
|
||||
type: 'exercise';
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
id: string;
|
||||
type: "exercise";
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
};
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface MediaLibraryBlock {
|
||||
type: 'media_library';
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
id: string;
|
||||
type: "media_library";
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
};
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface OnlineTrainingBlock {
|
||||
type: 'online_training';
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
id: string;
|
||||
type: "online_training";
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
};
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ResourceBlock {
|
||||
type: 'resource';
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
id: string;
|
||||
type: "resource";
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
};
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface TestBlock {
|
||||
type: 'test';
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
id: string;
|
||||
type: "test";
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
};
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface VideoBlock {
|
||||
type: 'video';
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
id: string;
|
||||
type: "video";
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
};
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
||||
export interface CircleGoal {
|
||||
type: 'goal';
|
||||
type: "goal";
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CircleJobSituation {
|
||||
type: 'job_situation';
|
||||
type: "job_situation";
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
|
@ -124,9 +123,19 @@ export interface CourseWagtailPage {
|
|||
}
|
||||
|
||||
export interface LearningContent extends CourseWagtailPage {
|
||||
type: 'learnpath.LearningContent';
|
||||
type: "learnpath.LearningContent";
|
||||
minutes: number;
|
||||
contents: (AssignmentBlock | BookBlock | DocumentBlock | ExerciseBlock | MediaLibraryBlock | OnlineTrainingBlock | ResourceBlock | TestBlock | VideoBlock)[];
|
||||
contents: (
|
||||
| AssignmentBlock
|
||||
| BookBlock
|
||||
| DocumentBlock
|
||||
| ExerciseBlock
|
||||
| MediaLibraryBlock
|
||||
| OnlineTrainingBlock
|
||||
| ResourceBlock
|
||||
| TestBlock
|
||||
| VideoBlock
|
||||
)[];
|
||||
parentCircle: Circle;
|
||||
parentLearningSequence?: LearningSequence;
|
||||
parentLearningUnit?: LearningUnit;
|
||||
|
|
@ -135,13 +144,13 @@ export interface LearningContent extends CourseWagtailPage {
|
|||
}
|
||||
|
||||
export interface LearningUnitQuestion extends CourseWagtailPage {
|
||||
type: 'learnpath.LearningUnitQuestion';
|
||||
type: "learnpath.LearningUnitQuestion";
|
||||
parentLearningSequence?: LearningSequence;
|
||||
parentLearningUnit?: LearningUnit;
|
||||
}
|
||||
|
||||
export interface LearningUnit extends CourseWagtailPage {
|
||||
type: 'learnpath.LearningUnit';
|
||||
type: "learnpath.LearningUnit";
|
||||
learningContents: LearningContent[];
|
||||
minutes: number;
|
||||
parentLearningSequence?: LearningSequence;
|
||||
|
|
@ -150,22 +159,26 @@ export interface LearningUnit extends CourseWagtailPage {
|
|||
}
|
||||
|
||||
export interface LearningSequence extends CourseWagtailPage {
|
||||
type: 'learnpath.LearningSequence';
|
||||
type: "learnpath.LearningSequence";
|
||||
icon: string;
|
||||
learningUnits: LearningUnit[];
|
||||
minutes: number;
|
||||
}
|
||||
|
||||
export type CircleChild = LearningContent | LearningUnit | LearningSequence | LearningUnitQuestion;
|
||||
export type CircleChild =
|
||||
| LearningContent
|
||||
| LearningUnit
|
||||
| LearningSequence
|
||||
| LearningUnitQuestion;
|
||||
|
||||
export interface WagtailCircle extends CourseWagtailPage {
|
||||
type: 'learnpath.Circle';
|
||||
type: "learnpath.Circle";
|
||||
children: CircleChild[];
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Topic extends CourseWagtailPage {
|
||||
type: 'learnpath.Topic';
|
||||
type: "learnpath.Topic";
|
||||
is_visible: boolean;
|
||||
circles: Circle[];
|
||||
}
|
||||
|
|
@ -186,17 +199,16 @@ export interface CourseCompletion {
|
|||
}
|
||||
|
||||
export interface CircleDiagramData {
|
||||
index: number
|
||||
title: string
|
||||
icon: string
|
||||
startAngle: number
|
||||
endAngle: number
|
||||
arrowStartAngle: number
|
||||
arrowEndAngle: number
|
||||
done: boolean
|
||||
index: number;
|
||||
title: string;
|
||||
icon: string;
|
||||
startAngle: number;
|
||||
endAngle: number;
|
||||
arrowStartAngle: number;
|
||||
arrowEndAngle: number;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
|
||||
export interface Course {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -223,7 +235,7 @@ export interface MediaLink {
|
|||
description: string;
|
||||
link_display_text: string;
|
||||
url: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface MediaContentCollection {
|
||||
|
|
@ -231,26 +243,26 @@ export interface MediaContentCollection {
|
|||
value: {
|
||||
title: string;
|
||||
contents: (MediaDocument | MediaLink)[];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface MediaCategoryPage extends CourseWagtailPage {
|
||||
type: 'media_library.MediaCategoryPage';
|
||||
type: "media_library.MediaCategoryPage";
|
||||
overview_icon: string;
|
||||
introduction_text: string;
|
||||
description_title: string;
|
||||
description_text: string;
|
||||
items: {
|
||||
type: 'item';
|
||||
type: "item";
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
};
|
||||
course_category: CourseCategory;
|
||||
body: MediaContentCollection[];
|
||||
}
|
||||
|
||||
export interface MediaLibraryPage extends CourseWagtailPage {
|
||||
type: 'media_library.MediaLibraryPage';
|
||||
type: "media_library.MediaLibraryPage";
|
||||
course: Course;
|
||||
children: MediaCategoryPage[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { expect, test } from 'vitest'
|
||||
import { humanizeDuration } from '../humanizeDuration'
|
||||
import { expect, test } from "vitest";
|
||||
import { humanizeDuration } from "../humanizeDuration";
|
||||
|
||||
test('format duration for humans', () => {
|
||||
expect(humanizeDuration(1)).toBe('1 Minute')
|
||||
expect(humanizeDuration(15)).toBe('15 Minuten')
|
||||
expect(humanizeDuration(42)).toBe('45 Minuten')
|
||||
expect(humanizeDuration(60)).toBe('1 Stunde')
|
||||
expect(humanizeDuration(122)).toBe('2 Stunden')
|
||||
expect(humanizeDuration(120)).toBe('2 Stunden')
|
||||
expect(humanizeDuration(132)).toBe('2 Stunden 15 Minuten')
|
||||
expect(humanizeDuration(632)).toBe('10 Stunden')
|
||||
})
|
||||
test("format duration for humans", () => {
|
||||
expect(humanizeDuration(1)).toBe("1 Minute");
|
||||
expect(humanizeDuration(15)).toBe("15 Minuten");
|
||||
expect(humanizeDuration(42)).toBe("45 Minuten");
|
||||
expect(humanizeDuration(60)).toBe("1 Stunde");
|
||||
expect(humanizeDuration(122)).toBe("2 Stunden");
|
||||
expect(humanizeDuration(120)).toBe("2 Stunden");
|
||||
expect(humanizeDuration(132)).toBe("2 Stunden 15 Minuten");
|
||||
expect(humanizeDuration(632)).toBe("10 Stunden");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,29 +1,30 @@
|
|||
function pluralize(text: string, count: number) {
|
||||
if (count === 1) {
|
||||
return text;
|
||||
}
|
||||
return text + 'n';
|
||||
if (count === 1) {
|
||||
return text;
|
||||
}
|
||||
return text + "n";
|
||||
}
|
||||
|
||||
|
||||
export function humanizeDuration(minutes: number) {
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const remainingMinutes = minutes % 60
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
|
||||
if (hours === 0 && minutes < 16) {
|
||||
return pluralize(`${remainingMinutes} Minute`, remainingMinutes)
|
||||
}
|
||||
if (hours === 0 && minutes < 16) {
|
||||
return pluralize(`${remainingMinutes} Minute`, remainingMinutes);
|
||||
}
|
||||
|
||||
// Remaining minutes are rounded to 15 mins
|
||||
const roundToMinutes = 15
|
||||
const roundedMinutes = Math.round((minutes % 60) / roundToMinutes) * roundToMinutes
|
||||
// Remaining minutes are rounded to 15 mins
|
||||
const roundToMinutes = 15;
|
||||
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 minutesString = roundedMinutes > 0 && hours < showMinutesUpToHours
|
||||
? pluralize(`${roundedMinutes} Minute`, roundedMinutes) : ''
|
||||
const showMinutesUpToHours = 10;
|
||||
const minutesString =
|
||||
roundedMinutes > 0 && hours < showMinutesUpToHours
|
||||
? pluralize(`${roundedMinutes} Minute`, roundedMinutes)
|
||||
: "";
|
||||
|
||||
const delimiter = hoursString && minutesString ? ' ' : ''
|
||||
return `${hoursString}${delimiter}${minutesString}`
|
||||
}
|
||||
const delimiter = hoursString && minutesString ? " " : "";
|
||||
return `${hoursString}${delimiter}${minutesString}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import type { LearningContentType } from '@/types';
|
||||
import type { LearningContentType } from "@/types";
|
||||
|
||||
export const learningContentTypesToName = new Map<LearningContentType, string>([
|
||||
['assignment', 'Auftrag'],
|
||||
['book', 'Buch'],
|
||||
['document', 'Dokument'],
|
||||
['exercise', 'Übung'],
|
||||
['media_library', 'Mediathek'],
|
||||
['online_training', 'Online-Training'],
|
||||
['video', 'Video'],
|
||||
['test', 'Test'],
|
||||
['resource', 'Hilfsmittel'],
|
||||
]);
|
||||
["assignment", "Auftrag"],
|
||||
["book", "Buch"],
|
||||
["document", "Dokument"],
|
||||
["exercise", "Übung"],
|
||||
["media_library", "Mediathek"],
|
||||
["online_training", "Online-Training"],
|
||||
["video", "Video"],
|
||||
["test", "Test"],
|
||||
["resource", "Hilfsmittel"],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
let url = document.location.href;
|
||||
if (url.charAt(url.length - 1) !== '/') {
|
||||
url += '/';
|
||||
if (url.charAt(url.length - 1) !== "/") {
|
||||
url += "/";
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<main class="px-4 py-8">
|
||||
<h1>404 - Not Found as Vue view...</h1>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,57 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import LearningSequence from '@/components/circle/LearningSequence.vue'
|
||||
import CircleOverview from '@/components/circle/CircleOverview.vue'
|
||||
import CircleDiagram from '@/components/circle/CircleDiagram.vue'
|
||||
import CircleDiagram from "@/components/circle/CircleDiagram.vue";
|
||||
import CircleOverview from "@/components/circle/CircleOverview.vue";
|
||||
import LearningSequence from "@/components/circle/LearningSequence.vue";
|
||||
import * as log from "loglevel";
|
||||
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useCircleStore } from '@/stores/circle'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useRoute } from 'vue-router'
|
||||
import _ from 'lodash'
|
||||
import { humanizeDuration } from '@/utils/humanizeDuration'
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import { humanizeDuration } from "@/utils/humanizeDuration";
|
||||
import _ from "lodash";
|
||||
import { computed, onMounted } from "vue";
|
||||
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<{
|
||||
learningPathSlug: string
|
||||
circleSlug: string
|
||||
}>()
|
||||
learningPathSlug: string;
|
||||
circleSlug: string;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore()
|
||||
appStore.showMainNavigationBar = true
|
||||
const appStore = useAppStore();
|
||||
appStore.showMainNavigationBar = true;
|
||||
|
||||
const circleStore = useCircleStore()
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const duration = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
const minutes = _.sumBy(circleStore.circle.learningSequences, 'minutes')
|
||||
return humanizeDuration(minutes)
|
||||
const minutes = _.sumBy(circleStore.circle.learningSequences, "minutes");
|
||||
return humanizeDuration(minutes);
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
return "";
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug('CircleView.vue mounted', props.learningPathSlug, props.circleSlug)
|
||||
log.debug("CircleView.vue mounted", props.learningPathSlug, props.circleSlug);
|
||||
|
||||
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) => {
|
||||
return ls.slug.endsWith(route.hash.replace('#', ''))
|
||||
})
|
||||
return ls.slug.endsWith(route.hash.replace("#", ""));
|
||||
});
|
||||
if (hashLearningSequence) {
|
||||
document.getElementById(hashLearningSequence.slug)?.scrollIntoView({ behavior: 'smooth' })
|
||||
document
|
||||
.getElementById(hashLearningSequence.slug)
|
||||
?.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
log.error(error);
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -89,7 +91,10 @@ onMounted(async () => {
|
|||
</div>
|
||||
|
||||
<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" />
|
||||
Das lernst du in diesem Circle
|
||||
</div>
|
||||
|
|
@ -106,7 +111,10 @@ onMounted(async () => {
|
|||
{{ circleStore.circle?.description }}
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -116,17 +124,22 @@ onMounted(async () => {
|
|||
<div class="leading-relaxed mt-4">
|
||||
Tausche dich mit der Fachexpertin aus für den Circle Analyse aus.
|
||||
</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 class="flex-auto bg-gray-200 px-4 py-8 lg:px-24">
|
||||
<div
|
||||
v-for="learningSequence in circleStore.circle?.learningSequences || []"
|
||||
v-for="learningSequence in circleStore.circle?.learningSequences ||
|
||||
[]"
|
||||
:key="learningSequence.translation_key"
|
||||
>
|
||||
<LearningSequence :learning-sequence="learningSequence"></LearningSequence>
|
||||
<LearningSequence
|
||||
:learning-sequence="learningSequence"
|
||||
></LearningSequence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<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>
|
||||
|
||||
<template>
|
||||
|
|
@ -16,7 +16,9 @@ const userStore = useUserStore()
|
|||
<div class="mt-8 p-8 break-words bg-white max-w-xl">
|
||||
<h3>Versicherungsvermittler/in</h3>
|
||||
<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>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter()
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<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="-mx-16 pt-4 pb-24 px-16 mb-20 bg-gray-200">
|
||||
<nav>
|
||||
<a
|
||||
class="block my-9 cursor-pointer flex items-center"
|
||||
@click="router.go(-1)"><it-icon-arrow-left /><span>zurück</span></a>
|
||||
<a class="block my-9 cursor-pointer flex items-center" @click="router.go(-1)"
|
||||
><it-icon-arrow-left /><span>zurück</span></a
|
||||
>
|
||||
</nav>
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
|
|
@ -23,7 +22,7 @@ const router = useRouter()
|
|||
|
||||
<style scoped>
|
||||
.it-icon-hf {
|
||||
color: blue
|
||||
color: blue;
|
||||
}
|
||||
.it-icon-hf > * {
|
||||
@apply m-auto;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import { onMounted, reactive, watch } from 'vue'
|
||||
import { useCircleStore } from '@/stores/circle'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import LearningContent from '@/components/circle/LearningContent.vue'
|
||||
import type { LearningContent as LearningContentType } from '@/types'
|
||||
import LearningContent from "@/components/circle/LearningContent.vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningContent as LearningContentType } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
import { onMounted, reactive, watch } from "vue";
|
||||
|
||||
log.debug('LearningContentView created')
|
||||
log.debug("LearningContentView created");
|
||||
|
||||
const props = defineProps<{
|
||||
learningPathSlug: string
|
||||
circleSlug: string
|
||||
contentSlug: string
|
||||
}>()
|
||||
learningPathSlug: string;
|
||||
circleSlug: string;
|
||||
contentSlug: string;
|
||||
}>();
|
||||
|
||||
const state: { learningContent?: LearningContentType } = reactive({})
|
||||
const state: { learningContent?: LearningContentType } = reactive({});
|
||||
|
||||
const appStore = useAppStore()
|
||||
appStore.showMainNavigationBar = false
|
||||
const appStore = useAppStore();
|
||||
appStore.showMainNavigationBar = false;
|
||||
|
||||
const circleStore = useCircleStore()
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const loadLearningContent = async () => {
|
||||
try {
|
||||
|
|
@ -27,33 +27,41 @@ const loadLearningContent = async () => {
|
|||
props.learningPathSlug,
|
||||
props.circleSlug,
|
||||
props.contentSlug
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.contentSlug,
|
||||
async () => {
|
||||
log.debug(
|
||||
'LearningContentView props.contentSlug changed',
|
||||
"LearningContentView props.contentSlug changed",
|
||||
props.learningPathSlug,
|
||||
props.circleSlug,
|
||||
props.contentSlug
|
||||
)
|
||||
await loadLearningContent()
|
||||
);
|
||||
await loadLearningContent();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug('LearningContentView mounted', props.learningPathSlug, props.circleSlug, props.contentSlug)
|
||||
await loadLearningContent()
|
||||
})
|
||||
log.debug(
|
||||
"LearningContentView mounted",
|
||||
props.learningPathSlug,
|
||||
props.circleSlug,
|
||||
props.contentSlug
|
||||
);
|
||||
await loadLearningContent();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LearningContent v-if="state.learningContent" :learning-content="state.learningContent" />
|
||||
<LearningContent
|
||||
v-if="state.learningContent"
|
||||
:learning-content="state.learningContent"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,55 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import * as log from "loglevel";
|
||||
|
||||
import { onMounted } from 'vue'
|
||||
import { useLearningPathStore } from '@/stores/learningPath'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
import LearningPathDiagram from '@/components/circle/LearningPathDiagram.vue'
|
||||
import LearningPathViewVertical from '@/views/LearningPathViewVertical.vue'
|
||||
import type { LearningPath } from '@/services/learningPath'
|
||||
import LearningPathDiagram from "@/components/circle/LearningPathDiagram.vue";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import LearningPathViewVertical from "@/views/LearningPathViewVertical.vue";
|
||||
|
||||
log.debug('LearningPathView created')
|
||||
log.debug("LearningPathView created");
|
||||
|
||||
const props = defineProps<{
|
||||
learningPathSlug: string
|
||||
}>()
|
||||
learningPathSlug: string;
|
||||
}>();
|
||||
|
||||
const learningPathStore = useLearningPathStore()
|
||||
const userStore = useUserStore()
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug('LearningPathView mounted')
|
||||
log.debug("LearningPathView mounted");
|
||||
|
||||
try {
|
||||
await learningPathStore.loadLearningPath(props.learningPathSlug)
|
||||
await learningPathStore.loadLearningPath(props.learningPathSlug);
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
log.error(error);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
|
||||
if (learningPath.nextLearningContent) {
|
||||
const circle = learningPath.nextLearningContent.parentCircle
|
||||
const lsShortSlug = learningPath.nextLearningContent.parentLearningSequence?.slug.replace(`${circle.slug}-`, '')
|
||||
const url = `/learn/${learningPath.slug}/${learningPath.nextLearningContent.parentCircle.slug}#${lsShortSlug}`
|
||||
const circle = learningPath.nextLearningContent.parentCircle;
|
||||
const lsShortSlug =
|
||||
learningPath.nextLearningContent.parentLearningSequence?.slug.replace(
|
||||
`${circle.slug}-`,
|
||||
""
|
||||
);
|
||||
const url = `/learn/${learningPath.slug}/${learningPath.nextLearningContent.parentCircle.slug}#${lsShortSlug}`;
|
||||
const isFirst =
|
||||
learningPath.nextLearningContent.translation_key ===
|
||||
learningPath.circles[0].flatLearningContents[0].translation_key
|
||||
return [url, isFirst]
|
||||
learningPath.circles[0].flatLearningContents[0].translation_key;
|
||||
return [url, isFirst];
|
||||
}
|
||||
|
||||
return ['', false]
|
||||
}
|
||||
return ["", false];
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200" v-if="learningPathStore.learningPath">
|
||||
<div v-if="learningPathStore.learningPath" class="bg-gray-200">
|
||||
<Teleport to="body">
|
||||
<LearningPathViewVertical
|
||||
:show="learningPathStore.page === 'OVERVIEW'"
|
||||
:learning-path-slug="props.learningPathSlug"
|
||||
@closemodal="learningPathStore.page = 'INDEX'"
|
||||
v-bind:learning-path-slug="props.learningPathSlug"
|
||||
/>
|
||||
</Teleport>
|
||||
|
||||
|
|
@ -57,7 +61,11 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
|
|||
<div class="flex flex-col h-max">
|
||||
<div class="bg-white py-8 flex flex-col">
|
||||
<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 />
|
||||
Listenansicht anzeigen
|
||||
</button>
|
||||
|
|
@ -65,7 +73,7 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
|
|||
<LearningPathDiagram
|
||||
class="max-w-[1680px] w-full"
|
||||
identifier="mainVisualization"
|
||||
v-bind:vertical="false"
|
||||
:vertical="false"
|
||||
></LearningPathDiagram>
|
||||
</div>
|
||||
|
||||
|
|
@ -80,11 +88,19 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
|
|||
<h2>Willkommmen zurück, {{ userStore.first_name }}</h2>
|
||||
<p class="mt-4 text-xl"></p>
|
||||
</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
|
||||
<h3>
|
||||
{{ learningPathStore.learningPath.nextLearningContent.parentCircle.title }}:
|
||||
{{ learningPathStore.learningPath.nextLearningContent.parentLearningSequence?.title }}
|
||||
{{
|
||||
learningPathStore.learningPath.nextLearningContent.parentCircle.title
|
||||
}}:
|
||||
{{
|
||||
learningPathStore.learningPath.nextLearningContent
|
||||
.parentLearningSequence?.title
|
||||
}}
|
||||
</h3>
|
||||
<router-link
|
||||
class="mt-4 btn-blue"
|
||||
|
|
@ -92,7 +108,9 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
|
|||
data-cy="lp-continue-button"
|
||||
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>
|
||||
</router-link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import { useLearningPathStore } from '@/stores/learningPath'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import * as log from "loglevel";
|
||||
|
||||
import LearningPathDiagram from '@/components/circle/LearningPathDiagram.vue'
|
||||
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue'
|
||||
import LearningPathDiagram from "@/components/circle/LearningPathDiagram.vue";
|
||||
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
||||
|
||||
log.debug('LearningPathView created')
|
||||
log.debug("LearningPathView created");
|
||||
|
||||
const props = defineProps<{
|
||||
learningPathSlug: string
|
||||
show: boolean
|
||||
}>()
|
||||
learningPathSlug: string;
|
||||
show: boolean;
|
||||
}>();
|
||||
|
||||
const learningPathStore = useLearningPathStore()
|
||||
const userStore = useUserStore()
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const emits = defineEmits(['closemodal'])
|
||||
const emits = defineEmits(["closemodal"]);
|
||||
</script>
|
||||
<template>
|
||||
<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>
|
||||
<div class="learningpath flex flex-col">
|
||||
<div class="flex flex-col h-max">
|
||||
<LearningPathDiagram
|
||||
class="w-full"
|
||||
identifier="verticalVisualization"
|
||||
v-bind:vertical="true"
|
||||
:vertical="true"
|
||||
></LearningPathDiagram>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,41 +1,46 @@
|
|||
<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 { useCircleStore } from '@/stores/circle'
|
||||
import type { LearningUnit } from '@/types'
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningUnit } from "@/types";
|
||||
import { onMounted, reactive } from "vue";
|
||||
|
||||
log.debug('LearningUnitSelfEvaluationView created')
|
||||
log.debug("LearningUnitSelfEvaluationView created");
|
||||
|
||||
const props = defineProps<{
|
||||
learningPathSlug: string
|
||||
circleSlug: string
|
||||
learningUnitSlug: string
|
||||
}>()
|
||||
learningPathSlug: string;
|
||||
circleSlug: string;
|
||||
learningUnitSlug: string;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore()
|
||||
appStore.showMainNavigationBar = false
|
||||
const appStore = useAppStore();
|
||||
appStore.showMainNavigationBar = false;
|
||||
|
||||
const circleStore = useCircleStore()
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const state: { learningUnit?: LearningUnit } = reactive({})
|
||||
const state: { learningUnit?: LearningUnit } = reactive({});
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug('LearningUnitSelfEvaluationView mounted', props.learningPathSlug, props.circleSlug, props.learningUnitSlug)
|
||||
log.debug(
|
||||
"LearningUnitSelfEvaluationView mounted",
|
||||
props.learningPathSlug,
|
||||
props.circleSlug,
|
||||
props.learningUnitSlug
|
||||
);
|
||||
|
||||
try {
|
||||
state.learningUnit = await circleStore.loadSelfEvaluation(
|
||||
props.learningPathSlug,
|
||||
props.circleSlug,
|
||||
props.learningUnitSlug
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
log.error(error);
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -1,34 +1,38 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import { reactive } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import * as log from "loglevel";
|
||||
import { reactive } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute()
|
||||
const route = useRoute();
|
||||
|
||||
log.debug('LoginView.vue created')
|
||||
log.debug(route.query)
|
||||
log.debug("LoginView.vue created");
|
||||
log.debug(route.query);
|
||||
|
||||
const state = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userStore = useUserStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="px-8 py-8">
|
||||
<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">
|
||||
<label class="block mb-1" for="email">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="state.username"
|
||||
type="text"
|
||||
name="username"
|
||||
v-model="state.username"
|
||||
class="py-2 px-3 border border-gray-500 mt-1 block w-96"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -36,9 +40,9 @@ const userStore = useUserStore()
|
|||
<label class="block mb-1" for="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="state.password"
|
||||
type="password"
|
||||
name="password"
|
||||
v-model="state.password"
|
||||
class="py-2 px-3 border border-gray-500 mt-1 block w-96"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,195 +1,205 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import LinkCard from '@/components/mediaCenter/LinkCard.vue'
|
||||
import HandlungsfeldLayout from '@/views/HandlungsfeldLayout.vue'
|
||||
import MediaLink from '@/components/mediaCenter/MediaLink.vue'
|
||||
import { useMediaCenterStore } from '@/stores/mediaCenter'
|
||||
import { computed } from 'vue'
|
||||
import LinkCard from "@/components/mediaCenter/LinkCard.vue";
|
||||
import MediaLink from "@/components/mediaCenter/MediaLink.vue";
|
||||
import { useMediaCenterStore } from "@/stores/mediaCenter";
|
||||
import HandlungsfeldLayout from "@/views/HandlungsfeldLayout.vue";
|
||||
import * as log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
|
||||
const field = {
|
||||
title: 'Fahrzeug',
|
||||
title: "Fahrzeug",
|
||||
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.',
|
||||
icon: '/static/icons/demo/icon-hf-fahrzeug-big.svg',
|
||||
"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",
|
||||
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.',
|
||||
items: ['Motorfahrzeughaftpflichtversicherung', 'Motorfahrzeugkaskoversicherung', 'Insassenunfallversicherung'],
|
||||
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: [
|
||||
{
|
||||
title: 'Lernmedien',
|
||||
type: 'learnmedia',
|
||||
moreLink: '',
|
||||
title: "Lernmedien",
|
||||
type: "learnmedia",
|
||||
moreLink: "",
|
||||
items: [
|
||||
{
|
||||
title: 'Die Motorfahrzeughaftpflicht',
|
||||
description: 'Buch «Sach- und Vermögensversicherungen» – Kapitel 16',
|
||||
iconUrl: '/static/icons/demo/icon-hf-book.png',
|
||||
linkText: 'PDF anzeigen',
|
||||
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
|
||||
title: "Die Motorfahrzeughaftpflicht",
|
||||
description: "Buch «Sach- und Vermögensversicherungen» – Kapitel 16",
|
||||
iconUrl: "/static/icons/demo/icon-hf-book.png",
|
||||
linkText: "PDF anzeigen",
|
||||
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
|
||||
openWindow: true,
|
||||
},
|
||||
{
|
||||
title: 'Die Motorfahrzeughaftpflicht',
|
||||
iconUrl: '/static/icons/demo/icon-hf-book.png',
|
||||
description: 'Buch «Sach- und Vermögensversicherungen» – Kapitel 16',
|
||||
linkText: 'PDF anzeigen',
|
||||
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
|
||||
title: "Die Motorfahrzeughaftpflicht",
|
||||
iconUrl: "/static/icons/demo/icon-hf-book.png",
|
||||
description: "Buch «Sach- und Vermögensversicherungen» – Kapitel 16",
|
||||
linkText: "PDF anzeigen",
|
||||
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
|
||||
openWindow: true,
|
||||
},
|
||||
{
|
||||
title: 'Die Motorfahrzeughaftpflicht',
|
||||
iconUrl: '/static/icons/demo/icon-hf-book.png',
|
||||
description: 'Buch «Sach- und Vermögensversicherungen» – Kapitel 16',
|
||||
linkText: 'PDF anzeigen',
|
||||
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
|
||||
title: "Die Motorfahrzeughaftpflicht",
|
||||
iconUrl: "/static/icons/demo/icon-hf-book.png",
|
||||
description: "Buch «Sach- und Vermögensversicherungen» – Kapitel 16",
|
||||
linkText: "PDF anzeigen",
|
||||
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
|
||||
openWindow: true,
|
||||
},
|
||||
{
|
||||
title: 'Die Motorfahrzeughaftpflicht',
|
||||
iconUrl: '/static/icons/demo/icon-hf-book.png',
|
||||
description: 'Buch «Sach- und Vermögensversicherungen» – Kapitel 16',
|
||||
linkText: 'PDF anzeigen',
|
||||
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
|
||||
title: "Die Motorfahrzeughaftpflicht",
|
||||
iconUrl: "/static/icons/demo/icon-hf-book.png",
|
||||
description: "Buch «Sach- und Vermögensversicherungen» – Kapitel 16",
|
||||
linkText: "PDF anzeigen",
|
||||
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
|
||||
openWindow: true,
|
||||
},
|
||||
{
|
||||
title: 'Die Motorfahrzeughaftpflicht',
|
||||
iconUrl: '/static/icons/demo/icon-hf-book.png',
|
||||
description: 'Buch «Sach- und Vermögensversicherungen» – Kapitel 16',
|
||||
linkText: 'PDF anzeigen',
|
||||
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
|
||||
title: "Die Motorfahrzeughaftpflicht",
|
||||
iconUrl: "/static/icons/demo/icon-hf-book.png",
|
||||
description: "Buch «Sach- und Vermögensversicherungen» – Kapitel 16",
|
||||
linkText: "PDF anzeigen",
|
||||
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
|
||||
openWindow: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Links',
|
||||
type: 'externalLinks',
|
||||
moreLink: '',
|
||||
title: "Links",
|
||||
type: "externalLinks",
|
||||
moreLink: "",
|
||||
items: [
|
||||
{
|
||||
title: 'Nationales Versicherungsbüro',
|
||||
iconUrl: '',
|
||||
description: '',
|
||||
linkText: 'Link öffnen',
|
||||
link: 'https://www.nbi-ngf.ch/h',
|
||||
title: "Nationales Versicherungsbüro",
|
||||
iconUrl: "",
|
||||
description: "",
|
||||
linkText: "Link öffnen",
|
||||
link: "https://www.nbi-ngf.ch/h",
|
||||
openWindow: true,
|
||||
},
|
||||
{
|
||||
title: 'Adressen der Strassenverkehrsämter',
|
||||
iconUrl: '',
|
||||
description: '',
|
||||
linkText: 'Link öffnen',
|
||||
link: 'https://asa.ch/strassenverkehrsaemter/adressen/',
|
||||
title: "Adressen der Strassenverkehrsämter",
|
||||
iconUrl: "",
|
||||
description: "",
|
||||
linkText: "Link öffnen",
|
||||
link: "https://asa.ch/strassenverkehrsaemter/adressen/",
|
||||
openWindow: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Verankerung im Lernpfad',
|
||||
type: 'internalLinks',
|
||||
moreLink: '',
|
||||
title: "Verankerung im Lernpfad",
|
||||
type: "internalLinks",
|
||||
moreLink: "",
|
||||
items: [
|
||||
{
|
||||
title: 'Circle: Einstieg – Lernsequenz: Anwenden',
|
||||
iconUrl: '',
|
||||
description: '',
|
||||
linkText: 'Lerineinheit anzeigen',
|
||||
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse',
|
||||
title: "Circle: Einstieg – Lernsequenz: Anwenden",
|
||||
iconUrl: "",
|
||||
description: "",
|
||||
linkText: "Lerineinheit anzeigen",
|
||||
link: "http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse",
|
||||
openWindow: false,
|
||||
},
|
||||
{
|
||||
title: 'Circle: Einstieg – Lernsequenz: Anwenden',
|
||||
iconUrl: '',
|
||||
description: '',
|
||||
linkText: 'Lerineinheit anzeigen',
|
||||
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse',
|
||||
title: "Circle: Einstieg – Lernsequenz: Anwenden",
|
||||
iconUrl: "",
|
||||
description: "",
|
||||
linkText: "Lerineinheit anzeigen",
|
||||
link: "http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse",
|
||||
openWindow: false,
|
||||
},
|
||||
{
|
||||
title: 'Circle: Einstieg – Lernsequenz: Anwenden',
|
||||
iconUrl: '',
|
||||
description: '',
|
||||
linkText: 'Lerineinheit anzeigen',
|
||||
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse',
|
||||
title: "Circle: Einstieg – Lernsequenz: Anwenden",
|
||||
iconUrl: "",
|
||||
description: "",
|
||||
linkText: "Lerineinheit anzeigen",
|
||||
link: "http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse",
|
||||
openWindow: false,
|
||||
},
|
||||
{
|
||||
title: 'Circle: Einstieg – Lernsequenz: Anwenden',
|
||||
iconUrl: '',
|
||||
description: '',
|
||||
linkText: 'Lerineinheit anzeigen',
|
||||
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse',
|
||||
title: "Circle: Einstieg – Lernsequenz: Anwenden",
|
||||
iconUrl: "",
|
||||
description: "",
|
||||
linkText: "Lerineinheit anzeigen",
|
||||
link: "http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse",
|
||||
openWindow: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Querverweise',
|
||||
type: 'realtiveLinks',
|
||||
moreLink: '',
|
||||
title: "Querverweise",
|
||||
type: "realtiveLinks",
|
||||
moreLink: "",
|
||||
items: [
|
||||
{
|
||||
title: 'Rechtsstreigkeiten',
|
||||
iconUrl: '/static/icons/demo/icon-hf-einkommenssicherung.svg',
|
||||
description: 'Lernmedium: Verkehrsrechtsschutz – Buch «Sach- und Vermögensversicherungen/Kapitel 12.3»',
|
||||
linkText: 'Handlungsfeldanzeigen',
|
||||
link: 'http://localhost:8000/mediacenter/handlungsfeld',
|
||||
title: "Rechtsstreigkeiten",
|
||||
iconUrl: "/static/icons/demo/icon-hf-einkommenssicherung.svg",
|
||||
description:
|
||||
"Lernmedium: Verkehrsrechtsschutz – Buch «Sach- und Vermögensversicherungen/Kapitel 12.3»",
|
||||
linkText: "Handlungsfeldanzeigen",
|
||||
link: "http://localhost:8000/mediacenter/handlungsfeld",
|
||||
openWindow: false,
|
||||
},
|
||||
{
|
||||
title: 'Rechtsstreigkeiten',
|
||||
iconUrl: '/static/icons/demo/icon-hf-einkommenssicherung.svg',
|
||||
description: 'Lernmedium: Verkehrsrechtsschutz – Buch «Sach- und Vermögensversicherungen/Kapitel 12.3»',
|
||||
linkText: 'Handlungsfeldanzeigen',
|
||||
link: 'http://localhost:8000/mediacenter/handlungsfeld',
|
||||
title: "Rechtsstreigkeiten",
|
||||
iconUrl: "/static/icons/demo/icon-hf-einkommenssicherung.svg",
|
||||
description:
|
||||
"Lernmedium: Verkehrsrechtsschutz – Buch «Sach- und Vermögensversicherungen/Kapitel 12.3»",
|
||||
linkText: "Handlungsfeldanzeigen",
|
||||
link: "http://localhost:8000/mediacenter/handlungsfeld",
|
||||
openWindow: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
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(() => {
|
||||
return mediaStore.mediaCenterPage?.children.find((category) => category.slug === props.mediaCategorySlug)
|
||||
})
|
||||
return mediaStore.mediaCenterPage?.children.find(
|
||||
(category) => category.slug === props.mediaCategorySlug
|
||||
);
|
||||
});
|
||||
|
||||
const maxCardItems = 4
|
||||
const maxListItems = 6
|
||||
const maxCardItems = 4;
|
||||
const maxListItems = 6;
|
||||
|
||||
const displayAsCard = (itemType: string): boolean => {
|
||||
return itemType === 'learnmedia' || itemType === 'realtiveLinks'
|
||||
}
|
||||
return itemType === "learnmedia" || itemType === "realtiveLinks";
|
||||
};
|
||||
|
||||
const hasMoreItems = (items: object[], maxItems: number): boolean => {
|
||||
return items.length > maxItems
|
||||
}
|
||||
return items.length > maxItems;
|
||||
};
|
||||
|
||||
const getMaxDisplayItems = (items: object[], maxItems: number) => {
|
||||
return items.slice(0, maxItems)
|
||||
}
|
||||
return items.slice(0, maxItems);
|
||||
};
|
||||
|
||||
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 maxItems = displayAsCard(itemType) ? maxCardItems : maxListItems
|
||||
return hasMoreItems(items, maxItems)
|
||||
}
|
||||
const maxItems = displayAsCard(itemType) ? maxCardItems : maxListItems;
|
||||
return hasMoreItems(items, maxItems);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body" v-if="mediaStore.mediaCenterPage && mediaCategory">
|
||||
<Teleport v-if="mediaStore.mediaCenterPage && mediaCategory" to="body">
|
||||
<HandlungsfeldLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between">
|
||||
|
|
@ -206,13 +216,19 @@ const hasMoreItemsForType = (itemType: string, items: object[]) => {
|
|||
<h2 class="mb-4">{{ mediaCategory.description_title }}</h2>
|
||||
<p class="mb-4 lg:w-2/3">{{ mediaCategory.description_text }}</p>
|
||||
<ul>
|
||||
<li 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>
|
||||
<li
|
||||
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 }}
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
<ul
|
||||
:class="{
|
||||
|
|
@ -221,7 +237,10 @@ const hasMoreItemsForType = (itemType: string, items: object[]) => {
|
|||
'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
|
||||
v-if="displayAsCard(item.type)"
|
||||
: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">
|
||||
<h4 class="text-bold">{{ subItem.title }}</h4>
|
||||
<media-link :blank="subItem.openWindow" :to="subItem.link" class="link">{{
|
||||
subItem.linkText
|
||||
}}</media-link>
|
||||
<media-link
|
||||
:blank="subItem.openWindow"
|
||||
:to="subItem.link"
|
||||
class="link"
|
||||
>{{ subItem.linkText }}</media-link
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,69 +1,69 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useMediaCenterStore } from '@/stores/mediaCenter'
|
||||
import { useMediaCenterStore } from "@/stores/mediaCenter";
|
||||
import * as log from "loglevel";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
log.debug('HandlungsfelderOverview created')
|
||||
log.debug("HandlungsfelderOverview created");
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: 'Fahrzeug',
|
||||
icon: 'icon-hf-fahrzeug',
|
||||
name: "Fahrzeug",
|
||||
icon: "icon-hf-fahrzeug",
|
||||
},
|
||||
{
|
||||
name: 'Reisen',
|
||||
icon: 'icon-hf-reisen',
|
||||
name: "Reisen",
|
||||
icon: "icon-hf-reisen",
|
||||
},
|
||||
{
|
||||
name: 'Einkommenssicherung',
|
||||
icon: 'icon-hf-einkommenssicherung',
|
||||
name: "Einkommenssicherung",
|
||||
icon: "icon-hf-einkommenssicherung",
|
||||
},
|
||||
{
|
||||
name: 'Gesundheit',
|
||||
icon: 'icon-hf-fahrzeug',
|
||||
name: "Gesundheit",
|
||||
icon: "icon-hf-fahrzeug",
|
||||
},
|
||||
{
|
||||
name: 'Haushalt',
|
||||
icon: 'icon-hf-reisen',
|
||||
name: "Haushalt",
|
||||
icon: "icon-hf-reisen",
|
||||
},
|
||||
{
|
||||
name: 'Sparen',
|
||||
icon: 'icon-hf-einkommenssicherung',
|
||||
name: "Sparen",
|
||||
icon: "icon-hf-einkommenssicherung",
|
||||
},
|
||||
{
|
||||
name: 'Pensionierung',
|
||||
icon: 'icon-hf-fahrzeug',
|
||||
name: "Pensionierung",
|
||||
icon: "icon-hf-fahrzeug",
|
||||
},
|
||||
{
|
||||
name: 'KMU',
|
||||
icon: 'icon-hf-reisen',
|
||||
name: "KMU",
|
||||
icon: "icon-hf-reisen",
|
||||
},
|
||||
{
|
||||
name: 'Wohneigentum',
|
||||
icon: 'icon-hf-einkommenssicherung',
|
||||
name: "Wohneigentum",
|
||||
icon: "icon-hf-einkommenssicherung",
|
||||
},
|
||||
{
|
||||
name: 'Rechtsstreitigkeiten',
|
||||
icon: 'icon-hf-fahrzeug',
|
||||
name: "Rechtsstreitigkeiten",
|
||||
icon: "icon-hf-fahrzeug",
|
||||
},
|
||||
{
|
||||
name: 'Erben / Vererben',
|
||||
icon: 'icon-hf-reisen',
|
||||
name: "Erben / Vererben",
|
||||
icon: "icon-hf-reisen",
|
||||
},
|
||||
{
|
||||
name: 'Selbstständigkeit',
|
||||
icon: 'icon-hf-einkommenssicherung',
|
||||
name: "Selbstständigkeit",
|
||||
icon: "icon-hf-einkommenssicherung",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const mediaStore = useMediaCenterStore()
|
||||
const dropdownSelected = ref(mediaStore.selectedLearningPath)
|
||||
const mediaStore = useMediaCenterStore();
|
||||
const dropdownSelected = ref(mediaStore.selectedLearningPath);
|
||||
|
||||
watch(dropdownSelected, (newValue) =>
|
||||
mediaStore.$patch({
|
||||
selectedLearningPath: newValue,
|
||||
})
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -74,8 +74,14 @@ watch(dropdownSelected, (newValue) =>
|
|||
</div>
|
||||
<div v-if="mediaStore.mediaCenterPage">
|
||||
<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">
|
||||
<router-link :to="`/mediacenter/${mediaStore.mediaCenterPage.slug}/handlungsfelder/${cat.slug}`">
|
||||
<li
|
||||
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`" />
|
||||
<h3 class="text-base text-center">{{ cat.title }}</h3>
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import OverviewCard from '@/components/mediaCenter/OverviewCard.vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useMediaCenterStore } from '@/stores/mediaCenter'
|
||||
import OverviewCard from "@/components/mediaCenter/OverviewCard.vue";
|
||||
import { useMediaCenterStore } from "@/stores/mediaCenter";
|
||||
import * as log from "loglevel";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
log.debug('MediaMainView created')
|
||||
log.debug("MediaMainView created");
|
||||
|
||||
const mediaStore = useMediaCenterStore()
|
||||
const dropdownSelected = ref(mediaStore.selectedLearningPath)
|
||||
const mediaStore = useMediaCenterStore();
|
||||
const dropdownSelected = ref(mediaStore.selectedLearningPath);
|
||||
|
||||
watch(dropdownSelected, (newValue) =>
|
||||
mediaStore.$patch({
|
||||
selectedLearningPath: newValue,
|
||||
})
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
import { onMounted } from 'vue'
|
||||
import { useMediaCenterStore } from '@/stores/mediaCenter'
|
||||
import { useMediaCenterStore } from "@/stores/mediaCenter";
|
||||
import * as log from "loglevel";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
log.debug('MediaCenterView created')
|
||||
log.debug("MediaCenterView created");
|
||||
|
||||
const props = defineProps<{
|
||||
mediaCenterPageSlug: string
|
||||
}>()
|
||||
mediaCenterPageSlug: string;
|
||||
}>();
|
||||
|
||||
const mediaCenterStore = useMediaCenterStore()
|
||||
const mediaCenterStore = useMediaCenterStore();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug('MediaCenterView mounted', props.mediaCenterPageSlug)
|
||||
log.debug("MediaCenterView mounted", props.mediaCenterPageSlug);
|
||||
|
||||
try {
|
||||
await mediaCenterStore.loadMediaCenterPage(props.mediaCenterPageSlug)
|
||||
await mediaCenterStore.loadMediaCenterPage(props.mediaCenterPageSlug);
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
log.error(error);
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -30,7 +30,9 @@ onMounted(async () => {
|
|||
<li class="ml-10">Handlungsfelder</li>
|
||||
<li class="ml-10">Allgemeines zu Versicherungen</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>
|
||||
</nav>
|
||||
<main class="px-8 py-8">
|
||||
|
|
|
|||
|
|
@ -1,52 +1,52 @@
|
|||
<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 = {
|
||||
title: 'Fahrzeug: Lernmedien',
|
||||
title: "Fahrzeug: Lernmedien",
|
||||
items: [
|
||||
{
|
||||
title: 'Die Motorfahrzeughaftpflicht',
|
||||
description: 'Buch «Sach- und Vermögensversicherungen» – Kapitel 16',
|
||||
iconUrl: '/static/icons/demo/icon-hf-book.png',
|
||||
linkText: 'PDF anzeigen',
|
||||
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
|
||||
title: "Die Motorfahrzeughaftpflicht",
|
||||
description: "Buch «Sach- und Vermögensversicherungen» – Kapitel 16",
|
||||
iconUrl: "/static/icons/demo/icon-hf-book.png",
|
||||
linkText: "PDF anzeigen",
|
||||
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
|
||||
openWindow: true,
|
||||
},
|
||||
{
|
||||
title: 'Die Motorfahrzeughaftpflicht',
|
||||
iconUrl: '/static/icons/demo/icon-hf-book.png',
|
||||
description: 'Buch «Sach- und Vermögensversicherungen» – Kapitel 16',
|
||||
linkText: 'PDF anzeigen',
|
||||
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
|
||||
title: "Die Motorfahrzeughaftpflicht",
|
||||
iconUrl: "/static/icons/demo/icon-hf-book.png",
|
||||
description: "Buch «Sach- und Vermögensversicherungen» – Kapitel 16",
|
||||
linkText: "PDF anzeigen",
|
||||
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
|
||||
openWindow: true,
|
||||
},
|
||||
{
|
||||
title: 'Die Motorfahrzeughaftpflicht',
|
||||
iconUrl: '/static/icons/demo/icon-hf-book.png',
|
||||
description: 'Buch «Sach- und Vermögensversicherungen» – Kapitel 16',
|
||||
linkText: 'PDF anzeigen',
|
||||
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
|
||||
title: "Die Motorfahrzeughaftpflicht",
|
||||
iconUrl: "/static/icons/demo/icon-hf-book.png",
|
||||
description: "Buch «Sach- und Vermögensversicherungen» – Kapitel 16",
|
||||
linkText: "PDF anzeigen",
|
||||
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
|
||||
openWindow: true,
|
||||
},
|
||||
{
|
||||
title: 'Die Motorfahrzeughaftpflicht',
|
||||
iconUrl: '/static/icons/demo/icon-hf-book.png',
|
||||
description: 'Buch «Sach- und Vermögensversicherungen» – Kapitel 16',
|
||||
linkText: 'PDF anzeigen',
|
||||
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
|
||||
title: "Die Motorfahrzeughaftpflicht",
|
||||
iconUrl: "/static/icons/demo/icon-hf-book.png",
|
||||
description: "Buch «Sach- und Vermögensversicherungen» – Kapitel 16",
|
||||
linkText: "PDF anzeigen",
|
||||
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
|
||||
openWindow: true,
|
||||
},
|
||||
{
|
||||
title: 'Die Motorfahrzeughaftpflicht',
|
||||
iconUrl: '/static/icons/demo/icon-hf-book.png',
|
||||
description: 'Buch «Sach- und Vermögensversicherungen» – Kapitel 16',
|
||||
linkText: 'PDF anzeigen',
|
||||
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
|
||||
title: "Die Motorfahrzeughaftpflicht",
|
||||
iconUrl: "/static/icons/demo/icon-hf-book.png",
|
||||
description: "Buch «Sach- und Vermögensversicherungen» – Kapitel 16",
|
||||
linkText: "PDF anzeigen",
|
||||
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
|
||||
openWindow: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -58,7 +58,11 @@ const data = {
|
|||
<template #body>
|
||||
<section class="mb-20">
|
||||
<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 v-if="item.iconUrl">
|
||||
<img class="mr-6 max-h-[70px]" :src="item.iconUrl" />
|
||||
|
|
@ -69,7 +73,9 @@ const data = {
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel';
|
||||
|
||||
log.debug('MessagesView created');
|
||||
import * as log from "loglevel";
|
||||
|
||||
log.debug("MessagesView created");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -11,5 +10,4 @@ log.debug('MessagesView created');
|
|||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel';
|
||||
|
||||
log.debug('ProfileView created');
|
||||
import * as log from "loglevel";
|
||||
|
||||
log.debug("ProfileView created");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -11,5 +10,4 @@ log.debug('ProfileView created');
|
|||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel';
|
||||
|
||||
log.debug('ShopView created');
|
||||
import * as log from "loglevel";
|
||||
|
||||
log.debug("ShopView created");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -11,5 +10,4 @@ log.debug('ShopView created');
|
|||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,40 +1,40 @@
|
|||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import ItCheckbox from '@/components/ui/ItCheckbox.vue'
|
||||
import ItDropdown from '@/components/ui/ItDropdown.vue'
|
||||
import IconLogout from '@/components/icons/IconLogout.vue'
|
||||
import IconSettings from '@/components/icons/IconSettings.vue'
|
||||
import ItDropdownSelect from '@/components/ui/ItDropdownSelect.vue'
|
||||
import IconLogout from "@/components/icons/IconLogout.vue";
|
||||
import IconSettings from "@/components/icons/IconSettings.vue";
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import ItDropdown from "@/components/ui/ItDropdown.vue";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import { reactive } from "vue";
|
||||
|
||||
const state = reactive({
|
||||
checkboxValue: true,
|
||||
dropdownValues: [
|
||||
{ id: 1, name: 'Wade Cooper' },
|
||||
{ id: 2, name: 'Arlene Mccoy' },
|
||||
{ id: 3, name: 'Devon Webb' },
|
||||
{ id: 4, name: 'Tom Cook' },
|
||||
{ id: 5, name: 'Tanya Fox' },
|
||||
{ id: 6, name: 'Hellen Schmidt' },
|
||||
{ id: 7, name: 'Caroline Schultz' },
|
||||
{ id: 8, name: 'Mason Heaney' },
|
||||
{ id: 9, name: 'Claudie Smitham' },
|
||||
{ id: 10, name: 'Emil Schaefer' },
|
||||
{ id: 1, name: "Wade Cooper" },
|
||||
{ id: 2, name: "Arlene Mccoy" },
|
||||
{ id: 3, name: "Devon Webb" },
|
||||
{ id: 4, name: "Tom Cook" },
|
||||
{ id: 5, name: "Tanya Fox" },
|
||||
{ id: 6, name: "Hellen Schmidt" },
|
||||
{ id: 7, name: "Caroline Schultz" },
|
||||
{ id: 8, name: "Mason Heaney" },
|
||||
{ id: 9, name: "Claudie Smitham" },
|
||||
{ id: 10, name: "Emil Schaefer" },
|
||||
],
|
||||
dropdownSelected: {
|
||||
id: -1,
|
||||
name: 'Select a name',
|
||||
name: "Select a name",
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const dropdownData = [
|
||||
[
|
||||
{
|
||||
title: 'Option 1',
|
||||
title: "Option 1",
|
||||
icon: IconLogout,
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
title: 'Option 2',
|
||||
title: "Option 2",
|
||||
icon: null,
|
||||
data: {
|
||||
test: 12,
|
||||
|
|
@ -43,27 +43,37 @@ const dropdownData = [
|
|||
],
|
||||
[
|
||||
{
|
||||
title: 'Option 3',
|
||||
title: "Option 3",
|
||||
icon: IconSettings,
|
||||
data: {
|
||||
amount: 34,
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
// 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
|
||||
// noch nirgendwo verwendet werden.
|
||||
const colors = ['blue', 'sky', 'green', 'red', 'orange', 'yellow', 'stone', 'gray', 'slate']
|
||||
const colorValues = [200, 300, 400, 500, 600, 700, 800, 900]
|
||||
const colors = [
|
||||
"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) {
|
||||
return `bg-${color}-${value}`
|
||||
return `bg-${color}-${value}`;
|
||||
}
|
||||
|
||||
function log(data: any) {
|
||||
console.log(data)
|
||||
console.log(data);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -76,8 +86,8 @@ function log(data: any) {
|
|||
</div>
|
||||
|
||||
<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
|
||||
<it-icon-message/>
|
||||
The icons are defined as Web Components, so they can also be used in the backend.
|
||||
Use them like <it-icon-message/>
|
||||
</p>
|
||||
|
||||
<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">
|
||||
<tr class="h-12 md:h-18 lg:h-24">
|
||||
<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 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 v-for="value in colorValues" 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>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -313,8 +328,13 @@ function log(data: any) {
|
|||
<button disabled class="btn-blue">Blue disabled</button>
|
||||
</div>
|
||||
|
||||
<div 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">
|
||||
<div
|
||||
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>
|
||||
</button>
|
||||
|
||||
|
|
@ -336,19 +356,26 @@ function log(data: any) {
|
|||
|
||||
<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 }}
|
||||
|
||||
<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>
|
||||
|
||||
<h2 class="mt-8 mb-8">Dropdown</h2>
|
||||
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,40 +1,40 @@
|
|||
const colors = require('./src/colors.json');
|
||||
const colors = require("./src/colors.json");
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
'./index.html',
|
||||
'./src/**/*.{vue,js,ts,jsx,tsx}',
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
// TODO: wenn man den server-pfad auch angibt wird Tailwind langsamer?! (Startzeit erhöht sich stark...)
|
||||
// '../server/vbv_lernwelt/**/*.{html,js,py}',
|
||||
],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ['Buenos Aires', 'sans-serif'],
|
||||
sans: ["Buenos Aires", "sans-serif"],
|
||||
},
|
||||
extend: {
|
||||
spacing: {
|
||||
'128': '32rem',
|
||||
128: "32rem",
|
||||
},
|
||||
maxWidth: {
|
||||
'8xl': '88rem',
|
||||
'9xl': '96rem',
|
||||
"8xl": "88rem",
|
||||
"9xl": "96rem",
|
||||
},
|
||||
backgroundImage: {
|
||||
'handlungsfelder-overview': "url('/static/icons/icon-handlungsfelder-overview.svg')",
|
||||
'lernmedien-overview': "url('/static/icons/icon-lernmedien-overview.svg')",
|
||||
}
|
||||
"handlungsfelder-overview":
|
||||
"url('/static/icons/icon-handlungsfelder-overview.svg')",
|
||||
"lernmedien-overview": "url('/static/icons/icon-lernmedien-overview.svg')",
|
||||
},
|
||||
},
|
||||
colors: colors,
|
||||
},
|
||||
safelist: [
|
||||
{ pattern: /bg-(blue|sky|green|red|orange|yellow|stone|gray|slate)-(200|300|400|500|600|700|800|900)/, },
|
||||
'it-icon',
|
||||
'bg-handlungsfelder-overview',
|
||||
'bg-lernmedien-overview',
|
||||
|
||||
{
|
||||
pattern:
|
||||
/bg-(blue|sky|green|red|orange|yellow|stone|gray|slate)-(200|300|400|500|600|700|800|900)/,
|
||||
},
|
||||
"it-icon",
|
||||
"bg-handlungsfelder-overview",
|
||||
"bg-lernmedien-overview",
|
||||
],
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/forms'),
|
||||
],
|
||||
}
|
||||
plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
@apply text-black
|
||||
@apply text-black;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
@ -11,94 +11,93 @@ body {
|
|||
}
|
||||
|
||||
svg {
|
||||
@apply fill-current
|
||||
@apply fill-current;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
.it-icon {
|
||||
@apply w-8 h-8 inline-block
|
||||
@apply w-8 h-8 inline-block;
|
||||
}
|
||||
|
||||
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 {
|
||||
@apply text-4xl md:text-5xl xl:text-7xl font-bold
|
||||
@apply text-4xl md:text-5xl xl:text-7xl font-bold;
|
||||
}
|
||||
|
||||
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 {
|
||||
@apply text-2xl md:text-3xl xl:text-4xl font-bold
|
||||
@apply text-2xl md:text-3xl xl:text-4xl font-bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-xl xl:text-2xl font-bold
|
||||
@apply text-xl xl:text-2xl font-bold;
|
||||
}
|
||||
|
||||
.heading-3 {
|
||||
@apply text-xl xl:text-2xl font-bold
|
||||
@apply text-xl xl:text-2xl font-bold;
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply underline underline-offset-2
|
||||
@apply underline underline-offset-2;
|
||||
}
|
||||
|
||||
.link-large {
|
||||
@apply text-lg underline xl:text-xl
|
||||
@apply text-lg underline xl:text-xl;
|
||||
}
|
||||
|
||||
.text-large {
|
||||
@apply text-lg xl:text-xl
|
||||
@apply text-lg xl:text-xl;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
@apply text-base font-bold
|
||||
@apply text-base font-bold;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@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 {
|
||||
.circle-title {
|
||||
@apply text-9xl font-bold
|
||||
@apply text-9xl font-bold;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply font-semibold py-2 px-4 align-middle inline-block
|
||||
bg-blue-900 text-white border-2 border-blue-900
|
||||
hover:bg-blue-700 hover:border-blue-700
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply font-semibold py-2 px-4 align-middle inline-block
|
||||
bg-white text-blue-900 border-2 border-blue-900
|
||||
hover:bg-gray-200
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.btn-blue {
|
||||
@apply font-semibold py-2 px-4 align-middle inline-block
|
||||
bg-sky-500 text-blue-900 border-2 border-sky-500
|
||||
hover:bg-sky-400 hover:border-sky-400
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
@apply font-semibold py-2 px-4 align-middle inline-block
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,6 @@
|
|||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": [],
|
||||
"types": [
|
||||
"node",
|
||||
"jsdom",
|
||||
"vitest/globals"
|
||||
]
|
||||
"types": ["node", "jsdom", "vitest/globals"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
const replace = require("replace-in-file");
|
||||
const gitHash = require('child_process')
|
||||
const gitHash = require("child_process")
|
||||
.execSync("git rev-parse --short HEAD")
|
||||
.toString().trim()
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
const options = {
|
||||
files: "dist/static/vue/*.js",
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import { fileURLToPath, URL } from "url";
|
||||
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
// import vueI18n from '@intlify/vite-plugin-vue-i18n'
|
||||
import alias from "@rollup/plugin-alias";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }
|
||||
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
|
||||
return {
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
// 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: {
|
||||
port: 5173,
|
||||
hmr: { port: 5173 }
|
||||
hmr: { port: 5173 },
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
assetsDir: 'static/vue',
|
||||
assetsDir: "static/vue",
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
environment: "jsdom",
|
||||
},
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 .
|
||||
|
|
@ -5,7 +5,8 @@
|
|||
"build": "npm install --prefix client && npm run build --prefix client && npm run build:tailwind --prefix client",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:ci": "cypress run"
|
||||
"cypress:ci": "cypress run",
|
||||
"prettier": "npm run prettier --prefix client"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^10.6.0"
|
||||
|
|
|
|||
|
|
@ -78,26 +78,24 @@ THIRD_PARTY_APPS = [
|
|||
"rest_framework.authtoken",
|
||||
"corsheaders",
|
||||
"drf_spectacular",
|
||||
|
||||
'wagtail.contrib.forms',
|
||||
'wagtail.contrib.redirects',
|
||||
'wagtail.contrib.styleguide',
|
||||
'wagtail.embeds',
|
||||
'wagtail.sites',
|
||||
'wagtail.users',
|
||||
'wagtail.snippets',
|
||||
'wagtail.documents',
|
||||
'wagtail.images',
|
||||
'wagtail.search',
|
||||
'wagtail.admin',
|
||||
'wagtail',
|
||||
"wagtail.contrib.forms",
|
||||
"wagtail.contrib.redirects",
|
||||
"wagtail.contrib.styleguide",
|
||||
"wagtail.embeds",
|
||||
"wagtail.sites",
|
||||
"wagtail.users",
|
||||
"wagtail.snippets",
|
||||
"wagtail.documents",
|
||||
"wagtail.images",
|
||||
"wagtail.search",
|
||||
"wagtail.admin",
|
||||
"wagtail",
|
||||
# 'wagtail.locales',
|
||||
"wagtail_localize",
|
||||
"wagtail_localize.locales",
|
||||
'wagtail.api.v2',
|
||||
|
||||
'modelcluster',
|
||||
'taggit',
|
||||
"wagtail.api.v2",
|
||||
"modelcluster",
|
||||
"taggit",
|
||||
]
|
||||
|
||||
LOCAL_APPS = [
|
||||
|
|
@ -199,32 +197,32 @@ MEDIA_ROOT = str(APPS_DIR / "media")
|
|||
MEDIA_URL = "/media/"
|
||||
|
||||
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_SITE_NAME = 'VBV Lernwelt'
|
||||
WAGTAIL_SITE_NAME = "VBV Lernwelt"
|
||||
WAGTAIL_I18N_ENABLED = True
|
||||
|
||||
LANGUAGES = [
|
||||
('en-US', "English (American)"),
|
||||
('fr-CH', "Swiss French"),
|
||||
('de-CH', "Swiss German"),
|
||||
('it-CH', "Swiss Italian")
|
||||
("en-US", "English (American)"),
|
||||
("fr-CH", "Swiss French"),
|
||||
("de-CH", "Swiss German"),
|
||||
("it-CH", "Swiss Italian"),
|
||||
]
|
||||
|
||||
WAGTAILDOCS_DOCUMENT_MODEL = 'media_library.LibraryDocument'
|
||||
WAGTAILDOCS_DOCUMENT_MODEL = "media_library.LibraryDocument"
|
||||
|
||||
|
||||
WAGTAIL_CONTENT_LANGUAGES = [
|
||||
('fr-CH', "Swiss French"),
|
||||
('de-CH', "Swiss German"),
|
||||
('it-CH', "Swiss Italian")
|
||||
("fr-CH", "Swiss French"),
|
||||
("de-CH", "Swiss German"),
|
||||
("it-CH", "Swiss Italian"),
|
||||
]
|
||||
|
||||
WAGTAILSEARCH_BACKENDS = {
|
||||
'default': {
|
||||
'BACKEND': 'wagtail.search.backends.database',
|
||||
"default": {
|
||||
"BACKEND": "wagtail.search.backends.database",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -456,13 +454,13 @@ CORS_URLS_REGEX = r"^/api/.*$"
|
|||
CSP_DEFAULT_SRC = [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
'ws://localhost:5173',
|
||||
'ws://127.0.0.1:5173',
|
||||
'localhost:8000',
|
||||
'localhost:8001',
|
||||
'blob:',
|
||||
'data:',
|
||||
'http://*'
|
||||
"ws://localhost:5173",
|
||||
"ws://127.0.0.1:5173",
|
||||
"localhost:8000",
|
||||
"localhost:8001",
|
||||
"blob:",
|
||||
"data:",
|
||||
"http://*",
|
||||
]
|
||||
CSP_FRAME_ANCESTORS = ("'self'",)
|
||||
|
||||
|
|
@ -498,7 +496,10 @@ ALLOWED_HOSTS = env.list(
|
|||
# CACHES
|
||||
CACHES = {
|
||||
"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"),
|
||||
},
|
||||
}
|
||||
|
|
@ -524,9 +525,7 @@ CACHES["api_page_cache"] = {
|
|||
IT_OAUTH_TENANT_ID = env.str("IT_OAUTH_TENANT_ID", default=None)
|
||||
|
||||
if IT_OAUTH_TENANT_ID:
|
||||
IT_OAUTH_AUTHORIZE_PARAMS = {
|
||||
'tenant_id': IT_OAUTH_TENANT_ID
|
||||
}
|
||||
IT_OAUTH_AUTHORIZE_PARAMS = {"tenant_id": IT_OAUTH_TENANT_ID}
|
||||
else:
|
||||
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_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/"),
|
||||
"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"),
|
||||
"api_base_url": env(
|
||||
"IT_OAUTH_API_BASE_URL",
|
||||
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": {
|
||||
'scope': env("IT_OAUTH_SCOPE", default=''),
|
||||
'token_endpoint_auth_method': 'client_secret_post',
|
||||
'token_placement': 'body',
|
||||
}
|
||||
"scope": env("IT_OAUTH_SCOPE", default=""),
|
||||
"token_endpoint_auth_method": "client_secret_post",
|
||||
"token_placement": "body",
|
||||
},
|
||||
}
|
||||
|
||||
if APP_ENVIRONMENT == "development":
|
||||
|
|
@ -555,7 +562,7 @@ if APP_ENVIRONMENT == "development":
|
|||
# django-debug-toolbar
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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
|
||||
# MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405
|
||||
# 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
|
||||
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
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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
|
||||
# ------------------------------------------------------------------------------
|
||||
# Django Admin URL regex.
|
||||
ADMIN_URL = env("IT_DJANGO_ADMIN_URL", 'admin/')
|
||||
ADMIN_URL = env("IT_DJANGO_ADMIN_URL", "admin/")
|
||||
|
||||
# Anymail
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
|
||||
import os
|
||||
|
||||
os.environ['IT_APP_ENVIRONMENT'] = 'development'
|
||||
os.environ["IT_APP_ENVIRONMENT"] = "development"
|
||||
|
||||
from .base import * # noqa
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
|
||||
import os
|
||||
|
||||
os.environ['IT_APP_ENVIRONMENT'] = 'development'
|
||||
os.environ["IT_APP_ENVIRONMENT"] = "development"
|
||||
|
||||
from .base import * # noqa
|
||||
|
||||
# GENERAL
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
|
||||
DATABASES['default']['NAME'] = 'vbv_lernwelt_cypress'
|
||||
DATABASES["default"]["NAME"] = "vbv_lernwelt_cypress"
|
||||
|
||||
# EMAIL
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -6,16 +6,27 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
|||
from django.urls import include, path, re_path
|
||||
from django.views import defaults as default_views
|
||||
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.views import (
|
||||
rate_limit_exceeded_view,
|
||||
check_rate_limit,
|
||||
cypress_reset_view,
|
||||
generate_web_component_icons,
|
||||
me_user_view,
|
||||
permission_denied_view,
|
||||
check_rate_limit, cypress_reset_view, vue_home, vue_login, me_user_view, vue_logout, generate_web_component_icons, )
|
||||
from vbv_lernwelt.course.views import page_api_view, request_course_completion, mark_course_completion
|
||||
rate_limit_exceeded_view,
|
||||
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):
|
||||
|
|
@ -106,4 +117,4 @@ if settings.DEBUG:
|
|||
|
||||
|
||||
# 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")]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from django.test import TestCase, override_settings
|
||||
from django.test import override_settings, TestCase
|
||||
|
||||
|
||||
class RateLimitTest(TestCase):
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-
|
|||
flake8 # https://github.com/PyCQA/flake8
|
||||
flake8-isort # https://github.com/gforcada/flake8-isort
|
||||
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
|
||||
pre-commit # https://github.com/pre-commit/pre-commit
|
||||
ufmt
|
||||
|
||||
# Django
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -28,14 +28,17 @@ attrs==21.4.0
|
|||
# via
|
||||
# jsonschema
|
||||
# pytest
|
||||
# usort
|
||||
authlib==1.0.0
|
||||
# via -r requirements.in
|
||||
backcall==0.2.0
|
||||
# via ipython
|
||||
beautifulsoup4==4.9.3
|
||||
# via wagtail
|
||||
black==22.3.0
|
||||
# via -r requirements-dev.in
|
||||
black==22.8.0
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
# ufmt
|
||||
certifi==2021.10.8
|
||||
# via
|
||||
# requests
|
||||
|
|
@ -52,7 +55,10 @@ click==8.1.1
|
|||
# via
|
||||
# black
|
||||
# django-click
|
||||
# moreorless
|
||||
# pip-tools
|
||||
# ufmt
|
||||
# usort
|
||||
# uvicorn
|
||||
concurrent-log-handler==0.9.20
|
||||
# via -r requirements.in
|
||||
|
|
@ -86,7 +92,6 @@ django==3.2.13
|
|||
# django-debug-toolbar
|
||||
# django-extensions
|
||||
# django-filter
|
||||
# django-htmx
|
||||
# django-model-utils
|
||||
# django-modelcluster
|
||||
# django-permissionedforms
|
||||
|
|
@ -212,8 +217,14 @@ l18n==2021.3
|
|||
# via wagtail
|
||||
lazy-object-proxy==1.7.1
|
||||
# via astroid
|
||||
libcst==0.4.7
|
||||
# via
|
||||
# ufmt
|
||||
# usort
|
||||
markupsafe==2.1.1
|
||||
# via jinja2
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
marshmallow==3.15.0
|
||||
# via environs
|
||||
matplotlib-inline==0.1.3
|
||||
|
|
@ -222,6 +233,10 @@ mccabe==0.6.1
|
|||
# via
|
||||
# flake8
|
||||
# pylint
|
||||
moreorless==0.4.0
|
||||
# via
|
||||
# ufmt
|
||||
# usort
|
||||
mypy==0.942
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
|
|
@ -231,6 +246,7 @@ mypy-extensions==0.4.3
|
|||
# via
|
||||
# black
|
||||
# mypy
|
||||
# typing-inspect
|
||||
nodeenv==1.6.0
|
||||
# via pre-commit
|
||||
openpyxl==3.0.9
|
||||
|
|
@ -244,7 +260,9 @@ packaging==21.3
|
|||
parso==0.8.3
|
||||
# via jedi
|
||||
pathspec==0.9.0
|
||||
# via black
|
||||
# via
|
||||
# black
|
||||
# trailrunner
|
||||
pep517==0.12.0
|
||||
# via pip-tools
|
||||
pexpect==4.8.0
|
||||
|
|
@ -329,6 +347,7 @@ pytz==2022.1
|
|||
pyyaml==6.0
|
||||
# via
|
||||
# drf-spectacular
|
||||
# libcst
|
||||
# pre-commit
|
||||
# uvicorn
|
||||
redis==4.2.1
|
||||
|
|
@ -362,6 +381,8 @@ sqlparse==0.4.2
|
|||
# django-debug-toolbar
|
||||
stack-data==0.2.0
|
||||
# via ipython
|
||||
stdlibs==2022.6.8
|
||||
# via usort
|
||||
structlog==21.5.0
|
||||
# via -r requirements.in
|
||||
tablib[xls,xlsx]==3.2.1
|
||||
|
|
@ -378,6 +399,7 @@ toml==0.10.2
|
|||
# via
|
||||
# ipdb
|
||||
# pre-commit
|
||||
# usort
|
||||
tomli==2.0.1
|
||||
# via
|
||||
# black
|
||||
|
|
@ -386,6 +408,12 @@ tomli==2.0.1
|
|||
# pep517
|
||||
# pylint
|
||||
# pytest
|
||||
tomlkit==0.11.5
|
||||
# via ufmt
|
||||
trailrunner==1.2.1
|
||||
# via
|
||||
# ufmt
|
||||
# usort
|
||||
traitlets==5.1.1
|
||||
# via
|
||||
# ipython
|
||||
|
|
@ -403,8 +431,15 @@ typing-extensions==4.1.1
|
|||
# django-stubs
|
||||
# django-stubs-ext
|
||||
# djangorestframework-stubs
|
||||
# libcst
|
||||
# mypy
|
||||
# typing-inspect
|
||||
# ufmt
|
||||
# wagtail-localize
|
||||
typing-inspect==0.8.0
|
||||
# via libcst
|
||||
ufmt==2.0.1
|
||||
# via -r requirements-dev.in
|
||||
uritemplate==4.1.1
|
||||
# via
|
||||
# coreapi
|
||||
|
|
@ -413,6 +448,8 @@ urllib3==1.26.9
|
|||
# via
|
||||
# requests
|
||||
# sentry-sdk
|
||||
usort==1.0.5
|
||||
# via ufmt
|
||||
uvicorn[standard]==0.17.6
|
||||
# via -r requirements.in
|
||||
uvloop==0.16.0
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class MediaLibraryConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'vbv_lernwelt.competence'
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "vbv_lernwelt.competence"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
from vbv_lernwelt.competence.factories import CompetenceProfilePageFactory, PerformanceCriteriaFactory, \
|
||||
CompetencePageFactory
|
||||
from vbv_lernwelt.competence.factories import (
|
||||
CompetencePageFactory,
|
||||
CompetenceProfilePageFactory,
|
||||
PerformanceCriteriaFactory,
|
||||
)
|
||||
from vbv_lernwelt.competence.models import CompetencePage
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -11,135 +14,143 @@ def create_default_competence_profile():
|
|||
course_page = CoursePage.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
|
||||
|
||||
competence_profile_page = CompetenceProfilePageFactory(
|
||||
title='Kompetenzprofil',
|
||||
title="Kompetenzprofil",
|
||||
parent=course_page,
|
||||
)
|
||||
|
||||
competences = [{
|
||||
'competence_id': 'A1',
|
||||
'title': 'Weiterempfehlung für Neukunden generieren',
|
||||
'items': [
|
||||
'Verhandlungsgeschick',
|
||||
'Überzeugtes Auftreten',
|
||||
],
|
||||
}, {
|
||||
'competence_id': 'A2',
|
||||
'title': 'Kundengespräche vereinbaren',
|
||||
'items': [
|
||||
'Gesprächsführung / Fragetechniken',
|
||||
'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',
|
||||
'Datenschutzgesetz',
|
||||
'Kommunikation in den sozialen Medien',
|
||||
]
|
||||
}, {
|
||||
'competence_id': 'A4',
|
||||
'title': 'Kundendaten erfassen',
|
||||
'items': []
|
||||
}, {
|
||||
'competence_id': 'B1',
|
||||
'title': 'Wünsche, Ziele und Bedürfnisse der Kunden im Gespräch ermitteln',
|
||||
'items': [
|
||||
'Gesprächsführung',
|
||||
'Fragetechniken',
|
||||
'Kundenpsychologie',
|
||||
]
|
||||
}, {
|
||||
'competence_id': 'B2',
|
||||
'title': 'Analyse des Kundenbedarfs und des Kundenbedürfnisses durchführen',
|
||||
'items': [
|
||||
'Fragetechniken',
|
||||
'Visuelle Hilfsmittel / Visualisierungstechniken',
|
||||
]
|
||||
}, {
|
||||
'competence_id': 'B3',
|
||||
'title': 'Individuelle Lösungsvorschläge erarbeiten',
|
||||
'items': [
|
||||
'Fundierte Produktekenntnisse',
|
||||
'Regulatorische Vorschriften',
|
||||
]
|
||||
}, {
|
||||
'competence_id': 'B4',
|
||||
'title': 'Lösungsvorschläge präsentieren und umsetzen',
|
||||
'items': [
|
||||
'Verhandlungsstrategien',
|
||||
'Fundierte Produktkenntnisse',
|
||||
'Visuelle Hilfsmittel / Visualisierungstechniken',
|
||||
]
|
||||
}, {
|
||||
'competence_id': 'C1',
|
||||
'title': 'Cross- und Upselling; bestehende fremdverwaltete Versicherungspolicen prüfen und in das Portfolio aufnehmen',
|
||||
'items': [
|
||||
'Produktkenntnisse',
|
||||
'Gesprächsführung',
|
||||
'Kommunikation',
|
||||
'Fragetechnik',
|
||||
'Verhandlungsgeschick',
|
||||
'Vertragsrecht',
|
||||
'Regulatorische Vorgaben',
|
||||
'UVG, BVG, KVG, VVG',
|
||||
]
|
||||
}, {
|
||||
'competence_id': 'C2',
|
||||
'title': 'Änderungswünsche entgegennehmen und bestehende Verträge anpassen',
|
||||
'items': [
|
||||
'Produktkenntnisse',
|
||||
'Gesprächsführung',
|
||||
'Kommunikation',
|
||||
'Fragetechnik',
|
||||
'Verhandlungsgeschick',
|
||||
'Vertragsrecht',
|
||||
'Regulatorische Vorgaben',
|
||||
'UVG, BVG, KVG, VVG',
|
||||
]
|
||||
}, {
|
||||
'competence_id': 'C3',
|
||||
'title': 'Kunden im Schadenfall unterstützen',
|
||||
'items': []
|
||||
}, {
|
||||
'competence_id': 'C4',
|
||||
'title': 'Bestehende Kunden pflegen',
|
||||
'items': []
|
||||
}, {
|
||||
'competence_id': 'C5',
|
||||
'title': 'Versicherungsanträge nachbearbeiten',
|
||||
'items': []
|
||||
}]
|
||||
competences = [
|
||||
{
|
||||
"competence_id": "A1",
|
||||
"title": "Weiterempfehlung für Neukunden generieren",
|
||||
"items": [
|
||||
"Verhandlungsgeschick",
|
||||
"Überzeugtes Auftreten",
|
||||
],
|
||||
},
|
||||
{
|
||||
"competence_id": "A2",
|
||||
"title": "Kundengespräche vereinbaren",
|
||||
"items": [
|
||||
"Gesprächsführung / Fragetechniken",
|
||||
"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",
|
||||
"Datenschutzgesetz",
|
||||
"Kommunikation in den sozialen Medien",
|
||||
],
|
||||
},
|
||||
{"competence_id": "A4", "title": "Kundendaten erfassen", "items": []},
|
||||
{
|
||||
"competence_id": "B1",
|
||||
"title": "Wünsche, Ziele und Bedürfnisse der Kunden im Gespräch ermitteln",
|
||||
"items": [
|
||||
"Gesprächsführung",
|
||||
"Fragetechniken",
|
||||
"Kundenpsychologie",
|
||||
],
|
||||
},
|
||||
{
|
||||
"competence_id": "B2",
|
||||
"title": "Analyse des Kundenbedarfs und des Kundenbedürfnisses durchführen",
|
||||
"items": [
|
||||
"Fragetechniken",
|
||||
"Visuelle Hilfsmittel / Visualisierungstechniken",
|
||||
],
|
||||
},
|
||||
{
|
||||
"competence_id": "B3",
|
||||
"title": "Individuelle Lösungsvorschläge erarbeiten",
|
||||
"items": [
|
||||
"Fundierte Produktekenntnisse",
|
||||
"Regulatorische Vorschriften",
|
||||
],
|
||||
},
|
||||
{
|
||||
"competence_id": "B4",
|
||||
"title": "Lösungsvorschläge präsentieren und umsetzen",
|
||||
"items": [
|
||||
"Verhandlungsstrategien",
|
||||
"Fundierte Produktkenntnisse",
|
||||
"Visuelle Hilfsmittel / Visualisierungstechniken",
|
||||
],
|
||||
},
|
||||
{
|
||||
"competence_id": "C1",
|
||||
"title": "Cross- und Upselling; bestehende fremdverwaltete Versicherungspolicen prüfen und in das Portfolio aufnehmen",
|
||||
"items": [
|
||||
"Produktkenntnisse",
|
||||
"Gesprächsführung",
|
||||
"Kommunikation",
|
||||
"Fragetechnik",
|
||||
"Verhandlungsgeschick",
|
||||
"Vertragsrecht",
|
||||
"Regulatorische Vorgaben",
|
||||
"UVG, BVG, KVG, VVG",
|
||||
],
|
||||
},
|
||||
{
|
||||
"competence_id": "C2",
|
||||
"title": "Änderungswünsche entgegennehmen und bestehende Verträge anpassen",
|
||||
"items": [
|
||||
"Produktkenntnisse",
|
||||
"Gesprächsführung",
|
||||
"Kommunikation",
|
||||
"Fragetechnik",
|
||||
"Verhandlungsgeschick",
|
||||
"Vertragsrecht",
|
||||
"Regulatorische Vorgaben",
|
||||
"UVG, BVG, KVG, VVG",
|
||||
],
|
||||
},
|
||||
{
|
||||
"competence_id": "C3",
|
||||
"title": "Kunden im Schadenfall unterstützen",
|
||||
"items": [],
|
||||
},
|
||||
{"competence_id": "C4", "title": "Bestehende Kunden pflegen", "items": []},
|
||||
{
|
||||
"competence_id": "C5",
|
||||
"title": "Versicherungsanträge nachbearbeiten",
|
||||
"items": [],
|
||||
},
|
||||
]
|
||||
|
||||
for c in competences:
|
||||
CompetencePageFactory(
|
||||
parent=competence_profile_page,
|
||||
competence_id=c['competence_id'],
|
||||
title=c['title'],
|
||||
items=[
|
||||
('item', i) for i in c['items']
|
||||
]
|
||||
competence_id=c["competence_id"],
|
||||
title=c["title"],
|
||||
items=[("item", i) for i in c["items"]],
|
||||
)
|
||||
|
||||
PerformanceCriteriaFactory(
|
||||
parent=CompetencePage.objects.get(competence_id='B1'),
|
||||
competence_id='B1.3',
|
||||
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'),
|
||||
parent=CompetencePage.objects.get(competence_id="B1"),
|
||||
competence_id="B1.3",
|
||||
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"
|
||||
),
|
||||
)
|
||||
PerformanceCriteriaFactory(
|
||||
parent=CompetencePage.objects.get(competence_id='B2'),
|
||||
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.',
|
||||
learning_unit=LearningUnit.objects.get(slug='versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug'),
|
||||
parent=CompetencePage.objects.get(competence_id="B2"),
|
||||
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.",
|
||||
learning_unit=LearningUnit.objects.get(
|
||||
slug="versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug"
|
||||
),
|
||||
)
|
||||
PerformanceCriteriaFactory(
|
||||
parent=CompetencePage.objects.get(competence_id='B2'),
|
||||
competence_id='B2.2',
|
||||
title='Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Risiken aufzuzeigen.',
|
||||
learning_unit=LearningUnit.objects.get(slug='versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug'),
|
||||
parent=CompetencePage.objects.get(competence_id="B2"),
|
||||
competence_id="B2.2",
|
||||
title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Risiken aufzuzeigen.",
|
||||
learning_unit=LearningUnit.objects.get(
|
||||
slug="versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,32 @@
|
|||
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):
|
||||
title = 'Kompetenzprofil'
|
||||
title = "Kompetenzprofil"
|
||||
|
||||
class Meta:
|
||||
model = CompetenceProfilePage
|
||||
|
||||
|
||||
class CompetencePageFactory(wagtail_factories.PageFactory):
|
||||
competence_id = 'A1'
|
||||
title = 'Weiterempfehlung für Neukunden generieren'
|
||||
competence_id = "A1"
|
||||
title = "Weiterempfehlung für Neukunden generieren"
|
||||
|
||||
class Meta:
|
||||
model = CompetencePage
|
||||
|
||||
|
||||
class PerformanceCriteriaFactory(wagtail_factories.PageFactory):
|
||||
competence_id = 'A1.1'
|
||||
title = 'Bestehende Kunden so zu beraten, dass sie von diesen weiterempfohlen werden'
|
||||
competence_id = "A1.1"
|
||||
title = (
|
||||
"Bestehende Kunden so zu beraten, dass sie von diesen weiterempfohlen werden"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PerformanceCriteria
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# Generated by Django 3.2.13 on 2022-09-28 12:51
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import wagtail.blocks
|
||||
import wagtail.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
@ -11,41 +11,76 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0069_log_entry_jsonfield'),
|
||||
("wagtailcore", "0069_log_entry_jsonfield"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CompetencePage',
|
||||
name="CompetencePage",
|
||||
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')),
|
||||
('items', wagtail.fields.StreamField([('item', wagtail.blocks.TextBlock())], use_json_field=True)),
|
||||
(
|
||||
"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")),
|
||||
(
|
||||
"items",
|
||||
wagtail.fields.StreamField(
|
||||
[("item", wagtail.blocks.TextBlock())], use_json_field=True
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
bases=("wagtailcore.page",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CompetenceProfilePage',
|
||||
name="CompetenceProfilePage",
|
||||
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={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
bases=("wagtailcore.page",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PerformanceCriteria',
|
||||
name="PerformanceCriteria",
|
||||
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={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
bases=("wagtailcore.page",),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Generated by Django 3.2.13 on 2022-09-28 12:51
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
@ -9,14 +9,19 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('learnpath', '0001_initial'),
|
||||
('competence', '0001_initial'),
|
||||
("learnpath", "0001_initial"),
|
||||
("competence", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='performancecriteria',
|
||||
name='learning_unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='learnpath.learningunit'),
|
||||
model_name="performancecriteria",
|
||||
name="learning_unit",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="learnpath.learningunit",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -10,86 +10,117 @@ from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
|
|||
|
||||
|
||||
class CompetenceProfilePage(Page):
|
||||
parent_page_types = ['course.CoursePage']
|
||||
subpage_types = ['competence.CompetencePage']
|
||||
parent_page_types = ["course.CoursePage"]
|
||||
subpage_types = ["competence.CompetencePage"]
|
||||
|
||||
content_panels = [
|
||||
FieldPanel('title', classname="full title"),
|
||||
FieldPanel("title", classname="full title"),
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def get_serializer_class(cls):
|
||||
return get_it_serializer_class(
|
||||
cls, [
|
||||
'id', 'title', 'slug', 'type', 'translation_key',
|
||||
'course',
|
||||
'children',
|
||||
]
|
||||
cls,
|
||||
[
|
||||
"id",
|
||||
"title",
|
||||
"slug",
|
||||
"type",
|
||||
"translation_key",
|
||||
"course",
|
||||
"children",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class CompetencePage(Page):
|
||||
parent_page_types = ['competence.CompetenceProfilePage']
|
||||
subpage_types = ['competence.PerformanceCriteria']
|
||||
competence_id = models.TextField(default='A1')
|
||||
items = StreamField([
|
||||
('item', blocks.TextBlock()),
|
||||
], use_json_field=True)
|
||||
parent_page_types = ["competence.CompetenceProfilePage"]
|
||||
subpage_types = ["competence.PerformanceCriteria"]
|
||||
competence_id = models.TextField(default="A1")
|
||||
items = StreamField(
|
||||
[
|
||||
("item", blocks.TextBlock()),
|
||||
],
|
||||
use_json_field=True,
|
||||
)
|
||||
|
||||
content_panels = [
|
||||
FieldPanel('title'),
|
||||
FieldPanel('competence_id'),
|
||||
FieldPanel("title"),
|
||||
FieldPanel("competence_id"),
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def get_serializer_class(cls):
|
||||
return get_it_serializer_class(
|
||||
cls, [
|
||||
'id', 'title', 'slug', 'type', 'translation_key',
|
||||
'children',
|
||||
]
|
||||
cls,
|
||||
[
|
||||
"id",
|
||||
"title",
|
||||
"slug",
|
||||
"type",
|
||||
"translation_key",
|
||||
"children",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class PerformanceCriteria(Page):
|
||||
parent_page_types = ['competence.CompetenceProfilePage']
|
||||
competence_id = models.TextField(default='A1.1')
|
||||
parent_page_types = ["competence.CompetenceProfilePage"]
|
||||
competence_id = models.TextField(default="A1.1")
|
||||
learning_unit = models.ForeignKey(
|
||||
'learnpath.LearningUnit',
|
||||
"learnpath.LearningUnit",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
content_panels = [
|
||||
FieldPanel('title'),
|
||||
FieldPanel('competence_id'),
|
||||
FieldPanel('learning_unit'),
|
||||
FieldPanel("title"),
|
||||
FieldPanel("competence_id"),
|
||||
FieldPanel("learning_unit"),
|
||||
]
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
profile_parent = self.get_ancestors().exact_type(CompetenceProfilePage).last()
|
||||
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:
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def get_serializer_class(cls):
|
||||
from vbv_lernwelt.competence.serializers import PerformanceCriteriaSerializer
|
||||
|
||||
return PerformanceCriteriaSerializer
|
||||
|
||||
def get_admin_display_title(self):
|
||||
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:
|
||||
return f'{self.competence_id} {self.draft_title[:30]}'
|
||||
return f"{self.competence_id} {self.draft_title[:30]}"
|
||||
|
|
|
|||
|
|
@ -6,18 +6,37 @@ from vbv_lernwelt.learnpath.models import LearningUnit
|
|||
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
|
||||
|
||||
|
||||
class PerformanceCriteriaSerializer(get_it_serializer_class(PerformanceCriteria, [
|
||||
'id', 'title', 'slug', 'type', 'translation_key',
|
||||
'competence_id', 'learning_unit', 'circle', 'course_category',
|
||||
])):
|
||||
class PerformanceCriteriaSerializer(
|
||||
get_it_serializer_class(
|
||||
PerformanceCriteria,
|
||||
[
|
||||
"id",
|
||||
"title",
|
||||
"slug",
|
||||
"type",
|
||||
"translation_key",
|
||||
"competence_id",
|
||||
"learning_unit",
|
||||
"circle",
|
||||
"course_category",
|
||||
],
|
||||
)
|
||||
):
|
||||
learning_unit = serializers.SerializerMethodField()
|
||||
circle = serializers.SerializerMethodField()
|
||||
course_category = serializers.SerializerMethodField()
|
||||
|
||||
def get_learning_unit(self, obj):
|
||||
learning_unit_serializer = get_it_serializer_class(LearningUnit, [
|
||||
'id', 'title', 'slug', 'type', 'translation_key',
|
||||
])
|
||||
learning_unit_serializer = get_it_serializer_class(
|
||||
LearningUnit,
|
||||
[
|
||||
"id",
|
||||
"title",
|
||||
"slug",
|
||||
"type",
|
||||
"translation_key",
|
||||
],
|
||||
)
|
||||
return learning_unit_serializer(obj.learning_unit).data
|
||||
|
||||
def get_circle(self, obj):
|
||||
|
|
@ -29,7 +48,17 @@ class PerformanceCriteriaSerializer(get_it_serializer_class(PerformanceCriteria,
|
|||
return None
|
||||
|
||||
|
||||
class PerformanceCriteriaLearningPathSerializer(get_it_serializer_class(PerformanceCriteria, [
|
||||
'id', 'title', 'slug', 'type', 'translation_key', 'competence_id',
|
||||
])):
|
||||
class PerformanceCriteriaLearningPathSerializer(
|
||||
get_it_serializer_class(
|
||||
PerformanceCriteria,
|
||||
[
|
||||
"id",
|
||||
"title",
|
||||
"slug",
|
||||
"type",
|
||||
"translation_key",
|
||||
"competence_id",
|
||||
],
|
||||
)
|
||||
):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -10,16 +10,19 @@ class CompetenceAPITestCase(APITestCase):
|
|||
def setUp(self) -> None:
|
||||
create_default_users()
|
||||
create_test_course()
|
||||
self.user = User.objects.get(username='student')
|
||||
self.client.login(username='student', password='test')
|
||||
self.user = User.objects.get(username="student")
|
||||
self.client.login(username="student", password="test")
|
||||
|
||||
def test_get_learnpathPage(self):
|
||||
slug = 'test-lehrgang-competence'
|
||||
slug = "test-lehrgang-competence"
|
||||
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)
|
||||
data = response.json()
|
||||
|
||||
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(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"],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth import admin as auth_admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth import admin as auth_admin, get_user_model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
User = get_user_model()
|
||||
|
|
|
|||
|
|
@ -6,15 +6,21 @@ from vbv_lernwelt.core.models import User
|
|||
|
||||
def create_default_users(user_model=User, group_model=Group, default_password=None):
|
||||
if default_password is None:
|
||||
default_password = 'test'
|
||||
default_password = "test"
|
||||
|
||||
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')
|
||||
student_group, created = group_model.objects.get_or_create(name='student_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"
|
||||
)
|
||||
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(
|
||||
user_model=user_model, username=email, password=password,
|
||||
user_model=user_model,
|
||||
username=email,
|
||||
password=password,
|
||||
)
|
||||
student_user.first_name = first_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.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(
|
||||
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()
|
||||
|
||||
_create_admin_user(
|
||||
email='info@iterativ.ch',
|
||||
first_name='Info',
|
||||
last_name='Iterativ',
|
||||
avatar_url='/static/avatars/avatar_iterativ.png'
|
||||
email="info@iterativ.ch",
|
||||
first_name="Info",
|
||||
last_name="Iterativ",
|
||||
avatar_url="/static/avatars/avatar_iterativ.png",
|
||||
)
|
||||
|
||||
_create_admin_user(
|
||||
email='admin',
|
||||
first_name='Peter',
|
||||
last_name='Adminson',
|
||||
avatar_url='/static/avatars/avatar_iterativ.png'
|
||||
email="admin",
|
||||
first_name="Peter",
|
||||
last_name="Adminson",
|
||||
avatar_url="/static/avatars/avatar_iterativ.png",
|
||||
)
|
||||
|
||||
_create_student_user(
|
||||
email='student',
|
||||
first_name='Student',
|
||||
last_name='Meier',
|
||||
avatar_url='/static/avatars/avatar_iterativ.png'
|
||||
email="student",
|
||||
first_name="Student",
|
||||
last_name="Meier",
|
||||
avatar_url="/static/avatars/avatar_iterativ.png",
|
||||
)
|
||||
|
||||
_create_student_user(
|
||||
email='daniel.egger@iterativ.ch',
|
||||
first_name='Daniel',
|
||||
last_name='Egger',
|
||||
avatar_url='/static/avatars/avatar_iterativ.png'
|
||||
email="daniel.egger@iterativ.ch",
|
||||
first_name="Daniel",
|
||||
last_name="Egger",
|
||||
avatar_url="/static/avatars/avatar_iterativ.png",
|
||||
)
|
||||
|
||||
_create_student_user(
|
||||
email='axel.manderbach@lernetz.ch',
|
||||
first_name='Axel',
|
||||
last_name='Manderbach',
|
||||
avatar_url='/static/avatars/avatar_axel.jpg'
|
||||
email="axel.manderbach@lernetz.ch",
|
||||
first_name="Axel",
|
||||
last_name="Manderbach",
|
||||
avatar_url="/static/avatars/avatar_axel.jpg",
|
||||
)
|
||||
|
||||
_create_student_user(
|
||||
email='christoph.bosshard@vbv-afa.ch',
|
||||
first_name='Christoph',
|
||||
last_name='Bosshard',
|
||||
avatar_url='/static/avatars/avatar_christoph.png',
|
||||
password='myvbv1234'
|
||||
email="christoph.bosshard@vbv-afa.ch",
|
||||
first_name="Christoph",
|
||||
last_name="Bosshard",
|
||||
avatar_url="/static/avatars/avatar_christoph.png",
|
||||
password="myvbv1234",
|
||||
)
|
||||
|
||||
_create_student_user(
|
||||
email='alexandra.vangelista@lernetz.ch',
|
||||
first_name='Alexandra',
|
||||
last_name='Vangelista',
|
||||
avatar_url='/static/avatars/avatar_alexandra.png',
|
||||
password='myvbv1234'
|
||||
email="alexandra.vangelista@lernetz.ch",
|
||||
first_name="Alexandra",
|
||||
last_name="Vangelista",
|
||||
avatar_url="/static/avatars/avatar_alexandra.png",
|
||||
password="myvbv1234",
|
||||
)
|
||||
|
||||
_create_student_user(
|
||||
email='chantal.rosenberg@vbv-afa.ch',
|
||||
first_name='Chantal',
|
||||
last_name='Rosenberg',
|
||||
avatar_url='/static/avatars/avatar_chantal.png',
|
||||
password='myvbv1234'
|
||||
email="chantal.rosenberg@vbv-afa.ch",
|
||||
first_name="Chantal",
|
||||
last_name="Rosenberg",
|
||||
avatar_url="/static/avatars/avatar_chantal.png",
|
||||
password="myvbv1234",
|
||||
)
|
||||
|
||||
|
||||
def _get_or_create_user(user_model, *args, **kwargs):
|
||||
username = kwargs.get('username', None)
|
||||
password = kwargs.get('password', None)
|
||||
username = kwargs.get("username", None)
|
||||
password = kwargs.get("password", None)
|
||||
created = False
|
||||
|
||||
user = user_model.objects.filter(username=username).first()
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import djclick as click
|
||||
from django.conf import settings
|
||||
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):
|
||||
sql_list = (
|
||||
'DROP SCHEMA public CASCADE',
|
||||
f'CREATE SCHEMA public AUTHORIZATION {db_config_user}',
|
||||
'GRANT ALL ON SCHEMA public TO postgres',
|
||||
'GRANT ALL ON SCHEMA public TO public',
|
||||
"DROP SCHEMA public CASCADE",
|
||||
f"CREATE SCHEMA public AUTHORIZATION {db_config_user}",
|
||||
"GRANT ALL ON SCHEMA public TO postgres",
|
||||
"GRANT ALL ON SCHEMA public TO public",
|
||||
"COMMENT ON SCHEMA public IS 'standard public schema';",
|
||||
)
|
||||
|
||||
|
|
@ -21,11 +21,11 @@ def reset_schema(db_config_user):
|
|||
|
||||
@click.command()
|
||||
def command():
|
||||
user = settings.DATABASES['default']['USER']
|
||||
user = settings.DATABASES["default"]["USER"]
|
||||
print(user)
|
||||
|
||||
reset_schema(db_config_user=user)
|
||||
call_command('createcachetable')
|
||||
call_command('migrate')
|
||||
call_command('create_default_users')
|
||||
call_command('create_default_learning_path')
|
||||
call_command("createcachetable")
|
||||
call_command("migrate")
|
||||
call_command("create_default_users")
|
||||
call_command("create_default_learning_path")
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ from django.contrib.auth.models import AbstractUser
|
|||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
def create_or_update_by_email(self, email: str, first_name: str, last_name: str, username: str) -> tuple[
|
||||
AbstractUser, bool]:
|
||||
def create_or_update_by_email(
|
||||
self, email: str, first_name: str, last_name: str, username: str
|
||||
) -> tuple[AbstractUser, bool]:
|
||||
# create or sync user with OpenID Data
|
||||
user, created = self.model.objects.get_or_create(email=email, defaults={
|
||||
"first_name": first_name,
|
||||
"last_name": last_name,
|
||||
"username": username
|
||||
})
|
||||
user, created = self.model.objects.get_or_create(
|
||||
email=email,
|
||||
defaults={
|
||||
"first_name": first_name,
|
||||
"last_name": last_name,
|
||||
"username": username,
|
||||
},
|
||||
)
|
||||
|
||||
return user, created
|
||||
|
|
|
|||
|
|
@ -55,13 +55,15 @@ class UserLoggedInCookieMiddleWare(MiddlewareMixin):
|
|||
If the user is not authenticated and the cookie remains, delete it
|
||||
"""
|
||||
|
||||
cookie_name = 'loginStatus'
|
||||
cookie_name = "loginStatus"
|
||||
|
||||
def process_response(self, request, response):
|
||||
# if user and no cookie, set cookie
|
||||
if request.user.is_authenticated and not request.COOKIES.get(self.cookie_name):
|
||||
response.set_cookie(self.cookie_name, 'true')
|
||||
elif not request.user.is_authenticated and request.COOKIES.get(self.cookie_name):
|
||||
response.set_cookie(self.cookie_name, "true")
|
||||
elif not request.user.is_authenticated and request.COOKIES.get(
|
||||
self.cookie_name
|
||||
):
|
||||
# else if no user and cookie remove user cookie, logout
|
||||
response.delete_cookie(self.cookie_name)
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue