Update async component definitions

This commit is contained in:
Ramon Wenger 2022-03-24 17:31:18 +01:00
parent 5bc0c29ea0
commit 39e7d27587
73 changed files with 4697 additions and 4276 deletions

View File

@ -1,98 +1,105 @@
<template> <template>
<div class="add-content"> <div class="add-content">
<a class="add-content__button" @click="addContent"> <a
class="add-content__button"
@click="addContent"
>
<add-pointer class="add-content__icon" /> <add-pointer class="add-content__icon" />
</a> </a>
</div> </div>
</template> </template>
<script> <script>
import { CREATE_CONTENT_BLOCK_AFTER_PAGE, CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE } from '@/router/module.names'; import {
CREATE_CONTENT_BLOCK_AFTER_PAGE,
CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
} from '@/router/module.names';
import {defineAsyncComponent} from 'vue';
const AddPointer = () => 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 ( return Object.prototype.hasOwnProperty.call(prop, 'after' )
Object.prototype.hasOwnProperty.call(prop, 'after') || Object.prototype.hasOwnProperty.call(prop, 'parent') || Object.prototype.hasOwnProperty.call(prop, 'parent');
); }
}, },
}, },
},
components: { components: {
AddPointer, AddPointer
}, },
computed: { computed: {
parent() { parent() {
return this.where.parent; return this.where.parent;
}, },
after() { after() {
return this.where.after; return this.where.after;
}, },
isObjectiveGroup() { isObjectiveGroup() {
return this.parent && this.parent.__typename === 'ObjectiveGroupNode'; return this.parent && this.parent.__typename === 'ObjectiveGroupNode';
}, },
slug() { slug() {
return this.$route.params.slug; return this.$route.params.slug;
},
},
methods: {
addContent() {
if (this.isObjectiveGroup) {
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 {
route = {
name: CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
params: {
parent: this.parent.id,
},
};
}
this.$router.push(route);
} }
}, },
},
};
methods: {
addContent() {
if (this.isObjectiveGroup) {
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 {
route = {
name: CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
params: {
parent: this.parent.id
}
};
}
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,38 +1,42 @@
<template> <template>
<div class="add-content-element" @click="$emit('add-element', index)"> <div
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>
<script> <script>
const AddIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddIcon'); import {defineAsyncComponent} from 'vue';
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

@ -11,71 +11,69 @@
</template> </template>
<script> <script>
const AddIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddIcon.vue'); import {defineAsyncComponent} from '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

@ -12,8 +12,9 @@
import { MODULE_PAGE } from '@/router/module.names'; import { MODULE_PAGE } from '@/router/module.names';
import { ROOMS_PAGE } from '@/router/room.names'; import { ROOMS_PAGE } from '@/router/room.names';
import { PROJECTS_PAGE } from '@/router/portfolio.names'; import { PROJECTS_PAGE } from '@/router/portfolio.names';
import {defineAsyncComponent} from 'vue';
const ChevronLeft = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronLeft'); const ChevronLeft = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronLeft'));
export default { export default {
props: { props: {

View File

@ -1,86 +1,93 @@
<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 :class="'color-chooser__color--' + color.name" class="color-chooser__color"> <div
<tick class="color-chooser__selected-icon" v-if="selectedColor === color.name" /> :class="'color-chooser__color--' + color.name"
class="color-chooser__color"
>
<tick
class="color-chooser__selected-icon"
v-if="selectedColor === color.name"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const Tick = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Tick'); import {defineAsyncComponent} from 'vue';
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;
&__color-wrapper {
margin-right: 10px;
border-radius: 50px;
padding: 10px;
&--selected {
border: 1px solid $color-charcoal-dark;
}
}
&__selected-icon {
width: 17px;
fill: $color-charcoal-dark;
}
&__color {
width: 46px;
height: 46px;
border-radius: 23px;
display: flex; display: flex;
justify-content: center;
@supports (display: grid) {
display: grid;
}
justify-items: center;
align-items: center;
@include skillbox-colors; &__color-wrapper {
margin-right: 10px;
border-radius: 50px;
padding: 10px;
&--selected {
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;
}
} }
}
</style> </style>

View File

@ -1,20 +1,38 @@
<template> <template>
<div <div
:class="{ 'hideable-element--greyed-out': hidden }" :class="{'hideable-element--greyed-out': hidden}"
class="content-block__container hideable-element content-list__parent" class="content-block__container hideable-element content-list__parent"
> >
<div :class="specialClass" :style="instrumentStyle" class="content-block" data-cy="content-block"> <div
<div class="block-actions" v-if="canEditModule && !isInstrumentBlock"> :class="specialClass"
<user-widget v-bind="me" class="block-actions__user-widget content-block__user-widget" v-if="isMine" /> :style="instrumentStyle"
class="content-block"
data-cy="content-block"
>
<div
class="block-actions"
v-if="canEditModule && !isInstrumentBlock"
>
<user-widget
v-bind="me"
class="block-actions__user-widget content-block__user-widget"
v-if="isMine"
/>
<more-options-widget> <more-options-widget>
<li class="popover-links__link" v-if="!isInstrumentBlock"> <li
class="popover-links__link"
v-if="!isInstrumentBlock"
>
<popover-link <popover-link
data-cy="duplicate-content-block-link" data-cy="duplicate-content-block-link"
text="Duplizieren" text="Duplizieren"
@link-action="duplicateContentBlock(contentBlock)" @link-action="duplicateContentBlock(contentBlock)"
/> />
</li> </li>
<li class="popover-links__link" v-if="isMine"> <li
class="popover-links__link"
v-if="isMine"
>
<popover-link <popover-link
data-cy="delete-content-block-link" data-cy="delete-content-block-link"
text="Löschen" text="Löschen"
@ -22,13 +40,22 @@
/> />
</li> </li>
<li class="popover-links__link" v-if="isMine"> <li
<popover-link text="Bearbeiten" @link-action="editContentBlock(contentBlock)" /> class="popover-links__link"
v-if="isMine"
>
<popover-link
text="Bearbeiten"
@link-action="editContentBlock(contentBlock)"
/>
</li> </li>
</more-options-widget> </more-options-widget>
</div> </div>
<div class="content-block__visibility"> <div class="content-block__visibility">
<visibility-action :block="contentBlock" v-if="canEditModule" /> <visibility-action
:block="contentBlock"
v-if="canEditModule"
/>
</div> </div>
<h3 <h3
@ -39,7 +66,10 @@
> >
{{ instrumentLabel }} {{ instrumentLabel }}
</h3> </h3>
<h4 class="content-block__title" v-if="!contentBlock.indent"> <h4
class="content-block__title"
v-if="!contentBlock.indent"
>
{{ contentBlock.title }} {{ contentBlock.title }}
</h4> </h4>
@ -55,107 +85,110 @@
/> />
</div> </div>
<add-content-button :where="{ after: contentBlock }" v-if="canEditModule" /> <add-content-button
:where="{after: contentBlock}"
v-if="canEditModule"
/>
</div> </div>
</template> </template>
<script> <script>
import AddContentButton from '@/components/AddContentButton'; import AddContentButton from '@/components/AddContentButton';
import MoreOptionsWidget from '@/components/MoreOptionsWidget'; import MoreOptionsWidget from '@/components/MoreOptionsWidget';
import UserWidget from '@/components/UserWidget'; import UserWidget from '@/components/UserWidget';
import VisibilityAction from '@/components/visibility/VisibilityAction'; import VisibilityAction from '@/components/visibility/VisibilityAction';
import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql'; import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql';
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql'; import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
import DUPLICATE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/duplicateContentBlock.gql'; import DUPLICATE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/duplicateContentBlock.gql';
import me from '@/mixins/me'; import me from '@/mixins/me';
import { hidden } from '@/helpers/visibility'; import {hidden} from '@/helpers/visibility';
import { CONTENT_TYPE } from '@/consts/types'; import {CONTENT_TYPE} from '@/consts/types';
import PopoverLink from '@/components/ui/PopoverLink'; import PopoverLink from '@/components/ui/PopoverLink';
import { insertAtIndex, removeAtIndex } from '@/graphql/immutable-operations'; import {insertAtIndex, removeAtIndex} from '@/graphql/immutable-operations';
import { EDIT_CONTENT_BLOCK_PAGE } from '@/router/module.names'; import {EDIT_CONTENT_BLOCK_PAGE} from '@/router/module.names';
import { instrumentCategory } from '@/helpers/instrumentType'; import {defineAsyncComponent} from 'vue';
import {instrumentCategory} from '@/helpers/instrumentType';
const ContentComponent = () => const ContentComponent = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentComponent'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ContentComponent');
export default {
name: 'ContentBlock',
props: {
contentBlock: {
type: Object,
default: () => ({}),
},
parent: {
type: Object,
default: () => ({}),
},
editMode: {
type: Boolean,
default: true,
},
},
mixins: [me], export default {
name: 'ContentBlock',
props: {
contentBlock: {
type: Object,
default: () => ({}),
},
parent: {
type: Object,
default: () => ({}),
},
editMode: {
type: Boolean,
default: true,
},
},
components: { mixins: [me],
PopoverLink,
ContentComponent,
AddContentButton,
VisibilityAction,
MoreOptionsWidget,
UserWidget,
},
computed: { components: {
canEditModule() { PopoverLink,
return !this.contentBlock.indent && this.editMode; ContentComponent,
AddContentButton,
VisibilityAction,
MoreOptionsWidget,
UserWidget,
}, },
specialClass() {
return `content-block--${this.contentBlock.type.toLowerCase()}`; computed: {
}, canEditModule() {
isInstrumentBlock() { return !this.contentBlock.indent && this.editMode;
return !!this.contentBlock.instrumentCategory; },
}, specialClass() {
// 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 return `content-block--${this.contentBlock.type.toLowerCase()}`;
instrumentStyle() { },
if (this.isInstrumentBlock) { isInstrumentBlock() {
return { return !!this.contentBlock.instrumentCategory;
backgroundColor: this.contentBlock.instrumentCategory.background, },
}; // 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
} instrumentStyle() {
return {}; if (this.isInstrumentBlock) {
}, return {
instrumentLabel() { backgroundColor: this.contentBlock.instrumentCategory.background
const contentType = this.contentBlock.type.toLowerCase(); };
if (contentType.startsWith('base')) { }
// all legacy instruments start with `base` return {};
return instrumentCategory(contentType); },
} instrumentLabel() {
if (this.isInstrumentBlock) { const contentType = this.contentBlock.type.toLowerCase();
return instrumentCategory(this.contentBlock.instrumentCategory.name); if (contentType.startsWith('base')) { // all legacy instruments start with `base`
} return instrumentCategory(contentType);
return ''; }
}, if (this.isInstrumentBlock) {
// 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 return instrumentCategory(this.contentBlock.instrumentCategory.name);
instrumentLabelStyle() { }
if (this.isInstrumentBlock) { return '';
return { },
color: this.contentBlock.instrumentCategory.foreground, // 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
}; instrumentLabelStyle() {
} if (this.isInstrumentBlock) {
return {}; return {
}, color: this.contentBlock.instrumentCategory.foreground
canEditContentBlock() { };
return this.isMine && !this.contentBlock.indent; }
}, return {};
isMine() { },
return this.contentBlock.mine; canEditContentBlock() {
}, return this.isMine && !this.contentBlock.indent;
contentBlocksWithContentLists() { },
/* isMine() {
return this.contentBlock.mine;
},
contentBlocksWithContentLists() {
/*
collects all content_list_items in content_lists: collects all content_list_items in content_lists:
{ {
text_block, text_block,
@ -169,238 +202,221 @@ export default {
text_block text_block
} }
*/ */
let contentList = []; let contentList = [];
let newContent = this.contentBlock.contents.reduce((newContents, content, index) => { let newContent = this.contentBlock.contents.reduce((newContents, content, index) => {
// collect content_list_items // collect content_list_items
if (content.type === 'content_list_item') { if (content.type === 'content_list_item') {
contentList = [...contentList, content]; contentList = [...contentList, content];
if (index === this.contentBlock.contents.length - 1) { if (index === this.contentBlock.contents.length - 1) { // content is last element of contents array
// content is last element of contents array let updatedContent = [...newContents, ...this.createContentListOrBlocks(contentList)];
let updatedContent = [...newContents, ...this.createContentListOrBlocks(contentList)]; return updatedContent;
return updatedContent; }
}
return newContents;
} else {
// handle all other items and reset current content_list if necessary
if (contentList.length !== 0) {
newContents = [...newContents, ...this.createContentListOrBlocks(contentList), content];
contentList = [];
return newContents; return newContents;
} else { } else {
return [...newContents, content]; // handle all other items and reset current content_list if necessary
if (contentList.length !== 0) {
newContents = [...newContents, ...this.createContentListOrBlocks(contentList), content];
contentList = [];
return newContents;
} else {
return [...newContents, content];
}
} }
} }, []);
}, []); return Object.assign({}, this.contentBlock, {
return Object.assign({}, this.contentBlock, { contents: newContent,
contents: newContent, });
}); },
hidden() {
return hidden({
block: this.contentBlock,
schoolClass: this.schoolClass,
type: CONTENT_TYPE,
});
},
root() {
// we need the root content block id, not the generated content block if inside a content list block
return this.contentBlock.root ? this.contentBlock.root : this.contentBlock.id;
},
}, },
hidden() { methods: {
return hidden({ duplicateContentBlock({id}) {
block: this.contentBlock, const parent = this.parent;
schoolClass: this.schoolClass, this.$apollo.mutate({
type: CONTENT_TYPE, mutation: DUPLICATE_CONTENT_BLOCK_MUTATION,
}); variables: {
}, input: {
root() { id,
// we need the root content block id, not the generated content block if inside a content list block
return this.contentBlock.root ? this.contentBlock.root : this.contentBlock.id;
},
},
methods: {
duplicateContentBlock({ id }) {
const parent = this.parent;
this.$apollo.mutate({
mutation: DUPLICATE_CONTENT_BLOCK_MUTATION,
variables: {
input: {
id,
},
},
update(
store,
{
data: {
duplicateContentBlock: { contentBlock },
}, },
}
) {
if (contentBlock) {
const query = CHAPTER_QUERY;
const variables = {
id: parent.id,
};
const { chapter } = store.readQuery({ query, variables });
const index = chapter.contentBlocks.findIndex((contentBlock) => contentBlock.id === id);
const contentBlocks = insertAtIndex(chapter.contentBlocks, index, contentBlock);
const data = {
chapter: {
...chapter,
contentBlocks,
},
};
store.writeQuery({ query, variables, data });
}
},
});
},
editContentBlock(contentBlock) {
const route = {
name: EDIT_CONTENT_BLOCK_PAGE,
params: {
id: contentBlock.id,
},
};
this.$router.push(route);
},
deleteContentBlock(contentBlock) {
this.$modal
.open('confirm')
.then(() => {
this.doDeleteContentBlock(contentBlock);
})
.catch();
},
doDeleteContentBlock(contentBlock) {
const parent = this.parent;
const id = contentBlock.id;
this.$apollo.mutate({
mutation: DELETE_CONTENT_BLOCK_MUTATION,
variables: {
input: {
id,
}, },
}, update(store, {data: {duplicateContentBlock: {contentBlock}}}) {
update( if (contentBlock) {
store, const query = CHAPTER_QUERY;
{ const variables = {
data: { id: parent.id,
deleteContentBlock: { success }, };
const {chapter} = store.readQuery({query, variables});
const index = chapter.contentBlocks.findIndex(contentBlock => contentBlock.id === id);
const contentBlocks = insertAtIndex(chapter.contentBlocks, index, contentBlock);
const data = {
chapter: {
...chapter,
contentBlocks,
},
};
store.writeQuery({query, variables, data});
}
},
});
},
editContentBlock(contentBlock) {
const route = {
name: EDIT_CONTENT_BLOCK_PAGE,
params: {
id: contentBlock.id,
},
};
this.$router.push(route);
},
deleteContentBlock(contentBlock) {
this.$modal.open('confirm').then(() => {
this.doDeleteContentBlock(contentBlock);
})
.catch();
},
doDeleteContentBlock(contentBlock) {
const parent = this.parent;
const id = contentBlock.id;
this.$apollo.mutate({
mutation: DELETE_CONTENT_BLOCK_MUTATION,
variables: {
input: {
id,
}, },
} },
) { update(store, {data: {deleteContentBlock: {success}}}) {
if (success) { if (success) {
const query = CHAPTER_QUERY; const query = CHAPTER_QUERY;
const variables = { const variables = {
id: parent.id, id: parent.id,
}; };
const { chapter } = store.readQuery({ query, variables }); const {chapter} = store.readQuery({query, variables});
const index = chapter.contentBlocks.findIndex((contentBlock) => contentBlock.id === id); const index = chapter.contentBlocks.findIndex(contentBlock => contentBlock.id === id);
const contentBlocks = removeAtIndex(chapter.contentBlocks, index); const contentBlocks = removeAtIndex(chapter.contentBlocks, index);
const data = { const data = {
chapter: { chapter: {
...chapter, ...chapter,
contentBlocks, contentBlocks,
}, },
}; };
store.writeQuery({ query, variables, data }); store.writeQuery({query, variables, data});
} }
}, },
}); });
}, },
createContentListOrBlocks(contentList) { createContentListOrBlocks(contentList) {
return [ return [{
{
type: 'content_list', type: 'content_list',
contents: contentList, contents: contentList,
id: contentList[0].id, id: contentList[0].id,
}, }];
]; },
}, },
}, };
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.content-block { .content-block {
margin-bottom: $section-spacing; margin-bottom: $section-spacing;
position: relative;
&__container {
position: relative; position: relative;
}
&__title { &__container {
line-height: 1.5; position: relative;
margin-top: -0.5rem; // to offset the 1.5 line height, it leaves a padding on top
}
&__instrument-label {
margin-bottom: $medium-spacing;
@include regular-text();
}
&__action-button {
cursor: pointer;
}
&__user-widget {
margin-right: 0;
}
&--base_communication {
@include content-box($color-accent-1-list);
.content-block__instrument-label {
color: $color-accent-1-dark;
} }
}
&--task { &__title {
@include light-border(bottom); line-height: 1.5;
margin-top: -0.5rem; // to offset the 1.5 line height, it leaves a padding on top
}
.content-block__title { &__instrument-label {
color: $color-brand; margin-bottom: $medium-spacing;
margin-top: $default-padding; @include regular-text();
margin-bottom: $large-spacing; }
@include light-border(bottom);
@include desktop { &__action-button {
margin-top: 0; cursor: pointer;
}
&__user-widget {
margin-right: 0;
}
&--base_communication {
@include content-box($color-accent-1-list);
.content-block__instrument-label {
color: $color-accent-1-dark;
} }
} }
}
&--base_society { &--task {
@include content-box($color-accent-2-list); @include light-border(bottom);
.content-block__instrument-label { .content-block__title {
color: $color-accent-2-dark; color: $color-brand;
} margin-top: $default-padding;
} margin-bottom: $large-spacing;
@include light-border(bottom);
&--base_interdisciplinary { @include desktop {
@include content-box($color-accent-4-list); margin-top: 0;
}
.content-block__instrument-label { }
color: $color-accent-4-dark;
}
}
&--instrument {
@include content-box-base;
}
:deep(p) {
line-height: 1.5;
margin-bottom: 1em;
&:last-child {
margin-bottom: 0;
}
}
:deep(.text-block) {
ul {
@include list-parent;
} }
li { &--base_society {
@include list-child; @include content-box($color-accent-2-list);
.content-block__instrument-label {
color: $color-accent-2-dark;
}
}
&--base_interdisciplinary {
@include content-box($color-accent-4-list);
.content-block__instrument-label {
color: $color-accent-4-dark;
}
}
&--instrument {
@include content-box-base;
}
/deep/ p {
line-height: 1.5; line-height: 1.5;
margin-bottom: 1em;
&:last-child {
margin-bottom: 0;
}
} }
/deep/ .text-block {
ul {
@include list-parent;
}
li {
@include list-child;
line-height: 1.5;
}
}
} }
}
</style> </style>

View File

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

View File

@ -1,4 +1,10 @@
<a class="header-bar__sidebar-link" data-cy="open-sidebar-link" @click.stop="openSidebar('navigation')"> <template>
<header class="header-bar">
<a
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" />
@ -22,112 +28,113 @@
</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';
const Hamburger = () => 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;
background-color: $color-white;
grid-auto-rows: 50px;
width: 100%;
max-width: 100vw;
grid-template-columns: 1fr 1fr 1fr;
@include desktop {
grid-template-columns: 50px 1fr auto;
grid-template-rows: 50px;
grid-auto-rows: auto;
}
/*
* For IE10+
*/
-ms-grid-columns: 1fr 1fr 1fr;
-ms-grid-rows: 50px 50px;
/*
* For IE10+
*/
& > :nth-child(1) {
-ms-grid-column: 1;
-ms-grid-row-align: center;
}
&__content-navigation {
grid-column: 2;
justify-content: space-between; justify-content: space-between;
} background-color: $color-white;
grid-auto-rows: 50px;
width: 100%;
max-width: 100vw;
&__sidebar-link { grid-template-columns: 1fr 1fr 1fr;
padding: $small-spacing;
cursor: pointer;
}
&__sidebar-icon {
width: 30px;
height: 30px;
}
/*
* For IE10+
*/
& > :nth-child(3) {
-ms-grid-column: 3;
-ms-grid-row-align: center;
-ms-grid-column-align: end;
justify-self: end;
}
& > :nth-child(4) {
-ms-grid-row: 2;
-ms-grid-column: 1;
-ms-grid-column-span: 3;
}
}
.user-header {
display: flex;
&__current-class {
margin-right: $large-spacing;
}
&__sidebar-link {
cursor: pointer;
display: none;
@include desktop { @include desktop {
display: flex; grid-template-columns: 50px 1fr auto;
grid-template-rows: 50px;
grid-auto-rows: auto;
}
/*
* For IE10+
*/
-ms-grid-columns: 1fr 1fr 1fr;
-ms-grid-rows: 50px 50px;
/*
* For IE10+
*/
& > :nth-child(1) {
-ms-grid-column: 1;
-ms-grid-row-align: center;
}
&__content-navigation {
grid-column: 2;
justify-content: space-between;
}
&__sidebar-link {
padding: $small-spacing;
cursor: pointer;
}
&__sidebar-icon {
width: 30px;
height: 30px;
}
/*
* For IE10+
*/
& > :nth-child(3) {
-ms-grid-column: 3;
-ms-grid-row-align: center;
-ms-grid-column-align: end;
justify-self: end;
}
& > :nth-child(4) {
-ms-grid-row: 2;
-ms-grid-column: 1;
-ms-grid-column-span: 3;
}
}
.user-header {
display: flex;
&__current-class {
margin-right: $large-spacing;
}
&__sidebar-link {
cursor: pointer;
display: none;
@include desktop {
display: flex;
}
} }
} }
}
</style> </style>

View File

@ -10,65 +10,67 @@
</template> </template>
<script> <script>
const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon'); import {defineAsyncComponent} from 'vue';
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: 0; left: 30px;
top: 18px; top: 0px;
margin-left: -1px; width: 400px;
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;
width: 10px; border: 1px solid $color-silver-dark;
height: 10px; border-radius: 5px;
transform: rotate(-45deg) translateY(-50%); padding: $small-spacing;
@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,51 +1,58 @@
<template> <template>
<button :disabled="loading || disabled" class="loading-button button button--primary button--big"> <button
: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 class="loading-button__icon" v-else /> <loading-icon
class="loading-button__icon"
v-else
/>
</button> </button>
</template> </template>
<script> <script>
const LoadingIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/LoadingIcon'); import {defineAsyncComponent} from 'vue';
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: ''
}
}, },
disabled: { components: {
type: Boolean, LoadingIcon
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,59 +4,66 @@
<hamburger class="mobile-header__hamburger" /> <hamburger class="mobile-header__hamburger" />
</a> </a>
<router-link to="/" data-cy="mobile-home-link"> <router-link
to="/"
data-cy="mobile-home-link"
>
<logo /> <logo />
</router-link> </router-link>
<user-widget v-bind="me" @click.stop="openSidebar('profile')" /> <user-widget
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';
const Logo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Logo'); const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo'));
const Hamburger = () => 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,11 +1,7 @@
<template> <template>
<div class="modal__backdrop"> <div class="modal__backdrop">
<div <div
:class="{ :class="{'modal--hide-header': hideHeader || fullscreen, 'modal--fullscreen': fullscreen, 'modal--small': small}"
'modal--hide-header': hideHeader || fullscreen,
'modal--fullscreen': fullscreen,
'modal--small': small,
}"
class="modal" class="modal"
> >
<div class="modal__header"> <div class="modal__header">
@ -13,14 +9,20 @@
</div> </div>
<div class="modal__body"> <div class="modal__body">
<slot /> <slot />
<div class="modal__close-button" @click="hideModal"> <div
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 class="button" @click="hideModal">Abbrechen</a> <a
class="button"
@click="hideModal"
>Abbrechen</a>
</slot> </slot>
</div> </div>
</div> </div>
@ -28,158 +30,160 @@
</template> </template>
<script> <script>
const Cross = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'); import {defineAsyncComponent} from 'vue';
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: {
type: Boolean,
default: false,
},
small: {
type: Boolean,
default: false,
},
},
components: { components: {
Cross, Cross
},
methods: {
hideModal() {
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;
} }
position: fixed; grid-template-rows: auto 1fr 65px;
top: 0; grid-template-areas: "header" "body" "footer";
left: 0; -ms-grid-rows: auto 1fr 65px;
bottom: 0; position: relative;
right: 0;
background-color: rgba($color-white, 0.8);
z-index: 90;
}
&__header { &__backdrop {
grid-area: header; display: flex;
-ms-grid-row: 1; justify-content: center;
padding: 10px $modal-lateral-padding; @supports (display: grid) {
border-bottom: 1px solid $color-silver-light; display: grid;
} }
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba($color-white, 0.8);
z-index: 90;
}
&__body { &__header {
grid-area: body; grid-area: header;
-ms-grid-row: 2; -ms-grid-row: 1;
padding: 10px $modal-lateral-padding; padding: 10px $modal-lateral-padding;
overflow: auto; border-bottom: 1px solid $color-silver-light;
box-sizing: border-box; }
min-height: 30vh;
}
&__close-button { &__body {
display: none; grid-area: body;
cursor: pointer; -ms-grid-row: 2;
position: absolute; padding: 10px $modal-lateral-padding;
right: 15px; overflow: auto;
top: 15px; box-sizing: border-box;
background: rgba($color-white, 0.5); min-height: 30vh;
border-radius: 40px; }
padding: 10px;
align-content: center;
}
&__footer { &__close-button {
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;
} }
#{$parent}__body { &__footer {
padding: $default-padding; grid-area: footer;
} -ms-grid-row: 3;
} border-top: 1px solid $color-silver-light;
padding: 16px $modal-lateral-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 { $parent: &;
padding: 0;
scrollbar-width: none;
margin-right: -5px;
height: auto; &--hide-header {
max-height: 95vh; grid-template-rows: 1fr 65px;
grid-template-areas: "body" "footer";
&::-webkit-scrollbar { #{$parent}__header {
display: none; 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;
}
} }
#{$parent}__close-button { &--small {
display: flex; height: auto;
#{$parent}__body {
min-height: 0;
}
} }
} }
&--small {
height: auto;
#{$parent}__body {
min-height: 0;
}
}
}
</style> </style>

