Upgrade code according to migration guide for Vue 3

Update npm dependencies

Update vue router version

Disable validation temporarily

Specify property

Update dependencies

Update store to v4

Update async component definitions

Update some event emitters

Update tiptap vue version

Implement some router changes for v4

Remove obsolete tag attributes

Update dependencies

Fix some cypress tests

Fix most jest tests

Fix some more cypress tests

Fix school class cypress test

Fix another cypress test

Disable failing test temporarily

Fix validation

Fix error messages for validation

Fix e2e test for beta login page

Apply prettier
This commit is contained in:
Ramon Wenger 2022-03-23 16:21:06 +01:00
parent 4b55f8952c
commit a52671fd40
106 changed files with 4604 additions and 5195 deletions

View File

@ -1 +1,3 @@
bundle-analysis
dist dist
node_modules

View File

@ -1,6 +1,6 @@
import module from '../../../fixtures/module.minimal'; import module from '../../../fixtures/module.minimal';
import {getMinimalMe} from '../../../support/helpers'; import { getMinimalMe } from '../../../support/helpers';
import {hasOperationName} from '../../../support/graphql'; import { hasOperationName } from '../../../support/graphql';
let snapshotTitle; let snapshotTitle;
let deleteSuccess; let deleteSuccess;
@ -133,7 +133,7 @@ describe('Snapshot', () => {
}, },
}, },
}, },
MeQuery: getMinimalMe({isTeacher}), MeQuery: getMinimalMe({ isTeacher }),
ModuleDetailsQuery: { ModuleDetailsQuery: {
module, module,
}, },
@ -219,9 +219,11 @@ describe('Snapshot', () => {
cy.getByDataCy('module-snapshots-button').click(); cy.getByDataCy('module-snapshots-button').click();
cy.getByDataCy('create-snapshot-button').click(); cy.getByDataCy('create-snapshot-button').click();
cy.getByDataCy('show-all-snapshots-button').click(); cy.getByDataCy('show-all-snapshots-button').click();
cy.getByDataCy('snapshot-list').should('exist').within(() => { cy.getByDataCy('snapshot-list')
cy.get('.snapshots__snapshot').should('have.length', 1); .should('exist')
}); .within(() => {
cy.get('.snapshots__snapshot').should('have.length', 1);
});
waitNTimes(7); waitNTimes(7);
}); });

View File

