Merge branch 'feature/vue3-upgrade-2023-01-25' into develop

This commit is contained in:
Ramon Wenger 2023-02-01 16:03:39 +01:00
commit 300cb8681f
129 changed files with 12708 additions and 14370 deletions

View File

@ -42,7 +42,7 @@ module.exports = {
'@': resolve('src'),
styles: resolve('src/styles'),
gql: resolve('src/graphql/gql'),
// vue: '@vue/compat',
vue: '@vue/compat',
},
},
module: {
@ -60,7 +60,7 @@ module.exports = {
},
compilerOptions: {
compatConfig: {
MODE: 2,
MODE: 2
},
},
},

View File

@ -1,10 +1,9 @@
import module from '../../../fixtures/module.minimal';
import { getMinimalMe } from '../../../support/helpers';
import { hasOperationName } from '../../../support/graphql';
import {getMinimalMe} from '../../../support/helpers';
import {hasOperationName} from '../../../support/graphql';
let snapshotTitle;
let deleteSuccess;
let page;
const moduleWithSnapshots = {
...module,
@ -28,68 +27,71 @@ const moduleWithSnapshots = {
],
};
const mockDeleteSnapshot = (success) => {
cy.intercept('POST', '/api/graphql', (req) => {
if (hasOperationName(req, 'DeleteSnapshot')) {
let result;
if (success) {
result = {
message: 'yay!',
__typename: 'Success',
};
} else {
result = {
reason: 'Not the owner',
__typename: 'NotOwner',
};
}
req.reply({
data: {
deleteSnapshot: {
result,
},
},
});
}
});
};
// const mockDeleteSnapshot = (success) => {
// cy.intercept('POST', '/api/graphql', (req) => {
// if (hasOperationName(req, 'DeleteSnapshot')) {
// let result;
// if (success) {
// result = {
// message: 'yay!',
// __typename: 'Success',
// };
// } else {
// result = {
// reason: 'Not the owner',
// __typename: 'NotOwner',
// };
// }
// req.reply({
// data: {
// deleteSnapshot: {
// result,
// },
// },
// });
//
// }
// });
// };
const mockUpdateSnapshot = (title) => {
cy.intercept('POST', '/api/graphql', (req) => {
if (hasOperationName(req, 'UpdateSnapshot')) {
let snapshot;
if (title) {
snapshot = {
__typename: 'SnapshotNode',
id: 'U25hcHNob3ROb2RlOjQ=',
title,
};
} else {
snapshot = {
__typename: 'NotOwner',
reason: 'Not the owner',
};
}
req.reply({
data: {
updateSnapshot: {
snapshot,
},
},
});
}
});
};
// const mockUpdateSnapshot = (title) => {
// cy.intercept('POST', '/api/graphql', (req) => {
// if (hasOperationName(req, 'UpdateSnapshot')) {
// let snapshot;
// if (title) {
// snapshot = {
// __typename: 'SnapshotNode',
// id: 'U25hcHNob3ROb2RlOjQ=',
// title,
// };
// } else {
// snapshot = {
// __typename: 'NotOwner',
// reason: 'Not the owner',
// };
// }
// req.reply({
// data: {
// updateSnapshot: {
// snapshot,
// },
// },
// });
// }
// });
//
// };
// wait for the specified amount of requests in the test, so they don't spill over to the next test
const waitForNRequests = (n) => {
const waitNTimes = (n) => {
for (let i = 0; i < n; i++) {
cy.wait('@graphqlRequest');
}
};
describe('Snapshot', () => {
const operations = (isTeacher) => ({
const operations = isTeacher => ({
operations: {
UpdateSnapshot: {
updateSnapshot: {
@ -127,22 +129,21 @@ describe('Snapshot', () => {
};
}
return result;
},
},
},
MeQuery: getMinimalMe({ isTeacher }),
MeQuery: getMinimalMe({isTeacher}),
ModuleDetailsQuery: {
module,
},
CreateSnapshot: {
createSnapshot: {
snapshot: {
id: 'snapshot-id',
title: 'Mi Snapshot',
created: '2022-01-01',
creator: 'me',
shared: false,
mine: true,
id: '',
title: '',
created: '',
creator: '',
},
success: true,
},
@ -178,7 +179,6 @@ describe('Snapshot', () => {
},
SnapshotDetail: {
snapshot: {
title: 'Shared snapshot',
chapters: [],
module: {},
},
@ -194,7 +194,6 @@ describe('Snapshot', () => {
beforeEach(() => {
snapshotTitle = false;
deleteSuccess = false;
page = moduleWithSnapshots;
cy.setup();
});
@ -202,7 +201,7 @@ describe('Snapshot', () => {
cy.mockGraphqlOps(operations(true));
cy.visit('module/miteinander-reden/');
cy.getByDataCy('snapshot-menu').should('be.visible');
waitForNRequests(4);
waitNTimes(4);
});
it('Menu is not visible for student', () => {
@ -211,27 +210,19 @@ describe('Snapshot', () => {
cy.getByDataCy('module-title').should('be.visible');
cy.getByDataCy('snapshot-menu').should('not.exist');
waitForNRequests(3);
waitNTimes(3);
});
it('Creates Snapshot', () => {
cy.mockGraphqlOps(operations(true));
cy.visit('module/miteinander-reden/snapshots');
cy.getByDataCy('snapshot-list')
.should('exist')
.within(() => {
cy.get('.snapshots__snapshot').should('have.length', 1);
});
cy.getByDataCy('back-link').click();
cy.visit('module/miteinander-reden/');
cy.getByDataCy('module-snapshots-button').click();
cy.getByDataCy('create-snapshot-button').click();
cy.getByDataCy('show-all-snapshots-button').click();
cy.getByDataCy('snapshot-list')
.should('exist')
.within(() => {
cy.get('.snapshots__snapshot').should('have.length', 2);
});
waitForNRequests(7);
cy.getByDataCy('snapshot-list').should('exist').within(() => {
cy.get('.snapshots__snapshot').should('have.length', 1);
});
waitNTimes(7);
});
it('Applies Snapshot', () => {
@ -243,7 +234,7 @@ describe('Snapshot', () => {
cy.getByDataCy('module-title').should('exist');
cy.getByDataCy('snapshot-header').should('not.exist');
waitForNRequests(8);
waitNTimes(9);
});
it('Renames Snapshot', () => {
@ -252,12 +243,12 @@ describe('Snapshot', () => {
snapshotTitle = newTitle;
// mockUpdateSnapshot(newTitle);
cy.visit('module/miteinander-reden/snapshots');
cy.getByDataCy('snapshot-link').should('contain.text', 'Old Title');
cy.getByDataCy('snapshot-link').should('have.text', 'Old Title');
cy.getByDataCy('rename-snapshot-button').click();
cy.getByDataCy('edit-name-input').clear().type(newTitle);
cy.getByDataCy('modal-save-button').click();
cy.getByDataCy('snapshot-link').should('contain.text', 'New Title');
waitForNRequests(5);
cy.getByDataCy('snapshot-link').should('have.text', 'New Title');
waitNTimes(5);
});
it('Deletes Snapshot', () => {
@ -269,7 +260,7 @@ describe('Snapshot', () => {
cy.getByDataCy('delete-snapshot-button').click();
cy.getByDataCy('modal-save-button').click();
cy.getByDataCy('snapshot-entry').should('have.length', 0);
waitForNRequests(6);
waitNTimes(6);
});
it('Displays the Snapshot list correcly', () => {
@ -278,16 +269,13 @@ describe('Snapshot', () => {
cy.getByDataCy('snapshot-entry').should('have.length', 1);
cy.getByDataCy('delete-snapshot-button').should('exist');
cy.getByDataCy('rename-snapshot-button').should('exist');
cy.getByDataCy('snapshot-link').should('contain.text', 'Old Title');
cy.getByDataCy('snapshot-link').should('have.text', 'Old Title');
cy.getByDataCy('team-snapshots-link').click();
cy.getByDataCy('snapshot-entry').should('have.length', 1);
cy.getByDataCy('snapshot-link').should('contain.text', 'Shared snapshot');
cy.getByDataCy('snapshot-link').should('have.text', 'Shared snapshot');
cy.getByDataCy('delete-snapshot-button').should('not.exist');
cy.getByDataCy('rename-snapshot-button').should('not.exist');
cy.getByDataCy('snapshot-link').click();
cy.getByDataCy('module-title').should('contain.text', 'Shared snapshot');
waitForNRequests(5);
waitNTimes(4);
});
afterEach(() => {});
});

View File

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

View File

@ -27,6 +27,7 @@ describe('The Room Page (Teacher)', () => {
AddRoomEntry: {
addRoomEntry: {
roomEntry: {
slug: 'entry-slug',
title: entryTitle,
contents: [
{

View File

@ -321,8 +321,9 @@ describe('Teacher Class Management', () => {
it('tries to create a new class with duplicate name', () => {
const name = 'Hill Billy Valley';
const oldName = 'Some stupid class';
let selectedClass = teacher.selectedClass;
selectedClass.name = 'Some stupid class';
selectedClass.name = oldName;
const schoolClasses = [teacher.selectedClass];
@ -337,10 +338,6 @@ describe('Teacher Class Management', () => {
MeQuery: () => ({
me: me(),
}),
WhateverNode() {
console.log('Through here');
return {};
},
MySchoolClassQuery: () => ({
me: me(),
}),
@ -366,6 +363,7 @@ describe('Teacher Class Management', () => {
cy.visit('/me/my-class');
cy.get('h1').should('exist');
cy.getByDataCy('group-list-name').should('contain', oldName);
cy.get('[data-cy=header-user-widget]').within(() => {
cy.get('[data-cy=user-widget-avatar]').click();

View File

@ -1,21 +1,42 @@
module.exports = {
moduleFileExtensions: ['js', 'jsx', 'ts', 'json', 'vue'],
moduleFileExtensions: [
'js',
'jsx',
'ts',
'json',
'vue',
],
transform: {
'\\.(gql|graphql)$': 'jest-transform-graphql',
'\\.(gql|graphql)$': '@graphql-tools/jest-transform',
'^.+\\.js$': 'babel-jest',
'^.+\\.ts$': 'babel-jest',
'^.+\\.vue$': '@vue/vue2-jest',
'^.+\\.vue$': '@vue/vue3-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
},
modulePaths: ['<rootDir>/src', '<rootDir>/node_modules'],
transformIgnorePatterns: ['/node_modules/'],
modulePaths: [
'<rootDir>/src',
'<rootDir>/node_modules',
],
transformIgnorePatterns: [
'/node_modules/',
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^gql/(.*)$': '<rootDir>/src/graphql/gql/$1',
},
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
snapshotSerializers: [
'<rootDir>/node_modules/jest-serializer-vue',
],
testEnvironment: 'jsdom',
testMatch: ['**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'],
testURL: 'http://localhost/',
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
testEnvironmentOptions: {
url: 'http://localhost/',
customExportConditions: ['node', 'node-addons'] // needed according to https://github.com/vuejs/vue-jest/issues/479
},
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)',
],
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
};

13228
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,11 +30,10 @@
"prettier:check": "prettier . --check"
},
"dependencies": {
"@apollo/client": "^3.5.8",
"@apollo/client": "^3.5.10",
"@babel/core": "^7.16.7",
"@babel/eslint-plugin": "^7.16.5",
"@babel/plugin-transform-runtime": "^7.5.0",
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.5.4",
"@babel/preset-stage-2": "^7.0.0",
"@babel/preset-typescript": "^7.16.7",
@ -48,17 +47,21 @@
"@tiptap/extension-list-item": "^2.0.0-beta.20",
"@tiptap/extension-paragraph": "^2.0.0-beta.23",
"@tiptap/extension-text": "^2.0.0-beta.15",
"@tiptap/vue-2": "^2.0.0-beta.77",
"@tiptap/vue-3": "^2.0.0-beta.90",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"@vue/test-utils": "^1.3.0",
"@vue/vue2-jest": "^27.0.0",
"@vue/apollo-option": "^4.0.0-alpha.16",
"@vue/compat": "3.2.30",
"@vue/compiler-sfc": "3.2.30",
"@vue/test-utils": "^2.2.0",
"@vue/vue3-jest": "^29.1.1",
"autoprefixer": "^10.4.12",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^27.5.1",
"babel-jest": "^29.2.2",
"babel-loader": "^8.0.6",
"chalk": "^2.0.1",
"copy-webpack-plugin": "^10.1.0",
"core-js": "^3.26.0",
"css-loader": "^3.6.0",
"css-minimizer-webpack-plugin": "^3.4.1",
"cy2": "^1.2.1",
@ -76,7 +79,8 @@
"graphql-tag": "^2.10.1",
"graphql-tools": "^8.2.5",
"html-webpack-plugin": "^5.5.0",
"jest": "^27.5.1",
"jest": "^29.2.2",
"jest-environment-jsdom": "^29.2.2",
"jest-serializer-vue": "^2.0.2",
"jest-transform-graphql": "^2.1.0",
"jest-transform-stub": "^2.0.0",
@ -85,6 +89,7 @@
"loglevel": "^1.8.0",
"mini-css-extract-plugin": "^2.4.5",
"mock-apollo-client": "^1.2.0",
"node-sass": "^7.0.3",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^15.0.0",
@ -92,26 +97,24 @@
"postcss-url": "^10.1.3",
"prettier": "2.8.2",
"rimraf": "^2.6.0",
"sass": "^1.56.1",
"sass-loader": "^12.6.0",
"semver": "^5.3.0",
"shelljs": "^0.8.5",
"survey-knockout": "^1.9.41",
"survey-core": "1.9.41",
"survey-knockout-ui": "1.9.41",
"ts-loader": "^8.3.0",
"typescript": "^4.5.4",
"uploadcare-widget": "^3.6.0",
"url-loader": "^4.1.1",
"vee-validate": "^3.4.14",
"vue": "^2.7.13",
"vue-apollo": "^3.1.0",
"vue-loader": "^15.10.0",
"vee-validate": "^4.5.10",
"vue": "3.2.30",
"vue-loader": "^16.8.3",
"vue-matomo": "^4.1.0",
"vue-router": "^3.5.3",
"vue-scrollto": "^2.11.0",
"vue-router": "^4.0.14",
"vue-scrollto": "^2.20.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.7.13",
"vue-vimeo-player": "^0.2.2",
"vuex": "^3.0.1",
"vue-vimeo-player": "^1.1.2",
"vuejs3-logger": "1.0.0",
"vuex": "4.0.1",
"webpack": "^5.67.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.1",
@ -126,11 +129,5 @@
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"resolutions": {
"vue": "2.6.14"
},
"optionalDependencies": {
"fsevents": "^2.3.2"
}
]
}

View File

@ -9,38 +9,54 @@
</template>
<script>
import { defineAsyncComponent } from 'vue';
import { mapGetters } from 'vuex';
import ScrollUp from '@/components/ScrollUp';
import ReadOnlyBanner from '@/components/ReadOnlyBanner';
import modals from '@/components/modals';
const NewContentBlockWizard = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/NewContentBlockWizard');
const EditContentBlockWizard = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/EditContentBlockWizard');
const EditRoomEntryWizard = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/rooms/room-entries/EditRoomEntryWizard');
const NewProjectEntryWizard = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/portfolio/NewProjectEntryWizard');
const EditProjectEntryWizard = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/portfolio/EditProjectEntryWizard');
const NewObjectiveWizard = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/objective-groups/NewObjectiveWizard');
const NewNoteWizard = () => import(/* webpackChunkName: "content-forms" */ '@/components/notes/NewNoteWizard');
const EditNoteWizard = () => import(/* webpackChunkName: "content-forms" */ '@/components/notes/EditNoteWizard');
const EditClassNameWizard = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/school-class/EditClassNameWizard');
const EditTeamNameWizard = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/profile/EditTeamNameWizard');
const EditSnapshotTitleWizard = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/snapshots/EditSnapshotTitleWizard');
const DefaultLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/DefaultLayout');
const SimpleLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/SimpleLayout');
const FullScreenLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/FullScreenLayout');
const PublicLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/PublicLayout');
const BlankLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/BlankLayout');
const SplitLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/SplitLayout');
const NewContentBlockWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/NewContentBlockWizard')
);
const EditContentBlockWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/EditContentBlockWizard')
);
const EditRoomEntryWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/rooms/room-entries/EditRoomEntryWizard')
);
const NewProjectEntryWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/portfolio/NewProjectEntryWizard')
);
const EditProjectEntryWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/portfolio/EditProjectEntryWizard')
);
const NewObjectiveWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/objective-groups/NewObjectiveWizard')
);
const NewNoteWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/notes/NewNoteWizard')
);
const EditNoteWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/notes/EditNoteWizard')
);
const EditClassNameWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/school-class/EditClassNameWizard')
);
const EditTeamNameWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/profile/EditTeamNameWizard')
);
const EditSnapshotTitleWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/snapshots/EditSnapshotTitleWizard')
);
const DefaultLayout = defineAsyncComponent(() => import(/* webpackChunkName: "layouts" */ '@/layouts/DefaultLayout'));
const SimpleLayout = defineAsyncComponent(() => import(/* webpackChunkName: "layouts" */ '@/layouts/SimpleLayout'));
const FullScreenLayout = defineAsyncComponent(() =>
import(/* webpackChunkName: "layouts" */ '@/layouts/FullScreenLayout')
);
const PublicLayout = defineAsyncComponent(() => import(/* webpackChunkName: "layouts" */ '@/layouts/PublicLayout'));
const BlankLayout = defineAsyncComponent(() => import(/* webpackChunkName: "layouts" */ '@/layouts/BlankLayout'));
const SplitLayout = defineAsyncComponent(() => import(/* webpackChunkName: "layouts" */ '@/layouts/SplitLayout'));
export default {
name: 'App',

View File

@ -1,98 +1,105 @@
<template>
<div class="add-content">
<a class="add-content__button" @click="addContent">
<a
class="add-content__button"
@click="addContent"
>
<add-pointer class="add-content__icon" />
</a>
</div>
</template>
<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 {
props: {
where: {
type: Object,
validator(prop) {
return (
Object.prototype.hasOwnProperty.call(prop, 'after') || Object.prototype.hasOwnProperty.call(prop, 'parent')
);
export default {
props: {
where: {
type: Object,
validator(prop) {
return Object.prototype.hasOwnProperty.call(prop, 'after' )
|| Object.prototype.hasOwnProperty.call(prop, 'parent');
}
},
},
},
components: {
AddPointer,
},
components: {
AddPointer
},
computed: {
parent() {
return this.where.parent;
},
after() {
return this.where.after;
},
isObjectiveGroup() {
return this.parent && this.parent.__typename === 'ObjectiveGroupNode';
},
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);
computed: {
parent() {
return this.where.parent;
},
after() {
return this.where.after;
},
isObjectiveGroup() {
return this.parent && this.parent.__typename === 'ObjectiveGroupNode';
},
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);
}
}
}
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.add-content {
display: none;
position: relative;
@include desktop {
display: flex;
.add-content {
display: none;
position: relative;
@include desktop {
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>

View File

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

View File

@ -11,71 +11,69 @@
</template>
<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 {
props: {
route: {
type: String,
default: null,
export default {
props: {
route: {
type: String,
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: {
AddIcon,
},
components: {
AddIcon
},
computed: {
component() {
// only use the router link if the route prop is provided, otherwise render a normal anchor tag
return this.route ? 'router-link' : 'a';
computed: {
component() {
// only use the router link if the route prop is provided, otherwise render a normal anchor tag
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>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.add-widget {
display: none;
align-items: center;
justify-content: center;
@include widget-shadow;
cursor: pointer;
.add-widget {
display: none;
align-items: center;
justify-content: center;
@include widget-shadow;
cursor: pointer;
@include desktop {
display: flex;
@include desktop {
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>

View File

@ -1,68 +1,76 @@
<template>
<router-link :to="to" data-cy="back-link" class="sub-navigation-item back-link">
<router-link
:to="to"
class="sub-navigation-item back-link"
>
<chevron-left class="back-link__icon sub-navigation-item__icon" />
{{ fullTitle }}
</router-link>
</template>
<script>
import { MODULE_PAGE } from '@/router/module.names';
import { ROOMS_PAGE } from '@/router/room.names';
import { PROJECTS_PAGE } from '@/router/portfolio.names';
import { MODULE_PAGE } from '@/router/module.names';
import { ROOMS_PAGE } from '@/router/room.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 {
props: {
title: {
type: String,
default: '',
export default {
props: {
title: {
type: String,
default: '',
},
type: {
type: String,
default: 'topic',
},
slug: {
type: String,
default: '',
},
},
type: {
type: String,
default: 'topic',
},
slug: {
type: String,
default: '',
},
},
components: {
ChevronLeft,
},
components: {
ChevronLeft,
},
computed: {
to() {
switch (this.type) {
case 'topic':
return { name: 'topic', params: { topicSlug: this.slug } };
case 'module':
return { name: MODULE_PAGE };
case 'portfolio':
return { name: PROJECTS_PAGE };
default:
return { name: ROOMS_PAGE };
}
computed: {
to() {
switch (this.type) {
case 'topic':
if (this.slug) {
return {name: 'topic', params: {topicSlug: this.slug}};
} else {
return {};
}
case 'module':
return {name: MODULE_PAGE};
case 'portfolio':
return {name: PROJECTS_PAGE};
default:
return {name: ROOMS_PAGE};
}
},
fullTitle() {
switch (this.type) {
case 'topic':
return `${this.$flavor.textTopic}: ${this.title}`;
case 'module':
return `${this.$flavor.textModule}: ${this.title}`;
default:
return this.title;
}
},
},
fullTitle() {
switch (this.type) {
case 'topic':
return `${this.$flavor.textTopic}: ${this.title}`;
case 'module':
return `${this.$flavor.textModule}: ${this.title}`;
default:
return this.title;
}
},
},
};
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import '~styles/helpers';
.back-link {
@include regular-text;
}
.back-link {
@include regular-text;
}
</style>

View File

@ -1,86 +1,93 @@
<template>
<div class="color-chooser">
<div
:class="{ 'color-chooser__color-wrapper--selected': selectedColor === color.name }"
:class="{'color-chooser__color-wrapper--selected': selectedColor === color.name}"
class="color-chooser__color-wrapper"
data-cy="color-select"
v-for="(color, index) in colors"
:key="index"
@click="$emit('input', color.name)"
>
<div :class="'color-chooser__color--' + color.name" class="color-chooser__color">
<tick class="color-chooser__selected-icon" v-if="selectedColor === color.name" />
<div
:class="'color-chooser__color--' + color.name"
class="color-chooser__color"
>
<tick
class="color-chooser__selected-icon"
v-if="selectedColor === color.name"
/>
</div>
</div>
</div>
</template>
<script>
const Tick = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Tick');
import {defineAsyncComponent} from 'vue';
const Tick = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Tick'));
export default {
props: ['selectedColor'],
export default {
props: ['selectedColor'],
components: {
Tick,
},
components: {
Tick
},
data() {
return {
colors: [
{
name: 'yellow',
},
{
name: 'blue',
},
{
name: 'red',
},
{
name: 'green',
},
],
};
},
};
data() {
return {
colors: [
{
name: 'yellow'
},
{
name: 'blue'
},
{
name: 'red'
},
{
name: 'green'
}
]
};
},
};
</script>
<style scoped lang="scss">
@import '@/styles/_variables.scss';
@import '@/styles/_mixins.scss';
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.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;
.color-chooser {
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>

View File

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

View File

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

View File

@ -1,126 +1,140 @@
<template>
<header class="header-bar">
<a class="header-bar__sidebar-link" data-cy="open-sidebar-link" @click.stop="openSidebar('navigation')">
<a
class="header-bar__sidebar-link"
data-cy="open-sidebar-link"
@click.stop="openSidebar('navigation')"
>
<hamburger class="header-bar__sidebar-icon" />
</a>
<content-navigation class="header-bar__content-navigation" />
<div class="user-header">
<a class="user-header__sidebar-link">
<current-class class="user-header__current-class" @click.native.stop="openSidebar('profile')" />
<a
class="user-header__sidebar-link"
>
<current-class
class="user-header__current-class"
@click="openSidebar('profile')"
/>
</a>
<user-widget v-bind="me" data-cy="header-user-widget" @click.native.stop="openSidebar('profile')" />
<user-widget
:avatar-url="me.avatarUrl"
data-cy="header-user-widget"
@click="openSidebar('profile')"
/>
</div>
</header>
</template>
<script>
import ContentNavigation from '@/components/book-navigation/ContentNavigation.vue';
import UserWidget from '@/components/UserWidget.vue';
import CurrentClass from '@/components/school-class/CurrentClass';
import ContentNavigation from '@/components/book-navigation/ContentNavigation.vue';
import UserWidget from '@/components/UserWidget.vue';
import CurrentClass from '@/components/school-class/CurrentClass';
import openSidebar from '@/mixins/open-sidebar';
import me from '@/mixins/me';
import openSidebar from '@/mixins/open-sidebar';
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 {
mixins: [openSidebar, me],
export default {
mixins: [openSidebar, me],
components: {
ContentNavigation,
UserWidget,
CurrentClass,
Hamburger,
},
};
components: {
ContentNavigation,
UserWidget,
CurrentClass,
Hamburger,
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.header-bar {
display: flex;
flex-direction: row;
@supports (display: grid) {
display: grid;
}
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;
.header-bar {
display: flex;
flex-direction: row;
@supports (display: grid) {
display: grid;
}
align-items: center;
justify-content: space-between;
}
background-color: $color-white;
grid-auto-rows: 50px;
width: 100%;
max-width: 100vw;
&__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;
grid-template-columns: 1fr 1fr 1fr;
@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>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,59 +1,68 @@
<template>
<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" />
</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 />
</widget-popover>
</div>
</template>
<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 {
components: {
WidgetPopover,
Ellipses,
},
export default {
components: {
WidgetPopover,
Ellipses
},
data() {
return {
showMenu: false,
};
},
};
data() {
return {
showMenu: false
};
}
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.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;
.more-options {
display: flex;
justify-content: center;
}
justify-content: flex-end;
&__popover {
min-width: 200px;
@include popover-defaults();
&__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;
justify-content: center;
}
&__popover {
min-width: 200px;
@include popover-defaults();
}
}
}
</style>

View File

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

View File

@ -4,14 +4,14 @@
{{ name }}
</div>
<div class="student-submission__entry entry">
<p>{{ submission.text | trimToLength(50) }}</p>
<p>{{ text }}</p>
<p class="entry__document" v-if="submission.document && submission.document.length > 0">
<student-submission-document :document="submission.document" class="entry-document" />
</p>
</div>
<div class="student-submission__feedback entry" v-if="submission.submissionFeedback">
<p :class="{ 'entry__text--final': submission.submissionFeedback.final }" class="entry__text">
{{ submission.submissionFeedback.text | trimToLength(50) }}
{{ feedback }}
</p>
</div>
</div>
@ -25,7 +25,21 @@ export default {
components: {
StudentSubmissionDocument,
},
filters: {
computed: {
text() {
return this.trimToLength(this.submission.text, 50);
},
feedback() {
return this.trimToLength(this.submission.submissionFeedback.text, 50);
},
name() {
return this.submission && this.submission.student
? `${this.submission.student.firstName} ${this.submission.student.lastName}`
: '';
},
},
methods: {
trimToLength: function (text, numberOfChars) {
if (!text) {
return '';
@ -40,14 +54,6 @@ export default {
return `${text.substring(0, index)}`;
},
},
computed: {
name() {
return this.submission && this.submission.student
? `${this.submission.student.firstName} ${this.submission.student.lastName}`
: '';
},
},
};
</script>

View File

@ -1,40 +1,44 @@
<template>
<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>
</p>
</div>
</template>
<script>
import filenameFromUrl from '@/helpers/urls';
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon');
import {defineAsyncComponent} from 'vue';
import filenameFromUrl from '@/helpers/urls';
const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
export default {
name: 'StudentSubmissionDocument',
props: ['document'],
components: { DocumentIcon },
export default {
name: 'StudentSubmissionDocument',
props: ['document'],
components: { DocumentIcon },
computed: {
filename() {
return filenameFromUrl(this.document);
computed: {
filename() {
return filenameFromUrl(this.document);
}
},
},
};
};
</script>
<style scoped lang="scss">
.content {
display: flex;
.content {
display: flex;
&__icon {
width: 25px;
align-self: center;
}
&__icon {
width: 25px;
align-self: center;
}
&__text {
align-self: center;
padding-left: 5px;
&__text {
align-self: center;
padding-left: 5px;
}
}
}
</style>

View File

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

View File

@ -1,18 +1,23 @@
<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__item">
<router-link
:class="{ 'content-navigation__link--active': isActive('book') }"
:class="{'content-navigation__link--active': isActive('book')}"
:to="topicRoute"
active-class="content-navigation__link--active"
class="content-navigation__link"
@click.native="close"
@click="close"
>
{{ $flavor.textTopics }}
</router-link>
<topic-navigation v-if="isSidebar" />
<topic-navigation
v-if="isSidebar"
/>
</div>
<div class="content-navigation__item">
@ -20,7 +25,7 @@
to="/instruments"
active-class="content-navigation__link--active"
class="content-navigation__link"
@click.native="close"
@click="close"
>
{{ $flavor.textInstruments }}
</router-link>
@ -28,52 +33,63 @@
<div class="content-navigation__item">
<router-link
:to="{ name: 'news' }"
:to="{name: 'news'}"
active-class="content-navigation__link--active"
class="content-navigation__link"
data-cy="news-navigation-link"
v-if="!me.readOnly"
@click.native="close"
@click="close"
>
News
</router-link>
</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" />
</router-link>
<div class="content-navigation__secondary">
<div class="content-navigation__item content-navigation__item--secondary">
<router-link
:class="{ 'content-navigation__link--active': isRoomUrl() }"
:class="{'content-navigation__link--active': isRoomUrl()}"
to="/rooms"
active-class="content-navigation__link--active"
class="content-navigation__link content-navigation__link--secondary"
@click.native="close"
@click="close"
>
Räume
</router-link>
</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
to="/portfolio"
active-class="content-navigation__link--active"
class="content-navigation__link content-navigation__link--secondary"
@click.native="close"
@click="close"
>
Portfolio
</router-link>
</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
:href="$flavor.supportLink"
target="_blank"
class="content-navigation__link content-navigation__link--secondary"
@click="close"
>Support
>Support
</a>
</div>
</div>
@ -81,142 +97,142 @@
</template>
<script>
import TopicNavigation from '@/components/book-navigation/TopicNavigation';
import TopicNavigation from '@/components/book-navigation/TopicNavigation';
import sidebarMixin from '@/mixins/sidebar';
import meMixin from '@/mixins/me';
import sidebarMixin from '@/mixins/sidebar';
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 {
props: {
isSidebar: {
default: false,
export default {
props: {
isSidebar: {
default: false
}
},
},
mixins: [sidebarMixin, meMixin],
mixins: [sidebarMixin, meMixin],
components: {
TopicNavigation,
Logo,
},
components: {
TopicNavigation,
Logo
},
computed: {
showPortfolio() {
return this.$flavor.showPortfolio;
computed: {
showPortfolio() {
return this.$flavor.showPortfolio;
}
},
},
methods: {
isActive(linkName) {
return linkName === 'book' && this.$route.path.indexOf('module') > -1;
},
isRoomUrl() {
return this.$route.path.indexOf('room') > -1;
},
close() {
this.closeSidebar('navigation');
},
},
};
methods: {
isActive(linkName) {
return linkName === 'book' && this.$route.path.indexOf('module') > -1;
},
isRoomUrl() {
return this.$route.path.indexOf('room') > -1;
},
close() {
this.closeSidebar('navigation');
}
}
};
</script>
<style scoped lang="scss">
@import '@/styles/_variables.scss';
@import '@/styles/_mixins.scss';
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.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;
.content-navigation {
display: flex;
justify-self: center;
align-items: center;
/*
* For IE10+
*/
-ms-grid-column: 2;
-ms-grid-row-align: center;
-ms-grid-column-align: center;
}
&__logo-icon {
width: auto;
height: 31px;
}
&__link {
&--secondary {
@include regular-text;
&__link {
padding: 0 24px;
@include navigation-link;
}
&--active {
color: $color-brand;
}
}
&__primary, &__secondary {
display: none;
flex-direction: row;
$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;
@include desktop {
display: flex;
}
}
#{$parent}__item {
width: 100%;
//border-bottom: 1px solid $color-white;
&__logo {
color: #17A887;
font-size: 36px;
font-weight: 800;
font-family: $sans-serif-font-family;
display: flex;
justify-self: center;
/*&:nth-child(1) {*/
/* order: 3;*/
/* border-bottom: 0;*/
/*}*/
/*
* For IE10+
*/
-ms-grid-column: 2;
-ms-grid-row-align: center;
-ms-grid-column-align: center;
}
/*&:nth-child(2) {*/
/* order: 1;*/
/*}*/
&__logo-icon {
width: auto;
height: 31px;
}
/*&:nth-child(3) {*/
/* order: 2;*/
/*}*/
&__link {
&--secondary {
@include regular-text;
}
&--active {
color: $color-brand;
}
}
$parent: &;
&--sidebar {
flex-direction: column;
#{$parent}__primary, #{$parent}__secondary {
display: flex;
flex-direction: column;
width: 100%;
}
#{$parent}__link {
@include heading-4;
line-height: 2.5em;
padding: 0;
display: block;
margin-bottom: 0.5*$small-spacing;
&:only-child {
margin-bottom: 0;
}
}
#{$parent}__item {
width: 100%;
//border-bottom: 1px solid $color-white;
/*&:nth-child(1) {*/
/* order: 3;*/
/* border-bottom: 0;*/
/*}*/
/*&:nth-child(2) {*/
/* order: 1;*/
/*}*/
/*&:nth-child(3) {*/
/* order: 2;*/
/*}*/
}
}
}
}
</style>

View File

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

View File

@ -1,44 +1,55 @@
<template>
<div :class="{ 'sub-navigation-item--active': show }" class="sub-navigation-item" v-click-outside="close">
<div class="sub-navigation-item__title" @click="show = !show">
<div
:class="{ 'sub-navigation-item--active': show}"
class="sub-navigation-item"
v-click-outside="close"
>
<div
class="sub-navigation-item__title"
@click="show = !show"
>
{{ title }}
<chevron-down class="sub-navigation-item__icon sub-navigation-item__chevron-down" />
<chevron-up class="sub-navigation-item__icon sub-navigation-item__chevron-up" />
</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 />
</div>
</div>
</template>
<script>
const ChevronDown = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronDown');
const ChevronUp = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronUp');
import {defineAsyncComponent} from 'vue';
const ChevronDown = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown'));
const ChevronUp = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronUp'));
export default {
props: ['title'],
export default {
props: ['title'],
components: {
ChevronDown,
ChevronUp,
},
data() {
return {
show: false,
};
},
watch: {
$route() {
this.show = false;
components: {
ChevronDown,
ChevronUp
},
},
methods: {
close() {
this.show = false;
data() {
return {
show: false
};
},
},
};
watch: {
$route() {
this.show = false;
}
},
methods: {
close() {
this.show = false;
}
}
};
</script>

View File

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

View File

@ -105,7 +105,7 @@
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
import { PropType, defineComponent } from 'vue';
import Toggle from '@/components/ui/Toggle.vue';
import ContentFormSection from '@/components/content-block-form/ContentFormSection.vue';
import InputWithLabel from '@/components/ui/InputWithLabel.vue';
@ -130,7 +130,7 @@ interface ContentBlockFormData {
localContentBlock: any;
}
export default Vue.extend({
export default defineComponent({
props: {
title: {
type: String,

View File

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

View File

@ -55,7 +55,7 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import WidgetPopover from '@/components/ui/WidgetPopover.vue';
import Ellipses from '@/components/icons/Ellipses.vue';
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText.vue';
@ -65,7 +65,7 @@ interface Data {
show: boolean;
}
export default Vue.extend({
export default defineComponent({
props: {
actions: {
type: Object as () => ActionOptions,

View File

@ -1,8 +1,13 @@
<template>
<div class="content-form-section">
<h2 class="content-form-section__heading">
<component class="content-form-section__icon" :is="icon" />
<span class="content-form-section__title" data-cy="content-form-section-title">{{ title }}</span>
<component
class="content-form-section__icon"
:is="icon"
/> <span
class="content-form-section__title"
data-cy="content-form-section-title"
>{{ title }}</span>
</h2>
<content-element-actions
@ -22,73 +27,74 @@
</template>
<script lang="ts">
import formElementIcons from '@/components/ui/form-element-icons.js';
import ContentElementActions from '@/components/content-block-form/ContentElementActions.vue';
import { ActionOptions } from '@/@types';
import {defineComponent} from "vue";
import formElementIcons from '@/components/ui/form-element-icons.js';
import ContentElementActions from '@/components/content-block-form/ContentElementActions.vue';
import {ActionOptions} from "@/@types";
export default {
props: {
title: {
type: String,
default: '',
export default defineComponent({
props: {
title: {
type: String,
default: ''
},
icon: {
type: String,
default: ''
},
actions: {
type: Object as () => ActionOptions,
default: () => {}
}
},
icon: {
type: String,
default: '',
},
actions: {
type: Object as () => ActionOptions,
default: () => {},
},
},
components: {
ContentElementActions,
...formElementIcons,
},
};
components: {
ContentElementActions,
...formElementIcons
}
});
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import '~styles/helpers';
.content-form-section {
@include default-box-shadow;
border-radius: $default-border-radius;
padding: $small-spacing $medium-spacing;
margin-bottom: $medium-spacing;
.content-form-section {
@include default-box-shadow;
border-radius: $default-border-radius;
padding: $small-spacing $medium-spacing;
margin-bottom: $medium-spacing;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
grid-template-areas: 'h a' 'c c';
align-items: center;
grid-row-gap: $medium-spacing;
&__heading {
display: flex;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
grid-template-areas: 'h a' 'c c';
align-items: center;
grid-area: h;
margin-bottom: 0;
}
grid-row-gap: $medium-spacing;
&__actions {
grid-area: a;
justify-self: end;
}
&__heading {
display: flex;
align-items: center;
grid-area: h;
margin-bottom: 0;
}
&__title {
@include heading-3;
margin-bottom: 0;
}
&__actions {
grid-area: a;
justify-self: end;
}
&__icon {
width: 28px;
height: 28px;
margin-right: $small-spacing;
}
&__title {
@include heading-3;
margin-bottom: 0;
}
&__content {
grid-area: c;
&__icon {
width: 28px;
height: 28px;
margin-right: $small-spacing;
}
&__content {
grid-area: c;
}
}
}
</style>

View File

@ -18,150 +18,167 @@
/>
</template>
<add-content-element :index="-1" 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="-1"
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>
<template #footer>
<div>
<a
:class="{ 'button--disabled': disableSave }"
:class="{'button--disabled': disableSave}"
class="button button--primary"
data-cy="modal-save-button"
@click="save"
>Speichern</a
>
<a class="button" @click="$emit('hide')">Abbrechen</a>
>Speichern</a>
<a
class="button"
@click="$emit('hide')"
>Abbrechen</a>
</div>
</template>
</modal>
</template>
<script>
import { meQuery } from '@/graphql/queries';
import {defineAsyncComponent} from 'vue';
import {meQuery} from '@/graphql/queries';
const ModalInput = () => import(/* webpackChunkName: "content-forms" */ '@/components/ModalInput');
const AddContentElement = () => import(/* webpackChunkName: "content-forms" */ '@/components/AddContentElement');
const ContentElement = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/ContentElement');
const ModalInput = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/ModalInput'));
const AddContentElement = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/AddContentElement'));
const ContentElement = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-block-form/ContentElement'));
const Modal = () => import('@/components/Modal.vue');
const Checkbox = () => import('@/components/ui/Checkbox.vue');
const Modal = defineAsyncComponent(() => import('@/components/Modal'));
const Checkbox = defineAsyncComponent(() => import('@/components/ui/Checkbox'));
export default {
props: {
contentBlock: Object,
blockType: {
type: String,
default: 'ContentBlock',
export default {
props: {
contentBlock: Object,
blockType: {
type: String,
default: 'ContentBlock',
},
showTaskSelection: {
type: Boolean,
default: false,
},
disableSave: {
type: Boolean,
default: false,
},
},
showTaskSelection: {
type: Boolean,
default: false,
},
disableSave: {
type: Boolean,
default: false,
},
},
components: {
ContentElement,
Modal,
ModalInput,
AddContentElement,
Checkbox,
},
components: {
ContentElement,
Modal,
ModalInput,
AddContentElement,
Checkbox,
},
data() {
return {
error: false,
localContentBlock: Object.assign(
{},
{
data() {
return {
error: false,
localContentBlock: Object.assign({}, {
title: this.contentBlock.title,
contents: [...this.contentBlock.contents],
id: this.contentBlock.id || undefined,
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>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.contents-form {
/* top level does not exist, because of the modal */
.contents-form {
/* 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>

View File

@ -1,75 +1,77 @@
<template>
<contents-form :content-block="contentBlock" :show-task-selection="true" @save="saveContentBlock" @hide="hideModal" />
<contents-form
:content-block="contentBlock"
:show-task-selection="true"
@save="saveContentBlock"
@hide="hideModal"
/>
</template>
<script>
import ContentsForm from '@/components/content-block-form/ContentsForm';
import ContentsForm from '@/components/content-block-form/ContentsForm';
import store from '@/store/index';
import {store} from '@/store';
import EDIT_CONTENT_BLOCK_MUTATION from 'gql/mutations/editContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import CONTENT_BLOCK_QUERY from '@/graphql/gql/queries/contentBlockQuery.gql';
import { setUserBlockType } from '@/helpers/content-block';
import EDIT_CONTENT_BLOCK_MUTATION from 'gql/mutations/editContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import CONTENT_BLOCK_QUERY from '@/graphql/gql/queries/contentBlockQuery.gql';
import {setUserBlockType} from '@/helpers/content-block';
export default {
components: {
ContentsForm,
},
data() {
return {
contentBlock: {},
};
},
created() {
// debugger;
},
methods: {
hideModal() {
this.$store.dispatch('resetCurrentNoteBlock');
this.$store.dispatch('hideModal');
export default {
components: {
ContentsForm,
},
saveContentBlock(contentBlock) {
this.$apollo
.mutate({
data() {
return {
contentBlock: {},
};
},
created() {
// debugger;
},
methods: {
hideModal() {
this.$store.dispatch('resetCurrentNoteBlock');
this.$store.dispatch('hideModal');
},
saveContentBlock(contentBlock) {
this.$apollo.mutate({
mutation: EDIT_CONTENT_BLOCK_MUTATION,
variables: {
input: {
contentBlock: {
title: contentBlock.title,
contents: contentBlock.contents.filter((value) => Object.keys(value).length > 0),
contents: contentBlock.contents.filter(value => Object.keys(value).length > 0),
type: setUserBlockType(contentBlock.isAssignment),
},
id: contentBlock.id,
},
},
refetchQueries: [
{
query: MODULE_DETAILS_QUERY,
variables: {
slug: this.$route.params.slug,
},
refetchQueries: [{
query: MODULE_DETAILS_QUERY,
variables: {
slug: this.$route.params.slug,
},
],
})
.then(() => {
}],
}).then(() => {
this.hideModal();
});
},
},
},
apollo: {
contentBlock() {
return {
query: CONTENT_BLOCK_QUERY,
variables: {
id: store.state.currentNoteBlock,
},
};
apollo: {
contentBlock() {
return {
query: CONTENT_BLOCK_QUERY,
variables: {
id: store.state.currentNoteBlock,
},
};
},
},
},
};
};
</script>

View File

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

View File

@ -1,40 +1,43 @@
<template>
<content-list :items="contentBlocks">
<content-list
:items="contentBlocks"
>
<template #default="{ item }">
<content-block :content-block="item" :parent="parent" />
<content-block
:content-block="item"
:parent="parent"
/>
</template>
</content-list>
</template>
<script>
import ContentList from '@/components/content-blocks/ContentList.vue';
export default {
name: 'ContentBlockList',
props: ['contents', 'parent'],
import {defineAsyncComponent} from 'vue';
const ContentList = defineAsyncComponent(() => import('@/components/content-blocks/ContentList'));
const ContentBlock = defineAsyncComponent(() => import('@/components/ContentBlock'));
components: {
ContentList,
// https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components
ContentBlock: () => import('@/components/ContentBlock.vue'),
},
export default {
name: 'ContentBlockList',
props: ['contents', 'parent'],
computed: {
contentBlocks() {
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,
});
});
components: {
ContentList,
ContentBlock
},
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
</style>
computed: {
contentBlocks() {
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>
<div class="document-block">
<document-icon class="document-block__icon" />
<a :href="value.url" class="document-block__link" target="_blank">{{ urlName }}</a>
<a class="document-block__remove" v-if="showTrashIcon" @click="$emit('trash')">
<a
: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" />
</a>
</div>
</template>
<script>
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon');
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon');
import {defineAsyncComponent} from 'vue';
const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
export default {
props: {
value: Object,
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;
export default {
props: {
value: Object,
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;
}
}
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.document-block {
display: grid;
grid-template-columns: 50px 1fr 50px;
align-items: center;
&__icon {
width: 30px;
height: 30px;
}
&__link {
text-decoration: underline;
}
&__remove {
display: flex;
justify-content: center;
.document-block {
display: grid;
grid-template-columns: 50px 1fr 50px;
align-items: center;
width: 50px;
height: 50px;
}
&__trash-icon {
width: 25px;
height: 25px;
fill: $color-silver-dark;
cursor: pointer;
justify-self: center;
&__icon {
width: 30px;
height: 30px;
}
&__link {
text-decoration: underline;
}
&__remove {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
}
&__trash-icon {
width: 25px;
height: 25px;
fill: $color-silver-dark;
cursor: pointer;
justify-self: center;
}
}
}
</style>

View File

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

View File

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

View File

@ -1,95 +1,114 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<div class="solution" data-cy="solution">
<a class="solution__toggle" data-cy="show-solution" @click="toggle"
>Lösung
<div
class="solution"
data-cy="solution"
>
<a
class="solution__toggle"
data-cy="show-solution"
@click="toggle"
>Lösung
<template v-if="!visible">anzeigen</template>
<template v-else>ausblenden</template>
</a>
<transition name="fade">
<div class="solution__hidden fade" v-if="visible">
<p class="solution__text solution-text" data-cy="solution-text" v-html="sanitizedText" />
<cms-document-block :solution="true" class="solution__document" :value="value.document" v-if="value.document" />
<div
class="solution__hidden fade"
v-if="visible"
>
<p
class="solution__text solution-text"
data-cy="solution-text"
v-html="sanitizedText"
/>
<cms-document-block
:solution="true"
class="solution__document"
:value="value.document"
v-if="value.document"
/>
</div>
</transition>
</div>
</template>
<script>
import { sanitizeAsHtml } from '@/helpers/text';
import CmsDocumentBlock from '@/components/content-blocks/CmsDocumentBlock';
import {sanitizeAsHtml} from '@/helpers/text';
import CmsDocumentBlock from '@/components/content-blocks/CmsDocumentBlock';
export default {
props: ['value'],
components: { CmsDocumentBlock },
export default {
props: ['value'],
components: {CmsDocumentBlock},
data() {
return {
visible: false,
};
},
computed: {
sanitizedText() {
return sanitizeAsHtml(this.value.text);
data() {
return {
visible: false,
};
},
},
methods: {
toggle() {
this.visible = !this.visible;
computed: {
sanitizedText() {
return sanitizeAsHtml(this.value.text);
},
},
},
};
methods: {
toggle() {
this.visible = !this.visible;
},
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.solution {
display: grid;
grid-auto-rows: auto;
grid-row-gap: 15px;
.solution {
display: grid;
grid-auto-rows: auto;
grid-row-gap: 15px;
margin-bottom: 1rem;
margin-bottom: 1rem;
&__toggle {
font-family: $sans-serif-font-family;
color: $color-silver-dark;
font-size: toRem(15px);
/*margin-bottom: 15px;*/
display: block;
cursor: pointer;
font-weight: $font-weight-regular;
}
&__text {
font-size: toRem(18px);
color: $color-silver-dark;
:deep(p) {
font-size: toRem(18px);
&__toggle {
font-family: $sans-serif-font-family;
color: $color-silver-dark;
font-size: toRem(15px);
/*margin-bottom: 15px;*/
display: block;
cursor: pointer;
font-weight: $font-weight-regular;
}
:deep(ul) {
padding-left: $medium-spacing;
&__text {
font-size: toRem(18px);
color: $color-silver-dark;
> li {
list-style: disc outside none;
/deep/ p {
font-size: toRem(18px);
color: $color-silver-dark;
}
/deep/ ul {
padding-left: $medium-spacing;
> li {
list-style: disc outside none;
color: $color-silver-dark;
}
}
}
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity .3s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
.fade-enter-from,
.fade-leave-active {
opacity: 0;
}
</style>

View File

@ -1,9 +1,19 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<div :data-scrollto="value.id" class="assignment">
<p class="assignment__main-text" data-cy="assignment-main-text" v-html="assignment.assignment" />
<div
: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">
<submission-form
@ -23,92 +33,101 @@
@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 v-if="!isStudent">
<router-link :to="{ name: 'submissions', params: { id: assignment.id } }" class="button button--primary">
Zu den Ergebnissen
<router-link
:to="{name: 'submissions', params: { id: assignment.id }}"
class="button button--primary"
>
Zu den
Ergebnissen
</router-link>
</template>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import ASSIGNMENT_QUERY from '@/graphql/gql/queries/assignmentQuery.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import UPDATE_ASSIGNMENT_MUTATION from '@/graphql/gql/mutations/updateAssignmentMutation.gql';
import UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS from '@/graphql/gql/mutations/updateAssignmentMutationWithSuccess.gql';
import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql';
import debounce from 'lodash/debounce';
import cloneDeep from 'lodash/cloneDeep';
import { sanitize } from '@/helpers/text';
import {mapActions, mapGetters} from 'vuex';
import ASSIGNMENT_QUERY from '@/graphql/gql/queries/assignmentQuery.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import UPDATE_ASSIGNMENT_MUTATION from '@/graphql/gql/mutations/updateAssignmentMutation.gql';
import UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS from '@/graphql/gql/mutations/updateAssignmentMutationWithSuccess.gql';
import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql';
import debounce from 'lodash/debounce';
import cloneDeep from 'lodash/cloneDeep';
import {sanitize} from '@/helpers/text';
import {defineAsyncComponent} from 'vue';
const SubmissionForm = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/SubmissionForm');
const Solution = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/Solution');
const SpellCheck = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/SpellCheck');
const SubmissionForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SubmissionForm'));
const Solution = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Solution'));
const SpellCheck = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SpellCheck'));
export default {
props: ['value'],
export default {
props: ['value'],
components: {
Solution,
SubmissionForm,
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;
components: {
Solution,
SubmissionForm,
SpellCheck,
},
submission() {
return this.assignment.submission ? this.assignment.submission : {};
},
isStudent() {
return !this.me.permissions.includes('users.can_manage_school_class_content');
},
solution() {
data() {
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: {
...mapActions(['scrollToAssignmentReady']),
_save: debounce(function (submission) {
this.saving++;
this.$apollo
.mutate({
computed: {
...mapGetters(['scrollToAssignmentId']),
final() {
return !!this.submission && this.submission.final;
},
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,
variables: {
input: {
@ -119,14 +138,7 @@ export default {
},
},
},
update(
store,
{
data: {
updateAssignment: { successful, updatedAssignment },
},
}
) {
update(store, {data: {updateAssignment: {successful, updatedAssignment}}}) {
try {
if (successful) {
const query = ASSIGNMENT_QUERY;
@ -137,82 +149,80 @@ export default {
submission,
});
const data = {
assignment,
assignment
};
store.writeQuery({ query, variables, data });
store.writeQuery({query, variables, data});
}
} catch (e) {
console.error(e);
// Query did not exist in the cache, and apollo throws a generic Error. Do nothing
}
},
})
.then(() => {
}).then(() => {
this.saving--;
if (this.saving === 0) {
this.unsaved = false;
}
});
}, 500),
saveInput: function (answer) {
// reset corrections on input
this.corrections = '';
this.unsaved = true;
/*
}, 500),
saveInput: function (answer) {
// reset corrections on input
this.corrections = '';
this.unsaved = true;
/*
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
*/
this.assignment.submission.text = answer;
this._save(this.assignment.submission);
},
changeDocumentUrl(documentUrl) {
this.assignment.submission.document = documentUrl;
this._save(this.assignment.submission);
},
turnIn() {
// reset corrections on turn in
this.corrections = '';
this.$apollo.mutate({
mutation: UPDATE_ASSIGNMENT_MUTATION,
variables: {
input: {
assignment: {
id: this.assignment.id,
answer: this.assignment.submission.text,
document: this.assignment.submission.document,
final: true,
this.assignment.submission.text = answer;
this._save(this.assignment.submission);
},
changeDocumentUrl(documentUrl) {
this.assignment.submission.document = documentUrl;
this._save(this.assignment.submission);
},
turnIn() {
// reset corrections on turn in
this.corrections = '';
this.$apollo.mutate({
mutation: UPDATE_ASSIGNMENT_MUTATION,
variables: {
input: {
assignment: {
id: this.assignment.id,
answer: this.assignment.submission.text,
document: this.assignment.submission.document,
final: true,
},
},
},
},
});
},
reopen() {
this.$apollo.mutate({
mutation: UPDATE_ASSIGNMENT_MUTATION,
variables: {
input: {
assignment: {
id: this.assignment.id,
answer: this.assignment.submission.text,
document: this.assignment.submission.document,
final: false,
});
},
reopen() {
this.$apollo.mutate({
mutation: UPDATE_ASSIGNMENT_MUTATION,
variables: {
input: {
assignment: {
id: this.assignment.id,
answer: this.assignment.submission.text,
document: this.assignment.submission.document,
final: false,
},
},
},
},
});
},
initialSubmission() {
return {
text: '',
document: '',
final: false,
};
},
spellcheck() {
let self = this;
this.spellcheckLoading = true;
this.$apollo
.mutate({
});
},
initialSubmission() {
return {
text: '',
document: '',
final: false,
};
},
spellcheck() {
let self = this;
this.spellcheckLoading = true;
this.$apollo.mutate({
mutation: SPELL_CHECK_MUTATION,
variables: {
input: {
@ -220,96 +230,90 @@ export default {
text: this.assignment.submission.text,
},
},
update(
store,
{
data: {
spellCheck: { results },
},
}
) {
update(store, {data: {spellCheck: {results}}}) {
self.corrections = results;
},
})
.then(() => {
}).then(() => {
this.spellcheckLoading = false;
});
},
},
},
apollo: {
assignment: {
query: ASSIGNMENT_QUERY,
variables() {
return {
id: this.value.id,
};
apollo: {
assignment: {
query: ASSIGNMENT_QUERY,
variables() {
return {
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) {
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));
}
me: {
query: ME_QUERY,
},
},
me: {
query: ME_QUERY,
},
},
};
};
</script>
<style scoped lang="scss">
@import '@/styles/_variables.scss';
@import '@/styles/_functions.scss';
@import '@/styles/_mixins.scss';
@import '@/styles/_variables.scss';
@import '@/styles/_functions.scss';
@import '@/styles/_mixins.scss';
.assignment {
margin-bottom: 3rem;
position: relative;
.assignment {
margin-bottom: 3rem;
position: relative;
&__title {
font-size: toRem(17px);
margin-bottom: 1rem;
}
&__main-text {
:deep(ul) {
@include list-parent;
&__title {
font-size: toRem(17px);
margin-bottom: 1rem;
}
:deep(li) {
@include list-child;
&__main-text {
/deep/ ul{
@include list-parent
}
/deep/ li {
@include list-child;
}
}
}
&__toggle-input-container {
display: flex;
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-container {
display: flex;
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;
}
}
&__feedback {
@include regular-text;
}
}
&__feedback {
@include regular-text;
}
}
</style>

View File

@ -1,99 +1,109 @@
<template>
<div 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"
data-cy="final-submission"
>
<document-block
:value="{url: userInput.document}"
class="final-submission__document"
v-if="userInput.document"
/>
<div class="final-submission__explanation">
<info-icon class="final-submission__explanation-icon" />
<span class="final-submission__explanation-text">{{ sharedMsg }}</span>
<a class="final-submission__reopen" data-cy="final-submission-reopen" v-if="showReopen" @click="$emit('reopen')"
>Bearbeiten</a
>
<a
class="final-submission__reopen"
data-cy="final-submission-reopen"
v-if="showReopen"
@click="$emit('reopen')"
>Bearbeiten</a>
</div>
</div>
</template>
<script>
import { newLineToParagraph } from '@/helpers/text';
const DocumentBlock = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/DocumentBlock');
import {newLineToParagraph} from '@/helpers/text';
import {defineAsyncComponent} from 'vue';
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 {
props: {
userInput: {
type: Object,
default: () => ({}),
export default {
props: {
userInput: {
type: Object,
default: () => ({})
},
showReopen: {
type: Boolean,
default: true
},
sharedMsg: {
type: String,
default: ''
}
},
showReopen: {
type: Boolean,
default: true,
},
sharedMsg: {
type: String,
default: '',
},
},
components: {
InfoIcon,
DocumentBlock,
},
computed: {
text() {
return newLineToParagraph(this.userInput.text);
components: {
InfoIcon,
DocumentBlock,
},
},
};
computed: {
text() {
return newLineToParagraph(this.userInput.text);
}
}
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.final-submission {
&__text {
background-color: $color-white;
@include input-box-shadow;
border-radius: $input-border-radius;
padding: 15px;
font-size: toRem(17px);
font-family: $sans-serif-font-family;
margin-bottom: 20px;
font-weight: $font-weight-regular;
.final-submission {
&__text {
background-color: $color-white;
@include input-box-shadow;
border-radius: $input-border-radius;
padding: 15px;
font-size: toRem(17px);
font-family: $sans-serif-font-family;
margin-bottom: 20px;
font-weight: $font-weight-regular;
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
word-break: break-word;
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
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>

View File

@ -10,7 +10,10 @@
/>
</div>
<div class="submission-form-container__actions" v-if="!isFinalOrReadOnly">
<div
class="submission-form-container__actions"
v-if="!isFinalOrReadOnly"
>
<button
class="submission-form-container__submit button button--primary button--white-bg"
data-cy="submission-form-submit"
@ -26,7 +29,11 @@
>
{{ spellcheckText }}
</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 />
</div>
@ -41,112 +48,116 @@
</template>
<script>
const SubmissionInput = () => import('@/components/content-blocks/assignment/SubmissionInput.vue');
const FinalSubmission = () => import('@/components/content-blocks/assignment/FinalSubmission.vue');
const FileUpload = () => import('@/components/ui/file-upload/FileUpload.vue');
import {defineAsyncComponent} from 'vue';
const SubmissionInput = defineAsyncComponent(() => import('@/components/content-blocks/assignment/SubmissionInput'));
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: {
FileUpload,
SubmissionInput,
FinalSubmission,
},
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,
},
computed: {
final() {
return !!this.userInput && this.userInput.final;
components: {
FileUpload,
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: {
reopenSubmission() {
this.$emit('reopen');
computed: {
final() {
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>
<style scoped lang="scss">
@import '~styles/helpers';
@import '~styles/helpers';
.submission-form-container {
@include form-with-border;
.submission-form-container {
@include form-with-border;
margin-bottom: $medium-spacing;
margin-bottom: $medium-spacing;
display: none;
@include desktop {
display: block;
}
display: none;
@include desktop {
display: block;
}
&__inputs {
margin-bottom: 12px;
}
&__inputs {
margin-bottom: 12px;
}
&__submit {
margin-right: $medium-spacing;
}
&__submit {
margin-right: $medium-spacing;
}
&__actions {
display: flex;
align-items: center;
}
&__actions {
display: flex;
align-items: center;
}
&__document {
&:hover {
cursor: pointer;
&__document {
&:hover {
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>

View File

@ -4,68 +4,75 @@
:placeholder="placeholder"
:readonly="readonly"
:value="inputText"
:class="{ 'submission-form__textarea--readonly': readonly }"
:class="{'submission-form__textarea--readonly': readonly}"
data-cy="submission-textarea"
rows="1"
class="submission-form__textarea"
v-auto-grow
@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" />
</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" />
</div>
</div>
</template>
<script>
const TickCircleIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TickCircleIcon');
const LoadingIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/LoadingIcon');
import {defineAsyncComponent} from 'vue';
const TickCircleIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TickCircleIcon'));
const LoadingIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/LoadingIcon'));
export default {
props: {
inputText: String,
saved: Boolean,
readonly: Boolean,
placeholder: {
type: String,
default: 'Ergebnis erfassen',
export default {
props: {
inputText: String,
saved: Boolean,
readonly: Boolean,
placeholder: {
type: String,
default: 'Ergebnis erfassen'
}
},
},
components: {
TickCircleIcon,
LoadingIcon,
},
};
components: {
TickCircleIcon,
LoadingIcon
}
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.submission-form {
display: flex;
flex-direction: row;
justify-content: space-between;
.submission-form {
display: flex;
flex-direction: row;
justify-content: space-between;
&__textarea {
@include borderless-textarea;
&__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>

View File

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

View File

@ -1,119 +1,133 @@
<template>
<div class="document-form" ref="documentform">
<div v-if="!value.url" ref="uploadcare-panel" />
<div class="document-form__spinner" v-if="loading">
<div
class="document-form"
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" />
</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" />
<a :href="previewUrl" class="document-form__link" target="_blank">{{ previewLink }}</a>
<a
:href="previewUrl"
class="document-form__link"
target="_blank"
>{{ previewLink }}</a>
</div>
</div>
</template>
<script>
import { uploadcare } from '@/helpers/uploadcare';
import LoadingIcon from '@/components/icons/LoadingIcon';
import {uploadcare} from '@/helpers/uploadcare';
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 {
props: ['value', 'index'],
export default {
props: ['value', 'index'],
components: {
LoadingIcon,
DocumentIcon,
},
data() {
return {
loading: false,
};
},
computed: {
previewUrl() {
if (this.value && this.value.url) {
return this.value.url;
}
return null;
components: {
LoadingIcon,
DocumentIcon,
},
previewLink() {
if (this.value && this.value.url) {
const parts = this.value.url.split('/');
return parts[parts.length - 1];
}
return '';
},
},
mounted() {
uploadcare(
this,
(url) => {
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() {
uploadcare(this, url => {
this.$emit('change-url', url, this.index);
this.loading = false;
},
() => {
}, () => {
this.loading = true;
}
);
},
};
});
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.document-form {
&__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;
.document-form {
&__uploaded {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
}
&__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;
width: 100%;
justify-content: center;
align-items: center;
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
text-decoration: underline;
}
}
}
}
</style>

View File

@ -1,130 +1,147 @@
<template>
<div class="tip-tap">
<editor-content class="tip-tap__editor-wrapper" :editor="editor" />
<editor-content
class="tip-tap__editor-wrapper"
:editor="editor"
/>
<toggle :bordered="false" :checked="isList" label="Als Liste formatieren" @input="toggleList" />
<toggle
:bordered="false"
:checked="isList"
label="Als Liste formatieren"
@input="toggleList"
/>
</div>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
import { Editor, EditorContent } from '@tiptap/vue-2';
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import BulletList from '@tiptap/extension-bullet-list';
import ListItem from '@tiptap/extension-list-item';
import Toggle from '@/components/ui/Toggle.vue';
import {PropType, defineComponent} from 'vue';
import {Editor, EditorContent} from "@tiptap/vue-3";
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import BulletList from '@tiptap/extension-bullet-list';
import ListItem from '@tiptap/extension-list-item';
import Toggle from "@/components/ui/Toggle.vue";
interface Data {
editor: Editor | undefined;
}
interface Value {
text: string;
}
interface Data {
editor: Editor | undefined;
}
interface Value {
text: string;
}
export default Vue.extend({
props: {
value: {
type: Object as PropType<Value>,
validator(value: Value) {
return Object.prototype.hasOwnProperty.call(value, 'text');
},
},
},
components: {
Toggle,
EditorContent,
},
data(): Data {
return {
editor: undefined,
};
},
computed: {
isList(): boolean {
return this.editor?.isActive('bulletList') || false;
},
text(): string {
return this.value.text;
},
},
watch: {
value({ text }: Value) {
const editor = this.editor as Editor; // editor is always initialized on mount, cast it
const isSame = editor.getHTML() === text;
if (isSame) {
return;
}
editor.commands.setContent(text, false);
},
},
mounted() {
this.editor = new Editor({
editorProps: {
attributes: {
class: 'tip-tap__editor',
export default defineComponent({
props: {
value: {
type: Object as PropType<Value>,
validator: (value: Value) => {
return Object.prototype.hasOwnProperty.call(value, 'text');
},
},
content: this.text,
extensions: [Document, Paragraph, Text, BulletList, ListItem],
onUpdate: () => {
const text = (this.editor as Editor).getHTML();
this.$emit('input', text);
this.$emit('change-text', text);
},
});
},
beforeDestroy() {
this.editor?.destroy();
},
methods: {
toggleList() {
const editor = this.editor as Editor;
editor.chain().selectAll().toggleBulletList().run();
},
},
});
components: {
Toggle,
EditorContent
},
data(): Data {
return {
editor: undefined,
};
},
computed: {
isList(): boolean {
return this.editor?.isActive('bulletList') || false;
},
text(): string {
return this.value?.text || '';
}
},
watch: {
value({text}: Value) {
const editor = this.editor as Editor; // editor is always initialized on mount, cast it
const isSame = editor.getHTML() === text;
if (isSame) {
return;
}
editor.commands.setContent(text, false);
}
},
mounted() {
this.editor = new Editor({
editorProps: {
attributes: {
class: 'tip-tap__editor'
}
},
content: this.text,
extensions: [
Document,
Paragraph,
Text,
BulletList,
ListItem
],
onUpdate: () => {
const text=(this.editor as Editor).getHTML();
this.$emit('input', text);
this.$emit('change-text', text);
}
});
},
beforeUnmount() {
this.editor?.destroy();
},
methods: {
toggleList() {
const editor = this.editor as Editor;
editor.chain().selectAll().toggleBulletList().run();
},
}
});
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import '~styles/helpers';
.tip-tap {
&__editor-wrapper {
margin-bottom: $medium-spacing;
}
:deep(.tip-tap__editor) {
@include inputstyle;
flex-direction: column;
min-height: 150px;
}
.tip-tap {
:deep(ul) {
padding-left: $medium-spacing;
list-style: initial;
}
&__editor-wrapper {
margin-bottom: $medium-spacing;
}
:deep(li) {
@include inputfont;
}
/deep/ &__editor {
@include inputstyle;
flex-direction: column;
min-height: 150px;
}
:deep(div) {
@include inputfont;
}
/deep/ ul {
padding-left: $medium-spacing;
list-style: initial;
}
:deep(p) {
@include inputfont;
/deep/ li {
@include inputfont;
}
/deep/ div {
@include inputfont;
}
/deep/ p {
@include inputfont;
}
}
}
</style>

View File

@ -1,11 +1,21 @@
<template>
<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" />
<p class="video-form__help-description help-text__description">
Sie können Videos auf
<a class="video-form__platform-link help-text__link" 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>
Sie können Videos auf <a
class="video-form__platform-link help-text__link"
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.
</p>
@ -14,7 +24,7 @@
class="video-form__video-link skillbox-input"
placeholder="Bsp: https://www.youtube.com/watch?v=dQw4w9WgXcQ"
@input="$emit('change-url', $event.target.value, index)"
/>
>
</div>
<div v-if="isYoutube">
@ -30,64 +40,67 @@
</template>
<script>
import YoutubeEmbed from '@/components/videos/YoutubeEmbed';
import VimeoEmbed from '@/components/videos/VimeoEmbed';
import SrfEmbed from '@/components/videos/SrfEmbed';
import { isVimeoUrl, isYoutubeUrl, isSrfUrl } from '@/helpers/video';
const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon');
import YoutubeEmbed from '@/components/videos/YoutubeEmbed';
import VimeoEmbed from '@/components/videos/VimeoEmbed';
import SrfEmbed from '@/components/videos/SrfEmbed';
import {isVimeoUrl, isYoutubeUrl, isSrfUrl} from '@/helpers/video';
import {defineAsyncComponent} from 'vue';
const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon'));
export default {
props: ['value', 'index'],
export default {
props: ['value', 'index'],
components: {
InfoIcon,
YoutubeEmbed,
VimeoEmbed,
SrfEmbed,
},
components: {
InfoIcon,
YoutubeEmbed,
VimeoEmbed,
SrfEmbed
},
computed: {
isYoutube() {
return isYoutubeUrl(this.value.url);
},
isVimeo() {
return isVimeoUrl(this.value.url);
},
isSrf() {
return isSrfUrl(this.value.url);
},
},
};
computed: {
isYoutube() {
return isYoutubeUrl(this.value.url);
},
isVimeo() {
return isVimeoUrl(this.value.url);
},
isSrf() {
return isSrfUrl(this.value.url);
}
}
};
</script>
<style scoped lang="scss">
@import '@/styles/_variables.scss';
@import '@/styles/_functions.scss';
@import "@/styles/_variables.scss";
@import "@/styles/_functions.scss";
.video-form {
display: grid;
grid-auto-rows: auto;
grid-template-columns: 40px 1fr;
grid-column-gap: 16px;
grid-row-gap: 20px;
align-items: center;
.video-form {
display: grid;
grid-auto-rows: auto;
grid-template-columns: 40px 1fr;
grid-column-gap: 16px;
grid-row-gap: 20px;
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>

View File

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

View File

@ -1,12 +1,13 @@
<template>
<div class="filter-group">
<filter-entry
:text="title"
v-bind="$attrs"
:text="title"
:type="category"
:category="category"
:is-category="true"
:id="category.id"
@click.native="setCategoryFilter(category.id)"
@filter="setCategoryFilter(category.id)"
/>
<div class="filter-group__children">
<filter-entry
@ -16,83 +17,84 @@
v-for="type in types"
:id="type.id"
:key="type.id"
@click.native="setFilter(`type:${type.id}`)"
@filter="setFilter(`type:${type.id}`)"
/>
</div>
</div>
</template>
<script>
import FilterEntry from '@/components/instruments/FilterEntry';
import {defineComponent} from 'vue';
import FilterEntry from '@/components/instruments/FilterEntry';
import SET_FILTER_MUTATION from 'gql/local/mutations/setInstrumentFilter.gql';
import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql';
import SET_FILTER_MUTATION from 'gql/local/mutations/setInstrumentFilter.gql';
import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql';
export default {
props: {
title: {
type: String,
default: '',
},
types: {
type: Array,
default: () => [],
},
category: {
type: Object,
default: () => ({}),
},
},
components: {
FilterEntry,
},
apollo: {
instrumentFilter: {
query: INSTRUMENT_FILTER_QUERY,
},
},
data() {
return {
instrumentFilter: {
currentFilter: '',
export default defineComponent({
props: {
title: {
type: String,
default: '',
},
};
},
inheritAttrs: false,
types: {
type: Array,
default: () => [],
},
category: {
type: Object,
default: () => ({}),
},
},
components: {
FilterEntry,
},
methods: {
setCategoryFilter(category) {
if (category) {
this.setFilter(`category:${category}`);
} else {
this.setFilter(``);
apollo: {
instrumentFilter: {
query: INSTRUMENT_FILTER_QUERY
}
},
setFilter(filter) {
this.$apollo.mutate({
mutation: SET_FILTER_MUTATION,
variables: {
filter,
},
});
},
data() {
return {
instrumentFilter: {
currentFilter: ''
}
};
},
};
inheritAttrs: false,
methods: {
setCategoryFilter(category) {
if (category) {
this.setFilter(`category:${category}`);
} else {
this.setFilter(``);
}
},
setFilter(filter) {
this.$apollo.mutate({
mutation: SET_FILTER_MUTATION,
variables: {
filter
}
});
}
},
});
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import '~styles/helpers';
.filter-group {
border-bottom: 1px solid $color-silver;
padding: $medium-spacing 0;
display: flex;
flex-direction: column;
.filter-group {
border-bottom: 1px solid $color-silver;
padding: $medium-spacing 0;
display: flex;
flex-direction: column;
&__children {
padding-left: $medium-spacing;
&__children {
padding-left: $medium-spacing;
}
}
}
</style>

View File

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

View File

@ -5,28 +5,8 @@
:slug="module.topic.slug"
class="module-navigation__topic-link"
type="topic"
v-if="module.topic"
/>
<div class="module-navigation__module-content" v-if="false">
<!-- Do not display this for now, might be used later again though -->
<router-link :to="moduleContentLink" tag="h3" class="module-navigation__heading">
Inhalte: {{ module.metaTitle }}
</router-link>
<div class="module-navigation__anchors" v-if="onModulePage">
<a href="#" class="module-navigation__anchor" v-scroll-to="'#meta-title'">Einleitung</a>
<a href="#" class="module-navigation__anchor" v-scroll-to="'#objectives'">Lernziele</a>
<a
href="#"
class="module-navigation__anchor"
v-for="(chapter, index) in module.chapters"
:key="chapter.id"
v-scroll-to="chapterId(index)"
>{{ chapter.title }}</a
>
<a href="#" class="module-navigation__anchor" v-scroll-to="'#objectives-confirm'">Lernzielkontrolle</a>
</div>
</div>
<div
class="module-navigation__toggle-menu"
data-cy="module-teacher-menu"

View File

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

View File

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

View File

@ -6,36 +6,40 @@
placeholder="Lernziel erfassen..."
@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" />
</a>
</div>
</template>
<script>
import ModalInput from '@/components/ModalInput';
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon');
import ModalInput from '@/components/ModalInput';
import {defineAsyncComponent} from 'vue';
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
export default {
props: ['objective'],
export default {
props: ['objective'],
components: {
ModalInput,
TrashIcon,
},
};
components: {
ModalInput,
TrashIcon
}
};
</script>
<style scoped lang="scss">
@import '@/styles/_variables.scss';
@import "@/styles/_variables.scss";
.objective-form {
display: grid;
grid-template-columns: 1fr 50px;
margin-bottom: 10px;
.objective-form {
display: grid;
grid-template-columns: 1fr 50px;
margin-bottom: 10px;
&__input {
width: $modal-input-width;
&__input {
width: $modal-input-width;
}
}
}
</style>

View File

@ -2,8 +2,8 @@
<div class="page-form-input">
<label :for="id" class="page-form-input__label">{{ label }}</label>
<component
:value="value"
:class="classes"
:value.prop="value"
:data-cy="cyId"
:is="type"
:id="id"

View File

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

View File

@ -1,9 +1,25 @@
<template>
<div class="portfolio-onboarding">
<h1 class="portfolio-onboarding__heading" data-cy="page-title">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">
<h1
class="portfolio-onboarding__heading"
data-cy="page-title"
>
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.
</p>
@ -12,33 +28,34 @@
</template>
<script>
const PortfolioIllustration = () => import('@/components/illustrations/PortfolioIllustration.vue');
const CreateProjectButton = () => import('@/components/portfolio/CreateProjectButton.vue');
export default {
components: { CreateProjectButton, PortfolioIllustration },
};
import {defineAsyncComponent} from 'vue';
const PortfolioIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/PortfolioIllustration'));
const CreateProjectButton = defineAsyncComponent(() => import('@/components/portfolio/CreateProjectButton'));
export default {
components: {CreateProjectButton, PortfolioIllustration},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import '~styles/helpers';
.portfolio-onboarding {
@include onboarding-page;
.portfolio-onboarding {
@include onboarding-page;
&__heading {
@include heading-1;
&__heading {
@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>

View File

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

View File

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

View File

@ -1,7 +1,12 @@
<template>
<modal :hide-header="false">
<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>
<div class="project-entry-modal">
@ -18,7 +23,7 @@
icon="document-with-lines-icon"
data-cy="use-template-button"
text="Vorlage nutzen"
@click.native="useTemplate"
@click="useTemplate"
/>
<file-upload
@ -30,101 +35,107 @@
</div>
</div>
</div>
<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>
<a
class="button button--primary"
data-cy="modal-save-button"
@click="$emit('save', localProjectEntry)"
>Speichern</a>
<a
class="button"
@click="$emit('hide')"
>Abbrechen</a>
</template>
</modal>
</template>
<script>
import Modal from '@/components/Modal';
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText';
import Modal from '@/components/Modal';
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText';
import { PROJECT_ENTRY_TEMPLATE } from '@/consts/strings.consts';
const FileUpload = () => import('@/components/ui/file-upload/FileUpload.vue');
import {PROJECT_ENTRY_TEMPLATE} from '@/consts/strings.consts';
export default {
props: {
projectEntry: {
type: Object,
default: null,
import {defineAsyncComponent} from 'vue';
const FileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/FileUpload'));
export default {
props: {
projectEntry: {
type: Object,
default: null,
},
},
},
components: {
FileUpload,
ButtonWithIconAndText,
Modal,
},
components: {
FileUpload,
ButtonWithIconAndText,
Modal,
},
data() {
return {
localProjectEntry: Object.assign(
{},
{
data() {
return {
localProjectEntry: Object.assign({}, {
...this.projectEntry,
}
),
};
},
}),
};
},
methods: {
setDocumentUrl(url) {
this.localProjectEntry.documentUrl = url;
methods: {
setDocumentUrl(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>
<style lang="scss" scoped>
@import '~styles/helpers';
@import "~styles/helpers";
.project-entry-modal {
display: flex;
flex-direction: column;
&__form-field {
@include inputstyle;
padding: 0;
.project-entry-modal {
display: flex;
flex-direction: column;
grid-template-rows: auto 1rem;
grid-template-columns: 1fr 1fr;
width: 100%;
}
&__textarea {
@include auto-grow;
border: 0;
min-height: 400px;
padding: $medium-spacing;
}
&__buttons {
display: grid;
grid-template-columns: 1fr 1fr;
padding: $medium-spacing;
}
&__button {
@include regular-text;
&--template {
&__form-field {
@include inputstyle;
padding: 0;
display: flex;
flex-direction: column;
grid-template-rows: auto 1rem;
grid-template-columns: 1fr 1fr;
width: 100%;
}
&--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>

View File

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

View File

@ -1,11 +1,6 @@
<template>
<li class="project">
<router-link
:to="{ name: 'project', params: { slug: project.slug } }"
tag="div"
class="project__link"
data-cy="project-link"
>
<router-link :to="{ name: 'project', params: { slug: project.slug } }" class="project__link" data-cy="project-link">
<span class="project__title" data-cy="project-title">{{ project.title }}</span>
<owner-widget :owner="project.student" class="project__owner" />

View File

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

View File

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

View File

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

View File

@ -1,9 +1,15 @@
<template>
<div class="content-bookmark module-activity-entry">
<!-- 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'">
<link-block :value="content.value" :no-margin="true" />
<link-block
:value="content.value"
:no-margin="true"
/>
</div>
<p v-else>
{{ type }}
@ -12,34 +18,35 @@
</template>
<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 {
props: ['bookmark'],
components: { LinkBlock },
computed: {
content() {
return this.bookmark.contentBlock
? this.bookmark.contentBlock.contents.find((e) => e.id === this.bookmark.uuid)
: this.bookmark.instrument.contents.find((e) => e.id === this.bookmark.uuid);
},
text() {
return this.content.value.text ? this.content.value.text : 'TO BE DEFINED';
},
type() {
switch (this.content.type) {
case 'assignment':
return 'Aufgabe & Ergebnis';
case 'link_block':
return this.content;
case 'survey':
return 'Übung';
case 'image_url_block':
return 'Bild';
default:
return this.content.type;
export default {
props: ['bookmark'],
components: {LinkBlock},
computed: {
content() {
return this.bookmark.contentBlock
? this.bookmark.contentBlock.contents.find(e => e.id === this.bookmark.uuid)
: this.bookmark.instrument.contents.find(e => e.id === this.bookmark.uuid);
},
text() {
return this.content.value.text ? this.content.value.text : 'TO BE DEFINED';
},
type() {
switch (this.content.type) {
case 'assignment':
return 'Aufgabe & Ergebnis';
case 'link_block':
return this.content;
case 'survey':
return 'Übung';
case 'image_url_block':
return 'Bild';
default:
return this.content.type;
}
}
},
},
};
}
};
</script>

View File

@ -1,27 +1,32 @@
<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" />
</a>
</template>
<script>
const PenIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/PenIcon');
import {defineAsyncComponent} from 'vue';
const PenIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PenIcon'));
export default {
components: {
PenIcon,
},
};
export default {
components: {
PenIcon
}
};
</script>
<style scoped lang="scss">
@import '~styles/_variables.scss';
@import "~styles/_variables.scss";
.edit-group-name {
&__icon {
width: 20px;
height: 20px;
fill: $color-brand;
.edit-group-name {
&__icon {
width: 20px;
height: 20px;
fill: $color-brand;
}
}
}
</style>

View File

@ -6,10 +6,15 @@
<modal-input :value="name" :placeholder="placeholder" data-cy="edit-name-input" @input="$emit('input', $event)" />
<template #footer>
<div slot="footer">
<a class="button button--primary" data-cy="modal-save-button" @click="$emit('save')">Speichern</a>
<a class="button" @click="$emit('cancel')">Abbrechen</a>
</div>
<a
class="button button--primary"
data-cy="modal-save-button"
@click="$emit('save')"
>Speichern</a>
<a
class="button"
@click="$emit('cancel')"
>Abbrechen</a>
</template>
</modal>
</template>

View File

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

View File

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

View File

@ -1,60 +1,65 @@
<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" />
<span class="add-room-entry-button__text">Beitrag erfassen</span>
</router-link>
</template>
<script>
import { ADD_ROOM_ENTRY_PAGE } from '@/router/room.names';
const PlusIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/PlusIcon');
import {defineAsyncComponent} from 'vue';
const PlusIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PlusIcon'));
import { ADD_ROOM_ENTRY_PAGE } from '@/router/room.names';
export default {
props: ['parent'],
export default {
props: ['parent'],
components: {
PlusIcon,
},
components: {
PlusIcon,
},
data() {
return {
addRoomEntryRoute: {
name: ADD_ROOM_ENTRY_PAGE,
},
};
},
};
data() {
return {
addRoomEntryRoute: {
name: ADD_ROOM_ENTRY_PAGE,
},
};
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.add-room-entry-button {
border: 2px solid $color-white;
border-radius: 12px;
height: 150px;
box-sizing: border-box;
margin-bottom: 25px;
justify-content: center;
align-items: center;
break-inside: avoid-column;
overflow: hidden;
cursor: pointer;
.add-room-entry-button {
border: 2px solid $color-white;
border-radius: 12px;
height: 150px;
box-sizing: border-box;
margin-bottom: 25px;
justify-content: center;
align-items: center;
break-inside: avoid-column;
overflow: hidden;
cursor: pointer;
display: none;
@include desktop {
display: flex;
display: none;
@include desktop {
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>

View File

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

View File

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

View File

@ -1,8 +1,21 @@
<template>
<div class="room-entry" data-cy="room-entry">
<router-link :to="{ name: 'article', params: { slug: slug } }" class="room-entry__router-link" tag="div">
<div class="room-entry__header" v-if="image">
<img :src="image" :alt="title" class="room-entry__image" />
<div
class="room-entry"
data-cy="room-entry"
>
<router-link
:to="{name: 'article', params: { slug: slug }}"
class="room-entry__router-link"
>
<div
class="room-entry__header"
v-if="image"
>
<img
:src="image"
:alt="title"
class="room-entry__image"
>
</div>
<div class="room-entry__content">
<h2 class="room-entry__title">

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,25 @@
<template>
<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" />
<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.
</p>
<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
</router-link>
</div>
@ -14,43 +27,43 @@
</template>
<script>
import { NEW_ROOM_PAGE } from '@/router/room.names';
const RoomsIllustration = () =>
import(/* webpackChunkName: "illustrations" */ '@/components/illustrations/RoomsIllustration');
import {NEW_ROOM_PAGE} from '@/router/room.names';
import {defineAsyncComponent} from 'vue';
const RoomsIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/RoomsIllustration'));
export default {
props: {
isTeacher: {
type: Boolean,
default: false,
export default {
props: {
isTeacher: {
type: Boolean,
default: false,
},
},
},
components: { RoomsIllustration },
components: {RoomsIllustration},
data() {
return {
newRoomRoute: NEW_ROOM_PAGE,
};
},
};
data() {
return {
newRoomRoute: NEW_ROOM_PAGE,
};
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import '~styles/helpers';
.rooms-onboarding {
@include onboarding-page;
.rooms-onboarding {
@include onboarding-page;
&__heading {
margin-bottom: $large-spacing;
&__heading {
margin-bottom: $large-spacing;
}
&__illustration {
@include onboarding-illustration;
}
&__text {
@include onboarding-text;
}
}
&__illustration {
@include onboarding-illustration;
}
&__text {
@include onboarding-text;
}
}
</style>

View File

@ -1,14 +1,24 @@
<template>
<div class="class-selection" v-if="currentClassSelection">
<div
class="class-selection"
v-if="currentClassSelection"
>
<div
data-cy="class-selection"
class="class-selection__selected-class selected-class"
@click.stop="showPopover = !showPopover"
@click.stop="toggle"
>
<current-class class="selected-class__text" />
<current-class
class="selected-class__text"
/>
<chevron-down class="selected-class__dropdown-icon" />
</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
:label="schoolClass.name"
:item="schoolClass"
@ -26,105 +36,122 @@
v-if="me.isTeacher && !me.readOnly"
@click="closeSidebar"
>
<router-link :to="{ name: 'create-class' }" tag="span" class="popover-links__link-with-icon">
<router-link
:to="{name: 'create-class'}"
class="popover-links__link-with-icon"
>
<add-icon class="popover-links__icon" />
<span>Klasse erfassen</span>
</router-link>
</li>
<li 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
class="popover-links__link popover-links__link--large popover-links__divider"
@click="closeSidebar"
>
<router-link
:to="{name: 'old-classes'}"
>
Alte Klassen anzeigen
</router-link>
</li>
</widget-popover>
</div>
</template>
<script>
import WidgetPopover from '@/components/ui/WidgetPopover';
import CurrentClass from '@/components/school-class/CurrentClass';
import WidgetPopover from '@/components/ui/WidgetPopover';
import CurrentClass from '@/components/school-class/CurrentClass';
import updateSelectedClassMixin from '@/mixins/update-selected-class';
import sidebarMixin from '@/mixins/sidebar';
import meMixin from '@/mixins/me';
const ChevronDown = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronDown');
const AddIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddIcon');
import updateSelectedClassMixin from '@/mixins/update-selected-class';
import sidebarMixin from '@/mixins/sidebar';
import meMixin from '@/mixins/me';
import {defineAsyncComponent} from 'vue';
const ChevronDown = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown'));
const AddIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon'));
export default {
props: {
mobile: {
type: Boolean,
default: false,
export default {
props: {
mobile: {
type: Boolean,
default: false
}
},
},
mixins: [updateSelectedClassMixin, sidebarMixin, meMixin],
components: {
WidgetPopover,
ChevronDown,
CurrentClass,
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];
mixins: [updateSelectedClassMixin, sidebarMixin, meMixin],
components: {
WidgetPopover,
ChevronDown,
CurrentClass,
AddIcon
},
},
methods: {
updateSelectedClassAndHidePopover(selectedClass) {
this.updateSelectedClass(selectedClass);
this.showPopover = false;
this.closeSidebar('profile');
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: {
toggle() {
this.showPopover = !this.showPopover;
},
updateSelectedClassAndHidePopover(selectedClass) {
this.updateSelectedClass(selectedClass);
this.showPopover = false;
this.closeSidebar('profile');
}
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import "~styles/helpers";
.class-selection {
position: relative;
cursor: pointer;
margin-bottom: $medium-spacing;
border-radius: 4px;
.class-selection {
position: relative;
cursor: pointer;
margin-bottom: $medium-spacing;
border-radius: 4px;
&__popover {
white-space: nowrap;
top: 100%;
left: 0;
transform: translateY($small-spacing);
}
}
&__popover {
white-space: nowrap;
top: 100%;
left: 0;
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 {
width: 20px;
height: 20px;
fill: $color-charcoal-dark;
.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 {
width: 20px;
height: 20px;
fill: $color-charcoal-dark;
}
}
}
</style>

View File

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

View File

@ -6,39 +6,47 @@
class="base-input-container__input"
data-cy="base-input-input"
@change.prevent="$emit('input', $event.target.checked, item)"
/>
>
<span
:class="{
'base-input-container__checkbox': type === 'checkbox',
'base-input-container__radiobutton': type === 'radiobutton',
}"
:class="{'base-input-container__checkbox': type==='checkbox', 'base-input-container__radiobutton': type === 'radiobutton'}"
class="base-input-container__icon checkbox"
>
<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 class="base-input-container__label" data-cy="base-input-label" v-if="label">{{ label }}</span>
<slot class="base-input-container__label" v-if="!label" />
<span
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>
</template>
<script>
const Tick = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Tick');
const CircleIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CircleIcon');
import {defineAsyncComponent} from 'vue';
const Tick = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Tick'));
const CircleIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CircleIcon'));
export default {
props: {
label: String,
checked: {
type: Boolean,
export default {
props: {
label: String,
checked: {
type: Boolean
},
item: Object,
type: String
},
item: Object,
type: String,
},
components: {
Tick,
CircleIcon,
},
};
components: {
Tick,
CircleIcon
}
};
</script>

View File

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

View File

@ -1,7 +1,11 @@
<template>
<div class="file-upload">
<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 v-else>
<simple-file-upload
@ -14,24 +18,26 @@
</template>
<script>
const SimpleFileUpload = () => import('@/components/ui/file-upload/SimpleFileUpload.vue');
const DocumentBlock = () => import('@/components/content-blocks/DocumentBlock.vue');
import {defineAsyncComponent} from 'vue';
const SimpleFileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUpload'));
const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'));
export default {
props: {
document: {
type: String,
default: '',
export default {
props: {
document: {
type: String,
default: '',
},
withText: {
type: Boolean,
default: false
}
},
withText: {
type: Boolean,
default: false,
},
},
components: { SimpleFileUpload, DocumentBlock },
};
components: {SimpleFileUpload, DocumentBlock},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
@import '~styles/helpers';
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,85 +0,0 @@
<template>
<ValidationProvider v-slot="{ errors }" :name="name" :rules="rules">
<div class="skillboxform-input">
<label :for="id" class="skillboxform-input__label">{{ label }}</label>
<input
:value="value"
:class="{ 'skillboxform-input__input--error': errors.length }"
v-bind="$attrs"
class="change-form__email skillbox-input skillboxform-input__input"
autocomplete="off"
:id="id"
@input="$emit('input', $event.target.value)"
/>
<small :data-cy="localErrorsCy" class="skillboxform-input__error" v-if="errors.length">{{ errors[0] }}</small>
<small :data-cy="remoteErrorsCy" class="skillboxform-input__error" v-for="error in remoteErrors" :key="error">{{
error
}}</small>
</div>
</ValidationProvider>
</template>
<script>
import { extend, localize, ValidationProvider } from 'vee-validate';
import de from 'vee-validate/dist/locale/de.json';
import { required } from 'vee-validate/dist/rules';
extend('required', required);
localize('de', {
...de,
names: {
password: 'Passwort',
email: 'E-Mail',
coupon: 'Coupon-Code',
},
});
// todo: use this in beta-login, license-activation and PasswordChangeForm
export default {
props: {
value: {
type: String,
default: '',
},
remoteErrors: {
type: Array,
default: undefined,
},
name: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
rules: {
type: String,
default: 'required',
},
},
components: {
ValidationProvider,
},
inheritAttrs: false,
computed: {
id() {
return this.$attrs.id || this._uid;
},
remoteErrorsCy() {
return `${this.name}-remote-errors`;
},
localErrorsCy() {
return `${this.name}-local-errors`;
},
},
mounted() {},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
</style>

View File

@ -1,84 +1,93 @@
<template>
<div class="visibility-action">
<a class="visibility-action__action-button" 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
class="visibility-action__action-button"
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>
</div>
</template>
<script>
import me from '@/mixins/me';
import me from '@/mixins/me';
import { TYPES, CONTENT_TYPE } from '@/consts/types';
import { createVisibilityMutation, hidden } from '@/helpers/visibility';
import {TYPES, CONTENT_TYPE} from '@/consts/types';
import {createVisibilityMutation, hidden} from '@/helpers/visibility';
const EyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon');
const ClosedEyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ClosedEyeIcon');
import {defineAsyncComponent} from 'vue';
const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon'));
const ClosedEyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ClosedEyeIcon'));
export default {
props: {
block: {
type: Object,
default: () => ({}),
export default {
props: {
block: {
type: Object,
default: () => ({})
},
type: {
type: String,
default: CONTENT_TYPE,
validator: value => {
// value must be one of TYPES
return TYPES.indexOf(value) !== -1;
}
}
},
type: {
type: String,
default: CONTENT_TYPE,
validator: (value) => {
// value must be one of TYPES
return TYPES.indexOf(value) !== -1;
mixins: [me],
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
});
},
},
},
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>
<style scoped lang="scss">
.visibility-action {
margin-top: 9px;
.visibility-action {
margin-top: 9px;
position: absolute;
left: -70px;
top: 0px;
display: grid;
position: absolute;
left: -70px;
top: 0px;
display: grid;
&__visibility-menu {
top: 40px;
&__visibility-menu {
top: 40px;
}
}
}
</style>

View File

@ -1,4 +1,4 @@
const resizeElement = (el) => {
const resizeElement = (el: HTMLElement) => {
el.style.height = `auto`;
el.style.height = `${el.clientHeight - el.offsetHeight + el.scrollHeight}px`;
};
@ -6,13 +6,13 @@ const resizeElement = (el) => {
export default {
update: resizeElement,
inserted: resizeElement,
bind(el) {
created(el: HTMLElement) {
el.classList.add('skillbox-auto-grow');
el.addEventListener('input', () => {
resizeElement(el);
});
},
unbind(el) {
unmounted(el: HTMLElement) {
el.classList.remove('skillbox-auto-grow');
el.removeEventListener('input', () => {
resizeElement(el);

View File

@ -1,14 +0,0 @@
// taken from https://stackoverflow.com/questions/36170425/detect-click-outside-element
export default {
bind(el, binding, vnode) {
el.clickOutsideEvent = (event) => {
if (!(el === event.target || el.contains(event.target))) {
vnode.context[binding.expression](event);
}
};
document.body.addEventListener('click', el.clickOutsideEvent);
},
unbind(el) {
document.body.removeEventListener('click', el.clickOutsideEvent);
},
};

View File

@ -0,0 +1,46 @@
// taken from https://stackoverflow.com/questions/36170425/detect-click-outside-element
import {DirectiveBinding, VNode} from "vue";
declare global {
interface HTMLElement {
clickOutsideEvent: (event: Event) => void
}
}
/*
todo:
there is a special interaction with nested elements where the parent has a @click event:
the parent triggers the event, something happens, but the click event bubbles to the child element.
If the event is then used to open some kind of sidebar or modal that has the `click-outside` propert, t
he bubbled event will be outside of it, thereby closing it.
example:
<div
class="sidebar"
v-if="showSidebar"
v-click-outside="showSidebar=false"
>
...
</div>
<a class="sidebar-toggle" @click="showSidebar=true">
<span>Hello</span>
</a>
FIX:
In this example, setting the event on the a-tag as `@click.stop` will solve the problem
*/
export default {
unmounted(el: HTMLElement) {
document.body.removeEventListener('click', el.clickOutsideEvent);
},
created: (el: HTMLElement, binding: DirectiveBinding) => {
el.clickOutsideEvent = (event: Event) => {
if (!(el === event.target || el.contains(event.target as Node))) {
const eventHandler = binding.value;
eventHandler(event);
}
};
document.body.addEventListener('click', el.clickOutsideEvent);
}
};

View File

@ -1,4 +1,4 @@
import log from 'loglevel';
// import log from 'loglevel';
import type { FlavorValues } from '@/helpers/types';
import { defaultFlavorValues, dhaValues, dhfValues, myKvValues } from '@/helpers/app-flavor.constants';
@ -18,6 +18,6 @@ switch (process.env.VUE_APP_FLAVOR) {
flavorValues = defaultFlavorValues;
}
log.debug('flavorValues', flavorValues);
// log.debug('flavorValues', flavorValues);
export default flavorValues;

View File

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

View File

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

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