View File

@ -1,59 +1,68 @@
<template> <template>
<div class="more-options"> <div class="more-options">
<a class="more-options__more-link" data-cy="more-options-link" @click.stop="showMenu = !showMenu"> <a
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 class="more-options__popover" v-if="showMenu" @hide-me="showMenu = false"> <widget-popover
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';
const Ellipses = () => 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;
justify-content: flex-end;
&__ellipses {
width: 30px;
height: 30px;
fill: $color-charcoal-dark;
margin-top: -7px;
}
&__more-link {
background-color: rgba($color-white, 0.9);
width: 35px;
height: 15px;
border-radius: 15px;
display: flex; display: flex;
justify-content: center; justify-content: flex-end;
}
&__popover { &__ellipses {
min-width: 200px; width: 30px;
@include popover-defaults(); height: 30px;
fill: $color-charcoal-dark;
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();
}
} }
}
</style> </style>

View File

@ -11,7 +11,8 @@
</template> </template>
<script> <script>
const ArrowUp = () => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowUp'); import {defineAsyncComponent} from 'vue';
const ArrowUp = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowUp'));
export default { export default {
components: { components: {

View File

@ -1,40 +1,44 @@
<template> <template>
<div class="submission-document"> <div class="submission-document">
<p class="submission-document__content content" v-if="document && document.length > 0"> <p
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 filenameFromUrl from '@/helpers/urls'; import {defineAsyncComponent} from 'vue';
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon'); import filenameFromUrl from '@/helpers/urls';
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 { &__text {
align-self: center; align-self: center;
padding-left: 5px; padding-left: 5px;
}
} }
}
</style> </style>

View File

@ -1,9 +1,12 @@
<template> <template>
<nav :class="{ 'content-navigation--sidebar': isSidebar }" class="content-navigation"> <nav
: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"
@ -12,7 +15,9 @@
{{ $flavor.textTopics }} {{ $flavor.textTopics }}
</router-link> </router-link>
<topic-navigation v-if="isSidebar" /> <topic-navigation
v-if="isSidebar"
/>
</div> </div>
<div class="content-navigation__item"> <div class="content-navigation__item">
@ -28,7 +33,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"
@ -40,14 +45,19 @@
</div> </div>
</div> </div>
<router-link to="/" class="content-navigation__logo" data-cy="home-link" v-if="!isSidebar"> <router-link
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"
@ -57,7 +67,10 @@
</router-link> </router-link>
</div> </div>
<div class="content-navigation__item content-navigation__item--secondary" v-if="showPortfolio"> <div
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"
@ -67,13 +80,16 @@
Portfolio Portfolio
</router-link> </router-link>
</div> </div>
<div class="content-navigation__item content-navigation__item--secondary" v-if="isSidebar"> <div
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>
@ -81,142 +97,142 @@
</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';
import {defineAsyncComponent} from 'vue';
const Logo = () => 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: { computed: {
showPortfolio() { showPortfolio() {
return this.$flavor.showPortfolio; return this.$flavor.showPortfolio;
}
}, },
},
methods: { methods: {
isActive(linkName) { isActive(linkName) {
return linkName === 'book' && this.$route.path.indexOf('module') > -1; return linkName === 'book' && this.$route.path.indexOf('module') > -1;
}, },
isRoomUrl() { isRoomUrl() {
return this.$route.path.indexOf('room') > -1; return this.$route.path.indexOf('room') > -1;
}, },
close() { close() {
this.closeSidebar('navigation'); 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;
align-items: center;
&__link {
padding: 0 24px;
@include navigation-link;
}
&__primary,
&__secondary {
display: none;
flex-direction: row;
@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; align-items: center;
/* &__link {
* For IE10+ padding: 0 24px;
*/ @include navigation-link;
-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 { &__primary, &__secondary {
color: $color-brand; display: none;
} flex-direction: row;
}
$parent: &; @include desktop {
display: flex;
&--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 { &__logo {
width: 100%; color: #17A887;
//border-bottom: 1px solid $color-white; font-size: 36px;
font-weight: 800;
font-family: $sans-serif-font-family;
display: flex;
justify-self: center;
/*&:nth-child(1) {*/ /*
/* order: 3;*/ * For IE10+
/* border-bottom: 0;*/ */
/*}*/ -ms-grid-column: 2;
-ms-grid-row-align: center;
-ms-grid-column-align: center;
}
/*&:nth-child(2) {*/ &__logo-icon {
/* order: 1;*/ width: auto;
/*}*/ height: 31px;
}
/*&:nth-child(3) {*/ &__link {
/* order: 2;*/ &--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

@ -23,8 +23,9 @@
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';
const Cross = () => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon'); const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon'));
export default { export default {
mixins: [sidebarMixin], mixins: [sidebarMixin],

View File

@ -1,44 +1,55 @@
<template> <template>
<div :class="{ 'sub-navigation-item--active': show }" class="sub-navigation-item" v-click-outside="close"> <div
<div class="sub-navigation-item__title" @click="show = !show"> :class="{ 'sub-navigation-item--active': 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 class="sub-navigation-item__nav-items book-subnavigation" v-if="show"> <div
class="sub-navigation-item__nav-items book-subnavigation"
v-if="show"
>
<slot /> <slot />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const ChevronDown = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronDown'); import {defineAsyncComponent} from 'vue';
const ChevronUp = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronUp'); const ChevronDown = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown'));
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;
}, },
},
methods: { data() {
close() { return {
this.show = false; show: false
};
}, },
},
}; watch: {
$route() {
this.show = false;
}
},
methods: {
close() {
this.show = false;
}
}
};
</script> </script>

View File

@ -26,10 +26,14 @@
: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"
/> />
@ -39,321 +43,313 @@
</template> </template>
<script> <script>
import ContentFormSection from '@/components/content-block-form/ContentFormSection'; import ContentFormSection from '@/components/content-block-form/ContentFormSection';
import ContentElementActions from '@/components/content-block-form/ContentElementActions'; import ContentElementActions from '@/components/content-block-form/ContentElementActions';
import {defineAsyncComponent} from 'vue';
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'); const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
const ContentBlockElementChooserWidget = () => const ContentBlockElementChooserWidget = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/ContentBlockElementChooserWidget'));
import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/ContentBlockElementChooserWidget'); const LinkForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/LinkForm'));
const LinkForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/LinkForm'); const VideoForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/VideoForm'));
const VideoForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/VideoForm'); const ImageForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/ImageForm'));
const ImageForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/ImageForm'); const DocumentForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/DocumentForm'));
const DocumentForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/DocumentForm'); const AssignmentForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/AssignmentForm'));
const AssignmentForm = () => const TextForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/TipTap.vue'));
import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/AssignmentForm'); const SubtitleForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/SubtitleForm'));
const TextForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/TipTap.vue'); // readonly blocks
const SubtitleForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/SubtitleForm'); const Assignment = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/assignment/Assignment'));
// readonly blocks const SurveyBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/SurveyBlock'));
const Assignment = () => const Solution = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/Solution'));
import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/assignment/Assignment'); const ImageBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ImageBlock'));
const SurveyBlock = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/SurveyBlock'); const Instruction = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/Instruction'));
const Solution = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/Solution'); const ModuleRoomSlug = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ModuleRoomSlug'));
const ImageBlock = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/ImageBlock'); const CmsDocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/CmsDocumentBlock'));
const Instruction = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/Instruction'); const ThinglinkBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ThinglinkBlock'));
const ModuleRoomSlug = () => const InfogramBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/InfogramBlock'));
import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/ModuleRoomSlug');
const CmsDocumentBlock = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/CmsDocumentBlock');
const ThinglinkBlock = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/ThinglinkBlock');
const InfogramBlock = () => 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
topLevel: {
type: Boolean,
default: true,
},
firstElement: {
type: Boolean,
required: true,
},
lastElement: {
type: Boolean,
required: true,
},
},
components: { components: {
ContentElementActions, ContentElementActions,
ContentFormSection, ContentFormSection,
TrashIcon, TrashIcon,
ContentBlockElementChooserWidget, ContentBlockElementChooserWidget,
LinkForm, LinkForm,
VideoForm, VideoForm,
ImageForm, ImageForm,
DocumentForm, DocumentForm,
AssignmentForm, AssignmentForm,
TextForm, TextForm,
SubtitleForm, SubtitleForm,
SurveyBlock, SurveyBlock,
Solution, Solution,
ImageBlock, ImageBlock,
Instruction, Instruction,
ModuleRoomSlug, ModuleRoomSlug,
CmsDocumentBlock, CmsDocumentBlock,
InfogramBlock, InfogramBlock,
ThinglinkBlock, ThinglinkBlock,
Assignment, Assignment
}, },
computed: { computed: {
actions() { actions() {
return { return {
up: !this.firstElement, up: !this.firstElement,
down: !this.lastElement, down: !this.lastElement,
extended: this.topLevel, 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;
},
}, },
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: { methods: {
getType(element) { getType(element) {
switch (element.type) { switch (element.type) {
case 'subtitle': case 'subtitle':
return { return {
component: 'subtitle-form', component: 'subtitle-form',
title: 'Untertitel', title: 'Untertitel',
icon: 'title-icon', icon: 'title-icon',
}; };
case 'link_block': case 'link_block':
return { return {
component: 'link-form', component: 'link-form',
title: 'Link', title: 'Link',
icon: 'link-icon', icon: 'link-icon',
}; };
case 'video_block': case 'video_block':
return { return {
component: 'video-form', component: 'video-form',
title: 'Video', title: 'Video',
icon: 'video-icon', icon: 'video-icon',
}; };
case 'image_url_block': case 'image_url_block':
return { return {
component: 'image-form', component: 'image-form',
title: 'Bild', title: 'Bild',
icon: 'image-icon', icon: 'image-icon',
}; };
case 'text_block': case 'text_block':
return { return {
component: 'text-form', component: 'text-form',
title: 'Text', title: 'Text',
icon: 'text-icon', icon: 'text-icon',
}; };
case 'assignment': case 'assignment':
return { return {
component: element.id ? 'assignment' : 'assignment-form', // prevent editing of existing assignments component: element.id ? 'assignment' : 'assignment-form', // prevent editing of existing assignments
title: 'Aufgabe & Ergebnis', title: 'Aufgabe & Ergebnis',
icon: 'speech-bubble-icon', icon: 'speech-bubble-icon',
}; };
case 'document_block': case 'document_block':
return { return {
component: 'document-form', component: 'document-form',
title: 'Dokument', title: 'Dokument',
icon: 'document-icon', icon: 'document-icon',
}; };
case 'survey': case 'survey':
return { return {
component: 'survey-block', component: 'survey-block',
title: 'Übung', title: 'Übung',
}; };
case 'solution': case 'solution':
return { return {
component: 'solution', component: 'solution',
title: 'Lösung', title: 'Lösung',
}; };
case 'image_block': case 'image_block':
return { return {
component: 'image-block', component: 'image-block',
title: 'Bild', title: 'Bild',
}; };
case 'instruction': case 'instruction':
return { return {
component: 'instruction', component: 'instruction',
title: 'Instruktion', title: 'Instruktion',
}; };
case 'module_room_slug': case 'module_room_slug':
return { return {
component: 'module-room-slug', component: 'module-room-slug',
title: 'Raum', title: 'Raum',
}; };
case 'cms_document_block': case 'cms_document_block':
return { return {
component: 'cms-document-block', component: 'cms-document-block',
title: 'Dokument', title: 'Dokument',
}; };
case 'thinglink_block': case 'thinglink_block':
return { return {
component: 'thinglink-block', component: 'thinglink-block',
title: 'Interaktive Grafik', title: 'Interaktive Grafik'
}; };
case 'infogram_block': case 'infogram_block':
return { return {
component: 'infogram-block', component: 'infogram-block',
title: 'Interaktive Grafik', title: 'Interaktive Grafik'
}; };
} }
return { return {
component: CHOOSER, component: CHOOSER,
title: '', title: '',
icon: '', icon: '',
}; };
}, },
_updateProperty(value, key) { _updateProperty(value, key) {
// const content = this.localContentBlock.contents[index]; // const content = this.localContentBlock.contents[index];
const content = this.element; const content = this.element;
this.update({ this.update({
...content, ...content,
value: { value: {
...content.value, ...content.value,
[key]: value, [key]: value,
}, },
}); });
}, },
changeUrl(value) { changeUrl(value) {
this._updateProperty(value, 'url'); this._updateProperty(value, 'url');
}, },
changeText(value) { changeText(value) {
this._updateProperty(value, 'text'); this._updateProperty(value, 'text');
}, },
changeAssignmentTitle(value) { changeAssignmentTitle(value) {
this._updateProperty(value, 'title'); this._updateProperty(value, 'title');
}, },
changeAssignmentAssignment(value) { changeAssignmentAssignment(value) {
this._updateProperty(value, 'assignment'); this._updateProperty(value, 'assignment');
}, },
changeType({ type, convertToList }, value) { changeType({type, convertToList}, value) {
let el = { let el = {
type: type, type: type,
value: Object.assign({}, value), value: Object.assign({}, value),
}; };
switch (type) { switch (type) {
case 'subtitle': case 'subtitle':
el = { el = {
...el, ...el,
value: { value: {
text: '', text: '',
}, },
}; };
break; break;
case 'text_block': case 'text_block':
el = { el = {
...el, ...el,
value: { value: {
text: '', text: '',
}, },
}; };
break; break;
case 'link_block': case 'link_block':
el = { el = {
...el, ...el,
value: { value: {
text: '', text: '',
url: '',
},
};
break;
case 'video_block':
el = {
...el,
value: {
url: '',
},
};
break;
case 'document_block':
el = {
...el,
value: Object.assign(
{
url: '', url: '',
}, },
value };
), break;
}; case 'video_block':
break; el = {
case 'image_url_block': ...el,
el = { value: {
...el, url: '',
value: { },
url: '', };
}, break;
}; case 'document_block':
break; el = {
} ...el,
value: Object.assign({
url: '',
}, value),
};
break;
case 'image_url_block':
el = {
...el,
value: {
url: '',
},
};
break;
}
if (convertToList) { if (convertToList) {
el = { el = {
type: 'content_list_item', type: 'content_list_item',
contents: [el], contents: [el],
}; };
} }
this.update(el); this.update(el);
},
update(element) {
this.$emit('update', element);
},
switchToDocument(value) {
this.changeType('document_block', value);
},
}, },
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

@ -18,150 +18,167 @@
/> />
</template> </template>
<add-content-element :index="-1" class="contents-form__add" @add-element="addElement" /> <add-content-element
<div class="contents-form__element" v-for="(element, index) in localContentBlock.contents" :key="index"> :index="-1"
<content-element :element="element" @update="update(index, $event)" @remove="remove(index)" /> class="contents-form__add"
@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 :index="index" class="contents-form__add" @add-element="addElement" /> <add-content-element
: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
<a class="button" @click="$emit('hide')">Abbrechen</a> class="button"
@click="$emit('hide')"
>Abbrechen</a>
</div> </div>
</template> </template>
</modal> </modal>
</template> </template>
<script> <script>
import { meQuery } from '@/graphql/queries'; import {defineAsyncComponent} from 'vue';
import {meQuery} from '@/graphql/queries';
const ModalInput = () => import(/* webpackChunkName: "content-forms" */ '@/components/ModalInput'); const ModalInput = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/ModalInput'));
const AddContentElement = () => import(/* webpackChunkName: "content-forms" */ '@/components/AddContentElement'); const AddContentElement = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/AddContentElement'));
const ContentElement = () => const ContentElement = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-block-form/ContentElement'));
import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/ContentElement');
const Modal = () => import('@/components/Modal.vue'); const Modal = defineAsyncComponent(() => import('@/components/Modal'));
const Checkbox = () => import('@/components/ui/Checkbox.vue'); 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: {
type: Boolean,
default: false,
},
disableSave: {
type: Boolean,
default: false,
},
},
components: { components: {
ContentElement, ContentElement,
Modal, Modal,
ModalInput, ModalInput,
AddContentElement, AddContentElement,
Checkbox, Checkbox,
}, },
data() { data() {
return { return {
error: false, error: false,
localContentBlock: Object.assign( 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);
} }
), },
me: {}, 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);
},
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,40 +1,43 @@
<template> <template>
<content-list :items="contentBlocks"> <content-list
:items="contentBlocks"
>
<template #default="{ item }"> <template #default="{ item }">
<content-block :content-block="item" :parent="parent" /> <content-block
:content-block="item"
:parent="parent"
/>
</template> </template>
</content-list> </content-list>
</template> </template>
<script> <script>
import ContentList from '@/components/content-blocks/ContentList.vue'; import {defineAsyncComponent} from 'vue';
export default { const ContentList = defineAsyncComponent(() => import('@/components/content-blocks/ContentList'));
name: 'ContentBlockList', const ContentBlock = defineAsyncComponent(() => import('@/components/ContentBlock'));
props: ['contents', 'parent'],
components: { export default {
ContentList, name: 'ContentBlockList',
// https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components props: ['contents', 'parent'],
ContentBlock: () => import('@/components/ContentBlock.vue'),
},
computed: { components: {
contentBlocks() { ContentList,
return this.contents.map((contentBlock) => { ContentBlock
const contents = contentBlock.value ? [...contentBlock.value] : [];
return Object.assign({}, contentBlock, {
contents,
indent: true,
bookmarks: this.parent.bookmarks,
notes: this.parent.notes,
root: this.parent.id,
});
});
}, },
},
};
</script>
<style scoped lang="scss"> computed: {
@import '~styles/helpers'; contentBlocks() {
</style> return this.contents.map(contentBlock => {
const contents = contentBlock.value ? [...contentBlock.value] : [];
return Object.assign({}, contentBlock, {
contents,
indent: true,
bookmarks: this.parent.bookmarks,
notes: this.parent.notes,
root: this.parent.id
});
});
}
}
};
</script>

View File

@ -1,71 +1,80 @@
<template> <template>
<div class="document-block"> <div class="document-block">
<document-icon class="document-block__icon" /> <document-icon class="document-block__icon" />
<a :href="value.url" class="document-block__link" target="_blank">{{ urlName }}</a> <a
<a class="document-block__remove" v-if="showTrashIcon" @click="$emit('trash')"> :href="value.url"
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>
</template> </template>
<script> <script>
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon'); import {defineAsyncComponent} from 'vue';
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'); const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
export default { export default {
props: { props: {
value: Object, value: Object,
showTrashIcon: Boolean, showTrashIcon: Boolean,
},
components: {
DocumentIcon,
TrashIcon,
},
computed: {
urlName: function () {
if (this.value && this.value.url) {
const parts = this.value.url.split('/');
return parts[parts.length - 1];
}
return null;
}, },
},
}; components: {
DocumentIcon,
TrashIcon,
},
computed: {
urlName: function() {
if (this.value && this.value.url) {
const parts = this.value.url.split('/');
return parts[parts.length - 1];
}
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;
&__icon {
width: 30px;
height: 30px;
}
&__link {
text-decoration: underline;
}
&__remove {
display: flex;
justify-content: center;
align-items: center; align-items: center;
width: 50px;
height: 50px;
}
&__trash-icon { &__icon {
width: 25px; width: 30px;
height: 25px; height: 30px;
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,50 +1,57 @@
<template> <template>
<div class="instruction" v-if="me.isTeacher"> <div
class="instruction"
v-if="me.isTeacher"
>
<bulb-icon class="instruction__icon" /> <bulb-icon class="instruction__icon" />
<a :href="url" class="instruction__link">{{ text }}</a> <a
:href="url"
class="instruction__link"
>{{ text }}</a>
</div> </div>
</template> </template>
<script> <script>
import me from '@/mixins/me'; import me from '@/mixins/me';
const BulbIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/BulbIcon'); import {defineAsyncComponent} from 'vue';
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() {
return this.value.document ? this.value.document.url : this.value.url; computed: {
}, 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,52 +1,60 @@
<template> <template>
<div :class="{ 'link-block--no-margin': noMargin }" class="link-block"> <div
:class="{ 'link-block--no-margin': noMargin}"
class="link-block"
>
<link-icon class="link-block__icon" /> <link-icon class="link-block__icon" />
<a :href="href" class="link-block__link" target="_blank">{{ value.text }}</a> <a
:href="href"
class="link-block__link"
target="_blank"
>{{ value.text }}</a>
</div> </div>
</template> </template>
<script> <script>
const LinkIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/LinkIcon'); import {defineAsyncComponent} from 'vue';
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 { &__icon {
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
&__link { &__link {
text-decoration: underline; text-decoration: underline;
}
} }
}
</style> </style>

View File

@ -1,9 +1,19 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div :data-scrollto="value.id" class="assignment"> <div
<p class="assignment__main-text" data-cy="assignment-main-text" v-html="assignment.assignment" /> :data-scrollto="value.id"
class="assignment"
>
<p
class="assignment__main-text"
data-cy="assignment-main-text"
v-html="assignment.assignment"
/>
<solution :value="solution" v-if="assignment.solution" /> <solution
:value="solution"
v-if="assignment.solution"
/>
<template v-if="isStudent"> <template v-if="isStudent">
<submission-form <submission-form
@ -23,92 +33,101 @@
@spellcheck="spellcheck" @spellcheck="spellcheck"
/> />
<spell-check :corrections="corrections" :text="submission.text" /> <spell-check
:corrections="corrections"
:text="submission.text"
/>
<p class="assignment__feedback" v-if="assignment.submission.submissionFeedback" v-html="feedbackText" /> <p
class="assignment__feedback"
v-if="assignment.submission.submissionFeedback"
v-html="feedbackText"
/>
</template> </template>
<template v-if="!isStudent"> <template v-if="!isStudent">
<router-link :to="{ name: 'submissions', params: { id: assignment.id } }" class="button button--primary"> <router-link
Zu den Ergebnissen :to="{name: 'submissions', params: { id: assignment.id }}"
class="button button--primary"
>
Zu den
Ergebnissen
</router-link> </router-link>
</template> </template>
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import {mapActions, mapGetters} from 'vuex';
import ASSIGNMENT_QUERY from '@/graphql/gql/queries/assignmentQuery.gql'; import ASSIGNMENT_QUERY from '@/graphql/gql/queries/assignmentQuery.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import UPDATE_ASSIGNMENT_MUTATION from '@/graphql/gql/mutations/updateAssignmentMutation.gql'; import UPDATE_ASSIGNMENT_MUTATION from '@/graphql/gql/mutations/updateAssignmentMutation.gql';
import UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS from '@/graphql/gql/mutations/updateAssignmentMutationWithSuccess.gql'; import UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS from '@/graphql/gql/mutations/updateAssignmentMutationWithSuccess.gql';
import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql'; import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import { sanitize } from '@/helpers/text'; import {sanitize} from '@/helpers/text';
import {defineAsyncComponent} from 'vue';
const SubmissionForm = () => const SubmissionForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SubmissionForm'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/SubmissionForm'); const Solution = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Solution'));
const Solution = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/Solution'); const SpellCheck = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SpellCheck'));
const SpellCheck = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/SpellCheck');
export default { export default {
props: ['value'], props: ['value'],
components: { components: {
Solution, Solution,
SubmissionForm, SubmissionForm,
SpellCheck, SpellCheck,
},
data() {
return {
assignment: {
submission: this.initialSubmission(),
},
me: {
permissions: [],
},
inputType: 'text',
unsaved: false,
saving: 0,
corrections: '',
spellcheckLoading: false,
};
},
computed: {
...mapGetters(['scrollToAssignmentId']),
final() {
return !!this.submission && this.submission.final;
}, },
submission() {
return this.assignment.submission ? this.assignment.submission : {}; data() {
},
isStudent() {
return !this.me.permissions.includes('users.can_manage_school_class_content');
},
solution() {
return { return {
text: this.assignment.solution, assignment: {
submission: this.initialSubmission(),
},
me: {
permissions: [],
},
inputType: 'text',
unsaved: false,
saving: 0,
corrections: '',
spellcheckLoading: false,
}; };
}, },
id() {
return this.assignment.id ? this.assignment.id.replace(/=/g, '') : '';
},
feedbackText() {
let feedback = this.assignment.submission.submissionFeedback;
let sanitizedFeedbackText = sanitize(feedback.text);
return `<span class="inline-title">Feedback von ${feedback.teacher.firstName} ${feedback.teacher.lastName}:</span> ${sanitizedFeedbackText}`;
},
},
methods: { computed: {
...mapActions(['scrollToAssignmentReady']), ...mapGetters(['scrollToAssignmentId']),
_save: debounce(function (submission) { final() {
this.saving++; return !!this.submission && this.submission.final;
this.$apollo },
.mutate({ submission() {
return this.assignment.submission ? this.assignment.submission : {};
},
isStudent() {
return !this.me.permissions.includes('users.can_manage_school_class_content');
},
solution() {
return {
text: this.assignment.solution,
};
},
id() {
return this.assignment.id ? this.assignment.id.replace(/=/g, '') : '';
},
feedbackText() {
let feedback = this.assignment.submission.submissionFeedback;
let sanitizedFeedbackText = sanitize(feedback.text);
return `<span class="inline-title">Feedback von ${feedback.teacher.firstName} ${feedback.teacher.lastName}:</span> ${sanitizedFeedbackText}`;
},
},
methods: {
...mapActions(['scrollToAssignmentReady']),
_save: debounce(function (submission) {
this.saving++;
this.$apollo.mutate({
mutation: UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS, mutation: UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS,
variables: { variables: {
input: { input: {
@ -119,14 +138,7 @@ export default {
}, },
}, },
}, },
update( update(store, {data: {updateAssignment: {successful, updatedAssignment}}}) {
store,
{
data: {
updateAssignment: { successful, updatedAssignment },
},
}
) {
try { try {
if (successful) { if (successful) {
const query = ASSIGNMENT_QUERY; const query = ASSIGNMENT_QUERY;
@ -137,82 +149,80 @@ export default {
submission, submission,
}); });
const data = { const data = {
assignment, assignment
}; };
store.writeQuery({ query, variables, data }); store.writeQuery({query, variables, data});
} }
} catch (e) { } catch (e) {
console.error(e); console.error(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.saving--; this.saving--;
if (this.saving === 0) { if (this.saving === 0) {
this.unsaved = false; this.unsaved = false;
} }
}); });
}, 500), }, 500),
saveInput: function (answer) { saveInput: function (answer) {
// reset corrections on input // reset corrections on input
this.corrections = ''; this.corrections = '';
this.unsaved = true; this.unsaved = true;
/* /*
We update the assignment on this component, so the changes are reflected on it. The server does not return We update the assignment on this component, so the changes are reflected on it. The server does not return
the updated entity, to prevent the UI to update when the user is entering his input the updated entity, to prevent the UI to update when the user is entering his input
*/ */
this.assignment.submission.text = answer; this.assignment.submission.text = answer;
this._save(this.assignment.submission); this._save(this.assignment.submission);
}, },
changeDocumentUrl(documentUrl) { changeDocumentUrl(documentUrl) {
this.assignment.submission.document = documentUrl; this.assignment.submission.document = documentUrl;
this._save(this.assignment.submission); this._save(this.assignment.submission);
}, },
turnIn() { turnIn() {
// reset corrections on turn in // reset corrections on turn in
this.corrections = ''; this.corrections = '';
this.$apollo.mutate({ this.$apollo.mutate({
mutation: UPDATE_ASSIGNMENT_MUTATION, mutation: UPDATE_ASSIGNMENT_MUTATION,
variables: { variables: {
input: { input: {
assignment: { assignment: {
id: this.assignment.id, id: this.assignment.id,
answer: this.assignment.submission.text, answer: this.assignment.submission.text,
document: this.assignment.submission.document, document: this.assignment.submission.document,
final: true, final: true,
},
}, },
}, },
}, });
}); },
}, reopen() {
reopen() { this.$apollo.mutate({
this.$apollo.mutate({ mutation: UPDATE_ASSIGNMENT_MUTATION,
mutation: UPDATE_ASSIGNMENT_MUTATION, variables: {
variables: { input: {
input: { assignment: {
assignment: { id: this.assignment.id,
id: this.assignment.id, answer: this.assignment.submission.text,
answer: this.assignment.submission.text, document: this.assignment.submission.document,
document: this.assignment.submission.document, final: false,
final: false, },
}, },
}, },
}, });
}); },
}, initialSubmission() {
initialSubmission() { return {
return { text: '',
text: '', document: '',
document: '', final: false,
final: false, };
}; },
}, spellcheck() {
spellcheck() { let self = this;
let self = this; this.spellcheckLoading = true;
this.spellcheckLoading = true; this.$apollo.mutate({
this.$apollo
.mutate({
mutation: SPELL_CHECK_MUTATION, mutation: SPELL_CHECK_MUTATION,
variables: { variables: {
input: { input: {
@ -220,96 +230,90 @@ export default {
text: this.assignment.submission.text, text: this.assignment.submission.text,
}, },
}, },
update( update(store, {data: {spellCheck: {results}}}) {
store,
{
data: {
spellCheck: { results },
},
}
) {
self.corrections = results; self.corrections = results;
}, },
}) }).then(() => {
.then(() => {
this.spellcheckLoading = false; this.spellcheckLoading = false;
}); });
},
}, },
},
apollo: { apollo: {
assignment: { assignment: {
query: ASSIGNMENT_QUERY, query: ASSIGNMENT_QUERY,
variables() { variables() {
return { return {
id: this.value.id, id: this.value.id,
}; };
},
result(response) {
const data = response.data;
this.assignment = cloneDeep(data.assignment);
this.assignment.submission = Object.assign(this.initialSubmission(), this.assignment.submission);
if (this.assignment.id === this.scrollToAssignmentId && 'stale' in response) {
this.$nextTick(() => this.scrollToAssignmentReady(true));
}
},
}, },
result(response) { me: {
const data = response.data; query: ME_QUERY,
this.assignment = cloneDeep(data.assignment);
this.assignment.submission = Object.assign(this.initialSubmission(), this.assignment.submission);
if (this.assignment.id === this.scrollToAssignmentId && 'stale' in response) {
this.$nextTick(() => this.scrollToAssignmentReady(true));
}
}, },
}, },
me: { };
query: ME_QUERY,
},
},
};
</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';
@import '@/styles/_mixins.scss'; @import '@/styles/_mixins.scss';
.assignment { .assignment {
margin-bottom: 3rem; margin-bottom: 3rem;
position: relative; position: relative;
&__title { &__title {
font-size: toRem(17px); font-size: toRem(17px);
margin-bottom: 1rem; margin-bottom: 1rem;
}
&__main-text {
:deep(ul) {
@include list-parent;
} }
:deep(li) { &__main-text {
@include list-child; /deep/ ul{
@include list-parent
}
/deep/ li {
@include list-child;
}
} }
}
&__toggle-input-container { &__toggle-input-container {
display: flex; display: flex;
margin-bottom: 15px; margin-bottom: 15px;
}
&__toggle-input {
border: 0;
font-family: $sans-serif-font-family;
background: transparent;
font-size: toRem(14px);
padding: 5px 0;
margin-right: 15px;
outline: 0;
color: $color-silver-dark;
cursor: pointer;
border-bottom: 2px solid transparent;
&--active {
border-bottom-color: $color-charcoal-dark;
color: $color-charcoal-dark;
} }
&__toggle-input {
border: 0;
font-family: $sans-serif-font-family;
background: transparent;
font-size: toRem(14px);
padding: 5px 0;
margin-right: 15px;
outline: 0;
color: $color-silver-dark;
cursor: pointer;
border-bottom: 2px solid transparent;
&--active {
border-bottom-color: $color-charcoal-dark;
color: $color-charcoal-dark;
}
}
&__feedback {
@include regular-text;
}
} }
&__feedback {
@include regular-text;
}
}
</style> </style>

View File

@ -1,99 +1,109 @@
<template> <template>
<div class="final-submission" data-cy="final-submission"> <div
<document-block :value="{ url: userInput.document }" class="final-submission__document" v-if="userInput.document" /> class="final-submission"
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 class="final-submission__reopen" data-cy="final-submission-reopen" v-if="showReopen" @click="$emit('reopen')" <a
>Bearbeiten</a class="final-submission__reopen"
> 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';
const DocumentBlock = () => import {defineAsyncComponent} from 'vue';
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/DocumentBlock'); const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'));
const InfoIcon = () => 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: {
type: Boolean,
default: true,
},
sharedMsg: {
type: String,
default: '',
},
},
components: { components: {
InfoIcon, InfoIcon,
DocumentBlock, DocumentBlock,
},
computed: {
text() {
return newLineToParagraph(this.userInput.text);
}, },
},
}; 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,7 +10,10 @@
/> />
</div> </div>
<div class="submission-form-container__actions" v-if="!isFinalOrReadOnly"> <div
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"
@ -26,7 +29,11 @@
> >
{{ spellcheckText }} {{ spellcheckText }}
</button> </button>
<file-upload :document="userInput.document" v-if="allowsDocuments" @change-document-url="changeDocumentUrl" /> <file-upload
:document="userInput.document"
v-if="allowsDocuments"
@change-document-url="changeDocumentUrl"
/>
<slot /> <slot />
</div> </div>
@ -41,112 +48,116 @@
</template> </template>
<script> <script>
const SubmissionInput = () => import('@/components/content-blocks/assignment/SubmissionInput.vue'); import {defineAsyncComponent} from 'vue';
const FinalSubmission = () => import('@/components/content-blocks/assignment/FinalSubmission.vue'); const SubmissionInput = defineAsyncComponent(() => import('@/components/content-blocks/assignment/SubmissionInput'));
const FileUpload = () => import('@/components/ui/file-upload/FileUpload.vue'); const FinalSubmission = defineAsyncComponent(() => import('@/components/content-blocks/assignment/FinalSubmission'));
const FileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/FileUpload'));
export default {
props: {
userInput: Object,
saved: Boolean,
placeholder: String,
action: String,
reopen: Function,
document: String,
readOnly: {
type: Boolean,
default: false,
},
spellcheck: {
type: Boolean,
default: false,
},
spellcheckLoading: {
type: Boolean,
default: false,
},
sharedMsg: String,
},
components: { export default {
FileUpload, props: {
SubmissionInput, userInput: Object,
FinalSubmission, saved: Boolean,
}, placeholder: String,
action: String,
reopen: Function,
document: String,
readOnly: {
type: Boolean,
default: false,
},
spellcheck: {
type: Boolean,
default: false,
},
spellcheckLoading: {
type: Boolean,
default: false,
},
sharedMsg: String,
},
computed: { components: {
final() { FileUpload,
return !!this.userInput && this.userInput.final; SubmissionInput,
FinalSubmission,
}, },
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: { computed: {
reopenSubmission() { final() {
this.$emit('reopen'); 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...';
}
},
}, },
saveInput(input) {
this.$emit('saveInput', input); methods: {
reopenSubmission() {
this.$emit('reopen');
},
saveInput(input) {
this.$emit('saveInput', input);
},
changeDocumentUrl(documentUrl) {
this.$emit('changeDocumentUrl', documentUrl);
},
}, },
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,68 +4,75 @@
: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 class="submission-form__save-status submission-form__save-status--saved" v-if="saved"> <div
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 class="submission-form__save-status submission-form__save-status--unsaved" v-if="!saved"> <div
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>
const TickCircleIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TickCircleIcon'); import {defineAsyncComponent} from 'vue';
const LoadingIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/LoadingIcon'); const TickCircleIcon = defineAsyncComponent(() => 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: {
components: { TickCircleIcon,
TickCircleIcon, LoadingIcon
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"
@ -20,43 +20,44 @@
</template> </template>
<script> <script>
const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon'); import {defineAsyncComponent} from 'vue';
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,119 +1,133 @@
<template> <template>
<div class="document-form" ref="documentform"> <div
<div v-if="!value.url" ref="uploadcare-panel" /> class="document-form"
<div class="document-form__spinner" v-if="loading"> ref="documentform"
>
<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 class="document-form__uploaded" v-if="value.url"> <div
class="document-form__uploaded"
v-if="value.url"
>
<document-icon class="document-form__icon" /> <document-icon class="document-form__icon" />
<a :href="previewUrl" class="document-form__link" target="_blank">{{ previewLink }}</a> <a
:href="previewUrl"
class="document-form__link"
target="_blank"
>{{ previewLink }}</a>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { uploadcare } from '@/helpers/uploadcare'; import {uploadcare} from '@/helpers/uploadcare';
import LoadingIcon from '@/components/icons/LoadingIcon'; import LoadingIcon from '@/components/icons/LoadingIcon';
import {defineAsyncComponent} from 'vue';
const DocumentIcon = () => 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() {
if (this.value && this.value.url) {
const parts = this.value.url.split('/');
return parts[parts.length - 1];
}
return '';
},
},
mounted() { data() {
uploadcare( return {
this, loading: false,
(url) => { };
},
computed: {
previewUrl() {
if (this.value && this.value.url) {
return this.value.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;
align-items: center;
}
&__link {
text-decoration: underline;
}
&__spinner {
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;
display: flex; display: flex;
width: 100%;
justify-content: center;
align-items: center; align-items: center;
font-family: $sans-serif-font-family; }
font-weight: $font-weight-regular;
&__link {
text-decoration: underline; text-decoration: underline;
} }
&__spinner {
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;
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,11 +1,21 @@
<template> <template>
<div> <div>
<div class="video-form" v-if="!isVimeo && !isYoutube && !isSrf"> <div
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 Sie können Videos auf <a
<a class="video-form__platform-link help-text__link" href="https://youtube.com/" target="_blank">Youtube</a> class="video-form__platform-link help-text__link"
oder <a class="video-form__platform-link help-text__link" href="https://vimeo.com/" target="_blank">Vimeo</a> href="https://youtube.com/"
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>
@ -14,7 +24,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">
@ -30,64 +40,67 @@
</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';
const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon'); import {defineAsyncComponent} from 'vue';
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: { computed: {
isYoutube() { isYoutube() {
return isYoutubeUrl(this.value.url); return isYoutubeUrl(this.value.url);
}, },
isVimeo() { isVimeo() {
return isVimeoUrl(this.value.url); return isVimeoUrl(this.value.url);
}, },
isSrf() { isSrf() {
return isSrfUrl(this.value.url); 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,164 +1,176 @@
<template> <template>
<a :class="typeClass" class="filter-entry" data-cy="filter-entry" :style="categoryStyle"> <a
:class="typeClass"
class="filter-entry"
data-cy="filter-entry"
:style="categoryStyle"
>
<span class="filter-entry__text">{{ text }}</span> <span class="filter-entry__text">{{ text }}</span>
<span :style="activeStyle" class="filter-entry__icon-wrapper"> <span
<chevron-right :style="{ fill: category.foreground }" class="filter-entry__icon" /> :style="activeStyle"
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';
const ChevronRight = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronRight'));
const ChevronRight = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronRight'); export default {
props: {
export default { text: {
props: { type: String,
text: { required: true,
type: String,
required: true,
},
id: {
type: String,
default: '',
},
isCategory: {
type: Boolean,
default: false,
},
category: {
type: Object,
default: () => ({}),
},
},
components: {
ChevronRight,
},
apollo: {
instrumentFilter: {
query: INSTRUMENT_FILTER_QUERY,
},
},
data() {
return {
instrumentFilter: {
currentFilter: '',
}, },
}; id: {
}, type: String,
default: '',
},
isCategory: {
type: Boolean,
default: false,
},
category: {
type: Object,
default: () => ({}),
},
},
computed: { components: {
isActive() { ChevronRight,
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() { apollo: {
if (this.isActive) { instrumentFilter: {
return { query: INSTRUMENT_FILTER_QUERY,
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() { data() {
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 {
'filter-entry--active': this.isActive, instrumentFilter: {
'filter-entry--category': this.isCategory, 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 {
'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;
justify-content: center; cursor: pointer;
height: 20px;
width: 20px;
border-radius: 10px;
}
&__icon { &__text {
width: 10px; @include sub-heading;
height: 10px; line-height: 1.5;
} color: inherit;
$root: &;
@mixin filter-block($color) {
&#{$root}--category {
color: $color;
} }
&#{$root}--active { &__icon-wrapper {
#{$root}__icon-wrapper { display: flex;
background-color: $color; 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;
} }
} }
#{$root}__icon { &--language-communication {
fill: $color; @include filter-block($color-accent-1-dark);
}
}
&--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 { &--society {
background-color: black; @include filter-block($color-accent-2-dark);
} }
#{$root}__icon { &--interdisciplinary {
fill: white; @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

@ -1,11 +1,12 @@
const Modal = () => import(/* webpackChunkName: "modals" */ '@/components/Modal'); import {defineAsyncComponent} from 'vue';
const FullscreenImage = () => import(/* webpackChunkName: "modals" */ '@/components/FullscreenImage'); const Modal = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/Modal'));
const FullscreenInfographic = () => import(/* webpackChunkName: "modals" */ '@/components/FullscreenInfographic'); const FullscreenImage = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/FullscreenImage'));
const FullscreenVideo = () => import(/* webpackChunkName: "modals" */ '@/components/FullscreenVideo'); const FullscreenInfographic = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/FullscreenInfographic'));
const DeactivatePerson = () => import(/* webpackChunkName: "modals" */ '@/components/profile/DeactivatePerson'); const FullscreenVideo = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/FullscreenVideo'));
const SnapshotCreated = () => import(/* webpackChunkName: "modals" */ '@/components/modules/SnapshotCreated'); const DeactivatePerson = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/profile/DeactivatePerson'));
const ChangeVisibility = () => import(/* webpackChunkName: "modals" */ '@/components/rooms/ChangeVisibility'); const SnapshotCreated = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/modules/SnapshotCreated'));
const Confirm = () => import(/* webpackChunkName: "modals" */ '@/components/modals/Confirm'); const ChangeVisibility = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/rooms/ChangeVisibility'));
const Confirm = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/modals/Confirm'));
export default { export default {
Modal, Modal,

View File

@ -1,7 +1,10 @@
<template> <template>
<div class="bookmark-actions" v-if="!editMode"> <div
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')"
@ -29,73 +32,74 @@
</template> </template>
<script> <script>
const BookmarkIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/BookmarkIcon'); import {defineAsyncComponent} from 'vue';
const AddNoteIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddNoteIcon'); const BookmarkIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/BookmarkIcon'));
const NoteIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/NoteIcon'); const AddNoteIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddNoteIcon'));
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
}
}, },
note: { components: {
type: [Object, Boolean], BookmarkIcon,
default: false, AddNoteIcon,
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, &--bookmarked, &--noted {
&--noted { opacity: 1;
opacity: 1; }
}
$parent: &;
&:hover {
#{$parent}__action {
opacity: 1;
}
} }
} }
$parent: &;
&:hover {
#{$parent}__action {
opacity: 1;
}
}
}
</style> </style>

View File

@ -6,36 +6,40 @@
placeholder="Lernziel erfassen..." placeholder="Lernziel erfassen..."
@input="$emit('input', $event)" @input="$emit('input', $event)"
/> />
<a class="icon-button" @click="$emit('delete')"> <a
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';
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'); import {defineAsyncComponent} from 'vue';
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,45 +1,49 @@
<template> <template>
<a class="add-project-entry" @click="addProjectEntry"> <a
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>
</template> </template>
<script> <script>
const PlusIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/PlusIcon'); import {defineAsyncComponent} from 'vue';
export default { const PlusIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PlusIcon'));
props: ['project'], export default {
components: { PlusIcon }, props: ['project'],
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,9 +1,25 @@
<template> <template>
<div class="portfolio-onboarding"> <div class="portfolio-onboarding">
<h1 class="portfolio-onboarding__heading" data-cy="page-title">Portfolio</h1> <h1
<portfolio-illustration data-cy="portfolio-onboarding-illustration" class="portfolio-onboarding__illustration" /> class="portfolio-onboarding__heading"
<h2 class="portfolio-onboarding__subheading" data-cy="portfolio-onboarding-subtitle">Woran denken Sie gerade?</h2> data-cy="page-title"
<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>
@ -12,33 +28,34 @@
</template> </template>
<script> <script>
const PortfolioIllustration = () => import('@/components/illustrations/PortfolioIllustration.vue'); import {defineAsyncComponent} from 'vue';
const CreateProjectButton = () => import('@/components/portfolio/CreateProjectButton.vue'); const PortfolioIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/PortfolioIllustration'));
export default { const CreateProjectButton = defineAsyncComponent(() => import('@/components/portfolio/CreateProjectButton'));
components: { CreateProjectButton, PortfolioIllustration }, export default {
}; components: {CreateProjectButton, PortfolioIllustration},
};
</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,20 +1,48 @@
<template> <template>
<div class="project-actions" data-cy="project-actions"> <div
<a class="project-actions__more-link" @click.stop="toggleMenu"> class="project-actions"
data-cy="project-actions"
>
<a
class="project-actions__more-link"
@click.stop="toggleMenu"
>
<ellipses /> <ellipses />
</a> </a>
<widget-popover class="project-actions__popover" v-if="showMenu" @hide-me="showMenu = false"> <widget-popover
class="project-actions__popover"
v-if="showMenu"
@hide-me="showMenu = false"
>
<li class="popover-links__link"> <li class="popover-links__link">
<a data-cy="delete-project" @click="deleteProject(slug)">Projekt löschen</a> <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 data-cy="edit-project" @click="editProject(slug)">Projekt bearbeiten</a> <a
data-cy="edit-project"
@click="editProject(slug)"
>Projekt bearbeiten</a>
</li> </li>
<li class="popover-links__link" v-if="!final && shareButtons"> <li
<a data-cy="share-project" @click="updateProjectShareState(slug, true)">Projekt teilen</a> class="popover-links__link"
v-if="!final && shareButtons"
>
<a
data-cy="share-project"
@click="updateProjectShareState(slug, true)"
>Projekt teilen</a>
</li> </li>
<li class="popover-links__link" v-if="final && shareButtons"> <li
<a data-cy="unshare-project" @click="updateProjectShareState(slug, false)">Projekt nicht mehr teilen</a> class="popover-links__link"
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>
@ -27,8 +55,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';
const Ellipses = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Ellipses.vue'); import {defineAsyncComponent} from 'vue';
const Ellipses = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Ellipses.vue'));
export default { export default {
props: { props: {
@ -64,50 +93,42 @@ 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 this.$apollo.mutate({
.mutate({ mutation: DELETE_PROJECT_MUTATION,
mutation: DELETE_PROJECT_MUTATION, variables: {
variables: { input: {
input: { slug,
slug,
},
}, },
update( },
store, update(store, {data: {deleteProject: {success}}}) {
{ if (success) {
data: { const {projects: prevProjects} = store.readQuery({query: PROJECTS_QUERY});
deleteProject: { success },
},
}
) {
if (success) {
const { projects: prevProjects } = store.readQuery({ query: PROJECTS_QUERY });
if (prevProjects) {
let index = prevProjects.findIndex((project) => project.slug === slug);
const projects = removeAtIndex(prevProjects, index);
const data = { if (prevProjects) {
projects, let index = prevProjects.findIndex(project => project.slug === slug);
}; const projects = removeAtIndex(prevProjects, index);
store.writeQuery({ query: PROJECTS_QUERY, data });
} const data = {
projects
};
store.writeQuery({query: PROJECTS_QUERY, data});
} }
}, }
}) },
.then(() => { }).then(() => {
this.$router.push('/portfolio'); 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,22 +1,44 @@
<template> <template>
<div class="project-entry" data-cy="project-entry"> <div
<more-options-widget class="project-entry__more" data-cy="project-entry-more" v-if="!readOnly"> class="project-entry"
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 data-cy="edit-project-entry" @click="editProjectEntry()">Eintrag bearbeiten</a> <a
data-cy="edit-project-entry"
@click="editProjectEntry()"
>Eintrag bearbeiten</a>
</li> </li>
<li class="popover-links__link"> <li class="popover-links__link">
<a data-cy="delete-project-entry" @click="deleteProjectEntry()">Eintrag löschen</a> <a
data-cy="delete-project-entry"
@click="deleteProjectEntry()"
>Eintrag löschen</a>
</li> </li>
</more-options-widget> </more-options-widget>
<h3 class="project-entry__heading" data-cy="project-entry-date"> <h3
class="project-entry__heading"
data-cy="project-entry-date"
>
{{ createdDateTime }} {{ createdDateTime }}
</h3> </h3>
<p class="project-entry__paragraph" data-cy="project-entry-activity"> <p
class="project-entry__paragraph"
data-cy="project-entry-activity"
>
{{ description }} {{ description }}
</p> </p>
<p class="project-entry__paragraph" v-if="documentUrl"> <p
<document-block :value="{ url: documentUrl }" /> class="project-entry__paragraph"
v-if="documentUrl"
>
<document-block :value="{url: documentUrl}" />
</p> </p>
<div class="project-entry__date"> <div class="project-entry__date">
{{ createdDate }} {{ createdDate }}
@ -25,116 +47,110 @@
</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';
import { dateFilter, dateTimeFilter } from '@/filters/date-filter'; import {dateFilter, dateTimeFilter} from '@/filters/date-filter';
import { removeAtIndex } from '@/graphql/immutable-operations.ts'; import {removeAtIndex} from '@/graphql/immutable-operations.ts';
import {defineAsyncComponent} from 'vue';
const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'));
const DocumentBlock = () =>
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: {
createdDate() {
return dateFilter(this.created);
}, },
createdDateTime() {
return dateTimeFilter(this.created);
},
},
methods: { computed: {
editProjectEntry() { createdDate() {
this.$store.dispatch('editProjectEntry', this.id); return dateFilter(this.created);
},
createdDateTime() {
return dateTimeFilter(this.created);
},
}, },
deleteProjectEntry() {
const projectEntry = this; // otherwise we run into scope errors methods: {
this.$apollo.mutate({ editProjectEntry() {
mutation: DELETE_PROJECT_ENTRY_MUTATION, this.$store.dispatch('editProjectEntry', this.id);
variables: { },
input: { deleteProjectEntry() {
id: this.id, const projectEntry = this; // otherwise we run into scope errors
}, this.$apollo.mutate({
}, mutation: DELETE_PROJECT_ENTRY_MUTATION,
update( variables: {
store, input: {
{ id: this.id,
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,
},
}; };
store.writeQuery({ query, variables, data }); 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,
},
};
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 { &__heading {
font-size: toRem(22px); font-size: toRem(22px);
margin-bottom: 6px; 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;
} }
&__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,7 +1,12 @@
<template> <template>
<modal :hide-header="false"> <modal :hide-header="false">
<template #header> <template #header>
<h2 class="project-entry-modal__heading" data-cy="modal-title">Beitrag erfassen</h2> <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">
@ -30,26 +35,6 @@
</div> </div>
</div> </div>
</div> </div>
<<<<<<< HEAD
<div slot="footer">
<a class="button button--primary" data-cy="modal-save-button" @click="$emit('save', localProjectEntry)"
>Speichern</a
>
<a class="button" @click="$emit('hide')">Abbrechen</a>
</div>
||||||| parent of a423cfde (Apply code changes from migration guide for Vue 3)
<div slot="footer">
<a
class="button button--primary"
data-cy="modal-save-button"
@click="$emit('save', localProjectEntry)"
>Speichern</a>
<a
class="button"
@click="$emit('hide')"
>Abbrechen</a>
</div>
=======
<template #footer> <template #footer>
<a <a
class="button button--primary" class="button button--primary"
@ -61,105 +46,96 @@
@click="$emit('hide')" @click="$emit('hide')"
>Abbrechen</a> >Abbrechen</a>
</template> </template>
>>>>>>> a423cfde (Apply code changes from migration guide for Vue 3)
</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';
<<<<<<< HEAD
import { PROJECT_ENTRY_TEMPLATE } from '@/consts/strings.consts';
const FileUpload = () => import('@/components/ui/file-upload/FileUpload.vue');
||||||| parent of a423cfde (Apply code changes from migration guide for Vue 3)
import {PROJECT_ENTRY_TEMPLATE} from '@/consts/strings.consts';
const FileUpload = () => import('@/components/ui/file-upload/FileUpload');
=======
import {PROJECT_ENTRY_TEMPLATE} from '@/consts/strings.consts'; import {PROJECT_ENTRY_TEMPLATE} from '@/consts/strings.consts';
const FileUpload = () => import('@/components/ui/file-upload/FileUpload'); import {defineAsyncComponent} from 'vue';
>>>>>>> a423cfde (Apply code changes from migration guide for Vue 3) 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;
grid-template-columns: 1fr 1fr;
width: 100%;
}
&__textarea { &__form-field {
@include auto-grow; @include inputstyle;
border: 0; padding: 0;
min-height: 400px; display: flex;
padding: $medium-spacing; flex-direction: column;
} grid-template-rows: auto 1rem;
grid-template-columns: 1fr 1fr;
&__buttons { width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
padding: $medium-spacing;
}
&__button {
@include regular-text;
&--template {
} }
&--document { &__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;
}
} }
&__heading {
@include heading-3;
margin-bottom: 0;
}
}
</style> </style>

View File

@ -9,35 +9,36 @@
</template> </template>
<script> <script>
const ShareIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ShareIcon'); import {defineAsyncComponent} from 'vue';
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,
},
}, },
}, components: {ShareIcon},
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

@ -7,60 +7,64 @@
<slot /> <slot />
</div> </div>
<div class="activity-entry__link" @click="$emit('link')"> <div
class="activity-entry__link"
@click="$emit('link')"
>
<chevron-right class="activity-entry__icon" /> <chevron-right class="activity-entry__icon" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const ChevronRight = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronRight'); import {defineAsyncComponent} from 'vue';
const ChevronRight = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronRight'));
export default { export default {
props: ['title'], props: ['title'],
components: { components: {
ChevronRight, ChevronRight
}, }
}; };
</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";
.activity-entry { .activity-entry {
padding: $small-spacing 0; padding: $small-spacing 0;
border-bottom: 1px solid $color-silver; border-bottom: 1px solid $color-silver;
display: flex;
justify-content: space-between;
&__title {
@include small-text;
// todo: make style definition for small text and silver color
color: $color-silver-dark;
margin-bottom: 0;
}
&__content {
flex-grow: 1;
@include regular-text;
line-height: $default-line-height;
}
&__link {
display: flex; display: flex;
flex-grow: 0; justify-content: space-between;
align-content: center;
cursor: pointer;
}
&__icon { &__title {
fill: $color-brand; @include small-text;
width: 30px; // todo: make style definition for small text and silver color
} color: $color-silver-dark;
margin-bottom: 0;
}
:deep(p) { &__content {
@include regular-text; flex-grow: 1;
@include regular-text;
line-height: $default-line-height;
}
&__link {
display: flex;
flex-grow: 0;
align-content: center;
cursor: pointer;
}
&__icon {
fill: $color-brand;
width: 30px;
}
/deep/ p {
@include regular-text;
}
} }
}
</style> </style>

View File

@ -35,9 +35,10 @@
<script> <script>
import TOGGLE_SIDEBAR from '@/graphql/gql/local/mutations/toggleSidebar.gql'; import TOGGLE_SIDEBAR from '@/graphql/gql/local/mutations/toggleSidebar.gql';
import {defineAsyncComponent} from 'vue';
const DefaultAvatar = () => import(/* webpackChunkName: "icons" */'@/components/icons/DefaultAvatar'); const DefaultAvatar = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DefaultAvatar'));
const PenIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/PenIcon'); const PenIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PenIcon'));
export default { export default {
props: { props: {

View File

@ -1,9 +1,15 @@
<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 v-if="content.type === 'text_block'" v-html="text" /> <div
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 :value="content.value" :no-margin="true" /> <link-block
:value="content.value"
:no-margin="true"
/>
</div> </div>
<p v-else> <p v-else>
{{ type }} {{ type }}
@ -12,34 +18,35 @@
</template> </template>
<script> <script>
const LinkBlock = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/LinkBlock'); import {defineAsyncComponent} from 'vue';
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,27 +1,32 @@
<template> <template>
<a class="edit-group-name" data-cy="edit-group-name-link" @click="$emit('edit')"> <a
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>
<script> <script>
const PenIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/PenIcon'); import {defineAsyncComponent} from 'vue';
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

@ -1,101 +1,105 @@
<template> <template>
<div class="profile"> <div class="profile">
<h1 class="profile__header">Profilbild</h1> <h1 class="profile__header">
<div class="profile-avatar" v-if="me.avatarUrl"> Profilbild
</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 class="profile-avatar__remove icon-button" @click="deleteAvatar()"> <a
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 v-else @avatarUpdate="updateAvatar" /> <avatar-upload-form
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';
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'); import {defineAsyncComponent} from 'vue';
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: {
avatarUrl: '', query: ME_QUERY,
}, },
};
},
apollo: {
me: {
query: ME_QUERY,
}, },
}, methods: {
methods: { deleteAvatar () {
deleteAvatar() { this.updateAvatar('');
this.updateAvatar(''); },
}, updateAvatar (url) {
updateAvatar(url) { this.$apollo.mutate({
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

@ -84,7 +84,8 @@
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';
const Cross = () => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon'); import {defineAsyncComponent} from 'vue';
const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon'));
export default { export default {

View File

@ -1,60 +1,65 @@
<template> <template>
<router-link class="add-room-entry-button" data-cy="add-room-entry-button" :to="addRoomEntryRoute"> <router-link
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>
</template> </template>
<script> <script>
import { ADD_ROOM_ENTRY_PAGE } from '@/router/room.names'; import {defineAsyncComponent} from 'vue';
const PlusIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/PlusIcon'); const PlusIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PlusIcon'));
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,56 @@
<template> <template>
<div class="entry-count-widget"> <div class="entry-count-widget">
<component :is="icon" /> <component :is="icon" />
<span data-cy="entry-count" <span data-cy="entry-count">{{ entryCount }} <template v-if="verbose">{{ entryCount === 1 ? 'Beitrag' : 'Beiträge' }}</template></span>
>{{ entryCount }} <template v-if="verbose">{{ entryCount === 1 ? 'Beitrag' : 'Beiträge' }}</template></span
>
</div> </div>
</template> </template>
<script> <script>
import SpeechBubbleIcon from '@/components/icons/SpeechBubbleIcon'; import {defineAsyncComponent} from 'vue';
const Cards = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Cards.vue'); import SpeechBubbleIcon from '@/components/icons/SpeechBubbleIcon';
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,66 +1,71 @@
<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 class="more-actions__popover" v-if="showMenu" @hide-me="showMenu = false"> <widget-popover
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';
const Ellipses = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Ellipses'); import {defineAsyncComponent} from 'vue';
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() {
return {
showMenu: false,
};
},
methods: {
toggleMenu: function () {
this.showMenu = !this.showMenu;
}, },
}, data() {
}; return {
showMenu: false,
};
},
methods: {
toggleMenu: function () {
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

@ -8,33 +8,34 @@
</template> </template>
<script> <script>
const Group = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Group.vue'); import {defineAsyncComponent} from '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

@ -12,41 +12,43 @@
</template> </template>
<script> <script>
const EyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon');
const ClosedEyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ClosedEyeIcon');
export default { import {defineAsyncComponent} from 'vue';
props: { const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon'));
restricted: { const ClosedEyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ClosedEyeIcon'));
type: Boolean,
default: false, export default {
props: {
restricted: {
type: Boolean,
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;
flex-shrink: 0; }
& > span {
@include room-widget-text-style;
}
} }
& > span {
@include room-widget-text-style;
}
}
</style> </style>

View File

@ -1,12 +1,25 @@
<template> <template>
<div class="rooms-onboarding"> <div class="rooms-onboarding">
<h1 class="rooms-onboarding__heading" data-cy="page-title">Räume</h1> <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 data-cy="rooms-onboarding-text" class="rooms-onboarding__text"> <p
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 :to="newRoomRoute" class="button button--primary" data-cy="create-room-button" v-if="isTeacher"> <router-link
:to="newRoomRoute"
class="button button--primary"
data-cy="create-room-button"
v-if="isTeacher"
>
Raum erstellen Raum erstellen
</router-link> </router-link>
</div> </div>
@ -14,43 +27,43 @@
</template> </template>
<script> <script>
import { NEW_ROOM_PAGE } from '@/router/room.names'; import {NEW_ROOM_PAGE} from '@/router/room.names';
const RoomsIllustration = () => import {defineAsyncComponent} from 'vue';
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

@ -1,5 +1,8 @@
<template> <template>
<div class="class-selection" v-if="currentClassSelection"> <div
class="class-selection"
v-if="currentClassSelection"
>
<div <div
data-cy="class-selection" data-cy="class-selection"
class="class-selection__selected-class selected-class" class="class-selection__selected-class selected-class"
@ -8,7 +11,12 @@
<current-class class="selected-class__text" /> <current-class class="selected-class__text" />
<chevron-down class="selected-class__dropdown-icon" /> <chevron-down class="selected-class__dropdown-icon" />
</div> </div>
<widget-popover :mobile="mobile" class="class-selection__popover" v-if="showPopover" @hide-me="showPopover = false"> <widget-popover
: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"
@ -26,105 +34,121 @@
v-if="me.isTeacher && !me.readOnly" v-if="me.isTeacher && !me.readOnly"
@click="closeSidebar" @click="closeSidebar"
> >
<router-link :to="{ name: 'create-class' }" tag="span" class="popover-links__link-with-icon"> <router-link
:to="{name: 'create-class'}"
tag="span"
class="popover-links__link-with-icon"
>
<add-icon class="popover-links__icon" /> <add-icon class="popover-links__icon" />
<span>Klasse erfassen</span> <span>Klasse erfassen</span>
</router-link> </router-link>
</li> </li>
<li class="popover-links__link popover-links__link--large popover-links__divider" @click="closeSidebar"> <li
<router-link :to="{ name: 'old-classes' }" tag="span"> Alte Klassen anzeigen </router-link> class="popover-links__link popover-links__link--large popover-links__divider"
@click="closeSidebar"
>
<router-link
:to="{name: 'old-classes'}"
tag="span"
>
Alte Klassen anzeigen
</router-link>
</li> </li>
</widget-popover> </widget-popover>
</div> </div>
</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';
import meMixin from '@/mixins/me'; import meMixin from '@/mixins/me';
const ChevronDown = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronDown'); import {defineAsyncComponent} from 'vue';
const AddIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddIcon'); const ChevronDown = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown'));
const AddIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon'));
export default { export default {
props: {
mobile: { props: {
type: Boolean, mobile: {
default: false, type: Boolean,
default: false
}
}, },
},
mixins: [updateSelectedClassMixin, sidebarMixin, meMixin], mixins: [updateSelectedClassMixin, sidebarMixin, meMixin],
components: { components: {
WidgetPopover, WidgetPopover,
ChevronDown, ChevronDown,
CurrentClass, CurrentClass,
AddIcon, AddIcon
},
data() {
return {
showPopover: false,
};
},
computed: {
currentClassSelection() {
let currentClass = this.me.schoolClasses.find((schoolClass) => {
return schoolClass.id === this.me.selectedClass.id;
});
return currentClass || this.me.schoolClasses[0];
}, },
},
methods: { data() {
updateSelectedClassAndHidePopover(selectedClass) { return {
this.updateSelectedClass(selectedClass); showPopover: false
this.showPopover = false; };
this.closeSidebar('profile');
}, },
},
}; computed: {
currentClassSelection() {
let currentClass = this.me.schoolClasses.find(schoolClass => {
return schoolClass.id === this.me.selectedClass.id;
});
return currentClass || this.me.schoolClasses[0];
}
},
methods: {
updateSelectedClassAndHidePopover(selectedClass) {
this.updateSelectedClass(selectedClass);
this.showPopover = false;
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;
} }
&__dropdown-icon { .selected-class {
width: 20px; width: 100%;
height: 20px; box-sizing: border-box;
fill: $color-charcoal-dark; 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;
}
&__dropdown-icon {
width: 20px;
height: 20px;
fill: $color-charcoal-dark;
}
} }
}
</style> </style>

View File

@ -6,39 +6,47 @@
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="{ :class="{'base-input-container__checkbox': type==='checkbox', 'base-input-container__radiobutton': type === 'radiobutton'}"
'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 data-cy="circle-icon" v-if="type === 'radiobutton'" /> <circle-icon
data-cy="circle-icon"
v-if="type === 'radiobutton'"
/>
</span> </span>
<span class="base-input-container__label" data-cy="base-input-label" v-if="label">{{ label }}</span> <span
<slot class="base-input-container__label" v-if="!label" /> class="base-input-container__label"
data-cy="base-input-label"
v-if="label"
>{{ label }}</span>
<slot
class="base-input-container__label"
v-if="!label"
/>
</label> </label>
</template> </template>
<script> <script>
const Tick = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Tick'); import {defineAsyncComponent} from 'vue';
const CircleIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CircleIcon'); const Tick = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Tick'));
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,56 +1,65 @@
<template> <template>
<li class="popover-links__link"> <li
<a class="popover-link" @click="$emit('link-action')"> class="popover-links__link"
<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>
const EyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon'); import {defineAsyncComponent} from 'vue';
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'); const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon'));
const PenIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/PenIcon'); const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
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: '',
},
}, },
text: { components: {
type: String, EyeIcon,
default: '', TrashIcon,
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,7 +1,11 @@
<template> <template>
<div class="file-upload"> <div class="file-upload">
<template v-if="document"> <template v-if="document">
<document-block :value="{ url: document }" show-trash-icon @trash="$emit('change-document-url', '')" /> <document-block
: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
@ -14,24 +18,26 @@
</template> </template>
<script> <script>
const SimpleFileUpload = () => import('@/components/ui/file-upload/SimpleFileUpload.vue'); import {defineAsyncComponent} from 'vue';
const DocumentBlock = () => import('@/components/content-blocks/DocumentBlock.vue'); const SimpleFileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUpload'));
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
}
}, },
withText: { components: {SimpleFileUpload, DocumentBlock},
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

@ -1,80 +1,72 @@
<template> <template>
<div class="simple-file-upload"> <div class="simple-file-upload">
<<<<<<< HEAD
<component :is="button" @click.native="clickUploadCare" />
||||||| parent of a423cfde (Apply code changes from migration guide for Vue 3)
<component
:is="button"
@click.native="clickUploadCare"
/>
=======
<component <component
:is="button" :is="button"
@click="clickUploadCare" @click="clickUploadCare"
/> />
>>>>>>> a423cfde (Apply code changes from migration guide for Vue 3)
<simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" /> <simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" />
</div> </div>
</template> </template>
<script> <script>
const SimpleFileUploadHiddenInput = () => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput.vue'); import {defineAsyncComponent} from 'vue';
const SimpleFileUploadIcon = () => import('@/components/ui/file-upload/SimpleFileUploadIcon.vue'); const SimpleFileUploadHiddenInput = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput'));
const SimpleFileUploadIconAndText = () => import('@/components/ui/file-upload/SimpleFileUploadIconAndText.vue'); const SimpleFileUploadIcon = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadIcon'));
const DocumentIcon = () => import('@/components/icons/DocumentIcon.vue'); const SimpleFileUploadIconAndText = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadIconAndText'));
const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
export default { export default {
props: { props: {
value: { value: {
type: String, type: String,
default: '', default: ''
},
withText: {
type: Boolean,
default: false
}
}, },
withText: {
type: Boolean,
default: false,
},
},
components: { components: {
SimpleFileUploadHiddenInput, SimpleFileUploadHiddenInput,
DocumentIcon, DocumentIcon,
SimpleFileUploadIcon, SimpleFileUploadIcon,
SimpleFileUploadIconAndText, SimpleFileUploadIconAndText
},
computed: {
button() {
return this.withText ? 'simple-file-upload-icon-and-text' : 'simple-file-upload-icon';
}, },
},
methods: { computed: {
clickUploadCare() { button() {
// workaround for styling the uploadcare widget return this.withText ? 'simple-file-upload-icon-and-text' : 'simple-file-upload-icon';
let button = this.$el.querySelector('.uploadcare--widget__button'); }
button.click();
}, },
},
}; methods: {
clickUploadCare() {
// workaround for styling the uploadcare widget
let button = this.$el.querySelector('.uploadcare--widget__button');
button.click();
}
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/_helpers'; @import "~styles/_helpers";
.simple-file-upload { .simple-file-upload {
height: 25px;
overflow: hidden;
cursor: pointer;
&__link {
display: inline-block;
overflow: hidden;
width: 25px;
height: 25px; height: 25px;
} overflow: hidden;
} cursor: pointer;
:deep(.uploadcare--widget) { &__link {
display: none; display: inline-block;
} overflow: hidden;
width: 25px;
height: 25px;
}
}
/deep/ .uploadcare--widget {
display: none;
}
</style> </style>

View File

@ -5,20 +5,21 @@
</template> </template>
<script> <script>
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon'); import {defineAsyncComponent} from 'vue';
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

@ -12,49 +12,50 @@
</template> </template>
<script> <script>
const SimpleFileUploadHiddenInput = () => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput.vue'); import {defineAsyncComponent} from 'vue';
const ButtonWithIconAndText = () => import('@/components/ui/ButtonWithIconAndText.vue'); const SimpleFileUploadHiddenInput = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput'));
const ButtonWithIconAndText = defineAsyncComponent(() => import('@/components/ui/ButtonWithIconAndText'));
export default { export default {
props: ['value'], props: ['value'],
components: { components: {
ButtonWithIconAndText, ButtonWithIconAndText,
SimpleFileUploadHiddenInput, SimpleFileUploadHiddenInput,
},
methods: {
clickUploadCare() {
// workaround for styling the uploadcare widget
let button = this.$el.querySelector('.uploadcare--widget__button');
button.click();
}, },
},
}; methods: {
clickUploadCare() {
// workaround for styling the uploadcare widget
let button = this.$el.querySelector('.uploadcare--widget__button');
button.click();
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/_helpers'; @import "~styles/_helpers";
.simple-file-upload { .simple-file-upload {
width: 25px;
height: 25px;
overflow: hidden;
&__icon {
width: 25px;
fill: $color-silver-dark;
}
&__link {
display: inline-block;
overflow: hidden;
width: 25px; width: 25px;
height: 25px; height: 25px;
} overflow: hidden;
}
:deep(.uploadcare--widget) { &__icon {
display: none; width: 25px;
} fill: $color-silver-dark;
}
&__link {
display: inline-block;
overflow: hidden;
width: 25px;
height: 25px;
}
}
/deep/ .uploadcare--widget {
display: none;
}
</style> </style>

View File

@ -1,18 +1,17 @@
const LinkIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/LinkIcon'); import {defineAsyncComponent} from 'vue';
const VideoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/VideoIcon'); const LinkIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/LinkIcon'));
const ImageIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ImageIcon'); const VideoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/VideoIcon'));
const TextIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TextIcon'); const ImageIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ImageIcon'));
const SpeechBubbleIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/SpeechBubbleIcon'); const TextIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TextIcon'));
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon'); const SpeechBubbleIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/SpeechBubbleIcon'));
const TitleIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TitleIcon'); const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
const DocumentWithLinesIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentWithLinesIcon'); const TitleIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TitleIcon'));
const DocumentWithLinesIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentWithLinesIcon'));
const ArrowThinBottom = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ArrowThinBottom'); const ArrowThinBottom = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinBottom'));
const ArrowThinDown = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ArrowThinDown'); const ArrowThinDown = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinDown'));
const ArrowThinTop = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ArrowThinTop'); const ArrowThinTop = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinTop'));
const ArrowThinUp = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ArrowThinUp'); const ArrowThinUp = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinUp'));
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon');
/* /*
for icons with a single word, leave the *-icon name, to prevent conflicts for icons with a single word, leave the *-icon name, to prevent conflicts
@ -31,5 +30,5 @@ export default {
ArrowThinDown, ArrowThinDown,
ArrowThinTop, ArrowThinTop,
ArrowThinUp, ArrowThinUp,
TrashIcon, TrashIcon
}; };

View File

@ -1,84 +1,93 @@
<template> <template>
<div class="visibility-action"> <div class="visibility-action">
<a class="visibility-action__action-button" v-if="canManageContent" @click="toggleVisibility()"> <a
<closed-eye-icon class="visibility-action__action-icon action-icon" v-if="hidden" /> class="visibility-action__action-button"
<eye-icon class="visibility-action__action-icon action-icon" v-else /> v-if="canManageContent"
@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';
const EyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon'); import {defineAsyncComponent} from 'vue';
const ClosedEyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ClosedEyeIcon'); const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon'));
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, mixins: [me],
default: CONTENT_TYPE,
validator: (value) => { components: {
// value must be one of TYPES EyeIcon,
return TYPES.indexOf(value) !== -1; ClosedEyeIcon
},
computed: {
hidden() {
return hidden({type: this.type, block: this.block, schoolClass: this.schoolClass});
}
},
methods: {
toggleVisibility() {
const hidden = !this.hidden;
const schoolClassId = this.schoolClass.id;
const visibility = [{
schoolClassId,
hidden
}];
const {mutation, variables} = createVisibilityMutation(this.type, this.block.id, visibility);
this.$apollo.mutate({
mutation,
variables
});
}, },
}, },
}, };
mixins: [me],
components: {
EyeIcon,
ClosedEyeIcon,
},
computed: {
hidden() {
return hidden({ type: this.type, block: this.block, schoolClass: this.schoolClass });
},
},
methods: {
toggleVisibility() {
const hidden = !this.hidden;
const schoolClassId = this.schoolClass.id;
const visibility = [
{
schoolClassId,
hidden,
},
];
const { mutation, variables } = createVisibilityMutation(this.type, this.block.id, visibility);
this.$apollo.mutate({
mutation,
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,6 +1,12 @@
<template> <template>
<div :class="specialContainerClass" class="container layout layout--fullscreen"> <div
<div class="close-button" @click="back"> :class="specialContainerClass"
class="container layout layout--fullscreen"
>
<div
class="close-button"
@click="back"
>
<cross class="close-button__icon" /> <cross class="close-button__icon" />
</div> </div>
@ -9,37 +15,39 @@
</template> </template>
<script> <script>
const Cross = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'); import {defineAsyncComponent} from 'vue';
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: {
methods: { back() {
back() { this.$router.go(-1);
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,98 +1,107 @@
<template> <template>
<div :class="{ 'layout--full-width': $route.meta.fullWidth }" class="skillbox layout layout--simple"> <div
<div class="close-button" @click="back"> :class="{'layout--full-width': $route.meta.fullWidth}"
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 class="layout__footer" v-if="enableFooter" /> <simple-footer
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';
const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon'));
const Cross = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'); export default {
components: {
Cross,
SimpleFooter
},
export default { computed: {
components: { enableFooter() {
Cross, if (this.$route.meta.hideFooter) {
SimpleFooter, return false;
}, }
return this.$flavor.showFooter;
computed: {
enableFooter() {
if (this.$route.meta.hideFooter) {
return false;
} }
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;
}
}
} }
width: 100%; $parent: &;
@include desktop { &--full-width {
grid-template-columns: 1fr 640px 1fr; #{$parent}__content {
grid-template-rows: 60px auto-fill 105px; grid-column: 1 / span 3;
-ms-grid-columns: 1fr 640px 1fr; grid-row: 1 / span 2;
}
}
& > :nth-child(2) { &__footer {
grid-column: 2; @include desktop {
-ms-grid-column: 2; grid-column: 1 / span 3;
} }
} }
} }
$parent: &; .close-button {
justify-self: end;
cursor: pointer;
&--full-width { display:flex;
#{$parent}__content { justify-content:flex-end;
grid-column: 1 / span 3;
grid-row: 1 / span 2; margin-right: $small-spacing;
} margin-top: $small-spacing;
}
&__footer {
@include desktop { @include desktop {
grid-column: 1 / span 3; grid-column: 3;
grid-row: 1;
-ms-grid-column: 3;
-ms-grid-row: 1;
margin-right: $medium-spacing;
margin-top: $medium-spacing;
} }
} }
}
.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>
@ -10,155 +10,154 @@
</template> </template>
<script> <script>
import flavorValues from '@/helpers/app-flavor'; import {defineAsyncComponent} from 'vue';
const ContentsIllustration = () => import flavorValues from '@/helpers/app-flavor';
import(/* webpackChunkName: "illustrations" */ '@/components/illustrations/ContentsIllustration'); const ContentsIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/ContentsIllustration'));
const PortfolioIllustration = () => const PortfolioIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/PortfolioIllustration'));
import(/* webpackChunkName: "illustrations" */ '@/components/illustrations/PortfolioIllustration'); const RoomsIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/RoomsIllustration'));
const RoomsIllustration = () => const HelloIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/HelloIllustration'));
import(/* webpackChunkName: "illustrations" */ '@/components/illustrations/RoomsIllustration'); const HelloMyKVIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/HelloMyKVIllustration'));
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: flavorValues.helloIllustration, 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;
display: flex;
position: relative;
width: 100%;
min-height: 100vh;
&--illustration {
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 50%;
background: $color-brand-dark;
}
}
&__illustration {
width: 30vw;
min-width: 400px;
align-self: center;
background-color: $color-brand; 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; display: flex;
flex-direction: column; position: relative;
z-index: 1;
width: 100%; width: 100%;
min-height: 100vh;
@include desktop { &--illustration {
padding: 2 * $large-spacing; &::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 50%;
background: $color-brand-dark;
}
} }
}
&__logo { &__illustration {
width: 300px; width: 30vw;
height: 50px; min-width: 400px;
margin-bottom: $large-spacing; align-self: center;
background-color: $color-brand;
z-index: 1;
display: none;
@include desktop { &--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; margin-bottom: 70px;
} }
}
&__page-subheading { &__paragraph {
@include regular-text; @include regular-text;
color: $color-brand; margin-bottom: $medium-spacing;
margin-bottom: $small-spacing;
}
&__page-heading { &:last-of-type {
@include heading-2; margin-bottom: 2*$large-spacing;
color: $color-brand; }
margin-bottom: 2 * $large-spacing; }
}
&__heading { &__button {
@include heading-2; @include regular-text;
margin-bottom: $small-spacing; 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;
}
&__claim { &__secondary-link {
@include heading-2; @include inline-title;
margin-bottom: 70px; cursor: pointer;
}
&__paragraph { @include desktop {
@include regular-text; margin-top: auto;
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

@ -37,13 +37,14 @@
import ADD_COMMENT_MUTATION from 'gql/mutations/addComment.gql'; import ADD_COMMENT_MUTATION from 'gql/mutations/addComment.gql';
import CommentInput from '@/components/rooms/CommentInput'; import CommentInput from '@/components/rooms/CommentInput';
import Comment from '@/components/rooms/Comment'; import Comment from '@/components/rooms/Comment';
const TextBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/TextBlock'); import {defineAsyncComponent} from 'vue';
const ImageBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ImageBlock'); const TextBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/TextBlock'));
const ImageUrlBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ImageUrlBlock'); const ImageBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ImageBlock'));
const VideoBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/VideoBlock'); const ImageUrlBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ImageUrlBlock'));
const LinkBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/LinkBlock'); const VideoBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/VideoBlock'));
const DocumentBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'); const LinkBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/LinkBlock'));
const SubtitleBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/SubtitleBlock'); const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'));
const SubtitleBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/SubtitleBlock'));
export default { export default {
components: { components: {

View File

@ -1,39 +1,64 @@
<template> <template>
<div class="hello" data-cy="hello-page"> <div
class="hello"
data-cy="hello-page"
>
<div class="about"> <div class="about">
<div class="about__logos logos"> <div class="about__logos logos">
<a href="https://www.hep-verlag.ch/" target="_blank"> <a
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 href="https://www.ehb.swiss/" target="_blank" v-if="$flavor.showEHB"> <a
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 Zusammenarbeit mit der Eidgenössischen Hochschule {{ $flavor.textAppName }} ist ein Angebot des hep Verlags in
für Berufsbildung (EHB). Zusammenarbeit mit der Eidgenössischen Hochschule 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 class="login-actions__title" data-cy="hello-title"> <h2
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 class="button button--primary button--big actions__submit" href="/api/oauth/login/" data-cy="oauth-login" <a
>Mit hep Konto anmelden</a class="button button--primary button--big actions__submit"
> 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 class="hep-link" href="/api/oauth/login/" data-cy="oauth-login">Jetzt registrieren</a> <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 class="hep-link" href="https://myskillbox.ch/anleitung" data-cy="oauth-login">Anleitung anschauen</a> <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">
@ -52,131 +77,133 @@
</template> </template>
<script> <script>
const HepLogoNoClaim = () => import(/* webpackChunkName: "icons" */ '@/components/icons/HepLogoNoClaim'); import {defineAsyncComponent} from 'vue';
const EhbLogo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/EhbLogo'); const HepLogoNoClaim = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/HepLogoNoClaim'));
const Logo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Logo'); const EhbLogo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EhbLogo'));
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;
width: 300px;
margin: $small-spacing auto $hello-block-margin;
@include desktop {
display: none;
}
}
.about {
display: none;
margin-bottom: $hello-block-margin;
@include desktop {
display: block; display: block;
} width: 300px;
margin: $small-spacing auto $hello-block-margin;
&__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: inline-block; display: none;
flex-direction: row; }
}
&:not(:last-child) { .about {
margin-right: 1rem; 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 {
display: inline-block;
flex-direction: row;
&:not(:last-child) {
margin-right: 1rem;
}
}
}
}
</style> </style>