@ -1,7 +1,7 @@
describe('Room Team Management - Read only', () => { describe('Room Team Management - Read only', () => {
const SELECTED_CLASS_ID = 'selectedClassId'; const SELECTED_CLASS_ID = 'selectedClassId';
const getOperations = ({readOnly, classReadOnly}) => ({ const getOperations = ({ readOnly, classReadOnly }) => ({
MeQuery: { MeQuery: {
me: { me: {
readOnly, readOnly,
@ -13,23 +13,25 @@ describe('Room Team Management - Read only', () => {
}, },
}, },
RoomsQuery: { RoomsQuery: {
rooms: [{ rooms: [
id: '', {
slug: 'some-room', id: '',
title: 'some room', slug: 'some-room',
entryCount: 3, title: 'some room',
appearance: 'red', entryCount: 3,
description: 'some description', appearance: 'red',
schoolClass: { description: 'some description',
id: SELECTED_CLASS_ID, schoolClass: {
name: 'bla', id: SELECTED_CLASS_ID,
name: 'bla',
},
}, },
}], ],
}, },
}); });
const checkRoomsReadOnly = ({editable, readOnly, classReadOnly = false}) => { const checkRoomsReadOnly = ({ editable, readOnly, classReadOnly = false }) => {
const operations = getOperations({readOnly, classReadOnly}); const operations = getOperations({ readOnly, classReadOnly });
cy.mockGraphqlOps({ cy.mockGraphqlOps({
operations, operations,
@ -48,14 +50,14 @@ describe('Room Team Management - Read only', () => {
}); });
it('can edit room', () => { it('can edit room', () => {
checkRoomsReadOnly({editable: true, readOnly: false}); checkRoomsReadOnly({ editable: true, readOnly: false });
}); });
it('can not edit room', () => { it('can not edit room', () => {
checkRoomsReadOnly({editable: false, readOnly: true}); checkRoomsReadOnly({ editable: false, readOnly: true });
}); });
it('can not edit room of inactive class', () => { it('can not edit room of inactive class', () => {
checkRoomsReadOnly({editable: false, readOnly: false, classReadOnly: true}); checkRoomsReadOnly({ editable: false, readOnly: false, classReadOnly: true });
}); });
}); });

View File

@ -1,11 +1,5 @@
module.exports = { module.exports = {
moduleFileExtensions: [ moduleFileExtensions: ['js', 'jsx', 'ts', 'json', 'vue'],
'js',
'jsx',
'ts',
'json',
'vue',
],
transform: { transform: {
'\\.(gql|graphql)$': '@graphql-tools/jest-transform', '\\.(gql|graphql)$': '@graphql-tools/jest-transform',
'^.+\\.js$': 'babel-jest', '^.+\\.js$': 'babel-jest',
@ -13,20 +7,13 @@ module.exports = {
'^.+\\.vue$': '@vue/vue3-jest', '^.+\\.vue$': '@vue/vue3-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
}, },
modulePaths: [ modulePaths: ['<rootDir>/src', '<rootDir>/node_modules'],
'<rootDir>/src', transformIgnorePatterns: ['/node_modules/'],
'<rootDir>/node_modules',
],
transformIgnorePatterns: [
'/node_modules/',
],
moduleNameMapper: { moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', '^@/(.*)$': '<rootDir>/src/$1',
'^gql/(.*)$': '<rootDir>/src/graphql/gql/$1', '^gql/(.*)$': '<rootDir>/src/graphql/gql/$1',
}, },
snapshotSerializers: [ snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
'<rootDir>/node_modules/jest-serializer-vue',
],
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
testEnvironmentOptions: { testEnvironmentOptions: {
url: 'http://localhost/', url: 'http://localhost/',

View File

@ -3,6 +3,9 @@
"version": "1.0.0", "version": "1.0.0",
"description": "skillbox vue client", "description": "skillbox vue client",
"author": "ramon / chrigu", "author": "ramon / chrigu",
"prettier": {
"singleQuote": true
},
"private": true, "private": true,
"prettier": { "prettier": {
"singleQuote": true "singleQuote": true

View File

@ -1,9 +1,6 @@
<template> <template>
<div class="add-content"> <div class="add-content">
<a <a class="add-content__button" @click="addContent">
class="add-content__button"
@click="addContent"
>
<add-pointer class="add-content__icon" /> <add-pointer class="add-content__icon" />
</a> </a>
</div> </div>
@ -18,88 +15,88 @@
const AddPointer = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddPointer')); const AddPointer = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddPointer'));
export default { export default {
props: { props: {
where: { where: {
type: Object, type: Object,
validator(prop) { validator(prop) {
return Object.prototype.hasOwnProperty.call(prop, 'after' ) return (
|| Object.prototype.hasOwnProperty.call(prop, 'parent'); Object.prototype.hasOwnProperty.call(prop, 'after') || Object.prototype.hasOwnProperty.call(prop, 'parent')
} );
}, },
}, },
},
components: { components: {
AddPointer AddPointer,
},
computed: {
parent() {
return this.where.parent;
}, },
after() {
computed: { return this.where.after;
parent() {
return this.where.parent;
},
after() {
return this.where.after;
},
isObjectiveGroup() {
return this.parent && this.parent.__typename === 'ObjectiveGroupNode';
},
slug() {
return this.$route.params.slug;
}
}, },
isObjectiveGroup() {
return this.parent && this.parent.__typename === 'ObjectiveGroupNode';
},
slug() {
return this.$route.params.slug;
},
},
methods: {
methods: { addContent() {
addContent() { if (this.isObjectiveGroup) {
if (this.isObjectiveGroup) { this.$modal.open('new-objective-wizard', { parent: this.parent.id });
this.$modal.open('new-objective-wizard', {parent: this.parent.id}); } else {
let route;
if (this.after && this.after.id) {
route = {
name: CREATE_CONTENT_BLOCK_AFTER_PAGE,
params: {
after: this.after.id,
slug: this.slug,
},
};
} else { } else {
let route; route = {
if (this.after && this.after.id) { name: CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
route = { params: {
name: CREATE_CONTENT_BLOCK_AFTER_PAGE, parent: this.parent.id,
params: { },
after: this.after.id, };
slug: this.slug
}
};
} else {
route = {
name: CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
params: {
parent: this.parent.id
}
};
}
this.$router.push(route);
} }
this.$router.push(route);
} }
} },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.add-content { .add-content {
display: none; display: none;
position: relative; position: relative;
@include desktop { @include desktop {
display: flex; display: flex;
}
z-index: 1;
justify-content: flex-end;
&__button {
margin-right: -85px;
cursor: pointer;
display: inline-grid;
}
&__icon {
width: 40px;
fill: $color-silver-dark;
}
} }
z-index: 1;
justify-content: flex-end;
&__button {
margin-right: -85px;
cursor: pointer;
display: inline-grid;
}
&__icon {
width: 40px;
fill: $color-silver-dark;
}
}
</style> </style>

View File

@ -1,8 +1,5 @@
<template> <template>
<div <div class="add-content-element" @click="$emit('add-element', index)">
class="add-content-element"
@click="$emit('add-element', index)"
>
<add-icon class="add-content-element__icon" /> <add-icon class="add-content-element__icon" />
</div> </div>
</template> </template>
@ -11,32 +8,32 @@
import {defineAsyncComponent} from 'vue'; import {defineAsyncComponent} from 'vue';
const AddIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon')); const AddIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon'));
export default { export default {
props: ['index'], props: ['index'],
components: { components: {
AddIcon AddIcon,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.add-content-element { .add-content-element {
display: flex; display: flex;
justify-content: center; justify-content: center;
border-bottom: 2px solid $color-silver-dark; border-bottom: 2px solid $color-silver-dark;
margin-bottom: 21px + 25px; margin-bottom: 21px + 25px;
cursor: pointer; cursor: pointer;
&__icon { &__icon {
width: 40px; width: 40px;
height: 40px; height: 40px;
fill: $color-silver-dark; fill: $color-silver-dark;
margin-bottom: -21px; margin-bottom: -21px;
background-color: $color-white; background-color: $color-white;
border-radius: 50px; border-radius: 50px;
}
} }
}
</style> </style>

View File

@ -14,66 +14,69 @@
import {defineAsyncComponent} from 'vue'; import {defineAsyncComponent} from 'vue';
const AddIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon.vue')); const AddIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon.vue'));
export default { export default {
props: { props: {
route: { route: {
type: String, type: String,
default: null default: null,
},
reverse: { // use reverse colors
type: Boolean,
default: false
},
click: {
type: Function,
default: null
}
}, },
reverse: {
// use reverse colors
type: Boolean,
default: false,
},
click: {
type: Function,
default: null,
},
},
components: { components: {
AddIcon AddIcon,
}, },
computed: { computed: {
component() { component() {
// only use the router link if the route prop is provided, otherwise render a normal anchor tag // only use the router link if the route prop is provided, otherwise render a normal anchor tag
return this.route ? 'router-link' : 'a'; return this.route ? 'router-link' : 'a';
},
properties() {
return this.route ? {
to: this.route,
tag: 'div'
} : {};
}
}, },
}; properties() {
return this.route
? {
to: this.route,
tag: 'div',
}
: {};
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.add-widget { .add-widget {
display: none; display: none;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@include widget-shadow; @include widget-shadow;
cursor: pointer; cursor: pointer;
@include desktop { @include desktop {
display: flex; display: flex;
}
&__add {
width: 80px;
fill: $color-silver-dark;
}
&--reverse {
@include widget-shadow-reverse;
}
&--reverse &__add {
fill: white;
}
} }
&__add {
width: 80px;
fill: $color-silver-dark;
}
&--reverse {
@include widget-shadow-reverse;
}
&--reverse &__add {
fill: white;
}
}
</style> </style>

View File

@ -1,8 +1,5 @@
<template> <template>
<router-link <router-link :to="to" class="sub-navigation-item back-link">
:to="to"
class="sub-navigation-item back-link"
>
<chevron-left class="back-link__icon sub-navigation-item__icon" /> <chevron-left class="back-link__icon sub-navigation-item__icon" />
{{ fullTitle }} {{ fullTitle }}
</router-link> </router-link>
@ -16,25 +13,25 @@
const ChevronLeft = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronLeft')); const ChevronLeft = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronLeft'));
export default { export default {
props: { props: {
title: { title: {
type: String, type: String,
default: '', default: '',
},
type: {
type: String,
default: 'topic',
},
slug: {
type: String,
default: '',
},
}, },
type: {
type: String,
default: 'topic',
},
slug: {
type: String,
default: '',
},
},
components: { components: {
ChevronLeft, ChevronLeft,
}, },
computed: { computed: {
to() { to() {
@ -68,9 +65,9 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.back-link { .back-link {
@include regular-text; @include regular-text;
} }
</style> </style>

View File

@ -1,21 +1,15 @@
<template> <template>
<div class="color-chooser"> <div class="color-chooser">
<div <div
:class="{'color-chooser__color-wrapper--selected': selectedColor === color.name}" :class="{ 'color-chooser__color-wrapper--selected': selectedColor === color.name }"
class="color-chooser__color-wrapper" class="color-chooser__color-wrapper"
data-cy="color-select" data-cy="color-select"
v-for="(color, index) in colors" v-for="(color, index) in colors"
:key="index" :key="index"
@click="$emit('input', color.name)" @click="$emit('input', color.name)"
> >
<div <div :class="'color-chooser__color--' + color.name" class="color-chooser__color">
:class="'color-chooser__color--' + color.name" <tick class="color-chooser__selected-icon" v-if="selectedColor === color.name" />
class="color-chooser__color"
>
<tick
class="color-chooser__selected-icon"
v-if="selectedColor === color.name"
/>
</div> </div>
</div> </div>
</div> </div>
@ -25,69 +19,69 @@
import {defineAsyncComponent} from 'vue'; import {defineAsyncComponent} from 'vue';
const Tick = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Tick')); const Tick = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Tick'));
export default { export default {
props: ['selectedColor'], props: ['selectedColor'],
components: { components: {
Tick Tick,
}, },
data() { data() {
return { return {
colors: [ colors: [
{ {
name: 'yellow' name: 'yellow',
}, },
{ {
name: 'blue' name: 'blue',
}, },
{ {
name: 'red' name: 'red',
}, },
{ {
name: 'green' name: 'green',
} },
] ],
}; };
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.color-chooser { .color-chooser {
display: flex; display: flex;
&__color-wrapper { &__color-wrapper {
margin-right: 10px; margin-right: 10px;
border-radius: 50px; border-radius: 50px;
padding: 10px; padding: 10px;
&--selected { &--selected {
border: 1px solid $color-charcoal-dark; border: 1px solid $color-charcoal-dark;
}
}
&__selected-icon {
width: 17px;
fill: $color-charcoal-dark;
}
&__color {
width: 46px;
height: 46px;
border-radius: 23px;
display: flex;
justify-content: center;
@supports (display: grid) {
display: grid
}
justify-items: center;
align-items: center;
@include skillbox-colors;
} }
} }
&__selected-icon {
width: 17px;
fill: $color-charcoal-dark;
}
&__color {
width: 46px;
height: 46px;
border-radius: 23px;
display: flex;
justify-content: center;
@supports (display: grid) {
display: grid;
}
justify-items: center;
align-items: center;
@include skillbox-colors;
}
}
</style> </style>

View File

@ -1,37 +1,38 @@
<template> <template>
<modal :fullscreen="true"> <modal :fullscreen="true">
<component <component :value="value" :is="type" />
:value="value"
:is="type"
/>
</modal> </modal>
</template> </template>
<script> <script>
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const InfogramBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/InfogramBlock')); const InfogramBlock = defineAsyncComponent(() =>
const GeniallyBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/GeniallyBlock')); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/InfogramBlock')
);
const GeniallyBlock = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/GeniallyBlock')
);
export default { export default {
components: { components: {
Modal, Modal,
InfogramBlock, InfogramBlock,
GeniallyBlock GeniallyBlock,
},
computed: {
id() {
return this.$store.state.infographic.id;
}, },
type() {
computed: { return this.$store.state.infographic.type;
id() { },
return this.$store.state.infographic.id; value() {
}, return {
type() { id: this.id,
return this.$store.state.infographic.type; };
}, },
value() { },
return { };
id: this.id
};
}
}
};
</script> </script>

View File

@ -1,140 +1,127 @@
<template> <template>
<header class="header-bar"> <header class="header-bar">
<a <a class="header-bar__sidebar-link" data-cy="open-sidebar-link" @click.stop="openSidebar('navigation')">
class="header-bar__sidebar-link"
data-cy="open-sidebar-link"
@click.stop="openSidebar('navigation')"
>
<hamburger class="header-bar__sidebar-icon" /> <hamburger class="header-bar__sidebar-icon" />
</a> </a>
<content-navigation class="header-bar__content-navigation" /> <content-navigation class="header-bar__content-navigation" />
<div class="user-header"> <div class="user-header">
<a <a class="user-header__sidebar-link">
class="user-header__sidebar-link" <current-class class="user-header__current-class" @click="openSidebar('profile')" />
>
<current-class
class="user-header__current-class"
@click="openSidebar('profile')"
/>
</a> </a>
<user-widget <user-widget :avatar-url="me.avatarUrl" data-cy="header-user-widget" @click="openSidebar('profile')" />
:avatar-url="me.avatarUrl"
data-cy="header-user-widget"
@click="openSidebar('profile')"
/>
</div> </div>
</header> </header>
</template> </template>
<script> <script>
import ContentNavigation from '@/components/book-navigation/ContentNavigation.vue'; import ContentNavigation from '@/components/book-navigation/ContentNavigation.vue';
import UserWidget from '@/components/UserWidget.vue'; import UserWidget from '@/components/UserWidget.vue';
import CurrentClass from '@/components/school-class/CurrentClass'; import CurrentClass from '@/components/school-class/CurrentClass';
import openSidebar from '@/mixins/open-sidebar'; import openSidebar from '@/mixins/open-sidebar';
import me from '@/mixins/me'; import me from '@/mixins/me';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Hamburger = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Hamburger')); const Hamburger = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/Hamburger'));
export default { export default {
mixins: [openSidebar, me], mixins: [openSidebar, me],
components: { components: {
ContentNavigation, ContentNavigation,
UserWidget, UserWidget,
CurrentClass, CurrentClass,
Hamburger, Hamburger,
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.header-bar { .header-bar {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@supports (display: grid) { @supports (display: grid) {
display: grid; display: grid;
} }
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background-color: $color-white; background-color: $color-white;
grid-auto-rows: 50px; grid-auto-rows: 50px;
width: 100%; width: 100%;
max-width: 100vw; max-width: 100vw;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
@include desktop { @include desktop {
grid-template-columns: 50px 1fr auto; grid-template-columns: 50px 1fr auto;
grid-template-rows: 50px; grid-template-rows: 50px;
grid-auto-rows: auto; grid-auto-rows: auto;
} }
/* /*
* For IE10+ * For IE10+
*/ */
-ms-grid-columns: 1fr 1fr 1fr; -ms-grid-columns: 1fr 1fr 1fr;
-ms-grid-rows: 50px 50px; -ms-grid-rows: 50px 50px;
/* /*
* For IE10+ * For IE10+
*/ */
& > :nth-child(1) { & > :nth-child(1) {
-ms-grid-column: 1; -ms-grid-column: 1;
-ms-grid-row-align: center; -ms-grid-row-align: center;
} }
&__content-navigation { &__content-navigation {
grid-column: 2; grid-column: 2;
justify-content: space-between; justify-content: space-between;
} }
&__sidebar-link { &__sidebar-link {
padding: $small-spacing; padding: $small-spacing;
cursor: pointer; cursor: pointer;
} }
&__sidebar-icon { &__sidebar-icon {
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
/* /*
* For IE10+ * For IE10+
*/ */
& > :nth-child(3) { & > :nth-child(3) {
-ms-grid-column: 3; -ms-grid-column: 3;
-ms-grid-row-align: center; -ms-grid-row-align: center;
-ms-grid-column-align: end; -ms-grid-column-align: end;
justify-self: end; justify-self: end;
}
& > :nth-child(4) {
-ms-grid-row: 2;
-ms-grid-column: 1;
-ms-grid-column-span: 3;
}
} }
.user-header { & > :nth-child(4) {
display: flex; -ms-grid-row: 2;
-ms-grid-column: 1;
-ms-grid-column-span: 3;
}
}
&__current-class { .user-header {
margin-right: $large-spacing; display: flex;
}
&__sidebar-link { &__current-class {
cursor: pointer; margin-right: $large-spacing;
display: none; }
@include desktop { &__sidebar-link {
display: flex; cursor: pointer;
} display: none;
@include desktop {
display: flex;
} }
} }
}
</style> </style>

View File

@ -10,67 +10,66 @@
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon')); const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon'));
export default { export default {
props: ['text'], props: ['text'],
components: { components: {
InfoIcon InfoIcon,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.helpful-tooltip { .helpful-tooltip {
position: relative; position: relative;
&__icon { &__icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
fill: $color-silver-dark; fill: $color-silver-dark;
} }
&__tooltip { &__tooltip {
visibility: hidden; visibility: hidden;
position: absolute;
left: 30px;
top: 0px;
width: 400px;
}
&__text {
display: inline-table;
width: auto;
background-color: $color-white;
border: 1px solid $color-silver-dark;
border-radius: 5px;
padding: $small-spacing;
@include small-text;
&::before {
content: '';
position: absolute; position: absolute;
left: 30px; left: 0;
top: 0px; top: 18px;
width: 400px; margin-left: -1px;
} border-left: 1px solid $color-silver-dark;
border-top: 1px solid $color-silver-dark;
&__text {
display: inline-table;
width: auto;
background-color: $color-white; background-color: $color-white;
border: 1px solid $color-silver-dark; width: 10px;
border-radius: 5px; height: 10px;
padding: $small-spacing; transform: rotate(-45deg) translateY(-50%);
@include small-text;
&::before {
content: '';
position: absolute;
left: 0;
top: 18px;
margin-left: -1px;
border-left: 1px solid $color-silver-dark;
border-top: 1px solid $color-silver-dark;
background-color: $color-white;
width: 10px;
height: 10px;
transform: rotate(-45deg) translateY(-50%);
}
}
&:hover &__tooltip {
visibility: visible;
} }
} }
&:hover &__tooltip {
visibility: visible;
}
}
</style> </style>

View File

@ -1,58 +1,54 @@
<template> <template>
<button <button :disabled="loading || disabled" class="loading-button button button--primary button--big">
:disabled="loading || disabled"
class="loading-button button button--primary button--big"
>
<template v-if="!loading"> <template v-if="!loading">
{{ label }} {{ label }}
</template> </template>
<loading-icon <loading-icon class="loading-button__icon" v-else />
class="loading-button__icon"
v-else
/>
</button> </button>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const LoadingIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/LoadingIcon')); const LoadingIcon = defineAsyncComponent(() =>
import(/* webpackChunkName: "icons" */ '@/components/icons/LoadingIcon')
);
export default { export default {
props: { props: {
loading: { loading: {
type: Boolean, type: Boolean,
default: false default: false,
},
disabled: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
}
}, },
components: { disabled: {
LoadingIcon type: Boolean,
} default: false,
}; },
label: {
type: String,
default: '',
},
},
components: {
LoadingIcon,
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.loading-button { .loading-button {
height: 52px; height: 52px;
min-width: 100px; min-width: 100px;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
&__icon { &__icon {
width: 14px; width: 14px;
height: 14px; height: 14px;
margin: 0 auto; margin: 0 auto;
@include spin; @include spin;
fill: $color-brand; fill: $color-brand;
}
} }
}
</style> </style>

View File

@ -4,66 +4,60 @@
<hamburger class="mobile-header__hamburger" /> <hamburger class="mobile-header__hamburger" />
</a> </a>
<router-link <router-link to="/" data-cy="mobile-home-link">
to="/"
data-cy="mobile-home-link"
>
<logo /> <logo />
</router-link> </router-link>
<user-widget <user-widget v-bind="me" @click.stop="openSidebar('profile')" />
v-bind="me"
@click.stop="openSidebar('profile')"
/>
</div> </div>
</template> </template>
<script> <script>
import UserWidget from '@/components/UserWidget'; import UserWidget from '@/components/UserWidget';
import me from '@/mixins/me'; import me from '@/mixins/me';
import openSidebar from '@/mixins/open-sidebar'; import openSidebar from '@/mixins/open-sidebar';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo')); const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/Logo'));
const Hamburger = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Hamburger')); const Hamburger = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/Hamburger'));
export default { export default {
mixins: [me, openSidebar], mixins: [me, openSidebar],
components: { components: {
Logo, Logo,
Hamburger, Hamburger,
UserWidget, UserWidget,
},
methods: {
showMobileNavigation() {
this.$store.dispatch('showMobileNavigation', true);
}, },
},
methods: { };
showMobileNavigation() {
this.$store.dispatch('showMobileNavigation', true);
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.mobile-header { .mobile-header {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
display: flex; display: flex;
@include desktop { @include desktop {
display: none; display: none;
}
padding: 0 $medium-spacing;
&__hamburger {
width: 30px;
height: 30px;
fill: $color-silver-dark;
}
} }
padding: 0 $medium-spacing;
&__hamburger {
width: 30px;
height: 30px;
fill: $color-silver-dark;
}
}
</style> </style>

View File

@ -1,7 +1,11 @@
<template> <template>
<div class="modal__backdrop"> <div class="modal__backdrop">
<div <div
:class="{'modal--hide-header': hideHeader || fullscreen, 'modal--fullscreen': fullscreen, 'modal--small': small}" :class="{
'modal--hide-header': hideHeader || fullscreen,
'modal--fullscreen': fullscreen,
'modal--small': small,
}"
class="modal" class="modal"
> >
<div class="modal__header"> <div class="modal__header">
@ -9,20 +13,14 @@
</div> </div>
<div class="modal__body"> <div class="modal__body">
<slot /> <slot />
<div <div class="modal__close-button" @click="hideModal">
class="modal__close-button"
@click="hideModal"
>
<cross class="modal__close-icon" /> <cross class="modal__close-icon" />
</div> </div>
</div> </div>
<div class="modal__footer"> <div class="modal__footer">
<slot name="footer"> <slot name="footer">
<!--<a class="button button&#45;&#45;active">Speichern</a>--> <!--<a class="button button&#45;&#45;active">Speichern</a>-->
<a <a class="button" @click="hideModal">Abbrechen</a>
class="button"
@click="hideModal"
>Abbrechen</a>
</slot> </slot>
</div> </div>
</div> </div>
@ -30,160 +28,159 @@
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon')); const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'));
export default { export default {
props: { props: {
hideHeader: { hideHeader: {
type: Boolean, type: Boolean,
default: false default: false,
},
fullscreen: {
type: Boolean,
default: false
},
small: {
type: Boolean,
default: false
}
}, },
fullscreen: {
components: { type: Boolean,
Cross default: false,
}, },
small: {
type: Boolean,
default: false,
},
},
methods: { components: {
hideModal() { Cross,
this.$store.dispatch('hideModal'); },
}
} methods: {
}; hideModal() {
this.$store.dispatch('hideModal');
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.modal { .modal {
align-self: center; align-self: center;
justify-self: center; justify-self: center;
width: 700px; width: 700px;
height: 80vh; height: 80vh;
background-color: $color-white; background-color: $color-white;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15); box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
border: 1px solid $color-silver-light; border: 1px solid $color-silver-light;
display: -ms-grid; display: -ms-grid;
@supports (display: grid) {
display: grid;
}
grid-template-rows: auto 1fr 65px;
grid-template-areas: 'header' 'body' 'footer';
-ms-grid-rows: auto 1fr 65px;
position: relative;
&__backdrop {
display: flex;
justify-content: center;
@supports (display: grid) { @supports (display: grid) {
display: grid; display: grid;
} }
grid-template-rows: auto 1fr 65px; position: fixed;
grid-template-areas: "header" "body" "footer"; top: 0;
-ms-grid-rows: auto 1fr 65px; left: 0;
position: relative; bottom: 0;
right: 0;
background-color: rgba($color-white, 0.8);
z-index: 90;
}
&__backdrop { &__header {
display: flex; grid-area: header;
justify-content: center; -ms-grid-row: 1;
@supports (display: grid) { padding: 10px $modal-lateral-padding;
display: grid; border-bottom: 1px solid $color-silver-light;
} }
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba($color-white, 0.8);
z-index: 90;
}
&__header { &__body {
grid-area: header; grid-area: body;
-ms-grid-row: 1; -ms-grid-row: 2;
padding: 10px $modal-lateral-padding; padding: 10px $modal-lateral-padding;
border-bottom: 1px solid $color-silver-light; overflow: auto;
} box-sizing: border-box;
min-height: 30vh;
}
&__body { &__close-button {
grid-area: body; display: none;
-ms-grid-row: 2; cursor: pointer;
padding: 10px $modal-lateral-padding; position: absolute;
overflow: auto; right: 15px;
box-sizing: border-box; top: 15px;
min-height: 30vh; background: rgba($color-white, 0.5);
} border-radius: 40px;
padding: 10px;
align-content: center;
}
&__close-button { &__footer {
grid-area: footer;
-ms-grid-row: 3;
border-top: 1px solid $color-silver-light;
padding: 16px $modal-lateral-padding;
}
$parent: &;
&--hide-header {
grid-template-rows: 1fr 65px;
grid-template-areas: 'body' 'footer';
#{$parent}__header {
display: none; display: none;
cursor: pointer;
position: absolute;
right: 15px;
top: 15px;
background: rgba($color-white, 0.5);
border-radius: 40px;
padding: 10px;
align-content: center;
} }
&__footer { #{$parent}__body {
grid-area: footer; padding: $default-padding;
-ms-grid-row: 3;
border-top: 1px solid $color-silver-light;
padding: 16px $modal-lateral-padding;
}
$parent: &;
&--hide-header {
grid-template-rows: 1fr 65px;
grid-template-areas: "body" "footer";
#{$parent}__header {
display: none;
}
#{$parent}__body {
padding: $default-padding;
}
}
&--fullscreen {
width: 95vw;
height: auto;
grid-template-rows: 1fr;
-ms-grid-rows: 1fr;
grid-template-areas: "body";
overflow: hidden;
#{$parent}__footer {
display: none;
}
#{$parent}__body {
padding: 0;
scrollbar-width: none;
margin-right: -5px;
height: auto;
max-height: 95vh;
&::-webkit-scrollbar {
display: none;
}
}
#{$parent}__close-button {
display: flex;
}
}
&--small {
height: auto;
#{$parent}__body {
min-height: 0;
}
} }
} }
&--fullscreen {
width: 95vw;
height: auto;
grid-template-rows: 1fr;
-ms-grid-rows: 1fr;
grid-template-areas: 'body';
overflow: hidden;
#{$parent}__footer {
display: none;
}
#{$parent}__body {
padding: 0;
scrollbar-width: none;
margin-right: -5px;
height: auto;
max-height: 95vh;
&::-webkit-scrollbar {
display: none;
}
}
#{$parent}__close-button {
display: flex;
}
}
&--small {
height: auto;
#{$parent}__body {
min-height: 0;
}
}
}
</style> </style>

View File

@ -1,68 +1,60 @@
<template> <template>
<div class="more-options"> <div class="more-options">
<a <a class="more-options__more-link" data-cy="more-options-link" @click.stop="showMenu = !showMenu">
class="more-options__more-link"
data-cy="more-options-link"
@click.stop="showMenu = !showMenu"
>
<ellipses class="more-options__ellipses" /> <ellipses class="more-options__ellipses" />
</a> </a>
<widget-popover <widget-popover class="more-options__popover" v-if="showMenu" @hide-me="showMenu = false">
class="more-options__popover"
v-if="showMenu"
@hide-me="showMenu = false"
>
<slot /> <slot />
</widget-popover> </widget-popover>
</div> </div>
</template> </template>
<script> <script>
import WidgetPopover from '@/components/ui/WidgetPopover'; import WidgetPopover from '@/components/ui/WidgetPopover';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Ellipses = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Ellipses.vue')); const Ellipses = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/Ellipses.vue'));
export default { export default {
components: { components: {
WidgetPopover, WidgetPopover,
Ellipses Ellipses,
}, },
data() { data() {
return { return {
showMenu: false showMenu: false,
}; };
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.more-options { .more-options {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
&__ellipses { &__ellipses {
width: 30px; width: 30px;
height: 30px; height: 30px;
fill: $color-charcoal-dark; fill: $color-charcoal-dark;
margin-top: -7px; margin-top: -7px;
}
&__more-link {
background-color: rgba($color-white, 0.9);
width: 35px;
height: 15px;
border-radius: 15px;
display: flex;
justify-content: center;
}
&__popover {
min-width: 200px;
@include popover-defaults();
}
} }
&__more-link {
background-color: rgba($color-white, 0.9);
width: 35px;
height: 15px;
border-radius: 15px;
display: flex;
justify-content: center;
}
&__popover {
min-width: 200px;
@include popover-defaults();
}
}
</style> </style>

View File

@ -1,79 +1,74 @@
<template> <template>
<transition name="fade"> <transition name="fade">
<a <a class="scroll-up" v-if="scroll > 200" @click="scrollTop">
class="scroll-up"
v-if="scroll>200"
@click="scrollTop"
>
<arrow-up class="scroll-up__icon" /> <arrow-up class="scroll-up__icon" />
</a> </a>
</transition> </transition>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const ArrowUp = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowUp')); const ArrowUp = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/ArrowUp'));
export default { export default {
components: { components: {
ArrowUp ArrowUp,
}, },
data() { data() {
return { return {
scroll: 0 scroll: 0,
}; };
}, },
mounted() { mounted() {
let html = document.scrollingElement; let html = document.scrollingElement;
document.body.onscroll = () => { document.body.onscroll = () => {
this.scroll = html.scrollTop; this.scroll = html.scrollTop;
}; };
}, },
unmounted() { unmounted() {
document.body.onscroll = null; document.body.onscroll = null;
}, },
methods: { methods: {
scrollTop() { scrollTop() {
document.scrollingElement.scrollTop = 0; document.scrollingElement.scrollTop = 0;
}
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import '@/styles/_variables.scss';
@import '@/styles/_mixins.scss'; @import '@/styles/_mixins.scss';
.scroll-up { .scroll-up {
position: fixed; position: fixed;
right: $large-spacing; right: $large-spacing;
bottom: $large-spacing; bottom: $large-spacing;
padding: $medium-spacing; padding: $medium-spacing;
border-radius: 100px; border-radius: 100px;
@include default-box-shadow; @include default-box-shadow;
cursor: pointer; cursor: pointer;
background-color: $color-white; background-color: $color-white;
border: 1px solid $color-silver; border: 1px solid $color-silver;
z-index: 2; z-index: 2;
&__icon {
width: 50px;
height: 50px;
fill: $color-brand;
}
&__icon {
width: 50px;
height: 50px;
fill: $color-brand;
} }
}
.fade-enter-active, .fade-leave-active { .fade-enter-active,
transition: opacity .3s; .fade-leave-active {
} transition: opacity 0.3s;
}
.fade-enter-from, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ .fade-enter-from, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
{ opacity: 0;
opacity: 0; }
}
</style> </style>

View File

@ -1,44 +1,43 @@
<template> <template>
<div class="submission-document"> <div class="submission-document">
<p <p class="submission-document__content content" v-if="document && document.length > 0">
class="submission-document__content content"
v-if="document && document.length > 0"
>
<document-icon class="content__icon" /><span class="content__text">{{ filename }}</span> <document-icon class="content__icon" /><span class="content__text">{{ filename }}</span>
</p> </p>
</div> </div>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
import filenameFromUrl from '@/helpers/urls'; import filenameFromUrl from '@/helpers/urls';
const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon')); const DocumentIcon = defineAsyncComponent(() =>
import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon')
);
export default { export default {
name: 'StudentSubmissionDocument', name: 'StudentSubmissionDocument',
props: ['document'], props: ['document'],
components: { DocumentIcon }, components: { DocumentIcon },
computed: { computed: {
filename() { filename() {
return filenameFromUrl(this.document); return filenameFromUrl(this.document);
}
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.content { .content {
display: flex; display: flex;
&__icon { &__icon {
width: 25px; width: 25px;
align-self: center; align-self: center;
}
&__text {
align-self: center;
padding-left: 5px;
}
} }
&__text {
align-self: center;
padding-left: 5px;
}
}
</style> </style>

View File

@ -1,82 +1,72 @@
<template> <template>
<div <div :class="{ 'user-widget--is-profile': isProfile }" class="user-widget" @click.stop="$emit('click', $event)">
:class="{'user-widget--is-profile': isProfile}" <div class="user-widget__avatar" data-cy="user-widget-avatar">
class="user-widget" <avatar :avatar-url="avatarUrl" :icon-highlighted="isProfile" />
@click.stop="$emit('click', $event)"
>
<div
class="user-widget__avatar"
data-cy="user-widget-avatar"
>
<avatar
:avatar-url="avatarUrl"
:icon-highlighted="isProfile"
/>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Avatar from '@/components/profile/Avatar'; import Avatar from '@/components/profile/Avatar';
export default { export default {
props: { props: {
avatarUrl: { avatarUrl: {
type: String type: String,
}
}, },
},
emits: ['click'], emits: ['click'],
components: { emits: ['click'],components: {
Avatar Avatar,
},
computed: {
isProfile() {
return this.$route.meta.isProfile;
}, },
computed: { },
isProfile() { };
return this.$route.meta.isProfile;
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.user-widget { .user-widget {
color: $color-silver-dark;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
// todo: do we need the margin right always? just do it where needed --> content block actions and objecives override this
margin-right: $medium-spacing;
&__popover {
top: 40px;
white-space: nowrap;
}
&__name {
padding: 0px $small-spacing;
color: $color-silver-dark; color: $color-silver-dark;
display: flex; font-family: $sans-serif-font-family;
justify-content: space-between; }
align-items: center;
position: relative;
// todo: do we need the margin right always? just do it where needed --> content block actions and objecives override this
margin-right: $medium-spacing;
&__popover { &__date {
top: 40px; font-family: $sans-serif-font-family;
white-space: nowrap; }
}
&__name { &__avatar {
padding: 0px $small-spacing; width: 30px;
color: $color-silver-dark; height: 30px;
font-family: $sans-serif-font-family; fill: $color-silver-dark;
} cursor: pointer;
}
&__date { &--is-profile {
font-family: $sans-serif-font-family; & > span {
} color: $color-brand;
&__avatar {
width: 30px;
height: 30px;
fill: $color-silver-dark;
cursor: pointer;
}
&--is-profile {
& > span {
color: $color-brand;
}
} }
} }
}
</style> </style>

View File

@ -1,12 +1,9 @@
<template> <template>
<nav <nav :class="{ 'content-navigation--sidebar': isSidebar }" class="content-navigation">
:class="{'content-navigation--sidebar': isSidebar}"
class="content-navigation"
>
<div class="content-navigation__primary"> <div class="content-navigation__primary">
<div class="content-navigation__item"> <div class="content-navigation__item">
<router-link <router-link
:class="{'content-navigation__link--active': isActive('book')}" :class="{ 'content-navigation__link--active': isActive('book') }"
:to="topicRoute" :to="topicRoute"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link" class="content-navigation__link"
@ -15,9 +12,7 @@
{{ $flavor.textTopics }} {{ $flavor.textTopics }}
</router-link> </router-link>
<topic-navigation <topic-navigation v-if="isSidebar" />
v-if="isSidebar"
/>
</div> </div>
<div class="content-navigation__item"> <div class="content-navigation__item">
@ -33,7 +28,7 @@
<div class="content-navigation__item"> <div class="content-navigation__item">
<router-link <router-link
:to="{name: 'news'}" :to="{ name: 'news' }"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link" class="content-navigation__link"
data-cy="news-navigation-link" data-cy="news-navigation-link"
@ -45,19 +40,14 @@
</div> </div>
</div> </div>
<router-link <router-link to="/" class="content-navigation__logo" data-cy="home-link" v-if="!isSidebar">
to="/"
class="content-navigation__logo"
data-cy="home-link"
v-if="!isSidebar"
>
<logo class="content-navigation__logo-icon" /> <logo class="content-navigation__logo-icon" />
</router-link> </router-link>
<div class="content-navigation__secondary"> <div class="content-navigation__secondary">
<div class="content-navigation__item content-navigation__item--secondary"> <div class="content-navigation__item content-navigation__item--secondary">
<router-link <router-link
:class="{'content-navigation__link--active': isRoomUrl()}" :class="{ 'content-navigation__link--active': isRoomUrl() }"
to="/rooms" to="/rooms"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link content-navigation__link--secondary" class="content-navigation__link content-navigation__link--secondary"
@ -67,10 +57,7 @@
</router-link> </router-link>
</div> </div>
<div <div class="content-navigation__item content-navigation__item--secondary" v-if="showPortfolio">
class="content-navigation__item content-navigation__item--secondary"
v-if="showPortfolio"
>
<router-link <router-link
to="/portfolio" to="/portfolio"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
@ -80,16 +67,13 @@
Portfolio Portfolio
</router-link> </router-link>
</div> </div>
<div <div class="content-navigation__item content-navigation__item--secondary" v-if="isSidebar">
class="content-navigation__item content-navigation__item--secondary"
v-if="isSidebar"
>
<a <a
:href="$flavor.supportLink" :href="$flavor.supportLink"
target="_blank" target="_blank"
class="content-navigation__link content-navigation__link--secondary" class="content-navigation__link content-navigation__link--secondary"
@click="close" @click="close"
>Support >Support
</a> </a>
</div> </div>
</div> </div>
@ -97,7 +81,7 @@
</template> </template>
<script> <script>
import TopicNavigation from '@/components/book-navigation/TopicNavigation'; import TopicNavigation from '@/components/book-navigation/TopicNavigation';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
import meMixin from '@/mixins/me'; import meMixin from '@/mixins/me';
@ -105,134 +89,135 @@
const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo')); const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo'));
export default { export default {
props: { props: {
isSidebar: { isSidebar: {
default: false default: false,
}
}, },
},
mixins: [sidebarMixin, meMixin], mixins: [sidebarMixin, meMixin],
components: { components: {
TopicNavigation, TopicNavigation,
Logo Logo,
},
computed: {
showPortfolio() {
return this.$flavor.showPortfolio;
}, },
},
computed: { methods: {
showPortfolio() { isActive(linkName) {
return this.$flavor.showPortfolio; return linkName === 'book' && this.$route.path.indexOf('module') > -1;
}
}, },
isRoomUrl() {
methods: { return this.$route.path.indexOf('room') > -1;
isActive(linkName) { },
return linkName === 'book' && this.$route.path.indexOf('module') > -1; close() {
}, this.closeSidebar('navigation');
isRoomUrl() { },
return this.$route.path.indexOf('room') > -1; },
}, };
close() {
this.closeSidebar('navigation');
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.content-navigation { .content-navigation {
display: flex; display: flex;
align-items: center; align-items: center;
&__link { &__link {
padding: 0 24px; padding: 0 24px;
@include navigation-link; @include navigation-link;
} }
&__primary, &__secondary { &__primary,
display: none; &__secondary {
flex-direction: row; display: none;
flex-direction: row;
@include desktop { @include desktop {
display: flex;
}
}
&__logo {
color: #17A887;
font-size: 36px;
font-weight: 800;
font-family: $sans-serif-font-family;
display: flex; display: flex;
justify-self: center;
/*
* For IE10+
*/
-ms-grid-column: 2;
-ms-grid-row-align: center;
-ms-grid-column-align: center;
}
&__logo-icon {
width: auto;
height: 31px;
}
&__link {
&--secondary {
@include regular-text;
}
&--active {
color: $color-brand;
}
}
$parent: &;
&--sidebar {
flex-direction: column;
#{$parent}__primary, #{$parent}__secondary {
display: flex;
flex-direction: column;
width: 100%;
}
#{$parent}__link {
@include heading-4;
line-height: 2.5em;
padding: 0;
display: block;
margin-bottom: 0.5*$small-spacing;
&:only-child {
margin-bottom: 0;
}
}
#{$parent}__item {
width: 100%;
//border-bottom: 1px solid $color-white;
/*&:nth-child(1) {*/
/* order: 3;*/
/* border-bottom: 0;*/
/*}*/
/*&:nth-child(2) {*/
/* order: 1;*/
/*}*/
/*&:nth-child(3) {*/
/* order: 2;*/
/*}*/
}
} }
} }
&__logo {
color: #17a887;
font-size: 36px;
font-weight: 800;
font-family: $sans-serif-font-family;
display: flex;
justify-self: center;
/*
* For IE10+
*/
-ms-grid-column: 2;
-ms-grid-row-align: center;
-ms-grid-column-align: center;
}
&__logo-icon {
width: auto;
height: 31px;
}
&__link {
&--secondary {
@include regular-text;
}
&--active {
color: $color-brand;
}
}
$parent: &;
&--sidebar {
flex-direction: column;
#{$parent}__primary,
#{$parent}__secondary {
display: flex;
flex-direction: column;
width: 100%;
}
#{$parent}__link {
@include heading-4;
line-height: 2.5em;
padding: 0;
display: block;
margin-bottom: 0.5 * $small-spacing;
&:only-child {
margin-bottom: 0;
}
}
#{$parent}__item {
width: 100%;
//border-bottom: 1px solid $color-white;
/*&:nth-child(1) {*/
/* order: 3;*/
/* border-bottom: 0;*/
/*}*/
/*&:nth-child(2) {*/
/* order: 1;*/
/*}*/
/*&:nth-child(3) {*/
/* order: 2;*/
/*}*/
}
}
}
</style> </style>

View File

@ -1,18 +1,8 @@
<template> <template>
<transition name="slide"> <transition name="slide">
<div <div class="navigation-sidebar" v-if="sidebar.navigation" v-click-outside="close">
class="navigation-sidebar" <content-navigation :is-sidebar="true" class="navigation-sidebar__main" />
v-if="sidebar.navigation" <div class="navigation-sidebar__close-button" @click="close">
v-click-outside="close"
>
<content-navigation
:is-sidebar="true"
class="navigation-sidebar__main"
/>
<div
class="navigation-sidebar__close-button"
@click="close"
>
<cross class="navigation-sidebar__close-icon" /> <cross class="navigation-sidebar__close-icon" />
</div> </div>
</div> </div>
@ -20,94 +10,95 @@
</template> </template>
<script> <script>
import ContentNavigation from '@/components/book-navigation/ContentNavigation'; import ContentNavigation from '@/components/book-navigation/ContentNavigation';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon')); const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'));
export default { export default {
mixins: [sidebarMixin], mixins: [sidebarMixin],
components: { components: {
ContentNavigation, ContentNavigation,
Cross Cross,
},
methods: {
close() {
this.closeSidebar('navigation');
}, },
},
methods: { };
close() {
this.closeSidebar('navigation');
}
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
$desktop-width: 285px; $desktop-width: 285px;
.navigation-sidebar { .navigation-sidebar {
position: fixed; position: fixed;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
top: 0; top: 0;
background-color: white; background-color: white;
z-index: 20; z-index: 20;
@include desktop { @include desktop {
box-shadow: 0px 2px 9px rgba(0, 0, 0, 0.12); box-shadow: 0px 2px 9px rgba(0, 0, 0, 0.12);
}
display: grid;
grid-template-columns: 1fr 50px;
grid-template-rows: 50px max-content auto 100px;
grid-template-areas: "m m" "m m" "s s" "s s";
&--with-subnavigation {
grid-template-areas: "m m" "m m" "sub sub" "s s";
}
height: 100vh;
overflow-y: auto;
@include desktop {
width: $desktop-width;
}
&__main {
padding: $medium-spacing;
grid-area: m;
}
&__main-link {
}
&__close-button {
grid-row: 1;
grid-column: 2;
align-self: center;
justify-self: center;
cursor: pointer;
}
} }
.slide { display: grid;
&-enter-active, &-leave-active {
transition: left 0.2s;
}
&-enter-from, &-leave-to { grid-template-columns: 1fr 50px;
left: -100vw; grid-template-rows: 50px max-content auto 100px;
@include desktop {
left: -$desktop-width; grid-template-areas: 'm m' 'm m' 's s' 's s';
}
&--with-subnavigation {
grid-template-areas: 'm m' 'm m' 'sub sub' 's s';
}
height: 100vh;
overflow-y: auto;
@include desktop {
width: $desktop-width;
}
&__main {
padding: $medium-spacing;
grid-area: m;
}
&__main-link {
}
&__close-button {
grid-row: 1;
grid-column: 2;
align-self: center;
justify-self: center;
cursor: pointer;
}
}
.slide {
&-enter-active,
&-leave-active {
transition: left 0.2s;
}
&-enter-from,
&-leave-to {
left: -100vw;
@include desktop {
left: -$desktop-width;
} }
} }
}
</style> </style>

View File

@ -1,21 +1,11 @@
<template> <template>
<div <div :class="{ 'sub-navigation-item--active': show }" class="sub-navigation-item" v-click-outside="close">
:class="{ 'sub-navigation-item--active': show}" <div class="sub-navigation-item__title" @click="show = !show">
class="sub-navigation-item"
v-click-outside="close"
>
<div
class="sub-navigation-item__title"
@click="show = !show"
>
{{ title }} {{ title }}
<chevron-down class="sub-navigation-item__icon sub-navigation-item__chevron-down" /> <chevron-down class="sub-navigation-item__icon sub-navigation-item__chevron-down" />
<chevron-up class="sub-navigation-item__icon sub-navigation-item__chevron-up" /> <chevron-up class="sub-navigation-item__icon sub-navigation-item__chevron-up" />
</div> </div>
<div <div class="sub-navigation-item__nav-items book-subnavigation" v-if="show">
class="sub-navigation-item__nav-items book-subnavigation"
v-if="show"
>
<slot /> <slot />
</div> </div>
</div> </div>
@ -26,30 +16,30 @@
const ChevronDown = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown')); const ChevronDown = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown'));
const ChevronUp = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronUp')); const ChevronUp = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronUp'));
export default { export default {
props: ['title'], props: ['title'],
components: { components: {
ChevronDown, ChevronDown,
ChevronUp ChevronUp,
},
data() {
return {
show: false,
};
},
watch: {
$route() {
this.show = false;
}, },
},
data() { methods: {
return { close() {
show: false this.show = false;
};
}, },
},
watch: { };
$route() {
this.show = false;
}
},
methods: {
close() {
this.show = false;
}
}
};
</script> </script>

View File

@ -1,8 +1,8 @@
<template> <template>
<nav class="topic-navigation"> <nav class="topic-navigation">
<router-link <router-link
:to="{name: 'topic', params: {topicSlug: topic.slug}}" :to="{ name: 'topic', params: { topicSlug: topic.slug } }"
:class="{'topic-navigation__topic--active': topic.active, 'book-subnavigation__item--mobile': mobile}" :class="{ 'topic-navigation__topic--active': topic.active, 'book-subnavigation__item--mobile': mobile }"
tag="div" tag="div"
active-class="book-subnavigation__item--active" active-class="book-subnavigation__item--active"
class="topic-navigation__topic book-subnavigation__item" class="topic-navigation__topic book-subnavigation__item"
@ -17,8 +17,8 @@
</template> </template>
<script> <script>
import ALL_TOPICS_QUERY from '@/graphql/gql/queries/allTopicsQuery.gql'; import ALL_TOPICS_QUERY from '@/graphql/gql/queries/allTopicsQuery.gql';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
export default { export default {
props: { props: {
@ -26,40 +26,42 @@
default: false, default: false,
}, },
}, },
},
mixins: [sidebarMixin], mixins: [sidebarMixin],
data() { data() {
return { return {
topics: [], topics: [],
}; };
}, },
methods: { methods: {
topicId(id) { topicId(id) {
return atob(id); return atob(id);
}, },
}, },
},
apollo: { apollo: {
topics: { topics: {
query: ALL_TOPICS_QUERY, query: ALL_TOPICS_QUERY,
manual: true, manual: true,
result({data, loading}) { result({ data, loading }) {
if (!loading) { if (!loading) {
this.topics = this.$getRidOfEdges(data).topics; this.topics = this.$getRidOfEdges(data).topics;
} }
},
}, },
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import "~styles/helpers";
.topic-navigation { .topic-navigation {
&__topic { &__topic {
}
} }
}
</style> </style>

View File

@ -26,14 +26,10 @@
:class="['content-element__component']" :class="['content-element__component']"
v-bind="element" v-bind="element"
:is="component" :is="component"
@change-text="changeText" @change-text="changeText"
@link-change-url="changeUrl" @link-change-url="changeUrl"
@change-url="changeUrl" @change-url="changeUrl"
@switch-to-document="switchToDocument" @switch-to-document="switchToDocument"
@assignment-change-title="changeAssignmentTitle" @assignment-change-title="changeAssignmentTitle"
@assignment-change-assignment="changeAssignmentAssignment" @assignment-change-assignment="changeAssignmentAssignment"
/> />
@ -67,289 +63,292 @@
const ThinglinkBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ThinglinkBlock')); const ThinglinkBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ThinglinkBlock'));
const InfogramBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/InfogramBlock')); const InfogramBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/InfogramBlock'));
const CHOOSER = 'content-block-element-chooser-widget'; const CHOOSER = 'content-block-element-chooser-widget';
export default { export default {
props: { props: {
element: { element: {
type: Object, type: Object,
default: null, default: null,
},
// is this element at the top level, or is it nested? we assume top level
topLevel: {
type: Boolean,
default: true,
},
firstElement: {
type: Boolean,
required: true,
},
lastElement: {
type: Boolean,
required: true,
},
}, },
// is this element at the top level, or is it nested? we assume top level
components: { topLevel: {
ContentElementActions, type: Boolean,
ContentFormSection, default: true,
TrashIcon,
ContentBlockElementChooserWidget,
LinkForm,
VideoForm,
ImageForm,
DocumentForm,
AssignmentForm,
TextForm,
SubtitleForm,
SurveyBlock,
Solution,
ImageBlock,
Instruction,
ModuleRoomSlug,
CmsDocumentBlock,
InfogramBlock,
ThinglinkBlock,
Assignment
}, },
firstElement: {
computed: { type: Boolean,
actions() { required: true,
return {
up: !this.firstElement,
down: !this.lastElement,
extended: this.topLevel,
};
},
isChooser() {
return this.component === CHOOSER;
},
type() {
return this.getType(this.element);
},
component() {
return this.type.component;
},
title() {
return this.type.title;
},
icon() {
return this.type.icon;
},
}, },
lastElement: {
type: Boolean,
required: true,
},
},
methods: { components: {
getType(element) { ContentElementActions,
switch (element.type) { ContentFormSection,
case 'subtitle': TrashIcon,
return { ContentBlockElementChooserWidget,
component: 'subtitle-form', LinkForm,
title: 'Untertitel', VideoForm,
icon: 'title-icon', ImageForm,
}; DocumentForm,
case 'link_block': AssignmentForm,
return { TextForm,
component: 'link-form', SubtitleForm,
title: 'Link', SurveyBlock,
icon: 'link-icon', Solution,
}; ImageBlock,
case 'video_block': Instruction,
return { ModuleRoomSlug,
component: 'video-form', CmsDocumentBlock,
title: 'Video', InfogramBlock,
icon: 'video-icon', ThinglinkBlock,
}; Assignment,
case 'image_url_block': },
return {
component: 'image-form',
title: 'Bild',
icon: 'image-icon',
};
case 'text_block':
return {
component: 'text-form',
title: 'Text',
icon: 'text-icon',
};
case 'assignment':
return {
component: element.id ? 'assignment' : 'assignment-form', // prevent editing of existing assignments
title: 'Aufgabe & Ergebnis',
icon: 'speech-bubble-icon',
};
case 'document_block':
return {
component: 'document-form',
title: 'Dokument',
icon: 'document-icon',
};
case 'survey':
return {
component: 'survey-block',
title: 'Übung',
};
case 'solution':
return {
component: 'solution',
title: 'Lösung',
};
case 'image_block':
return {
component: 'image-block',
title: 'Bild',
};
case 'instruction':
return {
component: 'instruction',
title: 'Instruktion',
};
case 'module_room_slug':
return {
component: 'module-room-slug',
title: 'Raum',
};
case 'cms_document_block':
return {
component: 'cms-document-block',
title: 'Dokument',
};
case 'thinglink_block':
return {
component: 'thinglink-block',
title: 'Interaktive Grafik'
};
case 'infogram_block':
return {
component: 'infogram-block',
title: 'Interaktive Grafik'
};
}
return {
component: CHOOSER,
title: '',
icon: '',
};
},
_updateProperty(value, key) {
// const content = this.localContentBlock.contents[index];
const content = this.element;
this.update({
...content,
value: {
...content.value,
[key]: value,
},
});
},
changeUrl(value) {
this._updateProperty(value, 'url');
},
changeText(value) {
this._updateProperty(value, 'text');
},
changeAssignmentTitle(value) {
this._updateProperty(value, 'title');
},
changeAssignmentAssignment(value) {
this._updateProperty(value, 'assignment');
},
changeType({type, convertToList}, value) {
let el = {
type: type,
value: Object.assign({}, value),
};
switch (type) {
case 'subtitle':
el = {
...el,
value: {
text: '',
},
};
break;
case 'text_block':
el = {
...el,
value: {
text: '',
},
};
break;
case 'link_block':
el = {
...el,
value: {
text: '',
url: '',
},
};
break;
case 'video_block':
el = {
...el,
value: {
url: '',
},
};
break;
case 'document_block':
el = {
...el,
value: Object.assign({
url: '',
}, value),
};
break;
case 'image_url_block':
el = {
...el,
value: {
url: '',
},
};
break;
}
if (convertToList) { computed: {
el = { actions() {
type: 'content_list_item', return {
contents: [el], up: !this.firstElement,
down: !this.lastElement,
extended: this.topLevel,
};
},
isChooser() {
return this.component === CHOOSER;
},
type() {
return this.getType(this.element);
},
component() {
return this.type.component;
},
title() {
return this.type.title;
},
icon() {
return this.type.icon;
},
},
methods: {
getType(element) {
switch (element.type) {
case 'subtitle':
return {
component: 'subtitle-form',
title: 'Untertitel',
icon: 'title-icon',
}; };
} case 'link_block':
this.update(el); return {
}, component: 'link-form',
update(element) { title: 'Link',
this.$emit('update', element); icon: 'link-icon',
}, };
switchToDocument(value) { case 'video_block':
this.changeType('document_block', value); return {
}, component: 'video-form',
title: 'Video',
icon: 'video-icon',
};
case 'image_url_block':
return {
component: 'image-form',
title: 'Bild',
icon: 'image-icon',
};
case 'text_block':
return {
component: 'text-form',
title: 'Text',
icon: 'text-icon',
};
case 'assignment':
return {
component: element.id ? 'assignment' : 'assignment-form', // prevent editing of existing assignments
title: 'Aufgabe & Ergebnis',
icon: 'speech-bubble-icon',
};
case 'document_block':
return {
component: 'document-form',
title: 'Dokument',
icon: 'document-icon',
};
case 'survey':
return {
component: 'survey-block',
title: 'Übung',
};
case 'solution':
return {
component: 'solution',
title: 'Lösung',
};
case 'image_block':
return {
component: 'image-block',
title: 'Bild',
};
case 'instruction':
return {
component: 'instruction',
title: 'Instruktion',
};
case 'module_room_slug':
return {
component: 'module-room-slug',
title: 'Raum',
};
case 'cms_document_block':
return {
component: 'cms-document-block',
title: 'Dokument',
};
case 'thinglink_block':
return {
component: 'thinglink-block',
title: 'Interaktive Grafik',
};
case 'infogram_block':
return {
component: 'infogram-block',
title: 'Interaktive Grafik',
};
}
return {
component: CHOOSER,
title: '',
icon: '',
};
}, },
}; _updateProperty(value, key) {
// const content = this.localContentBlock.contents[index];
const content = this.element;
this.update({
...content,
value: {
...content.value,
[key]: value,
},
});
},
changeUrl(value) {
this._updateProperty(value, 'url');
},
changeText(value) {
this._updateProperty(value, 'text');
},
changeAssignmentTitle(value) {
this._updateProperty(value, 'title');
},
changeAssignmentAssignment(value) {
this._updateProperty(value, 'assignment');
},
changeType({ type, convertToList }, value) {
let el = {
type: type,
value: Object.assign({}, value),
};
switch (type) {
case 'subtitle':
el = {
...el,
value: {
text: '',
},
};
break;
case 'text_block':
el = {
...el,
value: {
text: '',
},
};
break;
case 'link_block':
el = {
...el,
value: {
text: '',
url: '',
},
};
break;
case 'video_block':
el = {
...el,
value: {
url: '',
},
};
break;
case 'document_block':
el = {
...el,
value: Object.assign(
{
url: '',
},
value
),
};
break;
case 'image_url_block':
el = {
...el,
value: {
url: '',
},
};
break;
}
if (convertToList) {
el = {
type: 'content_list_item',
contents: [el],
};
}
this.update(el);
},
update(element) {
this.$emit('update', element);
},
switchToDocument(value) {
this.changeType('document_block', value);
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.content-element { .content-element {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&__actions { &__actions {
display: inline-flex; display: inline-flex;
justify-self: flex-end; justify-self: flex-end;
align-self: flex-end; align-self: flex-end;
}
&__section {
display: grid;
//grid-template-columns: 1fr 50px;
grid-auto-rows: auto;
/*width: 95%; // reserve space for scrollbar*/
}
&__chooser {
grid-column: 1 / span 2;
}
} }
&__section {
display: grid;
//grid-template-columns: 1fr 50px;
grid-auto-rows: auto;
/*width: 95%; // reserve space for scrollbar*/
}
&__chooser {
grid-column: 1 / span 2;
}
}
</style> </style>

View File

@ -1,13 +1,8 @@
<template> <template>
<div class="content-form-section"> <div class="content-form-section">
<h2 class="content-form-section__heading"> <h2 class="content-form-section__heading">
<component <component class="content-form-section__icon" :is="icon" />
class="content-form-section__icon" <span class="content-form-section__title" data-cy="content-form-section-title">{{ title }}</span>
:is="icon"
/> <span
class="content-form-section__title"
data-cy="content-form-section-title"
>{{ title }}</span>
</h2> </h2>
<content-element-actions <content-element-actions
@ -55,46 +50,46 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.content-form-section { .content-form-section {
@include default-box-shadow; @include default-box-shadow;
border-radius: $default-border-radius; border-radius: $default-border-radius;
padding: $small-spacing $medium-spacing; padding: $small-spacing $medium-spacing;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-template-rows: auto; grid-template-rows: auto;
grid-template-areas: 'h a' 'c c'; grid-template-areas: 'h a' 'c c';
align-items: center;
grid-row-gap: $medium-spacing;
&__heading {
display: flex;
align-items: center; align-items: center;
grid-row-gap: $medium-spacing; grid-area: h;
margin-bottom: 0;
&__heading {
display: flex;
align-items: center;
grid-area: h;
margin-bottom: 0;
}
&__actions {
grid-area: a;
justify-self: end;
}
&__title {
@include heading-3;
margin-bottom: 0;
}
&__icon {
width: 28px;
height: 28px;
margin-right: $small-spacing;
}
&__content {
grid-area: c;
}
} }
&__actions {
grid-area: a;
justify-self: end;
}
&__title {
@include heading-3;
margin-bottom: 0;
}
&__icon {
width: 28px;
height: 28px;
margin-right: $small-spacing;
}
&__content {
grid-area: c;
}
}
</style> </style>

View File

@ -18,41 +18,23 @@
/> />
</template> </template>
<add-content-element <add-content-element :index="-1" class="contents-form__add" @add-element="addElement" />
:index="-1" <div class="contents-form__element" v-for="(element, index) in localContentBlock.contents" :key="index">
class="contents-form__add" <content-element :element="element" @update="update(index, $event)" @remove="remove(index)" />
@add-element="addElement"
/>
<div
class="contents-form__element"
v-for="(element, index) in localContentBlock.contents"
:key="index"
>
<content-element
:element="element"
@update="update(index, $event)"
@remove="remove(index)"
/>
<add-content-element <add-content-element :index="index" class="contents-form__add" @add-element="addElement" />
:index="index"
class="contents-form__add"
@add-element="addElement"
/>
</div> </div>
<template #footer> <template #footer>
<div> <div>
<a <a
:class="{'button--disabled': disableSave}" :class="{ 'button--disabled': disableSave }"
class="button button--primary" class="button button--primary"
data-cy="modal-save-button" data-cy="modal-save-button"
@click="save" @click="save"
>Speichern</a> >Speichern</a
<a >
class="button" <a class="button" @click="$emit('hide')">Abbrechen</a>
@click="$emit('hide')"
>Abbrechen</a>
</div> </div>
</template> </template>
</modal> </modal>
@ -69,116 +51,117 @@
const Modal = defineAsyncComponent(() => import('@/components/Modal')); const Modal = defineAsyncComponent(() => import('@/components/Modal'));
const Checkbox = defineAsyncComponent(() => import('@/components/ui/Checkbox')); const Checkbox = defineAsyncComponent(() => import('@/components/ui/Checkbox'));
export default { export default {
props: { props: {
contentBlock: Object, contentBlock: Object,
blockType: { blockType: {
type: String, type: String,
default: 'ContentBlock', default: 'ContentBlock',
},
showTaskSelection: {
type: Boolean,
default: false,
},
disableSave: {
type: Boolean,
default: false,
},
}, },
showTaskSelection: {
components: { type: Boolean,
ContentElement, default: false,
Modal,
ModalInput,
AddContentElement,
Checkbox,
}, },
disableSave: {
type: Boolean,
default: false,
},
},
data() { components: {
return { ContentElement,
error: false, Modal,
localContentBlock: Object.assign({}, { ModalInput,
AddContentElement,
Checkbox,
},
data() {
return {
error: false,
localContentBlock: Object.assign(
{},
{
title: this.contentBlock.title, title: this.contentBlock.title,
contents: [...this.contentBlock.contents], contents: [...this.contentBlock.contents],
id: this.contentBlock.id || undefined, id: this.contentBlock.id || undefined,
isAssignment: this.contentBlock.type && this.contentBlock.type.toLowerCase() === 'task', isAssignment: this.contentBlock.type && this.contentBlock.type.toLowerCase() === 'task',
}),
me: {},
};
},
apollo: {
me: meQuery,
},
computed: {
titlePlaceholder() {
return this.blockType === 'RoomEntry' ? 'Titel für Raumeintrag erfassen' : 'Titel für Inhaltsblock erfassen';
},
taskSelection() {
return this.showTaskSelection && this.me.permissions.includes('users.can_manage_school_class_content');
},
},
methods: {
setContentBlockType(checked) {
this.localContentBlock.isAssignment = checked;
},
update(index, element) {
this.localContentBlock.contents.splice(index, 1, element);
},
save() {
if (!this.disableSave) {
if (!this.localContentBlock.title) {
this.error = true;
return false;
}
this.$emit('save', this.localContentBlock);
} }
}, ),
updateTitle(title) { me: {},
this.localContentBlock.title = title; };
this.error = false; },
},
addElement(index) {
this.localContentBlock.contents.splice(index + 1, 0, {
hideAssignment: this.blockType !== 'ContentBlock',
});
},
remove(index) {
this.localContentBlock.contents.splice(index, 1);
},
apollo: {
me: meQuery,
},
computed: {
titlePlaceholder() {
return this.blockType === 'RoomEntry' ? 'Titel für Raumeintrag erfassen' : 'Titel für Inhaltsblock erfassen';
}, },
}; taskSelection() {
return this.showTaskSelection && this.me.permissions.includes('users.can_manage_school_class_content');
},
},
methods: {
setContentBlockType(checked) {
this.localContentBlock.isAssignment = checked;
},
update(index, element) {
this.localContentBlock.contents.splice(index, 1, element);
},
save() {
if (!this.disableSave) {
if (!this.localContentBlock.title) {
this.error = true;
return false;
}
this.$emit('save', this.localContentBlock);
}
},
updateTitle(title) {
this.localContentBlock.title = title;
this.error = false;
},
addElement(index) {
this.localContentBlock.contents.splice(index + 1, 0, {
hideAssignment: this.blockType !== 'ContentBlock',
});
},
remove(index) {
this.localContentBlock.contents.splice(index, 1);
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.contents-form { .contents-form {
/* top level does not exist, because of the modal */ /* top level does not exist, because of the modal */
&__element { &__element {
}
&__element-component {
margin-bottom: 25px;
}
&__remove {
}
&__trash-icon {
}
&__add {
grid-column: 1 / span 2;
}
&__task {
margin: 15px 0 10px;
}
} }
&__element-component {
margin-bottom: 25px;
}
&__remove {
}
&__trash-icon {
}
&__add {
grid-column: 1 / span 2;
}
&__task {
margin: 15px 0 10px;
}
}
</style> </style>

View File

@ -1,14 +1,9 @@
<template> <template>
<contents-form <contents-form :content-block="contentBlock" :show-task-selection="true" @save="saveContentBlock" @hide="hideModal" />
:content-block="contentBlock"
:show-task-selection="true"
@save="saveContentBlock"
@hide="hideModal"
/>
</template> </template>
<script> <script>
import ContentsForm from '@/components/content-block-form/ContentsForm'; import ContentsForm from '@/components/content-block-form/ContentsForm';
import {store} from '@/store'; import {store} from '@/store';
@ -28,17 +23,18 @@
}; };
}, },
created() { created() {
// debugger; // debugger;
}, },
methods: { methods: {
hideModal() { hideModal() {
this.$store.dispatch('resetCurrentNoteBlock'); this.$store.dispatch('resetCurrentNoteBlock');
this.$store.dispatch('hideModal'); this.$store.dispatch('hideModal');
}, },
saveContentBlock(contentBlock) { saveContentBlock(contentBlock) {
this.$apollo.mutate({ this.$apollo
.mutate({
mutation: EDIT_CONTENT_BLOCK_MUTATION, mutation: EDIT_CONTENT_BLOCK_MUTATION,
variables: { variables: {
input: { input: {
@ -61,6 +57,7 @@
}); });
}, },
}, },
},
apollo: { apollo: {
contentBlock() { contentBlock() {

View File

@ -1,9 +1,5 @@
<template> <template>
<div <div :class="componentClass" :data-scrollto="component.id" data-cy="content-component">
:class="componentClass"
:data-scrollto="component.id"
data-cy="content-component"
>
<bookmark-actions <bookmark-actions
:bookmarked="bookmarked" :bookmarked="bookmarked"
:note="note" :note="note"
@ -13,11 +9,7 @@
@edit-note="editNote" @edit-note="editNote"
@bookmark="bookmarkContent(component.id, !bookmarked)" @bookmark="bookmarkContent(component.id, !bookmarked)"
/> />
<component <component v-bind="component" :parent="parent" :is="component.type" />
v-bind="component"
:parent="parent"
:is="component.type"
/>
</div> </div>
</template> </template>
@ -50,60 +42,60 @@ export default {
props: { props: {
component: { component: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
}, },
parent: { parent: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
}, },
bookmarks: { bookmarks: {
type: Array, type: Array,
default: () => ([]) default: () => [],
}, },
notes: { notes: {
type: Array, type: Array,
default: () => ([]) default: () => [],
}, },
root: { root: {
type: String, type: String,
default: '' default: '',
}, },
editMode: { editMode: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
components: { components: {
'text_block': TextBlock, text_block: TextBlock,
'basic_knowledge': InstrumentWidget, // for legacy basic_knowledge: InstrumentWidget, // for legacy
'instrument': InstrumentWidget, instrument: InstrumentWidget,
'image_block': ImageBlock, image_block: ImageBlock,
'image_url_block': ImageUrlBlock, image_url_block: ImageUrlBlock,
'video_block': VideoBlock, video_block: VideoBlock,
'link_block': LinkBlock, link_block: LinkBlock,
'document_block': DocumentBlock, document_block: DocumentBlock,
'infogram_block': InfogramBlock, infogram_block: InfogramBlock,
'genially_block': GeniallyBlock, genially_block: GeniallyBlock,
'subtitle': SubtitleBlock, subtitle: SubtitleBlock,
'section_title': SectionTitleBlock, section_title: SectionTitleBlock,
'content_list': ContentListBlock, content_list: ContentListBlock,
'module_room_slug': ModuleRoomSlug, module_room_slug: ModuleRoomSlug,
'thinglink_block': ThinglinkBlock, thinglink_block: ThinglinkBlock,
'cms_document_block': CmsDocumentBlock, cms_document_block: CmsDocumentBlock,
Survey, Survey,
Solution, Solution,
Instruction, Instruction,
Assignment, Assignment,
BookmarkActions BookmarkActions,
}, },
computed: { computed: {
bookmarked() { bookmarked() {
return this.bookmarks && !!this.bookmarks.find(bookmark => bookmark.uuid === this.component.id); return this.bookmarks && !!this.bookmarks.find((bookmark) => bookmark.uuid === this.component.id);
}, },
note() { note() {
const bookmark = this.bookmarks && this.bookmarks.find(bookmark => bookmark.uuid === this.component.id); const bookmark = this.bookmarks && this.bookmarks.find((bookmark) => bookmark.uuid === this.component.id);
return bookmark && bookmark.note; return bookmark && bookmark.note;
}, },
showBookmarkActions() { showBookmarkActions() {
@ -115,18 +107,19 @@ export default {
classes.push('content-component--bookmarked'); classes.push('content-component--bookmarked');
} }
return classes; return classes;
} },
}, },
methods: { methods: {
addNote(id) { addNote(id) {
const type = Object.prototype.hasOwnProperty.call(this.parent, '__typename') const type = Object.prototype.hasOwnProperty.call(this.parent, '__typename')
? this.parent.__typename : 'ContentBlockNode'; ? this.parent.__typename
: 'ContentBlockNode';
this.$store.dispatch('addNote', { this.$store.dispatch('addNote', {
content: id, content: id,
type, type,
block: this.root block: this.root,
}); });
}, },
editNote() { editNote() {
@ -134,37 +127,36 @@ export default {
}, },
bookmarkContent(uuid, bookmarked) { bookmarkContent(uuid, bookmarked) {
this.$apollo.mutate(constructContentComponentBookmarkMutation(uuid, bookmarked, this.parent, this.root)); this.$apollo.mutate(constructContentComponentBookmarkMutation(uuid, bookmarked, this.parent, this.root));
} },
} },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~styles/helpers"; @import '~styles/helpers';
.content-component { .content-component {
position: relative; position: relative;
&--bookmarked { &--bookmarked {
}
&--subtitle {
margin-top: $section-spacing;
margin-bottom: $large-spacing;
}
&--section_title {
margin-top: $section-spacing;
margin-bottom: $large-spacing;
}
&--text_block {
margin-bottom: $large-spacing;
}
&--document_block {
margin-bottom: $large-spacing;
}
} }
&--subtitle {
margin-top: $section-spacing;
margin-bottom: $large-spacing;
}
&--section_title {
margin-top: $section-spacing;
margin-bottom: $large-spacing;
}
&--text_block {
margin-bottom: $large-spacing;
}
&--document_block {
margin-bottom: $large-spacing;
}
}
</style> </style>

View File

@ -1,12 +1,7 @@
<template> <template>
<content-list <content-list :items="contentBlocks">
:items="contentBlocks"
>
<template #default="{ item }"> <template #default="{ item }">
<content-block <content-block :content-block="item" :parent="parent" />
:content-block="item"
:parent="parent"
/>
</template> </template>
</content-list> </content-list>
</template> </template>

View File

@ -1,16 +1,8 @@
<template> <template>
<div class="document-block"> <div class="document-block">
<document-icon class="document-block__icon" /> <document-icon class="document-block__icon" />
<a <a :href="value.url" class="document-block__link" target="_blank">{{ urlName }}</a>
:href="value.url" <a class="document-block__remove" v-if="showTrashIcon" @click="$emit('trash')">
class="document-block__link"
target="_blank"
>{{ urlName }}</a>
<a
class="document-block__remove"
v-if="showTrashIcon"
@click="$emit('trash')"
>
<trash-icon class="document-block__trash-icon" /> <trash-icon class="document-block__trash-icon" />
</a> </a>
</div> </div>
@ -21,60 +13,60 @@
const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon')); const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon')); const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
export default { export default {
props: { props: {
value: Object, value: Object,
showTrashIcon: Boolean, showTrashIcon: Boolean,
}, },
components: { components: {
DocumentIcon, DocumentIcon,
TrashIcon, TrashIcon,
}, },
computed: { computed: {
urlName: function() { urlName: function () {
if (this.value && this.value.url) { if (this.value && this.value.url) {
const parts = this.value.url.split('/'); const parts = this.value.url.split('/');
return parts[parts.length - 1]; return parts[parts.length - 1];
}
return null;
} }
} return null;
}; },
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.document-block { .document-block {
display: grid; display: grid;
grid-template-columns: 50px 1fr 50px; grid-template-columns: 50px 1fr 50px;
align-items: center; align-items: center;
&__icon { &__icon {
width: 30px; width: 30px;
height: 30px; height: 30px;
}
&__link {
text-decoration: underline;
}
&__remove {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
}
&__trash-icon {
width: 25px;
height: 25px;
fill: $color-silver-dark;
cursor: pointer;
justify-self: center;
}
} }
&__link {
text-decoration: underline;
}
&__remove {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
}
&__trash-icon {
width: 25px;
height: 25px;
fill: $color-silver-dark;
cursor: pointer;
justify-self: center;
}
}
</style> </style>

View File

@ -1,57 +1,51 @@
<template> <template>
<div <div class="instruction" v-if="me.isTeacher">
class="instruction"
v-if="me.isTeacher"
>
<bulb-icon class="instruction__icon" /> <bulb-icon class="instruction__icon" />
<a <a :href="url" class="instruction__link">{{ text }}</a>
:href="url"
class="instruction__link"
>{{ text }}</a>
</div> </div>
</template> </template>
<script> <script>
import me from '@/mixins/me'; import me from '@/mixins/me';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const BulbIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/BulbIcon')); const BulbIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/BulbIcon'));
export default { export default {
props: ['value'], props: ['value'],
mixins: [me], mixins: [me],
components: { components: {
BulbIcon BulbIcon,
},
computed: {
text() {
return this.value.text ? this.value.text : 'Anweisungen';
}, },
url() {
computed: { return this.value.document ? this.value.document.url : this.value.url;
text() { },
return this.value.text ? this.value.text : 'Anweisungen'; },
}, };
url() {
return this.value.document ? this.value.document.url : this.value.url;
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.instruction { .instruction {
margin-bottom: 1rem; margin-bottom: 1rem;
display: flex; display: flex;
align-items: center; align-items: center;
&__icon { &__icon {
width: 40px; width: 40px;
height: 40px; height: 40px;
margin-right: $small-spacing; margin-right: $small-spacing;
}
&__link {
@include heading-3;
}
} }
&__link {
@include heading-3;
}
}
</style> </style>

View File

@ -1,60 +1,53 @@
<template> <template>
<div <div :class="{ 'link-block--no-margin': noMargin }" class="link-block">
:class="{ 'link-block--no-margin': noMargin}"
class="link-block"
>
<link-icon class="link-block__icon" /> <link-icon class="link-block__icon" />
<a <a :href="href" class="link-block__link" target="_blank">{{ value.text }}</a>
:href="href"
class="link-block__link"
target="_blank"
>{{ value.text }}</a>
</div> </div>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const LinkIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/LinkIcon')); const LinkIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/LinkIcon'));
export default { export default {
props: { props: {
value: Object, value: Object,
noMargin: { noMargin: {
default: false default: false,
}
}, },
},
components: { components: {
LinkIcon LinkIcon,
},
computed: {
href() {
const url = this.value.url;
return url.startsWith('http') ? this.value.url : `http://${this.value.url}`;
}, },
},
computed: { };
href() {
const url = this.value.url;
return url.startsWith('http') ? this.value.url : `http://${this.value.url}`;
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.link-block { .link-block {
margin-bottom: 30px; margin-bottom: 30px;
display: grid; display: grid;
grid-template-columns: 50px 1fr; grid-template-columns: 50px 1fr;
align-items: center; align-items: center;
&--no-margin { &--no-margin {
margin-bottom: 0; margin-bottom: 0;
}
&__icon {
width: 30px;
height: 30px;
}
&__link {
text-decoration: underline;
}
} }
&__icon {
width: 30px;
height: 30px;
}
&__link {
text-decoration: underline;
}
}
</style> </style>

View File

@ -1,109 +1,101 @@
<template> <template>
<div <div class="final-submission" data-cy="final-submission">
class="final-submission" <document-block :value="{ url: userInput.document }" class="final-submission__document" v-if="userInput.document" />
data-cy="final-submission"
>
<document-block
:value="{url: userInput.document}"
class="final-submission__document"
v-if="userInput.document"
/>
<div class="final-submission__explanation"> <div class="final-submission__explanation">
<info-icon class="final-submission__explanation-icon" /> <info-icon class="final-submission__explanation-icon" />
<span class="final-submission__explanation-text">{{ sharedMsg }}</span> <span class="final-submission__explanation-text">{{ sharedMsg }}</span>
<a <a class="final-submission__reopen" data-cy="final-submission-reopen" v-if="showReopen" @click="$emit('reopen')"
class="final-submission__reopen" >Bearbeiten</a
data-cy="final-submission-reopen" >
v-if="showReopen"
@click="$emit('reopen')"
>Bearbeiten</a>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {newLineToParagraph} from '@/helpers/text'; import { newLineToParagraph } from '@/helpers/text';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock')); const DocumentBlock = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/DocumentBlock')
);
const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon')); const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon'));
export default { export default {
props: { props: {
userInput: { userInput: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
},
showReopen: {
type: Boolean,
default: true
},
sharedMsg: {
type: String,
default: ''
}
}, },
showReopen: {
components: { type: Boolean,
InfoIcon, default: true,
DocumentBlock,
}, },
sharedMsg: {
type: String,
default: '',
},
},
computed: { components: {
text() { InfoIcon,
return newLineToParagraph(this.userInput.text); DocumentBlock,
} },
}
}; computed: {
text() {
return newLineToParagraph(this.userInput.text);
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.final-submission { .final-submission {
&__text { &__text {
background-color: $color-white; background-color: $color-white;
@include input-box-shadow; @include input-box-shadow;
border-radius: $input-border-radius; border-radius: $input-border-radius;
padding: 15px; padding: 15px;
font-size: toRem(17px); font-size: toRem(17px);
font-family: $sans-serif-font-family; font-family: $sans-serif-font-family;
margin-bottom: 20px; margin-bottom: 20px;
font-weight: $font-weight-regular; font-weight: $font-weight-regular;
overflow-wrap: break-word; overflow-wrap: break-word;
word-wrap: break-word; word-wrap: break-word;
hyphens: auto; hyphens: auto;
word-break: break-word; word-break: break-word;
}
&__document {
margin-bottom: $small-spacing;
}
&__explanation {
display: flex;
align-items: center;
}
&__explanation-icon {
width: 40px;
height: 40px;
fill: $color-brand;
margin-right: 8px;
}
&__explanation-text {
color: $color-brand;
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
margin-right: $medium-spacing;
}
&__reopen {
@include small-text;
cursor: pointer;
color: $color-charcoal-light;
}
} }
&__document {
margin-bottom: $small-spacing;
}
&__explanation {
display: flex;
align-items: center;
}
&__explanation-icon {
width: 40px;
height: 40px;
fill: $color-brand;
margin-right: 8px;
}
&__explanation-text {
color: $color-brand;
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
margin-right: $medium-spacing;
}
&__reopen {
@include small-text;
cursor: pointer;
color: $color-charcoal-light;
}
}
</style> </style>

View File

@ -10,10 +10,7 @@
/> />
</div> </div>
<div <div class="submission-form-container__actions" v-if="!isFinalOrReadOnly">
class="submission-form-container__actions"
v-if="!isFinalOrReadOnly"
>
<button <button
class="submission-form-container__submit button button--primary button--white-bg" class="submission-form-container__submit button button--primary button--white-bg"
data-cy="submission-form-submit" data-cy="submission-form-submit"
@ -29,11 +26,7 @@
> >
{{ spellcheckText }} {{ spellcheckText }}
</button> </button>
<file-upload <file-upload :document="userInput.document" v-if="allowsDocuments" @change-document-url="changeDocumentUrl" />
:document="userInput.document"
v-if="allowsDocuments"
@change-document-url="changeDocumentUrl"
/>
<slot /> <slot />
</div> </div>
@ -48,116 +41,113 @@
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const SubmissionInput = defineAsyncComponent(() => import('@/components/content-blocks/assignment/SubmissionInput')); const SubmissionInput = defineAsyncComponent(() => import('@/components/content-blocks/assignment/SubmissionInput'));
const FinalSubmission = defineAsyncComponent(() => import('@/components/content-blocks/assignment/FinalSubmission')); const FinalSubmission = defineAsyncComponent(() => import('@/components/content-blocks/assignment/FinalSubmission'));
const FileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/FileUpload')); const FileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/FileUpload'));
export default {
export default { props: {
props: { userInput: Object,
userInput: Object, saved: Boolean,
saved: Boolean, placeholder: String,
placeholder: String, action: String,
action: String, reopen: Function,
reopen: Function, document: String,
document: String, readOnly: {
readOnly: { type: Boolean,
type: Boolean, default: false,
default: false,
},
spellcheck: {
type: Boolean,
default: false,
},
spellcheckLoading: {
type: Boolean,
default: false,
},
sharedMsg: String,
}, },
spellcheck: {
components: { type: Boolean,
FileUpload, default: false,
SubmissionInput,
FinalSubmission,
}, },
spellcheckLoading: {
computed: { type: Boolean,
final() { default: false,
return !!this.userInput && this.userInput.final;
},
isFinalOrReadOnly() {
return this.final || this.readOnly;
},
allowsDocuments() {
return 'document' in this.userInput;
},
showSpellcheckButton() {
return this.spellcheck && process.env.VUE_APP_ENABLE_SPELLCHECK;
},
spellcheckText() {
if (!this.spellcheckLoading) {
return 'Rechtschreibung prüfen';
} else {
return 'Wird geprüft...';
}
},
}, },
sharedMsg: String,
},
methods: { components: {
reopenSubmission() { FileUpload,
this.$emit('reopen'); SubmissionInput,
}, FinalSubmission,
saveInput(input) { },
this.$emit('saveInput', input);
}, computed: {
changeDocumentUrl(documentUrl) { final() {
this.$emit('changeDocumentUrl', documentUrl); return !!this.userInput && this.userInput.final;
},
}, },
isFinalOrReadOnly() {
return this.final || this.readOnly;
},
allowsDocuments() {
return 'document' in this.userInput;
},
showSpellcheckButton() {
return this.spellcheck && process.env.VUE_APP_ENABLE_SPELLCHECK;
},
spellcheckText() {
if (!this.spellcheckLoading) {
return 'Rechtschreibung prüfen';
} else {
return 'Wird geprüft...';
}
},
},
}; methods: {
reopenSubmission() {
this.$emit('reopen');
},
saveInput(input) {
this.$emit('saveInput', input);
},
changeDocumentUrl(documentUrl) {
this.$emit('changeDocumentUrl', documentUrl);
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.submission-form-container { .submission-form-container {
@include form-with-border; @include form-with-border;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;
display: none; display: none;
@include desktop { @include desktop {
display: block; display: block;
} }
&__inputs { &__inputs {
margin-bottom: 12px; margin-bottom: 12px;
} }
&__submit { &__submit {
margin-right: $medium-spacing; margin-right: $medium-spacing;
} }
&__actions { &__actions {
display: flex; display: flex;
align-items: center; align-items: center;
} }
&__document { &__document {
&:hover { &:hover {
cursor: pointer; cursor: pointer;
}
}
&__spellcheck {
/* so the button does not change size when changing the text */
width: 235px;
text-align: center;
display: inline-block;
} }
} }
&__spellcheck {
/* so the button does not change size when changing the text */
width: 235px;
text-align: center;
display: inline-block;
}
}
</style> </style>

View File

@ -4,75 +4,73 @@
:placeholder="placeholder" :placeholder="placeholder"
:readonly="readonly" :readonly="readonly"
:value="inputText" :value="inputText"
:class="{'submission-form__textarea--readonly': readonly}" :class="{ 'submission-form__textarea--readonly': readonly }"
data-cy="submission-textarea" data-cy="submission-textarea"
rows="1" rows="1"
class="submission-form__textarea" class="submission-form__textarea"
v-auto-grow v-auto-grow
@input="$emit('input', $event.target.value)" @input="$emit('input', $event.target.value)"
/> />
<div <div class="submission-form__save-status submission-form__save-status--saved" v-if="saved">
class="submission-form__save-status submission-form__save-status--saved"
v-if="saved"
>
<tick-circle-icon class="submission-form__save-status-icon" /> <tick-circle-icon class="submission-form__save-status-icon" />
</div> </div>
<div <div class="submission-form__save-status submission-form__save-status--unsaved" v-if="!saved">
class="submission-form__save-status submission-form__save-status--unsaved"
v-if="!saved"
>
<loading-icon class="submission-form__save-status-icon submission-form__saving-icon" /> <loading-icon class="submission-form__save-status-icon submission-form__saving-icon" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const TickCircleIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TickCircleIcon')); const TickCircleIcon = defineAsyncComponent(() =>
const LoadingIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/LoadingIcon')); import(/* webpackChunkName: "icons" */ '@/components/icons/TickCircleIcon')
);
const LoadingIcon = defineAsyncComponent(() =>
import(/* webpackChunkName: "icons" */ '@/components/icons/LoadingIcon')
);
export default { export default {
props: { props: {
inputText: String, inputText: String,
saved: Boolean, saved: Boolean,
readonly: Boolean, readonly: Boolean,
placeholder: { placeholder: {
type: String, type: String,
default: 'Ergebnis erfassen' default: 'Ergebnis erfassen',
}
}, },
components: { },
TickCircleIcon, components: {
LoadingIcon TickCircleIcon,
} LoadingIcon,
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.submission-form { .submission-form {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
&__textarea { &__textarea {
@include borderless-textarea; @include borderless-textarea;
}
&__save-status {
position: relative;
align-items: center;
}
&__save-status-icon {
width: 22px;
height: 22px;
fill: $color-silver-dark;
}
&__saving-icon {
@include spin;
}
} }
&__save-status {
position: relative;
align-items: center;
}
&__save-status-icon {
width: 22px;
height: 22px;
fill: $color-silver-dark;
}
&__saving-icon {
@include spin;
}
}
</style> </style>

View File

@ -5,7 +5,7 @@
class="assignment-form__title skillbox-input" class="assignment-form__title skillbox-input"
placeholder="Aufgabentitel" placeholder="Aufgabentitel"
@input="$emit('assignment-change-title', $event.target.value, index)" @input="$emit('assignment-change-title', $event.target.value, index)"
> />
<textarea <textarea
:value="value.assignment" :value="value.assignment"
class="assignment-form__exercise-text skillbox-textarea" class="assignment-form__exercise-text skillbox-textarea"
@ -23,41 +23,41 @@
import {defineAsyncComponent} from 'vue'; import {defineAsyncComponent} from 'vue';
const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon')); const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon'));
export default { export default {
props: ['value', 'index'], props: ['value', 'index'],
components: { components: {
InfoIcon InfoIcon,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.assignment-form { .assignment-form {
display: grid; display: grid;
grid-auto-rows: auto; grid-auto-rows: auto;
grid-row-gap: 13px; grid-row-gap: 13px;
grid-template-columns: 40px 1fr; grid-template-columns: 40px 1fr;
grid-column-gap: 16px; grid-column-gap: 16px;
align-items: center; align-items: center;
&__title { &__title {
width: $modal-input-width; width: $modal-input-width;
grid-column: span 2; grid-column: span 2;
}
&__exercise-text {
width: $modal-input-width;
grid-column: span 2;
}
&__help-icon {
width: 40px;
height: 40px;
}
&__help-description {
}
} }
&__exercise-text {
width: $modal-input-width;
grid-column: span 2;
}
&__help-icon {
width: 40px;
height: 40px;
}
&__help-description {
}
}
</style> </style>

View File

@ -1,28 +1,12 @@
<template> <template>
<div <div class="document-form" ref="documentform">
class="document-form" <div v-if="!value.url" ref="uploadcare-panel" />
ref="documentform" <div class="document-form__spinner" v-if="loading">
>
<div
v-if="!value.url"
ref="uploadcare-panel"
/>
<div
class="document-form__spinner"
v-if="loading"
>
<loading-icon class="document-form__loading-icon" /> <loading-icon class="document-form__loading-icon" />
</div> </div>
<div <div class="document-form__uploaded" v-if="value.url">
class="document-form__uploaded"
v-if="value.url"
>
<document-icon class="document-form__icon" /> <document-icon class="document-form__icon" />
<a <a :href="previewUrl" class="document-form__link" target="_blank">{{ previewLink }}</a>
:href="previewUrl"
class="document-form__link"
target="_blank"
>{{ previewLink }}</a>
</div> </div>
</div> </div>
</template> </template>
@ -34,100 +18,103 @@
const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon')); const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
export default { export default {
props: ['value', 'index'], props: ['value', 'index'],
components: { components: {
LoadingIcon, LoadingIcon,
DocumentIcon, DocumentIcon,
},
data() {
return {
loading: false,
};
},
computed: {
previewUrl() {
if (this.value && this.value.url) {
return this.value.url;
}
return null;
}, },
previewLink() {
data() { if (this.value && this.value.url) {
return { const parts = this.value.url.split('/');
loading: false, return parts[parts.length - 1];
}; }
return '';
}, },
},
computed: { mounted() {
previewUrl() { uploadcare(
if (this.value && this.value.url) { this,
return this.value.url; (url) => {
}
return null;
},
previewLink() {
if (this.value && this.value.url) {
const parts = this.value.url.split('/');
return parts[parts.length - 1];
}
return '';
},
},
mounted() {
uploadcare(this, url => {
this.$emit('change-url', url, this.index); this.$emit('change-url', url, this.index);
this.loading = false; this.loading = false;
}, () => { },
() => {
this.loading = true; this.loading = true;
}
}); );
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.document-form { .document-form {
&__uploaded { &__uploaded {
display: flex; display: flex;
align-items: center; align-items: center;
} }
&__link { &__link {
text-decoration: underline; text-decoration: underline;
} }
&__spinner { &__spinner {
width: 100%; width: 100%;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
}
&__loading-icon {
@include spin;
fill: $color-silver-dark;
}
&__icon {
width: 30px;
height: 30px;
margin-right: $small-spacing;
}
&__file-input {
width: 0.1px;
height: 0.1px;
overflow: hidden;
opacity: 0;
position: absolute;
z-index: -1;
& + label {
cursor: pointer;
background-color: $color-silver-light;
height: 150px; height: 150px;
display: flex; display: flex;
align-items: center; width: 100%;
justify-content: center; justify-content: center;
} align-items: center;
font-family: $sans-serif-font-family;
&__loading-icon { font-weight: $font-weight-regular;
@include spin; text-decoration: underline;
fill: $color-silver-dark;
}
&__icon {
width: 30px;
height: 30px;
margin-right: $small-spacing;
}
&__file-input {
width: 0.1px;
height: 0.1px;
overflow: hidden;
opacity: 0;
position: absolute;
z-index: -1;
& + label {
cursor: pointer;
background-color: $color-silver-light;
height: 150px;
display: flex;
width: 100%;
justify-content: center;
align-items: center;
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
text-decoration: underline;
}
} }
} }
}
</style> </style>

View File

@ -1,21 +1,11 @@
<template> <template>
<div> <div>
<div <div class="video-form" v-if="!isVimeo && !isYoutube && !isSrf">
class="video-form"
v-if="!isVimeo && !isYoutube && !isSrf"
>
<info-icon class="video-form__help-icon help-text__icon" /> <info-icon class="video-form__help-icon help-text__icon" />
<p class="video-form__help-description help-text__description"> <p class="video-form__help-description help-text__description">
Sie können Videos auf <a Sie können Videos auf
class="video-form__platform-link help-text__link" <a class="video-form__platform-link help-text__link" href="https://youtube.com/" target="_blank">Youtube</a>
href="https://youtube.com/" oder <a class="video-form__platform-link help-text__link" href="https://vimeo.com/" target="_blank">Vimeo</a>
target="_blank"
>Youtube</a>
oder <a
class="video-form__platform-link help-text__link"
href="https://vimeo.com/"
target="_blank"
>Vimeo</a>
hochladen und anschliessen einen Link hier einfügen. hochladen und anschliessen einen Link hier einfügen.
</p> </p>
@ -24,7 +14,7 @@
class="video-form__video-link skillbox-input" class="video-form__video-link skillbox-input"
placeholder="Bsp: https://www.youtube.com/watch?v=dQw4w9WgXcQ" placeholder="Bsp: https://www.youtube.com/watch?v=dQw4w9WgXcQ"
@input="$emit('change-url', $event.target.value, index)" @input="$emit('change-url', $event.target.value, index)"
> />
</div> </div>
<div v-if="isYoutube"> <div v-if="isYoutube">
@ -40,67 +30,65 @@
</template> </template>
<script> <script>
import YoutubeEmbed from '@/components/videos/YoutubeEmbed'; import YoutubeEmbed from '@/components/videos/YoutubeEmbed';
import VimeoEmbed from '@/components/videos/VimeoEmbed'; import VimeoEmbed from '@/components/videos/VimeoEmbed';
import SrfEmbed from '@/components/videos/SrfEmbed'; import SrfEmbed from '@/components/videos/SrfEmbed';
import {isVimeoUrl, isYoutubeUrl, isSrfUrl} from '@/helpers/video'; import { isVimeoUrl, isYoutubeUrl, isSrfUrl } from '@/helpers/video';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon')); const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon'));
export default { export default {
props: ['value', 'index'], props: ['value', 'index'],
components: { components: {
InfoIcon, InfoIcon,
YoutubeEmbed, YoutubeEmbed,
VimeoEmbed, VimeoEmbed,
SrfEmbed SrfEmbed,
},
computed: {
isYoutube() {
return isYoutubeUrl(this.value.url);
}, },
isVimeo() {
computed: { return isVimeoUrl(this.value.url);
isYoutube() { },
return isYoutubeUrl(this.value.url); isSrf() {
}, return isSrfUrl(this.value.url);
isVimeo() { },
return isVimeoUrl(this.value.url); },
}, };
isSrf() {
return isSrfUrl(this.value.url);
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_functions.scss"; @import '@/styles/_functions.scss';
.video-form { .video-form {
display: grid; display: grid;
grid-auto-rows: auto; grid-auto-rows: auto;
grid-template-columns: 40px 1fr; grid-template-columns: 40px 1fr;
grid-column-gap: 16px; grid-column-gap: 16px;
grid-row-gap: 20px; grid-row-gap: 20px;
align-items: center; align-items: center;
&__help-icon { &__help-icon {
}
&__help-description {
}
&__platform-link {
font-family: $sans-serif-font-family;
text-decoration: underline;
font-weight: $font-weight-regular;
font-size: toRem(17px);
}
&__video-link {
grid-column: 1 / span 2;
width: $modal-input-width
}
} }
&__help-description {
}
&__platform-link {
font-family: $sans-serif-font-family;
text-decoration: underline;
font-weight: $font-weight-regular;
font-size: toRem(17px);
}
&__video-link {
grid-column: 1 / span 2;
width: $modal-input-width;
}
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 378 505"> <svg xmlns="http://www.w3.org/2000/svg"xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 378 505">
<defs> <defs>
<clipPath id="b"> <clipPath id="b">
<path <path
@ -284,14 +284,9 @@
/> />
<path <path
d="M339.92,387.48c0-.82-.33-1.61-.91-2.19-.58-.58-1.37-.91-2.19-.91H23.71c-.82,0-1.61,.33-2.19,.91-.58,.58-.91,1.37-.91,2.19h0c0,.82,.33,1.61,.91,2.19,.58,.58,1.37,.91,2.19,.91H336.83c.82,0,1.61-.33,2.19-.91,.58-.58,.91-1.37,.91-2.19h0Z" d="M339.92,387.48c0-.82-.33-1.61-.91-2.19-.58-.58-1.37-.91-2.19-.91H23.71c-.82,0-1.61,.33-2.19,.91-.58,.58-.91,1.37-.91,2.19h0c0,.82,.33,1.61,.91,2.19,.58,.58,1.37,.91,2.19,.91H336.83c.82,0,1.61-.33,2.19-.91,.58-.58,.91-1.37,.91-2.19h0Z"
fill="#fff" fill="#fff" fill-rule="evenodd" />
fill-rule="evenodd"
/>
<polygon <polygon
points="291.56 386.68 286.32 386.68 286.32 505.02 291.56 505.02 291.56 386.68 291.56 386.68" points="291.56 386.68 286.32 386.68 286.32 505.02 291.56 505.02 291.56 386.68 291.56 386.68" fill="#fff" fill-rule="evenodd" />
fill="#fff"
fill-rule="evenodd"
/>
<polygon <polygon
points="72.67 387.59 67.43 387.59 67.43 505.02 72.67 505.02 72.67 387.59 72.67 387.59" points="72.67 387.59 67.43 387.59 67.43 505.02 72.67 505.02 72.67 387.59 72.67 387.59"
fill="#fff" fill="#fff"

View File

@ -1,179 +1,169 @@
<template> <template>
<a <a :class="typeClass" class="filter-entry" data-cy="filter-entry" :style="categoryStyle" @click="$emit('filter')"
:class="typeClass"
class="filter-entry"
data-cy="filter-entry"
:style="categoryStyle"
@click="$emit('filter')"
> >
<span class="filter-entry__text">{{ text }}</span> <span class="filter-entry__text">{{ text }}</span>
<span <span :style="activeStyle" class="filter-entry__icon-wrapper">
:style="activeStyle" <chevron-right :style="{ fill: category.foreground }" class="filter-entry__icon" />
class="filter-entry__icon-wrapper"
>
<chevron-right
:style="{fill: category.foreground}"
class="filter-entry__icon"
/>
</span> </span>
</a> </a>
</template> </template>
<script> <script>
import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql'; import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const ChevronRight = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronRight')); const ChevronRight = defineAsyncComponent(() =>
import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronRight')
);
export default { export default {
props: { props: {
text: { text: {
type: String, type: String,
required: true, required: true,
},
id: {
type: String,
default: '',
},
isCategory: {
type: Boolean,
default: false,
},
category: {
type: Object,
default: () => ({}),
},
}, },
id: {
type: String,
default: '',
},
isCategory: {
type: Boolean,
default: false,
},
category: {
type: Object,
default: () => ({}),
},
},
components: { components: {
ChevronRight, ChevronRight,
}, },
emits: ['filter'], emits: ['filter'],
apollo: { apollo: {
instrumentFilter: { instrumentFilter: {
query: INSTRUMENT_FILTER_QUERY, query: INSTRUMENT_FILTER_QUERY,
},
}, },
},
data() { data() {
return {
instrumentFilter: {
currentFilter: '',
},
};
},
computed: {
isActive() {
if (!this.instrumentFilter.currentFilter) {
return this.id === '';
}
// eslint-disable-next-line
const [_, identifier] = this.instrumentFilter.currentFilter.split(':');
return this.id === identifier;
},
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
activeStyle() {
if (this.isActive) {
return {
backgroundColor: this.category.background,
};
}
return {};
},
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
categoryStyle() {
if (this.isCategory) {
return {
color: this.category.foreground,
};
}
return {};
},
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
typeClass() {
return { return {
instrumentFilter: { 'filter-entry--active': this.isActive,
currentFilter: '', 'filter-entry--category': this.isCategory,
},
}; };
}, },
},
computed: { };
isActive() {
if (!this.instrumentFilter.currentFilter) {
return this.id === '';
}
// eslint-disable-next-line
const [_, identifier] = this.instrumentFilter.currentFilter.split(':');
return this.id === identifier;
},
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
activeStyle() {
if (this.isActive) {
return {
backgroundColor: this.category.background,
};
}
return {};
},
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
categoryStyle() {
if (this.isCategory) {
return {
color: this.category.foreground,
};
}
return {};
},
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
typeClass() {
return {
'filter-entry--active': this.isActive,
'filter-entry--category': this.isCategory,
};
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.filter-entry { .filter-entry {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
&__text {
@include sub-heading;
line-height: 1.5;
color: inherit;
}
&__icon-wrapper {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
cursor: pointer; justify-content: center;
height: 20px;
width: 20px;
border-radius: 10px;
}
&__text { &__icon {
@include sub-heading; width: 10px;
line-height: 1.5; height: 10px;
color: inherit; }
$root: &;
@mixin filter-block($color) {
&#{$root}--category {
color: $color;
} }
&__icon-wrapper { &#{$root}--active {
display: flex;
align-items: center;
justify-content: center;
height: 20px;
width: 20px;
border-radius: 10px;
}
&__icon {
width: 10px;
height: 10px;
}
$root: &;
@mixin filter-block($color) {
&#{$root}--category {
color: $color;
}
&#{$root}--active {
#{$root}__icon-wrapper {
background-color: $color;
}
}
#{$root}__icon {
fill: $color;
}
}
&--language-communication {
@include filter-block($color-accent-1-dark);
}
&--society {
@include filter-block($color-accent-2-dark);
}
&--interdisciplinary {
@include filter-block($color-accent-4-dark);
}
&--active {
#{$root}__text {
font-weight: 600;
}
#{$root}__icon-wrapper { #{$root}__icon-wrapper {
background-color: black; background-color: $color;
} }
}
#{$root}__icon { #{$root}__icon {
fill: white; fill: $color;
}
} }
} }
&--language-communication {
@include filter-block($color-accent-1-dark);
}
&--society {
@include filter-block($color-accent-2-dark);
}
&--interdisciplinary {
@include filter-block($color-accent-4-dark);
}
&--active {
#{$root}__text {
font-weight: 600;
}
#{$root}__icon-wrapper {
background-color: black;
}
#{$root}__icon {
fill: white;
}
}
}
</style> </style>

View File

@ -24,77 +24,76 @@
</template> </template>
<script> <script>
import {defineComponent} from 'vue'; import { defineComponent } from 'vue';
import FilterEntry from '@/components/instruments/FilterEntry'; import FilterEntry from '@/components/instruments/FilterEntry';
import SET_FILTER_MUTATION from 'gql/local/mutations/setInstrumentFilter.gql'; import SET_FILTER_MUTATION from 'gql/local/mutations/setInstrumentFilter.gql';
import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql'; import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql';
export default defineComponent({ export default defineComponent({
props: { props: {
title: { title: {
type: String, type: String,
default: '', default: '',
},
types: {
type: Array,
default: () => [],
},
category: {
type: Object,
default: () => ({}),
},
}, },
components: { types: {
FilterEntry, type: Array,
default: () => [],
}, },
category: {
apollo: { type: Object,
instrumentFilter: { default: () => ({}),
query: INSTRUMENT_FILTER_QUERY
}
}, },
data() {
return {
instrumentFilter: {
currentFilter: ''
}
};
}, },
inheritAttrs: false, components: {
FilterEntry,
},
methods: { apollo: {
setCategoryFilter(category) { instrumentFilter: {
if (category) { query: INSTRUMENT_FILTER_QUERY,
this.setFilter(`category:${category}`); },
} else { },
this.setFilter(``);
} data() {
return {
instrumentFilter: {
currentFilter: '',
}, },
setFilter(filter) { };
this.$apollo.mutate({ },
mutation: SET_FILTER_MUTATION, inheritAttrs: false,
variables: {
filter methods: {
} setCategoryFilter(category) {
}); if (category) {
this.setFilter(`category:${category}`);
} else {
this.setFilter(``);
} }
}, },
setFilter(filter) {
this.$apollo.mutate({
mutation: SET_FILTER_MUTATION,
variables: {
filter,
},
});
},
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.filter-group { .filter-group {
border-bottom: 1px solid $color-silver; border-bottom: 1px solid $color-silver;
padding: $medium-spacing 0; padding: $medium-spacing 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&__children { &__children {
padding-left: $medium-spacing; padding-left: $medium-spacing;
}
} }
}
</style> </style>

View File

@ -1,12 +1,6 @@
<template> <template>
<router-link <router-link :to="moduleLink" :class="['module-teaser', { 'module-teaser--small': !teaser }]">
:to="moduleLink" <div :style="{ backgroundImage: 'url(' + heroImage + ')' }" class="module-teaser__image" />
:class="['module-teaser', {'module-teaser--small': !teaser}]"
>
<div
:style="{backgroundImage: 'url('+heroImage+')'}"
class="module-teaser__image"
/>
<div class="module-teaser__body"> <div class="module-teaser__body">
<h3 class="module-teaser__meta-title"> <h3 class="module-teaser__meta-title">
{{ metaTitle }} {{ metaTitle }}
@ -22,8 +16,8 @@
</template> </template>
<script> <script>
export default { export default {
props: ['metaTitle', 'title', 'teaser', 'id', 'slug', 'heroImage'], props: ['metaTitle', 'title', 'teaser', 'id', 'slug', 'heroImage'],
computed: { computed: {
moduleLink() { moduleLink() {
@ -38,55 +32,56 @@
return {}; return {};
} }
} }
} },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import "~styles/helpers";
.module-teaser { .module-teaser {
box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12); box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12);
border: 1px solid #E2E2E2; border: 1px solid #e2e2e2;
height: 330px; height: 330px;
max-width: 380px; max-width: 380px;
width: 100%; width: 100%;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;
&--small { &--small {
height: 300px; height: 300px;
}
&__image {
width: 100%;
max-height: 150px;
height: 150px;
background-position: center;
background-size: 100% auto;
background-repeat: no-repeat;
}
&__body {
padding: 20px;
}
&__meta-title {
color: $color-silver-dark;
margin-bottom: $large-spacing;
@include regular-text;
}
&__title {
@include heading-3;
margin-bottom: 5px;
}
&__description {
line-height: $default-line-height;
font-size: 1.2rem;
}
} }
&__image {
width: 100%;
max-height: 150px;
height: 150px;
background-position: center;
background-size: 100% auto;
background-repeat: no-repeat;
}
&__body {
padding: 20px;
}
&__meta-title {
color: $color-silver-dark;
margin-bottom: $large-spacing;
@include regular-text;
}
&__title {
@include heading-3;
margin-bottom: 5px;
}
&__description {
line-height: $default-line-height;
font-size: 1.2rem;
}
}
</style> </style>

View File

@ -1,10 +1,7 @@
<template> <template>
<div <div class="bookmark-actions" v-if="!editMode">
class="bookmark-actions"
v-if="!editMode"
>
<a <a
:class="{'bookmark-actions__action--bookmarked': bookmarked}" :class="{ 'bookmark-actions__action--bookmarked': bookmarked }"
class="bookmark-actions__action bookmark-actions__bookmark" class="bookmark-actions__action bookmark-actions__bookmark"
data-cy="bookmark-action" data-cy="bookmark-action"
@click="$emit('bookmark')" @click="$emit('bookmark')"
@ -37,69 +34,69 @@
const AddNoteIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddNoteIcon')); const AddNoteIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddNoteIcon'));
const NoteIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/NoteIcon')); const NoteIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/NoteIcon'));
export default { export default {
props: { props: {
bookmarked: { bookmarked: {
type: Boolean, type: Boolean,
default: false default: false,
},
note: {
type: [Object, Boolean],
default: false
},
editMode: {
type: Boolean,
default: false
}
}, },
components: { note: {
BookmarkIcon, type: [Object, Boolean],
AddNoteIcon, default: false,
NoteIcon
}, },
}; editMode: {
type: Boolean,
default: false,
},
},
components: {
BookmarkIcon,
AddNoteIcon,
NoteIcon,
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.bookmark-actions { .bookmark-actions {
height: 100%; height: 100%;
min-height: 60px; min-height: 60px;
padding: 0 2*$large-spacing; padding: 0 2 * $large-spacing;
position: absolute; position: absolute;
right: -5*$large-spacing; right: -5 * $large-spacing;
display: none; display: none;
@include desktop { @include desktop {
display: flex; display: flex;
} }
flex-direction: column; flex-direction: column;
align-content: center; align-content: center;
&__action { &__action {
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
cursor: pointer; cursor: pointer;
width: 26px; width: 26px;
display: flex; display: flex;
justify-content: center; justify-content: center;
&--bookmarked, &--noted { &--bookmarked,
opacity: 1; &--noted {
} opacity: 1;
}
$parent: &;
&:hover {
#{$parent}__action {
opacity: 1;
}
} }
} }
$parent: &;
&:hover {
#{$parent}__action {
opacity: 1;
}
}
}
</style> </style>

View File

@ -6,40 +6,37 @@
placeholder="Lernziel erfassen..." placeholder="Lernziel erfassen..."
@input="$emit('input', $event)" @input="$emit('input', $event)"
/> />
<a <a class="icon-button" @click="$emit('delete')">
class="icon-button"
@click="$emit('delete')"
>
<trash-icon class="icon-button__icon icon-button__icon--subtle" /> <trash-icon class="icon-button__icon icon-button__icon--subtle" />
</a> </a>
</div> </div>
</template> </template>
<script> <script>
import ModalInput from '@/components/ModalInput'; import ModalInput from '@/components/ModalInput';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon')); const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'));
export default { export default {
props: ['objective'], props: ['objective'],
components: { components: {
ModalInput, ModalInput,
TrashIcon TrashIcon,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.objective-form { .objective-form {
display: grid; display: grid;
grid-template-columns: 1fr 50px; grid-template-columns: 1fr 50px;
margin-bottom: 10px; margin-bottom: 10px;
&__input { &__input {
width: $modal-input-width; width: $modal-input-width;
}
} }
}
</style> </style>

View File

@ -1,8 +1,5 @@
<template> <template>
<a <a class="add-project-entry" @click="addProjectEntry">
class="add-project-entry"
@click="addProjectEntry"
>
<plus-icon class="add-project-entry__icon" /> <plus-icon class="add-project-entry__icon" />
<span class="add-project-entry__text">Beitrag erfassen</span> <span class="add-project-entry__text">Beitrag erfassen</span>
</a> </a>
@ -15,35 +12,35 @@
props: ['project'], props: ['project'],
components: {PlusIcon}, components: {PlusIcon},
methods: { methods: {
addProjectEntry() { addProjectEntry() {
this.$store.dispatch('addProjectEntry', this.project); this.$store.dispatch('addProjectEntry', this.project);
} },
} },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~styles/helpers"; @import '~styles/helpers';
.add-project-entry { .add-project-entry {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 2px solid $color-brand; border: 2px solid $color-brand;
border-radius: $default-border-radius; border-radius: $default-border-radius;
cursor: pointer; cursor: pointer;
&__icon { &__icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
fill: $color-brand; fill: $color-brand;
margin-right: $small-spacing; margin-right: $small-spacing;
}
&__text {
@include navigation-link;
color: $color-brand;
}
} }
&__text {
@include navigation-link;
color: $color-brand;
}
}
</style> </style>

View File

@ -1,25 +1,9 @@
<template> <template>
<div class="portfolio-onboarding"> <div class="portfolio-onboarding">
<h1 <h1 class="portfolio-onboarding__heading" data-cy="page-title">Portfolio</h1>
class="portfolio-onboarding__heading" <portfolio-illustration data-cy="portfolio-onboarding-illustration" class="portfolio-onboarding__illustration" />
data-cy="page-title" <h2 class="portfolio-onboarding__subheading" data-cy="portfolio-onboarding-subtitle">Woran denken Sie gerade?</h2>
> <p class="portfolio-onboarding__text" data-cy="portfolio-onboarding-text">
Portfolio
</h1>
<portfolio-illustration
data-cy="portfolio-onboarding-illustration"
class="portfolio-onboarding__illustration"
/>
<h2
class="portfolio-onboarding__subheading"
data-cy="portfolio-onboarding-subtitle"
>
Woran denken Sie gerade?
</h2>
<p
class="portfolio-onboarding__text"
data-cy="portfolio-onboarding-text"
>
Hier können Sie Projekte erstellen, um Ihre Gedanken festzuhalten oder Ihre Arbeit zu dokumentieren. Hier können Sie Projekte erstellen, um Ihre Gedanken festzuhalten oder Ihre Arbeit zu dokumentieren.
</p> </p>
@ -37,25 +21,25 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.portfolio-onboarding { .portfolio-onboarding {
@include onboarding-page; @include onboarding-page;
&__heading { &__heading {
@include heading-1; @include heading-1;
}
&__subheading {
@include heading-2;
}
&__illustration {
@include onboarding-illustration;
}
&__text {
@include onboarding-text;
}
} }
&__subheading {
@include heading-2;
}
&__illustration {
@include onboarding-illustration;
}
&__text {
@include onboarding-text;
}
}
</style> </style>

View File

@ -1,48 +1,20 @@
<template> <template>
<div <div class="project-actions" data-cy="project-actions">
class="project-actions" <a class="project-actions__more-link" @click.stop="toggleMenu">
data-cy="project-actions"
>
<a
class="project-actions__more-link"
@click.stop="toggleMenu"
>
<ellipses /> <ellipses />
</a> </a>
<widget-popover <widget-popover class="project-actions__popover" v-if="showMenu" @hide-me="showMenu = false">
class="project-actions__popover"
v-if="showMenu"
@hide-me="showMenu = false"
>
<li class="popover-links__link"> <li class="popover-links__link">
<a <a data-cy="delete-project" @click="deleteProject(slug)">Projekt löschen</a>
data-cy="delete-project"
@click="deleteProject(slug)"
>Projekt löschen</a>
</li> </li>
<li class="popover-links__link"> <li class="popover-links__link">
<a <a data-cy="edit-project" @click="editProject(slug)">Projekt bearbeiten</a>
data-cy="edit-project"
@click="editProject(slug)"
>Projekt bearbeiten</a>
</li> </li>
<li <li class="popover-links__link" v-if="!final && shareButtons">
class="popover-links__link" <a data-cy="share-project" @click="updateProjectShareState(slug, true)">Projekt teilen</a>
v-if="!final && shareButtons"
>
<a
data-cy="share-project"
@click="updateProjectShareState(slug, true)"
>Projekt teilen</a>
</li> </li>
<li <li class="popover-links__link" v-if="final && shareButtons">
class="popover-links__link" <a data-cy="unshare-project" @click="updateProjectShareState(slug, false)">Projekt nicht mehr teilen</a>
v-if="final && shareButtons"
>
<a
data-cy="unshare-project"
@click="updateProjectShareState(slug, false)"
>Projekt nicht mehr teilen</a>
</li> </li>
</widget-popover> </widget-popover>
</div> </div>
@ -55,9 +27,9 @@ import DELETE_PROJECT_MUTATION from '@/graphql/gql/mutations/deleteProject.gql';
import PROJECTS_QUERY from '@/graphql/gql/queries/allProjects.gql'; import PROJECTS_QUERY from '@/graphql/gql/queries/allProjects.gql';
import updateProjectShareState from '@/mixins/update-project-share-state'; import updateProjectShareState from '@/mixins/update-project-share-state';
import {removeAtIndex} from '@/graphql/immutable-operations.ts'; import { removeAtIndex } from '@/graphql/immutable-operations.ts';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Ellipses = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Ellipses.vue')); const Ellipses = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/Ellipses.vue'));
export default { export default {
props: { props: {
@ -93,42 +65,50 @@ export default {
this.showMenu = !this.showMenu; this.showMenu = !this.showMenu;
}, },
editProject(slug) { editProject(slug) {
this.$router.push({name: 'edit-project', params: {slug}}); this.$router.push({ name: 'edit-project', params: { slug } });
}, },
deleteProject(slug) { deleteProject(slug) {
this.$apollo.mutate({ this.$apollo
mutation: DELETE_PROJECT_MUTATION, .mutate({
variables: { mutation: DELETE_PROJECT_MUTATION,
input: { variables: {
slug, input: {
slug,
},
}, },
}, update(
update(store, {data: {deleteProject: {success}}}) { store,
if (success) { {
const {projects: prevProjects} = store.readQuery({query: PROJECTS_QUERY}); data: {
deleteProject: { success },
},
if (prevProjects) {
let index = prevProjects.findIndex(project => project.slug === slug);
const projects = removeAtIndex(prevProjects, index);
const data = {
projects
};
store.writeQuery({query: PROJECTS_QUERY, data});
} }
} ) {
}, if (success) {
}).then(() => { const { projects: prevProjects } = store.readQuery({ query: PROJECTS_QUERY });
this.$router.push('/portfolio');
}); if (prevProjects) {
let index = prevProjects.findIndex((project) => project.slug === slug);
const projects = removeAtIndex(prevProjects, index);
const data = {
projects,
};
store.writeQuery({ query: PROJECTS_QUERY, data });
}
}
},
})
.then(() => {
this.$router.push('/portfolio');
});
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/_helpers.scss"; @import '~styles/_helpers.scss';
.project-actions { .project-actions {
position: relative; position: relative;

View File

@ -1,44 +1,22 @@
<template> <template>
<div <div class="project-entry" data-cy="project-entry">
class="project-entry" <more-options-widget class="project-entry__more" data-cy="project-entry-more" v-if="!readOnly">
data-cy="project-entry"
>
<more-options-widget
class="project-entry__more"
data-cy="project-entry-more"
v-if="!readOnly"
>
<li class="popover-links__link"> <li class="popover-links__link">
<a <a data-cy="edit-project-entry" @click="editProjectEntry()">Eintrag bearbeiten</a>
data-cy="edit-project-entry"
@click="editProjectEntry()"
>Eintrag bearbeiten</a>
</li> </li>
<li class="popover-links__link"> <li class="popover-links__link">
<a <a data-cy="delete-project-entry" @click="deleteProjectEntry()">Eintrag löschen</a>
data-cy="delete-project-entry"
@click="deleteProjectEntry()"
>Eintrag löschen</a>
</li> </li>
</more-options-widget> </more-options-widget>
<h3 <h3 class="project-entry__heading" data-cy="project-entry-date">
class="project-entry__heading"
data-cy="project-entry-date"
>
{{ createdDateTime }} {{ createdDateTime }}
</h3> </h3>
<p <p class="project-entry__paragraph" data-cy="project-entry-activity">
class="project-entry__paragraph"
data-cy="project-entry-activity"
>
{{ description }} {{ description }}
</p> </p>
<p <p class="project-entry__paragraph" v-if="documentUrl">
class="project-entry__paragraph" <document-block :value="{ url: documentUrl }" />
v-if="documentUrl"
>
<document-block :value="{url: documentUrl}" />
</p> </p>
<div class="project-entry__date"> <div class="project-entry__date">
{{ createdDate }} {{ createdDate }}
@ -47,7 +25,7 @@
</template> </template>
<script> <script>
import MoreOptionsWidget from '@/components/MoreOptionsWidget'; import MoreOptionsWidget from '@/components/MoreOptionsWidget';
import DELETE_PROJECT_ENTRY_MUTATION from '@/graphql/gql/mutations/deleteProjectEntry.gql'; import DELETE_PROJECT_ENTRY_MUTATION from '@/graphql/gql/mutations/deleteProjectEntry.gql';
import PROJECT_QUERY from '@/graphql/gql/queries/projectQuery.gql'; import PROJECT_QUERY from '@/graphql/gql/queries/projectQuery.gql';
@ -57,100 +35,106 @@
const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock')); const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'));
export default { export default {
props: ['description', 'documentUrl', 'created', 'id', 'readOnly'], props: ['description', 'documentUrl', 'created', 'id', 'readOnly'],
components: { components: {
DocumentBlock, DocumentBlock,
MoreOptionsWidget, MoreOptionsWidget,
}, },
computed: { computed: {
createdDate() { createdDate() {
return dateFilter(this.created); return dateFilter(this.created);
},
createdDateTime() {
return dateTimeFilter(this.created);
},
}, },
createdDateTime() {
return dateTimeFilter(this.created);
},
},
methods: { methods: {
editProjectEntry() { editProjectEntry() {
this.$store.dispatch('editProjectEntry', this.id); this.$store.dispatch('editProjectEntry', this.id);
}, },
deleteProjectEntry() { deleteProjectEntry() {
const projectEntry = this; // otherwise we run into scope errors const projectEntry = this; // otherwise we run into scope errors
this.$apollo.mutate({ this.$apollo.mutate({
mutation: DELETE_PROJECT_ENTRY_MUTATION, mutation: DELETE_PROJECT_ENTRY_MUTATION,
variables: { variables: {
input: { input: {
id: this.id, id: this.id,
},
},
update(
store,
{
data: {
deleteProjectEntry: { success },
}, },
}, }
update(store, {data: {deleteProjectEntry: {success}}}) { ) {
if (success) { if (success) {
const query = PROJECT_QUERY; const query = PROJECT_QUERY;
const variables = { const variables = {
slug: projectEntry.$route.params.slug, slug: projectEntry.$route.params.slug,
};
const { project } = store.readQuery({ query, variables });
if (project) {
const index = project.entries.findIndex((entry) => entry.id === projectEntry.id);
const entries = removeAtIndex(project.entries, index);
const data = {
project: {
...project,
entries,
},
}; };
const {project} = store.readQuery({query, variables}); store.writeQuery({ query, variables, data });
if (project) {
const index = project.entries.findIndex(entry => entry.id === projectEntry.id);
const entries = removeAtIndex(project.entries, index);
const data = {
project: {
...project,
entries,
},
};
store.writeQuery({query, variables, data});
}
} }
}, }
}); },
}, });
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.project-entry { .project-entry {
background-color: $color-white; background-color: $color-white;
border-radius: $default-border-radius; border-radius: $default-border-radius;
padding: 30px 20px; padding: 30px 20px;
position: relative; position: relative;
&__heading {
font-size: toRem(22px);
margin-bottom: 6px;
}
&__paragraph {
margin-bottom: 30px;
white-space: pre-line;
}
&__date {
font-family: $sans-serif-font-family;
color: $color-silver-dark;
font-size: toRem(17px);
}
&__link {
cursor: pointer;
@include heading-4;
}
&__more {
position: absolute;
top: 10px;
right: 10px;
display: none;
@include desktop {
display: block;
}
}
&__heading {
font-size: toRem(22px);
margin-bottom: 6px;
} }
&__paragraph {
margin-bottom: 30px;
white-space: pre-line;
}
&__date {
font-family: $sans-serif-font-family;
color: $color-silver-dark;
font-size: toRem(17px);
}
&__link {
cursor: pointer;
@include heading-4;
}
&__more {
position: absolute;
top: 10px;
right: 10px;
display: none;
@include desktop {
display: block;
}
}
}
</style> </style>

View File

@ -1,12 +1,7 @@
<template> <template>
<modal :hide-header="false"> <modal :hide-header="false">
<template #header> <template #header>
<h2 <h2 class="project-entry-modal__heading" data-cy="modal-title">Beitrag erfassen</h2>
class="project-entry-modal__heading"
data-cy="modal-title"
>
Beitrag erfassen
</h2>
</template> </template>
<div class="project-entry-modal"> <div class="project-entry-modal">
@ -36,106 +31,102 @@
</div> </div>
</div> </div>
<template #footer> <template #footer>
<a <a class="button button--primary" data-cy="modal-save-button" @click="$emit('save', localProjectEntry)"
class="button button--primary" >Speichern</a
data-cy="modal-save-button" >
@click="$emit('save', localProjectEntry)" <a class="button" @click="$emit('hide')">Abbrechen</a>
>Speichern</a>
<a
class="button"
@click="$emit('hide')"
>Abbrechen</a>
</template> </template>
</modal> </modal>
</template> </template>
<script> <script>
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText'; import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText';
import {PROJECT_ENTRY_TEMPLATE} from '@/consts/strings.consts'; import { PROJECT_ENTRY_TEMPLATE } from '@/consts/strings.consts';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const FileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/FileUpload')); const FileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/FileUpload'));
export default { export default {
props: { props: {
projectEntry: { projectEntry: {
type: Object, type: Object,
default: null, default: null,
},
}, },
},
components: { components: {
FileUpload, FileUpload,
ButtonWithIconAndText, ButtonWithIconAndText,
Modal, Modal,
}, },
data() { data() {
return { return {
localProjectEntry: Object.assign({}, { localProjectEntry: Object.assign(
{},
{
...this.projectEntry, ...this.projectEntry,
}), }
}; ),
}, };
},
methods: { methods: {
setDocumentUrl(url) { setDocumentUrl(url) {
this.localProjectEntry.documentUrl = url; this.localProjectEntry.documentUrl = url;
},
useTemplate() {
this.localProjectEntry.description = `${this.localProjectEntry.description}${PROJECT_ENTRY_TEMPLATE}`;
},
}, },
}; useTemplate() {
this.localProjectEntry.description = `${this.localProjectEntry.description}${PROJECT_ENTRY_TEMPLATE}`;
},
},
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~styles/helpers"; @import '~styles/helpers';
.project-entry-modal { .project-entry-modal {
display: flex;
flex-direction: column;
&__form-field {
@include inputstyle;
padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
grid-template-rows: auto 1rem;
&__form-field { grid-template-columns: 1fr 1fr;
@include inputstyle; width: 100%;
padding: 0;
display: flex;
flex-direction: column;
grid-template-rows: auto 1rem;
grid-template-columns: 1fr 1fr;
width: 100%;
}
&__textarea {
@include auto-grow;
border: 0;
min-height: 400px;
padding: $medium-spacing;
}
&__buttons {
display: grid;
grid-template-columns: 1fr 1fr;
padding: $medium-spacing;
}
&__button {
@include regular-text;
&--template {
}
&--document {
}
}
&__heading {
@include heading-3;
margin-bottom: 0;
}
} }
&__textarea {
@include auto-grow;
border: 0;
min-height: 400px;
padding: $medium-spacing;
}
&__buttons {
display: grid;
grid-template-columns: 1fr 1fr;
padding: $medium-spacing;
}
&__button {
@include regular-text;
&--template {
}
&--document {
}
}
&__heading {
@include heading-3;
margin-bottom: 0;
}
}
</style> </style>

View File

@ -1,17 +1,7 @@
<template> <template>
<page-form <page-form :title="title" @save="$emit('save', localProject)">
:title="title" <page-form-input label="Titel" v-model="localProject.title" />
@save="$emit('save', localProject)" <page-form-input label="Beschreibung" type="textarea" v-model="localProject.description" />
>
<page-form-input
label="Titel"
v-model="localProject.title"
/>
<page-form-input
label="Beschreibung"
type="textarea"
v-model="localProject.description"
/>
<template #footer> <template #footer>
<button <button
:class="{ 'button--disabled': !formValid }" :class="{ 'button--disabled': !formValid }"
@ -22,12 +12,7 @@
> >
Speichern Speichern
</button> </button>
<router-link <router-link to="/portfolio" class="button"> Abbrechen </router-link>
to="/portfolio"
class="button"
>
Abbrechen
</router-link>
</template> </template>
</page-form> </page-form>
</template> </template>

View File

@ -5,7 +5,7 @@
<owner-widget :owner="project.student" class="project__owner" /> <owner-widget :owner="project.student" class="project__owner" />
<entry-count-widget class="project__entry-count" :verbose="false" :entry-count="project.entriesCount" /> <entry-count-widgetclass="project__entry-count" :verbose="false" :entry-count="project.entriesCount" />
</router-link> </router-link>
<project-actions <project-actions
:final="project.final" :final="project.final"
@ -68,22 +68,20 @@ export default {
align-items: flex-start; align-items: flex-start;
flex-direction: column; flex-direction: column;
@include desktop { @include desktop {
display: flex; display: flex;
flex: 80%; flex: 80%;
flex-direction: row; flex-direction: row;align-items: center;
align-items: center; }}
}
}
&__title { &__title {
flex: 50%; flex: 50%;
@include heading-4; @include heading-4;
grid-column: 1 / span 2; grid-column: 1 / span 2;
} }
&__owner { &__owner {
flex: 40%; flex: 40%;
grid-column: 1 / span 1; grid-column: 1 / span 1;
} }
&__actions { &__actions {
@ -96,7 +94,6 @@ export default {
&__entry-count { &__entry-count {
justify-self: flex-end; justify-self: flex-end;
grid-column: 2 / span 1; grid-column: 2 / span 1;}
}
} }
</style> </style>

View File

@ -1,7 +1,5 @@
<template> <template>
<a <a class="share-icon" @click="$emit('share')"
class="share-icon"
@click="$emit('share')"
> >
<share-icon class="share-icon__icon" /> <share-icon class="share-icon__icon" />
<span class="share-icon__text"> <span class="share-icon__text">
@ -12,37 +10,37 @@
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const ShareIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ShareIcon')); const ShareIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/ShareIcon'));
export default { export default {
props: { props: {
final: { final: {
type: Boolean, type: Boolean,
default: false, default: false,
},
}, },
emits: ['share'], },
components: {ShareIcon}, emits: ['share'],
}; components: { ShareIcon },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.share-icon { .share-icon {
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
&__icon { &__icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
margin-right: $small-spacing; margin-right: $small-spacing;
}
&__text {
@include large-link;
}
} }
&__text {
@include large-link;
}
}
</style> </style>

View File

@ -2,31 +2,23 @@
<div class="avatar"> <div class="avatar">
<transition name="fade"> <transition name="fade">
<default-avatar <default-avatar
:class="{'avatar__placeholder--highlighted': iconHighlighted}" :class="{ 'avatar__placeholder--highlighted': iconHighlighted }"
class="avatar__placeholder" class="avatar__placeholder"
v-show="!isAvatarLoaded" v-show="!isAvatarLoaded"
/> />
</transition> </transition>
<transition name="show"> <transition name="show">
<div <div
:style="{'background-image': `url(${avatarUrl})`}" :style="{ 'background-image': `url(${avatarUrl})` }"
class="avatar__image" class="avatar__image"
v-show="isAvatarLoaded" v-show="isAvatarLoaded"
ref="avatarImage" ref="avatarImage"
/> />
</transition> </transition>
<img <img :src="avatarUrl" class="avatar__fake-image" ref="fakeImage" />
:src="avatarUrl"
class="avatar__fake-image"
ref="fakeImage"
>
<div <div class="avatar__edit" v-if="editable" @click="closeSidebar">
class="avatar__edit" <router-link :to="{ name: 'profile' }">
v-if="editable"
@click="closeSidebar"
>
<router-link :to="{name: 'profile'}">
<pen-icon /> <pen-icon />
</router-link> </router-link>
</div> </div>
@ -40,104 +32,104 @@
const DefaultAvatar = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DefaultAvatar')); const DefaultAvatar = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DefaultAvatar'));
const PenIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PenIcon')); const PenIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PenIcon'));
export default { export default {
props: { props: {
avatarUrl: { avatarUrl: {
type: String type: String,
},
iconHighlighted: {},
editable: {
default: false
}
}, },
components: { iconHighlighted: {},
DefaultAvatar, editable: {
PenIcon default: false,
}, },
data() { },
return { components: {
isAvatarLoaded: false DefaultAvatar,
}; PenIcon,
}, },
mounted() { data() {
if (this.avatarUrl !== '') { return {
this.$refs.fakeImage.addEventListener('load', () => { isAvatarLoaded: false,
if (this.$refs.fakeImage) { };
this.$refs.fakeImage.remove(); },
this.isAvatarLoaded = true; mounted() {
} if (this.avatarUrl !== '') {
}); this.$refs.fakeImage.addEventListener('load', () => {
} if (this.$refs.fakeImage) {
}, this.$refs.fakeImage.remove();
methods: { this.isAvatarLoaded = true;
closeSidebar() { }
this.$apollo.mutate({ });
mutation: TOGGLE_SIDEBAR,
variables: {
sidebar: {
profile: false
}
}
});
}
} }
}; },
methods: {
closeSidebar() {
this.$apollo.mutate({
mutation: TOGGLE_SIDEBAR,
variables: {
sidebar: {
profile: false,
},
},
});
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
$max-width: 100%; $max-width: 100%;
.avatar { .avatar {
height: $max-width;
width: $max-width;
overflow: hidden;
text-align: center;
&__placeholder {
height: $max-width; height: $max-width;
width: $max-width; fill: $color-silver-dark;
overflow: hidden; border-radius: 50%;
text-align: center;
&__placeholder { &--highlighted {
fill: $color-brand;
}
}
&__image {
background-size: cover;
background-position: center center;
height: 100%;
width: 100%;
border: 0;
border-radius: 50%;
&--landscape {
width: auto;
height: $max-width; height: $max-width;
fill: $color-silver-dark;
border-radius: 50%;
&--highlighted {
fill: $color-brand;
}
} }
}
&__image { &__fake-image {
background-size: cover; width: 0;
background-position: center center; height: 0;
height: 100%; }
width: 100%;
border: 0;
border-radius: 50%;
&--landscape { &__edit {
width: auto; position: absolute;
height: $max-width; box-sizing: border-box;
} width: 34px;
} height: 34px;
display: block;
&__fake-image { left: 50%;
width: 0; bottom: -3px;
height: 0; transform: translateX(80%);
} background-color: $color-white;
border-radius: 50%;
&__edit { padding: 6px;
position: absolute; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);
box-sizing: border-box; }
width: 34px;
height: 34px;
display: block;
left: 50%;
bottom: -3px;
transform: translateX(80%);
background-color: $color-white;
border-radius: 50%;
padding: 6px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);
}
.fade-leave-active, .show-enter-active { .fade-leave-active, .show-enter-active {
transition: opacity .5s; transition: opacity .5s;
@ -146,8 +138,8 @@
opacity: 0; opacity: 0;
} }
.show-enter-to { .show-enter-to {
opacity: 1; opacity: 1;
}
} }
}
</style> </style>

View File

@ -1,15 +1,9 @@
<template> <template>
<div class="content-bookmark module-activity-entry"> <div class="content-bookmark module-activity-entry">
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div v-if="content.type === 'text_block'" v-html="text" />
v-if="content.type === 'text_block'"
v-html="text"
/>
<div v-else-if="content.type === 'link_block'"> <div v-else-if="content.type === 'link_block'">
<link-block <link-block :value="content.value" :no-margin="true" />
:value="content.value"
:no-margin="true"
/>
</div> </div>
<p v-else> <p v-else>
{{ type }} {{ type }}
@ -21,32 +15,32 @@
import {defineAsyncComponent} from 'vue'; import {defineAsyncComponent} from 'vue';
const LinkBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/LinkBlock')); const LinkBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/LinkBlock'));
export default { export default {
props: ['bookmark'], props: ['bookmark'],
components: {LinkBlock}, components: { LinkBlock },
computed: { computed: {
content() { content() {
return this.bookmark.contentBlock return this.bookmark.contentBlock
? this.bookmark.contentBlock.contents.find(e => e.id === this.bookmark.uuid) ? this.bookmark.contentBlock.contents.find((e) => e.id === this.bookmark.uuid)
: this.bookmark.instrument.contents.find(e => e.id === this.bookmark.uuid); : this.bookmark.instrument.contents.find((e) => e.id === this.bookmark.uuid);
}, },
text() { text() {
return this.content.value.text ? this.content.value.text : 'TO BE DEFINED'; return this.content.value.text ? this.content.value.text : 'TO BE DEFINED';
}, },
type() { type() {
switch (this.content.type) { switch (this.content.type) {
case 'assignment': case 'assignment':
return 'Aufgabe & Ergebnis'; return 'Aufgabe & Ergebnis';
case 'link_block': case 'link_block':
return this.content; return this.content;
case 'survey': case 'survey':
return 'Übung'; return 'Übung';
case 'image_url_block': case 'image_url_block':
return 'Bild'; return 'Bild';
default: default:
return this.content.type; return this.content.type;
}
} }
} },
}; },
};
</script> </script>

View File

@ -1,9 +1,5 @@
<template> <template>
<a <a class="edit-group-name" data-cy="edit-group-name-link" @click="$emit('edit')">
class="edit-group-name"
data-cy="edit-group-name-link"
@click="$emit('edit')"
>
<pen-icon class="edit-group-name__icon" /> <pen-icon class="edit-group-name__icon" />
</a> </a>
</template> </template>
@ -12,21 +8,21 @@
import {defineAsyncComponent} from 'vue'; import {defineAsyncComponent} from 'vue';
const PenIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PenIcon')); const PenIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PenIcon'));
export default { export default {
components: { components: {
PenIcon PenIcon,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/_variables.scss"; @import '~styles/_variables.scss';
.edit-group-name { .edit-group-name {
&__icon { &__icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
fill: $color-brand; fill: $color-brand;
}
} }
}
</style> </style>

View File

@ -181,17 +181,13 @@ export default {
} }
&__role { &__role {
@include desktop { @include desktop {flex: 0 1 110px;
flex: 0 1 110px; text-align: right;
text-align: right; }}
}
}
&__action { &__action {
@include desktop { @include desktop {flex: 0 1 110px;
flex: 0 1 110px; padding-left: $large-spacing;
padding-left: $large-spacing;
}
} }
} }}
</style> </style>

View File

@ -1,105 +1,102 @@
<template> <template>
<div class="profile"> <div class="profile">
<h1 class="profile__header"> <h1 class="profile__header">Profilbild</h1>
Profilbild <div class="profile-avatar" v-if="me.avatarUrl">
</h1>
<div
class="profile-avatar"
v-if="me.avatarUrl"
>
<div class="profile-avatar__image"> <div class="profile-avatar__image">
<avatar :avatar-url="me.avatarUrl" /> <avatar :avatar-url="me.avatarUrl" />
</div> </div>
<a <a class="profile-avatar__remove icon-button" @click="deleteAvatar()">
class="profile-avatar__remove icon-button"
@click="deleteAvatar()"
>
<trash-icon class="profile-avatar__remove-icon icon-button__icon icon-button__icon--subtle" /> <trash-icon class="profile-avatar__remove-icon icon-button__icon icon-button__icon--subtle" />
</a> </a>
</div> </div>
<avatar-upload-form <avatar-upload-form v-else @avatarUpdate="updateAvatar" />
v-else
@avatarUpdate="updateAvatar"
/>
</div> </div>
</template> </template>
<script> <script>
import UPDATE_AVATAR_QUERY from '@/graphql/gql/mutations/updateAvatarUrl.gql'; import UPDATE_AVATAR_QUERY from '@/graphql/gql/mutations/updateAvatarUrl.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import AvatarUploadForm from '@/components/profile/AvatarUploadForm'; import AvatarUploadForm from '@/components/profile/AvatarUploadForm';
import Avatar from '@/components/profile/Avatar'; import Avatar from '@/components/profile/Avatar';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon')); const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'));
export default { export default {
components: { components: {
AvatarUploadForm, AvatarUploadForm,
Avatar, Avatar,
TrashIcon TrashIcon,
}, },
data() { data() {
return { return {
me: {
avatarUrl: ''
}
};
},
apollo: {
me: { me: {
query: ME_QUERY, avatarUrl: '',
}, },
};
},
apollo: {
me: {
query: ME_QUERY,
}, },
methods: { },
deleteAvatar () { methods: {
this.updateAvatar(''); deleteAvatar() {
}, this.updateAvatar('');
updateAvatar (url) { },
this.$apollo.mutate({ updateAvatar(url) {
this.$apollo
.mutate({
mutation: UPDATE_AVATAR_QUERY, mutation: UPDATE_AVATAR_QUERY,
variables: { variables: {
input: { input: {
avatarUrl: url avatarUrl: url,
} },
}, },
update(store, {data: {updateAvatar: {success}}}) { update(
store,
{
data: {
updateAvatar: { success },
},
}
) {
if (success) { if (success) {
const {me} = store.readQuery({query: ME_QUERY}); const { me } = store.readQuery({ query: ME_QUERY });
if (me) { if (me) {
const data = { const data = {
me: { me: {
...me, ...me,
avatarUrl: url avatarUrl: url,
} },
}; };
store.writeQuery({query: ME_QUERY, data}); store.writeQuery({ query: ME_QUERY, data });
} }
} }
} },
}).catch((error) => { })
.catch((error) => {
console.warn('UploadError', error); console.warn('UploadError', error);
}); });
} },
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.profile-avatar { .profile-avatar {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
&__image { &__image {
height: 230px; height: 230px;
width: 230px; width: 230px;
}
}
.profile-avatar {
margin-bottom: $large-spacing;
} }
}
.profile-avatar {
margin-bottom: $large-spacing;
}
</style> </style>

View File

@ -1,69 +1,31 @@
<template> <template>
<transition name="slide"> <transition name="slide">
<div <div class="profile-sidebar" data-cy="sidebar" v-if="sidebar.profile" v-click-outside="close">
class="profile-sidebar" <a class="profile-sidebar__close-link" data-cy="close-profile-sidebar-link" @click="close">
data-cy="sidebar"
v-if="sidebar.profile"
v-click-outside="close"
>
<a
class="profile-sidebar__close-link"
data-cy="close-profile-sidebar-link"
@click="close"
>
<cross class="profile-sidebar__close-icon" /> <cross class="profile-sidebar__close-icon" />
</a> </a>
<div class="profile-sidebar__section"> <div class="profile-sidebar__section">
<profile-widget class="profile-sidebar__item" /> <profile-widget class="profile-sidebar__item" />
<div <div class="profile-sidebar__item" @click="close">
class="profile-sidebar__item" <router-link to="/me/activity" class="profile-sidebar__link"> Meine Aktivitäten </router-link>
@click="close"
>
<router-link
to="/me/activity"
class="profile-sidebar__link"
>
Meine Aktivitäten
</router-link>
</div> </div>
<div <div class="profile-sidebar__item" v-if="me.isTeacher && !me.readOnly" @click="close">
class="profile-sidebar__item" <router-link :to="myTeamPage" data-cy="my-team-link" class="profile-sidebar__link"> Mein Team </router-link>
v-if="me.isTeacher && !me.readOnly"
@click="close"
>
<router-link
:to="myTeamPage"
data-cy="my-team-link"
class="profile-sidebar__link"
>
Mein Team
</router-link>
</div> </div>
</div> </div>
<div class="profile-sidebar__section"> <div class="profile-sidebar__section">
<div class="profile-sidebar__item"> <div class="profile-sidebar__item">
<class-selection-widget /> <class-selection-widget />
<div @click="close"> <div @click="close">
<router-link <router-link :to="{ name: 'my-class' }" data-cy="class-list-link" class="profile-sidebar__link">
:to="{name: 'my-class'}"
data-cy="class-list-link"
class="profile-sidebar__link"
>
Klassenliste Klassenliste
</router-link> </router-link>
</div> </div>
</div> </div>
</div> </div>
<div class="profile-sidebar__section"> <div class="profile-sidebar__section">
<div <div class="profile-sidebar__item" @click="close">
class="profile-sidebar__item" <router-link :to="{ name: 'join-class' }" data-cy="join-class-link" class="profile-sidebar__link">
@click="close"
>
<router-link
:to="{name:'join-class'}"
data-cy="join-class-link"
class="profile-sidebar__link"
>
Zugangscode Zugangscode
</router-link> </router-link>
</div> </div>
@ -76,112 +38,113 @@
</template> </template>
<script> <script>
import ProfileWidget from '@/components/profile/ProfileWidget'; import ProfileWidget from '@/components/profile/ProfileWidget';
import ClassSelectionWidget from '@/components/school-class/ClassSelectionWidget'; import ClassSelectionWidget from '@/components/school-class/ClassSelectionWidget';
import sidebar from '@/mixins/sidebar'; import sidebar from '@/mixins/sidebar';
import me from '@/mixins/me'; import me from '@/mixins/me';
import LogoutWidget from '@/components/LogoutWidget'; import LogoutWidget from '@/components/LogoutWidget';
import {MY_TEAM} from '@/router/me.names'; import { MY_TEAM } from '@/router/me.names';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon')); const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'));
export default { export default {
mixins: [sidebar, me],
mixins: [sidebar, me], components: {
LogoutWidget,
ClassSelectionWidget,
ProfileWidget,
Cross,
},
components: { computed: {
LogoutWidget, myTeamPage() {
ClassSelectionWidget, return {
ProfileWidget, name: MY_TEAM,
Cross, };
}, },
},
computed: { methods: {
myTeamPage() { close() {
return { this.closeSidebar('profile');
name: MY_TEAM,
};
},
}, },
},
methods: { };
close() {
this.closeSidebar('profile');
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
$desktop-width: 333px; $desktop-width: 333px;
.profile-sidebar { .profile-sidebar {
padding: $large-spacing 0; padding: $large-spacing 0;
box-sizing: border-box; box-sizing: border-box;
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
height: 100vh; height: 100vh;
background-color: $color-white; background-color: $color-white;
z-index: 15; z-index: 15;
box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12); box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12);
overflow-y: scroll; overflow-y: scroll;
width: 100%; width: 100%;
@include desktop {
width: $desktop-width;
}
display: flex;
flex-direction: column;
justify-content: flex-start;
&__section {
margin-bottom: $large-spacing;
&:last-of-type {
margin-top: auto;
}
}
&__item {
padding: $small-spacing $medium-spacing;
}
&__subtitle {
@include small-text;
margin: 0;
margin-bottom: $small-spacing;
}
&__link {
@include default-link;
display: block;
}
&__close-link {
position: absolute;
right: $small-spacing;
top: $small-spacing;
cursor: pointer;
}
}
.slide {
&-enter-active,
&-leave-active {
transition: right 0.2s;
}
&-enter-from,
&-leave-to {
right: -100vw;
@include desktop { @include desktop {
width: $desktop-width; right: -$desktop-width;
}
display: flex;
flex-direction: column;
justify-content: flex-start;
&__section {
margin-bottom: $large-spacing;
&:last-of-type {
margin-top: auto;
}
}
&__item {
padding: $small-spacing $medium-spacing;
}
&__subtitle {
@include small-text;
margin: 0;
margin-bottom: $small-spacing;
}
&__link {
@include default-link;
display: block;
}
&__close-link {
position: absolute;
right: $small-spacing;
top: $small-spacing;
cursor: pointer;
}
}
.slide {
&-enter-active, &-leave-active {
transition: right 0.2s;
}
&-enter-from, &-leave-to {
right: -100vw;
@include desktop {
right: -$desktop-width;
}
} }
} }
}
</style> </style>

View File

@ -32,19 +32,16 @@ export default {
.show-code { .show-code {
&__title { &__title {
@include regular-text; @include regular-text;
margin-bottom: $large-spacing;
margin-bottom: $large-spacing; @include desktop { margin-bottom: 2 * $large-spacing;
@include desktop { }}
margin-bottom: 2 * $large-spacing;
}
}
&__code { &__code {
font-size: toRem(60px); font-size: toRem(60px);
letter-spacing: toRem(10px); letter-spacing: toRem(10px);
@include desktop { @include desktop {
font-size: toRem(120px); font-size: toRem(120px);
letter-spacing: toRem(20px); letter-spacing: toRem(20px);
} }
font-weight: 600; font-weight: 600;
} }

View File

@ -1,9 +1,5 @@
<template> <template>
<router-link <router-link class="add-room-entry-button" data-cy="add-room-entry-button" :to="addRoomEntryRoute">
class="add-room-entry-button"
data-cy="add-room-entry-button"
:to="addRoomEntryRoute"
>
<plus-icon class="add-room-entry-button__icon" /> <plus-icon class="add-room-entry-button__icon" />
<span class="add-room-entry-button__text">Beitrag erfassen</span> <span class="add-room-entry-button__text">Beitrag erfassen</span>
</router-link> </router-link>
@ -14,52 +10,52 @@
const PlusIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PlusIcon')); const PlusIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PlusIcon'));
import { ADD_ROOM_ENTRY_PAGE } from '@/router/room.names'; import { ADD_ROOM_ENTRY_PAGE } from '@/router/room.names';
export default { export default {
props: ['parent'], props: ['parent'],
components: { components: {
PlusIcon, PlusIcon,
}, },
data() { data() {
return { return {
addRoomEntryRoute: { addRoomEntryRoute: {
name: ADD_ROOM_ENTRY_PAGE, name: ADD_ROOM_ENTRY_PAGE,
}, },
}; };
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.add-room-entry-button { .add-room-entry-button {
border: 2px solid $color-white; border: 2px solid $color-white;
border-radius: 12px; border-radius: 12px;
height: 150px; height: 150px;
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 25px; margin-bottom: 25px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
break-inside: avoid-column; break-inside: avoid-column;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
display: none; display: none;
@include desktop { @include desktop {
display: flex; display: flex;
}
&__icon {
width: 20px;
fill: $color-white;
margin-right: $small-spacing;
}
&__text {
@include regular-text;
color: $color-white;
}
} }
&__icon {
width: 20px;
fill: $color-white;
margin-right: $small-spacing;
}
&__text {
@include regular-text;
color: $color-white;
}
}
</style> </style>

View File

@ -1,56 +1,57 @@
<template> <template>
<div class="entry-count-widget"> <div class="entry-count-widget">
<component :is="icon" /> <component :is="icon" />
<span data-cy="entry-count">{{ entryCount }} <template v-if="verbose">{{ entryCount === 1 ? 'Beitrag' : 'Beiträge' }}</template></span> <span data-cy="entry-count"
>{{ entryCount }} <template v-if="verbose">{{ entryCount === 1 ? 'Beitrag' : 'Beiträge' }}</template></span
>
</div> </div>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
import SpeechBubbleIcon from '@/components/icons/SpeechBubbleIcon'; import SpeechBubbleIcon from '@/components/icons/SpeechBubbleIcon';
const Cards = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Cards.vue')); const Cards = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/Cards.vue'));
export default { export default {
props: { props: {
entryCount: { entryCount: {
type: Number, type: Number,
},
verbose: {
type: Boolean,
default: true,
},
icon: {
type: String,
default: 'cards'
}
}, },
verbose: {
type: Boolean,
default: true,
},
icon: {
type: String,
default: 'cards',
},
},
components: { components: {
'speech-bubble': SpeechBubbleIcon, 'speech-bubble': SpeechBubbleIcon,
SpeechBubbleIcon, SpeechBubbleIcon,
Cards, Cards,
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.entry-count-widget { .entry-count-widget {
display: flex; display: flex;
align-items: center; align-items: center;
opacity: 0.6; opacity: 0.6;
margin-right: $medium-spacing; margin-right: $medium-spacing;
svg { svg {
width: 30px; width: 30px;
fill: $color-charcoal-dark; fill: $color-charcoal-dark;
margin-right: 15px; margin-right: 15px;
}
& > span {
@include room-widget-text-style;
}
} }
& > span {
@include room-widget-text-style;
}
}
</style> </style>

View File

@ -1,71 +1,67 @@
<template> <template>
<div class="more-actions"> <div class="more-actions">
<a <a
:class="{'more-actions__toggle--background': background}" :class="{ 'more-actions__toggle--background': background }"
class="more-actions__toggle" class="more-actions__toggle"
data-cy="toggle-more-actions-menu" data-cy="toggle-more-actions-menu"
@click.stop="toggleMenu" @click.stop="toggleMenu"
> >
<ellipses /> <ellipses />
</a> </a>
<widget-popover <widget-popover class="more-actions__popover" v-if="showMenu" @hide-me="showMenu = false">
class="more-actions__popover"
v-if="showMenu"
@hide-me="showMenu = false"
>
<slot :toggle="toggleMenu" /> <slot :toggle="toggleMenu" />
</widget-popover> </widget-popover>
</div> </div>
</template> </template>
<script> <script>
import WidgetPopover from '@/components/ui/WidgetPopover'; import WidgetPopover from '@/components/ui/WidgetPopover';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Ellipses = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Ellipses')); const Ellipses = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/Ellipses'));
export default { export default {
props: { props: {
background: { background: {
type: Boolean, type: Boolean,
default: false default: false,
}
}, },
},
components: { components: {
Ellipses, Ellipses,
WidgetPopover, WidgetPopover,
}, },
data() { data() {
return { return {
showMenu: false, showMenu: false,
}; };
}, },
methods: { methods: {
toggleMenu: function () { toggleMenu: function () {
this.showMenu = !this.showMenu; this.showMenu = !this.showMenu;
},
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.more-actions { .more-actions {
svg { svg {
width: 30px; width: 30px;
fill: $color-charcoal-dark; fill: $color-charcoal-dark;
//margin-right: 15px; //margin-right: 15px;
} }
&__toggle { &__toggle {
display: flex; display: flex;
border-radius: 5px; border-radius: 5px;
&--background { &--background {
background: white; background: white;
}
} }
} }
}
</style> </style>

View File

@ -1,107 +1,82 @@
<template> <template>
<page-form <page-form class="room-form" title="Neuer Raum" @save="$emit('save', localRoom)">
class="room-form" <page-form-input label="Titel" v-model="localRoom.title" />
title="Neuer Raum"
@save="$emit('save', localRoom)"
>
<page-form-input
label="Titel"
v-model="localRoom.title"
/>
<page-form-input <page-form-input label="Beschreibung" type="textarea" v-model="localRoom.description" />
label="Beschreibung"
type="textarea"
v-model="localRoom.description"
/>
<h2 class="room-form__property-heading"> <h2 class="room-form__property-heading">Farbe</h2>
Farbe <color-chooser :selected-color="localRoom.appearance" @input="updateColor" />
</h2>
<color-chooser
:selected-color="localRoom.appearance"
@input="updateColor"
/>
<template #footer> <template #footer>
<button <button type="submit" data-cy="room-form-save" class="button button--primary room-form__save-button">
type="submit"
data-cy="room-form-save"
class="button button--primary room-form__save-button"
>
Speichern Speichern
</button> </button>
<router-link <router-link to="/rooms" class="button"> Abbrechen </router-link>
to="/rooms"
class="button"
>
Abbrechen
</router-link>
</template> </template>
</page-form> </page-form>
</template> </template>
<script> <script>
import ColorChooser from '@/components/ColorChooser'; import ColorChooser from '@/components/ColorChooser';
import PageForm from '@/components/page-form/PageForm'; import PageForm from '@/components/page-form/PageForm';
import PageFormInput from '@/components/page-form/PageFormInput'; import PageFormInput from '@/components/page-form/PageFormInput';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
export default { export default {
props: ['room'], props: ['room'],
components: { components: {
ColorChooser, ColorChooser,
PageForm, PageForm,
PageFormInput PageFormInput,
},
data() {
return {
localRoom: Object.assign({}, this.room),
me: {},
};
},
created() {
this.$store.dispatch('setSpecialContainerClass', this.localRoom.appearance);
},
beforeUnmount() {
this.$store.dispatch('setSpecialContainerClass', '');
},
methods: {
updateColor(newColor) {
this.localRoom.appearance = newColor;
this.$store.dispatch('setSpecialContainerClass', newColor);
}, },
},
data() { apollo: {
return { me: {
localRoom: Object.assign({}, this.room), query: ME_QUERY,
me: {}
};
}, },
},
created() { };
this.$store.dispatch('setSpecialContainerClass', this.localRoom.appearance);
},
beforeUnmount() {
this.$store.dispatch('setSpecialContainerClass', '');
},
methods: {
updateColor(newColor) {
this.localRoom.appearance = newColor;
this.$store.dispatch('setSpecialContainerClass', newColor);
}
},
apollo: {
me: {
query: ME_QUERY,
}
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.room-form { .room-form {
&__property-heading { &__property-heading {
@include page-form-input-heading; @include page-form-input-heading;
}
&__input,
&__textarea {
width: 100%;
margin-bottom: 35px;
}
&__save-button {
margin-right: 15px;
}
} }
&__input,
&__textarea {
width: 100%;
margin-bottom: 35px;
}
&__save-button {
margin-right: 15px;
}
}
</style> </style>

View File

@ -8,34 +8,34 @@
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Group = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Group.vue')); const Group = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/Group.vue'));
export default { export default {
props: ['name'], props: ['name'],
components: { components: {
Group Group,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.room-group-widget { .room-group-widget {
display: flex; display: flex;
align-items: center; align-items: center;
opacity: 0.6; opacity: 0.6;
svg { svg {
width: 30px; width: 30px;
fill: $color-charcoal-dark; fill: $color-charcoal-dark;
margin-right: 15px; margin-right: 15px;
}
& > span {
@include room-widget-text-style;;
}
} }
& > span {
@include room-widget-text-style;
}
}
</style> </style>

View File

@ -13,42 +13,43 @@
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon')); const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon'));
const ClosedEyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ClosedEyeIcon')); const ClosedEyeIcon = defineAsyncComponent(() =>
import(/* webpackChunkName: "icons" */ '@/components/icons/ClosedEyeIcon')
);
export default { export default {
props: { props: {
restricted: { restricted: {
type: Boolean, type: Boolean,
default: false default: false,
}
}, },
},
components: { components: {
ClosedEyeIcon, ClosedEyeIcon,
EyeIcon EyeIcon,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.room-visibility-widget { .room-visibility-widget {
display: flex; display: flex;
align-items: center; align-items: center;
opacity: 0.6; opacity: 0.6;
&__icon { &__icon {
width: 30px; width: 30px;
fill: $color-charcoal-dark; fill: $color-charcoal-dark;
margin-right: 15px; margin-right: 15px;
}
& > span {
@include room-widget-text-style;
}
} }
& > span {
@include room-widget-text-style;
}
}
</style> </style>

View File

@ -1,12 +1,6 @@
<template> <template>
<div <div :class="roomClass" class="room-widget">
:class="roomClass" <router-link :to="{ name: 'room', params: { slug: slug } }" class="room-widget__content">
class="room-widget"
>
<router-link
:to="{name: 'room', params: {slug: slug}}"
class="room-widget__content"
>
<h2 class="room-widget__title"> <h2 class="room-widget__title">
{{ title }} {{ title }}
</h2> </h2>

View File

@ -1,25 +1,12 @@
<template> <template>
<div class="rooms-onboarding"> <div class="rooms-onboarding">
<h1 <h1 class="rooms-onboarding__heading" data-cy="page-title">Räume</h1>
class="rooms-onboarding__heading"
data-cy="page-title"
>
Räume
</h1>
<rooms-illustration class="rooms-onboarding__illustration" /> <rooms-illustration class="rooms-onboarding__illustration" />
<p <p data-cy="rooms-onboarding-text" class="rooms-onboarding__text">
data-cy="rooms-onboarding-text"
class="rooms-onboarding__text"
>
Hier können Sie Räume erstellen, damit SchülerInnen zusammenarbeiten und Beiträge teilen können. Hier können Sie Räume erstellen, damit SchülerInnen zusammenarbeiten und Beiträge teilen können.
</p> </p>
<div class="rooms-onboarding__button"> <div class="rooms-onboarding__button">
<router-link <router-link :to="newRoomRoute" class="button button--primary" data-cy="create-room-button" v-if="isTeacher">
:to="newRoomRoute"
class="button button--primary"
data-cy="create-room-button"
v-if="isTeacher"
>
Raum erstellen Raum erstellen
</router-link> </router-link>
</div> </div>
@ -27,43 +14,45 @@
</template> </template>
<script> <script>
import {NEW_ROOM_PAGE} from '@/router/room.names'; import { NEW_ROOM_PAGE } from '@/router/room.names';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const RoomsIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/RoomsIllustration')); const RoomsIllustration = defineAsyncComponent(() =>
import(/* webpackChunkName: "illustrations" */ '@/components/illustrations/RoomsIllustration')
);
export default { export default {
props: { props: {
isTeacher: { isTeacher: {
type: Boolean, type: Boolean,
default: false, default: false,
},
}, },
components: {RoomsIllustration}, },
components: { RoomsIllustration },
data() { data() {
return { return {
newRoomRoute: NEW_ROOM_PAGE, newRoomRoute: NEW_ROOM_PAGE,
}; };
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.rooms-onboarding { .rooms-onboarding {
@include onboarding-page; @include onboarding-page;
&__heading { &__heading {
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
}
&__illustration {
@include onboarding-illustration;
}
&__text {
@include onboarding-text;
}
} }
&__illustration {
@include onboarding-illustration;
}
&__text {
@include onboarding-text;
}
}
</style> </style>

View File

@ -13,12 +13,7 @@
/> />
<chevron-down class="selected-class__dropdown-icon" /> <chevron-down class="selected-class__dropdown-icon" />
</div> </div>
<widget-popover <widget-popover :mobile="mobile" class="class-selection__popover" v-if="showPopover" @hide-me="showPopover = false">
:mobile="mobile"
class="class-selection__popover"
v-if="showPopover"
@hide-me="showPopover = false"
>
<li <li
:label="schoolClass.name" :label="schoolClass.name"
:item="schoolClass" :item="schoolClass"
@ -59,8 +54,8 @@
</template> </template>
<script> <script>
import WidgetPopover from '@/components/ui/WidgetPopover'; import WidgetPopover from '@/components/ui/WidgetPopover';
import CurrentClass from '@/components/school-class/CurrentClass'; import CurrentClass from '@/components/school-class/CurrentClass';
import updateSelectedClassMixin from '@/mixins/update-selected-class'; import updateSelectedClassMixin from '@/mixins/update-selected-class';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
@ -69,37 +64,36 @@
const ChevronDown = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown')); const ChevronDown = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown'));
const AddIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon')); const AddIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon'));
export default { export default {
props: {
props: { mobile: {
mobile: { type: Boolean,
type: Boolean, default: false,
default: false
}
}, },
},
mixins: [updateSelectedClassMixin, sidebarMixin, meMixin], mixins: [updateSelectedClassMixin, sidebarMixin, meMixin],
components: { components: {
WidgetPopover, WidgetPopover,
ChevronDown, ChevronDown,
CurrentClass, CurrentClass,
AddIcon AddIcon,
}, },
data() { data() {
return { return {
showPopover: false showPopover: false,
}; };
}, },
computed: { computed: {
currentClassSelection() { currentClassSelection() {
let currentClass = this.me.schoolClasses.find(schoolClass => { let currentClass = this.me.schoolClasses.find((schoolClass) => {
return schoolClass.id === this.me.selectedClass.id; return schoolClass.id === this.me.selectedClass.id;
}); });
return currentClass || this.me.schoolClasses[0]; return currentClass || this.me.schoolClasses[0];
}
}, },
},
methods: { methods: {
toggle() { toggle() {
@ -111,47 +105,46 @@
this.closeSidebar('profile'); this.closeSidebar('profile');
} }
}, },
},
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.class-selection { .class-selection {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;
border-radius: 4px; border-radius: 4px;
&__popover { &__popover {
white-space: nowrap; white-space: nowrap;
top: 100%; top: 100%;
left: 0; left: 0;
transform: translateY($small-spacing); transform: translateY($small-spacing);
} }
}
.selected-class {
width: 100%;
box-sizing: border-box;
padding: $small-spacing 0;
display: flex;
align-items: center;
justify-content: flex-start;
&__text {
line-height: $large-spacing;
@include heading-4;
margin-right: $small-spacing;
} }
.selected-class { &__dropdown-icon {
width: 100%; width: 20px;
box-sizing: border-box; height: 20px;
padding: $small-spacing 0; fill: $color-charcoal-dark;
display: flex;
align-items: center;
justify-content: flex-start;
&__text {
line-height: $large-spacing;
@include heading-4;
margin-right: $small-spacing;
}
&__dropdown-icon {
width: 20px;
height: 20px;
fill: $color-charcoal-dark;
}
} }
}
</style> </style>

View File

@ -1,27 +1,24 @@
<template> <template>
<span <span class="current-class" data-cy="current-class-name">{{ currentClassName }}</span>
class="current-class"
data-cy="current-class-name"
>{{ currentClassName }}</span>
</template> </template>
<script> <script>
import me from '@/mixins/me'; import me from '@/mixins/me';
export default { export default {
mixins: [me], mixins: [me],
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.current-class { .current-class {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-self: center; align-self: center;
line-height: 1; line-height: 1;
@include regular-text; @include regular-text;
} }
</style> </style>

View File

@ -6,26 +6,19 @@
class="base-input-container__input" class="base-input-container__input"
data-cy="base-input-input" data-cy="base-input-input"
@change.prevent="$emit('input', $event.target.checked, item)" @change.prevent="$emit('input', $event.target.checked, item)"
> />
<span <span
:class="{'base-input-container__checkbox': type==='checkbox', 'base-input-container__radiobutton': type === 'radiobutton'}" :class="{
'base-input-container__checkbox': type === 'checkbox',
'base-input-container__radiobutton': type === 'radiobutton',
}"
class="base-input-container__icon checkbox" class="base-input-container__icon checkbox"
> >
<tick v-if="type === 'checkbox'" /> <tick v-if="type === 'checkbox'" />
<circle-icon <circle-icon data-cy="circle-icon" v-if="type === 'radiobutton'" />
data-cy="circle-icon"
v-if="type === 'radiobutton'"
/>
</span> </span>
<span <span class="base-input-container__label" data-cy="base-input-label" v-if="label">{{ label }}</span>
class="base-input-container__label" <slot class="base-input-container__label" v-if="!label" />
data-cy="base-input-label"
v-if="label"
>{{ label }}</span>
<slot
class="base-input-container__label"
v-if="!label"
/>
</label> </label>
</template> </template>
@ -34,19 +27,19 @@
const Tick = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Tick')); const Tick = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Tick'));
const CircleIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CircleIcon')); const CircleIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CircleIcon'));
export default { export default {
props: { props: {
label: String, label: String,
checked: { checked: {
type: Boolean type: Boolean,
},
item: Object,
type: String
}, },
item: Object,
type: String,
},
components: { components: {
Tick, Tick,
CircleIcon CircleIcon,
} },
}; };
</script> </script>

View File

@ -1,65 +1,57 @@
<template> <template>
<li <li class="popover-links__link">
class="popover-links__link" <a class="popover-link" @click="$emit('link-action')">
> <component class="popover-link__icon" :is="icon" />
<a
class="popover-link"
@click="$emit('link-action')"
>
<component
class="popover-link__icon"
:is="icon"
/>
<span class="popover-link__text">{{ text }}</span> <span class="popover-link__text">{{ text }}</span>
</a> </a>
</li> </li>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon')); const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon'));
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon')); const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'));
const PenIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PenIcon')); const PenIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/PenIcon'));
export default { export default {
props: { props: {
icon: { icon: {
type: String, type: String,
default: '', default: '',
},
text: {
type: String,
default: '',
},
}, },
components: { text: {
EyeIcon, type: String,
TrashIcon, default: '',
PenIcon,
}, },
}; },
components: {
EyeIcon,
TrashIcon,
PenIcon,
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.popover-link { .popover-link {
@include popover-link; @include popover-link;
&__icon { &__icon {
width: 30px; width: 30px;
fill: $color-charcoal-dark; fill: $color-charcoal-dark;
margin-right: 15px; margin-right: 15px;
display: flex; display: flex;
flex-basis: auto; flex-basis: auto;
flex-shrink: 0; flex-shrink: 0;
}
&__text {
width: auto;
display: flex;
flex-basis: auto;
flex-shrink: 0;
}
} }
&__text {
width: auto;
display: flex;
flex-basis: auto;
flex-shrink: 0;
}
}
</style> </style>

View File

@ -1,11 +1,7 @@
<template> <template>
<div class="file-upload"> <div class="file-upload">
<template v-if="document"> <template v-if="document">
<document-block <document-block :value="{ url: document }" show-trash-icon @trash="$emit('change-document-url', '')" />
:value="{url: document}"
show-trash-icon
@trash="$emit('change-document-url', '')"
/>
</template> </template>
<template v-else> <template v-else>
<simple-file-upload <simple-file-upload
@ -18,26 +14,27 @@
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const SimpleFileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUpload')); const SimpleFileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUpload'));
const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock')); const DocumentBlock = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/DocumentBlock')
);
export default { export default {
props: { props: {
document: { document: {
type: String, type: String,
default: '', default: '',
},
withText: {
type: Boolean,
default: false
}
}, },
components: {SimpleFileUpload, DocumentBlock}, withText: {
}; type: Boolean,
default: false,
},
},
components: { SimpleFileUpload, DocumentBlock },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
</style> </style>

View File

@ -5,21 +5,23 @@
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon')); const DocumentIcon = defineAsyncComponent(() =>
import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon')
);
export default { export default {
components: {DocumentIcon}, components: { DocumentIcon },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.simple-file-upload-icon { .simple-file-upload-icon {
&__icon { &__icon {
width: 25px; width: 25px;
fill: $color-silver-dark; fill: $color-silver-dark;
}
} }
}
</style> </style>

View File

@ -30,5 +30,5 @@ export default {
ArrowThinDown, ArrowThinDown,
ArrowThinTop, ArrowThinTop,
ArrowThinUp, ArrowThinUp,
TrashIcon TrashIcon,
}; };

View File

@ -1,11 +1,6 @@
<template> <template>
<div <div class="skillboxform-input">
class="skillboxform-input" <label :for="id" class="skillboxform-input__label">
>
<label
:for="id"
class="skillboxform-input__label"
>
{{ label }} {{ label }}
</label> </label>
<slot :id="id" /> <slot :id="id" />
@ -13,8 +8,8 @@
</template> </template>
<script setup> <script setup>
defineProps({ defineProps({
id: String, id: String,
label: String label: String,
}) ; });
</script> </script>

View File

@ -1,93 +1,87 @@
<template> <template>
<div class="visibility-action"> <div class="visibility-action">
<a <a class="visibility-action__action-button" v-if="canManageContent" @click="toggleVisibility()">
class="visibility-action__action-button" <closed-eye-icon class="visibility-action__action-icon action-icon" v-if="hidden" />
v-if="canManageContent" <eye-icon class="visibility-action__action-icon action-icon" v-else />
@click="toggleVisibility()"
>
<closed-eye-icon
class="visibility-action__action-icon action-icon"
v-if="hidden"
/>
<eye-icon
class="visibility-action__action-icon action-icon"
v-else
/>
</a> </a>
</div> </div>
</template> </template>
<script> <script>
import me from '@/mixins/me'; import me from '@/mixins/me';
import {TYPES, CONTENT_TYPE} from '@/consts/types'; import { TYPES, CONTENT_TYPE } from '@/consts/types';
import {createVisibilityMutation, hidden} from '@/helpers/visibility'; import { createVisibilityMutation, hidden } from '@/helpers/visibility';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon')); const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon'));
const ClosedEyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ClosedEyeIcon')); const ClosedEyeIcon = defineAsyncComponent(() =>
import(/* webpackChunkName: "icons" */ '@/components/icons/ClosedEyeIcon')
);
export default { export default {
props: { props: {
block: { block: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
},
type: {
type: String,
default: CONTENT_TYPE,
validator: (value) => {
// value must be one of TYPES
return TYPES.indexOf(value) !== -1;
}, },
type: {
type: String,
default: CONTENT_TYPE,
validator: value => {
// value must be one of TYPES
return TYPES.indexOf(value) !== -1;
}
}
}, },
},
mixins: [me], mixins: [me],
components: { components: {
EyeIcon, EyeIcon,
ClosedEyeIcon ClosedEyeIcon,
},
computed: {
hidden() {
return hidden({ type: this.type, block: this.block, schoolClass: this.schoolClass });
}, },
},
computed: { methods: {
hidden() { toggleVisibility() {
return hidden({type: this.type, block: this.block, schoolClass: this.schoolClass}); const hidden = !this.hidden;
} const schoolClassId = this.schoolClass.id;
},
methods: { const visibility = [
toggleVisibility() { {
const hidden = !this.hidden;
const schoolClassId = this.schoolClass.id;
const visibility = [{
schoolClassId, schoolClassId,
hidden hidden,
}]; },
];
const {mutation, variables} = createVisibilityMutation(this.type, this.block.id, visibility); const { mutation, variables } = createVisibilityMutation(this.type, this.block.id, visibility);
this.$apollo.mutate({ this.$apollo.mutate({
mutation, mutation,
variables variables,
}); });
},
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.visibility-action { .visibility-action {
margin-top: 9px; margin-top: 9px;
position: absolute; position: absolute;
left: -70px; left: -70px;
top: 0px; top: 0px;
display: grid; display: grid;
&__visibility-menu { &__visibility-menu {
top: 40px; top: 40px;
}
} }
}
</style> </style>

View File

@ -1,56 +1,29 @@
<template> <template>
<footer <footer class="default-footer" data-cy="page-footer">
class="default-footer"
data-cy="page-footer"
>
<div class="default-footer__section"> <div class="default-footer__section">
<div class="default-footer__info"> <div class="default-footer__info">
<div class="default-footer__who-are-we who-are-we"> <div class="default-footer__who-are-we who-are-we">
<h5 class="who-are-we__title"> <h5 class="who-are-we__title">Wer sind wir?</h5>
Wer sind wir?
</h5>
<p class="who-are-we__text"> <p class="who-are-we__text">
mySkillbox ist ein Angebot des hep Verlags in mySkillbox ist ein Angebot des hep Verlags in Zusammenarbeit mit der Eidgenössischen Hochschule für
Zusammenarbeit mit der Eidgenössischen Hochschule für Berufsbildung (EHB). Berufsbildung (EHB).
</p> </p>
</div> </div>
<a <a href="https://www.hep-verlag.ch/" target="_blank">
href="https://www.hep-verlag.ch/"
target="_blank"
>
<hep-logo class="default-footer__logo-hep" /> <hep-logo class="default-footer__logo-hep" />
</a> </a>
<a <a href="https://www.ehb.swiss/" target="_blank">
href="https://www.ehb.swiss/"
target="_blank"
>
<ehb-logo class="default-footer__logo-ehb" /> <ehb-logo class="default-footer__logo-ehb" />
</a> </a>
</div> </div>
</div> </div>
<div class="default-footer__section"> <div class="default-footer__section">
<div class="default-footer__links"> <div class="default-footer__links">
<a <a href="https://myskillbox.ch/datenschutz" target="_blank" class="default-footer__link">Datenschutz</a>
href="https://myskillbox.ch/datenschutz" <a href="https://myskillbox.ch/impressum" target="_blank" class="default-footer__link">Impressum</a>
target="_blank" <a href="https://myskillbox.ch/agb" target="_blank" class="default-footer__link">AGB</a>
class="default-footer__link" <a :href="$flavor.supportLink" target="_blank" class="default-footer__link">Support</a>
>Datenschutz</a>
<a
href="https://myskillbox.ch/impressum"
target="_blank"
class="default-footer__link"
>Impressum</a>
<a
href="https://myskillbox.ch/agb"
target="_blank"
class="default-footer__link"
>AGB</a>
<a
:href="$flavor.supportLink"
target="_blank"
class="default-footer__link"
>Support</a>
</div> </div>
</div> </div>
</footer> </footer>
@ -62,99 +35,99 @@
const HepLogo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/HepLogo')); const HepLogo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/HepLogo'));
const EhbLogo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EhbLogo')); const EhbLogo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EhbLogo'));
export default { export default {
components: { components: {
HepLogo, HepLogo,
EhbLogo EhbLogo,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.default-footer { .default-footer {
background-color: $color-silver-light; background-color: $color-silver-light;
max-width: 100vw; max-width: 100vw;
overflow: hidden; overflow: hidden;
&__section { &__section {
width: 100%; width: 100%;
border-bottom: $color-silver 1px solid; border-bottom: $color-silver 1px solid;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
&__info { &__info {
width: 100%; width: 100%;
max-width: $footer-width; max-width: $footer-width;
padding: 2*$large-spacing 0; padding: 2 * $large-spacing 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@include desktop { @include desktop {
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
}
}
&__who-are-we {
width: 100%;
margin-bottom: $large-spacing;
@include desktop {
width: 330px;
margin-bottom: 0;
}
}
&__logo-hep {
width: auto;
height: 35px;
margin-bottom: $large-spacing;
@include desktop {
width: 147px;
margin-bottom: 0;
}
}
&__logo-ehb {
width: 100px;
height: 32px;
}
&__links {
width: 100%;
max-width: $footer-width;
padding: $large-spacing 0;
display: flex;
flex-direction: column;
@include desktop {
flex-direction: row;
}
}
&__link {
@include aside-with-cheese;
margin-right: $large-spacing;
margin-bottom: $small-spacing;
@include desktop {
margin-bottom: 0;
}
} }
} }
.who-are-we { &__who-are-we {
&__title { width: 100%;
@include heading-4; margin-bottom: $large-spacing;
}
&__text { @include desktop {
@include aside-text; width: 330px;
margin-bottom: 0;
} }
} }
&__logo-hep {
width: auto;
height: 35px;
margin-bottom: $large-spacing;
@include desktop {
width: 147px;
margin-bottom: 0;
}
}
&__logo-ehb {
width: 100px;
height: 32px;
}
&__links {
width: 100%;
max-width: $footer-width;
padding: $large-spacing 0;
display: flex;
flex-direction: column;
@include desktop {
flex-direction: row;
}
}
&__link {
@include aside-with-cheese;
margin-right: $large-spacing;
margin-bottom: $small-spacing;
@include desktop {
margin-bottom: 0;
}
}
}
.who-are-we {
&__title {
@include heading-4;
}
&__text {
@include aside-text;
}
}
</style> </style>

View File

@ -1,12 +1,6 @@
<template> <template>
<div <div :class="specialContainerClass" class="container layout layout--fullscreen">
:class="specialContainerClass" <div class="close-button" @click="back">
class="container layout layout--fullscreen"
>
<div
class="close-button"
@click="back"
>
<cross class="close-button__icon" /> <cross class="close-button__icon" />
</div> </div>
@ -15,39 +9,38 @@
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon')); const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'));
export default { export default {
components: { components: {
Cross Cross,
}, },
computed: { computed: {
specialContainerClass() { specialContainerClass() {
let cls = this.$store.state.specialContainerClass; let cls = this.$store.state.specialContainerClass;
return [cls ? `skillbox--${cls}` : '']; return [cls ? `skillbox--${cls}` : ''];
}
}, },
methods: { },
back() { methods: {
this.$router.go(-1); back() {
} this.$router.go(-1);
} },
}; },
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/styles/_default-layout.scss"; @import '@/styles/_default-layout.scss';
.close-button { .close-button {
margin-top: $medium-spacing; margin-top: $medium-spacing;
margin-right: $medium-spacing; margin-right: $medium-spacing;
justify-self: end; justify-self: end;
cursor: pointer; cursor: pointer;
display:flex;
justify-content:flex-end;
}
display: flex;
justify-content: flex-end;
}
</style> </style>

View File

@ -1,80 +1,74 @@
<template> <template>
<div class="layout layout--public public"> <div class="layout layout--public public">
<div class="public__logo"> <div class="public__logo">
<router-link <router-link :to="{ name: 'hello' }" class="hep-link">
:to="{name: 'hello'}"
class="hep-link"
>
<logo /> <logo />
</router-link> </router-link>
</div> </div>
<router-view class="public__content layout__content" /> <router-view class="public__content layout__content" />
<default-footer <default-footer class="skillbox__footer public__footer footer" v-if="$flavor.showFooter" />
class="skillbox__footer public__footer footer"
v-if="$flavor.showFooter"
/>
</div> </div>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
import DefaultFooter from '@/layouts/DefaultFooter'; import DefaultFooter from '@/layouts/DefaultFooter';
const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo')); const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/Logo'));
export default { export default {
components: { components: {
Logo, Logo,
DefaultFooter DefaultFooter,
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
@import "@/styles/_default-layout.scss"; @import '@/styles/_default-layout.scss';
@mixin content-block { @mixin content-block {
padding-right: $medium-spacing; padding-right: $medium-spacing;
padding-left: $medium-spacing; padding-left: $medium-spacing;
max-width: 800px; max-width: 800px;
min-width: 320px; min-width: 320px;
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
} }
.logo { .logo {
position: relative; position: relative;
width: auto;
height: 43px;
}
.public {
grid-template-areas: 'h' 'c' 'f';
&__content {
@include content-block();
margin-bottom: $large-spacing;
width: auto; width: auto;
height: 43px;
} }
.public { &__logo {
grid-template-areas: "h" "c" "f"; @include content-block();
margin-top: $medium-spacing;
&__content {
@include content-block();
margin-bottom: $large-spacing;
width: auto;
}
&__logo {
@include content-block();
margin-top: $medium-spacing
}
&__footer {
background-color: $color-silver-light;
display: block;
}
} }
.footer { &__footer {
padding: $large-spacing $medium-spacing 0; background-color: $color-silver-light;
display: block;
&__content {
@include content-block();
}
} }
}
.footer {
padding: $large-spacing $medium-spacing 0;
&__content {
@include content-block();
}
}
</style> </style>

View File

@ -1,107 +1,98 @@
<template> <template>
<div <div :class="{ 'layout--full-width': $route.meta.fullWidth }" class="skillbox layout layout--simple">
:class="{'layout--full-width': $route.meta.fullWidth}" <div class="close-button" @click="back">
class="skillbox layout layout--simple"
>
<div
class="close-button"
@click="back"
>
<cross class="close-button__icon" /> <cross class="close-button__icon" />
</div> </div>
<router-view class="layout__content" /> <router-view class="layout__content" />
<simple-footer <simple-footer class="layout__footer" v-if="enableFooter" />
class="layout__footer"
v-if="enableFooter"
/>
</div> </div>
</template> </template>
<script> <script>
import SimpleFooter from '@/layouts/SimpleFooter'; import SimpleFooter from '@/layouts/SimpleFooter';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon')); const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'));
export default { export default {
components: { components: {
Cross, Cross,
SimpleFooter SimpleFooter,
}, },
computed: { computed: {
enableFooter() { enableFooter() {
if (this.$route.meta.hideFooter) { if (this.$route.meta.hideFooter) {
return false; return false;
}
return this.$flavor.showFooter;
} }
return this.$flavor.showFooter;
}, },
},
methods: { methods: {
back() { back() {
this.$router.go(-1); this.$router.go(-1);
} },
} },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~styles/helpers"; @import '~styles/helpers';
.layout { .layout {
&--simple { &--simple {
display: -ms-grid; display: -ms-grid;
@supports (display: grid) { @supports (display: grid) {
display: grid; display: grid;
}
width: 100%;
@include desktop {
grid-template-columns: 1fr 640px 1fr;
grid-template-rows: 60px auto-fill 105px;
-ms-grid-columns: 1fr 640px 1fr;
& > :nth-child(2) {
grid-column: 2;
-ms-grid-column: 2;
}
}
} }
$parent: &; width: 100%;
&--full-width {
#{$parent}__content {
grid-column: 1 / span 3;
grid-row: 1 / span 2;
}
}
&__footer {
@include desktop {
grid-column: 1 / span 3;
}
}
}
.close-button {
justify-self: end;
cursor: pointer;
display:flex;
justify-content:flex-end;
margin-right: $small-spacing;
margin-top: $small-spacing;
@include desktop { @include desktop {
grid-column: 3; grid-template-columns: 1fr 640px 1fr;
grid-row: 1; grid-template-rows: 60px auto-fill 105px;
-ms-grid-column: 3; -ms-grid-columns: 1fr 640px 1fr;
-ms-grid-row: 1;
margin-right: $medium-spacing; & > :nth-child(2) {
margin-top: $medium-spacing; grid-column: 2;
-ms-grid-column: 2;
}
} }
} }
$parent: &;
&--full-width {
#{$parent}__content {
grid-column: 1 / span 3;
grid-row: 1 / span 2;
}
}
&__footer {
@include desktop {
grid-column: 1 / span 3;
}
}
}
.close-button {
justify-self: end;
cursor: pointer;
display: flex;
justify-content: flex-end;
margin-right: $small-spacing;
margin-top: $small-spacing;
@include desktop {
grid-column: 3;
grid-row: 1;
-ms-grid-column: 3;
-ms-grid-row: 1;
margin-right: $medium-spacing;
margin-top: $medium-spacing;
}
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div :class="['split-view', {'split-view--illustration': illustration}]"> <div :class="['split-view', { 'split-view--illustration': illustration }]">
<div :class="['split-view__illustration', illustrationAlignment]"> <div :class="['split-view__illustration', illustrationAlignment]">
<component :is="illustration" /> <component :is="illustration" />
</div> </div>
@ -19,145 +19,147 @@
const HelloMyKVIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/HelloMyKVIllustration')); const HelloMyKVIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/HelloMyKVIllustration'));
const Hello = flavorValues.appFlavor === 'my-kv' ? HelloMyKVIllustration : HelloIllustration; const Hello = flavorValues.appFlavor === 'my-kv' ? HelloMyKVIllustration : HelloIllustration;
export default { export default {
components: { components: {
contents: ContentsIllustration, contents: ContentsIllustration,
portfolio: PortfolioIllustration, portfolio: PortfolioIllustration,
rooms: RoomsIllustration, rooms: RoomsIllustration,
hello: Hello hello: Hello,
}, },
computed: { computed: {
illustration() { illustration() {
return this.$route.meta.illustration; return this.$route.meta.illustration;
},
illustrationAlignment() {
return this.$route.meta.illustrationAlign ? `split-view__illustration--${this.$route.meta.illustrationAlign}` : '';
}
}, },
}; illustrationAlignment() {
return this.$route.meta.illustrationAlign
? `split-view__illustration--${this.$route.meta.illustrationAlign}`
: '';
},
},
};
</script> </script>
<style lang="scss"> <style lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.split-view { .split-view {
background-color: $color-brand; background-color: $color-brand;
display: flex; display: flex;
position: relative; position: relative;
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
&--illustration { &--illustration {
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
top: 50%; top: 50%;
background: $color-brand-dark; background: $color-brand-dark;
}
}
&__illustration {
width: 30vw;
min-width: 400px;
align-self: center;
background-color: $color-brand;
z-index: 1;
display: none;
&--top {
align-self: center;
margin-top: -400px;
}
& > svg {
max-width: 300px;
max-height: 400px;
}
@include desktop {
display: flex;
justify-content: center;
}
}
&__content {
background-color: $color-white;
padding: $medium-spacing;
display: flex;
flex-direction: column;
z-index: 1;
width: 100%;
@include desktop {
padding: 2*$large-spacing;
}
}
&__logo {
width: 300px;
height: 50px;
margin-bottom: $large-spacing;
@include desktop {
margin-bottom: 70px;
}
}
&__page-subheading {
@include regular-text;
color: $color-brand;
margin-bottom: $small-spacing;
}
&__page-heading {
@include heading-2;
color: $color-brand;
margin-bottom: 2*$large-spacing;
}
&__heading {
@include heading-2;
margin-bottom: $small-spacing;
}
&__claim {
@include heading-2;
margin-bottom: 70px;
}
&__paragraph {
@include regular-text;
margin-bottom: $medium-spacing;
&:last-of-type {
margin-bottom: 2*$large-spacing;
}
}
&__button {
@include regular-text;
flex-grow: 0;
align-self: flex-start;
min-width: 150px;
display: inline-flex;
box-sizing: border-box;
justify-content: center;
margin-bottom: $large-spacing;
cursor: pointer;
}
&__secondary-link {
@include inline-title;
cursor: pointer;
@include desktop {
margin-top: auto;
}
} }
} }
&__illustration {
width: 30vw;
min-width: 400px;
align-self: center;
background-color: $color-brand;
z-index: 1;
display: none;
&--top {
align-self: center;
margin-top: -400px;
}
& > svg {
max-width: 300px;
max-height: 400px;
}
@include desktop {
display: flex;
justify-content: center;
}
}
&__content {
background-color: $color-white;
padding: $medium-spacing;
display: flex;
flex-direction: column;
z-index: 1;
width: 100%;
@include desktop {
padding: 2 * $large-spacing;
}
}
&__logo {
width: 300px;
height: 50px;
margin-bottom: $large-spacing;
@include desktop {
margin-bottom: 70px;
}
}
&__page-subheading {
@include regular-text;
color: $color-brand;
margin-bottom: $small-spacing;
}
&__page-heading {
@include heading-2;
color: $color-brand;
margin-bottom: 2 * $large-spacing;
}
&__heading {
@include heading-2;
margin-bottom: $small-spacing;
}
&__claim {
@include heading-2;
margin-bottom: 70px;
}
&__paragraph {
@include regular-text;
margin-bottom: $medium-spacing;
&:last-of-type {
margin-bottom: 2 * $large-spacing;
}
}
&__button {
@include regular-text;
flex-grow: 0;
align-self: flex-start;
min-width: 150px;
display: inline-flex;
box-sizing: border-box;
justify-content: center;
margin-bottom: $large-spacing;
cursor: pointer;
}
&__secondary-link {
@include inline-title;
cursor: pointer;
@include desktop {
margin-top: auto;
}
}
}
</style> </style>

View File

@ -96,16 +96,16 @@
ErrorMessage, ErrorMessage,
}, },
data() { data() {
return { return {
email: '', email: '',
password: '', password: '',
emailErrors: [], emailErrors: [],
passwordErrors: [], passwordErrors: [],
loginError: '', loginError: '',
submitted: false, submitted: false,
}; };
}, },
methods: { methods: {
required(value, {field}) { required(value, {field}) {
@ -124,13 +124,14 @@
}, },
}; };
const redirectUrl = this.$route.query.redirect ? this.$route.query.redirect : '/'; const redirectUrl = this.$route.query.redirect ? this.$route.query.redirect : '/';
this.$apollo.mutate({ this.$apollo
.mutate({
client: 'publicClient', client: 'publicClient',
mutation: BETA_LOGIN_MUTATION, mutation: BETA_LOGIN_MUTATION,
variables, variables,
update: (store, {data: {betaLogin}}) => { update: (store, { data: { betaLogin } }) => {
try { try {
if (betaLogin.success) { if (betaLogin.success) {
console.log(this); console.log(this);
@ -141,7 +142,8 @@
this.loginError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.'; this.loginError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.';
} }
}, },
}).catch(error => { })
.catch((error) => {
const firstError = error.graphQLErrors[0]; const firstError = error.graphQLErrors[0];
switch (firstError.message) { switch (firstError.message) {
case 'invalid_credentials': case 'invalid_credentials':
@ -158,24 +160,22 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.text-link { .text-link {
font-family: $sans-serif-font-family; font-family: $sans-serif-font-family;
color: $color-brand; color: $color-brand;
}
.actions {
display: flex;
justify-content: space-between;
&__reset {
display: inline-block;
margin-left: $large-spacing;
padding: 15px;
line-height: 19px;
} }
}
.actions {
display: flex;
justify-content: space-between;
&__reset {
display: inline-block;
margin-left: $large-spacing;
padding: 15px;
line-height: 19px;
}
}
</style> </style>

View File

@ -1,20 +1,15 @@
<template> <template>
<content-block-form <content-block-form title="Inhaltsblock erfassen" :content-block="contentBlock" @back="goToModule" @save="save" />
title="Inhaltsblock erfassen"
:content-block="contentBlock"
@back="goToModule"
@save="save"
/>
</template> </template>
<script> <script>
import {defineComponent} from 'vue'; import {defineComponent} from 'vue';
import ContentBlockForm from '@/components/content-block-form/ContentBlockForm'; import ContentBlockForm from '@/components/content-block-form/ContentBlockForm';
import {setUserBlockType} from '@/helpers/content-block'; import { setUserBlockType } from '@/helpers/content-block';
import NEW_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/addContentBlock.gql'; import NEW_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/addContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql'; import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import {cleanUpContents} from '@/components/content-block-form/helpers'; import { cleanUpContents } from '@/components/content-block-form/helpers';
export default defineComponent({ export default defineComponent({
props: { props: {
@ -27,59 +22,62 @@
default: '' default: ''
} }
}, },
},
components: { components: {
ContentBlockForm, ContentBlockForm,
},
data: () => ({
contentBlock: {
title: '',
isAssignment: false,
contents: [],
}, },
}),
data: () => ({ methods: {
contentBlock: { save({ title, contents, isAssignment }) {
title: '', let cleanedContents = cleanUpContents(contents);
isAssignment: false, const contentBlock = {
contents: [ title: title,
]}, contents: cleanedContents,
}), type: setUserBlockType(isAssignment),
};
methods: { let input;
save({title, contents, isAssignment}) { const { parent, after, slug } = this.$route.params;
let cleanedContents = cleanUpContents(contents); if (after) {
const contentBlock = { input = {
title: title, contentBlock,
contents: cleanedContents, after,
type: setUserBlockType(isAssignment),
}; };
let input; } else {
const { parent, after, slug } = this.$route.params; input = {
if(after) { contentBlock,
input = { parent,
contentBlock, };
after }
}; this.$apollo
} else { .mutate({
input = {
contentBlock,
parent
};
}
this.$apollo.mutate({
mutation: NEW_CONTENT_BLOCK_MUTATION, mutation: NEW_CONTENT_BLOCK_MUTATION,
variables: { variables: {
input input,
}, },
refetchQueries: [{ refetchQueries: [
query: MODULE_DETAILS_QUERY, {
variables: { query: MODULE_DETAILS_QUERY,
slug variables: {
} slug,
}] },
}).then(this.goToModule); },
}, ],
goToModule() { })
// use the history, so the scroll position is preserved .then(this.goToModule);
this.$router.go(-1); },
} goToModule() {
} // use the history, so the scroll position is preserved
this.$router.go(-1);
},
}); },
});
</script> </script>

View File

@ -1,64 +1,39 @@
<template> <template>
<div <div class="hello" data-cy="hello-page">
class="hello"
data-cy="hello-page"
>
<div class="about"> <div class="about">
<div class="about__logos logos"> <div class="about__logos logos">
<a <a href="https://www.hep-verlag.ch/" target="_blank">
href="https://www.hep-verlag.ch/"
target="_blank"
>
<hep-logo-no-claim class="logos__logo" /> <hep-logo-no-claim class="logos__logo" />
</a> </a>
<a <a href="https://www.ehb.swiss/" target="_blank" v-if="$flavor.showEHB">
href="https://www.ehb.swiss/"
target="_blank"
v-if="$flavor.showEHB"
>
<ehb-logo class="logos__logo" /> <ehb-logo class="logos__logo" />
</a> </a>
</div> </div>
<p class="about__text"> <p class="about__text">
<template v-if="$flavor.showEHB"> <template v-if="$flavor.showEHB">
{{ $flavor.textAppName }} ist ein Angebot des hep Verlags in {{ $flavor.textAppName }} ist ein Angebot des hep Verlags in Zusammenarbeit mit der Eidgenössischen Hochschule
Zusammenarbeit mit der Eidgenössischen Hochschule für Berufsbildung (EHB). für Berufsbildung (EHB).
</template>
<template v-else>
{{ $flavor.textAppName }} ist ein Angebot des hep Verlags.
</template> </template>
<template v-else> {{ $flavor.textAppName }} ist ein Angebot des hep Verlags. </template>
</p> </p>
</div> </div>
<logo class="logo" /> <logo class="logo" />
<div class="login-actions"> <div class="login-actions">
<h2 <h2 class="login-actions__title" data-cy="hello-title">
class="login-actions__title"
data-cy="hello-title"
>
Wollen Sie {{ $flavor.textAppName }} im Unterricht verwenden? Wollen Sie {{ $flavor.textAppName }} im Unterricht verwenden?
</h2> </h2>
<a <a class="button button--primary button--big actions__submit" href="/api/oauth/login/" data-cy="oauth-login"
class="button button--primary button--big actions__submit" >Mit hep Konto anmelden</a
href="/api/oauth/login/" >
data-cy="oauth-login"
>Mit hep Konto anmelden</a>
<div class="login-actions__register register"> <div class="login-actions__register register">
<p>Haben Sie noch kein hep Konto?</p> <p>Haben Sie noch kein hep Konto?</p>
<a <a class="hep-link" href="/api/oauth/login/" data-cy="oauth-login">Jetzt registrieren</a>
class="hep-link"
href="/api/oauth/login/"
data-cy="oauth-login"
>Jetzt registrieren</a>
</div> </div>
</div> </div>
<div class="information"> <div class="information">
<p>Was ist ein hep Konto und wie kann ich mich dafür registrieren?</p> <p>Was ist ein hep Konto und wie kann ich mich dafür registrieren?</p>
<a <a class="hep-link" href="https://myskillbox.ch/anleitung" data-cy="oauth-login">Anleitung anschauen</a>
class="hep-link"
href="https://myskillbox.ch/anleitung"
data-cy="oauth-login"
>Anleitung anschauen</a>
</div> </div>
<div class="links"> <div class="links">
<ul class="links__list"> <ul class="links__list">
@ -82,128 +57,127 @@
const EhbLogo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EhbLogo')); const EhbLogo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EhbLogo'));
const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo')); const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo'));
export default { export default {
components: { components: {
HepLogoNoClaim, HepLogoNoClaim,
EhbLogo, EhbLogo,
Logo Logo,
}, },
data() { data() {
return { return {
email: '', email: '',
submitted: false, submitted: false,
loading: false loading: false,
}; };
}, },
};
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
$hello-block-margin: 2*$medium-spacing; $hello-block-margin: 2 * $medium-spacing;
.hello { .hello {
max-width: 600px;
margin: 0 auto;
@include desktop {
max-width: 600px; max-width: 600px;
margin: 0 auto; margin: 0 auto;
@include desktop {
max-width: 600px;
margin: 0 auto;
}
} }
}
.logo { .logo {
display: block; display: block;
width: 300px; width: 300px;
margin: $small-spacing auto $hello-block-margin; margin: $small-spacing auto $hello-block-margin;
@include desktop { @include desktop {
display: none;
}
}
.about {
display: none; display: none;
margin-bottom: $hello-block-margin; }
}
.about {
display: none;
margin-bottom: $hello-block-margin;
@include desktop {
display: block;
}
&__text {
margin-top: $medium-spacing;
@include regular-text;
}
&__logos {
& a:first-child {
margin-right: $large-spacing;
}
}
}
.logos {
&__logo {
height: 30px;
}
}
.login-actions {
@include widget-shadow;
padding: $medium-spacing;
margin-bottom: $hello-block-margin;
&__title {
font-size: 2.125rem; // 34px
font-weight: 700;
}
&__register {
margin-top: $large-spacing;
> p,
a {
@include regular-text;
}
}
}
.information {
margin-top: $hello-block-margin;
> p,
a {
@include regular-text;
}
}
.links {
margin-top: $hello-block-margin;
display: flex;
&__list-item {
color: $color-silver-dark;
> a {
@include regular-text;
}
flex-direction: column;
margin-top: $medium-spacing;
&:first-child {
margin-top: 0;
}
@include desktop { @include desktop {
display: block; display: inline-block;
} flex-direction: row;
&__text { &:not(:last-child) {
margin-top: $medium-spacing; margin-right: 1rem;
@include regular-text;
}
&__logos {
& a:first-child {
margin-right: $large-spacing;
} }
} }
} }
}
.logos {
&__logo {
height: 30px;
}
}
.login-actions {
@include widget-shadow;
padding: $medium-spacing;
margin-bottom: $hello-block-margin;
&__title {
font-size: 2.125rem; // 34px
font-weight: 700;
}
&__register {
margin-top: $large-spacing;
> p, a {
@include regular-text;
}
}
}
.information {
margin-top: $hello-block-margin;
> p, a {
@include regular-text;
}
}
.links {
margin-top: $hello-block-margin;
display: flex;
&__list-item {
color: $color-silver-dark;
> a {
@include regular-text;
}
flex-direction: column;
margin-top: $medium-spacing;
&:first-child {
margin-top: 0;
}
@include desktop {
display: inline-block;
flex-direction: row;
&:not(:last-child) {
margin-right: 1rem;
}
}
}
}
</style> </style>

View File

@ -87,10 +87,11 @@ export default {
.instrument-overview { .instrument-overview {
display: grid; display: grid;
@include desktop { @include desktop {
grid-template-columns: 300px auto; grid-template-columns: 300px auto;
} }grid-column-gap: $small-spacing;
grid-column-gap: $small-spacing;
padding: 0 $small-spacing; padding: 0 $small-spacing;
box-sizing: border-box; box-sizing: border-box;
&__list { &__list {

View File

@ -2,8 +2,7 @@
<div class="license-activation public-page"> <div class="license-activation public-page">
<header class="info-header"> <header class="info-header">
<p class="info-header__text small-emph"> <p class="info-header__text small-emph">
Für <span class="info-header__emph">{{ me.email }}</span> haben wir keine Für <span class="info-header__emph">{{ me.email }}</span> haben wir keine gültige Lizenz gefunden
gültige Lizenz gefunden
</p> </p>
</header> </header>
<section class="coupon"> <section class="coupon">
@ -67,16 +66,10 @@
<h2>Oder, kaufen Sie eine Lizenz</h2> <h2>Oder, kaufen Sie eine Lizenz</h2>
<ul class="license-links"> <ul class="license-links">
<li class="license-links__item"> <li class="license-links__item">
<a <a :href="teacherEditionUrl" class="hep-link">{{ $flavor.textAppName }} für Lehrpersonen</a>
:href="teacherEditionUrl"
class="hep-link"
>{{ $flavor.textAppName }} für Lehrpersonen</a>
</li> </li>
<li class="license-links__item"> <li class="license-links__item">
<a <a :href="studentEditionUrl" class="hep-link">{{ $flavor.textAppName }} für Lernende</a>
:href="studentEditionUrl"
class="hep-link"
>{{ $flavor.textAppName }} für Lernende</a>
</li> </li>
</ul> </ul>
</section> </section>
@ -84,16 +77,16 @@
</template> </template>
<script> <script>
import {defineComponent} from 'vue'; import { defineComponent } from 'vue';
import REDEEM_COUPON from '@/graphql/gql/mutations/redeemCoupon.gql'; import REDEEM_COUPON from '@/graphql/gql/mutations/redeemCoupon.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import LoadingButton from '@/components/LoadingButton'; import LoadingButton from '@/components/LoadingButton';
import me from '@/mixins/me'; import me from '@/mixins/me';
import logout from '@/mixins/logout'; import logout from '@/mixins/logout';
import {Form, Field, ErrorMessage} from 'vee-validate'; import { Form, Field, ErrorMessage } from 'vee-validate';
import InputWrapper from '@/components/validation/InputWrapper'; import InputWrapper from '@/components/validation/InputWrapper';
export default defineComponent({ export default defineComponent({
mixins: [me, logout], mixins: [me, logout],
@ -105,90 +98,90 @@
Field Field
}, },
data() { data() {
return { return {
coupon: '', coupon: '',
couponErrors: [], couponErrors: [],
loginError: '', loginError: '',
submitted: false, submitted: false,
me: { me: {
email: '', email: '',
},
teacherEditionUrl: `${process.env.HEP_URL}/myskillbox-lehrpersonen`,
studentEditionUrl: `${process.env.HEP_URL}/myskillbox-fur-lernende`,
loading: false,
};
},
methods: {
required(value, {field}) {
if (value && value.trim()) {
return true;
}
return `${field} ist ein Pflichtfeld`;
}, },
validateBeforeSubmit({coupon: couponCode}) { teacherEditionUrl: `${process.env.HEP_URL}/myskillbox-lehrpersonen`,
console.log('coupon', couponCode); studentEditionUrl: `${process.env.HEP_URL}/myskillbox-fur-lernende`,
this.submitted = true; loading: false,
this.loading = true; };
this.$apollo.mutate({ },
methods: {
required(value, { field }) {
if (value && value.trim()) {
return true;
}
return `${field} ist ein Pflichtfeld`;
},
validateBeforeSubmit({ coupon: couponCode }) {
console.log('coupon', couponCode);
this.submitted = true;
this.loading = true;
this.$apollo
.mutate({
mutation: REDEEM_COUPON, mutation: REDEEM_COUPON,
variables: { variables: {
input: { input: {
couponCode couponCode
}, },
}, },
update: ( update: (store, { data: { coupon } }) => {
store,
{
data: {coupon},
},
) => {
if (coupon.success) { if (coupon.success) {
this.couponErrors = []; this.couponErrors = [];
this.$apollo.query({ this.$apollo
query: ME_QUERY, .query({
fetchPolicy: 'network-only', query: ME_QUERY,
}).then(() => this.$router.push('/')); fetchPolicy: 'network-only',
})
.then(() => this.$router.push('/'));
} }
}, },
}).catch(({message}) => { })
if (message.indexOf('invalid_coupon') > -1) { .catch(({ message }) => {
this.couponErrors = ['Der angegebene Coupon-Code ist ungültig.']; if (message.indexOf('invalid_coupon') > -1) {
} else { this.couponErrors = ['Der angegebene Coupon-Code ist ungültig.'];
this.couponErrors = ['Es ist ein Fehler aufgetreten. Bitte versuchen Sie es nochmals oder kontaktieren Sie den Administrator.']; } else {
} this.couponErrors = [
}) 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es nochmals oder kontaktieren Sie den Administrator.',
.finally(() => { ];
this.loading = false; }
}); })
}, .finally(() => {
this.loading = false;
});
}, },
}); },
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.text-link { .text-link {
font-family: $sans-serif-font-family; font-family: $sans-serif-font-family;
color: $color-brand; color: $color-brand;
}
.actions {
&__reset {
display: inline-block;
margin-left: $large-spacing;
} }
}
.actions { .get-license {
&__reset { margin-top: $large-spacing;
display: inline-block; }
margin-left: $large-spacing;
} .license-links {
&__item {
margin-bottom: $medium-spacing;
} }
}
.get-license {
margin-top: $large-spacing
}
.license-links {
&__item {
margin-bottom: $medium-spacing;
}
}
</style> </style>

View File

@ -1,8 +1,6 @@
<template> <template>
<div class="module-visibility"> <div class="module-visibility">
<h1 class="module-visibility__page-title"> <h1 class="module-visibility__page-title">Sichtbarkeit</h1>
Sichtbarkeit
</h1>
<div class="module-visibility__section"> <div class="module-visibility__section">
<p class="module-visibility__paragraph"> <p class="module-visibility__paragraph">
Wollen Sie die angepasste Sichtbarkeit ( Wollen Sie die angepasste Sichtbarkeit (
@ -18,127 +16,114 @@
class="skillbox-input skillbox-dropdown module-visibility__dropdown" class="skillbox-input skillbox-dropdown module-visibility__dropdown"
@change="select($event.target.value)" @change="select($event.target.value)"
> >
<option <option value="" selected>-</option>
value="" <option :value="schoolClass.id" v-for="schoolClass in schoolClasses" :key="schoolClass.id">
selected
>
-
</option>
<option
:value="schoolClass.id"
v-for="schoolClass in schoolClasses"
:key="schoolClass.id"
>
{{ schoolClass.name }} {{ schoolClass.name }}
</option> </option>
</select> </select>
für {{ currentClassName }} übernehmen. für {{ currentClassName }} übernehmen.
</div> </div>
<div class="module-visibility__section"> <div class="module-visibility__section">
<a <a class="button button--primary" data-cy="save-visibility-button" @click="sync">Anpassungen übernehmen</a>
class="button button--primary"
data-cy="save-visibility-button"
@click="sync"
>Anpassungen übernehmen</a>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import me from '@/mixins/me'; import me from '@/mixins/me';
import SYNC_VISIBILITY_MUTATION from '@/graphql/gql/mutations/syncModuleVisibility.gql'; import SYNC_VISIBILITY_MUTATION from '@/graphql/gql/mutations/syncModuleVisibility.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery'; import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery';
import {MODULE_PAGE} from '@/router/module.names'; import { MODULE_PAGE } from '@/router/module.names';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon')); const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon'));
export default { export default {
mixins: [me],
components: {
EyeIcon,
},
mixins: [me], data() {
components: { return {
EyeIcon, selectedClassId: '',
};
},
computed: {
schoolClasses() {
return this.me.schoolClasses.filter((schoolClass) => schoolClass.id !== this.me.selectedClass.id);
}, },
slug() {
data() { return this.$route.params.slug;
return {
selectedClassId: '',
};
}, },
},
computed: { methods: {
schoolClasses() { select(selectedClassId) {
return this.me.schoolClasses.filter(schoolClass => schoolClass.id !== this.me.selectedClass.id); this.selectedClassId = selectedClassId;
},
slug() {
return this.$route.params.slug;
}
}, },
sync() {
methods: { if (this.selectedClassId) {
select(selectedClassId) { const slug = this.slug;
this.selectedClassId = selectedClassId; this.$apollo
}, .mutate({
sync() { mutation: SYNC_VISIBILITY_MUTATION,
if (this.selectedClassId) { variables: {
const slug = this.slug; input: {
this.$apollo.mutate({ module: slug,
mutation: SYNC_VISIBILITY_MUTATION, templateSchoolClass: this.selectedClassId,
variables: { schoolClass: this.me.selectedClass.id,
input: { },
module: slug, },
templateSchoolClass: this.selectedClassId, refetchQueries: [
schoolClass: this.me.selectedClass.id, {
query: MODULE_DETAILS_QUERY,
variables: {
slug,
}, },
}, },
refetchQueries: [ ],
{ })
query: MODULE_DETAILS_QUERY, .then(() => {
variables: {
slug,
},
},
],
},
).then(() => {
this.$router.push({ this.$router.push({
name: MODULE_PAGE, name: MODULE_PAGE,
params: { params: {
slug slug,
} },
}); });
}); });
} }
},
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/_helpers'; @import '~styles/_helpers';
.module-visibility { .module-visibility {
@include settings-page; @include settings-page;
margin: 0 auto; margin: 0 auto;
&__inline-icon { &__inline-icon {
width: 25px; width: 25px;
height: 25px; height: 25px;
vertical-align: middle; vertical-align: middle;
}
&__dropdown {
width: 200px;
margin: 0 $medium-spacing;
}
&__form {
display: flex;
align-items: center;
@include regular-text;
font-weight: 600;
}
} }
&__dropdown {
width: 200px;
margin: 0 $medium-spacing;
}
&__form {
display: flex;
align-items: center;
@include regular-text;
font-weight: 600;
}
}
</style> </style>

View File

@ -1,23 +1,19 @@
<template> <template>
<div> <div>
<logo class="onboarding__logo" /> <logo class="onboarding__logo" />
<h1 class="onboarding__heading"> <h1 class="onboarding__heading">Herzlich willkommen!</h1>
Herzlich willkommen!
</h1>
<p class="onboarding__claim"> <p class="onboarding__claim">Schauen Sie sich die Einführung an und lernen Sie {{ $flavor.textAppName }} kennen.</p>
Schauen Sie sich die Einführung an und lernen Sie {{ $flavor.textAppName }} kennen.
</p>
</div> </div>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo')); const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/Logo'));
export default { export default {
components: { components: {
Logo Logo,
}, },
}; };
</script> </script>

View File

@ -137,15 +137,14 @@ export default {
@include desktop { @include desktop {
grid-template-areas: grid-template-areas:
'b a' 'b a'
't t' 't t'
'd d' 'd d'
'm m'; 'm m';
} }}
}
&__back { &__back {
grid-area: b; grid-area: b;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;
@include desktop { @include desktop {
margin-bottom: 0; margin-bottom: 0;
} }
@ -155,10 +154,8 @@ export default {
grid-area: a; grid-area: a;
display: flex; display: flex;
@include desktop { @include desktop {justify-self: end;
justify-self: end; }}
}
}
&__more { &__more {
display: none; display: none;
@ -198,29 +195,25 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;@include desktop {
flex-direction: row-reverse;align-items: center;
@include desktop {
flex-direction: row-reverse;
align-items: center;
margin-bottom: 0; margin-bottom: 0;
} }
justify-content: flex-start; justify-content: flex-start;
position: relative; position: relative;
& > :first-child {
margin-bottom: $medium-spacing;
& > :first-child {margin-bottom: $medium-spacing;
@include desktop { @include desktop {
margin-left: $large-spacing; margin-left: $large-spacing;
margin-bottom: 0; margin-bottom: 0;
} }
} }
& > :nth-child(2) { & > :nth-child(2) {
@include desktop { @include desktop {margin-left: $large-spacing;
margin-left: $large-spacing;
}
} }
} }}
&__content { &__content {
display: flex; display: flex;

View File

@ -1,23 +1,17 @@
<template> <template>
<content-block-form <content-block-form :content-block="roomEntry" :features="features" v-if="roomEntry.id" @save="save" @back="goBack" />
:content-block="roomEntry"
:features="features"
v-if="roomEntry.id"
@save="save"
@back="goBack"
/>
</template> </template>
<script> <script>
import {defineComponent} from 'vue'; import {defineComponent} from 'vue';
import ROOM_ENTRY_QUERY from 'gql/queries/roomEntryQuery.gql'; import ROOM_ENTRY_QUERY from 'gql/queries/roomEntryQuery.gql';
import ROOM_ENTRY_FRAGMENT from 'gql/fragments/roomEntryParts.gql'; import ROOM_ENTRY_FRAGMENT from 'gql/fragments/roomEntryParts.gql';
import UPDATE_ROOM_ENTRY_MUTATION from 'gql/mutations/rooms/updateRoomEntry.gql'; import UPDATE_ROOM_ENTRY_MUTATION from 'gql/mutations/rooms/updateRoomEntry.gql';
import ContentBlockForm from '@/components/content-block-form/ContentBlockForm'; import ContentBlockForm from '@/components/content-block-form/ContentBlockForm';
import {ROOMS_FEATURE_SET} from '@/consts/features.consts'; import { ROOMS_FEATURE_SET } from '@/consts/features.consts';
import {ROOM_PAGE} from '@/router/room.names'; import { ROOM_PAGE } from '@/router/room.names';
export default defineComponent( { export default defineComponent( {
props: { props: {
@ -30,81 +24,87 @@
required: true required: true
} }
}, },
},
components: { components: {
ContentBlockForm, ContentBlockForm,
}, },
data() { data() {
return { return {
features: ROOMS_FEATURE_SET, features: ROOMS_FEATURE_SET,
roomEntry: {
title: '',
contents: [],
},
};
},
apollo: {
roomEntry: { roomEntry: {
query: ROOM_ENTRY_QUERY, title: '',
variables() { contents: [],
return {
slug: this.entrySlug
};
}
}
},
methods: {
goBack() {
this.$router.push({
name: ROOM_PAGE,
params: {
slug: this.slug
}
});
}, },
save({title, contents}) { };
const entry = { },
slug: this.roomEntry.slug,
title, apollo: {
contents, roomEntry: {
query: ROOM_ENTRY_QUERY,
variables() {
return {
slug: this.entrySlug,
}; };
this.$apollo.mutate({ },
},
},
methods: {
goBack() {
this.$router.push({
name: ROOM_PAGE,
params: {
slug: this.slug,
},
});
},
save({ title, contents }) {
const entry = {
slug: this.roomEntry.slug,
title,
contents,
};
this.$apollo
.mutate({
mutation: UPDATE_ROOM_ENTRY_MUTATION, mutation: UPDATE_ROOM_ENTRY_MUTATION,
variables: { variables: {
input: { input: {
roomEntry: entry, roomEntry: entry,
} },
}, },
update: (store, {data: {updateRoomEntry: {roomEntry}}}) => { update: (
store,
{
data: {
updateRoomEntry: { roomEntry },
},
}
) => {
try { try {
const fragment = ROOM_ENTRY_FRAGMENT; const fragment = ROOM_ENTRY_FRAGMENT;
const id = store.identify(roomEntry); const id = store.identify(roomEntry);
const cachedEntry = store.readQuery({fragment, id}); const cachedEntry = store.readQuery({ fragment, id });
const data = Object.assign({}, cachedEntry, roomEntry); const data = Object.assign({}, cachedEntry, roomEntry);
store.writeFragment({ store.writeFragment({
id, id,
fragment, fragment,
data data,
}); });
} catch (e) { } catch (e) {
// Query did not exist in the cache, and apollo throws a generic Error. Do nothing // Query did not exist in the cache, and apollo throws a generic Error. Do nothing
} }
} },
}).then(() => { })
.then(() => {
this.goBack(); this.goBack();
}); });
}
}, },
} ); },
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
</style> </style>

View File

@ -1,107 +1,104 @@
<template> <template>
<content-block-form <content-block-form :content-block="roomEntry" :features="features" @save="save" @back="goBack" />
:content-block="roomEntry"
:features="features"
@save="save"
@back="goBack"
/>
</template> </template>
<script> <script>
import {defineComponent} from 'vue'; import { defineComponent } from 'vue';
import NEW_ROOM_ENTRY_MUTATION from 'gql/mutations/rooms/addRoomEntry.gql'; import NEW_ROOM_ENTRY_MUTATION from 'gql/mutations/rooms/addRoomEntry.gql';
import ROOM_ENTRIES_QUERY from '@/graphql/gql/queries/roomEntriesQuery.gql'; import ROOM_ENTRIES_QUERY from '@/graphql/gql/queries/roomEntriesQuery.gql';
import ContentBlockForm from '@/components/content-block-form/ContentBlockForm'; import ContentBlockForm from '@/components/content-block-form/ContentBlockForm';
import {ROOMS_FEATURE_SET} from '@/consts/features.consts'; import { ROOMS_FEATURE_SET } from '@/consts/features.consts';
import {ROOM_PAGE} from '@/router/room.names'; import { ROOM_PAGE } from '@/router/room.names';
export default defineComponent( { export default defineComponent({
props: { props: {
slug: { slug: {
type: String, type: String,
required: true required: true,
}
}, },
},
components: { components: {
ContentBlockForm, ContentBlockForm,
}, },
data() { data() {
return { return {
features: ROOMS_FEATURE_SET, features: ROOMS_FEATURE_SET,
roomEntry: { roomEntry: {
title: '', title: '',
contents: [], contents: [],
},
};
},
methods: {
goBack() {
this.$router.push({
name: ROOM_PAGE,
params: {
slug: this.slug
}
});
}, },
save({title, contents}) { };
const entry = { },
title,
contents, methods: {
roomSlug: this.slug goBack() {
}; this.$router.push({
this.$apollo.mutate({ name: ROOM_PAGE,
params: {
slug: this.slug,
},
});
},
save({ title, contents }) {
const entry = {
title,
contents,
roomSlug: this.slug,
};
this.$apollo
.mutate({
mutation: NEW_ROOM_ENTRY_MUTATION, mutation: NEW_ROOM_ENTRY_MUTATION,
variables: { variables: {
input: { input: {
roomEntry: entry, roomEntry: entry,
} },
}, },
update: (store, {data: {addRoomEntry: {roomEntry}}}) => { update: (
store,
{
data: {
addRoomEntry: { roomEntry },
},
}
) => {
try { try {
const query = ROOM_ENTRIES_QUERY; const query = ROOM_ENTRIES_QUERY;
const variables = {slug: this.slug}; const variables = { slug: this.slug };
const {room} = store.readQuery({query, variables}); const { room } = store.readQuery({ query, variables });
if (room && room.roomEntries) { if (room && room.roomEntries) {
const newEdge ={ const newEdge = {
node: roomEntry, node: roomEntry,
__typename: 'RoomEntryNodeEdge' __typename: 'RoomEntryNodeEdge',
}; };
const edges = [ const edges = [newEdge, ...room.roomEntries.edges];
newEdge,
...room.roomEntries.edges
];
const data = { const data = {
room: { room: {
...room, ...room,
roomEntries: { roomEntries: {
...room.roomEntries, ...room.roomEntries,
edges edges,
} },
} },
}; };
store.writeQuery({query, variables, data}); store.writeQuery({ query, variables, data });
} }
} catch (e) { } catch (e) {
// Query did not exist in the cache, and apollo throws a generic Error. Do nothing // Query did not exist in the cache, and apollo throws a generic Error. Do nothing
} }
} },
}).then(() => { })
.then(() => {
this.goBack(); this.goBack();
}); });
}
}, },
} ); },
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
</style> </style>

View File

@ -17,12 +17,7 @@
:key="index" :key="index"
/> />
</div> </div>
<router-link <router-link :to="topicRoute" class="button"> Alle {{ $flavor.textModules }} anzeigen </router-link>
:to="topicRoute"
class="button"
>
Alle {{ $flavor.textModules }} anzeigen
</router-link>
</div> </div>
<div class="start-page__news news" data-cy="news-teasers" v-if="!me.readOnly"> <div class="start-page__news news" data-cy="news-teasers" v-if="!me.readOnly">
<h2 class="start-page__heading">News</h2> <h2 class="start-page__heading">News</h2>

View File

@ -14,10 +14,9 @@
<script> <script>
import '@/styles/survey.modern.css'; import '@/styles/survey.modern.css';
import '@/styles/survey.reset.css'; import '@/styles/survey.reset.css';import { css } from '@/survey.config';
import { css } from '@/survey.config';
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { Model, StylesManager } from 'survey-knockout'; import { Model , StylesManager } from 'survey-knockout';
// we are switching to the knockout version because the Vue version only works with Vue 2 (as of July 2022) // we are switching to the knockout version because the Vue version only works with Vue 2 (as of July 2022)
import SURVEY_QUERY from '@/graphql/gql/queries/surveyQuery.gql'; import SURVEY_QUERY from '@/graphql/gql/queries/surveyQuery.gql';
@ -35,7 +34,8 @@ const Solution = defineAsyncComponent(() =>
*/ '@/components/content-blocks/Solution' */ '@/components/content-blocks/Solution'
) )
); );
StylesManager.applyTheme('modern'); StylesManager.applyTheme('modern')
);
const MODULE_QUERY = gql` const MODULE_QUERY = gql`
query ModuleSolutions($slug: String) { query ModuleSolutions($slug: String) {
@ -56,8 +56,7 @@ export default {
return { return {
survey: this.initSurvey(), survey: this.initSurvey(),
currentPage: null, currentPage: null,
surveyData: null, surveyData: null,title: '',
title: '',
module: {}, module: {},
completed: false, completed: false,
me: { me: {
@ -115,15 +114,14 @@ export default {
} }
}, },
destroyed() {}, destroyed() {},methods: {
methods: {
initSurvey(data, answers) { initSurvey(data, answers) {
let survey = new Model(data); let survey = new Model(data);
const flatAnswers = {}; const flatAnswers = {};
for (let k in answers) { for (let k in answers) {
flatAnswers[k] = answers[k].answer; flatAnswers[k] = answers[k].answer;
} }
if (Object.keys(flatAnswers).length > 0) { if (Object.keys(flatAnswers).length > 0) {
// answers are not empty // answers are not empty
survey.data = flatAnswers; survey.data = flatAnswers;
@ -215,6 +213,7 @@ export default {
survey.locale = 'de'; survey.locale = 'de';
survey.showProgressBar = 'bottom'; survey.showProgressBar = 'bottom';
survey.pageNextText = 'Speichern & Weiter'; survey.pageNextText = 'Speichern & Weiter';
survey.render('survey'); survey.render('survey');
return survey; return survey;
}, },
@ -226,7 +225,7 @@ export default {
this.survey.data = data; // reapply it this.survey.data = data; // reapply it
this.saveDisabled = false; this.saveDisabled = false;
}, },
loadSurveyFromServer(survey) { loadSurveyFromServer(survey) {
let json = JSON.parse(survey.data); let json = JSON.parse(survey.data);
json.showTitle = false; json.showTitle = false;
json.showProgressBar = 'bottom'; json.showProgressBar = 'bottom';
@ -254,7 +253,7 @@ export default {
result({ data, loading }) { result({ data, loading }) {
if (!loading) { if (!loading) {
this.surveyData = data.survey; this.surveyData = data.survey;
this.loadSurveyFromServer(data.survey); this.loadSurveyFromServer(data.survey);
const module = data.survey.module; const module = data.survey.module;
this.$apollo.addSmartQuery('module', { this.$apollo.addSmartQuery('module', {

View File

@ -5,21 +5,14 @@
</div> </div>
<div class="topic__content"> <div class="topic__content">
<h1 <h1 data-cy="topic-title" class="topic__title">
data-cy="topic-title"
class="topic__title"
>
{{ topic.title }} {{ topic.title }}
</h1> </h1>
<p class="topic__teaser"> <p class="topic__teaser">
{{ topic.teaser }} {{ topic.teaser }}
</p> </p>
<div class="topic__links"> <div class="topic__links">
<div <div class="topic__video-link topic__link" v-if="topic.vimeoId" @click="openVideo">
class="topic__video-link topic__link"
v-if="topic.vimeoId"
@click="openVideo"
>
<play-icon class="topic__video-link-icon topic__link-icon" /> <play-icon class="topic__video-link-icon topic__link-icon" />
<span class="topic__link-description">Video schauen</span> <span class="topic__link-description">Video schauen</span>
</div> </div>
@ -34,10 +27,7 @@
</a> </a>
</div> </div>
<div class="topic__modules"> <div class="topic__modules">
<module-teaser <module-teaser v-for="module in modules" v-bind="module" :key="module.slug"
v-for="module in modules"
v-bind="module"
:key="module.slug"
/> />
</div> </div>
</div> </div>
@ -45,173 +35,180 @@
</template> </template>
<script> <script>
import ModuleTeaser from '@/components/modules/ModuleTeaser.vue'; import ModuleTeaser from '@/components/modules/ModuleTeaser.vue';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
import TOPIC_QUERY from '@/graphql/gql/queries/topicQuery.gql'; import TOPIC_QUERY from '@/graphql/gql/queries/topicQuery.gql';
import me from '@/mixins/me'; import me from '@/mixins/me';
import TopicNavigation from '@/components/book-navigation/TopicNavigation'; import TopicNavigation from '@/components/book-navigation/TopicNavigation';
import UPDATE_LAST_TOPIC_MUTATION from '@/graphql/gql/mutations/updateLastTopic.gql'; import UPDATE_LAST_TOPIC_MUTATION from '@/graphql/gql/mutations/updateLastTopic.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
const PlayIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Play')); const PlayIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/Play'));
const BulbIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/BulbIcon')); const BulbIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/BulbIcon'));
export default { export default {
mixins: [me],
components: {
TopicNavigation,
ModuleTeaser,
PlayIcon,
BulbIcon,
},
mixins: [me], apollo: {
components: { topic() {
TopicNavigation,
ModuleTeaser,
PlayIcon,
BulbIcon,
},
apollo: {
topic() {
return {
query: TOPIC_QUERY,
variables: {
slug: this.$route.params.topicSlug,
},
update(data) {
return this.$getRidOfEdges(data).topic || {};
},
result() {
if (this.saveMe) {
this.saveMe = false;
this.updateLastVisitedTopic(this.topic.id);
}
},
};
},
},
data() {
return { return {
topic: { query: TOPIC_QUERY,
modules: { variables: {
edges: [], slug: this.$route.params.topicSlug,
}, },
update(data) {
return this.$getRidOfEdges(data).topic || {};
},
result() {
if (this.saveMe) {
this.saveMe = false;
this.updateLastVisitedTopic(this.topic.id);
}
}, },
saveMe: false,
}; };
}, },
},
computed: { data() {
modules() { return {
return this.topic.modules; topic: {
modules: {
edges: [],
},
}, },
}, saveMe: false,
};
},
mounted() { computed: {
if (!this.topic.id) { // component was loaded before topic, apollo not ready yet modules() {
this.saveMe = true; // needs saving, apollo will do this return this.topic.modules;
} else { },
this.updateLastVisitedTopic(this.topic.id); },
mounted() {
if (!this.topic.id) {
// component was loaded before topic, apollo not ready yet
this.saveMe = true; // needs saving, apollo will do this
} else {
this.updateLastVisitedTopic(this.topic.id);
}
},
methods: {
openVideo() {
this.$store.dispatch('showFullscreenVideo', this.topic.vimeoId);
},
updateLastVisitedTopic(topicId) {
if (!topicId) {
return;
} }
}, this.$apollo.mutate({
mutation: UPDATE_LAST_TOPIC_MUTATION,
methods: { variables: {
openVideo() { input: {
this.$store.dispatch('showFullscreenVideo', this.topic.vimeoId); id: topicId,
}, },
updateLastVisitedTopic(topicId) { },
if (!topicId) { update(
return; store,
} {
this.$apollo.mutate({ data: {
mutation: UPDATE_LAST_TOPIC_MUTATION, updateLastTopic: { topic },
variables: {
input: {
id: topicId,
}, },
}, }
update(store, {data: {updateLastTopic: {topic}}}) { ) {
if (topic) { if (topic) {
const query = ME_QUERY; const query = ME_QUERY;
const {me} = store.readQuery({query}); const { me } = store.readQuery({ query });
if (me) { if (me) {
const data = { const data = {
me: { me: {
...me, ...me,
lastTopic: topic, lastTopic: topic,
}, },
}; };
store.writeQuery({query, data}); store.writeQuery({ query, data });
}
} }
}, }
}); },
}, });
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.topic { .topic {
display: grid; display: grid;
padding: $large-spacing 0; padding: $large-spacing 0;
grid-template-columns: 1fr; grid-template-columns: 1fr;
@include desktop {
grid-template-columns: 300px 1fr;
}
&__navigation {
padding: 0 $medium-spacing;
display: none;
@include desktop { @include desktop {
grid-template-columns: 300px 1fr; display: block;
}
&__navigation {
padding: 0 $medium-spacing;
display: none;
@include desktop {
display: block;
}
}
&__teaser {
color: $color-charcoal-dark;
width: 90%;
@include lead-paragraph;
margin-bottom: $large-spacing;
}
&__links {
margin-bottom: $large-spacing;
display: flex;
}
&__link {
cursor: pointer;
display: flex;
align-items: center;
}
&__video-link {
margin-right: $large-spacing;
}
&__link-icon {
width: 40px;
height: 40px;
margin-right: $medium-spacing;
}
&__link-description {
@include heading-3;
}
&__modules {
margin-top: 40px;
display: flex;
flex-wrap: wrap;
@supports (display: grid) {
display: grid;
}
grid-column-gap: $large-spacing;
grid-row-gap: $large-spacing;
@include desktop {
grid-template-columns: repeat(3, minmax(auto, 380px));
}
} }
} }
&__teaser {
color: $color-charcoal-dark;
width: 90%;
@include lead-paragraph;
margin-bottom: $large-spacing;
}
&__links {
margin-bottom: $large-spacing;
display: flex;
}
&__link {
cursor: pointer;
display: flex;
align-items: center;
}
&__video-link {
margin-right: $large-spacing;
}
&__link-icon {
width: 40px;
height: 40px;
margin-right: $medium-spacing;
}
&__link-description {
@include heading-3;
}
&__modules {
margin-top: 40px;
display: flex;
flex-wrap: wrap;
@supports (display: grid) {
display: grid;
}
grid-column-gap: $large-spacing;
grid-row-gap: $large-spacing;
@include desktop {
grid-template-columns: repeat(3, minmax(auto, 380px));
}
}
}
</style> </style>

View File

@ -1,6 +1,6 @@
// adapted from // adapted from
// https://stackoverflow.com/questions/41791193/vuejs-reactive-binding-for-a-plugin-how-to/41801107#41801107 // https://stackoverflow.com/questions/41791193/vuejs-reactive-binding-for-a-plugin-how-to/41801107#41801107
import {reactive, App} from 'vue'; import { reactive, App } from 'vue';
class ModalStore { class ModalStore {
data: any; data: any;
@ -18,19 +18,19 @@ class ModalStore {
} }
interface Modal { interface Modal {
state: any, state: any;
component: string, component: string;
payload?: any, payload?: any;
confirm: (res: any) => void, confirm: (res: any) => void;
open: (component: string, payload?: any) => Promise<(resolve: () => any, reject: () => any) => void>, open: (component: string, payload?: any) => Promise<(resolve: () => any, reject: () => any) => void>;
cancel: () => void, cancel: () => void;
_resolve: (r?: any) => any, _resolve: (r?: any) => any;
_reject: (r?: any) => any _reject: (r?: any) => any;
} }
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
$modal: Modal $modal: Modal;
} }
} }

View File

@ -1,4 +1,4 @@
import {RouteLocationNormalized} from "vue-router"; import { RouteLocationNormalized } from 'vue-router';
function getCookieValue(cookieName: string) { function getCookieValue(cookieName: string) {
// https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript // https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript

View File

@ -1,4 +1,4 @@
import {createRouter, createWebHistory} from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import moduleRoutes from './module.routes'; import moduleRoutes from './module.routes';
import portfolioRoutes from './portfolio.routes'; import portfolioRoutes from './portfolio.routes';
@ -7,19 +7,19 @@ import meRoutes from './me.routes';
import authRoutes from './auth.routes'; import authRoutes from './auth.routes';
import roomRoutes from './room.routes'; import roomRoutes from './room.routes';
import {store} from '@/store'; import { store } from '@/store';
import {LAYOUT_SIMPLE} from '@/router/core.constants'; import { LAYOUT_SIMPLE } from '@/router/core.constants';
import {EMAIL_NOT_VERIFIED_STATE, NO_VALID_LICENSE_STATE, SUCCESS_STATE} from './oauth.names'; import { EMAIL_NOT_VERIFIED_STATE, NO_VALID_LICENSE_STATE, SUCCESS_STATE } from './oauth.names';
import start from '@/pages/start'; import start from '@/pages/start';
const instrument = () => import(/* webpackChunkName: "instruments" */'@/pages/instrument'); const instrument = () => import(/* webpackChunkName: "instruments" */ '@/pages/instrument');
const instrumentOverview = () => import(/* webpackChunkName: "instruments" */'@/pages/instrumentOverview'); const instrumentOverview = () => import(/* webpackChunkName: "instruments" */ '@/pages/instrumentOverview');
const article = () => import(/* webpackChunkName: "news" */'@/pages/article'); const article = () => import(/* webpackChunkName: "news" */ '@/pages/article');
const news = () => import(/* webpackChunkName: "news" */'@/pages/news'); const news = () => import(/* webpackChunkName: "news" */ '@/pages/news');
const surveyPage = () => import(/* webpackChunkName: "survey" */'@/pages/survey'); const surveyPage = () => import(/* webpackChunkName: "survey" */ '@/pages/survey');
const styleGuidePage = () => import('@/pages/styleguide'); const styleGuidePage = () => import('@/pages/styleguide');
const joinClass = () => import('@/pages/joinClass'); const joinClass = () => import('@/pages/joinClass');
@ -50,22 +50,22 @@ const routes = [
...onboardingRoutes, ...onboardingRoutes,
...portfolioRoutes, ...portfolioRoutes,
...meRoutes, ...meRoutes,
{path: '/article/:slug', name: 'article', component: article, meta: {layout: LAYOUT_SIMPLE}}, { path: '/article/:slug', name: 'article', component: article, meta: { layout: LAYOUT_SIMPLE } },
{ {
path: '/instruments/', path: '/instruments/',
name: 'instrument-overview', name: 'instrument-overview',
component: instrumentOverview, component: instrumentOverview,
}, },
{path: '/instrument/:slug', name: 'instrument', component: instrument, meta: {layout: LAYOUT_SIMPLE}}, { path: '/instrument/:slug', name: 'instrument', component: instrument, meta: { layout: LAYOUT_SIMPLE } },
{path: '/submission/:id', name: 'submission', component: submission, meta: {layout: LAYOUT_SIMPLE}}, { path: '/submission/:id', name: 'submission', component: submission, meta: { layout: LAYOUT_SIMPLE } },
{path: '/topic/:topicSlug', name: 'topic', component: topic, alias: '/book/topic/:topicSlug'}, { path: '/topic/:topicSlug', name: 'topic', component: topic, alias: '/book/topic/:topicSlug' },
{path: '/join-class', name: 'join-class', component: joinClass, meta: {layout: LAYOUT_SIMPLE}}, { path: '/join-class', name: 'join-class', component: joinClass, meta: { layout: LAYOUT_SIMPLE } },
{ {
path: '/survey/:id', path: '/survey/:id',
component: surveyPage, component: surveyPage,
name: 'survey', name: 'survey',
props: true, props: true,
meta: {layout: LAYOUT_SIMPLE}, meta: { layout: LAYOUT_SIMPLE },
}, },
{ {
path: '/news', path: '/news',
@ -74,7 +74,7 @@ const routes = [
}, },
{ {
path: '/oauth-redirect', path: '/oauth-redirect',
redirect: to => { redirect: (to) => {
switch (to.query.state) { switch (to.query.state) {
case EMAIL_NOT_VERIFIED_STATE: case EMAIL_NOT_VERIFIED_STATE:
return '/verify-email'; return '/verify-email';
@ -92,15 +92,15 @@ const routes = [
} }
}, },
}, },
{path: '/styleguide', component: styleGuidePage}, { path: '/styleguide', component: styleGuidePage },
{ {
path: '/not-found', path: '/not-found',
name: 'not-found', name: 'not-found',
...notFoundRoute ...notFoundRoute,
}, },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
...notFoundRoute ...notFoundRoute,
}, },
]; ];
@ -113,7 +113,7 @@ const router = createRouter({
if (savedPosition) { if (savedPosition) {
return savedPosition; return savedPosition;
} }
return {left: 0, top: 0}; return { left: 0, top: 0 };
}, },
}); });
@ -122,4 +122,4 @@ router.afterEach(() => {
store.dispatch('showMobileNavigation', false); store.dispatch('showMobileNavigation', false);
}); });
export {router, postLoginRedirectUrlKey}; export { router, postLoginRedirectUrlKey };

View File

@ -1,7 +1,7 @@
import { ONBOARDING_STEP_1, ONBOARDING_STEP_2, ONBOARDING_STEP_3 } from '@/router/onboarding.names'; import { ONBOARDING_STEP_1, ONBOARDING_STEP_2, ONBOARDING_STEP_3 } from '@/router/onboarding.names';
const onboarding = () => import('@/pages/onboarding.vue'); const onboarding = () => import( '@/pages/onboarding.vue');
const onboardingStart = () => import('@/pages/onboarding/start.vue'); const onboardingStart = () => import( '@/pages/onboarding/start.vue');
const onboardingStep1 = () => import(/* webpackChunkName: "onboarding" */ '@/pages/onboarding/step1.vue'); const onboardingStep1 = () => import(/* webpackChunkName: "onboarding" */ '@/pages/onboarding/step1.vue');
const onboardingStep2 = () => import(/* webpackChunkName: "onboarding" */ '@/pages/onboarding/step2.vue'); const onboardingStep2 = () => import(/* webpackChunkName: "onboarding" */ '@/pages/onboarding/step2.vue');
const onboardingStep3 = () => import(/* webpackChunkName: "onboarding" */ '@/pages/onboarding/step3.vue'); const onboardingStep3 = () => import(/* webpackChunkName: "onboarding" */ '@/pages/onboarding/step3.vue');

View File

@ -10,4 +10,3 @@ declare module '*.vue' {
const component: DefineComponent<{}, {}, any>; const component: DefineComponent<{}, {}, any>;
export default component; export default component;
} }

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