View File

@ -1,11 +1,18 @@
<template> <template>
<div class="instrument"> <div class="instrument">
<h1 class="instrument__title" data-cy="instrument-title"> <h1
class="instrument__title"
data-cy="instrument-title"
>
{{ instrument.title }} {{ instrument.title }}
</h1> </h1>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div class="instrument__intro intro" data-cy="instrument-intro" v-html="instrument.intro" /> <div
class="instrument__intro intro"
data-cy="instrument-intro"
v-html="instrument.intro"
/>
<content-component <content-component
:component="component" :component="component"
@ -20,80 +27,80 @@
</template> </template>
<script> <script>
import INSTRUMENT_QUERY from '@/graphql/gql/queries/instrumentQuery.gql'; import INSTRUMENT_QUERY from '@/graphql/gql/queries/instrumentQuery.gql';
const ContentComponent = () => import {defineAsyncComponent} from 'vue';
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ContentComponent'); const ContentComponent = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentComponent'));
export default { export default {
apollo: { apollo: {
instrument() { instrument() {
return { return {
query: INSTRUMENT_QUERY, query: INSTRUMENT_QUERY,
variables: { variables: {
slug: this.$route.params.slug, slug: this.$route.params.slug
}, }
}; };
}
}, },
},
components: { components: {
ContentComponent, ContentComponent
}, },
data() { data() {
return { return {
instrument: {}, instrument: {}
}; };
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.instrument { .instrument {
&__title { &__title {
font-size: toRem(35px); font-size: toRem(35px);
margin-bottom: $large-spacing;
line-height: $default-heading-line-height;
}
& :deep() {
& p {
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
line-height: $default-heading-line-height;
} }
& p:last-child { & /deep/ {
margin-bottom: 0; & p {
} margin-bottom: $large-spacing;
}
& ul { & p:last-child {
@include list-parent; margin-bottom: 0;
} }
& p + ul { & ul {
margin-top: -30px; @include list-parent;
} }
& li { & p + ul {
@include list-child; margin-top: -30px;
line-height: 1.5; }
}
& b { & li {
font-weight: 600; @include list-child;
} line-height: 1.5;
}
.brand { & b {
color: $color-brand; font-weight: 600;
font-weight: 600; }
}
.secondary { .brand {
color: $color-accent-2; color: $color-brand;
font-weight: 600; font-weight: 600;
}
.secondary {
color: $color-accent-2;
font-weight: 600;
}
} }
} }
}
</style> </style>

View File

@ -2,11 +2,14 @@
<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 gültige Lizenz gefunden Für <span class="info-header__emph">{{ me.email }}</span> haben wir keine
gültige Lizenz gefunden
</p> </p>
</header> </header>
<section class="coupon"> <section class="coupon">
<ValidationObserver v-slot="{ handleSubmit }"> <ValidationObserver
v-slot="{handleSubmit}"
>
<form <form
class="license-activation__form license-activation-form" class="license-activation__form license-activation-form"
novalidate novalidate
@ -30,7 +33,12 @@
data-cy="coupon-button" data-cy="coupon-button"
label="Coupon abschicken" label="Coupon abschicken"
/> />
<a class="button button--big" data-cy="license-activation-cancel" @click="logout">Abmelden </a> <a
class="button button--big"
data-cy="license-activation-cancel"
@click="logout"
>Abmelden
</a>
</div> </div>
</form> </form>
</ValidationObserver> </ValidationObserver>
@ -39,10 +47,16 @@
<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 :href="teacherEditionUrl" class="hep-link">{{ $flavor.textAppName }} für Lehrpersonen</a> <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 :href="studentEditionUrl" class="hep-link">{{ $flavor.textAppName }} für Lernende</a> <a
:href="studentEditionUrl"
class="hep-link"
>{{ $flavor.textAppName }} für Lernende</a>
</li> </li>
</ul> </ul>
</section> </section>
@ -50,101 +64,103 @@
</template> </template>
<script> <script>
import REDEEM_COUPON from '@/graphql/gql/mutations/redeemCoupon.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import LoadingButton from '@/components/LoadingButton';
import { ValidationObserver } from 'vee-validate';
import me from '@/mixins/me'; import REDEEM_COUPON from '@/graphql/gql/mutations/redeemCoupon.gql';
import logout from '@/mixins/logout'; import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import LoadingButton from '@/components/LoadingButton';
import {ValidationObserver} from 'vee-validate';
const ValidatedInput = () => import('@/components/validation/ValidatedInput.vue'); import me from '@/mixins/me';
import logout from '@/mixins/logout';
export default { import {defineAsyncComponent} from 'vue';
mixins: [me, logout], const ValidatedInput = defineAsyncComponent(() => import('@/components/validation/ValidatedInput'));
components: {
LoadingButton,
ValidationObserver,
ValidatedInput,
},
data() { export default {
return { mixins: [me, logout],
coupon: '', components: {
couponErrors: [], LoadingButton,
loginError: '', ValidationObserver,
submitted: false, ValidatedInput,
me: { },
email: '',
}, data() {
teacherEditionUrl: `${process.env.HEP_URL}/myskillbox-lehrpersonen`, return {
studentEditionUrl: `${process.env.HEP_URL}/myskillbox-fur-lernende`, coupon: '',
loading: false, couponErrors: [],
}; loginError: '',
}, submitted: false,
methods: { me: {
validateBeforeSubmit() { email: '',
this.submitted = true; },
this.loading = true; teacherEditionUrl: `${process.env.HEP_URL}/myskillbox-lehrpersonen`,
this.$apollo studentEditionUrl: `${process.env.HEP_URL}/myskillbox-fur-lernende`,
.mutate({ loading: false,
};
},
methods: {
validateBeforeSubmit() {
this.submitted = true;
this.loading = true;
this.$apollo.mutate({
mutation: REDEEM_COUPON, mutation: REDEEM_COUPON,
variables: { variables: {
input: { input: {
couponCode: this.coupon, couponCode: this.coupon,
}, },
}, },
update: (store, { data: { coupon } }) => { update: (
store,
{
data: {coupon},
},
) => {
if (coupon.success) { if (coupon.success) {
this.couponErrors = []; this.couponErrors = [];
this.$apollo this.$apollo.query({
.query({ query: ME_QUERY,
query: ME_QUERY, fetchPolicy: 'network-only',
fetchPolicy: 'network-only', }).then(() => this.$router.push('/'));
})
.then(() => this.$router.push('/'));
} }
}, },
}) }).catch(({message}) => {
.catch(({ message }) => { if (message.indexOf('invalid_coupon') > -1) {
if (message.indexOf('invalid_coupon') > -1) { this.couponErrors = ['Der angegebene Coupon-Code ist ungültig.'];
this.couponErrors = ['Der angegebene Coupon-Code ist ungültig.']; } else {
} else { this.couponErrors = ['Es ist ein Fehler aufgetreten. Bitte versuchen Sie es nochmals oder kontaktieren Sie den Administrator.'];
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;
} }
}
.get-license { .actions {
margin-top: $large-spacing; &__reset {
} 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,6 +1,8 @@
<template> <template>
<div class="module-visibility"> <div class="module-visibility">
<h1 class="module-visibility__page-title">Sichtbarkeit</h1> <h1 class="module-visibility__page-title">
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 (
@ -16,113 +18,127 @@
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 value="" selected>-</option> <option
<option :value="schoolClass.id" v-for="schoolClass in schoolClasses" :key="schoolClass.id"> value=""
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 class="button button--primary" data-cy="save-visibility-button" @click="sync">Anpassungen übernehmen</a> <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.gql'; import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery';
import { MODULE_PAGE } from '@/router/module.names'; import {MODULE_PAGE} from '@/router/module.names';
const EyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon'); import {defineAsyncComponent} from 'vue';
const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon'));
export default { export default {
mixins: [me],
components: {
EyeIcon,
},
data() { mixins: [me],
return { components: {
selectedClassId: '', EyeIcon,
};
},
computed: {
schoolClasses() {
return this.me.schoolClasses.filter((schoolClass) => schoolClass.id !== this.me.selectedClass.id);
}, },
slug() {
return this.$route.params.slug;
},
},
methods: { data() {
select(selectedClassId) { return {
this.selectedClassId = selectedClassId; selectedClassId: '',
};
}, },
sync() {
if (this.selectedClassId) { computed: {
const slug = this.slug; schoolClasses() {
this.$apollo return this.me.schoolClasses.filter(schoolClass => schoolClass.id !== this.me.selectedClass.id);
.mutate({ },
mutation: SYNC_VISIBILITY_MUTATION, slug() {
variables: { return this.$route.params.slug;
input: { }
module: slug, },
templateSchoolClass: this.selectedClassId,
schoolClass: this.me.selectedClass.id, methods: {
}, select(selectedClassId) {
}, this.selectedClassId = selectedClassId;
refetchQueries: [ },
{ sync() {
query: MODULE_DETAILS_QUERY, if (this.selectedClassId) {
variables: { const slug = this.slug;
slug, this.$apollo.mutate({
mutation: SYNC_VISIBILITY_MUTATION,
variables: {
input: {
module: slug,
templateSchoolClass: this.selectedClassId,
schoolClass: this.me.selectedClass.id,
}, },
}, },
], refetchQueries: [
}) {
.then(() => { query: MODULE_DETAILS_QUERY,
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,18 +1,23 @@
<template> <template>
<div> <div>
<logo class="onboarding__logo" /> <logo class="onboarding__logo" />
<h1 class="onboarding__heading">Herzlich willkommen!</h1> <h1 class="onboarding__heading">
Herzlich willkommen!
</h1>
<p class="onboarding__claim">Schauen Sie sich die Einführung an und lernen Sie {{ $flavor.textAppName }} kennen.</p> <p class="onboarding__claim">
Schauen Sie sich die Einführung an und lernen Sie {{ $flavor.textAppName }} kennen.
</p>
</div> </div>
</template> </template>
<script> <script>
const Logo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Logo'); import {defineAsyncComponent} from 'vue';
const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo'));
export default { export default {
components: { components: {
Logo, Logo
}, },
}; };
</script> </script>

View File

@ -5,282 +5,258 @@
</h1> </h1>
<div id="survey" /> <div id="survey" />
<solution :value="solution" v-if="showSolution" /> <solution
:value="solution"
v-if="showSolution"
/>
<div v-if="surveyComplete"> <div v-if="surveyComplete">
<a class="button button--primary" @click="reopen">Übung bearbeiten</a> <a
class="button button--primary"
@click="reopen"
>Übung bearbeiten</a>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import '@/styles/survey.modern.css'; import {css} from '@/survey.config';
import '@/styles/survey.reset.css'; import gql from 'graphql-tag';
import { css } from '@/survey.config'; import {Model} from 'survey-core';
import gql from 'graphql-tag'; // we are switching to the knockout version because the Vue version only works with Vue 2 (as of July 2022)
import { Model, StylesManager } from 'survey-knockout'; import 'survey-knockout-ui';
// 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';
import UPDATE_ANSWER from '@/graphql/gql/mutations/updateAnswer.gql'; import UPDATE_ANSWER from '@/graphql/gql/mutations/updateAnswer.gql';
import { extractSurveySolutions } from '@/helpers/survey-solutions'; import {extractSurveySolutions} from '@/helpers/survey-solutions';
import { isTeacher } from '@/helpers/is-teacher'; import {isTeacher} from '@/helpers/is-teacher';
import { meQuery } from '@/graphql/queries'; import {meQuery} from '@/graphql/queries';
const Solution = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/Solution'); import {defineAsyncComponent} from 'vue';
StylesManager.applyTheme('modern'); const Solution = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Solution'));
const MODULE_QUERY = gql` const MODULE_QUERY = gql`
query ModuleSolutions($slug: String) { query ModuleSolutions($slug: String) {
module(slug: $slug) { module(slug: $slug) {
solutionsEnabled solutionsEnabled
slug slug
}
} }
}
`; `;
export default { export default {
props: ['id'], props: ['id'],
components: { components: {
Solution, Solution,
},
data() {
return {
survey: this.initSurvey(),
currentPage: null,
surveyData: null,
title: '',
module: {},
completed: false,
me: {
permissions: [],
},
saveDisabled: false,
};
},
computed: {
surveyComplete() {
return this.survey && this.survey.isCompleted;
}, },
showSolution() {
return (this.module.solutionsEnabled || this.isTeacher) && !this.survey.isCompleted; data() {
},
solution() {
// todo: should this be done inside of Solution.vue?
return { return {
text: this.answers.reduce((previous, answer) => { survey: this.initSurvey(),
if (!answer.answer) { title: '',
return previous; module: {},
} completed: false,
if (answer.type === 'matrix' || answer.type === 'checkbox') { me: {
// wrap all the answers inside li tags and convert to a single string permissions: [],
const answerText = answer.answer.map((a) => `<li class="solution-text__list-item">${a}</li>`).join(''); },
return ` saveDisabled: false,
};
},
computed: {
surveyComplete() {
return this.survey && this.survey.isCompleted;
},
showSolution() {
return (this.module.solutionsEnabled || this.isTeacher) && !this.survey.isCompleted;
},
solution() {
// todo: should this be done inside of Solution.vue?
return {
text: this.answers.reduce((previous, answer) => {
if (!answer.answer) {
return previous;
}
if (answer.type === 'matrix' || answer.type === 'checkbox') {
// wrap all the answers inside li tags and convert to a single string
const answerText = answer.answer.map(a => `<li class="solution-text__list-item">${a}</li>`).join('');
return `
${previous} ${previous}
<h2 class="solution-text__heading">${answer.title}</h2> <h2 class="solution-text__heading">${answer.title}</h2>
<ul class="solution-text__answer solution-text__list">${answerText}</ul> <ul class="solution-text__answer solution-text__list">${answerText}</ul>
`; `;
} else { } else {
return ` return `
${previous} ${previous}
<h2 class="solution-text__heading">${answer.title}</h2> <h2 class="solution-text__heading">${answer.title}</h2>
<p class="solution-text__answer">${answer.answer}</p> <p class="solution-text__answer">${answer.answer}</p>
`; `;
} }
}, ''), }, ''),
};
},
answers() {
return this.currentPage && this.currentPage.elements
? this.currentPage.elements.reduce(extractSurveySolutions, [])
: [];
},
isTeacher() {
return isTeacher(this);
},
},
mounted() {
if (this.surveyData) {
this.loadSurveyFromServer(this.surveyData);
}
},
destroyed() {},
methods: {
initSurvey(data, answers) {
let survey = new Model(data);
const flatAnswers = {};
for (let k in answers) {
flatAnswers[k] = answers[k].answer;
}
if (Object.keys(flatAnswers).length > 0) {
// answers are not empty
survey.data = flatAnswers;
}
this.currentPage = survey.currentPage;
const updatePage = (sender, { oldCurrentPage, newCurrentPage }) => {
console.log(oldCurrentPage, newCurrentPage);
this.currentPage = newCurrentPage;
};
const saveSurvey = (sender, { exit }) => {
if (this.saveDisabled) {
return;
}
this.completed = true;
const data = {};
for (let k in survey.data) {
// if (survey.data.hasOwnProperty(k)) {
if (Object.prototype.hasOwnProperty.call(survey.data, k)) {
let question = sender.getQuestionByName(k);
data[k] = {
answer: survey.data[k],
correct: question && question.correctAnswer ? question.correctAnswer : '',
};
}
}
if (Object.keys(data).length === 0) {
// data is empty
return;
}
const answer = {
surveyId: this.id,
data: JSON.stringify(data),
}; };
},
answers() {
return this.survey.currentPage && this.survey.currentPage.elements
? this.survey.currentPage.elements.reduce(extractSurveySolutions, [])
: [];
},
isTeacher() {
return isTeacher(this);
},
},
this.$apollo methods: {
.mutate({ initSurvey(data, answers) {
let survey = new Model(data);
const flatAnswers = {};
for (let k in answers) {
flatAnswers[k] = answers[k].answer;
}
this.$log.debug('flatAnswers', flatAnswers);
this.$log.debug('data', survey.data);
if (Object.keys(flatAnswers).length > 0) {
// answers are not empty
survey.data = flatAnswers;
}
const saveSurvey = (sender, {exit}) => {
this.$log.debug('saving survey', sender);
if (this.saveDisabled) {
return;
}
this.completed = true;
const data = {};
for (let k in survey.data) {
// if (survey.data.hasOwnProperty(k)) {
if (Object.prototype.hasOwnProperty.call(survey.data, k)) {
let question = sender.getQuestionByName(k);
data[k] = {
answer: survey.data[k],
correct: question && question.correctAnswer ? question.correctAnswer : '',
};
}
}
if (Object.keys(data).length === 0) {
// data is empty
return;
}
const answer = {
surveyId: this.id,
data: JSON.stringify(data),
};
this.$apollo.mutate({
mutation: UPDATE_ANSWER, mutation: UPDATE_ANSWER,
variables: { variables: {
input: { input: {
answer, answer,
}, },
}, },
update: ( update: (store, {data: {updateAnswer: {answer}}}) => {
store,
{
data: {
updateAnswer: { answer },
},
}
) => {
const query = SURVEY_QUERY; const query = SURVEY_QUERY;
const variables = { id: this.id }; const variables = {id: this.id};
const { survey } = store.readQuery({ query, variables }); const {survey} = store.readQuery({query, variables});
if (survey) { if (survey) {
const newData = { const newData = { // data is already in use in parent scope
// data is already in use in parent scope
survey: { survey: {
...survey, ...survey,
answer, answer
}, }
}; };
store.writeQuery({ query, variables, data: newData }); store.writeQuery({query, variables, data: newData});
} }
}, },
}) }).then(() => {
.then(() => {
if (exit) { if (exit) {
this.$router.go(-1); this.$router.go(-1);
} }
}); });
};
survey.onComplete.add((sender, options) => {
saveSurvey(sender, {
...options,
exit: true,
});
});
survey.onCurrentPageChanged.add(saveSurvey);
survey.onCurrentPageChanged.add(updatePage);
survey.css = css;
survey.locale = 'de';
survey.showProgressBar = 'bottom';
survey.pageNextText = 'Speichern & Weiter';
survey.render('survey');
return survey;
},
reopen() {
this.saveDisabled = true; // disable saving, because resetting triggers a page change which we don't want to save
this.completed = false;
let data = this.survey.data; // save the data
this.survey.clear();
this.survey.data = data; // reapply it
this.saveDisabled = false;
},
loadSurveyFromServer(survey) {
let json = JSON.parse(survey.data);
json.showTitle = false;
json.showProgressBar = 'bottom';
let answer = {};
if (survey.answer && survey.answer.data) {
answer = JSON.parse(survey.answer.data);
}
if (!this.completed) {
this.survey = this.initSurvey(json, answer);
}
this.title = json.title;
},
},
apollo: {
survey: {
query: SURVEY_QUERY,
variables() {
return {
id: this.id,
}; };
},
manual: true,
result({ data, loading }) {
if (!loading) {
this.surveyData = data.survey;
this.loadSurveyFromServer(data.survey);
const module = data.survey.module;
this.$apollo.addSmartQuery('module', { survey.onComplete.add((sender, options) => {
query: MODULE_QUERY, saveSurvey(sender, {
variables: { ...options,
slug: module.slug, exit: true
},
}); });
} });
survey.onCurrentPageChanged.add(saveSurvey);
survey.css = css;
survey.locale = 'de';
survey.showProgressBar = 'bottom';
survey.pageNextText = 'Speichern & Weiter';
this.$log.debug(survey.data);
survey.render('survey');
return survey;
},
reopen() {
this.saveDisabled = true; // disable saving, because resetting triggers a page change which we don't want to save
this.completed = false;
let data = this.survey.data; // save the data
this.survey.clear();
this.survey.data = data; // reapply it
this.saveDisabled = false;
}, },
}, },
me: meQuery,
}, apollo: {
}; survey: {
query: SURVEY_QUERY,
variables() {
return {
id: this.id,
};
},
manual: true,
result({data, loading }) {
if (!loading) {
let json = JSON.parse(data.survey.data);
json.showTitle = false;
let answer = {};
if (data.survey.answer && data.survey.answer.data) {
answer = JSON.parse(data.survey.answer.data);
}
if (!this.completed) {
this.survey = this.initSurvey(json, answer);
}
this.title = json.title;
const module = data.survey.module;
this.$apollo.addSmartQuery('module', {
query: MODULE_QUERY,
variables: {
slug: module.slug
},
});
}
},
},
me: meQuery,
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.survey-page { .survey-page {
max-width: 800px; max-width: 800px;
display: grid; display: grid;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
grid-auto-rows: auto; grid-auto-rows: auto;
grid-row-gap: $large-spacing; grid-row-gap: $large-spacing;
justify-self: center; justify-self: center;
padding: 100px 0; padding: 100px 0;
width: 100%; width: 100%;
&__title { &__title {
@include meta-title; @include meta-title;
margin: 0; margin: 0;
}
} }
}
</style> </style>

View File

@ -46,15 +46,15 @@
<script> <script>
import ModuleTeaser from '@/components/modules/ModuleTeaser.vue'; import ModuleTeaser from '@/components/modules/ModuleTeaser.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 = () => import(/* webpackChunkName: "icons" */'@/components/icons/Play'); const BulbIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/BulbIcon'));
const BulbIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/BulbIcon');
export default { export default {