From 333b1181662f26afe13256b26c6c87605d627d10 Mon Sep 17 00:00:00 2001
From: Ron Lucke <lucke@elan-ev.de>
Date: Mon, 7 Feb 2022 14:27:25 +0000
Subject: [PATCH] StEP 00357

---
 app/controllers/admin/courseware.php          |  34 +
 app/controllers/contents/courseware.php       |  86 ++-
 app/controllers/course/courseware.php         |  24 +-
 app/routes/Activity.php                       |  11 +
 .../admin/courseware/admin_action_widget.php  |   1 +
 .../admin/courseware/admin_view_widget.php    |   1 +
 app/views/admin/courseware/index.php          |   1 +
 .../courseware/bookmark_filter_widget.php     |   1 +
 app/views/contents/courseware/bookmarks.php   |  38 +-
 .../contents/courseware/courses_overview.php  |   2 +-
 app/views/contents/courseware/index.php       |  51 +-
 .../courseware/overview_action_widget.php     |   1 +
 .../courseware/overview_filter_widget.php     |   1 +
 app/views/course/courseware/dashboard.php     |  11 +-
 .../courseware/dashboard_view_widget.php      |   1 +
 .../5.1.17_add_courseware_templates.php       |  34 +
 db/migrations/5.1.18_add_courseware_tasks.php |  78 +++
 ...urseware_structural_element_discussion.php |  49 ++
 lib/activities/Activity.php                   |   1 +
 lib/activities/ActivityObserver.php           |  34 +
 lib/activities/Context.php                    |  42 +-
 lib/activities/CourseContext.php              |   3 +
 lib/activities/CoursewareProvider.php         | 244 ++++++-
 lib/activities/Filter.php                     |  78 ++-
 lib/classes/JsonApi/RouteMap.php              |  40 ++
 .../JsonApi/Routes/Courseware/Authority.php   | 178 ++++-
 .../Routes/Courseware/BlockCommentsCreate.php |   3 +
 .../Courseware/BlockFeedbacksCreate.php       |   3 +
 .../Courseware/BlockFeedbacksDelete.php       |  33 +
 .../Courseware/BlockFeedbacksUpdate.php       | 113 +++
 .../Routes/Courseware/BlocksCreate.php        |   2 +
 .../Routes/Courseware/BlocksUpdate.php        |   3 +
 .../BookmarkedStructuralElementsIndex.php     |   2 +-
 .../Routes/Courseware/ContainersUpdate.php    |   2 +
 .../Courseware/CoursewareInstancesHelper.php  |   5 +-
 .../Rel/BookmarkedStructuralElements.php      |   4 +
 .../Rel/UsersBookmarkedStructuralElements.php | 180 +++++
 .../StructuralElementCommentsCreate.php       |  86 +++
 .../StructuralElementCommentsDelete.php       |  33 +
 ...ementCommentsOfStructuralElementsIndex.php |  35 +
 .../StructuralElementCommentsShow.php         |  34 +
 .../StructuralElementCommentsUpdate.php       | 113 +++
 .../StructuralElementFeedbackCreate.php       |  86 +++
 .../StructuralElementFeedbackDelete.php       |  33 +
 ...ementFeedbackOfStructuralElementsIndex.php |  38 +
 .../StructuralElementFeedbackShow.php         |  36 +
 .../StructuralElementFeedbackUpdate.php       | 113 +++
 .../Courseware/StructuralElementsCopy.php     |   3 +
 .../Courseware/StructuralElementsCreate.php   |  41 +-
 .../Routes/Courseware/TaskFeedbackCreate.php  |  89 +++
 .../Routes/Courseware/TaskFeedbackDelete.php  |  36 +
 .../Routes/Courseware/TaskFeedbackShow.php    |  38 +
 .../Routes/Courseware/TaskFeedbackUpdate.php  |  84 +++
 .../Routes/Courseware/TaskGroupsCreate.php    | 212 ++++++
 .../Routes/Courseware/TaskGroupsShow.php      |  46 ++
 .../JsonApi/Routes/Courseware/TasksDelete.php |  39 ++
 .../JsonApi/Routes/Courseware/TasksIndex.php  | 106 +++
 .../JsonApi/Routes/Courseware/TasksShow.php   |  46 ++
 .../JsonApi/Routes/Courseware/TasksUpdate.php | 118 ++++
 .../Routes/Courseware/TemplatesCreate.php     |  88 +++
 .../Routes/Courseware/TemplatesDelete.php     |  33 +
 .../Routes/Courseware/TemplatesIndex.php      |  32 +
 .../Routes/Courseware/TemplatesShow.php       |  34 +
 .../Routes/Courseware/TemplatesUpdate.php     |  80 +++
 ...UsersBookmarkedStructuralElementsIndex.php |  55 ++
 lib/classes/JsonApi/SchemaMap.php             |   6 +
 .../Schemas/Courseware/StructuralElement.php  |  64 +-
 .../Courseware/StructuralElementComment.php   |  62 ++
 .../Courseware/StructuralElementFeedback.php  |  62 ++
 .../JsonApi/Schemas/Courseware/Task.php       |  85 +++
 .../Schemas/Courseware/TaskFeedback.php       |  61 ++
 .../JsonApi/Schemas/Courseware/TaskGroup.php  | 104 +++
 .../JsonApi/Schemas/Courseware/Template.php   |  44 ++
 lib/classes/JsonApi/Schemas/User.php          |  18 +
 lib/models/Courseware/Block.php               |  15 +
 lib/models/Courseware/BlockComment.php        |  16 +
 lib/models/Courseware/BlockFeedback.php       |  16 +
 .../Courseware/BlockTypes/BlockType.php       |  15 +-
 lib/models/Courseware/BlockTypes/Code.php     |   8 +
 lib/models/Courseware/BlockTypes/Confirm.php  |   8 +
 lib/models/Courseware/BlockTypes/Date.php     |   8 +
 lib/models/Courseware/BlockTypes/Headline.php |   9 +
 lib/models/Courseware/BlockTypes/KeyPoint.php |   8 +
 lib/models/Courseware/BlockTypes/Link.php     |   9 +
 lib/models/Courseware/BlockTypes/Text.php     |   8 +
 .../Courseware/BlockTypes/Typewriter.php      |   8 +
 lib/models/Courseware/Bookmark.php            |   6 +-
 .../ContainerTypes/AccordionContainer.php     |  24 +
 .../ContainerTypes/ContainerType.php          |  11 +
 .../ContainerTypes/ListContainer.php          |  20 +
 .../ContainerTypes/TabsContainer.php          |  24 +
 lib/models/Courseware/StructuralElement.php   | 160 ++++-
 .../Courseware/StructuralElementComment.php   |  42 ++
 .../Courseware/StructuralElementFeedback.php  |  42 ++
 lib/models/Courseware/Task.php                | 217 ++++++
 lib/models/Courseware/TaskFeedback.php        |  58 ++
 lib/models/Courseware/TaskGroup.php           |  61 ++
 lib/models/Courseware/Template.php            |  28 +
 lib/navigation/AdminNavigation.php            |   7 +
 lib/navigation/ContentsNavigation.php         |   2 +-
 package.json                                  |   1 +
 .../images/icons/black/bullet-arrow.svg       |   1 +
 .../assets/images/icons/black/bullet-dot.svg  |   1 +
 .../icons/black/bullet-double-arrow.svg       |   1 +
 .../assets/images/icons/black/bullet-line.svg |   1 +
 .../images/icons/black/category-draft.svg     |   1 +
 .../images/icons/black/category-others.svg    |   1 +
 .../images/icons/black/category-portfolio.svg |   1 +
 .../images/icons/black/category-task.svg      |   1 +
 .../images/icons/black/category-template.svg  |   1 +
 public/assets/images/icons/black/content2.svg |   1 +
 .../assets/images/icons/blue/bullet-arrow.svg |   1 +
 .../assets/images/icons/blue/bullet-dot.svg   |   1 +
 .../images/icons/blue/bullet-double-arrow.svg |   1 +
 .../assets/images/icons/blue/bullet-line.svg  |   1 +
 .../images/icons/blue/category-draft.svg      |   1 +
 .../images/icons/blue/category-others.svg     |   1 +
 .../images/icons/blue/category-portfolio.svg  |   1 +
 .../images/icons/blue/category-task.svg       |   1 +
 .../images/icons/blue/category-template.svg   |   1 +
 public/assets/images/icons/blue/content2.svg  |   2 +-
 .../images/icons/green/bullet-arrow.svg       |   1 +
 .../assets/images/icons/green/bullet-dot.svg  |   1 +
 .../icons/green/bullet-double-arrow.svg       |   1 +
 .../assets/images/icons/green/bullet-line.svg |   1 +
 .../images/icons/green/category-draft.svg     |   1 +
 .../images/icons/green/category-others.svg    |   1 +
 .../images/icons/green/category-portfolio.svg |   1 +
 .../images/icons/green/category-task.svg      |   1 +
 .../images/icons/green/category-template.svg  |   1 +
 public/assets/images/icons/green/content2.svg |   1 +
 .../assets/images/icons/grey/bullet-arrow.svg |   1 +
 .../assets/images/icons/grey/bullet-dot.svg   |   1 +
 .../images/icons/grey/bullet-double-arrow.svg |   1 +
 .../assets/images/icons/grey/bullet-line.svg  |   1 +
 .../images/icons/grey/category-draft.svg      |   1 +
 .../images/icons/grey/category-others.svg     |   1 +
 .../images/icons/grey/category-portfolio.svg  |   1 +
 .../images/icons/grey/category-task.svg       |   1 +
 .../images/icons/grey/category-template.svg   |   1 +
 public/assets/images/icons/grey/content2.svg  |   1 +
 .../assets/images/icons/red/bullet-arrow.svg  |   1 +
 public/assets/images/icons/red/bullet-dot.svg |   1 +
 .../images/icons/red/bullet-double-arrow.svg  |   1 +
 .../assets/images/icons/red/bullet-line.svg   |   1 +
 .../images/icons/red/category-draft.svg       |   1 +
 .../images/icons/red/category-others.svg      |   1 +
 .../images/icons/red/category-portfolio.svg   |   1 +
 .../assets/images/icons/red/category-task.svg |   1 +
 .../images/icons/red/category-template.svg    |   1 +
 public/assets/images/icons/red/content2.svg   |   1 +
 .../images/icons/white/bullet-arrow.svg       |   1 +
 .../assets/images/icons/white/bullet-dot.svg  |   1 +
 .../icons/white/bullet-double-arrow.svg       |   1 +
 .../assets/images/icons/white/bullet-line.svg |   1 +
 .../images/icons/white/category-draft.svg     |   1 +
 .../images/icons/white/category-others.svg    |   1 +
 .../images/icons/white/category-portfolio.svg |   1 +
 .../images/icons/white/category-task.svg      |   1 +
 .../images/icons/white/category-template.svg  |   1 +
 public/assets/images/icons/white/content2.svg |   1 +
 .../images/icons/yellow/bullet-arrow.svg      |   1 +
 .../assets/images/icons/yellow/bullet-dot.svg |   1 +
 .../icons/yellow/bullet-double-arrow.svg      |   1 +
 .../images/icons/yellow/bullet-line.svg       |   1 +
 .../images/icons/yellow/category-draft.svg    |   1 +
 .../images/icons/yellow/category-others.svg   |   1 +
 .../icons/yellow/category-portfolio.svg       |   1 +
 .../images/icons/yellow/category-task.svg     |   1 +
 .../images/icons/yellow/category-template.svg |   1 +
 .../assets/images/icons/yellow/content2.svg   |   1 +
 .../core/ActivityFeed/ActivityFeed.php        |   3 +-
 .../javascripts/bootstrap/courseware.js       |  33 +
 .../assets/stylesheets/scss/courseware.scss   | 658 +++++++++++++++---
 .../vue/components/courseware/AdminApp.vue    |  33 +
 .../courseware/ContentBookmarkApp.vue         |  21 +
 .../courseware/ContentOverviewApp.vue         |  25 +
 .../CoursewareAccordionContainer.vue          |  51 +-
 .../courseware/CoursewareActionWidget.vue     |  74 +-
 .../courseware/CoursewareActivityItem.vue     |  40 +-
 .../CoursewareAdminActionWidget.vue           |  30 +
 .../courseware/CoursewareAdminTemplates.vue   | 237 +++++++
 .../courseware/CoursewareAdminViewWidget.vue  |  31 +
 .../courseware/CoursewareBlockActions.vue     |  21 +-
 .../courseware/CoursewareBlockComments.vue    |  72 +-
 .../courseware/CoursewareBlockDiscussion.vue  |  60 ++
 .../courseware/CoursewareBlockFeedback.vue    |  72 +-
 .../courseware/CoursewareChartBlock.vue       |   2 +-
 .../courseware/CoursewareCollapsibleBox.vue   |   5 +
 .../courseware/CoursewareContainerActions.vue |  12 +-
 .../CoursewareContentBookmarkFilterWidget.vue |  42 ++
 .../courseware/CoursewareContentBookmarks.vue | 107 +++
 .../CoursewareContentOverviewActionWidget.vue |  23 +
 .../CoursewareContentOverviewElements.vue     | 535 ++++++++++++++
 .../CoursewareContentOverviewFilterWidget.vue |  51 ++
 .../courseware/CoursewareCourseDashboard.vue  |  86 ++-
 .../courseware/CoursewareCourseManager.vue    | 265 +++----
 .../CoursewareDashboardActivities.vue         |  99 ++-
 .../CoursewareDashboardProgress.vue           |   9 +-
 .../CoursewareDashboardStudents.vue           | 377 ++++++++++
 .../courseware/CoursewareDashboardTasks.vue   | 264 +++++++
 .../CoursewareDashboardViewWidget.vue         |  59 ++
 .../courseware/CoursewareDateInput.vue        |  34 +
 .../courseware/CoursewareDefaultBlock.vue     |  66 +-
 .../CoursewareDefaultBlockElements.vue        |   4 -
 .../courseware/CoursewareDefaultContainer.vue |  21 +
 .../courseware/CoursewareHeadlineBlock.vue    |   8 +-
 .../courseware/CoursewareImageMapBlock.vue    |   2 +-
 .../courseware/CoursewareKeyPointBlock.vue    |   4 +-
 .../courseware/CoursewareListContainer.vue    |  80 ++-
 .../CoursewareManagerTaskDistributor.vue      | 316 +++++++++
 .../courseware/CoursewareOblong.vue           |  12 +-
 .../CoursewareStructuralElement.vue           | 245 ++++++-
 .../CoursewareStructuralElementComments.vue   | 128 ++++
 .../CoursewareStructuralElementDiscussion.vue |  60 ++
 .../CoursewareStructuralElementFeedback.vue   | 130 ++++
 .../CoursewareTableOfContentsBlock.vue        |  23 +-
 .../courseware/CoursewareTabsContainer.vue    |  62 +-
 .../courseware/CoursewareTreeItem.vue         | 122 +++-
 .../courseware/CoursewareViewWidget.vue       |  45 +-
 .../components/courseware/DashboardApp.vue    |  29 +-
 .../vue/components/courseware/IndexApp.vue    |   4 +-
 resources/vue/courseware-admin-app.js         |  42 ++
 .../vue/courseware-content-bookmark-app.js    |  81 +++
 .../vue/courseware-content-overview-app.js    |  97 +++
 resources/vue/courseware-dashboard-app.js     |  81 +++
 resources/vue/courseware-index-app.js         |   5 +
 resources/vue/courseware-manager-app.js       |   2 +
 .../vue/mixins/courseware/task-helper.js      |  66 ++
 .../courseware/courseware-admin.module.js     |  47 ++
 .../vue/store/courseware/courseware.module.js | 259 ++++++-
 231 files changed, 9410 insertions(+), 652 deletions(-)
 create mode 100755 app/controllers/admin/courseware.php
 create mode 100755 app/views/admin/courseware/admin_action_widget.php
 create mode 100755 app/views/admin/courseware/admin_view_widget.php
 create mode 100755 app/views/admin/courseware/index.php
 create mode 100755 app/views/contents/courseware/bookmark_filter_widget.php
 create mode 100755 app/views/contents/courseware/overview_action_widget.php
 create mode 100755 app/views/contents/courseware/overview_filter_widget.php
 create mode 100755 app/views/course/courseware/dashboard_view_widget.php
 create mode 100755 db/migrations/5.1.17_add_courseware_templates.php
 create mode 100755 db/migrations/5.1.18_add_courseware_tasks.php
 create mode 100755 db/migrations/5.1.19_add_courseware_structural_element_discussion.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksDelete.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksUpdate.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/Rel/UsersBookmarkedStructuralElements.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsCreate.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsDelete.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsOfStructuralElementsIndex.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsShow.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsUpdate.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackCreate.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackDelete.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackOfStructuralElementsIndex.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackShow.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackUpdate.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TaskFeedbackCreate.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TaskFeedbackDelete.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TaskFeedbackShow.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TaskFeedbackUpdate.php
 create mode 100644 lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php
 create mode 100644 lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TasksDelete.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TasksIndex.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TasksShow.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TemplatesCreate.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TemplatesDelete.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TemplatesIndex.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TemplatesShow.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/TemplatesUpdate.php
 create mode 100755 lib/classes/JsonApi/Routes/Courseware/UsersBookmarkedStructuralElementsIndex.php
 create mode 100755 lib/classes/JsonApi/Schemas/Courseware/StructuralElementComment.php
 create mode 100755 lib/classes/JsonApi/Schemas/Courseware/StructuralElementFeedback.php
 create mode 100755 lib/classes/JsonApi/Schemas/Courseware/Task.php
 create mode 100755 lib/classes/JsonApi/Schemas/Courseware/TaskFeedback.php
 create mode 100755 lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php
 create mode 100755 lib/classes/JsonApi/Schemas/Courseware/Template.php
 create mode 100755 lib/models/Courseware/StructuralElementComment.php
 create mode 100755 lib/models/Courseware/StructuralElementFeedback.php
 create mode 100755 lib/models/Courseware/Task.php
 create mode 100755 lib/models/Courseware/TaskFeedback.php
 create mode 100644 lib/models/Courseware/TaskGroup.php
 create mode 100755 lib/models/Courseware/Template.php
 create mode 100644 public/assets/images/icons/black/bullet-arrow.svg
 create mode 100644 public/assets/images/icons/black/bullet-dot.svg
 create mode 100644 public/assets/images/icons/black/bullet-double-arrow.svg
 create mode 100644 public/assets/images/icons/black/bullet-line.svg
 create mode 100644 public/assets/images/icons/black/category-draft.svg
 create mode 100644 public/assets/images/icons/black/category-others.svg
 create mode 100644 public/assets/images/icons/black/category-portfolio.svg
 create mode 100644 public/assets/images/icons/black/category-task.svg
 create mode 100644 public/assets/images/icons/black/category-template.svg
 create mode 100644 public/assets/images/icons/black/content2.svg
 create mode 100644 public/assets/images/icons/blue/bullet-arrow.svg
 create mode 100644 public/assets/images/icons/blue/bullet-dot.svg
 create mode 100644 public/assets/images/icons/blue/bullet-double-arrow.svg
 create mode 100644 public/assets/images/icons/blue/bullet-line.svg
 create mode 100644 public/assets/images/icons/blue/category-draft.svg
 create mode 100644 public/assets/images/icons/blue/category-others.svg
 create mode 100644 public/assets/images/icons/blue/category-portfolio.svg
 create mode 100644 public/assets/images/icons/blue/category-task.svg
 create mode 100644 public/assets/images/icons/blue/category-template.svg
 create mode 100644 public/assets/images/icons/green/bullet-arrow.svg
 create mode 100644 public/assets/images/icons/green/bullet-dot.svg
 create mode 100644 public/assets/images/icons/green/bullet-double-arrow.svg
 create mode 100644 public/assets/images/icons/green/bullet-line.svg
 create mode 100644 public/assets/images/icons/green/category-draft.svg
 create mode 100644 public/assets/images/icons/green/category-others.svg
 create mode 100644 public/assets/images/icons/green/category-portfolio.svg
 create mode 100644 public/assets/images/icons/green/category-task.svg
 create mode 100644 public/assets/images/icons/green/category-template.svg
 create mode 100644 public/assets/images/icons/green/content2.svg
 create mode 100644 public/assets/images/icons/grey/bullet-arrow.svg
 create mode 100644 public/assets/images/icons/grey/bullet-dot.svg
 create mode 100644 public/assets/images/icons/grey/bullet-double-arrow.svg
 create mode 100644 public/assets/images/icons/grey/bullet-line.svg
 create mode 100644 public/assets/images/icons/grey/category-draft.svg
 create mode 100644 public/assets/images/icons/grey/category-others.svg
 create mode 100644 public/assets/images/icons/grey/category-portfolio.svg
 create mode 100644 public/assets/images/icons/grey/category-task.svg
 create mode 100644 public/assets/images/icons/grey/category-template.svg
 create mode 100644 public/assets/images/icons/grey/content2.svg
 create mode 100644 public/assets/images/icons/red/bullet-arrow.svg
 create mode 100644 public/assets/images/icons/red/bullet-dot.svg
 create mode 100644 public/assets/images/icons/red/bullet-double-arrow.svg
 create mode 100644 public/assets/images/icons/red/bullet-line.svg
 create mode 100644 public/assets/images/icons/red/category-draft.svg
 create mode 100644 public/assets/images/icons/red/category-others.svg
 create mode 100644 public/assets/images/icons/red/category-portfolio.svg
 create mode 100644 public/assets/images/icons/red/category-task.svg
 create mode 100644 public/assets/images/icons/red/category-template.svg
 create mode 100644 public/assets/images/icons/red/content2.svg
 create mode 100644 public/assets/images/icons/white/bullet-arrow.svg
 create mode 100644 public/assets/images/icons/white/bullet-dot.svg
 create mode 100644 public/assets/images/icons/white/bullet-double-arrow.svg
 create mode 100644 public/assets/images/icons/white/bullet-line.svg
 create mode 100644 public/assets/images/icons/white/category-draft.svg
 create mode 100644 public/assets/images/icons/white/category-others.svg
 create mode 100644 public/assets/images/icons/white/category-portfolio.svg
 create mode 100644 public/assets/images/icons/white/category-task.svg
 create mode 100644 public/assets/images/icons/white/category-template.svg
 create mode 100644 public/assets/images/icons/white/content2.svg
 create mode 100644 public/assets/images/icons/yellow/bullet-arrow.svg
 create mode 100644 public/assets/images/icons/yellow/bullet-dot.svg
 create mode 100644 public/assets/images/icons/yellow/bullet-double-arrow.svg
 create mode 100644 public/assets/images/icons/yellow/bullet-line.svg
 create mode 100644 public/assets/images/icons/yellow/category-draft.svg
 create mode 100644 public/assets/images/icons/yellow/category-others.svg
 create mode 100644 public/assets/images/icons/yellow/category-portfolio.svg
 create mode 100644 public/assets/images/icons/yellow/category-task.svg
 create mode 100644 public/assets/images/icons/yellow/category-template.svg
 create mode 100644 public/assets/images/icons/yellow/content2.svg
 create mode 100755 resources/vue/components/courseware/AdminApp.vue
 create mode 100755 resources/vue/components/courseware/ContentBookmarkApp.vue
 create mode 100755 resources/vue/components/courseware/ContentOverviewApp.vue
 create mode 100755 resources/vue/components/courseware/CoursewareAdminActionWidget.vue
 create mode 100755 resources/vue/components/courseware/CoursewareAdminTemplates.vue
 create mode 100755 resources/vue/components/courseware/CoursewareAdminViewWidget.vue
 create mode 100755 resources/vue/components/courseware/CoursewareBlockDiscussion.vue
 create mode 100755 resources/vue/components/courseware/CoursewareContentBookmarkFilterWidget.vue
 create mode 100755 resources/vue/components/courseware/CoursewareContentBookmarks.vue
 create mode 100755 resources/vue/components/courseware/CoursewareContentOverviewActionWidget.vue
 create mode 100755 resources/vue/components/courseware/CoursewareContentOverviewElements.vue
 create mode 100755 resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue
 create mode 100755 resources/vue/components/courseware/CoursewareDashboardStudents.vue
 create mode 100755 resources/vue/components/courseware/CoursewareDashboardTasks.vue
 create mode 100755 resources/vue/components/courseware/CoursewareDashboardViewWidget.vue
 create mode 100644 resources/vue/components/courseware/CoursewareDateInput.vue
 create mode 100755 resources/vue/components/courseware/CoursewareManagerTaskDistributor.vue
 create mode 100755 resources/vue/components/courseware/CoursewareStructuralElementComments.vue
 create mode 100755 resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue
 create mode 100755 resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue
 create mode 100755 resources/vue/courseware-admin-app.js
 create mode 100755 resources/vue/courseware-content-bookmark-app.js
 create mode 100755 resources/vue/courseware-content-overview-app.js
 create mode 100755 resources/vue/mixins/courseware/task-helper.js
 create mode 100755 resources/vue/store/courseware/courseware-admin.module.js

diff --git a/app/controllers/admin/courseware.php b/app/controllers/admin/courseware.php
new file mode 100755
index 00000000000..964ff4de13a
--- /dev/null
+++ b/app/controllers/admin/courseware.php
@@ -0,0 +1,34 @@
+<?php
+
+class Admin_CoursewareController extends AuthenticatedController
+{
+    public function before_filter(&$action, &$args)
+    {
+        parent::before_filter($action, $args);
+        $GLOBALS['perm']->check('root');
+        PageLayout::setTitle(_('Coursewareverwaltung'));
+        Navigation::activateItem('/admin/locations/courseware');
+    }
+
+    public function index_action()
+    {
+        $this->setSidebar();
+    }
+
+    private function setSidebar()
+    {
+        $sidebar = Sidebar::Get();
+        $views = new TemplateWidget(
+            _('Ansichten'),
+            $this->get_template_factory()->open('admin/courseware/admin_view_widget')
+        );
+        $sidebar->addWidget($views)->addLayoutCSSClass('courseware-admin-view-widget');
+
+        $views = new TemplateWidget(
+            _('Aktionen'),
+            $this->get_template_factory()->open('admin/courseware/admin_action_widget')
+        );
+        $sidebar->addWidget($views)->addLayoutCSSClass('courseware-admin-action-widget');
+    }
+
+}
\ No newline at end of file
diff --git a/app/controllers/contents/courseware.php b/app/controllers/contents/courseware.php
index c00f3e08192..ffec9de5a98 100755
--- a/app/controllers/contents/courseware.php
+++ b/app/controllers/contents/courseware.php
@@ -30,18 +30,34 @@ class Contents_CoursewareController extends AuthenticatedController
      * @SuppressWarnings(PHPMD.Superglobals)
      * @SuppressWarnings(PHPMD.UnusedFormalParameter)
      */
-    public function index_action($action = false, $widgetId = null)
+    public function index_action()
     {
-        Navigation::activateItem('/contents/courseware/projects');
-        $this->setProjectsSidebar($action);
-        $this->courseware_root = StructuralElement::getCoursewareUser($this->user->id);
+        Navigation::activateItem('/contents/courseware/overview');
+        $this->user_id = $GLOBALS['user']->id;
+        $this->setOverviewSidebar();
+        $this->courseware_root = \Courseware\StructuralElement::getCoursewareUser($this->user_id);
         if (!$this->courseware_root) {
             // create initial courseware dataset
-            $new = StructuralElement::createEmptyCourseware($this->user->id, 'user');
+            $new = \Courseware\StructuralElement::createEmptyCourseware($this->user_id, 'user');
             $this->courseware_root = $new->getRoot();
         }
+        $this->licenses = $this->getLicences();
+    }
+
+    private function setOverviewSidebar()
+    {
+        $sidebar = Sidebar::Get();
+        $views = new TemplateWidget(
+            _('Aktionen'),
+            $this->get_template_factory()->open('contents/courseware/overview_action_widget')
+        );
+        $sidebar->addWidget($views)->addLayoutCSSClass('courseware-overview-filter-widget');
 
-        $this->elements = $this->getProjects('all');
+        $views = new TemplateWidget(
+            _('Filter'),
+            $this->get_template_factory()->open('contents/courseware/overview_filter_widget')
+        );
+        $sidebar->addWidget($views)->addLayoutCSSClass('courseware-overview-filter-widget');
     }
 
     /**
@@ -87,12 +103,7 @@ class Contents_CoursewareController extends AuthenticatedController
         $last[$this->user_id] = $this->entry_element_id;
         UserConfig::get($this->user_id)->store('COURSEWARE_LAST_ELEMENT', $last);
 
-        $this->licenses = array();
-        $sorm_licenses = License::findBySQL("1 ORDER BY name ASC");
-        foreach($sorm_licenses as $license) {
-            array_push($this->licenses, $license->toArray());
-        }
-        $this->licenses = json_encode($this->licenses);
+        $this->licenses = $this->getLicences();
 
         $this->oer_enabled = Config::get()->OERCAMPUS_ENABLED && $perm->have_perm(Config::get()->OER_PUBLIC_STATUS);
     }
@@ -111,8 +122,16 @@ class Contents_CoursewareController extends AuthenticatedController
             $this->get_template_factory()->open('course/courseware/view_widget')
         );
         $sidebar->addWidget($views)->addLayoutCSSClass('courseware-view-widget');
+    }
 
-
+    private function getLicences()
+    {
+        $licenses = array();
+        $sorm_licenses = License::findBySQL("1 ORDER BY name ASC");
+        foreach($sorm_licenses as $license) {
+            array_push($licenses, $license->toArray());
+        }
+        return json_encode($licenses);
     }
 
     /**
@@ -141,31 +160,21 @@ class Contents_CoursewareController extends AuthenticatedController
      * @SuppressWarnings(PHPMD.UnusedFormalParameter)
      */
 
-    public function bookmarks_action($action = false, $widgetId = null)
+    public function bookmarks_action()
     {
         Navigation::activateItem('/contents/courseware/bookmarks');
-        $this->bookmarks = array();
-        $cw_bookmarks =  Courseware\Bookmark::findUsersBookmarks($this->user->id);
-        foreach($cw_bookmarks as $bookmark) {
-            $bm = array();
-            $bm['bookmark'] = $bookmark;
-            $element = Courseware\StructuralElement::find($bookmark->element_id);
-            if(empty($element)) {
-                continue;
-            }
-            $element['payload'] = json_decode($element['payload'], true);
-            $bm['element'] = $element;
-            if ($element->range_type === 'course') {
-                $bm['url'] = URLHelper::getURL('dispatch.php/course/courseware/?cid='.$element['range_id'].'#/structural_element/'.$element['id']);
-                $bm['course'] = Course::find($element['range_id']);
-            }
-            if ($element->range_type === 'user' && $element->range_id === $this->user->id) {
-                $bm['url'] = URLHelper::getURL('dispatch.php/contents/courseware/courseware#/structural_element/'.$element['id']);
-                $bm['user'] = $this->user;
-            }
+        $this->user_id = $GLOBALS['user']->id;
+        $this->setBookmarkSidebar();
+    }
 
-            array_push($this->bookmarks, $bm);
-        }
+    private function setBookmarkSidebar()
+    {
+        $sidebar = Sidebar::Get();
+        $views = new TemplateWidget(
+            _('Filter'),
+            $this->get_template_factory()->open('contents/courseware/bookmark_filter_widget')
+        );
+        $sidebar->addWidget($views)->addLayoutCSSClass('courseware-bookmark-filter-widget');
     }
 
     /**
@@ -419,4 +428,11 @@ class Contents_CoursewareController extends AuthenticatedController
         $actions->addLink(_('Neues Lernmaterial anlegen'), $this->url_for('contents/courseware/create_project'), Icon::create('add', 'clickable'))->asDialog('size=700');
         $sidebar->addWidget($actions);
     }
+
+    public function pdf_export_action($element_id)
+    {
+        $element = \Courseware\StructuralElement::findOneById($element_id);
+
+        $this->render_pdf($element->pdfExport($this->user), trim($element->title).'.pdf');
+    }
 }
diff --git a/app/controllers/course/courseware.php b/app/controllers/course/courseware.php
index 1d93a0a7516..7c8cd786794 100755
--- a/app/controllers/course/courseware.php
+++ b/app/controllers/course/courseware.php
@@ -7,6 +7,7 @@ use Courseware\UserProgress;
 /**
  * @property ?string $entry_element_id
  * @property int $last_visitdate
+ * @property mixed $course_id
  * @property mixed $courseware_progress_data
  * @property mixed $courseware_chapter_counter
  */
@@ -71,10 +72,11 @@ class Course_CoursewareController extends AuthenticatedController
     public function dashboard_action(): void
     {
         global $perm, $user;
-        $course_progress = $perm->have_studip_perm('dozent', Context::getId(), $user->id);
-        $this->courseware_progress_data = $this->getProgressData($course_progress);
+        $this->is_teacher = $perm->have_studip_perm('tutor', Context::getId(), $user->id);
+        $this->courseware_progress_data = $this->getProgressData($this->is_teacher);
         $this->courseware_chapter_counter = $this->getChapterCounter($this->courseware_progress_data);
         Navigation::activateItem('course/courseware/dashboard');
+        $this->setDashboardSidebar();
     }
 
     public function manager_action(): void
@@ -89,6 +91,13 @@ class Course_CoursewareController extends AuthenticatedController
         }
     }
 
+    public function pdf_export_action($element_id)
+    {
+        $element = \Courseware\StructuralElement::findOneById($element_id);
+
+        $this->render_pdf($element->pdfExport($this->user), trim($element->title).'.pdf');
+    }
+
     private function setIndexSidebar(): void
     {
         $sidebar = Sidebar::Get();
@@ -105,6 +114,17 @@ class Course_CoursewareController extends AuthenticatedController
         $sidebar->addWidget($views)->addLayoutCSSClass('courseware-view-widget');
     }
 
+
+    private function setDashboardSidebar(): void
+    {
+        $sidebar = Sidebar::Get();
+        $views = new TemplateWidget(
+            _('Ansichten'),
+            $this->get_template_factory()->open('course/courseware/dashboard_view_widget')
+        );
+        $sidebar->addWidget($views)->addLayoutCSSClass('courseware-dashboard-view-widget');
+    }
+
     private function getProgressData(bool $showProgressForAllParticipants = false): iterable
     {
         /** @var ?\Course $course */
diff --git a/app/routes/Activity.php b/app/routes/Activity.php
index 8f6127fbeea..a37c01ec7f7 100644
--- a/app/routes/Activity.php
+++ b/app/routes/Activity.php
@@ -70,6 +70,17 @@ class Activity extends \RESTAPI\RouteMap
         $scrollfrom = \Request::int('scrollfrom', false);
         $filtertype = \Request::get('filtertype', '');
 
+        $objectType = \Request::get('object_type', '');
+        $filter->setObjectType($objectType);
+        
+        $objectId = \Request::get('object_id', '');
+        $filter->setObjectId($objectId);
+
+        $context = \Request::get('context_type', '');
+        $filter->setContext($context);
+
+        $contextId = \Request::get('context_id', '');
+        $filter->setContextId($contextId);
 
         if (!empty($filtertype)) {
             $filter->setType(json_decode($filtertype));
diff --git a/app/views/admin/courseware/admin_action_widget.php b/app/views/admin/courseware/admin_action_widget.php
new file mode 100755
index 00000000000..85f366ce3a6
--- /dev/null
+++ b/app/views/admin/courseware/admin_action_widget.php
@@ -0,0 +1 @@
+<aside id="courseware-admin-action-widget" class="widget-sidebar"></aside>
\ No newline at end of file
diff --git a/app/views/admin/courseware/admin_view_widget.php b/app/views/admin/courseware/admin_view_widget.php
new file mode 100755
index 00000000000..a46667c854d
--- /dev/null
+++ b/app/views/admin/courseware/admin_view_widget.php
@@ -0,0 +1 @@
+<aside id="courseware-admin-view-widget" class="widget-sidebar"></aside>
\ No newline at end of file
diff --git a/app/views/admin/courseware/index.php b/app/views/admin/courseware/index.php
new file mode 100755
index 00000000000..b40dc545db6
--- /dev/null
+++ b/app/views/admin/courseware/index.php
@@ -0,0 +1 @@
+<div id="courseware-admin-app"></div>
\ No newline at end of file
diff --git a/app/views/contents/courseware/bookmark_filter_widget.php b/app/views/contents/courseware/bookmark_filter_widget.php
new file mode 100755
index 00000000000..8bfb00e1601
--- /dev/null
+++ b/app/views/contents/courseware/bookmark_filter_widget.php
@@ -0,0 +1 @@
+<aside id="courseware-content-bookmark-filter-widget" class="widget-sidebar"></aside>
\ No newline at end of file
diff --git a/app/views/contents/courseware/bookmarks.php b/app/views/contents/courseware/bookmarks.php
index 14975e57f3f..a0803209707 100755
--- a/app/views/contents/courseware/bookmarks.php
+++ b/app/views/contents/courseware/bookmarks.php
@@ -1,34 +1,6 @@
-<div class="cw-bookmarks">
-    <? if(!empty($bookmarks)): ?>
-    <ul class="cw-tiles">
-        <? foreach($bookmarks as $bookmark) :?>
-            <li class="tile <?= htmlReady($bookmark['element']['payload']['color'])?>">
-                <a href="<?= htmlReady($bookmark['url'])?>">
-                    <? if ($element->getImageUrl() === null) : ?>
-                        <div class="preview-image default-image"></div>
-                    <? else : ?>
-                        <div class="preview-image" style="background-image: url(<?= htmlReady($element->getImageUrl()) ?>)" ></div>
-                    <? endif; ?>
-
-                    <div class="description">
-                        <header><?= htmlReady($bookmark['element']['title']) ?></header>
-                        <div class="description-text-wrapper">
-                            <p><?= htmlReady($bookmark['element']['payload']['description']) ?></p>
-                        </div>
-                        <footer>
-                        <? if($bookmark['course']): ?>
-                            <?= Icon::create('seminar', Icon::ROLE_INFO_ALT)?> <?= htmlReady($bookmark['course']['name'])?>
-                        <? endif; ?>
-                        <? if($bookmark['user']): ?>
-                            <?= Icon::create('headache', Icon::ROLE_INFO_ALT)?> <?= htmlReady($bookmark['user']->getFullName())?>
-                        <? endif; ?>
-                        </footer>
-                    </div>
-                </a>
-            </li>
-        <? endforeach; ?>
-    </ul>
-    <? else: ?>
-        <?= MessageBox::info(_('Sie haben noch keine Lesezeichen angelegt.')); ?>
-    <? endif; ?>
+<div
+    id="courseware-content-bookmark-app"
+    entry-type="users"
+    entry-id="<?= $user_id ?>"
+>
 </div>
diff --git a/app/views/contents/courseware/courses_overview.php b/app/views/contents/courseware/courses_overview.php
index 4786af7a110..f6a3d98816f 100644
--- a/app/views/contents/courseware/courses_overview.php
+++ b/app/views/contents/courseware/courses_overview.php
@@ -1,4 +1,4 @@
-<div class="cw-content-projects">
+<div class="cw-content-courses">
     <? if (empty($sem_courses)) : ?>
         <? if (!$all_semesters) : ?>
         <h2>
diff --git a/app/views/contents/courseware/index.php b/app/views/contents/courseware/index.php
index 5682e67e5cb..c0d761d0dc3 100755
--- a/app/views/contents/courseware/index.php
+++ b/app/views/contents/courseware/index.php
@@ -1,43 +1,10 @@
-<div class="cw-content-projects">
-    <? if (!empty($elements)): ?>
-        <ul class="cw-tiles">
-            <? foreach ($elements as $element) :?>
-                <li class="tile <?= htmlReady($element['payload']['color'])?>">
-                    <a href="<?= URLHelper::getLink('dispatch.php/contents/courseware/courseware#/structural_element/'.$element['id']) ?>">
-                        <? if ($element->getImageUrl() === null) : ?>
-                            <div class="preview-image default-image"></div>
-                        <? else : ?>
-                            <div class="preview-image" style="background-image: url(<?= htmlReady($element->getImageUrl()) ?>)" ></div>
-                        <? endif; ?>
-                        <div class="description">
-                            <header><?= htmlReady($element['title']) ?></header>
-                            <div class="description-text-wrapper">
-                                <p>
-                                    <?= htmlReady($element['payload']['description']) ?>
-                                </p>
-                            </div>
-                            <footer>
-                                <?= sprintf(ngettext('%d Seite', '%d Seiten', $element->countChildren()), $element->countChildren()); ?>
-                            </footer>
-                        </div>
-                    </a>
-                </li>
-            <? endforeach; ?>
-        </ul>
-    <? else : ?>
-        <div class="cw-contents-overview-teaser">
-            <div class="cw-contents-overview-teaser-content">
-                <header><?= _('Ihre persönlichen Lernmaterialien')?></header>
-                <p><?= _('Erstellen und Verwalten Sie hier ihre eigenen persönlichen Lernmaterialien in Form von ePorfolios, 
-                          Vorlagen für Veranstaltungen oder einfach nur persönliche Inhalte für das Studium. 
-                          Entwickeln Sie ihre eigenen (Lehr-)Materialien für Studium oder die Lehre und teilen diese mit anderen Nutzenden.')?></p>
-                <a class="button"
-                href="<?= $controller->link_for('contents/courseware/create_project', []) ?>"
-                data-dialog="size=700"
-                title="<?= _('Neues Lernmaterial anlegen') ?>">
-                    <?= _('Neues Lernmaterial anlegen') ?>
-                </a>
-            </div>
-        </div>
-    <? endif; ?> 
+<script>
+    STUDIP.COURSEWARE_USERS_ROOT_ID = <?=$courseware_root->id ?>
+</script>
+<div
+    id="courseware-content-overview-app"
+    entry-type="users"
+    entry-id="<?= $user_id ?>"
+    licenses='<?= $licenses ?>'
+>
 </div>
diff --git a/app/views/contents/courseware/overview_action_widget.php b/app/views/contents/courseware/overview_action_widget.php
new file mode 100755
index 00000000000..7f45e5e2298
--- /dev/null
+++ b/app/views/contents/courseware/overview_action_widget.php
@@ -0,0 +1 @@
+<aside id="courseware-content-overview-action-widget" class="widget-sidebar"></aside>
\ No newline at end of file
diff --git a/app/views/contents/courseware/overview_filter_widget.php b/app/views/contents/courseware/overview_filter_widget.php
new file mode 100755
index 00000000000..b0f767799b0
--- /dev/null
+++ b/app/views/contents/courseware/overview_filter_widget.php
@@ -0,0 +1 @@
+<aside id="courseware-content-overview-filter-widget" class="widget-sidebar"></aside>
\ No newline at end of file
diff --git a/app/views/course/courseware/dashboard.php b/app/views/course/courseware/dashboard.php
index d666b0d55fa..830cc90560d 100755
--- a/app/views/course/courseware/dashboard.php
+++ b/app/views/course/courseware/dashboard.php
@@ -1,9 +1,12 @@
-
-
-
 <script>
     STUDIP.courseware_progress_data = <?= json_encode($courseware_progress_data);?>;
     STUDIP.courseware_chapter_counter = <?= json_encode($courseware_chapter_counter);?>;
+    STUDIP.is_teacher = <?= json_encode($is_teacher);?>;
 </script>
 
-<div id="courseware-dashboard-app"></div>
+<div
+    id="courseware-dashboard-app"
+    entry-type="courses"
+    entry-id="<?= Context::getId() ?>"
+>
+</div>
diff --git a/app/views/course/courseware/dashboard_view_widget.php b/app/views/course/courseware/dashboard_view_widget.php
new file mode 100755
index 00000000000..4e9075aac21
--- /dev/null
+++ b/app/views/course/courseware/dashboard_view_widget.php
@@ -0,0 +1 @@
+<aside id="courseware-dashboard-view-widget" class="widget-sidebar"></aside>
\ No newline at end of file
diff --git a/db/migrations/5.1.17_add_courseware_templates.php b/db/migrations/5.1.17_add_courseware_templates.php
new file mode 100755
index 00000000000..bf758972c6a
--- /dev/null
+++ b/db/migrations/5.1.17_add_courseware_templates.php
@@ -0,0 +1,34 @@
+<?php
+
+class AddCoursewareTemplates extends \Migration
+{
+    public function description()
+    {
+        return 'Create Courseware template database tables';
+    }
+
+    public function up()
+    {
+        $db = \DBManager::get();
+
+        $db->exec("CREATE TABLE `cw_templates` (
+            `id` int(11) NOT NULL AUTO_INCREMENT,
+            `name` varchar(255) NOT NULL,
+            `purpose` ENUM('content', 'template', 'oer', 'portfolio', 'draft', 'other') COLLATE latin1_bin,
+            `structure` MEDIUMTEXT NOT NULL,
+            `mkdate` int(11) NOT NULL,
+            `chdate` int(11) NOT NULL,
+
+            PRIMARY KEY (`id`)
+            )
+        ");
+
+    }
+
+    public function down()
+    {
+        $db = \DBManager::get();
+
+        $db->exec("DROP TABLE IF EXISTS `cw_templates`");
+    }
+}
diff --git a/db/migrations/5.1.18_add_courseware_tasks.php b/db/migrations/5.1.18_add_courseware_tasks.php
new file mode 100755
index 00000000000..2cd45945c9c
--- /dev/null
+++ b/db/migrations/5.1.18_add_courseware_tasks.php
@@ -0,0 +1,78 @@
+<?php
+
+class AddCoursewareTasks extends \Migration
+{
+    public function description()
+    {
+        return 'Create Courseware Task database tables and settings';
+    }
+
+    public function up()
+    {
+        $db = \DBManager::get();
+
+        $db->exec("CREATE TABLE `cw_task_groups` (
+            `id` int(11) NOT NULL AUTO_INCREMENT,
+            `seminar_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
+            `lecturer_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
+            `target_id` int(11) NOT NULL,
+            `task_template_id` int(11) NOT NULL,
+            `solver_may_add_blocks` tinyint(1) NOT NULL,
+            `title` varchar(255) NOT NULL,
+            `mkdate` int(11) NOT NULL,
+            `chdate` int(11) NOT NULL,
+
+            PRIMARY KEY (`id`),
+            INDEX index_seminar_id (`seminar_id`),
+            INDEX index_lecturer_id (`lecturer_id`)
+            )
+        ");
+
+        $db->exec("CREATE TABLE `cw_tasks` (
+            `id` int(11) NOT NULL AUTO_INCREMENT,
+            `task_group_id` int(11) NOT NULL,
+            `structural_element_id` int(11) NOT NULL,
+            `solver_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
+            `solver_type` ENUM('autor', 'group') COLLATE latin1_bin,
+            `submission_date` int(11) NOT NULL,
+            `submitted` tinyint(1) NOT NULL,
+            `renewal` ENUM('pending', 'granted', 'declined') COLLATE latin1_bin,
+            `renewal_date` int(11) NOT NULL,
+            `feedback_id` int(11) NULL DEFAULT NULL,
+            `mkdate` int(11) NOT NULL,
+            `chdate` int(11) NOT NULL,
+
+            PRIMARY KEY (`id`),
+            INDEX index_task_group_id (`task_group_id`),
+            INDEX index_structural_element_id (`structural_element_id`),
+            INDEX index_solver_id (`solver_id`)
+            )
+        ");
+
+        $db->exec("CREATE TABLE `cw_task_feedbacks` (
+            `id` int(11) NOT NULL AUTO_INCREMENT,
+            `task_id` int(11) NOT NULL,
+            `lecturer_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
+            `content` MEDIUMTEXT NOT NULL,
+            `mkdate` int(11) NOT NULL,
+            `chdate` int(11) NOT NULL,
+
+            PRIMARY KEY (`id`),
+            INDEX index_task_id (`task_id`),
+            INDEX index_lecturer_id (`lecturer_id`)
+            )
+        ");
+
+        $db->exec("ALTER TABLE `cw_structural_elements`
+            CHANGE `purpose` `purpose` ENUM('content','draft','task','template','oer','other','portfolio')
+            CHARACTER SET latin1 COLLATE latin1_bin NULL DEFAULT NULL;"
+        );
+    }
+
+    public function down()
+    {
+        $db = \DBManager::get();
+
+        $db->exec("DROP TABLE IF EXISTS `cw_tasks`, `cw_task_feedbacks`");
+    }
+}
diff --git a/db/migrations/5.1.19_add_courseware_structural_element_discussion.php b/db/migrations/5.1.19_add_courseware_structural_element_discussion.php
new file mode 100755
index 00000000000..e10f52dbf6e
--- /dev/null
+++ b/db/migrations/5.1.19_add_courseware_structural_element_discussion.php
@@ -0,0 +1,49 @@
+<?php
+
+class AddCoursewareStructuralElementDiscussion extends \Migration
+{
+    public function description()
+    {
+        return 'Create Courseware structural element database tables for discussions';
+    }
+
+    public function up()
+    {
+        $db = \DBManager::get();
+
+        $db->exec("CREATE TABLE `cw_structural_element_comments` (
+            `id` int(11) NOT NULL AUTO_INCREMENT,
+            `structural_element_id` int(11) NOT NULL,
+            `user_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
+            `comment` MEDIUMTEXT NOT NULL,
+            `mkdate` int(11) NOT NULL,
+            `chdate` int(11) NOT NULL,
+
+            PRIMARY KEY (`id`),
+            INDEX index_structural_element_id (`structural_element_id`),
+            INDEX index_user_id (`user_id`)
+            )
+        ");
+
+        $db->exec("CREATE TABLE `cw_structural_element_feedbacks` (
+            `id` int(11) NOT NULL AUTO_INCREMENT,
+            `structural_element_id` int(11) NOT NULL,
+            `user_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
+            `feedback` MEDIUMTEXT NOT NULL,
+            `mkdate` int(11) NOT NULL,
+            `chdate` int(11) NOT NULL,
+
+            PRIMARY KEY (`id`),
+            INDEX index_structural_element_id (`structural_element_id`),
+            INDEX index_user_id (`user_id`)
+            )
+        ");
+    }
+
+    public function down()
+    {
+        $db = \DBManager::get();
+
+        $db->exec("DROP TABLE IF EXISTS `cw_structural_element_feedbacks`, `cw_structural_element_comments`");
+    }
+}
\ No newline at end of file
diff --git a/lib/activities/Activity.php b/lib/activities/Activity.php
index b8ed6624ecf..b46c9b23be4 100644
--- a/lib/activities/Activity.php
+++ b/lib/activities/Activity.php
@@ -143,6 +143,7 @@ class Activity extends \SimpleORMap
             'passed'      => _('bestand %s'),
             'shared'      => _('teilte %s'),
             'sent'        => _('sendete %s'),
+            'set'         => _('stellte %s'),
             'voided'      => _('löschte %s')
         ];
 
diff --git a/lib/activities/ActivityObserver.php b/lib/activities/ActivityObserver.php
index 80652cf8920..5bfd4cc6fef 100644
--- a/lib/activities/ActivityObserver.php
+++ b/lib/activities/ActivityObserver.php
@@ -39,5 +39,39 @@ class ActivityObserver
 
         //Notifications for ScheduleProvider (Course)
         \NotificationCenter::addObserver('\Studip\Activity\ScheduleProvider', 'postActivity','CourseDidChangeSchedule');
+
+
+        // Notifications for CoursewareProvider
+        foreach (
+            [
+                \Courseware\Block::class,
+                \Courseware\BlockComment::class,
+                \Courseware\BlockFeedback::class,
+                \Courseware\StructuralElementComment::class,
+                \Courseware\StructuralElement::class,
+                \Courseware\StructuralElementFeedback::class,
+                \Courseware\Task::class,
+                \Courseware\TaskFeedback::class,
+            ] as $class
+        ) {
+            \NotificationCenter::addObserver(
+                \Studip\Activity\CoursewareProvider::class,
+                'postActivity',
+                $class . 'DidCreate'
+            );
+        }
+
+        foreach (
+            [
+                \Courseware\Block::class,
+                \Courseware\TaskFeedback::class
+            ] as $class
+        ) {
+            \NotificationCenter::addObserver(
+                \Studip\Activity\CoursewareProvider::class,
+                'postActivity',
+                $class . 'DidUpdate'
+            );
+        }
     }
 }
diff --git a/lib/activities/Context.php b/lib/activities/Context.php
index 3f84452cef3..9f7978f521f 100644
--- a/lib/activities/Context.php
+++ b/lib/activities/Context.php
@@ -10,6 +10,24 @@ namespace Studip\Activity;
 
 abstract class Context
 {
+    public static $objectTypes = [
+        'documents',
+        'message',
+        'news',
+        'participants',
+        'schedule',
+        'wiki',
+        'courseware', 
+        'forum'
+    ];
+
+    public static $contexTypes = [
+        'system',
+        'course',
+        'institute',
+        'user'
+    ];
+
     protected
         $provider,
         $observer;
@@ -63,6 +81,24 @@ abstract class Context
     public function getActivities(Filter $filter)
     {
         $providers = $this->filterProvider($this->getProvider(), $filter);
+
+        $query = 'context = ? AND context_id = ? AND mkdate >= ? AND mkdate <= ? ORDER BY mkdate DESC';
+        $params = [$this->getContextType(), $this->getRangeId(), $filter->getStartDate(), $filter->getEndDate()];
+
+        if ($filter->getContext() !== null && $filter->getContextId() !== null) {
+            $params = [$filter->getContext(), $filter->getContextId(), $filter->getStartDate(), $filter->getEndDate()];
+        }
+
+        if(\in_array($filter->getObjectType(), $this::$objectTypes)) {
+            $query = 'object_type = ? AND ' . $query;
+            \array_unshift($params, $filter->getObjectType());
+
+            //Object ID Filter only available when object type is set
+            if($filter->getObjectId() !== null && \strlen($filter->getObjectId()) > 0) {
+                $query = 'object_id = ? AND ' . $query;
+                \array_unshift($params, $filter->getObjectId());
+            }
+        }
         $activities = Activity::findAndMapBySQL(
             function ($activity) use ($providers) {
                 if (isset($providers[$activity->provider])) {                        // provider is available
@@ -72,9 +108,9 @@ abstract class Context
                     }
                 }
             },
-            'context = ? AND context_id = ?  AND mkdate >= ? AND mkdate <= ? ORDER BY mkdate DESC'
-            ,
-            [$this->getContextType(), $this->getRangeId(), $filter->getStartDate(), $filter->getEndDate()]);
+            $query,
+            $params
+        );
         return array_filter($activities);
     }
 
diff --git a/lib/activities/CourseContext.php b/lib/activities/CourseContext.php
index 382284748bd..d09a508ac6e 100644
--- a/lib/activities/CourseContext.php
+++ b/lib/activities/CourseContext.php
@@ -52,6 +52,9 @@ class CourseContext extends Context
             }
             //news
             $this->addProvider('Studip\Activity\NewsProvider');
+
+            //courseware
+            $this->addProvider('Studip\Activity\CoursewareProvider');
         }
 
         return $this->provider;
diff --git a/lib/activities/CoursewareProvider.php b/lib/activities/CoursewareProvider.php
index 5e5faaec948..335f7bb665c 100755
--- a/lib/activities/CoursewareProvider.php
+++ b/lib/activities/CoursewareProvider.php
@@ -2,29 +2,43 @@
 
 namespace Studip\Activity;
 
+use Courseware\Block;
+use Courseware\BlockComment;
+use Courseware\BlockFeedback;
+use Courseware\Container;
+use Courseware\StructuralElement;
+use Courseware\StructuralElementComment;
+use Courseware\StructuralElementFeedback;
+use Courseware\Task;
+use Courseware\TaskFeedback;
 
 class CoursewareProvider implements ActivityProvider
 {
-
     public function getActivityDetails($activity)
     {
-        $structural_element = \Courseware\StructuralElement::find($activity->object_id);
+        $structural_element = StructuralElement::find($activity->object_id);
         if (!$structural_element) {
             return false;
         }
-        $payload = json_decode($structural_element['payload']);
 
-        $activity->content = formatReady($payload['description']);
+        $activity->content = formatReady($activity->getValue('content'));
 
-        if ($activity->context == "course") {
-            $url =  \URLHelper::getURL('dispatch.php/course/courseware/?cid='). $activity->context_id . '#/structural_element/' . $structural_element->id;
+        if ($activity->context == 'course') {
+            $url =
+                \URLHelper::getURL('dispatch.php/course/courseware/?cid=') .
+                $activity->context_id .
+                '#/structural_element/' .
+                $structural_element->id;
             $activity->object_url = [
-                $url => _('Zur Courseware in der Veranstaltung')
+                $url => _('Zur Courseware in der Veranstaltung'),
             ];
-        } elseif ($activity->context == "user") {
-            $url =  \URLHelper::getURL('dispatch.php/contents/my_contents'). '#/structural_element/' . $structural_element->id;
+        } elseif ($activity->context == 'user') {
+            $url =
+                \URLHelper::getURL('dispatch.php/contents/my_contents') .
+                '#/structural_element/' .
+                $structural_element->id;
             $activity->object_url = [
-                $url => _('Zur eigenen Courseware')
+                $url => _('Zur eigenen Courseware'),
             ];
         }
 
@@ -33,6 +47,212 @@ class CoursewareProvider implements ActivityProvider
 
     public static function getLexicalField()
     {
-        return _('eine Courseware-Aktivität');
+        return _('einen Courseware-Inhalt');
+    }
+
+    /**
+     * TODO
+     *
+     * @param String  $event a notification for an activity
+     * @param \SimpleORMap  $resource
+     */
+    public static function postActivity($event, $resource)
+    {
+        $data = null;
+        switch ($event) {
+            case Block::class . 'DidCreate':
+                /**
+                 * @var \Courseware\Block $resource
+                 * @var \Courseware\StructuralElement $structuralElement
+                 */
+                $structuralElement = $resource->getStructuralElement();
+                $data = [
+                    'provider' => self::class,
+                    'context' => $structuralElement->range_type,
+                    'context_id' => $structuralElement->range_id,
+                    'content' => null,
+                    'actor_type' => 'user',
+                    'actor_id' => $resource->owner_id,
+                    'verb' => 'created',
+                    'object_id' => $structuralElement->id,
+                    'object_type' => 'courseware',
+                    'mkdate' => time(),
+                ];
+                break;
+
+            case Block::class . 'DidUpdate':
+                /**
+                 * @var \Courseware\Block $resource
+                 * @var \Courseware\StructuralElement $structuralElement
+                 */
+                $structuralElement = $resource->getStructuralElement();
+                $payload = $resource->type->getPayload();
+                if (
+                    (isset($payload['text']) && $payload['text'] != '') ||
+                    (isset($payload['content']) && $payload['content'] != '')
+                ) {
+                    $data = [
+                        'provider' => self::class,
+                        'context' => $structuralElement->range_type,
+                        'context_id' => $structuralElement->range_id,
+                        'content' => null,
+                        'actor_type' => 'user',
+                        'actor_id' => $resource->editor_id,
+                        'verb' => 'edited',
+                        'object_id' => $structuralElement->id,
+                        'object_type' => 'courseware',
+                        'mkdate' => time(),
+                    ];
+                }
+                break;
+
+            case BlockComment::class . 'DidCreate':
+                /**
+                 * @var \Courseware\BlockComment $resource
+                 * @var \Courseware\StructuralElement $structuralElement
+                 */
+                $structuralElement = $resource->getStructuralElement();
+                $data = [
+                    'provider' => self::class,
+                    'context' => $structuralElement->range_type,
+                    'context_id' => $structuralElement->range_id,
+                    'content' => $resource->comment,
+                    'actor_type' => 'user',
+                    'actor_id' => $resource->user_id,
+                    'verb' => 'interacted',
+                    'object_id' => $structuralElement->id,
+                    'object_type' => 'courseware',
+                    'mkdate' => time(),
+                ];
+                break;
+
+            case BlockFeedback::class . 'DidCreate':
+                /**
+                 * @var \Courseware\BlockFeedback $resource
+                 * @var \Courseware\StructuralElement $structuralElement
+                 */
+                $structuralElement = $resource->getStructuralElement();
+                $data = [
+                    'provider' => self::class,
+                    'context' => $structuralElement->range_type,
+                    'context_id' => $structuralElement->range_id,
+                    'content' => $resource->feedback,
+                    'actor_type' => 'user',
+                    'actor_id' => $resource->user_id,
+                    'verb' => 'answered',
+                    'object_id' => $structuralElement->id,
+                    'object_type' => 'courseware',
+                    'mkdate' => time(),
+                ];
+                break;
+
+            case StructuralElement::class . 'DidCreate':
+                /**
+                 * @var \Courseware\StructuralElement $resource
+                 */
+                if ($resource->range_type === 'courses') {
+                    $data = [
+                        'provider' => self::class,
+                        'context' => $resource->range_type,
+                        'context_id' => $resource->range_id,
+                        'content' => null,
+                        'actor_type' => 'user',
+                        'actor_id' => $resource->owner_id,
+                        'verb' => 'created',
+                        'object_id' => $resource->id,
+                        'object_type' => 'courseware',
+                        'mkdate' => time(),
+                    ];
+                }
+                break;
+
+            case StructuralElementComment::class . 'DidCreate':
+                /**
+                 * @var \Courseware\StructuralElementComment $resource
+                 * @var \Courseware\StructuralElement $structuralElement
+                 */
+                $structuralElement = $resource['structural_element'];
+                $data = [
+                    'provider' => self::class,
+                    'context' => $structuralElement->range_type,
+                    'context_id' => $structuralElement->range_id,
+                    'content' => $resource->comment,
+                    'actor_type' => 'user',
+                    'actor_id' => $resource->user_id,
+                    'verb' => 'interacted',
+                    'object_id' => $structuralElement->id,
+                    'object_type' => 'courseware',
+                    'mkdate' => time(),
+                ];
+                break;
+
+            case StructuralElementFeedback::class . 'DidCreate':
+                /**
+                 * @var \Courseware\StructuralElementFeedback $resource
+                 * @var \Courseware\StructuralElement $structuralElement
+                 */
+                $structuralElement = $resource['structural_element'];
+                $data = [
+                    'provider' => self::class,
+                    'context' => $structuralElement->range_type,
+                    'context_id' => $structuralElement->range_id,
+                    'content' => $resource->feedback,
+                    'actor_type' => 'user',
+                    'actor_id' => $resource->user_id,
+                    'verb' => 'answered',
+                    'object_id' => $structuralElement->id,
+                    'object_type' => 'courseware',
+                    'mkdate' => time(),
+                ];
+                break;
+
+            case Task::class . 'DidCreate':
+                /**
+                 * @var \Courseware\Task $resource
+                 * @var \Courseware\StructuralElement $structuralElement
+                 */
+                $structuralElement = $resource['structural_element'];
+                if ($structuralElement->range_type === 'courses') {
+                    $data = [
+                        'provider' => self::class,
+                        'context' => $structuralElement->range_type,
+                        'context_id' => $structuralElement->range_id,
+                        'content' => null,
+                        'actor_type' => 'user',
+                        'actor_id' => $resource->task_group->lecturer_id,
+                        'verb' => 'set',
+                        'object_id' => $structuralElement->id,
+                        'object_type' => 'courseware',
+                        'mkdate' => time(),
+                    ];
+                }
+                break;
+
+            case TaskFeedback::class . 'DidCreate':
+                /**
+                 * @var \Courseware\TaskFeedback $resource
+                 * @var \Courseware\StructuralElement $structuralElement
+                 */
+                $structuralElement = $resource->getStructuralElement();
+                if ($structuralElement->range_type === 'courses') {
+                    $data = [
+                        'provider' => self::class,
+                        'context' => $structuralElement->range_type,
+                        'context_id' => $structuralElement->range_id,
+                        'content' => $resource->content,
+                        'actor_type' => 'user',
+                        'actor_id' => $resource->lecturer_id,
+                        'verb' => 'answered',
+                        'object_id' => $structuralElement->id,
+                        'object_type' => 'courseware',
+                        'mkdate' => time(),
+                    ];
+                }
+                break;
+        }
+
+        if ($data) {
+            Activity::create($data);
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/activities/Filter.php b/lib/activities/Filter.php
index 8e1947e5be1..e904f584d8d 100644
--- a/lib/activities/Filter.php
+++ b/lib/activities/Filter.php
@@ -13,7 +13,11 @@ class Filter
         $start_date,
         $end_date,
         $type,
-        $verb;
+        $verb,
+        $objectType,
+        $objectId,
+        $context,
+        $contextId;
 
     /**
      *
@@ -86,4 +90,76 @@ class Filter
     {
         $this->verb = $verb;
     }
+
+    /**
+     *
+     * @return string
+     */
+    public function getObjectType()
+    {
+        return $this->objectType;
+    }
+
+    /**
+     *
+     * @param string $objectType
+     */
+    public function setObjectType($objectType)
+    {
+        $this->objectType = $objectType;
+    }
+
+    /**
+     *
+     * @return string
+     */
+    public function getObjectId()
+    {
+        return $this->objectId;
+    }
+
+    /**
+     *
+     * @param string $objectId
+     */
+    public function setObjectId($objectId)
+    {
+        $this->objectId = $objectId;
+    }
+
+    /**
+     *
+     * @return string
+     */
+    public function getContext()
+    {
+        return $this->context;
+    }
+
+    /**
+     *
+     * @param string $context
+     */
+    public function setContext($context)
+    {
+        $this->context = $context;
+    }
+
+    /**
+     *
+     * @return string
+     */
+    public function getContextId()
+    {
+        return $this->contextId;
+    }
+
+    /**
+     *
+     * @param string $contextId
+     */
+    public function setContextId($contextId)
+    {
+        $this->contextId = $contextId;
+    }
 }
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index a577f8bd0b9..408e57286fc 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -307,6 +307,13 @@ class RouteMap
         );
         $group->get('/courseware-instances/{id}/bookmarks', Routes\Courseware\BookmarkedStructuralElementsIndex::class);
 
+        $group->get('/users/{id}/courseware-bookmarks', Routes\Courseware\UsersBookmarkedStructuralElementsIndex::class);
+        $this->addRelationship(
+            $group,
+            '/users/{id}/relationships/courseware-bookmarks',
+            Routes\Courseware\Rel\UsersBookmarkedStructuralElements::class
+        );
+
         $group->get('/courseware-blocks/{id}', Routes\Courseware\BlocksShow::class);
         $group->post('/courseware-blocks', Routes\Courseware\BlocksCreate::class);
         $group->patch('/courseware-blocks/{id}', Routes\Courseware\BlocksUpdate::class);
@@ -390,6 +397,18 @@ class RouteMap
         // not a JSON route
         $group->post('/courseware-structural-elements/{id}/copy', Routes\Courseware\StructuralElementsCopy::class);
 
+        $group->get('/courseware-structural-elements/{id}/comments', Routes\Courseware\StructuralElementCommentsOfStructuralElementsIndex::class);
+        $group->post('/courseware-structural-element-comments', Routes\Courseware\StructuralElementCommentsCreate::class);
+        $group->get('/courseware-structural-element-comments/{id}', Routes\Courseware\StructuralElementCommentsShow::class);
+        $group->patch('/courseware-structural-element-comments/{id}', Routes\Courseware\StructuralElementCommentsUpdate::class);
+        $group->delete('/courseware-structural-element-comments/{id}', Routes\Courseware\StructuralElementCommentsDelete::class);
+
+        $group->get('/courseware-structural-elements/{id}/feedback', Routes\Courseware\StructuralElementFeedbackOfStructuralElementsIndex::class);
+        $group->post('/courseware-structural-element-feedback', Routes\Courseware\StructuralElementFeedbackCreate::class);
+        $group->get('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackShow::class);
+        $group->patch('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackUpdate::class);
+        $group->delete('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackDelete::class);
+
         $group->get('/courseware-blocks/{id}/user-data-field', Routes\Courseware\UserDataFieldOfBlocksShow::class);
         $group->get('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsShow::class);
         $group->patch('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsUpdate::class);
@@ -407,6 +426,27 @@ class RouteMap
         $group->get('/courseware-blocks/{id}/feedback', Routes\Courseware\BlockFeedbacksOfBlocksIndex::class);
         $group->post('/courseware-block-feedback', Routes\Courseware\BlockFeedbacksCreate::class);
         $group->get('/courseware-block-feedback/{id}', Routes\Courseware\BlockFeedbacksShow::class);
+        $group->patch('/courseware-block-feedback/{id}', Routes\Courseware\BlockFeedbacksUpdate::class);
+        $group->delete('/courseware-block-feedback/{id}', Routes\Courseware\BlockFeedbacksDelete::class);
+
+        $group->get('/courseware-tasks/{id}', Routes\Courseware\TasksShow::class);
+        $group->get('/courseware-tasks', Routes\Courseware\TasksIndex::class);
+        $group->patch('/courseware-tasks/{id}', Routes\Courseware\TasksUpdate::class);
+        $group->delete('/courseware-tasks/{id}', Routes\Courseware\TasksDelete::class);
+
+        $group->get('/courseware-task-groups/{id}', Routes\Courseware\TaskGroupsShow::class);
+        $group->post('/courseware-task-groups', Routes\Courseware\TaskGroupsCreate::class);
+
+        $group->get('/courseware-task-feedback/{id}', Routes\Courseware\TaskFeedbackShow::class);
+        $group->post('/courseware-task-feedback', Routes\Courseware\TaskFeedbackCreate::class);
+        $group->patch('/courseware-task-feedback/{id}', Routes\Courseware\TaskFeedbackUpdate::class);
+        $group->delete('/courseware-task-feedback/{id}', Routes\Courseware\TaskFeedbackDelete::class);
+
+        $group->get('/courseware-templates/{id}', Routes\Courseware\TemplatesShow::class);
+        $group->get('/courseware-templates', Routes\Courseware\TemplatesIndex::class);
+        $group->post('/courseware-templates', Routes\Courseware\TemplatesCreate::class);
+        $group->patch('/courseware-templates/{id}', Routes\Courseware\TemplatesUpdate::class);
+        $group->delete('/courseware-templates/{id}', Routes\Courseware\TemplatesDelete::class);
     }
 
     private function addAuthenticatedFilesRoutes(RouteCollectorProxy $group): void
diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php
index cfc15ce4dc2..04f955e52a3 100755
--- a/lib/classes/JsonApi/Routes/Courseware/Authority.php
+++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php
@@ -8,6 +8,10 @@ use Courseware\BlockFeedback;
 use Courseware\Container;
 use Courseware\Instance;
 use Courseware\StructuralElement;
+use Courseware\Task;
+use Courseware\TaskFeedback;
+use Courseware\TaskGroup;
+use Courseware\Template;
 use Courseware\UserDataField;
 use Courseware\UserProgress;
 use User;
@@ -133,6 +137,21 @@ class Authority
         return self::canShowCoursewareInstance($user, $resource);
     }
 
+    public static function canAddBookmarkToAUser(User $actor, User $user)
+    {
+        return $actor->id === $user->id;
+    }
+
+    public static function canModifyBookmarksOfAUser(User $actor, User $user)
+    {
+        return $actor->id === $user->id;
+    }
+
+    public static function canIndexBookmarksOfAUser(User $actor, User $user)
+    {
+        return $actor->id === $user->id;
+    }
+
     /**
      * @SuppressWarnings(PHPMD.Superglobals)
      */
@@ -183,8 +202,12 @@ class Authority
 
     public static function canUpdateBlockComment(User $user, BlockComment $resource)
     {
-        return $user->id == $resource->user_id;
-        // should dozent be able to update?
+        $perm = $GLOBALS['perm']->have_studip_perm(
+            $resource->block->container->structural_element->course->config->COURSEWARE_EDITING_PERMISSION,
+            $resource->block->container->structural_element->course->id,
+            $user->id
+        );
+        return $user->id === $resource->user_id || $perm;
     }
 
     public static function canDeleteBlockComment(User $user, BlockComment $resource)
@@ -216,4 +239,155 @@ class Authority
     {
         return self::canUploadStructuralElementsImage($user, $resource);
     }
+
+    public static function canShowTaskGroup(User $user, TaskGroup $resource): bool
+    {
+        return $resource['lecturer_id'] === $user->id;
+    }
+
+    public static function canShowTask(User $user, Task $resource): bool
+    {
+        return self::canUpdateTask($user, $resource);
+    }
+
+    public static function canIndexTasks(User $user): bool
+    {
+        // TODO: filtered index permissions are handled in the route
+        return $GLOBALS['perm']->have_perm('root', $user->id);
+    }
+
+    public static function canCreateTasks(User $user, StructuralElement $resource): bool
+    {
+        return $resource->hasEditingPermission($user);
+    }
+
+    public static function canUpdateTask(User $user, Task $resource): bool
+    {
+        return $resource->canUpdate($user);
+    }
+
+    public static function canDeleteTask(User $user, Task $resource): bool
+    {
+        return self::canCreateTasks($user, $resource['structural_element']);
+    }
+
+    public static function canCreateTaskFeedback(User $user, Task $resource): bool
+    {
+        return self::canCreateTasks($user, $resource['structural_element']);
+    }
+
+    public static function canShowTaskFeedback(User $user, Task $resource): bool
+    {
+        return self::canShowTask($user, $resource);
+    }
+
+    public static function canUpdateTaskFeedback(User $user, Task $resource): bool
+    {
+        return self::canCreateTaskFeedback($user, $resource);
+    }
+
+    public static function canDeleteTaskFeedback(User $user, Task $resource): bool
+    {
+        return self::canCreateTaskFeedback($user, $resource);
+    }
+
+
+    public static function canIndexStructuralElementComments(User $user, StructuralElement $resource)
+    {
+        return self::canShowStructuralElement($user, $resource);
+    }
+
+    public static function canShowStructuralElementComment(User $user, StructuralElementComment $resource)
+    {
+        return self::canShowStructuralElement($user, $resource);
+    }
+
+    public static function canCreateStructuralElementComment(User $user, StructuralElement $resource)
+    {
+        return self::canShowStructuralElement($user, $resource);
+    }
+
+    public static function canUpdateStructuralElementComment(User $user, StructuralElementComment $resource)
+    {
+        if ($GLOBALS['perm']->have_perm('root')) {
+            return true;
+        }
+
+        $perm = $GLOBALS['perm']->have_studip_perm(
+            $resource->structural_element->course->config->COURSEWARE_EDITING_PERMISSION,
+            $resource->structural_element->course->id,
+            $user->id
+        );
+
+        return $user->id == $resource->user_id || $perm;
+    }
+
+    public static function canDeleteStructuralElementComment(User $user, StructuralElementComment $resource)
+    {
+        return self::canUpdateStructuralElementComment($user, $resource);
+    }
+
+    public static function canIndexStructuralElementFeedback(User $user, StructuralElement $resource)
+    {
+        return self::canUpdateStructuralElement($user, $resource);
+    }
+
+    public static function canCreateStructuralElementFeedback(User $user, StructuralElement $resource)
+    {
+        if ($GLOBALS['perm']->have_perm('root')) {
+            return true;
+        }
+
+        $perm = $GLOBALS['perm']->have_studip_perm(
+            $resource->course->config->COURSEWARE_EDITING_PERMISSION,
+            $resource->course->id,
+            $user->id
+        );
+
+        return $perm;
+    }
+
+    public static function canUpdateStructuralElementFeedback(User $user, StructuralElementComment $resource)
+    {
+        return self::canCreateStructuralElementFeedback($user, $resource->structural_element);
+    }
+
+    public static function canShowStructuralElementFeedback(User $user, StructuralElementFeedback $resource)
+    {
+        return $resource->user_id === $user->id || self::canUpdateStructuralElement($resource->structural_element);
+    }
+
+    public static function canDeleteStructuralElementFeedback(User $user, StructuralElementComment $resource)
+    {
+        return self::canUpdateStructuralElementFeedback($user, $resource);
+    }
+
+
+    public static function canShowTemplate(User $user, Template $resource)
+    {
+        // templates are for everybody, aren't they?
+        return true;
+    }
+
+    public static function canIndexTemplates(User $user)
+    {
+        // templates are for everybody, aren't they?
+        return true;
+    }
+
+    public static function canCreateTemplate(User $user)
+    {
+        return $GLOBALS['perm']->have_perm('admin');
+    }
+
+    public static function canUpdateTemplate(User $user, Template $resource)
+    {
+        return self::canCreateTemplate($user);
+    }
+
+    public static function canDeleteTemplate(User $user, Template $resource)
+    {
+        return self::canCreateTemplate($user);
+    }
+
 }
diff --git a/lib/classes/JsonApi/Routes/Courseware/BlockCommentsCreate.php b/lib/classes/JsonApi/Routes/Courseware/BlockCommentsCreate.php
index eec88186d9f..07034cf3c8c 100755
--- a/lib/classes/JsonApi/Routes/Courseware/BlockCommentsCreate.php
+++ b/lib/classes/JsonApi/Routes/Courseware/BlockCommentsCreate.php
@@ -9,6 +9,9 @@ use JsonApi\Schemas\Courseware\Block as BlockSchema;
 use JsonApi\Schemas\Courseware\BlockComment as BlockCommentSchema;
 use Psr\Http\Message\ResponseInterface as Response;
 use Psr\Http\Message\ServerRequestInterface as Request;
+use Studip\Activity\Activity;
+use Studip\Activity\CoursewareProvider;
+use Courseware\Container;
 
 /**
  * Create a comment on a block.
diff --git a/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksCreate.php b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksCreate.php
index 10c4759dafe..05e197460be 100755
--- a/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksCreate.php
+++ b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksCreate.php
@@ -2,6 +2,7 @@
 
 namespace JsonApi\Routes\Courseware;
 
+use Courseware\Container;
 use JsonApi\Errors\AuthorizationFailedException;
 use JsonApi\JsonApiController;
 use JsonApi\Routes\ValidationTrait;
@@ -9,6 +10,8 @@ use JsonApi\Schemas\Courseware\Block as BlockSchema;
 use JsonApi\Schemas\Courseware\BlockFeedback as BlockFeedbackSchema;
 use Psr\Http\Message\ResponseInterface as Response;
 use Psr\Http\Message\ServerRequestInterface as Request;
+use Studip\Activity\Activity;
+use Studip\Activity\CoursewareProvider;
 
 /**
  * Create feedback on a block.
diff --git a/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksDelete.php b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksDelete.php
new file mode 100755
index 00000000000..fa416e577b0
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksDelete.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\BlockFeedback;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\ConflictException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Delete one comment on a block.
+ */
+class BlockFeedbacksDelete extends JsonApiController
+{
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = BlockFeedback::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        if (!Authority::canDeleteBlockFeedback($this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+        $resource->delete();
+
+        return $this->getCodeResponse(204);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksUpdate.php b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksUpdate.php
new file mode 100755
index 00000000000..51cc616478e
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksUpdate.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\BlockFeedback;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\Block as BlockSchema;
+use JsonApi\Schemas\Courseware\BlockFeedback as BlockFeedbackSchema;
+use JsonApi\Schemas\User as UserSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Update a feedback on a block
+ */
+class BlockFeedbacksUpdate extends JsonApiController
+{
+    use ValidationTrait, UserProgressesHelper;
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = BlockFeedback::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        $json = $this->validate($request, $resource);
+        if (!Authority::canUpdateBlockFeedback($this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+        $blockFeedback = $this->updateBlockFeedback($json, $resource);
+
+        return $this->getContentResponse($blockFeedback);
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     * @SuppressWarnings(CyclomaticComplexity)
+     * @SuppressWarnings(NPathComplexity)
+     */
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+        if (BlockFeedbackSchema::TYPE !== self::arrayGet($json, 'data.type')) {
+            return 'Wrong `type` member of document´s `data`.';
+        }
+        if (self::arrayGet($json, 'data.id') !== $data->id) {
+            return 'Mismatch in document `id`.';
+        }
+
+        if (!($feedback = self::arrayGet($json, 'data.attributes.feedback'))) {
+            return 'Missing `feedback` attribute.';
+        }
+        if (!is_string($feedback)) {
+            return 'Attribute `feedback` must be a string.';
+        }
+        if ($feedback == '') {
+            return 'Attribute `feedback` must not be empty.';
+        }
+
+        if (self::arrayHas($json, 'data.relationships.user')) {
+            if (!($user = $this->getUserFromJson($json))) {
+                return 'Invalid `user` relationship.';
+            }
+            if ($user->id !== $data['user_id']) {
+                return 'Cannot update `user` relationship.';
+            }
+        }
+
+        if (self::arrayHas($json, 'data.relationships.block')) {
+            if (!($block = $this->getBlockFromJson($json))) {
+                return 'Invalid `block` relationship.';
+            }
+            if ($block->id !== $data['block_id']) {
+                return 'Cannot update `block` relationship.';
+            }
+        }
+    }
+
+    private function getBlockFromJson($json)
+    {
+        if (!$this->validateResourceObject($json, 'data.relationships.block', BlockSchema::TYPE)) {
+            return null;
+        }
+        $blockId = self::arrayGet($json, 'data.relationships.block.data.id');
+
+        return \Courseware\Block::find($blockId);
+    }
+
+    private function getUserFromJson($json)
+    {
+        if (!$this->validateResourceObject($json, 'data.relationships.user', UserSchema::TYPE)) {
+            return null;
+        }
+        $userId = self::arrayGet($json, 'data.relationships.user.data.id');
+
+        return \User::find($userId);
+    }
+
+    private function updateBlockFeedback(array $json, \Courseware\BlockFeedback $resource)
+    {
+        $resource->feedback = self::arrayGet($json, 'data.attributes.feedback', '');
+        $resource->store();
+
+        return $resource;
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php b/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php
index c29020ca6e6..04c7d928b1f 100755
--- a/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php
+++ b/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php
@@ -11,6 +11,8 @@ use JsonApi\Schemas\Courseware\Block as BlockSchema;
 use JsonApi\Schemas\Courseware\Container as ContainerSchema;
 use Psr\Http\Message\ResponseInterface as Response;
 use Psr\Http\Message\ServerRequestInterface as Request;
+use Studip\Activity\Activity;
+use Studip\Activity\CoursewareProvider;
 
 /**
  * Create a block in a container.
diff --git a/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php b/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php
index 09eb9b2b892..fcec2bb3697 100755
--- a/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php
+++ b/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php
@@ -3,6 +3,7 @@
 namespace JsonApi\Routes\Courseware;
 
 use Courseware\Block;
+use Courseware\Container;
 use JsonApi\Errors\AuthorizationFailedException;
 use JsonApi\Errors\RecordNotFoundException;
 use JsonApi\Errors\UnprocessableEntityException;
@@ -11,6 +12,8 @@ use JsonApi\Routes\ValidationTrait;
 use JsonApi\Schemas\Courseware\Block as BlockSchema;
 use Psr\Http\Message\ResponseInterface as Response;
 use Psr\Http\Message\ServerRequestInterface as Request;
+use Studip\Activity\Activity;
+use Studip\Activity\CoursewareProvider;
 
 /**
  * Update one Block.
diff --git a/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php
index a9b3c094d3e..51a2a017c8f 100755
--- a/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php
+++ b/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php
@@ -46,6 +46,6 @@ class BookmarkedStructuralElementsIndex extends JsonApiController
         $total = count($resources);
         list($offset, $limit) = $this->getOffsetAndLimit();
 
-        return $this->getPaginatedResponse(array_slice($resources, $offset, $limit), $total);
+        return $this->getPaginatedContentResponse(array_slice($resources, $offset, $limit), $total);
     }
 }
diff --git a/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php b/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php
index ae6a4e6960d..77d3d2c75b2 100755
--- a/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php
+++ b/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php
@@ -73,6 +73,8 @@ class ContainersUpdate extends JsonApiController
                 );
             }
 
+            $resource->position = $json['data']['attributes']['position'];
+
             $resource->editor_id = $user->id;
             $resource->store();
 
diff --git a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php
index d22d9e3f17b..843f7c2a902 100755
--- a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php
+++ b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php
@@ -11,7 +11,10 @@ trait CoursewareInstancesHelper
 {
     private function findInstance(string $instanceId): Instance
     {
-        list($rangeType, $rangeId) = explode('_', $instanceId);
+        [$rangeType, $rangeId] = explode('_', $instanceId);
+        if (!is_string($rangeType) || !is_string($rangeId)) {
+            throw new BadRequestException('Invalid instance id: "' . $instanceId . '".');
+        }
 
         return $this->findInstanceWithRange($rangeType, $rangeId);
     }
diff --git a/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php b/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php
index 7d191f53ccf..1e68624b153 100755
--- a/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php
+++ b/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php
@@ -169,6 +169,10 @@ class BookmarkedStructuralElements extends RelationshipsController
     private function addBookmarks(\User $user, array $newIds): void
     {
         foreach ($newIds as $structuralElementId) {
+            if (Bookmark::countBySQL('user_id = ? AND element_id = ?', [$user->id, $structuralElementId])) {
+                continue;
+            }
+
             Bookmark::create(['user_id' => $user->id, 'element_id' => $structuralElementId]);
         }
     }
diff --git a/lib/classes/JsonApi/Routes/Courseware/Rel/UsersBookmarkedStructuralElements.php b/lib/classes/JsonApi/Routes/Courseware/Rel/UsersBookmarkedStructuralElements.php
new file mode 100755
index 00000000000..7f05c66a648
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/Rel/UsersBookmarkedStructuralElements.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\Rel;
+
+use Courseware\Bookmark;
+use Courseware\Instance;
+use Courseware\StructuralElement;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\Errors\ConflictException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\Routes\Courseware\Authority;
+use JsonApi\Routes\Courseware\CoursewareInstancesHelper;
+use JsonApi\Routes\RelationshipsController;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+class UsersBookmarkedStructuralElements extends RelationshipsController
+{
+    use CoursewareInstancesHelper;
+
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    protected function fetchRelationship(Request $request, $related)
+    {
+        $bookmarks = array_column(Bookmark::findUsersBookmarks($related), 'element');
+        $total = count($bookmarks);
+        list($offset, $limit) = $this->getOffsetAndLimit();
+        $page = array_slice($bookmarks, $offset, $limit);
+
+        return $this->getPaginatedIdentifiersResponse($page, $total);
+    }
+
+    protected function replaceRelationship(Request $request, $related)
+    {
+        $json = $this->validate($request);
+        $structuralElements = $this->validateStructuralElements($user = $this->getUser($request), $json, $related);
+        $this->replaceBookmarks($related, $structuralElements);
+
+        return $this->getCodeResponse(204);
+    }
+
+    protected function addToRelationship(Request $request, $related)
+    {
+        $json = $this->validate($request);
+        $structuralElements = $this->validateStructuralElements($user = $this->getUser($request), $json, $related);
+        $this->addBookmarks($related, $structuralElements);
+
+        return $this->getCodeResponse(204);
+    }
+
+    protected function removeFromRelationship(Request $request, $related)
+    {
+        $json = $this->validate($request);
+        $structuralElements = $this->validateStructuralElements($user = $this->getUser($request), $json, $related);
+        $this->removeBookmarks($user, $structuralElements);
+
+        return $this->getCodeResponse(204);
+    }
+
+    protected function findRelated(array $args)
+    {
+        if (!($related = \User::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+
+        return $related;
+    }
+
+    protected function authorize(Request $request, $resource)
+    {
+        $observer = $this->getUser($request);
+        $observed = $resource;
+        switch ($request->getMethod()) {
+            case 'GET':
+                return Authority::canIndexBookmarksOfAUser($observer, $observed);
+
+            case 'DELETE':
+            case 'PATCH':
+            case 'POST':
+                return Authority::canModifyBookmarksOfAUser($observer, $observed);
+
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    protected function getRelationshipSelfLink($resource, $schema, $userData)
+    {
+        return $schema->getRelationshipSelfLink($resource, \JsonApi\Schemas\User::REL_COURSEWARE_BOOKMARKS);
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    protected function getRelationshipRelatedLink($resource, $schema, $userData)
+    {
+        return $schema->getRelationshipRelatedLink($resource, \JsonApi\Schemas\User::REL_COURSEWARE_BOOKMARKS);
+    }
+
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+
+        $data = self::arrayGet($json, 'data');
+
+        if (!is_array($data)) {
+            return 'Document´s ´data´ must be an array.';
+        }
+
+        foreach ($data as $item) {
+            if (\JsonApi\Schemas\Courseware\StructuralElement::TYPE !== self::arrayGet($item, 'type')) {
+                return 'Wrong `type` in document´s `data`.';
+            }
+
+            if (!self::arrayGet($item, 'id')) {
+                return 'Missing `id` of document´s `data`.';
+            }
+        }
+
+        if (self::arrayHas($json, 'data.attributes')) {
+            return 'Document must not have `attributes`.';
+        }
+    }
+
+    private function validateStructuralElements(\User $actor, $json, \User $user)
+    {
+        $structuralElements = [];
+
+        foreach (self::arrayGet($json, 'data') as $structuralElementResource) {
+            if (!($structuralElement = StructuralElement::find($structuralElementResource['id']))) {
+                throw new RecordNotFoundException();
+            }
+
+            if (!Authority::canModifyBookmarksOfAUser($actor, $user)) {
+                throw new AuthorizationFailedException();
+            }
+
+            if (!Authority::canShowStructuralElement($user, $structuralElement)) {
+                throw new RecordNotFoundException();
+            }
+
+            $structuralElements[] = $structuralElement->id;
+        }
+
+        return $structuralElements;
+    }
+
+    private function replaceBookmarks(\User $user, array $newIds)
+    {
+        $oldIds = array_column(Bookmark::findUsersBookmarks($user), 'element_id');
+        $onlyInOld = array_diff($oldIds, $newIds);
+        $onlyInNew = array_diff($newIds, $oldIds);
+
+        $this->removeBookmarks($user, $onlyInOld);
+        $this->addBookmarks($user, $onlyInNew);
+    }
+
+    private function addBookmarks(\User $user, array $newIds): void
+    {
+        foreach ($newIds as $structuralElementId) {
+            if (Bookmark::countBySQL('user_id = ? AND element_id = ?', [$user->id, $structuralElementId])) {
+                continue;
+            }
+            Bookmark::create(['user_id' => $user->id, 'element_id' => $structuralElementId]);
+        }
+    }
+
+    private function removeBookmarks(\User $user, array $oldIds): void
+    {
+        Bookmark::deleteBySQL('user_id = ? AND element_id IN (?)', [$user->id, $oldIds]);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsCreate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsCreate.php
new file mode 100755
index 00000000000..9dfa77bd091
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsCreate.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\StructuralElement as StructuralElementSchema;
+use JsonApi\Schemas\Courseware\StructuralElementComment as StructuralElementCommentSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Studip\Activity\Activity;
+use Studip\Activity\CoursewareProvider;
+use Courseware\Container;
+
+/**
+ * Create a comment on a StructuralElement.
+ */
+class StructuralElementCommentsCreate extends JsonApiController
+{
+    use ValidationTrait;
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $json = $this->validate($request);
+        $structuralElement = $this->getStructuralElementFromJson($json);
+        if (!Authority::canCreateStructuralElementComment($user = $this->getUser($request), $structuralElement)) {
+            throw new AuthorizationFailedException();
+        }
+        $structuralElementComment = $this->createStructuralElementComment($user, $json, $structuralElement);
+
+        return $this->getCreatedResponse($structuralElementComment);
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+        if (StructuralElementCommentSchema::TYPE !== self::arrayGet($json, 'data.type')) {
+            return 'Wrong `type` member of document´s `data`.';
+        }
+        if (self::arrayHas($json, 'data.id')) {
+            return 'New document must not have an `id`.';
+        }
+
+        if (!self::arrayHas($json, 'data.attributes.comment')) {
+            return 'Missing `comment` attribute.';
+        }
+
+        if (!self::arrayHas($json, 'data.relationships.structural-element')) {
+            return 'Missing `structural-element` relationship.';
+        }
+        if (!$this->getStructuralElementFromJson($json)) {
+            return 'Invalid `structural-element` relationship.';
+        }
+    }
+
+    private function getStructuralElementFromJson($json)
+    {
+        if (!$this->validateResourceObject($json, 'data.relationships.structural-element', StructuralElementSchema::TYPE)) {
+            return null;
+        }
+        $structuralElementId = self::arrayGet($json, 'data.relationships.structural-element.data.id');
+
+        return \Courseware\StructuralElement::find($structuralElementId);
+    }
+
+    private function createStructuralElementComment(\User $user, array $json, \Courseware\StructuralElement $structuralElement)
+    {
+        $structuralElementComment = \Courseware\StructuralElementComment::build([
+            'structural_element_id' => $structuralElement->id,
+            'user_id' => $user->id,
+            'comment' => self::arrayGet($json, 'data.attributes.comment', ''),
+        ]);
+        $structuralElementComment->store();
+
+        return $structuralElementComment;
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsDelete.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsDelete.php
new file mode 100755
index 00000000000..6ea097c4c11
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsDelete.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElementComment;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\ConflictException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Delete one comment on a structural element.
+ */
+class StructuralElementCommentsDelete extends JsonApiController
+{
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = StructuralElementComment::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        if (!Authority::canDeleteStructuralElementComment($this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+        $resource->delete();
+
+        return $this->getCodeResponse(204);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsOfStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsOfStructuralElementsIndex.php
new file mode 100755
index 00000000000..95159667b8d
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsOfStructuralElementsIndex.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElement;
+use Courseware\StructuralElementComment;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays the user progress of a structural element.
+ */
+class StructuralElementCommentsOfStructuralElementsIndex extends JsonApiController
+{
+    protected $allowedIncludePaths = ['structural-element', 'user'];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($structural_element = StructuralElement::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        if (!Authority::canIndexStructuralElementComments($this->getUser($request), $structural_element)) {
+            throw new AuthorizationFailedException();
+        }
+        $resources = StructuralElementComment::findBySql('structural_element_id = ?', [$structural_element->id]);
+
+        return $this->getContentResponse($resources);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsShow.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsShow.php
new file mode 100755
index 00000000000..6f334a92421
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsShow.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElementComment;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays a comment on a structural element.
+ */
+class StructuralElementCommentsShow extends JsonApiController
+{
+    protected $allowedIncludePaths = ['structural-element', 'user'];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = StructuralElementComment::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowStructuralElementComment($this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($resource);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsUpdate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsUpdate.php
new file mode 100755
index 00000000000..5728847c219
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsUpdate.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElementComment;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\StructuralElement as StructuralElementSchema;
+use JsonApi\Schemas\Courseware\StructuralElementComment as StructuralElementCommentSchema;
+use JsonApi\Schemas\User as UserSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Update a comment on a structural element
+ */
+class StructuralElementCommentsUpdate extends JsonApiController
+{
+    use ValidationTrait, UserProgressesHelper;
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = StructuralElementComment::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        $json = $this->validate($request, $resource);
+        if (!Authority::canUpdateStructuralElementComment($this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+        $structuralElementComment = $this->updateStructuralElementComment($json, $resource);
+
+        return $this->getContentResponse($structuralElementComment);
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     * @SuppressWarnings(CyclomaticComplexity)
+     * @SuppressWarnings(NPathComplexity)
+     */
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+        if (StructuralElementCommentSchema::TYPE !== self::arrayGet($json, 'data.type')) {
+            return 'Wrong `type` member of document´s `data`.';
+        }
+        if (self::arrayGet($json, 'data.id') !== $data->id) {
+            return 'Mismatch in document `id`.';
+        }
+
+        if (!($comment = self::arrayGet($json, 'data.attributes.comment'))) {
+            return 'Missing `comment` attribute.';
+        }
+        if (!is_string($comment)) {
+            return 'Attribute `comment` must be a string.';
+        }
+        if ($comment == '') {
+            return 'Attribute `comment` must not be empty.';
+        }
+
+        if (self::arrayHas($json, 'data.relationships.user')) {
+            if (!($user = $this->getUserFromJson($json))) {
+                return 'Invalid `user` relationship.';
+            }
+            if ($user->id !== $data['user_id']) {
+                return 'Cannot update `user` relationship.';
+            }
+        }
+
+        if (self::arrayHas($json, 'data.relationships.structural-element')) {
+            if (!($structural_element = $this->getStructuralElementFromJson($json))) {
+                return 'Invalid `structural-element` relationship.';
+            }
+            if ($structural_element->id !== $data['structural_element_id']) {
+                return 'Cannot update `structural-element` relationship.';
+            }
+        }
+    }
+
+    private function getStructuralElementFromJson($json)
+    {
+        if (!$this->validateResourceObject($json, 'data.relationships.structural-element', StructuralElementSchema::TYPE)) {
+            return null;
+        }
+        $structuralElementId = self::arrayGet($json, 'data.relationships.structural-element.data.id');
+
+        return \Courseware\StructuralElement::find($structuralElementId);
+    }
+
+    private function getUserFromJson($json)
+    {
+        if (!$this->validateResourceObject($json, 'data.relationships.user', UserSchema::TYPE)) {
+            return null;
+        }
+        $userId = self::arrayGet($json, 'data.relationships.user.data.id');
+
+        return \User::find($userId);
+    }
+
+    private function updateStructuralElementComment(array $json, \Courseware\StructuralElementComment $resource)
+    {
+        $resource->comment = self::arrayGet($json, 'data.attributes.comment', '');
+        $resource->store();
+
+        return $resource;
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackCreate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackCreate.php
new file mode 100755
index 00000000000..bcf5425783d
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackCreate.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Container;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\StructuralElement as StructuralElementSchema;
+use JsonApi\Schemas\Courseware\StructuralElementFeedback as StructuralElementFeedbackSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Studip\Activity\Activity;
+use Studip\Activity\CoursewareProvider;
+
+/**
+ * Create feedback on a structural-element.
+ */
+class StructuralElementFeedbackCreate extends JsonApiController
+{
+    use ValidationTrait;
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $json = $this->validate($request);
+        $structuralElement = $this->getStructuralElementFromJson($json);
+        if (!Authority::canCreateStructuralElementFeedback($user = $this->getUser($request), $structuralElement)) {
+            throw new AuthorizationFailedException();
+        }
+        $structuralElementFeedback = $this->createStructuralElementFeedback($user, $json, $structuralElement);
+
+        return $this->getCreatedResponse($structuralElementFeedback);
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+        if (StructuralElementFeedbackSchema::TYPE !== self::arrayGet($json, 'data.type')) {
+            return 'Wrong `type` member of document´s `data`.';
+        }
+        if (self::arrayHas($json, 'data.id')) {
+            return 'New document must not have an `id`.';
+        }
+
+        if (!self::arrayHas($json, 'data.attributes.feedback')) {
+            return 'Missing `feedback` attribute.';
+        }
+
+        if (!self::arrayHas($json, 'data.relationships.structural-element')) {
+            return 'Missing `structural-element` relationship.';
+        }
+        if (!$this->getStructuralElementFromJson($json)) {
+            return 'Invalid `structural-element` relationship.';
+        }
+    }
+
+    private function getStructuralElementFromJson($json)
+    {
+        if (!$this->validateResourceObject($json, 'data.relationships.structural-element', StructuralElementSchema::TYPE)) {
+            return null;
+        }
+        $structuralElementId = self::arrayGet($json, 'data.relationships.structural-element.data.id');
+
+        return \Courseware\StructuralElement::find($structuralElementId);
+    }
+
+    private function createStructuralElementFeedback(\User $user, array $json, \Courseware\StructuralElement $structuralElement)
+    {
+        $structuralElementFeedback = \Courseware\StructuralElementFeedback::build([
+            'structural_element_id' => $structuralElement->id,
+            'user_id' => $user->id,
+            'feedback' => self::arrayGet($json, 'data.attributes.feedback'),
+        ]);
+        $structuralElementFeedback->store();
+
+        return $structuralElementFeedback;
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackDelete.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackDelete.php
new file mode 100755
index 00000000000..5c61bad7874
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackDelete.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElementFeedback;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\ConflictException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Delete one comment on a structural element.
+ */
+class StructuralElementFeedbackDelete extends JsonApiController
+{
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = StructuralElementFeedback::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        if (!Authority::canDeleteStructuralElementFeedback($this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+        $resource->delete();
+
+        return $this->getCodeResponse(204);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackOfStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackOfStructuralElementsIndex.php
new file mode 100755
index 00000000000..855ce60c4c6
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackOfStructuralElementsIndex.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElement;
+use Courseware\StructuralElementFeedback;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays the feedback of a StructuralElement.
+ */
+class StructuralElementFeedbackOfStructuralElementsIndex extends JsonApiController
+{
+    protected $allowedIncludePaths = ['user', 'structural-element'];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        /** @var ?Block $block */
+        $structuralElement = StructuralElement::find($args['id']);
+        if (!$structuralElement) {
+            throw new RecordNotFoundException();
+        }
+        if (!Authority::canIndexStructuralElementFeedback($this->getUser($request), $structuralElement)) {
+            throw new AuthorizationFailedException();
+        }
+        /** @var StructuralElementFeedback[] $resources */
+        $resources = $structuralElement->feedback;
+
+        return $this->getContentResponse($resources);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackShow.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackShow.php
new file mode 100755
index 00000000000..752e178f6ec
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackShow.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElementFeedback;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays feedback on a StructuralElement.
+ */
+class StructuralElementFeedbackShow extends JsonApiController
+{
+    protected $allowedIncludePaths = ['user', 'structural-element'];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        /** @var ?StructuralElementFeedback $resource */
+        $resource = StructuralElementFeedback::find($args['id']);
+        if (!$resource) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowStructuralElementFeedback($this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($resource);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackUpdate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackUpdate.php
new file mode 100755
index 00000000000..5d4b0afc325
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackUpdate.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElementFeedback;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\StructuralElement as StructuralElementSchema;
+use JsonApi\Schemas\Courseware\StructuralElementFeedback as StructuralElementFeedbackSchema;
+use JsonApi\Schemas\User as UserSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Update a feedback on a structural-element
+ */
+class StructuralElementFeedbackUpdate extends JsonApiController
+{
+    use ValidationTrait, UserProgressesHelper;
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = StructuralElementFeedback::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        $json = $this->validate($request, $resource);
+        if (!Authority::canUpdateStructuralElementFeedback($this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+        $structuralElementFeedback = $this->updateStructuralElementFeedback($json, $resource);
+
+        return $this->getContentResponse($structuralElementFeedback);
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     * @SuppressWarnings(CyclomaticComplexity)
+     * @SuppressWarnings(NPathComplexity)
+     */
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+        if (StructuralElementFeedbackSchema::TYPE !== self::arrayGet($json, 'data.type')) {
+            return 'Wrong `type` member of document´s `data`.';
+        }
+        if (self::arrayGet($json, 'data.id') !== $data->id) {
+            return 'Mismatch in document `id`.';
+        }
+
+        if (!($feedback = self::arrayGet($json, 'data.attributes.feedback'))) {
+            return 'Missing `feedback` attribute.';
+        }
+        if (!is_string($feedback)) {
+            return 'Attribute `feedback` must be a string.';
+        }
+        if ($feedback == '') {
+            return 'Attribute `feedback` must not be empty.';
+        }
+
+        if (self::arrayHas($json, 'data.relationships.user')) {
+            if (!($user = $this->getUserFromJson($json))) {
+                return 'Invalid `user` relationship.';
+            }
+            if ($user->id !== $data['user_id']) {
+                return 'Cannot update `user` relationship.';
+            }
+        }
+
+        if (self::arrayHas($json, 'data.relationships.structural-element')) {
+            if (!($structuralElement = $this->getStructuralElementFromJson($json))) {
+                return 'Invalid `structural-element` relationship.';
+            }
+            if ($structuralElement->id !== $data['structural_element_id']) {
+                return 'Cannot update `structural-element` relationship.';
+            }
+        }
+    }
+
+    private function getStructuralElementFromJson($json)
+    {
+        if (!$this->validateResourceObject($json, 'data.relationships.structural-element', StructuralElementSchema::TYPE)) {
+            return null;
+        }
+        $structuralElementId = self::arrayGet($json, 'data.relationships.structural-element.data.id');
+
+        return \Courseware\StructuralElement::find($structuralElementId);
+    }
+
+    private function getUserFromJson($json)
+    {
+        if (!$this->validateResourceObject($json, 'data.relationships.user', UserSchema::TYPE)) {
+            return null;
+        }
+        $userId = self::arrayGet($json, 'data.relationships.user.data.id');
+
+        return \User::find($userId);
+    }
+
+    private function updateStructuralElementFeedback(array $json, \Courseware\StructuralElementFeedback $resource)
+    {
+        $resource->feedback = self::arrayGet($json, 'data.attributes.feedback', '');
+        $resource->store();
+
+        return $resource;
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php
index 9622fb66ab1..5dcb6d47194 100755
--- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php
@@ -33,6 +33,9 @@ class StructuralElementsCopy extends NonJsonApiController
         }
 
         $newElement = $this->copyElement($user, $sourceElement, $newParent);
+        if ($data['remove_purpose']) {
+            $newElement->purpose = '';
+        }
 
         return $this->redirectToStructuralElement($response, $newElement);
     }
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php
index ed1374af382..8f1c8bd8443 100755
--- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php
@@ -8,6 +8,7 @@ use JsonApi\Routes\ValidationTrait;
 use JsonApi\Schemas\Courseware\StructuralElement as StructuralElementSchema;
 use Psr\Http\Message\ResponseInterface as Response;
 use Psr\Http\Message\ServerRequestInterface as Request;
+use Studip\Activity\Activity;
 
 /**
  * Create a block in a container.
@@ -78,11 +79,49 @@ class StructuralElementsCreate extends JsonApiController
             'editor_id' => $user->id,
             'edit_blocker_id' => '',
             'title' => self::arrayGet($json, 'data.attributes.title', ''),
-            'purpose' => 'CONTENT',
+            'purpose' => self::arrayGet($json, 'data.attributes.purpose', 'content'),
+            'payload' => self::arrayGet($json, 'data.attributes.payload', ''),
             'position' => $parent->countChildren()
         ]);
 
         $struct->store();
+        $template = \Courseware\Template::find(self::arrayGet($json, 'data.templateId'));
+
+        if ($template) {
+            $structure = json_decode($template->structure, true);
+
+            foreach($structure['containers'] as $container) {
+
+                $new_container = \Courseware\Container::build([
+                    'structural_element_id' => $struct->id,
+                    'owner_id' => $user->id,
+                    'editor_id' => $user->id,
+                    'edit_blocker_id' => '',
+                    'position' => $struct->countContainers(),
+                    'container_type' => $container['attributes']['container-type'],
+                    'payload' => json_encode($container['attributes']['payload']),
+                ]);
+
+                $new_container->store();
+                $blockMap = [];
+                foreach($container['blocks'] as $block) {
+                    $new_block = \Courseware\Block::build([
+                        'container_id'    => $new_container->id,
+                        'owner_id'        => $user->id,
+                        'editor_id'       => $user->id,
+                        'position'        => $new_container->countBlocks(),
+                        'block_type'      => $block['attributes']['block-type'],
+                        'payload'         => json_encode($block['attributes']['payload']),
+                        'visible'         => 1,
+                    ]);
+
+                    $new_block->store();
+                    $blockMap[$block['id']] = $new_block->id;
+                }
+                $new_container['payload'] = $new_container->type->copyPayload($blockMap);
+                $new_container->store();
+            }
+        }
 
         return $struct;
     }
diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackCreate.php b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackCreate.php
new file mode 100755
index 00000000000..e7d75527797
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackCreate.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Task;
+use Courseware\TaskFeedback;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\UnprocessableEntityException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\Task as TaskSchema;
+use JsonApi\Schemas\Courseware\TaskFeedback as TaskFeedbackSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Studip\Activity\Activity;
+use Studip\Activity\CoursewareProvider;
+
+/**
+ * Create a Task.
+ */
+class TaskFeedbackCreate extends JsonApiController
+{
+    use ValidationTrait;
+
+        /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $json = $this->validate($request);
+        $task = $this->getTaskFromJson($json);
+        if (!Authority::canCreateTaskFeedback($lecturer = $this->getUser($request), $task)) {
+            throw new AuthorizationFailedException();
+        }
+
+        $feedback = $this->createTaskFeedback($lecturer, $json, $task);
+
+        return $this->getCreatedResponse($feedback);
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+        if (TaskFeedbackSchema::TYPE !== self::arrayGet($json, 'data.type')) {
+            return 'Wrong `type` member of document´s `data`.';
+        }
+        if (self::arrayHas($json, 'data.id')) {
+            return 'New document must not have an `id`.';
+        }
+        if (!$this->getTaskFromJson($json)) {
+            return 'Invalid `task` relationship.';
+        }
+    }
+
+    private function getTaskFromJson($json)
+    {
+        if (!$this->validateResourceObject($json, 'data.relationships.task', TaskSchema::TYPE)) {
+            return null;
+        }
+
+        $taskId = self::arrayGet($json, 'data.relationships.task.data.id');
+
+        return \Courseware\Task::find($taskId);
+    }
+
+    private function createTaskFeedback(\User $lecturer, array $json, \Courseware\Task $task): TaskFeedback
+    {
+        $get = function ($key, $default = '') use ($json) {
+            return self::arrayGet($json, $key, $default);
+        };
+
+        $feedback = TaskFeedback::build([
+            'lecturer_id' => $lecturer->id,
+            'task_id' => $task->id,
+            'content' => self::arrayGet($json, 'data.attributes.content', '')
+        ]);
+
+        $feedback->store();
+        $task->feedback_id = $feedback->id;
+        $task->store();
+
+        return $feedback;
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackDelete.php b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackDelete.php
new file mode 100755
index 00000000000..b7459f84320
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackDelete.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Task;
+use Courseware\TaskFeedback;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Delete one Task.
+ */
+class TaskFeedbackDelete extends JsonApiController
+{
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = TaskFeedback::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        $task = Task::find($resource->task_id);
+        if (!Authority::canDeleteTaskFeedback($user = $this->getUser($request), $task)) {
+            throw new AuthorizationFailedException();
+        }
+        $task->feedback_id = null;
+        $task->store();
+        $resource->delete();
+
+        return $this->getCodeResponse(204);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackShow.php b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackShow.php
new file mode 100755
index 00000000000..b54011876a4
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackShow.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Task;
+use Courseware\TaskFeedback;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays one Task.
+ */
+class TaskFeedbackShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [
+        'lecturer',
+        'task'
+    ];
+
+        /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = TaskFeedback::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        $task = Task::find($resource->task_id);
+        if (!Authority::canShowTaskFeedback($this->getUser($request), $task)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($resource);
+    }
+}
\ No newline at end of file
diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackUpdate.php b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackUpdate.php
new file mode 100755
index 00000000000..40978182128
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackUpdate.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Task;
+use Courseware\TaskFeedback;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\Errors\UnprocessableEntityException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Studip\Activity\Activity;
+use Studip\Activity\CoursewareProvider;
+
+/**
+ * Update one Task.
+ */
+class TaskFeedbackUpdate extends JsonApiController
+{
+    use ValidationTrait;
+        /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = TaskFeedback::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        $json = $this->validate($request, $resource);
+        $task = Task::find($resource->task_id);
+        if (!Authority::canUpdateTaskFeedback($user = $this->getUser($request), $task)) {
+            throw new AuthorizationFailedException();
+        }
+        $resource = $this->updateTaskFeedback($user, $resource, $json);
+
+        return $this->getContentResponse($resource);
+    }
+
+        /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+
+        if (!self::arrayHas($json, 'data.id')) {
+            return 'Document must have an `id`.';
+        }
+    }
+
+    private function updateTaskFeedback(\User $user, TaskFeedback $resource, array $json): TaskFeedback
+    {
+        if (self::arrayHas($json, 'data.attributes.content')) {
+            $resource->content = self::arrayGet(
+                $json,
+                'data.attributes.content'
+            );
+        }
+        $resource->store();
+
+        if ($struct->range_type === 'courses') {
+            $data = [
+                'provider'     => 'Studip\Activity\CoursewareProvider',
+                'context'      => 'course',
+                'context_id'   => $task->seminar_id,
+                'content'      => self::arrayGet($json, 'data.attributes.content', ''),
+                'actor_type'   => 'user',
+                'actor_id'     => $user->id,
+                'verb'         => 'answered',
+                'object_id'    => $task->structural_element_id,
+                'object_type'  => 'courseware',
+                'mkdate'       => time()
+            ];
+    
+            $activity = Activity::create($data);
+        }
+
+        return $resource;
+    }
+}
\ No newline at end of file
diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php
new file mode 100644
index 00000000000..67cc27c9513
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php
@@ -0,0 +1,212 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElement;
+use Courseware\Task;
+use Courseware\TaskGroup;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\TimestampTrait;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\StructuralElement as StructuralElementSchema;
+use JsonApi\Schemas\Courseware\TaskGroup as TaskGroupSchema;
+use JsonApi\Schemas\StatusGroup as StatusGroupSchema;
+use JsonApi\Schemas\User as UserSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Create a TaskGroup.
+ */
+class TaskGroupsCreate extends JsonApiController
+{
+    use TimestampTrait;
+    use ValidationTrait;
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     *
+     * @param array $args
+     *
+     * @return Response
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $json = $this->validate($request);
+        $structuralElement = $this->getTargetFromJson($json);
+        if (!Authority::canCreateTasks($user = $this->getUser($request), $structuralElement)) {
+            throw new AuthorizationFailedException();
+        }
+        $taskGroup = $this->createTaskGroup($user, $json);
+
+        return $this->getCreatedResponse($taskGroup);
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     *
+     * @param array $json
+     * @param mixed $data
+     *
+     * @return string|void
+     */
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+        if (TaskGroupSchema::TYPE !== self::arrayGet($json, 'data.type')) {
+            return 'Wrong `type` member of document´s `data`.';
+        }
+        if (self::arrayHas($json, 'data.id')) {
+            return 'New document must not have an `id`.';
+        }
+        if (!self::arrayHas($json, 'data.attributes.title')) {
+            return 'Missing `title` attribute.';
+        }
+        if (!self::arrayHas($json, 'data.attributes.submission-date')) {
+            return 'Missing `submission-date` attribute.';
+        }
+        $submissionDate = self::arrayGet($json, 'data.attributes.submission-date');
+        if (!self::isValidTimestamp($submissionDate)) {
+            return '`submission-date` is not an ISO 8601 timestamp.';
+        }
+
+        if (!self::arrayHas($json, 'data.relationships.target')) {
+            return 'Missing `target` relationship.';
+        }
+        if (!self::arrayHas($json, 'data.relationships.task-template')) {
+            return 'Missing `task-template` relationship.';
+        }
+
+        if (!self::arrayHas($json, 'data.relationships.solvers')) {
+            return 'Missing `solvers` relationship.';
+        }
+
+        if (!$this->validateSolvers($json)) {
+            return 'Invalid `solvers` relationship.';
+        }
+        if (!$this->getTargetFromJson($json)) {
+            return 'Invalid `target` relationship.';
+        }
+        if (!$this->getTaskTemplateFromJson($json)) {
+            return 'Invalid `task-template` relationship.';
+        }
+    }
+
+    private function validateSolvers(array $json): bool
+    {
+        if (!self::arrayHas($json, 'data.relationships.solvers.data')) {
+            return false;
+        }
+
+        $data = self::arrayGet($json, 'data.relationships.solvers.data');
+
+        if (!is_array($data) || !count($data)) {
+            return false;
+        }
+
+        foreach ($data as $resourceIdentifier) {
+            if (
+                !(
+                    $this->validateResourceObject($resourceIdentifier, '', UserSchema::TYPE) ||
+                    $this->validateResourceObject($resourceIdentifier, '', StatusGroupSchema::TYPE)
+                )
+            ) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private function getSolversFromJson(array $json): iterable
+    {
+        if (!self::arrayHas($json, 'data.relationships.solvers.data')) {
+            return [];
+        }
+
+        $solvers = [];
+        $mapping = [UserSchema::TYPE => \User::class, StatusGroupSchema::TYPE => \Statusgruppen::class];
+        foreach (self::arrayGet($json, 'data.relationships.solvers.data') as $resourceIdentifier) {
+            $solvers[] = $mapping[$resourceIdentifier['type']]::find($resourceIdentifier['id']);
+        }
+
+        return $solvers;
+    }
+
+    private function getTargetFromJson(array $json): ?StructuralElement
+    {
+        if (!$this->validateResourceObject($json, 'data.relationships.target', StructuralElementSchema::TYPE)) {
+            return null;
+        }
+        $resourceId = self::arrayGet($json, 'data.relationships.target.data.id');
+
+        return StructuralElement::find($resourceId);
+    }
+
+    private function getTaskTemplateFromJson(array $json): ?StructuralElement
+    {
+        if (!$this->validateResourceObject($json, 'data.relationships.task-template', StructuralElementSchema::TYPE)) {
+            return null;
+        }
+        $resourceId = self::arrayGet($json, 'data.relationships.task-template.data.id');
+
+        return StructuralElement::find($resourceId);
+    }
+
+    private function createTaskGroup(\User $lecturer, array $json): TaskGroup
+    {
+        $tasks = [];
+
+        $solvers = $this->getSolversFromJson($json);
+        $taskTemplate = $this->getTaskTemplateFromJson($json);
+        $target = $this->getTargetFromJson($json);
+
+        $solverMayAddBlocks = self::arrayGet($json, 'data.attributes.solver-may-add-blocks', '');
+        $submissionDate = self::arrayGet($json, 'data.attributes.submission-date', '');
+        $submissionDate = self::fromISO8601($submissionDate);
+        $title = self::arrayGet($json, 'data.attributes.title', '');
+
+        /** @var TaskGroup $taskGroup */
+        $taskGroup = TaskGroup::create([
+            'seminar_id' => $target['range_id'],
+            'lecturer_id' => $lecturer->getId(),
+            'target_id' => $target->getId(),
+            'task_template_id' => $taskTemplate->getId(),
+            'solver_may_add_blocks' => $solverMayAddBlocks,
+            'title' => $title,
+        ]);
+
+        foreach ($solvers as $solver) {
+            $task = Task::build([
+                'task_group_id' => $taskGroup->getId(),
+                'solver_id' => $solver->getId(),
+                'solver_type' => $this->getSolverType($solver),
+                'submission_date' => $submissionDate->getTimestamp(),
+            ]);
+
+            // copy task template
+            $taskElement = $taskTemplate->copy($lecturer, $target);
+            $taskElement->purpose = 'task';
+            $taskElement->store();
+
+            //update task with element id
+            $task['structural_element_id'] = $taskElement->id;
+            $task->store();
+        }
+
+        return $taskGroup;
+    }
+
+    /**
+     * @param \User|\Statusgruppen $solver
+     */
+    private function getSolverType($solver): string
+    {
+        $solverTypes = [\User::class => 'autor', \Statusgruppen::class => 'group'];
+
+        return $solverTypes[get_class($solver)];
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php
new file mode 100644
index 00000000000..c8ebb86e31b
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\TaskGroup;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Schemas\Courseware\TaskGroup as TaskGroupSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays one TaskGroup.
+ */
+class TaskGroupsShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [
+        TaskGroupSchema::REL_COURSE,
+        TaskGroupSchema::REL_LECTURER,
+        TaskGroupSchema::REL_SOLVERS,
+        TaskGroupSchema::REL_TARGET,
+        TaskGroupSchema::REL_TASK_TEMPLATE,
+        TaskGroupSchema::REL_TASKS,
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     * @param array $args
+     * @return Response
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        /** @var ?\Courseware\TaskGroup $resource */
+        $resource = TaskGroup::find($args['id']);
+        if (!$resource) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowTaskGroup($this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($resource);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksDelete.php b/lib/classes/JsonApi/Routes/Courseware/TasksDelete.php
new file mode 100755
index 00000000000..45c78dbd551
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TasksDelete.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Task;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Delete one Task.
+ */
+class TasksDelete extends JsonApiController
+{
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     * @param array $args
+     * @return Response
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        /** @var ?\Courseware\Task $resource */
+        $resource = Task::find($args['id']);
+        if (!$resource) {
+            throw new RecordNotFoundException();
+        }
+        if (!Authority::canDeleteTask($user = $this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+        if ($feedback = $resource->getFeedback()) {
+            $feedback->delete();
+        }
+        $resource->delete();
+
+        return $this->getCodeResponse(204);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php b/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php
new file mode 100755
index 00000000000..f0b2ce9a53a
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Task;
+use Courseware\TaskGroup;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\JsonApiController;
+use JsonApi\Schemas\Courseware\Task as TaskSchema;
+use JsonApi\Schemas\Courseware\TaskGroup as TaskGroupSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays all Tasks.
+ */
+class TasksIndex extends JsonApiController
+{
+    protected $allowedFilteringParameters = ['cid'];
+
+    protected $allowedIncludePaths = [
+        TaskSchema::REL_FEEDBACK,
+        TaskSchema::REL_SOLVER,
+        TaskSchema::REL_STRUCTURAL_ELEMENT,
+        TaskSchema::REL_TASK_GROUP,
+        TaskSchema::REL_TASK_GROUP . '.' . TaskGroupSchema::REL_LECTURER,
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     *
+     * @param array $args
+     *
+     * @return Response
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if ($error = $this->validateFilters()) {
+            throw new BadRequestException($error);
+        }
+
+        $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+        $resources = [];
+
+        if (empty($filtering)) {
+            if (!Authority::canIndexTasks($this->getUser($request))) {
+                throw new AuthorizationFailedException('Only root users may index all tasks without a `filter[cid]`.');
+            }
+
+            $resources = Task::findBySQL('1 ORDER BY mkdate', []);
+        } else {
+            $user = $this->getUser($request);
+            /** @var ?\Course $course */
+            $course = \Course::find($filtering['cid']);
+
+            if ($GLOBALS['perm']->have_studip_perm('tutor', $course->getId(), $user->getId())) {
+                $resources = $this->findTasksByCourse($course);
+            } else {
+                $resources = $this->findTasksByCourseMember($user, $course);
+            }
+        }
+
+        return $this->getContentResponse($resources);
+    }
+
+    private function validateFilters()
+    {
+        $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+
+        // course
+        if (isset($filtering['cid'])) {
+            $course = \Course::find($filtering['cid']);
+            if (!$course) {
+                return 'Could not find a course matching this `filter[cid]`.';
+            }
+        }
+    }
+
+    private function findTasksByCourse(\Course $course): \SimpleCollection
+    {
+        $taskGroups = TaskGroup::findBySQL('seminar_id = ?', [$course->getId()]);
+
+        $tasks = [];
+        foreach ($taskGroups as $taskGroup) {
+            $tasks[] = $taskGroup->tasks->getArrayCopy();
+        }
+        $tasks = \SimpleORMapCollection::createFromArray(array_flatten($tasks), false)->orderBy('id asc');
+
+        return $tasks;
+    }
+
+    private function findTasksByCourseMember(\User $user, \Course $course): \SimpleCollection
+    {
+        $groupIds = $course['statusgruppen']
+            ->filter(function (\Statusgruppen $group) use ($user) {
+                return $group->isMember($user->getId());
+            })
+            ->pluck('id');
+
+        return $this->findTasksByCourse($course)->filter(function ($task) use ($user, $groupIds) {
+            return ('autor' === $task['solver_type'] && $task['solver_id'] === $user->getId()) ||
+                ('group' === $task['solver_type'] && in_array($task['solver_id'], $groupIds));
+        });
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksShow.php b/lib/classes/JsonApi/Routes/Courseware/TasksShow.php
new file mode 100755
index 00000000000..619e7eab9e7
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TasksShow.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Task;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\Schemas\Courseware\Task as TaskSchema;
+use JsonApi\Schemas\Courseware\TaskGroup as TaskGroupSchema;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays one Task.
+ */
+class TasksShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [
+        TaskSchema::REL_FEEDBACK,
+        TaskSchema::REL_SOLVER,
+        TaskSchema::REL_STRUCTURAL_ELEMENT,
+        TaskSchema::REL_TASK_GROUP,
+        TaskSchema::REL_TASK_GROUP . '.' . TaskGroupSchema::REL_LECTURER,
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     * @param array $args
+     * @return Response
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        /** @var ?\Courseware\Task $resource */
+        $resource = Task::find($args['id']);
+        if (!$resource) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowTask($this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($resource);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php b/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php
new file mode 100755
index 00000000000..3728dba9a6b
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Task;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\TimestampTrait;
+use JsonApi\Routes\ValidationTrait;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Update one Task.
+ */
+class TasksUpdate extends JsonApiController
+{
+    use TimestampTrait;
+    use ValidationTrait;
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     * @param array $args
+     * @return Response
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        /** @var ?\Courseware\Task $resource */
+        $resource = Task::find($args['id']);
+        if (!$resource) {
+            throw new RecordNotFoundException();
+        }
+        $json = $this->validate($request, $resource);
+        if (!Authority::canUpdateTask($user = $this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+        $resource = $this->updateTask($user, $resource, $json);
+
+        return $this->getContentResponse($resource);
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     * @param array $json
+     * @param mixed $data
+     * @return string|void
+     */
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+
+        if (!self::arrayHas($json, 'data.id')) {
+            return 'Document must have an `id`.';
+        }
+
+        if (self::arrayHas($json, 'data.attributes.renewal-date')) {
+            $renewalDate = self::arrayGet($json, 'data.attributes.renewal-date');
+            if (!self::isValidTimestamp($renewalDate)) {
+                return '`renewal-date` is not an ISO 8601 timestamp.';
+            }
+        }
+    }
+
+    private function updateTask(\User $user, Task $resource, array $json): Task
+    {
+        if (Authority::canDeleteTask($user, $resource)) {
+            if (self::arrayHas($json, 'data.attributes.renewal')) {
+                $newRenewalState = self::arrayGet($json, 'data.attributes.renewal');
+                if ('declined' === $newRenewalState) {
+                    $resource->renewal = $newRenewalState;
+                }
+                if ('granted' === $newRenewalState && self::arrayHas($json, 'data.attributes.renewal-date')) {
+                    $renewalDate = self::arrayGet($json, 'data.attributes.renewal-date', '');
+                    $renewalDate = self::fromISO8601($renewalDate);
+
+                    $resource->renewal = $newRenewalState;
+                    $resource->renewal_date = $renewalDate->getTimestamp();
+                }
+            }
+        } else {
+            if (self::arrayHas($json, 'data.attributes.submitted')) {
+                $newSubmittedState = self::arrayGet($json, 'data.attributes.submitted');
+                if ($this->canSubmit($resource, $newSubmittedState)) {
+                    $resource->submitted = $newSubmittedState;
+                    if ('pending' === $resource->renewal) {
+                        $resource->renewal = '';
+                    }
+                }
+            }
+            if (self::arrayHas($json, 'data.attributes.renewal')) {
+                $newRenewalState = self::arrayGet($json, 'data.attributes.renewal');
+                if ('pending' === $newRenewalState) {
+                    $resource->renewal = $newRenewalState;
+                }
+            }
+        }
+
+        $resource->store();
+
+        return $resource;
+    }
+
+    private function canSubmit(Task $resource, string $newSubmittedState): bool
+    {
+        $now = time();
+        if (1 === (int) $resource->submitted || !$newSubmittedState) {
+            return false;
+        }
+        if ('granted' === $resource->renewal) {
+            return $now <= $resource->renewal_date;
+        } else {
+            return $now <= $resource->submission_date;
+        }
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TemplatesCreate.php b/lib/classes/JsonApi/Routes/Courseware/TemplatesCreate.php
new file mode 100755
index 00000000000..f23468c75ef
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TemplatesCreate.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Template;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\UnprocessableEntityException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\Task as TaskSchema;
+use JsonApi\Schemas\Courseware\StructuralElement as StructuralElementSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Create a Template.
+ */
+class TemplatesCreate extends JsonApiController
+{
+    use ValidationTrait;
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $json = $this->validate($request);
+        if (!Authority::canCreateTemplate($this->getUser($request))) {
+            throw new AuthorizationFailedException();
+        }
+
+        $template = $this->createTemplate($json);
+
+        return $this->getCreatedResponse($template);
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+
+        if (self::arrayHas($json, 'data.id')) {
+            return 'New document must not have an `id`.';
+        }
+        if (!self::arrayHas($json, 'data.name')) {
+            return 'Missing `name` value.';
+        }
+        if (!self::arrayHas($json, 'data.purpose')) {
+            return 'Missing `purpose` attribute.';
+        }
+        if (!self::arrayHas($json, 'data.structure')) {
+            return 'Missing `structure` attribute.';
+        }
+    }
+
+    private function createTemplate(array $json): Template
+    {
+        $get = function ($key, $default = '') use ($json) {
+            return self::arrayGet($json, $key, $default);
+        };
+
+        $template = Template::build([
+            'name' => $get('data.name'),
+            'purpose' => $get('data.purpose'),
+            'structure' => $this->cleanStructure($get('data.structure'), $get('data.name')),
+        ]);
+        $template->store();
+
+        return $template;
+    }
+
+    private function cleanStructure($json, $name): string
+    {
+        $structural_element_uploaded = json_decode($json, true);
+
+        $structural_element = [
+            'type' => $structural_element_uploaded['type'],
+            'attributes' => ['title' => $name],
+            'containers' => $structural_element_uploaded['containers'],
+        ];
+
+        return json_encode($structural_element);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TemplatesDelete.php b/lib/classes/JsonApi/Routes/Courseware/TemplatesDelete.php
new file mode 100755
index 00000000000..406a3720b17
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TemplatesDelete.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Template;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Delete one Template.
+ */
+class TemplatesDelete extends JsonApiController
+{
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = Template::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        if (!Authority::canDeleteTemplate($user = $this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+
+        $resource->delete();
+
+        return $this->getCodeResponse(204);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TemplatesIndex.php b/lib/classes/JsonApi/Routes/Courseware/TemplatesIndex.php
new file mode 100755
index 00000000000..1831bb7ef29
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TemplatesIndex.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Template;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays all Templates
+ */
+class TemplatesIndex extends JsonApiController
+{
+
+    protected $allowedIncludePaths = [];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!Authority::canIndexTemplates($this->getUser($request))) {
+            throw new AuthorizationFailedException();
+        }
+
+        $resources = Template::findBySQL('1 ORDER BY mkdate', []);
+
+        return $this->getContentResponse($resources);
+    }
+}
\ No newline at end of file
diff --git a/lib/classes/JsonApi/Routes/Courseware/TemplatesShow.php b/lib/classes/JsonApi/Routes/Courseware/TemplatesShow.php
new file mode 100755
index 00000000000..ef28e0b3e54
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TemplatesShow.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Template;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays one Template.
+ */
+class TemplatesShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [];
+
+        /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = Template::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowTemplate($this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($resource);
+    }
+}
\ No newline at end of file
diff --git a/lib/classes/JsonApi/Routes/Courseware/TemplatesUpdate.php b/lib/classes/JsonApi/Routes/Courseware/TemplatesUpdate.php
new file mode 100755
index 00000000000..2ccb8a79809
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TemplatesUpdate.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Template;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\Errors\UnprocessableEntityException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Update one Template.
+ */
+class TemplatesUpdate extends JsonApiController
+{
+    use ValidationTrait;
+        /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($resource = Template::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        $json = $this->validate($request, $resource);
+        if (!Authority::canUpdateTemplate($user = $this->getUser($request), $resource)) {
+            throw new AuthorizationFailedException();
+        }
+        $resource = $this->updateTemplate($resource, $json);
+
+        return $this->getContentResponse($resource);
+    }
+
+        /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    protected function validateResourceDocument($json, $data)
+    {
+        if (!self::arrayHas($json, 'data')) {
+            return 'Missing `data` member at document´s top level.';
+        }
+
+        if (!self::arrayHas($json, 'data.id')) {
+            return 'Document must have an `id`.';
+        }
+
+        if (!self::arrayHas($json, 'data.name')) {
+            return 'Document must have an `name`.';
+        }
+
+        if (!self::arrayHas($json, 'data.purpose')) {
+            return 'Document must have an `purpose`.';
+        }
+    }
+
+    private function updateTemplate(Template $resource, array $json): Template
+    {
+        if (self::arrayHas($json, 'data.name')) {
+            $resource->name = self::arrayGet(
+                $json,
+                'data.name'
+            );
+        }
+
+        if (self::arrayHas($json, 'data.purpose')) {
+            $resource->purpose = self::arrayGet(
+                $json,
+                'data.purpose'
+            );
+        }
+
+        $resource->store();
+
+        return $resource;
+    }
+
+}
\ No newline at end of file
diff --git a/lib/classes/JsonApi/Routes/Courseware/UsersBookmarkedStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/UsersBookmarkedStructuralElementsIndex.php
new file mode 100755
index 00000000000..618dc2e38e2
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/UsersBookmarkedStructuralElementsIndex.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\Bookmark;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Displays the user's bookmarked structural elements.
+ */
+class UsersBookmarkedStructuralElementsIndex extends JsonApiController
+{
+    use CoursewareInstancesHelper;
+
+    protected $allowedIncludePaths = [
+        'ancestors',
+        'containers',
+        'containers.blocks',
+        'containers.blocks.edit-blocker',
+        'containers.blocks.editor',
+        'containers.blocks.owner',
+        'containers.blocks.user-data-field',
+        'containers.blocks.user-progress',
+        'course',
+        'editor',
+        'owner',
+        'parent',
+    ];
+
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!($user = \User::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+        $actor = $this->getUser($request);
+        if (!Authority::canIndexBookmarksOfAUser($actor, $user)) {
+            throw new AuthorizationFailedException();
+        }
+
+        $resources = array_column(Bookmark::findUsersBookmarks($user), 'element');
+        $total = count($resources);
+        [$offset, $limit] = $this->getOffsetAndLimit();
+
+        return $this->getPaginatedContentResponse(array_slice($resources, $offset, $limit), $total);
+    }
+}
diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php
index 042ee58eab2..de6e24759ea 100644
--- a/lib/classes/JsonApi/SchemaMap.php
+++ b/lib/classes/JsonApi/SchemaMap.php
@@ -54,8 +54,14 @@ class SchemaMap
             \Courseware\Container::class => Schemas\Courseware\Container::class,
             \Courseware\Instance::class => Schemas\Courseware\Instance::class,
             \Courseware\StructuralElement::class => Schemas\Courseware\StructuralElement::class,
+            \Courseware\StructuralElementComment::class => Schemas\Courseware\StructuralElementComment::class,
+            \Courseware\StructuralElementFeedback::class => Schemas\Courseware\StructuralElementFeedback::class,
             \Courseware\UserDataField::class => Schemas\Courseware\UserDataField::class,
             \Courseware\UserProgress::class => Schemas\Courseware\UserProgress::class,
+            \Courseware\Task::class => Schemas\Courseware\Task::class,
+            \Courseware\TaskGroup::class => Schemas\Courseware\TaskGroup::class,
+            \Courseware\TaskFeedback::class => Schemas\Courseware\TaskFeedback::class,
+            \Courseware\Template::class => Schemas\Courseware\Template::class,
         ];
     }
 }
diff --git a/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php b/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php
index 15c5ee2427d..49bd4cdc81d 100755
--- a/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php
+++ b/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php
@@ -22,6 +22,7 @@ class StructuralElement extends SchemaProvider
     const REL_OWNER = 'owner';
     const REL_PARENT = 'parent';
     const REL_USER = 'user';
+    const REL_TASK = 'task';
 
     /**
      * {@inheritdoc}
@@ -79,11 +80,7 @@ class StructuralElement extends SchemaProvider
             $this->shouldInclude($context, self::REL_CONTAINERS)
         );
 
-        $relationships = $this->addRangeRelationship(
-            $relationships,
-            $resource,
-            $context
-        );
+        $relationships = $this->addRangeRelationship($relationships, $resource, $context);
 
         $relationships = $this->addOwnerRelationship(
             $relationships,
@@ -127,6 +124,12 @@ class StructuralElement extends SchemaProvider
             $this->shouldInclude($context, self::REL_IMAGE)
         );
 
+        $relationships = $this->addTaskRelationship(
+            $relationships,
+            $resource,
+            $this->shouldInclude($context, self::REL_TASK)
+        );
+
         return $relationships;
     }
 
@@ -158,11 +161,9 @@ class StructuralElement extends SchemaProvider
 
         if ($includeData) {
             $user = $this->currentUser;
-            $relation[self::RELATIONSHIP_DATA] = $resource->children->filter(
-                function ($child) use ($user) {
-                    return $child->canRead($user);
-                }
-            );
+            $relation[self::RELATIONSHIP_DATA] = $resource->children->filter(function ($child) use ($user) {
+                return $child->canRead($user);
+            });
         }
 
         $relationships[self::REL_CHILDREN] = $relation;
@@ -241,7 +242,9 @@ class StructuralElement extends SchemaProvider
             $relation[self::RELATIONSHIP_LINKS] = [
                 Link::RELATED => $this->createLinkToUser($resource['edit_blocker_id']),
             ];
-            $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->edit_blocker : new Identifier($resource['edit_blocker_id'], \JsonApi\Schemas\User::TYPE);
+            $relation[self::RELATIONSHIP_DATA] = $includeData
+                ? $resource->edit_blocker
+                : new Identifier($resource['edit_blocker_id'], \JsonApi\Schemas\User::TYPE);
         } else {
             $relation[self::RELATIONSHIP_DATA] = null;
         }
@@ -257,7 +260,9 @@ class StructuralElement extends SchemaProvider
             $relation[self::RELATIONSHIP_LINKS] = [
                 Link::RELATED => $this->createLinkToUser($resource['editor_id']),
             ];
-            $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->editor : new Identifier($resource['editor_id'], \JsonApi\Schemas\User::TYPE);
+            $relation[self::RELATIONSHIP_DATA] = $includeData
+                ? $resource->editor
+                : new Identifier($resource['editor_id'], \JsonApi\Schemas\User::TYPE);
         } else {
             $relation[self::RELATIONSHIP_DATA] = null;
         }
@@ -273,7 +278,9 @@ class StructuralElement extends SchemaProvider
             $relation[self::RELATIONSHIP_LINKS] = [
                 Link::RELATED => $this->createLinkToUser($resource['owner_id']),
             ];
-            $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->owner : new Identifier($resource['owner_id'], \JsonApi\Schemas\User::TYPE);
+            $relation[self::RELATIONSHIP_DATA] = $includeData
+                ? $resource->owner
+                : new Identifier($resource['owner_id'], \JsonApi\Schemas\User::TYPE);
         } else {
             $relation[self::RELATIONSHIP_DATA] = null;
         }
@@ -290,7 +297,9 @@ class StructuralElement extends SchemaProvider
             $relation[self::RELATIONSHIP_LINKS] = [
                 Link::RELATED => $this->createLinkToStructuralElement($resource['parent_id']),
             ];
-            $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->parent : new Identifier($resource['parent_id'], self::TYPE);
+            $relation[self::RELATIONSHIP_DATA] = $includeData
+                ? $resource->parent
+                : new Identifier($resource['parent_id'], self::TYPE);
         } else {
             $relation[self::RELATIONSHIP_DATA] = null;
         }
@@ -307,7 +316,9 @@ class StructuralElement extends SchemaProvider
                 self::RELATIONSHIP_LINKS => [
                     Link::RELATED => $this->createLinkToCourse($resource['range_id']),
                 ],
-                self::RELATIONSHIP_DATA => $includeData ? $resource->course : new Identifier($resource['range_id'], \JsonApi\Schemas\Course::TYPE),
+                self::RELATIONSHIP_DATA => $includeData
+                    ? $resource->course
+                    : new Identifier($resource['range_id'], \JsonApi\Schemas\Course::TYPE),
             ];
         } elseif ($resource['range_type'] === 'user') {
             $includeData = $this->shouldInclude($context, self::REL_USER);
@@ -315,13 +326,34 @@ class StructuralElement extends SchemaProvider
                 self::RELATIONSHIP_LINKS => [
                     Link::RELATED => $this->createLinkToUser($resource['range_id']),
                 ],
-                self::RELATIONSHIP_DATA => $includeData ? $resource->user : new Identifier($resource['range_id'], \JsonApi\Schemas\User::TYPE),
+                self::RELATIONSHIP_DATA => $includeData
+                    ? $resource->user
+                    : new Identifier($resource['range_id'], \JsonApi\Schemas\User::TYPE),
             ];
         }
 
         return $relationships;
     }
 
+    private function addTaskRelationship(
+        array $relationships,
+        $resource,
+        bool $shouldInclude
+    ): array {
+        $relationships[self::REL_TASK] = $resource->task
+            ? [
+                self::RELATIONSHIP_LINKS => [
+                    Link::RELATED => $this->createLinkToResource($resource->task),
+                ],
+                self::RELATIONSHIP_DATA => $resource->task,
+            ]
+            : [
+                self::RELATIONSHIP_DATA => null,
+            ];
+
+        return $relationships;
+    }
+
     private static $memo = [];
 
     private function createLinkToCourse($rangeId)
diff --git a/lib/classes/JsonApi/Schemas/Courseware/StructuralElementComment.php b/lib/classes/JsonApi/Schemas/Courseware/StructuralElementComment.php
new file mode 100755
index 00000000000..7fe75ff4f71
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Courseware/StructuralElementComment.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace JsonApi\Schemas\Courseware;
+
+use JsonApi\Schemas\SchemaProvider;
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class StructuralElementComment extends SchemaProvider
+{
+    const TYPE = 'courseware-structural-element-comments';
+
+    const REL_USER = 'user';
+    const REL_STRUCTURAL_ELEMENT = 'structural-element';
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'comment' => (string) $resource['comment'],
+            'mkdate' => date('c', $resource['mkdate']),
+            'chdate' => date('c', $resource['chdate']),
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $isPrimary = $context->getPosition()->getLevel() === 0;
+        $includeList = $context->getIncludePaths();
+
+        $relationships = [];
+
+        $relationships[self::REL_STRUCTURAL_ELEMENT] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($resource->structural_element),
+            ],
+            self::RELATIONSHIP_DATA => $resource->structural_element,
+        ];
+
+        $relationships[self::REL_USER] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($resource->user),
+            ],
+            self::RELATIONSHIP_DATA => $resource->user,
+        ];
+
+        return $relationships;
+    }
+}
diff --git a/lib/classes/JsonApi/Schemas/Courseware/StructuralElementFeedback.php b/lib/classes/JsonApi/Schemas/Courseware/StructuralElementFeedback.php
new file mode 100755
index 00000000000..c7a9cc80553
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Courseware/StructuralElementFeedback.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace JsonApi\Schemas\Courseware;
+
+use JsonApi\Schemas\SchemaProvider;
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class StructuralElementFeedback extends SchemaProvider
+{
+    const TYPE = 'courseware-structural-element-feedback';
+
+    const REL_USER = 'user';
+    const REL_STRUCTURAL_ELEMENT = 'structural-element';
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'feedback' => (string) $resource['feedback'],
+            'mkdate' => date('c', $resource['mkdate']),
+            'chdate' => date('c', $resource['chdate']),
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $isPrimary = $context->getPosition()->getLevel() === 0;
+        $includeList = $context->getIncludePaths();
+
+        $relationships = [];
+
+        $relationships[self::REL_STRUCTURAL_ELEMENT] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($resource->structural_element),
+            ],
+            self::RELATIONSHIP_DATA => $resource->structural_element,
+        ];
+
+        $relationships[self::REL_USER] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($resource->user),
+            ],
+            self::RELATIONSHIP_DATA => $resource->user,
+        ];
+
+        return $relationships;
+    }
+}
diff --git a/lib/classes/JsonApi/Schemas/Courseware/Task.php b/lib/classes/JsonApi/Schemas/Courseware/Task.php
new file mode 100755
index 00000000000..32d4a187b4d
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Courseware/Task.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace JsonApi\Schemas\Courseware;
+
+use JsonApi\Schemas\SchemaProvider;
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class Task extends SchemaProvider
+{
+    const TYPE = 'courseware-tasks';
+
+    const REL_FEEDBACK = 'task-feedback';
+    const REL_SOLVER = 'solver';
+    const REL_STRUCTURAL_ELEMENT = 'structural-element';
+    const REL_TASK_GROUP = 'task-group';
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'progress' => (float) $resource->getTaskProgress(),
+            'submission-date' => date('c', $resource['submission_date']),
+            'submitted' => (bool) $resource['submitted'],
+            'renewal' => empty($resource['renewal']) ? null : (string) $resource['renewal'],
+            'renewal-date' => $resource['renewal_date'] ? date('c', $resource['renewal_date']) : null,
+            'mkdate' => date('c', $resource['mkdate']),
+            'chdate' => date('c', $resource['chdate']),
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        $relationships[self::REL_FEEDBACK] = $resource->getFeedback()
+            ? [
+                self::RELATIONSHIP_LINKS => [
+                    Link::RELATED => $this->createLinkToResource($resource->getFeedback()),
+                ],
+                self::RELATIONSHIP_DATA => $resource->getFeedback(),
+            ]
+            : [self::RELATIONSHIP_DATA => null];
+
+        $relationships[self::REL_SOLVER] = $resource['solver_id']
+            ? [
+                self::RELATIONSHIP_LINKS => [
+                    Link::RELATED => $this->createLinkToResource($resource->getSolver()),
+                ],
+                self::RELATIONSHIP_DATA => $resource->getSolver(),
+            ]
+            : [self::RELATIONSHIP_DATA => null];
+
+        $relationships[self::REL_STRUCTURAL_ELEMENT] = $resource['structural_element_id']
+            ? [
+                self::RELATIONSHIP_LINKS => [
+                    Link::RELATED => $this->createLinkToResource($resource['structural_element']),
+                ],
+                self::RELATIONSHIP_DATA => $resource['structural_element'],
+            ]
+            : [self::RELATIONSHIP_DATA => null];
+
+        $relationships[self::REL_TASK_GROUP] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($resource['task_group']),
+            ],
+            self::RELATIONSHIP_DATA => $resource['task_group'],
+        ];
+
+        return $relationships;
+    }
+}
diff --git a/lib/classes/JsonApi/Schemas/Courseware/TaskFeedback.php b/lib/classes/JsonApi/Schemas/Courseware/TaskFeedback.php
new file mode 100755
index 00000000000..8d800c88a89
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Courseware/TaskFeedback.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace JsonApi\Schemas\Courseware;
+
+use JsonApi\Schemas\SchemaProvider;
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class TaskFeedback extends SchemaProvider
+{
+    const TYPE = 'courseware-task-feedback';
+
+    const REL_TASK = 'task';
+    const REL_LECTURER = 'lecturer';
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'content'   => (string) $resource['content'],
+            'mkdate'    => date('c', $resource['mkdate']),
+            'chdate'    => date('c', $resource['chdate']),
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        $relationships[self::REL_LECTURER] = $resource['lecturer_id']
+        ? [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($resource->lecturer),
+            ],
+            self::RELATIONSHIP_DATA => $resource->lecturer,
+        ]
+        : [self::RELATIONSHIP_DATA => $resource->lecturer];
+
+        $relationships[self::REL_TASK] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_TASK),
+            ],
+        ];
+
+        return $relationships;
+    }
+}
\ No newline at end of file
diff --git a/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php b/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php
new file mode 100755
index 00000000000..12dbc6c5855
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace JsonApi\Schemas\Courseware;
+
+use Courseware\StructuralElement;
+use JsonApi\Schemas\SchemaProvider;
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Identifier;
+use Neomerx\JsonApi\Schema\Link;
+
+class TaskGroup extends SchemaProvider
+{
+    const TYPE = 'courseware-task-groups';
+
+    const REL_COURSE = 'course';
+    const REL_LECTURER = 'lecturer';
+    const REL_SOLVERS = 'solvers';
+    const REL_TARGET = 'target';
+    const REL_TASK_TEMPLATE = 'task-template';
+    const REL_TASKS = 'tasks';
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'solver-may-add-blocks' => (bool) $resource['solver_may_add_blocks'],
+            'title' => (string) $resource->title,
+            'mkdate' => date('c', $resource['mkdate']),
+            'chdate' => date('c', $resource['chdate']),
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        $relationships[self::REL_COURSE] = $resource['seminar_id']
+            ? [
+                self::RELATIONSHIP_LINKS => [
+                    Link::RELATED => $this->createLinkToResource($resource->course),
+                ],
+                self::RELATIONSHIP_DATA => $resource->course,
+            ]
+            : [self::RELATIONSHIP_DATA => null];
+
+        $relationships[self::REL_LECTURER] = $resource['lecturer_id']
+            ? [
+                self::RELATIONSHIP_LINKS => [
+                    Link::RELATED => $this->createLinkToResource($resource->lecturer),
+                ],
+                self::RELATIONSHIP_DATA => $resource->lecturer,
+            ]
+            : [self::RELATIONSHIP_DATA => null];
+
+        $relationships[self::REL_SOLVERS] = [
+            self::RELATIONSHIP_DATA => $resource->getSolvers(),
+        ];
+
+        $target = StructuralElement::build(['id' => $resource['target_id']]);
+        $relationships[self::REL_TARGET] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($target),
+            ],
+            self::RELATIONSHIP_DATA => $this->shouldInclude($context, self::REL_TARGET) ? $resource['target'] : $target,
+        ];
+
+        $taskTemplate = StructuralElement::build(['id' => $resource['task_template_id']]);
+        $relationships[self::REL_TASK_TEMPLATE] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($taskTemplate),
+            ],
+            self::RELATIONSHIP_DATA => $this->shouldInclude($context, self::REL_TASK_TEMPLATE)
+                ? $resource['task_template']
+                : $taskTemplate,
+        ];
+
+        $relationships[self::REL_TASKS] = [
+            self::RELATIONSHIP_DATA => $this->shouldInclude($context, self::REL_TASKS)
+                ? $resource['tasks']
+                : \DBManager::get()->fetchFirst(
+                    'SELECT id FROM cw_tasks WHERE task_group_id = ?',
+                    [$resource->getId()],
+                    function ($id) {
+                        return new Identifier($id, Task::TYPE);
+                    }
+                ),
+        ];
+
+        return $relationships;
+    }
+}
diff --git a/lib/classes/JsonApi/Schemas/Courseware/Template.php b/lib/classes/JsonApi/Schemas/Courseware/Template.php
new file mode 100755
index 00000000000..4c0d0cecd63
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Courseware/Template.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace JsonApi\Schemas\Courseware;
+
+use JsonApi\Schemas\SchemaProvider;
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class Template extends SchemaProvider
+{
+    const TYPE = 'courseware-templates';
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'name'      => (string) $resource['name'],
+            'purpose'   => (string) $resource['purpose'],
+            'structure' => (string) $resource['structure'],
+            'mkdate'    => date('c', $resource['mkdate']),
+            'chdate'    => date('c', $resource['chdate']),
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        return $relationships;
+    }
+}
\ No newline at end of file
diff --git a/lib/classes/JsonApi/Schemas/User.php b/lib/classes/JsonApi/Schemas/User.php
index 657f8f9c105..14787ce6d8e 100644
--- a/lib/classes/JsonApi/Schemas/User.php
+++ b/lib/classes/JsonApi/Schemas/User.php
@@ -16,6 +16,7 @@ class User extends SchemaProvider
     const REL_CONTACTS = 'contacts';
     const REL_COURSES = 'courses';
     const REL_COURSE_MEMBERSHIPS = 'course-memberships';
+    const REL_COURSEWARE_BOOKMARKS = 'courseware-bookmarks';
     const REL_EVENTS = 'events';
     const REL_FILES = 'file-refs';
     const REL_FOLDERS = 'folders';
@@ -165,6 +166,7 @@ class User extends SchemaProvider
             $relationships = $this->getNewsRelationship($relationships, $user, $this->shouldInclude($context, self::REL_NEWS));
             $relationships = $this->getOutboxRelationship($relationships, $user, $this->shouldInclude($context, self::REL_OUTBOX));
             $relationships = $this->getScheduleRelationship($relationships, $user, $this->shouldInclude($context, self::REL_SCHEDULE));
+            $relationships = $this->getCoursewareBookmarksRelationship($relationships, $user, $this->shouldInclude($context, self::REL_COURSEWARE_BOOKMARKS));
         }
 
         return $relationships;
@@ -251,6 +253,22 @@ class User extends SchemaProvider
             self::RELATIONSHIP_LINKS => [
                 Link::RELATED => $this->getRelationshipRelatedLink($user, self::REL_COURSE_MEMBERSHIPS),
             ],
+            self::RELATIONSHIP_DATA => $resource->course_memberships,
+        ];
+
+        return $relationships;
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    private function getCoursewareBookmarksRelationship(array $relationships, \User $user, $includeData)
+    {
+        $relationships[self::REL_COURSEWARE_BOOKMARKS] = [
+            self::RELATIONSHIP_LINKS_SELF => true,
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($user, self::REL_COURSEWARE_BOOKMARKS),
+            ],
         ];
 
         return $relationships;
diff --git a/lib/models/Courseware/Block.php b/lib/models/Courseware/Block.php
index 48009fe8697..05d5d45c1b1 100755
--- a/lib/models/Courseware/Block.php
+++ b/lib/models/Courseware/Block.php
@@ -207,4 +207,19 @@ class Block extends \SimpleORMap
             return 'error';
         }
     }
+
+    public function getStructuralElement(): ?StructuralElement
+    {
+        $sql = 'SELECT se.*
+                FROM cw_blocks b
+                JOIN cw_containers c ON c.id = b.container_id
+                JOIN cw_structural_elements se ON se.id = c.structural_element_id
+                WHERE  b.id = ?';
+        $structuralElement = \DBManager::get()->fetchOne($sql, [$this->getId()]);
+        if (!count($structuralElement)) {
+            return null;
+        }
+
+        return StructuralElement::build($structuralElement, false);
+    }
 }
diff --git a/lib/models/Courseware/BlockComment.php b/lib/models/Courseware/BlockComment.php
index c985e8978f3..16f3a73293d 100755
--- a/lib/models/Courseware/BlockComment.php
+++ b/lib/models/Courseware/BlockComment.php
@@ -41,4 +41,20 @@ class BlockComment extends \SimpleORMap
 
         parent::configure($config);
     }
+
+    public function getStructuralElement(): ?StructuralElement
+    {
+        $sql = 'SELECT se.*
+                FROM cw_block_comments bc
+                JOIN cw_blocks b ON b.id = bc.block_id
+                JOIN cw_containers c ON c.id = b.container_id
+                JOIN cw_structural_elements se ON se.id = c.structural_element_id
+                WHERE  bc.id = ?';
+        $structuralElement = \DBManager::get()->fetchOne($sql, [$this->getId()]);
+        if (!count($structuralElement)) {
+            return null;
+        }
+
+        return StructuralElement::build($structuralElement, false);
+    }
 }
diff --git a/lib/models/Courseware/BlockFeedback.php b/lib/models/Courseware/BlockFeedback.php
index 08e5c6f7d06..422577097a1 100755
--- a/lib/models/Courseware/BlockFeedback.php
+++ b/lib/models/Courseware/BlockFeedback.php
@@ -39,4 +39,20 @@ class BlockFeedback extends \SimpleORMap
 
         parent::configure($config);
     }
+
+    public function getStructuralElement(): ?StructuralElement
+    {
+        $sql = 'SELECT se.*
+                FROM cw_block_feedbacks bf
+                JOIN cw_blocks b ON b.id = bf.block_id
+                JOIN cw_containers c ON c.id = b.container_id
+                JOIN cw_structural_elements se ON se.id = c.structural_element_id
+                WHERE  bf.id = ?';
+        $structuralElement = \DBManager::get()->fetchOne($sql, [$this->getId()]);
+        if (!count($structuralElement)) {
+            return null;
+        }
+
+        return StructuralElement::build($structuralElement, false);
+    }
 }
diff --git a/lib/models/Courseware/BlockTypes/BlockType.php b/lib/models/Courseware/BlockTypes/BlockType.php
index 0d994344d12..98e24aef69b 100755
--- a/lib/models/Courseware/BlockTypes/BlockType.php
+++ b/lib/models/Courseware/BlockTypes/BlockType.php
@@ -310,7 +310,7 @@ abstract class BlockType
                 $user
             );
 
-            return isset($copiedFile) ? $copiedFile->id : '';
+            return isset($copiedFile->id) ? $copiedFile->id : '';
         }
 
         return '';
@@ -373,4 +373,17 @@ abstract class BlockType
 
         return $destinationFolder;
     }
+
+    public function pdfExport()
+    {
+        $html = '<h5>' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '</h5>';
+        $html .= '<h6>' . _('Block-Daten') . ': ' . '</h6>';
+        foreach($this->getPayload() as $key => $value) {
+            if ($value !== '') {
+                $html .= '<h6>' . $key . ' => ' . $value . '</h6>';
+            }
+        }
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/BlockTypes/Code.php b/lib/models/Courseware/BlockTypes/Code.php
index 2ee634255a3..99888d4f380 100755
--- a/lib/models/Courseware/BlockTypes/Code.php
+++ b/lib/models/Courseware/BlockTypes/Code.php
@@ -58,4 +58,12 @@ class Code extends BlockType
     {
         return [];
     }
+
+    public function pdfExport()
+    {
+        $html = '<h5>' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '</h5>';
+        $html .= '<pre>' . htmlspecialchars($this->getPayload()['content']) . '</pre>';
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/BlockTypes/Confirm.php b/lib/models/Courseware/BlockTypes/Confirm.php
index 7204e6bf8c8..92c61d4d467 100755
--- a/lib/models/Courseware/BlockTypes/Confirm.php
+++ b/lib/models/Courseware/BlockTypes/Confirm.php
@@ -57,4 +57,12 @@ class Confirm extends BlockType
     {
         return [];
     }
+
+    public function pdfExport()
+    {
+        $html = '<h5>' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '</h5>';
+        $html .= '<p>' . htmlspecialchars($this->getPayload()['text']) . '</p>';
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/BlockTypes/Date.php b/lib/models/Courseware/BlockTypes/Date.php
index df37590b315..66075f32fcd 100755
--- a/lib/models/Courseware/BlockTypes/Date.php
+++ b/lib/models/Courseware/BlockTypes/Date.php
@@ -58,4 +58,12 @@ class Date extends BlockType
     {
         return [];
     }
+
+    public function pdfExport()
+    {
+        $html = '<h5>' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '</h5>';
+        $html .= '<p>' . date('d.m.Y h:i', (int) $this->getPayload()['timestamp'] / 1000) . '</p>';
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/BlockTypes/Headline.php b/lib/models/Courseware/BlockTypes/Headline.php
index 855e2a94a54..a3add744f57 100755
--- a/lib/models/Courseware/BlockTypes/Headline.php
+++ b/lib/models/Courseware/BlockTypes/Headline.php
@@ -106,4 +106,13 @@ class Headline extends BlockType
     {
         return [];
     }
+
+    public function pdfExport()
+    {
+        $html = '<h5>' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '</h5>';
+        $html .= '<h5>' . htmlspecialchars($this->getPayload()['title']) . '</h5>';
+        $html .= '<h6>' . htmlspecialchars($this->getPayload()['subtitle']) . '</h6>';
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/BlockTypes/KeyPoint.php b/lib/models/Courseware/BlockTypes/KeyPoint.php
index fae16d3b9a6..90f4852770a 100755
--- a/lib/models/Courseware/BlockTypes/KeyPoint.php
+++ b/lib/models/Courseware/BlockTypes/KeyPoint.php
@@ -59,4 +59,12 @@ class KeyPoint extends BlockType
     {
         return [];
     }
+
+    public function pdfExport()
+    {
+        $html = '<h5>' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '</h5>';
+        $html .= '<p>' . htmlspecialchars($this->getPayload()['text']) . '</p>';
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/BlockTypes/Link.php b/lib/models/Courseware/BlockTypes/Link.php
index 7b93aeb7166..1e804b74580 100755
--- a/lib/models/Courseware/BlockTypes/Link.php
+++ b/lib/models/Courseware/BlockTypes/Link.php
@@ -60,4 +60,13 @@ class Link extends BlockType
     {
         return [];
     }
+
+    public function pdfExport()
+    {
+        $html = '<h5>' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '</h5>';
+        $html .= '<p>' . htmlspecialchars($this->getPayload()['title']) . '</p>';
+        $html .= '<p>' . htmlspecialchars($this->getPayload()['url']) . '</p>';
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/BlockTypes/Text.php b/lib/models/Courseware/BlockTypes/Text.php
index c4fe67c68db..3857c02911e 100755
--- a/lib/models/Courseware/BlockTypes/Text.php
+++ b/lib/models/Courseware/BlockTypes/Text.php
@@ -165,4 +165,12 @@ class Text extends BlockType
             return array();
         });
     }
+
+    public function pdfExport()
+    {
+        $html = '<h5>' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '</h5>';
+        $html .= $this->getPayload()['text'];
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/BlockTypes/Typewriter.php b/lib/models/Courseware/BlockTypes/Typewriter.php
index 583e64e7a1e..064065bd613 100755
--- a/lib/models/Courseware/BlockTypes/Typewriter.php
+++ b/lib/models/Courseware/BlockTypes/Typewriter.php
@@ -60,4 +60,12 @@ class Typewriter extends BlockType
     {
         return [];
     }
+
+    public function pdfExport()
+    {
+        $html = '<h5>' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '</h5>';
+        $html .= '<p>' . htmlspecialchars($this->getPayload()['text']) . '</p>';
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/Bookmark.php b/lib/models/Courseware/Bookmark.php
index d6e5910521d..96f919dab1b 100755
--- a/lib/models/Courseware/Bookmark.php
+++ b/lib/models/Courseware/Bookmark.php
@@ -54,12 +54,12 @@ class Bookmark extends \SimpleORMap
     /**
      * Returns all bookmarks of a user.
      *
-     * @param string $userId the user's ID for whom to search for bookmarks
+     * @param \User $user the user for whom to search for bookmarks
      *
      * @return Bookmark[] the list of bookmarks
      */
-    public function findUsersBookmarks(string $userId): array
+    public function findUsersBookmarks($user): array
     {
-        return self::findBySQL('user_id = ?', [$userId]);
+        return self::findBySQL('user_id = ? ORDER BY chdate', [$user->id]);
     }
 }
diff --git a/lib/models/Courseware/ContainerTypes/AccordionContainer.php b/lib/models/Courseware/ContainerTypes/AccordionContainer.php
index 832520401aa..510a388cdbb 100755
--- a/lib/models/Courseware/ContainerTypes/AccordionContainer.php
+++ b/lib/models/Courseware/ContainerTypes/AccordionContainer.php
@@ -56,4 +56,28 @@ class AccordionContainer extends ContainerType
 
         return Schema::fromJsonString(file_get_contents($schemaFile));
     }
+
+    public function pdfExport()
+    {
+        $html = '<h3>' . sprintf(_('Container-Typ: %s'), $this->getTitle()) . '</h3>';
+
+        $payload = $this->getPayload();
+
+        $sections = $payload['sections'];
+        foreach ($sections as $section) {
+            $block_ids = $section['blocks'];
+            $html .= '<h4>' . $section['name'] . '</h4>';
+            foreach ($block_ids as $block_id) {
+                $block = $this->container->blocks->find($block_id);
+                if ($block) {
+                    $html .= $block->type->PdfExport();
+                }
+                else {
+                    $html .= '<p>' . _('Block konnte nicht gefunden werden') . '</p>';
+                }
+            }
+        }
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/ContainerTypes/ContainerType.php b/lib/models/Courseware/ContainerTypes/ContainerType.php
index 34030775887..a2552d415af 100755
--- a/lib/models/Courseware/ContainerTypes/ContainerType.php
+++ b/lib/models/Courseware/ContainerTypes/ContainerType.php
@@ -244,4 +244,15 @@ abstract class ContainerType
             return _('unbekannter Courseware-Container');
         }
     }
+
+    public function pdfExport()
+    {
+        $html = '<h3>' . sprintf(_('Container-Typ: %s'), $this->getTitle()) . '</h3>';
+
+        foreach ($this->container->blocks as $block) {
+            $html .= $block->type->PdfExport();
+        }
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/ContainerTypes/ListContainer.php b/lib/models/Courseware/ContainerTypes/ListContainer.php
index 4918271a40e..d8e283c24f5 100755
--- a/lib/models/Courseware/ContainerTypes/ListContainer.php
+++ b/lib/models/Courseware/ContainerTypes/ListContainer.php
@@ -56,4 +56,24 @@ class ListContainer extends ContainerType
 
         return Schema::fromJsonString(file_get_contents($schemaFile));
     }
+
+    public function pdfExport()
+    {
+        $html = '<h3>' . sprintf(_('Container-Typ: %s'), $this->getTitle()) . '</h3>';
+
+        $payload = $this->getPayload();
+        $block_ids = $payload['sections'][0]['blocks'];
+
+        foreach ($block_ids as $block_id) {
+            $block = $this->container->blocks->find($block_id);
+            if ($block) {
+                $html .= $block->type->PdfExport();
+            }
+            else {
+                $html .= '<p>' . _('Block konnte nicht gefunden werden') . '</p>';
+            }
+        }
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/ContainerTypes/TabsContainer.php b/lib/models/Courseware/ContainerTypes/TabsContainer.php
index b884bbb67ee..72a15f464a8 100755
--- a/lib/models/Courseware/ContainerTypes/TabsContainer.php
+++ b/lib/models/Courseware/ContainerTypes/TabsContainer.php
@@ -57,4 +57,28 @@ class TabsContainer extends ContainerType
 
         return Schema::fromJsonString(file_get_contents($schemaFile));
     }
+
+    public function pdfExport()
+    {
+        $html = '<h3>' . sprintf(_('Container-Typ: %s'), $this->getTitle()) . '</h3>';
+
+        $payload = $this->getPayload();
+
+        $sections = $payload['sections'];
+        foreach ($sections as $section) {
+            $block_ids = $section['blocks'];
+            $html .= '<h4>' . $section['name'] . '</h4>';
+            foreach ($block_ids as $block_id) {
+                $block = $this->container->blocks->find($block_id);
+                if ($block) {
+                    $html .= $block->type->PdfExport();
+                }
+                else {
+                    $html .= '<p>' . _('Block konnte nicht gefunden werden') . '</p>';
+                }
+            }
+        }
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/StructuralElement.php b/lib/models/Courseware/StructuralElement.php
index 44451504a56..54e28805414 100755
--- a/lib/models/Courseware/StructuralElement.php
+++ b/lib/models/Courseware/StructuralElement.php
@@ -44,6 +44,9 @@ use User;
  * @property \User                          $editor             belongs_to User
  * @property ?\User                         $edit_blocker       belongs_to User
  * @property ?\FileRef                      $image              has_one FileRef
+ * @property ?\Courseware\Task              $task               has_one Courseware\Task
+ * @property \SimpleORMapCollection         $comments           has_many Courseware\StructuralElementComment
+ * @property \SimpleORMapCollection         $feedback           has_many Courseware\StructuralElementFeedback
  *
  * @SuppressWarnings(PHPMD.TooManyPublicMethods)
  * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
@@ -76,6 +79,12 @@ class StructuralElement extends \SimpleORMap
             'order_by' => 'ORDER BY position',
         ];
 
+        $config['has_one']['task'] = [
+            'class_name' => Task::class,
+            'assoc_foreign_key' => 'structural_element_id',
+            'on_delete' => 'delete',
+        ];
+
         $config['belongs_to']['parent'] = [
             'class_name' => StructuralElement::class,
             'foreign_key' => 'parent_id',
@@ -97,14 +106,17 @@ class StructuralElement extends \SimpleORMap
             'class_name' => User::class,
             'foreign_key' => 'owner_id',
         ];
+
         $config['belongs_to']['editor'] = [
             'class_name' => User::class,
             'foreign_key' => 'editor_id',
         ];
+
         $config['belongs_to']['edit_blocker'] = [
             'class_name' => User::class,
             'foreign_key' => 'edit_blocker_id',
         ];
+
         $config['has_one']['image'] = [
             'class_name' => \FileRef::class,
             'foreign_key' => 'image_id',
@@ -112,6 +124,22 @@ class StructuralElement extends \SimpleORMap
             'on_store' => 'store',
         ];
 
+        $config['has_many']['comments'] = [
+            'class_name' => StructuralElementComment::class,
+            'assoc_foreign_key' => 'structural_element_id',
+            'on_delete' => 'delete',
+            'on_store' => 'store',
+            'order_by' => 'ORDER BY chdate',
+        ];
+
+        $config['has_many']['feedback'] = [
+            'class_name' => StructuralElementFeedback::class,
+            'assoc_foreign_key' => 'structural_element_id',
+            'on_delete' => 'delete',
+            'on_store' => 'store',
+            'order_by' => 'ORDER BY chdate',
+        ];
+
         parent::configure($config);
     }
 
@@ -169,6 +197,14 @@ class StructuralElement extends \SimpleORMap
         return null === $this->parent_id;
     }
 
+    /**
+     * @return bool true, if this object purpose is task, false otherwise
+     */
+    public function isTask(): bool
+    {
+        return $this->purpose === 'task';
+    }
+
     /**
      * @param User $user the user to validate
      *
@@ -187,12 +223,25 @@ class StructuralElement extends \SimpleORMap
                 return $this->range_id === $user->id;
 
             case 'course':
-                $haveStudipPerm = $GLOBALS['perm']->have_studip_perm(
-                    \CourseConfig::get($this->range_id)->COURSEWARE_EDITING_PERMISSION,
-                    $this->range_id,
-                    $user->id
-                );
-                if ($haveStudipPerm) {
+                $hasEditingPermission = $this->hasEditingPermission($user);
+                if ($this->isTask()) {
+                    // TODO: Was tun wir, wenn dieses Strukturelement purpose=task aber keinen Task hat?
+                    if (!$this->task) {
+                        return false;
+                    }
+
+                    if ($hasEditingPermission) {
+                        return false;
+                    }
+
+                    if ($this->task->isSubmitted()) {
+                        return false;
+                    }
+
+                    return $this->task->userIsASolver($user);
+                }
+
+                if ($hasEditingPermission) {
                     return true;
                 }
 
@@ -259,6 +308,19 @@ class StructuralElement extends \SimpleORMap
                     return false;
                 }
 
+                if ($this->isTask()) {
+                    // TODO: Was tun wir, wenn dieses Strukturelement purpose=task aber keinen Task hat?
+                    if (!$this->task) {
+                        return false;
+                    }
+
+                    if ($this->task->isSubmitted() && $this->hasEditingPermission($user)) {
+                        return true;
+                    }
+
+                    return $this->task->userIsASolver($user);
+                }
+
                 if ($this->canEdit($user)) {
                     return true;
                 }
@@ -274,6 +336,19 @@ class StructuralElement extends \SimpleORMap
         }
     }
 
+    /**
+     * @param \User|\Seminar_User $user
+     */
+    public function hasEditingPermission($user): bool
+    {
+        return $GLOBALS['perm']->have_perm('root', $user->id) ||
+            $GLOBALS['perm']->have_studip_perm(
+                \CourseConfig::get($this->range_id)->COURSEWARE_EDITING_PERMISSION,
+                $this->range_id,
+                $user->id
+            );
+    }
+
     private function hasReadApproval($user): bool
     {
         if (!count($this->read_approval)) {
@@ -410,10 +485,7 @@ class StructuralElement extends \SimpleORMap
         foreach ($this->containers as $container) {
             foreach ($container->blocks as $block) {
                 /** @var ?UserProgress $progress */
-                $progress = UserProgress::findOneBySQL('user_id = ? and block_id = ?', [
-                    $user->id,
-                    $block->id,
-                ]);
+                $progress = UserProgress::findOneBySQL('user_id = ? and block_id = ?', [$user->id, $block->id]);
 
                 if (!$progress || $progress->grade != 1) {
                     return false;
@@ -438,7 +510,11 @@ class StructuralElement extends \SimpleORMap
         if ('all' == $purpose) {
             return self::findBySQL('range_id = ? AND parent_id = ? ORDER BY position ASC', [$userId, $root->id]);
         } else {
-            return self::findBySQL('range_id = ? AND parent_id = ? AND purpose = ? ORDER BY position ASC', [$userId,  $root->id, $purpose]);
+            return self::findBySQL('range_id = ? AND parent_id = ? AND purpose = ? ORDER BY position ASC', [
+                $userId,
+                $root->id,
+                $purpose,
+            ]);
         }
     }
 
@@ -563,7 +639,7 @@ class StructuralElement extends \SimpleORMap
 SQL;
         $params = [$range->getRangeId(), $range->getRangeType(), $user->id];
 
-        return \DBManager::get()->fetchAll($sql, $params, StructuralElement::class.'::buildExisting');
+        return \DBManager::get()->fetchAll($sql, $params, StructuralElement::class . '::buildExisting');
     }
 
     /**
@@ -638,4 +714,64 @@ SQL;
             $child->copy($user, $newElement);
         }
     }
+
+    public function pdfExport($user)
+    {
+        $doc = new \ExportPDF('P', 'mm', 'A4', true, 'UTF-8', false);
+        $doc->setHeaderTitle(_('Courseware'));
+        if ($this->course) {
+            $doc->setHeaderTitle(sprintf(_('Courseware aus %s'), $this->course->name));
+        }
+        if ($this->user) {
+            $doc->setHeaderTitle(sprintf(_('Courseware von %s'), $this->user->getFullname()));
+        }
+
+        $doc->addPage();
+
+        if (!self::canRead($user)) {
+            $doc->addContent(_('Diese Seite steht Ihnen nicht zur Verfügung!'));
+
+            return $doc;
+        }
+
+        $doc->writeHTML($this->getElementPdfExport());
+
+        return $doc;
+    }
+
+    private function getElementPdfExport(string $parent_name = '', bool $with_children = false)
+    {
+        if ($parent_name !== '') {
+            $parent_name .= ' / ';
+        }
+        $html = '<h1>' . $parent_name . $this->title . '</h1>';
+        $html .= $this->getContainerPdfExport();
+        if ($with_children) {
+            $html .= $this->getChildrenPdfExport($parent_name);
+        }
+
+        return $html;
+    }
+
+    private function getChildrenPdfExport(string $parent_name)
+    {
+        $children = self::findBySQL('parent_id = ?', [$this->id]);
+        $html = '';
+        foreach ($children as $child) {
+            $html .= $child->getElementPdfExport($parent_name . $this->title);
+        }
+
+        return $html;
+    }
+
+    private function getContainerPdfExport()
+    {
+        $containers = \Courseware\Container::findBySQL('structural_element_id = ?', [$this->id]);
+
+        foreach ($containers as $container) {
+            $html .= $container->type->pdfExport();
+        }
+
+        return $html;
+    }
 }
diff --git a/lib/models/Courseware/StructuralElementComment.php b/lib/models/Courseware/StructuralElementComment.php
new file mode 100755
index 00000000000..e5fbc818a9c
--- /dev/null
+++ b/lib/models/Courseware/StructuralElementComment.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Courseware;
+
+use User;
+
+/**
+ * Courseware's comments on structural elements.
+ *
+ * @author  Ron Lucke <lucke@elan-ev.de>
+ * @license GPL2 or any later version
+ *
+ * @since   Stud.IP 5.1
+ *
+ * @property int                            $id                      database column
+ * @property int                            $structural_element_id   database column
+ * @property string                         $user_id                 database column
+ * @property string                         $comment                 database column
+ * @property int                            $mkdate                  database column
+ * @property int                            $chdate                  database column
+ * @property \User                          $user                    belongs_to User
+ * @property \Courseware\StructuralElement  $structural_element      belongs_to Courseware\StructuralElement
+ */
+class StructuralElementComment extends \SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'cw_structural_element_comments';
+
+        $config['belongs_to']['user'] = [
+            'class_name' => User::class,
+            'foreign_key' => 'user_id',
+        ];
+
+        $config['belongs_to']['structural_element'] = [
+            'class_name' => StructuralElement::class,
+            'foreign_key' => 'structural_element_id',
+        ];
+
+        parent::configure($config);
+    }
+}
diff --git a/lib/models/Courseware/StructuralElementFeedback.php b/lib/models/Courseware/StructuralElementFeedback.php
new file mode 100755
index 00000000000..a2e3b8b97ab
--- /dev/null
+++ b/lib/models/Courseware/StructuralElementFeedback.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Courseware;
+
+use User;
+
+/**
+ * Courseware's feedback on structural elements.
+ *
+ * @author  Ron Lucke <lucke@elan-ev.de>
+ * @license GPL2 or any later version
+ *
+ * @since   Stud.IP 5.1
+ *
+ * @property int                            $id                      database column
+ * @property int                            $structural_element_id   database column
+ * @property string                         $user_id                 database column
+ * @property string                         $feedback                 database column
+ * @property int                            $mkdate                  database column
+ * @property int                            $chdate                  database column
+ * @property \User                          $user                    belongs_to User
+ * @property \Courseware\StructuralElement  $structural_element      belongs_to Courseware\StructuralElement
+ */
+class StructuralElementFeedback extends \SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'cw_structural_element_feedbacks';
+
+        $config['belongs_to']['user'] = [
+            'class_name' => User::class,
+            'foreign_key' => 'user_id',
+        ];
+
+        $config['belongs_to']['structural_element'] = [
+            'class_name' => StructuralElement::class,
+            'foreign_key' => 'structural_element_id',
+        ];
+
+        parent::configure($config);
+    }
+}
diff --git a/lib/models/Courseware/Task.php b/lib/models/Courseware/Task.php
new file mode 100755
index 00000000000..fe61a2e618c
--- /dev/null
+++ b/lib/models/Courseware/Task.php
@@ -0,0 +1,217 @@
+<?php
+
+namespace Courseware;
+
+use User;
+
+/**
+ * Courseware's tasks.
+ *
+ * @author  Ron Lucke <lucke@elan-ev.de>
+ * @license GPL2 or any later version
+ *
+ * @since   Stud.IP 5.1
+ *
+ * @property int $id database column
+ * @property int $task_group_id database column
+ * @property int $structural_element_id database column
+ * @property string $solver_id database column
+ * @property string $solver_type database column
+ * @property int $submission_date database column
+ * @property int $submitted database column
+ * @property string $renewal database column
+ * @property int $renewal_date database column
+ * @property int $feedback_id database column
+ * @property int $mkdate database column
+ * @property int $chdate database column
+ * @property \Courseware\TaskGroup $task_group belongs_to Courseware\TaskGroup
+ * @property \Courseware\StructuralElement $structural_element belongs_to Courseware\TaskGroup
+ * @property \User $user belongs_to User
+ * @property \Statusgruppen $group belongs_to Statusgruppen
+ * @property \Courseware\TaskFeedback $task_feedback belongs_to Courseware\TaskFeedback
+ * @property-read \User|\Statusgruppen|null $solver belongs_to User or Statusgruppen
+ */
+class Task extends \SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'cw_tasks';
+
+        $config['belongs_to']['task_group'] = [
+            'class_name' => TaskGroup::class,
+            'foreign_key' => 'task_group_id',
+        ];
+
+        $config['belongs_to']['structural_element'] = [
+            'class_name' => StructuralElement::class,
+            'foreign_key' => 'structural_element_id',
+        ];
+
+        $config['belongs_to']['lecturer'] = [
+            'class_name' => User::class,
+            'foreign_key' => 'lecturer_id',
+        ];
+
+        $config['belongs_to']['user'] = [
+            'class_name' => User::class,
+            'foreign_key' => 'solver_id',
+            'assoc_foreign_key' => 'user_id',
+        ];
+
+        $config['belongs_to']['group'] = [
+            'class_name' => \Statusgruppen::class,
+            'foreign_key' => 'solver_id',
+            'assoc_foreign_key' => 'statusgruppe_id',
+        ];
+
+        $config['belongs_to']['course'] = [
+            'class_name' => \Course::class,
+            'foreign_key' => 'seminar_id',
+        ];
+
+        $config['belongs_to']['structural_element'] = [
+            'class_name' => StructuralElement::class,
+            'foreign_key' => 'structural_element_id',
+        ];
+
+        $config['belongs_to']['task_feedback'] = [
+            'class_name' => TaskFeedback::class,
+            'foreign_key' => 'feedback_id',
+        ];
+
+        $config['additional_fields']['solver'] = [
+            'get' => 'getSolver',
+            'set' => false,
+        ];
+
+        parent::configure($config);
+    }
+
+    /**
+     * Returns the structural element of this task.
+     * This structural element and all its children are part of the task.
+     *
+     * @return StructuralElement the structural element
+     */
+    public function getStructuralElement(): StructuralElement
+    {
+        return $this->structural_element;
+    }
+
+    /**
+     * Returns the feedback for this task.
+     *
+     * @return TaskFeedback the task feedback
+     */
+    public function getFeedback(): ?TaskFeedback
+    {
+        return $this->task_feedback;
+    }
+
+    /**
+     * @return bool true if task is submitted
+     */
+    public function isSubmitted(): bool
+    {
+        return 1 === (int) $this->submitted;
+    }
+
+    /**
+     * @param \User|\Seminar_User $user
+     */
+    public function canUpdate($user): bool
+    {
+        $perm = false;
+        switch ($this->solver_type) {
+            case 'autor':
+                if ($this->solver_id === $user->id) {
+                    return true;
+                }
+                break;
+
+            case 'group':
+                $group = \Statusgruppen::find($this->solver_id);
+                if (isset($group) && $group->isMember($user->id)) {
+                    return true;
+                }
+                break;
+        }
+
+        return $this->getStructuralElement()->hasEditingPermission($user);
+    }
+
+    /**
+     * @param \User|\Seminar_User $user
+     */
+    public function userIsASolver($user): bool
+    {
+        switch ($this->solver_type) {
+            case 'autor':
+                return $this->solver_id === $user->id;
+
+            case 'group':
+                $group = $this->getSolver();
+
+                return $group->isMember($user->id);
+        }
+
+        return false;
+    }
+
+    /**
+     * @return \User|\Statusgruppen|null the solver
+     */
+    public function getSolver()
+    {
+        switch ($this->solver_type) {
+            case 'autor':
+                return \User::find($this->solver_id);
+
+            case 'group':
+                return \Statusgruppen::find($this->solver_id);
+        }
+
+        return null;
+    }
+
+    public function getTaskProgress(): float
+    {
+        $children = $this->structural_element->findDescendants();
+
+        $element_counter = 1;
+        $progress = $this->getStructuralElementProgress($this->structural_element);
+        foreach ($children as $child) {
+            ++$element_counter;
+            $progress = ($this->getStructuralElementProgress($child) + $progress) / $element_counter;
+        }
+
+        return $progress * 100;
+    }
+
+    private function getStructuralElementProgress(StructuralElement $structural_element): float
+    {
+        $containers = Container::findBySQL('structural_element_id = ?', [intval($structural_element->id)]);
+        $counter = 0;
+        $progress = 0;
+        $b = [];
+        foreach ($containers as $container) {
+            $blockCount = $container->countBlocks();
+
+            $counter += $blockCount;
+            if ($blockCount > 0) {
+                $blocks = Block::findBySQL('container_id = ?', [$container->id]);
+                foreach ($blocks as $block) {
+                    $b[] = $block->id;
+                    if ($this->task_group->lecturer_id === $block->owner_id && $block->owner_id !== $block->editor_id) {
+                        ++$progress;
+                    }
+                }
+            }
+        }
+        if ($counter > 0) {
+            return $progress / $counter;
+        }
+
+        return 0;
+    }
+}
diff --git a/lib/models/Courseware/TaskFeedback.php b/lib/models/Courseware/TaskFeedback.php
new file mode 100755
index 00000000000..57e2ce07053
--- /dev/null
+++ b/lib/models/Courseware/TaskFeedback.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Courseware;
+
+use User;
+
+/**
+* Courseware's task feedback.
+*
+* @author  Ron Lucke <lucke@elan-ev.de>
+* @license GPL2 or any later version
+*
+* @since   Stud.IP 5.1
+*
+* @property int                            $id                     database column
+* @property int                            $task_id                database column
+* @property string                         $lecturer_id            database column
+* @property string                         $content                database column
+* @property int                            $mkdate                 database column
+* @property int                            $chdate                 database column
+
+* @property \User                          $lecturer               belongs_to User
+* @property \Courseware\Task               $task                   belongs_to Courseware\Task
+*/
+class TaskFeedback extends \SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'cw_task_feedbacks';
+
+        $config['belongs_to']['lecturer'] = [
+            'class_name' => User::class,
+            'foreign_key' => 'lecturer_id',
+        ];
+
+        $config['belongs_to']['task'] = [
+            'class_name' => Task::class,
+            'foreign_key' => 'task_id',
+        ];
+
+        parent::configure($config);
+    }
+
+    public function getStructuralElement(): ?StructuralElement
+    {
+        $sql = 'SELECT se.*
+                FROM cw_task_feedbacks tf
+                JOIN cw_tasks t ON t.id = tf.task_id
+                JOIN cw_structural_elements se ON se.id = t.structural_element_id
+                WHERE  tf.id = ?';
+        $structuralElement = \DBManager::get()->fetchOne($sql, [$this->getId()]);
+        if (!count($structuralElement)) {
+            return null;
+        }
+
+        return StructuralElement::build($structuralElement, false);
+    }
+}
diff --git a/lib/models/Courseware/TaskGroup.php b/lib/models/Courseware/TaskGroup.php
new file mode 100644
index 00000000000..7ca6eb1fa12
--- /dev/null
+++ b/lib/models/Courseware/TaskGroup.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Courseware;
+
+use User;
+
+/**
+ * Courseware's tasks.
+ *
+ * @author  Ron Lucke <lucke@elan-ev.de>
+ * @license GPL2 or any later version
+ *
+ * @since   Stud.IP 5.1
+ *
+ * @property int                           $id                    database column
+ * @property string                        $seminar_id            database column
+ * @property string                        $lecturer_id           database column
+ * @property int                           $structural_element_id database column
+ * @property int                           $solver_may_add_blocks database column
+ * @property string                        $title                 database column
+ * @property int                           $mkdate                database column
+ * @property int                           $chdate                database column
+ * @property \User                         $lecturer              belongs_to User
+ * @property \Course                       $course                belongs_to Course
+ * @property \Courseware\StructuralElement $structural_element    belongs_to Courseware\StructuralElement
+ * @property \SimpleORMapCollection        $tasks                 has_many Courseware\Task
+ */
+class TaskGroup extends \SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'cw_task_groups';
+
+        $config['belongs_to']['lecturer'] = [
+            'class_name' => User::class,
+            'foreign_key' => 'lecturer_id',
+        ];
+
+        $config['belongs_to']['course'] = [
+            'class_name' => \Course::class,
+            'foreign_key' => 'seminar_id',
+        ];
+
+        $config['has_many']['tasks'] = [
+            'class_name' => Task::class,
+            'assoc_foreign_key' => 'task_group_id',
+            'on_delete' => 'delete',
+            'on_store' => 'store',
+            'order_by' => 'ORDER BY mkdate',
+        ];
+
+        parent::configure($config);
+    }
+
+    public function getSolvers(): iterable
+    {
+        $solvers = $this->tasks->pluck('solver');
+
+        return $solvers;
+    }
+}
diff --git a/lib/models/Courseware/Template.php b/lib/models/Courseware/Template.php
new file mode 100755
index 00000000000..8161d989ee9
--- /dev/null
+++ b/lib/models/Courseware/Template.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Courseware;
+
+/**
+* Courseware's template.
+*
+* @author  Ron Lucke <lucke@elan-ev.de>
+* @license GPL2 or any later version
+*
+* @since   Stud.IP 5.1
+*
+* @property int                            $id                     database column
+* @property string                         $name                   database column
+* @property string                         $purpose                database column
+* @property string                         $structure              database column
+* @property int                            $mkdate                 database column
+* @property int                            $chdate                 database column
+*/
+class Template extends \SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'cw_templates';
+
+        parent::configure($config);
+    }
+}
\ No newline at end of file
diff --git a/lib/navigation/AdminNavigation.php b/lib/navigation/AdminNavigation.php
index 715f95fa777..915ceb3b48a 100644
--- a/lib/navigation/AdminNavigation.php
+++ b/lib/navigation/AdminNavigation.php
@@ -132,6 +132,13 @@ class AdminNavigation extends Navigation
             if (Config::get()->BANNER_ADS_ENABLE) {
                 $navigation->addSubNavigation('banner', new Navigation(_('Werbebanner'), 'dispatch.php/admin/banner'));
             }
+            $navigation->addSubNavigation(
+                'courseware',
+                new Navigation(
+                    _('Courseware'),
+                    'dispatch.php/admin/courseware/index'
+                )
+            );
             if (Config::get()->OERCAMPUS_ENABLED) {
                 $navigation->addSubNavigation(
                     'oer',
diff --git a/lib/navigation/ContentsNavigation.php b/lib/navigation/ContentsNavigation.php
index 0487ede97e8..767b86eed89 100755
--- a/lib/navigation/ContentsNavigation.php
+++ b/lib/navigation/ContentsNavigation.php
@@ -48,7 +48,7 @@ class ContentsNavigation extends Navigation
         $courseware->setImage(Icon::create('courseware'));
 
         $courseware->addSubNavigation(
-            'projects',
+            'overview',
             new Navigation(_('Ãœbersicht'), 'dispatch.php/contents/courseware/index')
         );
         $courseware->addSubNavigation(
diff --git a/package.json b/package.json
index 683e2c1039f..ea983208adc 100644
--- a/package.json
+++ b/package.json
@@ -88,6 +88,7 @@
         "vue-template-compiler": "^2.6.12",
         "vue-twentytwenty": "^0.10.1",
         "vue-typer": "^1.2.0",
+        "vuedraggable": "^2.24.3",
         "vuex": "^3.6.2",
         "webpack": "5.6.0",
         "webpack-cli": "4.2.0",
diff --git a/public/assets/images/icons/black/bullet-arrow.svg b/public/assets/images/icons/black/bullet-arrow.svg
new file mode 100644
index 00000000000..749a45eccc6
--- /dev/null
+++ b/public/assets/images/icons/black/bullet-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#00000" d="M19 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/black/bullet-dot.svg b/public/assets/images/icons/black/bullet-dot.svg
new file mode 100644
index 00000000000..846e6e49a53
--- /dev/null
+++ b/public/assets/images/icons/black/bullet-dot.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><circle cx="27" cy="27" r="9" fill="#00000"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/black/bullet-double-arrow.svg b/public/assets/images/icons/black/bullet-double-arrow.svg
new file mode 100644
index 00000000000..37f2642bbde
--- /dev/null
+++ b/public/assets/images/icons/black/bullet-double-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00000"><path d="M12.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/><path d="M25.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/black/bullet-line.svg b/public/assets/images/icons/black/bullet-line.svg
new file mode 100644
index 00000000000..e8c0e8311a5
--- /dev/null
+++ b/public/assets/images/icons/black/bullet-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#00000" d="M15 23h24v8H15z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/black/category-draft.svg b/public/assets/images/icons/black/category-draft.svg
new file mode 100644
index 00000000000..9841a6101eb
--- /dev/null
+++ b/public/assets/images/icons/black/category-draft.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00000"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm26.2-14.5c-1.7-.9-3.8-.4-4.8 1.2l-.6 1-.9 1.5L21 31.4l.1 6.6 6-3.2L35 21.5l.9-1.5.6-1c1-1.5.4-3.6-1.3-4.5zm-3.8 3 .6-1c.5-.8 1.6-1.1 2.4-.6.8.5 1.1 1.5.6 2.3l-.6 1-3-1.7z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/black/category-others.svg b/public/assets/images/icons/black/category-others.svg
new file mode 100644
index 00000000000..cba7cd90eb5
--- /dev/null
+++ b/public/assets/images/icons/black/category-others.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00000"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="M28 18c4.4 0 8 3.6 8 8s-3.6 8-8 8-8-3.6-8-8 3.6-8 8-8m0-3c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-5 11-11-4.9-11-11-11z" fill="#00000"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/black/category-portfolio.svg b/public/assets/images/icons/black/category-portfolio.svg
new file mode 100644
index 00000000000..3c60df21244
--- /dev/null
+++ b/public/assets/images/icons/black/category-portfolio.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00000"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm19-5.5c2.9 0 5.3-2.4 5.3-5.3s-2.4-5.3-5.3-5.3-5.3 2.4-5.3 5.3c0 3 2.4 5.3 5.3 5.3zm4.6 2.4h-9.1c-2 0-3.6 1.6-3.6 3.6V37h16.4v-7.5c-.1-2-1.7-3.6-3.7-3.6z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/black/category-task.svg b/public/assets/images/icons/black/category-task.svg
new file mode 100644
index 00000000000..e8dd23601d3
--- /dev/null
+++ b/public/assets/images/icons/black/category-task.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00000"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm31.1-8-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/black/category-template.svg b/public/assets/images/icons/black/category-template.svg
new file mode 100644
index 00000000000..fdba7ab5aa7
--- /dev/null
+++ b/public/assets/images/icons/black/category-template.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00000"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="m40.1 21-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z" fill="#00000"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/black/content2.svg b/public/assets/images/icons/black/content2.svg
new file mode 100644
index 00000000000..abb38b21776
--- /dev/null
+++ b/public/assets/images/icons/black/content2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00000"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3z"/><path d="M32 33V22H21v-4h15v15z"/><path d="M34 20v11-11m4-4H19v8h11v11h8V16zM5 31h18v18H5z"/><path d="M21 33v14H7V33h14m4-4H3v22h22V29z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/blue/bullet-arrow.svg b/public/assets/images/icons/blue/bullet-arrow.svg
new file mode 100644
index 00000000000..474d6cf5cc4
--- /dev/null
+++ b/public/assets/images/icons/blue/bullet-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#28497C" d="M19 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/blue/bullet-dot.svg b/public/assets/images/icons/blue/bullet-dot.svg
new file mode 100644
index 00000000000..a9874016463
--- /dev/null
+++ b/public/assets/images/icons/blue/bullet-dot.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><circle cx="27" cy="27" r="9" fill="#28497C"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/blue/bullet-double-arrow.svg b/public/assets/images/icons/blue/bullet-double-arrow.svg
new file mode 100644
index 00000000000..c686f81a796
--- /dev/null
+++ b/public/assets/images/icons/blue/bullet-double-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#28497C"><path d="M12.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/><path d="M25.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/blue/bullet-line.svg b/public/assets/images/icons/blue/bullet-line.svg
new file mode 100644
index 00000000000..fc53d9120d4
--- /dev/null
+++ b/public/assets/images/icons/blue/bullet-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#28497C" d="M15 23h24v8H15z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/blue/category-draft.svg b/public/assets/images/icons/blue/category-draft.svg
new file mode 100644
index 00000000000..4efaccdba9a
--- /dev/null
+++ b/public/assets/images/icons/blue/category-draft.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#28497C"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm26.2-14.5c-1.7-.9-3.8-.4-4.8 1.2l-.6 1-.9 1.5L21 31.4l.1 6.6 6-3.2L35 21.5l.9-1.5.6-1c1-1.5.4-3.6-1.3-4.5zm-3.8 3 .6-1c.5-.8 1.6-1.1 2.4-.6.8.5 1.1 1.5.6 2.3l-.6 1-3-1.7z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/blue/category-others.svg b/public/assets/images/icons/blue/category-others.svg
new file mode 100644
index 00000000000..04ec1b4e9e4
--- /dev/null
+++ b/public/assets/images/icons/blue/category-others.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#28497C"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="M28 18c4.4 0 8 3.6 8 8s-3.6 8-8 8-8-3.6-8-8 3.6-8 8-8m0-3c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-5 11-11-4.9-11-11-11z" fill="#28497C"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/blue/category-portfolio.svg b/public/assets/images/icons/blue/category-portfolio.svg
new file mode 100644
index 00000000000..9e4069895b8
--- /dev/null
+++ b/public/assets/images/icons/blue/category-portfolio.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#28497C"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm19-5.5c2.9 0 5.3-2.4 5.3-5.3s-2.4-5.3-5.3-5.3-5.3 2.4-5.3 5.3c0 3 2.4 5.3 5.3 5.3zm4.6 2.4h-9.1c-2 0-3.6 1.6-3.6 3.6V37h16.4v-7.5c-.1-2-1.7-3.6-3.7-3.6z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/blue/category-task.svg b/public/assets/images/icons/blue/category-task.svg
new file mode 100644
index 00000000000..639dd910f41
--- /dev/null
+++ b/public/assets/images/icons/blue/category-task.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#28497C"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm31.1-8-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/blue/category-template.svg b/public/assets/images/icons/blue/category-template.svg
new file mode 100644
index 00000000000..852b4c3b5cb
--- /dev/null
+++ b/public/assets/images/icons/blue/category-template.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#28497C"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="m40.1 21-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z" fill="#28497C"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/blue/content2.svg b/public/assets/images/icons/blue/content2.svg
index 9d8d7123c55..ce93472c6a4 100644
--- a/public/assets/images/icons/blue/content2.svg
+++ b/public/assets/images/icons/blue/content2.svg
@@ -1 +1 @@
-<svg width="16" height="16" id="icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54"><defs><style>.cls-1{fill:#28497c;}</style></defs><g id="content"><rect class="cls-1" x="3" y="29.9" width="21.07" height="21.07"/><polygon class="cls-1" points="36.95 33.41 36.95 17.02 20.56 17.02 20.56 25.21 28.75 25.21 28.75 33.41 36.95 33.41"/><polygon class="cls-1" points="6.51 2.97 6.51 25.21 13.54 25.21 13.54 9.99 43.98 9.99 43.98 40.43 28.75 40.43 28.75 47.46 51 47.46 51 2.97 6.51 2.97"/></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#28497C"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3z"/><path d="M32 33V22H21v-4h15v15z"/><path d="M34 20v11-11m4-4H19v8h11v11h8V16zM5 31h18v18H5z"/><path d="M21 33v14H7V33h14m4-4H3v22h22V29z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/green/bullet-arrow.svg b/public/assets/images/icons/green/bullet-arrow.svg
new file mode 100644
index 00000000000..e6845b6bdb9
--- /dev/null
+++ b/public/assets/images/icons/green/bullet-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#00962d" d="M19 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/green/bullet-dot.svg b/public/assets/images/icons/green/bullet-dot.svg
new file mode 100644
index 00000000000..e045370eed6
--- /dev/null
+++ b/public/assets/images/icons/green/bullet-dot.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><circle cx="27" cy="27" r="9" fill="#00962d"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/green/bullet-double-arrow.svg b/public/assets/images/icons/green/bullet-double-arrow.svg
new file mode 100644
index 00000000000..ea9fdb175f0
--- /dev/null
+++ b/public/assets/images/icons/green/bullet-double-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00962d"><path d="M12.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/><path d="M25.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/green/bullet-line.svg b/public/assets/images/icons/green/bullet-line.svg
new file mode 100644
index 00000000000..809690d8c49
--- /dev/null
+++ b/public/assets/images/icons/green/bullet-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#00962d" d="M15 23h24v8H15z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/green/category-draft.svg b/public/assets/images/icons/green/category-draft.svg
new file mode 100644
index 00000000000..df4399e46af
--- /dev/null
+++ b/public/assets/images/icons/green/category-draft.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00962d"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm26.2-14.5c-1.7-.9-3.8-.4-4.8 1.2l-.6 1-.9 1.5L21 31.4l.1 6.6 6-3.2L35 21.5l.9-1.5.6-1c1-1.5.4-3.6-1.3-4.5zm-3.8 3 .6-1c.5-.8 1.6-1.1 2.4-.6.8.5 1.1 1.5.6 2.3l-.6 1-3-1.7z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/green/category-others.svg b/public/assets/images/icons/green/category-others.svg
new file mode 100644
index 00000000000..6712133d18a
--- /dev/null
+++ b/public/assets/images/icons/green/category-others.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00962d"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="M28 18c4.4 0 8 3.6 8 8s-3.6 8-8 8-8-3.6-8-8 3.6-8 8-8m0-3c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-5 11-11-4.9-11-11-11z" fill="#00962d"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/green/category-portfolio.svg b/public/assets/images/icons/green/category-portfolio.svg
new file mode 100644
index 00000000000..a47558a419d
--- /dev/null
+++ b/public/assets/images/icons/green/category-portfolio.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00962d"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm19-5.5c2.9 0 5.3-2.4 5.3-5.3s-2.4-5.3-5.3-5.3-5.3 2.4-5.3 5.3c0 3 2.4 5.3 5.3 5.3zm4.6 2.4h-9.1c-2 0-3.6 1.6-3.6 3.6V37h16.4v-7.5c-.1-2-1.7-3.6-3.7-3.6z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/green/category-task.svg b/public/assets/images/icons/green/category-task.svg
new file mode 100644
index 00000000000..f6912c52440
--- /dev/null
+++ b/public/assets/images/icons/green/category-task.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00962d"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm31.1-8-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/green/category-template.svg b/public/assets/images/icons/green/category-template.svg
new file mode 100644
index 00000000000..74bb961c99b
--- /dev/null
+++ b/public/assets/images/icons/green/category-template.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00962d"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="m40.1 21-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z" fill="#00962d"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/green/content2.svg b/public/assets/images/icons/green/content2.svg
new file mode 100644
index 00000000000..39521421d37
--- /dev/null
+++ b/public/assets/images/icons/green/content2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#00962d"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3z"/><path d="M32 33V22H21v-4h15v15z"/><path d="M34 20v11-11m4-4H19v8h11v11h8V16zM5 31h18v18H5z"/><path d="M21 33v14H7V33h14m4-4H3v22h22V29z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/grey/bullet-arrow.svg b/public/assets/images/icons/grey/bullet-arrow.svg
new file mode 100644
index 00000000000..8fff0f178c8
--- /dev/null
+++ b/public/assets/images/icons/grey/bullet-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#6e6e6e" d="M19 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/grey/bullet-dot.svg b/public/assets/images/icons/grey/bullet-dot.svg
new file mode 100644
index 00000000000..01510e40b80
--- /dev/null
+++ b/public/assets/images/icons/grey/bullet-dot.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><circle cx="27" cy="27" r="9" fill="#6e6e6e"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/grey/bullet-double-arrow.svg b/public/assets/images/icons/grey/bullet-double-arrow.svg
new file mode 100644
index 00000000000..ceb9d9af651
--- /dev/null
+++ b/public/assets/images/icons/grey/bullet-double-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#6e6e6e"><path d="M12.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/><path d="M25.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/grey/bullet-line.svg b/public/assets/images/icons/grey/bullet-line.svg
new file mode 100644
index 00000000000..1f5af73db30
--- /dev/null
+++ b/public/assets/images/icons/grey/bullet-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#6e6e6e" d="M15 23h24v8H15z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/grey/category-draft.svg b/public/assets/images/icons/grey/category-draft.svg
new file mode 100644
index 00000000000..45b047475b3
--- /dev/null
+++ b/public/assets/images/icons/grey/category-draft.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#6e6e6e"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm26.2-14.5c-1.7-.9-3.8-.4-4.8 1.2l-.6 1-.9 1.5L21 31.4l.1 6.6 6-3.2L35 21.5l.9-1.5.6-1c1-1.5.4-3.6-1.3-4.5zm-3.8 3 .6-1c.5-.8 1.6-1.1 2.4-.6.8.5 1.1 1.5.6 2.3l-.6 1-3-1.7z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/grey/category-others.svg b/public/assets/images/icons/grey/category-others.svg
new file mode 100644
index 00000000000..06f88711b72
--- /dev/null
+++ b/public/assets/images/icons/grey/category-others.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#6e6e6e"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="M28 18c4.4 0 8 3.6 8 8s-3.6 8-8 8-8-3.6-8-8 3.6-8 8-8m0-3c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-5 11-11-4.9-11-11-11z" fill="#6e6e6e"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/grey/category-portfolio.svg b/public/assets/images/icons/grey/category-portfolio.svg
new file mode 100644
index 00000000000..ee7d3b81f82
--- /dev/null
+++ b/public/assets/images/icons/grey/category-portfolio.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#6e6e6e"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm19-5.5c2.9 0 5.3-2.4 5.3-5.3s-2.4-5.3-5.3-5.3-5.3 2.4-5.3 5.3c0 3 2.4 5.3 5.3 5.3zm4.6 2.4h-9.1c-2 0-3.6 1.6-3.6 3.6V37h16.4v-7.5c-.1-2-1.7-3.6-3.7-3.6z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/grey/category-task.svg b/public/assets/images/icons/grey/category-task.svg
new file mode 100644
index 00000000000..706598286a3
--- /dev/null
+++ b/public/assets/images/icons/grey/category-task.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#6e6e6e"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm31.1-8-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/grey/category-template.svg b/public/assets/images/icons/grey/category-template.svg
new file mode 100644
index 00000000000..9294d4a39d0
--- /dev/null
+++ b/public/assets/images/icons/grey/category-template.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#6e6e6e"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="m40.1 21-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z" fill="#6e6e6e"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/grey/content2.svg b/public/assets/images/icons/grey/content2.svg
new file mode 100644
index 00000000000..3521bf93473
--- /dev/null
+++ b/public/assets/images/icons/grey/content2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#6e6e6e"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3z"/><path d="M32 33V22H21v-4h15v15z"/><path d="M34 20v11-11m4-4H19v8h11v11h8V16zM5 31h18v18H5z"/><path d="M21 33v14H7V33h14m4-4H3v22h22V29z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/red/bullet-arrow.svg b/public/assets/images/icons/red/bullet-arrow.svg
new file mode 100644
index 00000000000..63b4f3c7f28
--- /dev/null
+++ b/public/assets/images/icons/red/bullet-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#cb1800" d="M19 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/red/bullet-dot.svg b/public/assets/images/icons/red/bullet-dot.svg
new file mode 100644
index 00000000000..4be587ba3bb
--- /dev/null
+++ b/public/assets/images/icons/red/bullet-dot.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><circle cx="27" cy="27" r="9" fill="#cb1800"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/red/bullet-double-arrow.svg b/public/assets/images/icons/red/bullet-double-arrow.svg
new file mode 100644
index 00000000000..5502413f297
--- /dev/null
+++ b/public/assets/images/icons/red/bullet-double-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#cb1800"><path d="M12.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/><path d="M25.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/red/bullet-line.svg b/public/assets/images/icons/red/bullet-line.svg
new file mode 100644
index 00000000000..9f2cc6b5da1
--- /dev/null
+++ b/public/assets/images/icons/red/bullet-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#cb1800" d="M15 23h24v8H15z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/red/category-draft.svg b/public/assets/images/icons/red/category-draft.svg
new file mode 100644
index 00000000000..9e3e0f474c5
--- /dev/null
+++ b/public/assets/images/icons/red/category-draft.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#cb1800"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm26.2-14.5c-1.7-.9-3.8-.4-4.8 1.2l-.6 1-.9 1.5L21 31.4l.1 6.6 6-3.2L35 21.5l.9-1.5.6-1c1-1.5.4-3.6-1.3-4.5zm-3.8 3 .6-1c.5-.8 1.6-1.1 2.4-.6.8.5 1.1 1.5.6 2.3l-.6 1-3-1.7z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/red/category-others.svg b/public/assets/images/icons/red/category-others.svg
new file mode 100644
index 00000000000..918d2eac5ed
--- /dev/null
+++ b/public/assets/images/icons/red/category-others.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#cb1800"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="M28 18c4.4 0 8 3.6 8 8s-3.6 8-8 8-8-3.6-8-8 3.6-8 8-8m0-3c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-5 11-11-4.9-11-11-11z" fill="#cb1800"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/red/category-portfolio.svg b/public/assets/images/icons/red/category-portfolio.svg
new file mode 100644
index 00000000000..c8e9adc3010
--- /dev/null
+++ b/public/assets/images/icons/red/category-portfolio.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#cb1800"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm19-5.5c2.9 0 5.3-2.4 5.3-5.3s-2.4-5.3-5.3-5.3-5.3 2.4-5.3 5.3c0 3 2.4 5.3 5.3 5.3zm4.6 2.4h-9.1c-2 0-3.6 1.6-3.6 3.6V37h16.4v-7.5c-.1-2-1.7-3.6-3.7-3.6z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/red/category-task.svg b/public/assets/images/icons/red/category-task.svg
new file mode 100644
index 00000000000..3a9bee92e99
--- /dev/null
+++ b/public/assets/images/icons/red/category-task.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#cb1800"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm31.1-8-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/red/category-template.svg b/public/assets/images/icons/red/category-template.svg
new file mode 100644
index 00000000000..e25390b462c
--- /dev/null
+++ b/public/assets/images/icons/red/category-template.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#cb1800"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="m40.1 21-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z" fill="#cb1800"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/red/content2.svg b/public/assets/images/icons/red/content2.svg
new file mode 100644
index 00000000000..0065efd12f8
--- /dev/null
+++ b/public/assets/images/icons/red/content2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#cb1800"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3z"/><path d="M32 33V22H21v-4h15v15z"/><path d="M34 20v11-11m4-4H19v8h11v11h8V16zM5 31h18v18H5z"/><path d="M21 33v14H7V33h14m4-4H3v22h22V29z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/white/bullet-arrow.svg b/public/assets/images/icons/white/bullet-arrow.svg
new file mode 100644
index 00000000000..cc3da9fc930
--- /dev/null
+++ b/public/assets/images/icons/white/bullet-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#fff" d="M19 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/white/bullet-dot.svg b/public/assets/images/icons/white/bullet-dot.svg
new file mode 100644
index 00000000000..0620a65f9bb
--- /dev/null
+++ b/public/assets/images/icons/white/bullet-dot.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><circle cx="27" cy="27" r="9" fill="#fff"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/white/bullet-double-arrow.svg b/public/assets/images/icons/white/bullet-double-arrow.svg
new file mode 100644
index 00000000000..67a3ae6ec4c
--- /dev/null
+++ b/public/assets/images/icons/white/bullet-double-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#fff"><path d="M12.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/><path d="M25.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/white/bullet-line.svg b/public/assets/images/icons/white/bullet-line.svg
new file mode 100644
index 00000000000..1bafde988d1
--- /dev/null
+++ b/public/assets/images/icons/white/bullet-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#fff" d="M15 23h24v8H15z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/white/category-draft.svg b/public/assets/images/icons/white/category-draft.svg
new file mode 100644
index 00000000000..d1cd91e4f06
--- /dev/null
+++ b/public/assets/images/icons/white/category-draft.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#fff"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm26.2-14.5c-1.7-.9-3.8-.4-4.8 1.2l-.6 1-.9 1.5L21 31.4l.1 6.6 6-3.2L35 21.5l.9-1.5.6-1c1-1.5.4-3.6-1.3-4.5zm-3.8 3 .6-1c.5-.8 1.6-1.1 2.4-.6.8.5 1.1 1.5.6 2.3l-.6 1-3-1.7z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/white/category-others.svg b/public/assets/images/icons/white/category-others.svg
new file mode 100644
index 00000000000..0b4cbb450bd
--- /dev/null
+++ b/public/assets/images/icons/white/category-others.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#fff"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="M28 18c4.4 0 8 3.6 8 8s-3.6 8-8 8-8-3.6-8-8 3.6-8 8-8m0-3c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-5 11-11-4.9-11-11-11z" fill="#fff"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/white/category-portfolio.svg b/public/assets/images/icons/white/category-portfolio.svg
new file mode 100644
index 00000000000..d7d8faf4fc9
--- /dev/null
+++ b/public/assets/images/icons/white/category-portfolio.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#fff"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm19-5.5c2.9 0 5.3-2.4 5.3-5.3s-2.4-5.3-5.3-5.3-5.3 2.4-5.3 5.3c0 3 2.4 5.3 5.3 5.3zm4.6 2.4h-9.1c-2 0-3.6 1.6-3.6 3.6V37h16.4v-7.5c-.1-2-1.7-3.6-3.7-3.6z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/white/category-task.svg b/public/assets/images/icons/white/category-task.svg
new file mode 100644
index 00000000000..534657edc11
--- /dev/null
+++ b/public/assets/images/icons/white/category-task.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#fff"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm31.1-8-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/white/category-template.svg b/public/assets/images/icons/white/category-template.svg
new file mode 100644
index 00000000000..b7b1d044f10
--- /dev/null
+++ b/public/assets/images/icons/white/category-template.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#fff"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="m40.1 21-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z" fill="#fff"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/white/content2.svg b/public/assets/images/icons/white/content2.svg
new file mode 100644
index 00000000000..bca119506cd
--- /dev/null
+++ b/public/assets/images/icons/white/content2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#fff"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3z"/><path d="M32 33V22H21v-4h15v15z"/><path d="M34 20v11-11m4-4H19v8h11v11h8V16zM5 31h18v18H5z"/><path d="M21 33v14H7V33h14m4-4H3v22h22V29z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/yellow/bullet-arrow.svg b/public/assets/images/icons/yellow/bullet-arrow.svg
new file mode 100644
index 00000000000..7af94239dcd
--- /dev/null
+++ b/public/assets/images/icons/yellow/bullet-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#ffad00" d="M19 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/yellow/bullet-dot.svg b/public/assets/images/icons/yellow/bullet-dot.svg
new file mode 100644
index 00000000000..644e1f89e0f
--- /dev/null
+++ b/public/assets/images/icons/yellow/bullet-dot.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><circle cx="27" cy="27" r="9" fill="#ffad00"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/yellow/bullet-double-arrow.svg b/public/assets/images/icons/yellow/bullet-double-arrow.svg
new file mode 100644
index 00000000000..44855a8f6ec
--- /dev/null
+++ b/public/assets/images/icons/yellow/bullet-double-arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#ffad00"><path d="M12.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/><path d="M25.5 11v8.1l7.9 7.9-7.9 7.9V43l16-16z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/yellow/bullet-line.svg b/public/assets/images/icons/yellow/bullet-line.svg
new file mode 100644
index 00000000000..40abc52968d
--- /dev/null
+++ b/public/assets/images/icons/yellow/bullet-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><path fill="#ffad00" d="M15 23h24v8H15z"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/yellow/category-draft.svg b/public/assets/images/icons/yellow/category-draft.svg
new file mode 100644
index 00000000000..e9ec7ee2359
--- /dev/null
+++ b/public/assets/images/icons/yellow/category-draft.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#ffad00"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm26.2-14.5c-1.7-.9-3.8-.4-4.8 1.2l-.6 1-.9 1.5L21 31.4l.1 6.6 6-3.2L35 21.5l.9-1.5.6-1c1-1.5.4-3.6-1.3-4.5zm-3.8 3 .6-1c.5-.8 1.6-1.1 2.4-.6.8.5 1.1 1.5.6 2.3l-.6 1-3-1.7z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/yellow/category-others.svg b/public/assets/images/icons/yellow/category-others.svg
new file mode 100644
index 00000000000..58fa3570510
--- /dev/null
+++ b/public/assets/images/icons/yellow/category-others.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#ffad00"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="M28 18c4.4 0 8 3.6 8 8s-3.6 8-8 8-8-3.6-8-8 3.6-8 8-8m0-3c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-5 11-11-4.9-11-11-11z" fill="#ffad00"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/yellow/category-portfolio.svg b/public/assets/images/icons/yellow/category-portfolio.svg
new file mode 100644
index 00000000000..bac2607f268
--- /dev/null
+++ b/public/assets/images/icons/yellow/category-portfolio.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#ffad00"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm19-5.5c2.9 0 5.3-2.4 5.3-5.3s-2.4-5.3-5.3-5.3-5.3 2.4-5.3 5.3c0 3 2.4 5.3 5.3 5.3zm4.6 2.4h-9.1c-2 0-3.6 1.6-3.6 3.6V37h16.4v-7.5c-.1-2-1.7-3.6-3.7-3.6z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/yellow/category-task.svg b/public/assets/images/icons/yellow/category-task.svg
new file mode 100644
index 00000000000..ddde9f909d0
--- /dev/null
+++ b/public/assets/images/icons/yellow/category-task.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#ffad00"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29zm31.1-8-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/yellow/category-template.svg b/public/assets/images/icons/yellow/category-template.svg
new file mode 100644
index 00000000000..9d5774ee1f2
--- /dev/null
+++ b/public/assets/images/icons/yellow/category-template.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#ffad00"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3zM5 49V31h3.2L23 45.8V49z"/><path d="M7.3 33 21 46.6v.4H7V33h.3M9 29H3v22h22v-6L9 29z"/></g><path d="m40.1 21-4.2-4.2L25.6 27l-5.4-5.3-4.2 4.2 9.6 9.6L40.1 21z" fill="#ffad00"/></svg>
\ No newline at end of file
diff --git a/public/assets/images/icons/yellow/content2.svg b/public/assets/images/icons/yellow/content2.svg
new file mode 100644
index 00000000000..3ec9dc681c6
--- /dev/null
+++ b/public/assets/images/icons/yellow/content2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54" xml:space="preserve"><g fill="#ffad00"><path d="M32 46v-2h15V7H10v15H8V5h41v41z"/><path d="M51 3H6v21h6V9h33v33H30v6h21V3z"/><path d="M32 33V22H21v-4h15v15z"/><path d="M34 20v11-11m4-4H19v8h11v11h8V16zM5 31h18v18H5z"/><path d="M21 33v14H7V33h14m4-4H3v22h22V29z"/></g></svg>
\ No newline at end of file
diff --git a/public/plugins_packages/core/ActivityFeed/ActivityFeed.php b/public/plugins_packages/core/ActivityFeed/ActivityFeed.php
index a966e95ccb6..f42b34b2731 100644
--- a/public/plugins_packages/core/ActivityFeed/ActivityFeed.php
+++ b/public/plugins_packages/core/ActivityFeed/ActivityFeed.php
@@ -115,7 +115,8 @@ class ActivityFeed extends StudIPPlugin implements PortalPlugin
             'wiki'         => _('Wiki'),
             'schedule'     => _('Ablaufplan'),
             'news'         => _('Ankündigungen'),
-            'blubber'      => _('Blubber')
+            'blubber'      => _('Blubber'),
+            'courseware'   => _('Courseware')
         ];
 
         $modules[\Context::INSTITUTE] = $modules[\Context::COURSE];
diff --git a/resources/assets/javascripts/bootstrap/courseware.js b/resources/assets/javascripts/bootstrap/courseware.js
index f9a85fe0700..2c2938d17d6 100755
--- a/resources/assets/javascripts/bootstrap/courseware.js
+++ b/resources/assets/javascripts/bootstrap/courseware.js
@@ -31,4 +31,37 @@ STUDIP.domReady(() => {
             });
         });
     }
+
+    if (document.getElementById('courseware-content-overview-app')) {
+        STUDIP.Vue.load().then(({ createApp }) => {
+            import(
+                /* webpackChunkName: "courseware-content-overview-app" */
+                '@/vue/courseware-content-overview-app.js'
+            ).then(({ default: mountApp }) => {
+                return mountApp(STUDIP, createApp, '#courseware-content-overview-app');
+            });
+        });
+    }
+
+    if (document.getElementById('courseware-content-bookmark-app')) {
+        STUDIP.Vue.load().then(({ createApp }) => {
+            import(
+                /* webpackChunkName: "courseware-content-bookmark-app" */
+                '@/vue/courseware-content-bookmark-app.js'
+            ).then(({ default: mountApp }) => {
+                return mountApp(STUDIP, createApp, '#courseware-content-bookmark-app');
+            });
+        });
+    }
+
+    if (document.getElementById('courseware-admin-app')) {
+        STUDIP.Vue.load().then(({ createApp }) => {
+            import(
+                /* webpackChunkName: "courseware-content-bookmark-app" */
+                '@/vue/courseware-admin-app.js'
+            ).then(({ default: mountApp }) => {
+                return mountApp(STUDIP, createApp, '#courseware-admin-app');
+            });
+        });
+    }
 });
diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss
index cd4c5c83bf3..54f44004568 100755
--- a/resources/assets/stylesheets/scss/courseware.scss
+++ b/resources/assets/stylesheets/scss/courseware.scss
@@ -10,6 +10,22 @@ $companion-types: (
     pointing: pointing-right
 );
 
+$element-icons: (
+    content: content2,
+    draft: category-draft,
+    task: category-task,
+    template: category-template,
+    oer: oer-campus,
+    other: category-others,
+    portfolio: category-portfolio
+);
+
+$tree-item-flag-icons: (
+    date: date,
+    write: edit,
+    cant-read: lock-locked2
+);
+
 $tile-colors: (
     black: #000,
     charcoal: #3c454e,
@@ -76,6 +92,10 @@ $media-buttons: (
 /* * * * * * * *
 c o n t e n t s
 * * * * * * * * */
+.cw-content-overview {
+    max-width: 1100px;
+}
+
 .cw-contents-overview-teaser {
     max-width: 782px;
     background-color: $content-color-20;
@@ -122,6 +142,56 @@ c o n t e n t s
 .cw-loading-indicator-content {
     margin-top: 76px;
 }
+.cw-content-loading {
+    /* Loading animation from activity feed */
+    .loading-indicator {
+        text-align: center;
+        padding: 1em 0;
+    }
+
+    .loading-indicator span {
+        background-color: #CCCCDD;
+        border-radius: 50%;
+        height: 10px;
+        position: relative;
+        width: 10px;
+        display: inline-block;
+    }
+
+    .loading-indicator span.load-1 {
+        animation: loading-animation-1 1s linear 20;
+    }
+
+    .loading-indicator span.load-2 {
+        animation: loading-animation-2 1s linear 20;
+    }
+
+    .loading-indicator span.load-3 {
+        animation: loading-animation-3 1s linear 20;
+    }
+    
+    @keyframes loading-animation-1 {
+      0%   { transform: scale(1); }
+      16%  { transform: scale(1.3); }
+      33%  { transform: scale(1); }
+      100% { transform: scale(1); }
+    }
+    
+    @keyframes loading-animation-2 {
+      0%   { transform: scale(1); }
+      33%  { transform: scale(1); }
+      49%  { transform: scale(1.3); }
+      65%  { transform: scale(1); }
+      100% { transform: scale(1); }
+    }
+    
+    @keyframes loading-animation-3 {
+      0%   { transform: scale(1); }
+      66%  { transform: scale(1); }
+      81%  { transform: scale(1.3); }
+      100% { transform: scale(1); }
+    }
+}
 
 /* * * * * * * * * * *
 c o n t e n t s  e n d
@@ -502,18 +572,29 @@ ribbon end
 
     }
 
+    .cw-structural-element-discussion {
+        max-width: 1606px;
+        width: 100%;
+        margin-bottom: 1em;
+    }
+
     .cw-container-wrapper {
-        max-width: 1115px;
+        max-width: 1095px;
         margin: 0;
         padding: 0;
         display: flex;
         flex-wrap: wrap;
         align-items: stretch;
+        justify-content: space-between;
 
         &.cw-container-wrapper-consume {
             margin: 0 auto;
             padding: 6em 1em 1em 1em;
         }
+
+        &.cw-container-wrapper-discuss {
+            max-width: 1606px;
+        }
     }
 
     .cw-structural-element-description {
@@ -647,7 +728,6 @@ ribbon end
     &.cw-container-colspan-half {
         max-width: 540px;
         width: 100%;
-        margin-right: 15px;
     }
     &.cw-container-colspan-half-center {
         width: 1095px;
@@ -734,6 +814,24 @@ form.cw-container-dialog-edit-form {
     }
 }
 
+.cw-container-wrapper-discuss {
+    flex-direction: column;
+
+    .cw-container-colspan-full {
+        max-width: unset;
+    }
+    .cw-container-colspan-half-center,
+    .cw-container-colspan-half {
+        max-width: 1050px;
+    }
+    .cw-container-colspan-half-center {
+        width: 100%;
+        .cw-container-content {
+            width: 1050px;
+        }
+    }
+}
+
 /* * * * * * *
  container end
 * * * * * * */
@@ -766,6 +864,19 @@ form.cw-container-dialog-edit-form {
         padding: 0.5em;
     }
 }
+.cw-container-wrapper-discuss {
+    .cw-container-colspan-full {
+        .cw-content-wrapper {
+            max-width: 1095px;
+        }
+    }
+    .cw-container-colspan-half,
+    .cw-container-colspan-half-center {
+        .cw-content-wrapper {
+            max-width: 540px;
+        }
+    }
+}
 .cw-block-header {
     background-color: $content-color-20;
     max-height: 30px;
@@ -788,6 +899,7 @@ form.cw-container-dialog-edit-form {
     }
 }
 
+.cw-discuss-wrapper,
 .cw-block-features {
 
     header{
@@ -802,6 +914,44 @@ form.cw-container-dialog-edit-form {
     }
 }
 
+.cw-discuss-wrapper {
+    flex-shrink: 3;
+    flex-grow: 2;
+    margin-left: 10px;
+}
+
+@media only screen and (max-width: 1820px) {
+    .cw-structural-element .cw-container-wrapper.cw-container-wrapper-discuss {
+        max-width: 1095px;
+        .cw-container.cw-container-list.cw-container-item.cw-container-colspan-full {
+            .cw-default-block {
+                flex-flow: column;
+                .cw-discuss-wrapper {
+                    margin-left: 0;
+                    margin-top: 8px;
+                }
+            }
+        }
+    }
+}
+
+@media only screen and (max-width: 1200px) {
+    .cw-structural-element .cw-container-wrapper.cw-container-wrapper-discuss {
+        max-width: 1095px;
+        .cw-container.cw-container-list.cw-container-item.cw-container-colspan-half,
+        .cw-container.cw-container-list.cw-container-item.cw-container-colspan-half-center {
+            .cw-default-block {
+                flex-flow: column;
+                .cw-discuss-wrapper {
+                    margin-left: 0;
+                    margin-top: 8px;
+                    max-width: 540px;
+                }
+            }
+        }
+    }
+}
+
 .cw-button-feature-close {
     float: right;
     border: none;
@@ -962,6 +1112,66 @@ label[for="cw-keypoint-color"] {
  block end
 * * * * * */
 
+/* * * * * * * *
+ sortable handle
+ * * * * * * * */
+.cw-container-list-sort-mode {
+    .block-ghost {
+        opacity: 0.6;
+    }
+    &.cw-container-list-sort-mode-empty {
+        min-height: 4em;
+        border: dashed thin $content-color-40;
+    }
+}
+.cw-structural-element-list-sort-mode {
+    list-style: none;
+    padding-left: 0;
+
+    .cw-container-item-sortable {
+        border: solid thin $content-color-40;
+        background-color: $content-color-20;
+        color: $base-color;
+        font-weight: 700;
+        margin-bottom: 0.5em;
+        padding: 0.5em;
+    }
+    .container-ghost {
+        opacity: 0.6;
+    }
+}
+.cw-structural-element-list-sort-mode,
+.cw-container-list-sort-mode {
+    .cw-sortable-handle {
+        display: inline-block;
+        cursor: grab;
+        background-image: url("#{$image-path}/anfasser_24.png");
+        background-repeat: no-repeat;
+        width: 7px;
+        height: 24px;
+        padding-right: 4px;
+        vertical-align: middle;
+    }
+    .cw-content-wrapper-active:hover {
+        border: solid thin $base-color;
+    }
+}
+
+.cw-container-item-sortable.sortable-chosen {
+        .cw-sortable-handle {
+            cursor: grabbing;
+        }
+}
+
+.cw-container-sort-buttons {
+    display: block;
+}
+
+
+/* * * * * * * * * * *
+ sortable handle end
+ * * * * * * * * * * */
+
 /* * * * *
  t r e e
  * * * * */
@@ -972,6 +1182,7 @@ label[for="cw-keypoint-color"] {
         padding-left: 1.25em;
         margin-bottom: 20px;
 
+        &.cw-tree-subchapter-list,
         &.cw-tree-chapter-list,
         &.cw-tree-root-list {
             padding-left: 0;
@@ -985,11 +1196,11 @@ label[for="cw-keypoint-color"] {
                 padding-left: 3px;
             }
         }
-        .cw-tree-item-is-root{
+        .cw-tree-item-is-root {
             display: block;
             font-size: 18px;
             .cw-tree-item-link {
-                padding-left: 24px;
+                padding-left: 26px;
 
                 @include background-icon(courseware, clickable, 18);
                 background-repeat: no-repeat;
@@ -1007,26 +1218,48 @@ label[for="cw-keypoint-color"] {
             &:hover {
                 background-color: $content-color-20;
             }
+            .cw-tree-item-link,
+            .cw-tree-item-link:hover,
+            .cw-tree-item-link.cw-tree-item-link-current {
+                background-image: none;
+            }
+
+            @each $type, $icon in $element-icons {
+                &.cw-tree-item-#{$type} .cw-tree-item-link {
+                    background-repeat: no-repeat;
+                    background-position: 3px 3px;
+                    padding-left: 26px;
+                    @include background-icon(#{$icon}, clickable, 18);
+                    &:hover {
+                        @include background-icon(#{$icon}, attention, 18);
+                    }
+                    &.cw-tree-item-link-current {
+                        @include background-icon(#{$icon}, info, 18);
+                    }
+                }
+            }
         }
 
         .cw-tree-item-link {
             display: inline-block;
             width: calc(100% - 14px);
             text-align: justify;
+            background-repeat: no-repeat;
+            padding-left: 20px;
+            background-position: 4px 1px;
+
+            @include background-icon(bullet-dot, clickable, 18);
+            &:hover {
+                @include background-icon(bullet-dot, attention, 18);
+            }
+            &.cw-tree-item-link-current {
+                @include background-icon(bullet-dot, info, 18);
+            }
 
             &:hover {
                 background-color: $light-gray-color-20;
                 color: $active-color;
             }
-            &::before {
-                content: '\2022';
-                color: $base-color;
-                font-weight: 700;
-                width: 1em;
-                margin-left: -1em;
-                margin-right: 4px;
-                vertical-align: top;
-            }
 
             &.cw-tree-item-link-current {
                 color: $black;
@@ -1035,17 +1268,35 @@ label[for="cw-keypoint-color"] {
                     color: $black;
                 }
             }
+            @each $type, $icon in $tree-item-flag-icons {
+                .cw-tree-item-flag-#{$type} {
+                    display: inline-block;
+                    width: 16px;
+                    height: 16px;
+                    vertical-align: top;
+                    @include background-icon(#{$icon}, clickable, 16);
+                }
+                &:hover .cw-tree-item-flag-#{$type} {
+                    @include background-icon(#{$icon}, attention, 16);
+                }
+                &.cw-tree-item-link-current .cw-tree-item-flag-#{$type} {
+                    @include background-icon(#{$icon}, info, 16);
+                }
+            }
         }
-
-    }
-
-    .cw-tree-item-first-level,
-    .cw-tree-item-is-root {
-        .cw-tree-item-link::before{
-            content: '';
-            width: 0;
-            margin: 0;
+        @each $type, $icon in $element-icons {
+            .cw-tree-item-#{$type} .cw-tree-item-link {
+                background-position: 0px 2px;
+                @include background-icon(#{$icon}, clickable, 16);
+                &:hover {
+                    @include background-icon(#{$icon}, attention, 16);
+                }
+                &.cw-tree-item-link-current {
+                    @include background-icon(#{$icon}, info, 16);
+                }
+            }
         }
+
     }
 
     .cw-tree-item {
@@ -1109,6 +1360,13 @@ c o l l a p s i b l e   b o x
     }
 }
 
+form .cw-collapsible .cw-collapsible-content.cw-collapsible-content-open {
+    padding: unset;
+    label {
+        margin: 1.5ex;
+    }
+}
+
 /* * * * * * * * * * * * * * * * * *
 c o l l a p s i b l e  b o x  e n d
 * * * * * * * * * * * * * * * * * */
@@ -1612,6 +1870,14 @@ c o m p a n i o n  o v e r l a y
             background-image: url("#{$image-path}/companion/Tin_#{$image}.svg");
         }
     }
+
+    &.cw-companion-box-in-form {
+        margin-top: 8px;
+    }
+
+    p {
+        margin: 0 1em 10px 0;
+    }
 }
 
 .cw-container-wrapper {
@@ -1650,6 +1916,9 @@ v i e w  w i d g e t
     .cw-action-widget-edit{
         @include background-icon(edit, clickable);
     }
+    .cw-action-widget-sort{
+        @include background-icon(arr_1sort, clickable);
+    }
     .cw-action-widget-add{
         @include background-icon(add, clickable);
     }
@@ -1665,6 +1934,9 @@ v i e w  w i d g e t
     .cw-action-widget-export{
         @include background-icon(export, clickable);
     }
+    .cw-action-widget-export-pdf{
+        @include background-icon(file-pdf, clickable);
+    }
     .cw-action-widget-oer{
         @include background-icon(oer-campus, clickable);
     }
@@ -1678,20 +1950,28 @@ v i e w  w i d g e t  e n d
 c o m m e n t s  &  f e e d b a c k
 * * * * * * * * * * * * * * * * * * */
 
+.cw-structural-element-feedback,
+.cw-structural-element-comments {
+    padding: 0 1em;
+}
+
+.cw-structural-element-feedback-items,
+.cw-structural-element-comments-items,
 .cw-block-feedback-items,
 .cw-block-comments-items {
     min-height: 1em;
     max-height: 225px;
     overflow-y: auto;
     overflow-x: hidden;
-    margin: -1em -1em 0em -0.5em;
+    margin: -1em -1em 0em 0em;
+    padding: 0em  1em 0em 0em;
     scroll-behavior: smooth;
 }
 
 .cw-talk-bubble {
     margin: 10px 20px;
     position: relative;
-    width: 80%;
+    width: 85%;
     height: auto;
     background-color: $dark-gray-color-10;
     border-radius: 5px;
@@ -1753,6 +2033,8 @@ c o m m e n t s  &  f e e d b a c k
     }
 }
 
+.cw-structural-element-feedback-create,
+.cw-structural-element-comment-create,
 .cw-block-feedback-create,
 .cw-block-comment-create {
     border-top: solid thin $content-color-40;
@@ -1760,6 +2042,21 @@ c o m m e n t s  &  f e e d b a c k
     textarea {
         width: calc(100% - 6px);
         resize: none;
+        border: solid thin $content-color-40;
+        &:active {
+            border: solid thin $content-color-80;
+        }
+    }
+}
+.cw-structural-element-comments-empty,
+.cw-structural-element-feedback-empty,
+.cw-block-comments-empty,
+.cw-block-feedback-empty {
+    .cw-structural-element-feedback-create,
+    .cw-structural-element-comment-create,
+    .cw-block-feedback-create,
+    .cw-block-comment-create {
+        border-top: none;
     }
 }
 
@@ -1893,8 +2190,7 @@ d a s h b o a r d
 
 .cw-dashboard {
     display: flex;
-    // TODO: Fixed width?
-    width: 1112px;
+    max-width: 1112px;
     flex-wrap: wrap;
 
     .cw-dashboard-box {
@@ -1902,24 +2198,28 @@ d a s h b o a r d
         margin-right: 1em;
 
         &.cw-dashboard-box-full {
-            // TODO: Fixed width?
-            width: 1095px;
+            max-width: 1095px;
+            width: calc(100% - 16px);
         }
         &.cw-dashboard-box-half {
-            // TODO: Fixed width?
-            width: 540px;
+            width: calc(50% - 16px);
+        }
+
+        &.cw-collapsible .cw-collapsible-content.cw-collapsible-content-open {
+            padding: 0;
         }
     }
     .cw-dashboard-overview {
         display: flex;
-        justify-content: space-evenly;
-        .cw-oblong {
-            margin-right: 1em;
-        }
+        padding: 10px;
+        flex-wrap: wrap;
+        justify-content: center;
+
     }
     .cw-dashboard-progress {
 
         .cw-dashboard-progress-breadcrumb {
+            padding: 10px;
             span {
                 color: $base-color;
                 cursor: pointer;
@@ -1932,6 +2232,7 @@ d a s h b o a r d
 
         .cw-dashboard-progress-chapter {
             text-align: center;
+            margin-bottom: -3.5em;
 
             h1 {
                 border: none;
@@ -1945,28 +2246,64 @@ d a s h b o a r d
 
                 &.cw-dashboard-progress-current {
                     font-size: 12px;
-                    margin: -4.5em 0 2em 260px;
+                    top: -4.5em;
+                    left: -2.5em;
                 }
             }
         }
 
         .cw-dashboard-progress-subchapter-list {
             border-top: solid thin $content-color-40;
-            margin: -0.5em;
-            height: 300px;
+            height: 349px;
             overflow-y: scroll;
             overflow-x: hidden;
-            padding: 1em;
+            padding: 0 1em 0 1em;
             scrollbar-width: thin;
             scrollbar-color: $base-color $dark-gray-color-5;
+
+            .cw-dashboard-empty-info {
+                margin-top: 10px;
+            }
+        }
+    }
+    &.cw-dashboard-task-view {
+        display: unset;
+        max-width: unset;
+        flex-wrap: unset;
+    }
+    &.cw-dashboard-activity-view {
+        .cw-dashboard-activities {
+            max-height: 760px;
+        }
+
+    }
+}
+
+#course-courseware-dashboard {
+    .action-menu-item a {
+        cursor: pointer;
+    }
+}
+
+.responsive-display {
+    .cw-dashboard {
+        .cw-dashboard-box {
+    
+            &.cw-dashboard-box-full {
+                width: 100%
+            }
+            &.cw-dashboard-box-half {
+                width: 100%
+            }
         }
     }
 }
 
 .cw-dashboard-progress-item {
     border-bottom: solid thin $content-color-40;
-    width: 492px;
+    width: 100%;
     cursor: pointer;
+    padding: 8px 0 8px 0;
 
     &:hover{
         background-color: hsla(217,6%,45%,.2);
@@ -1979,9 +2316,7 @@ d a s h b o a r d
     .cw-dashboard-progress-item-value,
     .cw-dashboard-progress-item-description {
         display: inline-block;
-        height: 70px;
         vertical-align: top;
-        line-height: 70px;
     }
 
     .cw-dashboard-progress-item-value {
@@ -1995,46 +2330,95 @@ d a s h b o a r d
         }
     }
     .cw-dashboard-progress-item-description {
-        width: 404px;
+        width: calc(100% - 90px);
         color: $base-color;
         padding-left: 14px;
         text-overflow: ellipsis;
         overflow: hidden;
         white-space: nowrap;
+        padding: 0.5em 0 0 1em;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
     }
 }
+.cw-dashboard-activities-wrapper {
+    .cw-companion-box {
+        margin: 10px;
+    }
 
-.cw-dashboard-activities {
-    max-height: 476px;
-    list-style: none;
-    padding: 0;
-    margin: -0.5em;
-    scrollbar-width: thin;
-    scrollbar-color:$base-color #f5f5f5;
-    overflow-y: auto;
-    overflow-x: hidden;
-
-    .cw-activity-item {
-        border-bottom: solid thin $content-color-40;
-        padding: 0.5em;
+    .cw-dashboard-activities {
+        max-height: 525px;
+        list-style: none;
+        padding: 0;
+        scrollbar-width: thin;
+        scrollbar-color:$base-color #f5f5f5;
+        overflow-y: auto;
+        overflow-x: hidden;
 
-        &:last-child {
-            border: none;
-        }
+        .cw-activity-item {
+            border-bottom: solid thin $content-color-40;
+            padding: 0.5em;
 
-        p {
-            margin: 0 0 4px 0;
-            img {
-                padding-right: 0.5em;
-                vertical-align: text-bottom;
+            &:last-child {
+                border: none;
             }
-            &.cw-activity-item-text {
-                padding-left: 23px;
+
+            p {
+                margin: 0 0 4px 0;
+                img {
+                    padding-right: 0.5em;
+                    vertical-align: text-bottom;
+                }
+                &.cw-activity-item-text {
+                    padding-left: 23px;
+                }
             }
         }
 
-        a{
+    }
+}
+
+.cw-dashboard-box {
+    .cw-dashboard-tasks-wrapper,
+    .cw-dashboard-students-wrapper {
+        padding: 10px;
+    }
+}
+
+.cw-dashboard-tasks-wrapper,
+.cw-dashboard-students-wrapper {
+    overflow-x: auto;
+    scrollbar-width: thin;
+    scrollbar-color:$base-color #f5f5f5;
+    max-height: 280px;
 
+    table.default {
+        margin: 0;
+        thead {
+            tr {
+                th {
+                    &.feedback {
+                        min-width: 11em;
+                    }
+                    &.renewal {
+                        min-width: 14em;
+                    }
+                }
+            }
+        }
+        tbody {
+            tr {
+                td {
+                    img {
+                        vertical-align: text-bottom;
+                        &.display-feedback,
+                        &.edit {
+                            cursor: pointer;
+                        }
+                    }
+                }
+            }
         }
     }
 }
@@ -2048,7 +2432,7 @@ o b l o n g
 * * * * * */
 
 .cw-oblong-large {
-    border: solid thin $base-color;
+    border: solid thin $content-color-40;
     width: 520px;
 
     .cw-oblong-value,
@@ -2061,25 +2445,29 @@ o b l o n g
     }
 
     .cw-oblong-value {
-        width: 90px;
-        color: $base-color;
+        width: 89px;
+        color: $black;
+        background-color: $content-color-20;
+        border-right: solid thin $content-color-40;
         font-size: xx-large;
     }
     .cw-oblong-description {
         width: 426px;
-        background-color: $base-color;
-        color: $white;
+        color: $black;
+        font-size: large;
         img {
             vertical-align: middle;
-            margin-right: 4px;
+            margin-right: 10px;
         }
 
     }
 }
 
 .cw-oblong-small {
-    border: solid thin $base-color;
-    width: 271px;
+    border: solid thin $content-color-40;
+    width: 340px;
+    margin-right: 1em;
+    margin-bottom: 5px;
 
     .cw-oblong-value,
     .cw-oblong-description {
@@ -2091,15 +2479,17 @@ o b l o n g
     }
 
     .cw-oblong-value {
-        width: 60px;
-        background-color: $base-color;
-        color: $white;
+        width: 59px;
+        background-color: $content-color-20;
+        border-right: solid thin $content-color-40;
+        color: $black;
         font-size: x-large;
     }
     .cw-oblong-description {
         width: calc(100% - 64px);
         background-color: $white;
-        color: $base-color;
+        color: $black;
+        overflow: hidden;
         img {
             vertical-align: middle;
             margin-right: 8px;
@@ -2321,9 +2711,7 @@ m a n a g e r
         }
 
     }
-    .cw-manager-element-subchapters {
 
-    }
     .cw-manager-element-item {
         border: solid thin $content-color-40;
         padding: 1em;
@@ -2367,8 +2755,8 @@ m a n a g e r
 
         &.cw-manager-filing-active {
             @include background-icon(arr_eol-down, info-alt, 24);
-            background-color: $activity-color;
-            border: solid thin $activity-color;
+            background-color: $base-color;
+            border: solid thin $base-color;
             color: $white;
         }
         &.cw-manager-filing-disabled {
@@ -3694,6 +4082,7 @@ headline block
         overflow: hidden;
         background-position: center;
         background-size: 1095px;
+        background-repeat: no-repeat;
 
         &.half {
             min-height: 300px;
@@ -3861,8 +4250,8 @@ headline block
         }
     }
 }
-
-.cw-container-colspan-half {
+.cw-container-colspan-half,
+.cw-container-colspan-half-center {
     .cw-block-headline {
         .cw-block-headline-content {
             min-height: 300px;
@@ -3997,6 +4386,23 @@ headline block
     }
 }
 
+.responsive-display {
+    .cw-block-headline {
+        .cw-block-headline-content {
+            background-size: 100%;
+            &.bigicon_before {
+                .icon-layer {
+                    background-size: 144px;
+                }
+                .cw-block-headline-textbox .cw-block-headline-title h1 {
+                    font-size: 4em;
+                    margin-left: 225px;
+                }
+            }
+        }
+    }
+}
+
 /*
 headline block end
 */
@@ -4004,6 +4410,12 @@ headline block end
 /*
 toc block
 */
+.cw-block-table-of-contents {
+    .cw-block-content {
+        overflow: unset;
+    }
+}
+
 .cw-block-table-of-contents-list {
     padding: 0;
     list-style: none;
@@ -4124,6 +4536,7 @@ cw tiles
             }
         };
     }
+
     .preview-image {
         height: 180px;
         width: 100%;
@@ -4135,14 +4548,16 @@ cw tiles
                 @include background-icon(courseware, clickable, 128);
         }
     }
+
     .description {
         height: 220px;
-        padding: 10px 14px;
+        padding: 14px;
         color: $white;
         position: relative;
 
         header {
-            font-size: 1.25em;
+            font-size: 20px;
+            line-height: 22px;
             color: $white;
             border: none;
             margin-bottom: 0.75em;
@@ -4150,6 +4565,16 @@ cw tiles
             overflow: hidden;
             text-overflow: ellipsis;
             white-space: nowrap;
+            background-repeat: no-repeat;
+            background-position: 0 0;
+
+            @each $type, $icon in $element-icons {
+                &.description-icon-#{$type} {
+                    width: 212px;
+                    padding-left: 28px;
+                    @include background-icon(#{$icon}, info_alt, 22);
+                }
+            }
         }
 
         .description-text-wrapper {
@@ -4160,11 +4585,11 @@ cw tiles
             -webkit-line-clamp: 7;
             -webkit-box-orient: vertical;
             p {
-                text-align: justify;
+                text-align: left;
             }
         }
 
-        footer{
+        footer {
             width: 242px;
             text-align: right;
             color: $white;
@@ -4232,3 +4657,60 @@ vSelect end
 }
 
 /* cw manager copy end*/
+
+/* courseware template preview */
+.cw-template-preview {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    width: calc(100% - 20px);;
+    padding: 10px;
+    .cw-template-preview-container-wrapper {
+        margin-bottom: 10px;
+
+        &.cw-template-preview-container-full {
+            width: 100%
+        }
+        &.cw-template-preview-container-half {
+            width: calc(50% - 4px);
+        }
+        &.cw-template-preview-container-half-center {
+            width: 100%;
+            .cw-template-preview-container-content {
+                width: 50%;
+                margin: auto;
+            }
+        }
+
+        .cw-template-preview-container-content {
+            border: solid thin $content-color-40;
+        }
+
+        .cw-template-preview-container-title {
+            font-weight: 700;
+            padding: 4px 4px 4px 8px;
+            color: $base-color;
+            background-color: $content-color-20;
+        }
+
+        .cw-template-preview-blocks {
+            border: solid thin $content-color-40;
+            padding: 1em;
+            margin: 5px;
+            background-color: $white;
+
+        }
+    }
+}
+/* courseware template preview end*/
+
+/* contents courseware courses */
+.cw-content-courses {
+    h2 {
+        margin-top: 0;
+    }
+    ul.cw-tiles {
+        margin-bottom: 20px;
+    }
+}
+/* contents courseware courses end*/
diff --git a/resources/vue/components/courseware/AdminApp.vue b/resources/vue/components/courseware/AdminApp.vue
new file mode 100755
index 00000000000..01bde3867f9
--- /dev/null
+++ b/resources/vue/components/courseware/AdminApp.vue
@@ -0,0 +1,33 @@
+<template>
+    <div class="cw-admin">
+        <courseware-admin-templates v-if="templatesView" />
+        <MountingPortal mountTo="#courseware-admin-view-widget" name="sidebar-views">
+            <courseware-admin-view-widget />
+        </MountingPortal>
+        <MountingPortal mountTo="#courseware-admin-action-widget" name="sidebar-views">
+            <courseware-admin-action-widget />
+        </MountingPortal>
+
+    </div>
+</template>
+
+<script>
+import CoursewareAdminActionWidget from './CoursewareAdminActionWidget.vue';
+import CoursewareAdminTemplates from './CoursewareAdminTemplates.vue';
+import CoursewareAdminViewWidget from './CoursewareAdminViewWidget.vue';
+export default {
+    components: {
+        CoursewareAdminActionWidget,
+        CoursewareAdminTemplates,
+        CoursewareAdminViewWidget
+    },
+    computed: {
+        adminViewMode() {
+            return this.$store.getters.adminViewMode;
+        },
+        templatesView() {
+            return this.adminViewMode === 'templates';
+        },
+    },
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/ContentBookmarkApp.vue b/resources/vue/components/courseware/ContentBookmarkApp.vue
new file mode 100755
index 00000000000..b0c9fbc51d2
--- /dev/null
+++ b/resources/vue/components/courseware/ContentBookmarkApp.vue
@@ -0,0 +1,21 @@
+<template>
+  <div class="cw-content-bookmark">
+        <courseware-content-bookmarks />
+        <MountingPortal mountTo="#courseware-content-bookmark-filter-widget" name="sidebar-views">
+            <courseware-content-bookmark-filter-widget />
+        </MountingPortal>
+  </div>
+</template>
+
+<script>
+import CoursewareContentBookmarks from './CoursewareContentBookmarks.vue';
+import CoursewareContentBookmarkFilterWidget from './CoursewareContentBookmarkFilterWidget.vue';
+
+export default {
+    components: {
+        CoursewareContentBookmarks,
+        CoursewareContentBookmarkFilterWidget
+    },
+
+}
+</script>
diff --git a/resources/vue/components/courseware/ContentOverviewApp.vue b/resources/vue/components/courseware/ContentOverviewApp.vue
new file mode 100755
index 00000000000..cad3fc0076a
--- /dev/null
+++ b/resources/vue/components/courseware/ContentOverviewApp.vue
@@ -0,0 +1,25 @@
+<template>
+  <div class="cw-content-overview">
+        <courseware-content-overview-elements />
+        <MountingPortal mountTo="#courseware-content-overview-action-widget" name="sidebar-views">
+            <courseware-content-overview-action-widget />
+        </MountingPortal>
+        <MountingPortal mountTo="#courseware-content-overview-filter-widget" name="sidebar-views">
+            <courseware-content-overview-filter-widget />
+        </MountingPortal>
+  </div>
+</template>
+
+<script>
+import CoursewareContentOverviewElements from './CoursewareContentOverviewElements.vue';
+import CoursewareContentOverviewActionWidget from './CoursewareContentOverviewActionWidget.vue';
+import CoursewareContentOverviewFilterWidget from './CoursewareContentOverviewFilterWidget.vue';
+
+export default {
+    components: {
+        CoursewareContentOverviewElements,
+        CoursewareContentOverviewActionWidget,
+        CoursewareContentOverviewFilterWidget
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/CoursewareAccordionContainer.vue
index 4a318c30f29..28f69aaba67 100755
--- a/resources/vue/components/courseware/CoursewareAccordionContainer.vue
+++ b/resources/vue/components/courseware/CoursewareAccordionContainer.vue
@@ -6,6 +6,7 @@
         :isTeacher="isTeacher"
         @storeContainer="storeContainer"
         @closeEdit="initCurrentData"
+        @sortBlocks="enableSort"
     >
         <template v-slot:containerContent>
             <courseware-collapsible-box
@@ -15,7 +16,7 @@
                 :icon="section.icon"
                 :open="index === 0"
             >
-                <ul class="cw-container-accordion-block-list">
+                <ul v-if="!sortMode" class="cw-container-accordion-block-list">
                     <li v-for="block in section.blocks" :key="block.id" class="cw-block-item">
                         <component
                             :is="component(block)"
@@ -24,11 +25,33 @@
                             :isTeacher="isTeacher"
                         />
                     </li>
-                    <li v-if="showEditMode">
+                    <li v-if="showEditMode && canAddElements">
                         <courseware-block-adder-area :container="container" :section="index" @updateContainerContent="updateContent"/>
                     </li>
                 </ul>
+                <draggable
+                    v-if="sortMode && canEdit"
+                    class="cw-container-list-block-list cw-container-list-sort-mode"
+                    :class="[section.blocks.length === 0 ? 'cw-container-list-sort-mode-empty' : '']"
+                    tag="ul"
+                    v-model="section.blocks"
+                    v-bind="dragOptions"
+                    handle=".cw-sortable-handle"
+                    @start="isDragging = true"
+                    @end="isDragging = false"
+                >
+                    <transition-group type="transition" name="flip-blocks" tag="div">
+                        <li v-for="block in section.blocks" :key="block.id" class="cw-block-item cw-block-item-sortable">
+                            <component :is="component(block)" :block="block" :canEdit="canEdit" :isTeacher="isTeacher" />
+                        </li>
+                    </transition-group>
+                
+                </draggable>
             </courseware-collapsible-box>
+            <div v-if="sortMode && canEdit">
+                <button class="button accept" @click="storeSort"><translate>Sortierung speichern</translate></button>
+                <button class="button cancel"  @click="resetSort"><translate>Sortieren abbrechen</translate></button>
+            </div>
         </template>
         <template v-slot:containerEditDialog>
             <form class="default cw-container-dialog-edit-form" @submit.prevent="">
@@ -87,11 +110,20 @@ export default {
         container: Object,
         canEdit: Boolean,
         isTeacher: Boolean,
+        canAddElements: Boolean,
     },
     data() {
         return {
             currentContainer: {},
             currentSections: [],
+            sortMode: false,
+            isDragging: false,
+            dragOptions: {
+                animation: 0,
+                group: "description",
+                disabled: false,
+                ghostClass: "block-ghost"
+            },
         };
     },
     computed: {
@@ -118,6 +150,7 @@ export default {
     methods: {
         ...mapActions({
             updateContainer: 'updateContainer',
+            lockObject: 'lockObject',
             unlockObject: 'unlockObject',
         }),
         initCurrentData() {
@@ -163,7 +196,19 @@ export default {
                 container: this.currentContainer,
                 structuralElementId: this.currentContainer.relationships['structural-element'].data.id,
             });
-            await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            await this.unlockObject({ id: this.currentContainer.id, type: 'courseware-containers' });
+            this.initCurrentData();
+        },
+        enableSort() {
+            this.sortMode = true;
+        },
+        async storeSort() {
+            this.sortMode = false;
+            this.storeContainer();
+        },
+        async resetSort() {
+            await this.unlockObject({ id: this.currentContainer.id, type: 'courseware-containers' });
+            this.sortMode = false;
             this.initCurrentData();
         },
         component(block) {
diff --git a/resources/vue/components/courseware/CoursewareActionWidget.vue b/resources/vue/components/courseware/CoursewareActionWidget.vue
index 7d94fc3a61a..4772bb2cc31 100644
--- a/resources/vue/components/courseware/CoursewareActionWidget.vue
+++ b/resources/vue/components/courseware/CoursewareActionWidget.vue
@@ -6,21 +6,29 @@
         <li class="cw-action-widget-show-consume-mode" @click="showConsumeMode">
             <translate>Vollbild einschalten</translate>
         </li>
-        <li v-show="canEdit" class="cw-action-widget-edit" @click="editElement">
+        <li v-if="canEdit" class="cw-action-widget-edit" @click="editElement">
             <translate>Seite bearbeiten</translate>
         </li>
-        <li v-show="canEdit" class="cw-action-widget-add" @click="addElement">
+        <li v-if="canEdit" class="cw-action-widget-sort" @click="sortContainers">
+            <translate>Abschnitte sortieren</translate>
+        </li>
+        <li v-if="canEdit" class="cw-action-widget-add" @click="addElement">
             <translate>Seite hinzufügen</translate>
         </li>
         <li class="cw-action-widget-info" @click="showElementInfo"><translate>Informationen anzeigen</translate></li>
         <li class="cw-action-widget-star" @click="createBookmark"><translate>Lesezeichen setzen</translate></li>
-        <li v-show="canEdit" @click="exportElement" class="cw-action-widget-export">
+        <li v-if="canExport" @click="exportElement" class="cw-action-widget-export">
             <translate>Seite exportieren</translate>
         </li>
-        <li v-show="canEdit && oerEnabled" @click="oerElement" class="cw-action-widget-oer">
+        <li v-if="(canEdit || userIsTeacher) && canVisit" class="cw-action-widget-export-pdf">
+            <a :href="pdfExportURL">
+                <translate>Seite als pdf-Dokument exportieren</translate>
+            </a>
+        </li>
+        <li v-if="canEdit && oerEnabled && userIsTeacher" @click="oerElement" class="cw-action-widget-oer">
             <translate>Seite auf %{oerTitle} veröffentlichen</translate>
         </li>
-        <li v-show="!isRoot && canEdit" class="cw-action-widget-trash" @click="deleteElement">
+        <li v-if="!isRoot && canEdit && !isTask" class="cw-action-widget-trash" @click="deleteElement">
             <translate>Seite löschen</translate>
         </li>
     </ul>
@@ -33,18 +41,23 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-action-widget',
-    props: ['structuralElement'],
+    props: ['structuralElement', 'canVisit'],
     components: {
         StudipIcon,
     },
     mixins: [CoursewareExport],
     computed: {
         ...mapGetters({
+            context: 'context',
             oerEnabled: 'oerEnabled',
             oerTitle: 'oerTitle',
             userId: 'userId',
             consumeMode: 'consumeMode',
-            showToolbar: 'showToolbar'
+            showToolbar: 'showToolbar',
+            userIsTeacher: 'userIsTeacher',
+            consumeMode: 'consumeMode',
+            showToolbar: 'showToolbar',
+            userIsTeacher: 'userIsTeacher',
         }),
         isRoot() {
             if (!this.structuralElement) {
@@ -76,6 +89,49 @@ export default {
         },
         tocText() {
             return this.showToolbar ? this.$gettext('Inhaltsverzeichnis ausblenden') : this.$gettext('Inhaltsverzeichnis anzeigen');
+        },
+        pdfExportURL() {
+            if (this.context.type === 'users') {
+                return STUDIP.URLHelper.getURL('dispatch.php/contents/courseware/pdf_export/' + this.structuralElement.id);
+            }
+            if (this.context.type === 'courses') {
+                return STUDIP.URLHelper.getURL('dispatch.php/course/courseware/pdf_export/' + this.structuralElement.id);
+            }
+
+            return '';
+        },
+        isTask() {
+            return this.structuralElement?.relationships.task.data !== null;
+        },
+        canExport() {
+            if (this.context.type === 'users') {
+                return true;
+            }
+
+            return this.canEdit && this.userIsTeacher;
+        },
+        tocText() {
+            return this.showToolbar ? this.$gettext('Inhaltsverzeichnis ausblenden') : this.$gettext('Inhaltsverzeichnis anzeigen');
+        },
+        pdfExportURL() {
+            if (this.context.type === 'users') {
+                return STUDIP.URLHelper.getURL('dispatch.php/contents/courseware/pdf_export/' + this.structuralElement.id);
+            }
+            if (this.context.type === 'courses') {
+                return STUDIP.URLHelper.getURL('dispatch.php/course/courseware/pdf_export/' + this.structuralElement.id);
+            }
+
+            return '';
+        },
+        isTask() {
+            return this.structuralElement?.relationships.task.data !== null;
+        },
+        canExport() {
+            if (this.context.type === 'users') {
+                return true;
+            }
+
+            return this.canEdit && this.userIsTeacher;
         }
     },
     methods: {
@@ -86,6 +142,7 @@ export default {
             showElementInfoDialog: 'showElementInfoDialog',
             showElementExportDialog: 'showElementExportDialog',
             showElementOerDialog: 'showElementOerDialog',
+            setStructuralElementSortMode: 'setStructuralElementSortMode',
             companionInfo: 'companionInfo',
             addBookmark: 'addBookmark',
             lockObject: 'lockObject',
@@ -112,6 +169,9 @@ export default {
             }
             this.showElementEditDialog(true);
         },
+        sortContainers() {
+            this.setStructuralElementSortMode(true);
+        },
         async deleteElement() {
             await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
             this.showElementDeleteDialog(true);
diff --git a/resources/vue/components/courseware/CoursewareActivityItem.vue b/resources/vue/components/courseware/CoursewareActivityItem.vue
index 6b18bfea61e..8a7a4a9cb67 100755
--- a/resources/vue/components/courseware/CoursewareActivityItem.vue
+++ b/resources/vue/components/courseware/CoursewareActivityItem.vue
@@ -1,16 +1,16 @@
 <template>
     <li class="cw-activity-item">
         <p v-if="item.username" class="cw-activity-item-user">
-            <a><studip-icon role="inactive" shape="headache" />{{ item.username }}</a>
+            <a :href="userUrl"><studip-icon role="inactive" shape="headache" />{{ item.username }}</a>
         </p>
         <p v-if="item.date" class="cw-activity-item-date">
             <studip-icon role="inactive" shape="timetable" />{{ item.date }}
         </p>
         <p class="cw-activity-item-element">
-            <a :href="item.element_id"><studip-icon role="inactive" :shape="shape" />{{ item.element_breadcrumb }}</a>
+            <a :href="linkUrl" :title="item.complete_breadcrumb"><studip-icon role="inactive" :shape="shape" />{{ item.element_breadcrumb }}</a>
         </p>
-        <p v-if="item.text" class="cw-activity-item-text">
-            {{ item.text }}
+        <p v-if="text" class="cw-activity-item-text">
+            <span v-html="text"></span>
         </p>
     </li>
 </template>
@@ -27,17 +27,37 @@ export default {
         item: Object,
     },
     computed: {
+        text() {
+            if (this.item.content == null || this.item.content == '') {
+                return this.item.text;
+            }
+
+            switch (this.item.type) {
+                case 'interacted':
+                    return this.item.username + ' commented: ' + this.item.content; //TODO: Localization
+                case 'answered':
+                    return this.item.username + ' added feedback: ' + this.item.content; //TODO: Localization
+                default:
+                    return this.item.text;
+            }
+        },
+
+        userUrl() {
+            return STUDIP.URLHelper.base_url + 'dispatch.php/profile?username=' + this.item.username;
+        },
+
+        linkUrl() {
+            return STUDIP.URLHelper.base_url + 'dispatch.php/course/courseware/?cid=' + this.item.context_id + '#/structural_element/' + this.item.element_id;
+        },
         shape() {
             switch (this.item.type) {
-                case 'comment':
+                case 'interacted':
                     return 'item';
-                case 'feedback':
+                case 'answered':
                     return 'support';
-                case 'new_block':
-                case 'new_element':
+                case 'created':
                     return 'add';
-                case 'updated_block':
-                case 'updated_element':
+                case 'edited':
                     return 'edit';
                 default:
                     return 'question-circle-full';
diff --git a/resources/vue/components/courseware/CoursewareAdminActionWidget.vue b/resources/vue/components/courseware/CoursewareAdminActionWidget.vue
new file mode 100755
index 00000000000..90ba4255b31
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareAdminActionWidget.vue
@@ -0,0 +1,30 @@
+<template>
+    <ul class="widget-list widget-links cw-action-widget">
+        <li v-if="templatesView" class="cw-action-widget-add" @click="addTemplate">
+            <translate>Vorlage hinzufügen</translate>
+        </li>
+    </ul>
+</template>
+
+<script>
+
+export default {
+    name: 'courseware-admin-action-widget',
+    computed: {
+        adminViewMode() {
+            return this.$store.getters.adminViewMode;
+        },
+        templatesView() {
+            return this.adminViewMode === 'templates';
+        },
+        showAddTemplateDialog() {
+            return this.$store.getters.showAddTemplateDialog;
+        },
+    },
+    methods: {
+        addTemplate() {
+            this.$store.dispatch('showAddTemplateDialog', true);
+        }
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareAdminTemplates.vue b/resources/vue/components/courseware/CoursewareAdminTemplates.vue
new file mode 100755
index 00000000000..c237ebb6491
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareAdminTemplates.vue
@@ -0,0 +1,237 @@
+<template>
+    <div class="cw-admin-templates">
+        <table class="default">
+            <caption>
+                <translate>Vorlagen</translate>
+            </caption>
+            <thead>
+                <tr>
+                    <th><translate>Art des Lernmaterials</translate></th>
+                    <th><translate>Name</translate></th>
+                    <th class="actions"><translate>Aktionen</translate></th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-for="template in templates" :key="template.id">
+                    <td>{{ getPurposeName(template.attributes.purpose) }}</td>
+                    <td>{{ template.attributes.name }}</td>
+                    <td class="actions">
+                        <studip-action-menu
+                            :items="menuItems"
+                            @editTemplate="editTemplate(template.id)"
+                            @deleteTemplate="confimDeleteTemplate(template.id)"
+                        />
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+        <studip-dialog
+            v-if="showAddTemplateDialog"
+            :title="$gettext('Vorlage hinzufügen')"
+            :confirmText="'Erstellen'"
+            :confirmClass="'accept'"
+            :closeText="$gettext('Schließen')"
+            :closeClass="'cancel'"
+            class="cw-admin-template-dialog"
+            height="360"
+            @close="closeAddDialog"
+            @confirm="createNewTemplate"
+        >
+            <template v-slot:dialogContent>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        <translate>Name der neuen Vorlage</translate>
+                        <input v-model="newTemplateName" type="text" />
+                    </label>
+                    <label>
+                        <translate>Art des Lernmaterials</translate>
+                        <select v-model="newElementPurpose">
+                            <option value="content"><translate>Inhalt</translate></option>
+                            <option value="template"><translate>Aufgabenvorlage</translate></option>
+                            <option value="oer"><translate>OER-Material</translate></option>
+                            <option value="portfolio"><translate>ePortfolio</translate></option>
+                            <option value="draft"><translate>Entwurf</translate></option>
+                            <option value="other"><translate>Sonstiges</translate></option>
+                        </select>
+                    </label>
+                    <label>
+                        <translate>Vorlage</translate><br>
+                        <button
+                            class="button"
+                            @click.prevent="chooseFile"
+                        >
+                            <translate>Vorlage-Archiv auswählen</translate>
+                        </button>
+                        <div v-if="importZip" class="cw-import-zip">
+                            <header>{{ importZip.name }}</header>
+                        </div>
+                        <input ref="importFile" type="file" accept=".zip" @change="setImport" style="visibility: hidden" />
+                    </label>
+                </form>
+            </template>
+        </studip-dialog>
+        <studip-dialog
+            v-if="showEditTemplateDialog"
+            :title="$gettext('Vorlage bearbeiten')"
+            :confirmText="'Speichern'"
+            :confirmClass="'accept'"
+            :closeText="$gettext('Schließen')"
+            :closeClass="'cancel'"
+            class="cw-admin-template-dialog"
+            @close="closeEditDialog"
+            @confirm="updateCurrentTemplate"
+        >
+            <template v-slot:dialogContent>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        <translate>Name der neuen Vorlage</translate>
+                        <input v-model="currentTemplate.attributes.name" type="text" />
+                    </label>
+                    <label>
+                        <translate>Art des Lernmaterials</translate>
+                        <select v-model="currentTemplate.attributes.purpose">
+                            <option value="content"><translate>Inhalt</translate></option>
+                            <option value="template"><translate>Aufgabenvorlage</translate></option>
+                            <option value="oer"><translate>OER-Material</translate></option>
+                            <option value="portfolio"><translate>ePortfolio</translate></option>
+                            <option value="draft"><translate>Entwurf</translate></option>
+                            <option value="other"><translate>Sonstiges</translate></option>
+                        </select>
+                    </label>
+                </form>
+            </template>
+        </studip-dialog>
+        <studip-dialog
+                v-if="showDeleteDialog"
+                :title="$gettext('Vorlage löschen')"
+                :question="$gettext('Möchten Sie diese Vorlage wirklich löschen?')"
+                height="180"
+                @confirm="deleteCurrentTemplate"
+                @close="closeDeleteDialog"
+        ></studip-dialog>
+    </div>
+</template>
+
+<script>
+import StudipActionMenu from '../StudipActionMenu.vue';
+import StudipDialog from './../StudipDialog.vue';
+
+import JSZip from 'jszip';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-admin-templates',
+    components: {
+        StudipActionMenu,
+        StudipDialog,
+    },
+    data() {
+        return {
+            menuItems: [
+                { id: 1, label: this.$gettext('Vorlage bearbeiten'), icon: 'edit', emit: 'editTemplate' },
+                { id: 2, label: this.$gettext('Vorlage löschen'), icon: 'trash', emit: 'deleteTemplate' }
+            ],
+            newTemplateName: '',
+            newElementPurpose: '',
+            importZip: null,
+            zip: null,
+            showEditTemplateDialog: false,
+            currentTemplate: null,
+            showDeleteDialog: false,
+        }
+    },
+    computed: {
+        ...mapGetters({
+            templateById: 'courseware-templates/byId',
+            showAddTemplateDialog: 'showAddTemplateDialog',
+            templates: 'courseware-templates/all',
+        }),
+    },
+    methods: {
+        ...mapActions({
+            createTemplate: 'courseware-templates/create',
+            updateTemplate: 'courseware-templates/update',
+            deleteTemplate: 'courseware-templates/delete'
+        }),
+        closeAddDialog() {
+            this.$store.dispatch('showAddTemplateDialog', false);
+            this.newTemplateName = '';
+            this.newElementPurpose = '';
+            this.importZip = null;
+            this.zip = null;
+        },
+        setImport(event) {
+            this.importZip = event.target.files[0];
+        },
+        chooseFile() {
+            this.$refs.importFile.click();
+        },
+        async createNewTemplate() {
+            let view = this;
+            let data = null;
+            this.zip = new JSZip();
+
+            await this.zip.loadAsync(this.importZip).then(async function () {
+                data = await view.zip.file('courseware.json').async('string');
+            });
+
+            this.createTemplate({
+                name: this.newTemplateName,
+                purpose: this.newElementPurpose,
+                structure: data
+            });
+
+            this.closeAddDialog();
+        },
+        editTemplate(templateId) {
+            this.currentTemplate = this.templateById({id: templateId});
+            this.showEditTemplateDialog = true;
+        },
+        closeEditDialog() {
+            this.showEditTemplateDialog = false;
+            this.currentTemplate = null;
+        },
+        updateCurrentTemplate() {
+            this.updateTemplate({
+                id: this.currentTemplate.id,
+                name: this.currentTemplate.attributes.name,
+                purpose: this.currentTemplate.attributes.purpose,
+            });
+            this.closeEditDialog();
+        },
+        confimDeleteTemplate(templateId) {
+            this.currentTemplate = this.templateById({id: templateId});
+            this.showDeleteDialog = true;
+        },
+        deleteCurrentTemplate() {
+            this.deleteTemplate( {
+                id: this.currentTemplate.id
+            });
+            this.closeDeleteDialog();
+        },
+        closeDeleteDialog() {
+            this.showDeleteDialog = false;
+            this.currentTemplate = null;
+        },
+        getPurposeName(purpose) {
+            switch (purpose) {
+                case 'content':
+                    return this.$gettext('Inhalt');
+                case 'template':
+                    return this.$gettext('Aufgabenvorlage');
+                case 'oer':
+                    return this.$gettext('OER-Material');
+                case 'portfolio':
+                    return this.$gettext('ePortfolio');
+                case 'draft':
+                    return this.$gettext('Entwurf');
+                case 'other':
+                    return this.$gettext('Sonstige');
+                default:
+                    return purpose;
+            }
+        }
+    }
+
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareAdminViewWidget.vue b/resources/vue/components/courseware/CoursewareAdminViewWidget.vue
new file mode 100755
index 00000000000..849d0705c55
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareAdminViewWidget.vue
@@ -0,0 +1,31 @@
+<template>
+  <ul class="widget-list widget-links sidebar-views cw-view-widget">
+        <li
+            :class="{ active: templatesView }"
+            @click="setTemplatesView"
+        >
+            <translate>Vorlagen</translate>
+        </li>
+  </ul>
+</template>
+
+<script>
+
+export default {
+    name: 'courseware-admin-view-widget',
+    computed: {
+        adminViewMode() {
+            return this.$store.getters.adminViewMode;
+        },
+        templatesView() {
+            return this.adminViewMode === 'templates';
+        },
+    },
+    methods: {
+        setTemplatesView() {
+            this.$store.dispatch('adminViewMode', 'templates');
+        },
+    }
+
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareBlockActions.vue b/resources/vue/components/courseware/CoursewareBlockActions.vue
index 2c4c12b0361..fd689fa6395 100755
--- a/resources/vue/components/courseware/CoursewareBlockActions.vue
+++ b/resources/vue/components/courseware/CoursewareBlockActions.vue
@@ -4,8 +4,6 @@
             :items="menuItems"
             @editBlock="editBlock"
             @setVisibility="setVisibility"
-            @showComments="showComments"
-            @showFeedback="showFeedback"
             @showInfo="showInfo"
             @deleteBlock="deleteBlock"
         />
@@ -31,9 +29,7 @@ export default {
     },
     data() {
         return {
-            menuItems: [
-                { id: 6, label: this.$gettext('Kommentare anzeigen'), icon: 'comment2', emit: 'showComments' },
-            ],
+            menuItems: [],
         };
     },
     computed: {
@@ -48,9 +44,6 @@ export default {
         },
     },
     mounted() {
-        if (this.deleteOnly) {
-            this.menuItems = [];
-        }
         if (this.canEdit) {
             if (!this.deleteOnly) {
                 this.menuItems.push({ id: 1, label: this.$gettext('Block bearbeiten'), icon: 'edit', emit: 'editBlock' });
@@ -62,12 +55,6 @@ export default {
                     icon: this.block.attributes.visible ? 'visibility-visible' : 'visibility-invisible', // do we change the icons ?
                     emit: 'setVisibility',
                 });
-                this.menuItems.push({
-                    id: 5,
-                    label: this.$gettext('Feedback anzeigen'),
-                    icon: 'comment',
-                    emit: 'showFeedback',
-                });
                 this.menuItems.push({
                     id: 7,
                     label: this.$gettext('Informationen zum Block'),
@@ -99,12 +86,6 @@ export default {
         editBlock() {
             this.$emit('editBlock');
         },
-        showFeedback() {
-            this.$emit('showFeedback');
-        },
-        showComments() {
-            this.$emit('showComments');
-        },
         showInfo() {
             this.$emit('showInfo');
         },
diff --git a/resources/vue/components/courseware/CoursewareBlockComments.vue b/resources/vue/components/courseware/CoursewareBlockComments.vue
index f182aa8168e..fb66627a07e 100755
--- a/resources/vue/components/courseware/CoursewareBlockComments.vue
+++ b/resources/vue/components/courseware/CoursewareBlockComments.vue
@@ -1,8 +1,7 @@
 <template>
-    <section class="cw-block-comments">
-        <header><translate>Kommentare</translate></header>
+    <section class="cw-block-comments" :class="[emptyComments ? 'cw-block-comments-empty' : '']">
         <div class="cw-block-features-content">
-            <div class="cw-block-comments-items" ref="comments">
+            <div class="cw-block-comments-items" v-show="!emptyComments" ref="commentsRef">
                 <courseware-talk-bubble
                     v-for="comment in comments"
                     :key="comment.id"
@@ -12,7 +11,6 @@
             <div class="cw-block-comment-create">
                 <textarea v-model="createComment" :placeholder="placeHolder" spellcheck="true"></textarea>
                 <button class="button" @click="postComment"><translate>Senden</translate></button>
-                <button class="button" @click="$emit('close')"><translate>Schließen</translate></button>
             </div>
         </div>
     </section>
@@ -29,7 +27,6 @@ export default {
     },
     props: {
         block: Object,
-        comments: Array,
     },
     data() {
         return {
@@ -41,21 +38,55 @@ export default {
         ...mapGetters({
             relatedUser: 'users/related',
             userId: 'userId',
+            getComments: 'courseware-block-comments/related',
         }),
+        comments() {
+            const parent = {
+                type: this.block.type,
+                id: this.block.id,
+            };
+
+            return this.getComments({ parent, relationship: 'comments' });
+        },
+        emptyComments() {
+            if (this.comments === null || this.comments.length === 0) {
+                return true;
+            }
+
+            return false;
+        }
     },
     methods: {
+        async loadComments() {
+            const parent = {
+                type: this.block.type,
+                id: this.block.id,
+            };
+            await this.$store.dispatch('courseware-block-comments/loadRelated', {
+                parent,
+                relationship: 'comments',
+                options: {
+                    include: 'user',
+                },
+            });
+        },
         async postComment() {
-            let data = {};
-            data.attributes = {};
-            data.attributes.comment = this.createComment;
-            data.relationships = {};
-            data.relationships.block = {};
-            data.relationships.block.data = {};
-            data.relationships.block.data.id = this.block.id;
-            data.relationships.block.data.type = this.block.type;
+            const data = {
+                attributes: {
+                    comment: this.createComment
+                },
+                relationships: {
+                    block: {
+                        data: {
+                            id: this.block.id,
+                            type: this.block.type
+                        }
+                    }
+                }
+            };
 
             await this.$store.dispatch('courseware-block-comments/create', data);
-            this.$emit('postComment');
+            this.loadComments();
             this.createComment = '';
         },
         buildPayload(comment) {
@@ -78,8 +109,19 @@ export default {
             return payload;
         },
     },
+    mounted() {
+        this.loadComments();
+    },
     updated() {
-        this.$refs.comments.scrollTop = this.$refs.comments.scrollHeight;
+        let ref = this.$refs["commentsRef"];
+        ref.scrollTop = ref.scrollHeight;
     },
+    watch: {
+        comments() {
+            if (this.comments && this.comments.length > 0) {
+                this.$emit('hasComments');
+            }
+        }
+    }
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareBlockDiscussion.vue b/resources/vue/components/courseware/CoursewareBlockDiscussion.vue
new file mode 100755
index 00000000000..e9ce9a3af5b
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareBlockDiscussion.vue
@@ -0,0 +1,60 @@
+<template>
+    <div class="cw-block-discussion">
+        <courseware-collapsible-box
+            :title="text.comments"
+            :open="hasComments"
+        >
+            <courseware-block-comments
+            :block="block"
+            @hasComments="hasComments = true"
+            />
+        </courseware-collapsible-box>
+
+        <courseware-collapsible-box
+            v-if="canEdit || userIsTeacher"
+            :title="text.feedback"
+            :open="hasFeedback"
+        >
+            <courseware-block-feedback
+                :block="block"
+                :canEdit="canEdit"
+                @hasFeedback="hasFeedback = true"
+            />
+        </courseware-collapsible-box>
+    </div>
+</template>
+
+<script>
+import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
+import CoursewareBlockComments from './CoursewareBlockComments.vue';
+import CoursewareBlockFeedback from './CoursewareBlockFeedback.vue';
+import { mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-block-discussion',
+    components: {
+        CoursewareCollapsibleBox,
+        CoursewareBlockComments,
+        CoursewareBlockFeedback,
+    },
+    props: {
+        block: Object,
+        canEdit: Boolean
+    },
+    data() {
+        return {
+            hasComments: false,
+            hasFeedback: false,
+            text: {
+                comments: this.$gettext('Kommentare'),
+                feedback: this.$gettext('Feedback')
+            }
+        }
+    },
+    computed: {
+        ...mapGetters({
+            userIsTeacher: 'userIsTeacher',
+        }),
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockFeedback.vue b/resources/vue/components/courseware/CoursewareBlockFeedback.vue
index 05ec040d17b..312eeb9caa4 100755
--- a/resources/vue/components/courseware/CoursewareBlockFeedback.vue
+++ b/resources/vue/components/courseware/CoursewareBlockFeedback.vue
@@ -1,36 +1,45 @@
 <template>
-    <section class="cw-block-feedback">
-        <header><translate>Feedback</translate></header>
+    <section
+        v-if="canEdit || userIsTeacher"
+        class="cw-block-feedback"
+        :class="[emptyFeedback ? 'cw-block-feedback-empty' : '']"
+    >
         <div class="cw-block-features-content">
-            <div class="cw-block-feedback-items"  ref="feedbacks">
+            <div class="cw-block-feedback-items" v-show="!emptyFeedback" ref="feedbacks">
                 <courseware-talk-bubble
                     v-for="feedback in feedback"
                     :key="feedback.id"
                     :payload="buildPayload(feedback)"
                 />
             </div>
-            <div class="cw-block-feedback-create">
+            <courseware-companion-box
+                v-if="!userIsTeacher && feedback.length === 0"
+                :msgCompanion="$gettext('Es wurde noch kein Feedback abgegeben.')"
+                mood="pointing"
+            />
+            <div v-if="userIsTeacher" class="cw-block-feedback-create">
                 <textarea v-model="feedbackText" :placeholder="placeHolder" spellcheck="true"></textarea>
                 <button class="button" @click="postFeedback"><translate>Senden</translate></button>
-                <button class="button" @click="$emit('close')"><translate>Schließen</translate></button>
             </div>
         </div>
     </section>
 </template>
 
 <script>
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
 import CoursewareTalkBubble from './CoursewareTalkBubble.vue';
-import { mapActions, mapGetters } from 'vuex';
+import { mapGetters } from 'vuex';
+
 
 export default {
     name: 'courseware-block-feedback',
     components: {
+        CoursewareCompanionBox,
         CoursewareTalkBubble,
     },
     props: {
         block: Object,
         canEdit: Boolean,
-        isTeacher: Boolean,
     },
     data() {
         return {
@@ -43,18 +52,22 @@ export default {
             userId: 'userId',
             getRelatedFeedback: 'courseware-block-feedback/related',
             getRelatedUser: 'users/related',
+            userIsTeacher: 'userIsTeacher',
         }),
         feedback() {
             const { id, type } = this.block;
 
             return this.getRelatedFeedback({ parent: { id, type }, relationship: 'feedback' });
         },
+        emptyFeedback() {
+            if (this.feedback === null || this.feedback.length === 0) {
+                return true;
+            }
+
+            return false;
+        }
     },
     methods: {
-        ...mapActions({
-            loadFeedback: 'loadFeedback',
-            createFeedback: 'createFeedback',
-        }),
         async postFeedback() {
             this.createFeedback({ blockId: this.block.id, feedback: this.feedbackText });
             this.feedbackText = '';
@@ -72,6 +85,36 @@ export default {
                 user_avatar: user?.meta?.avatar.small,
             };
         },
+        async loadFeedback() {
+            const parent = {
+                type: this.block.type,
+                id: this.block.id,
+            };
+            await this.$store.dispatch('courseware-block-feedback/loadRelated', {
+                parent,
+                relationship: 'feedback',
+                options: {
+                    include: 'user',
+                },
+            });
+        },
+        async createFeedback() {
+            const data = {
+                attributes: {
+                    feedback: this.feedbackText,
+                },
+                relationships: {
+                    block: {
+                        data: {
+                            type: this.block.type,
+                            id: this.block.id,
+                        },
+                    },
+                },
+            };
+            await this.$store.dispatch('courseware-block-feedback/create', data, { root: true });
+            this.loadFeedback();
+        }
     },
     async mounted() {
         await this.loadFeedback(this.block.id);
@@ -79,5 +122,12 @@ export default {
     updated() {
         this.$refs.feedbacks.scrollTop = this.$refs.feedbacks.scrollHeight;
     },
+    watch: {
+        feedback() {
+            if (this.feedback && this.feedback.length > 0) {
+                this.$emit('hasFeedback');
+            }
+        }
+    }
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareChartBlock.vue b/resources/vue/components/courseware/CoursewareChartBlock.vue
index 4bce8b46c7f..b67d1edc505 100755
--- a/resources/vue/components/courseware/CoursewareChartBlock.vue
+++ b/resources/vue/components/courseware/CoursewareChartBlock.vue
@@ -62,7 +62,7 @@
                                     <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
                                 </template>
                                 <template #no-options="{ search, searching, loading }">
-                                    <translate>Es steht keine Auswahl zur Verfügung</translate>.
+                                    <translate>Es steht keine Auswahl zur Verfügung.</translate>
                                 </template>
                                 <template #selected-option="{name, rgb}">
                                     <span class="vs__option-color" :style="{'background-color': 'rgb(' + rgb + ')'}"></span><span>{{name}}</span>
diff --git a/resources/vue/components/courseware/CoursewareCollapsibleBox.vue b/resources/vue/components/courseware/CoursewareCollapsibleBox.vue
index 9021e2e1ec3..3bd0f40f6a8 100755
--- a/resources/vue/components/courseware/CoursewareCollapsibleBox.vue
+++ b/resources/vue/components/courseware/CoursewareCollapsibleBox.vue
@@ -34,5 +34,10 @@ export default {
         };
     },
     methods: {},
+    watch: {
+        open(state) {
+            this.isOpen = state;
+        }
+    }
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareContainerActions.vue b/resources/vue/components/courseware/CoursewareContainerActions.vue
index be2c3dd209c..e1da9884676 100755
--- a/resources/vue/components/courseware/CoursewareContainerActions.vue
+++ b/resources/vue/components/courseware/CoursewareContainerActions.vue
@@ -4,6 +4,7 @@
             :items="menuItems" 
             @editContainer="editContainer"
             @deleteContainer="deleteContainer"
+            @sortBlocks="sortBlocks"
         />
     </div>
 </template>
@@ -18,11 +19,15 @@ export default {
     computed: {
         menuItems() {
             if (this.container.attributes["container-type"] === 'list') {
-                return [{ id: 1, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' }];
+                return [
+                    { id: 1, label: this.$gettext('Blöcke sortieren'), icon: 'arr_1sort', emit: 'sortBlocks' },
+                    { id: 2, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' }
+                ];
             } else {
                 return [
                     { id: 1, label: this.$gettext('Abschnitt bearbeiten'), icon: 'edit', emit: 'editContainer' },
-                    { id: 2, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' },
+                    { id: 2, label: this.$gettext('Blöcke sortieren'), icon: 'arr_1sort', emit: 'sortBlocks' },
+                    { id: 3, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' },
                 ];
             }
         },
@@ -37,6 +42,9 @@ export default {
         deleteContainer() {
             this.$emit('deleteContainer');
         },
+        sortBlocks() {
+            this.$emit('sortBlocks');
+        }
     },
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareContentBookmarkFilterWidget.vue b/resources/vue/components/courseware/CoursewareContentBookmarkFilterWidget.vue
new file mode 100755
index 00000000000..de8eb010ce1
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareContentBookmarkFilterWidget.vue
@@ -0,0 +1,42 @@
+<template>
+    <select v-model="bookmarkFilter" class="sidebar-selectlist">
+        <option value="all">
+            <translate>alle</translate>
+        </option>
+        <option v-for="course in courses" :key="course.id" :value="course.id">
+            {{ course.attributes.title }}
+        </option>
+    </select>
+    
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-content-bookmark-filter-widget',
+    data() {
+        return {
+            bookmarkFilter: 'all'
+        };
+    },
+    computed: {
+        ...mapGetters({
+            courses: 'courses/all',
+        }),
+    },
+    methods: {
+        ...mapActions({
+            setBookmarkFilter: 'setBookmarkFilter',
+        }),
+        setFilter() {
+            this.setBookmarkFilter(this.bookmarkFilter);
+        }
+    },
+    watch: {
+        bookmarkFilter() {
+            this.setFilter();
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/CoursewareContentBookmarks.vue b/resources/vue/components/courseware/CoursewareContentBookmarks.vue
new file mode 100755
index 00000000000..0718ff00813
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareContentBookmarks.vue
@@ -0,0 +1,107 @@
+<template>
+    <div class="cw-bookmarks">
+        <ul class="cw-tiles">
+            <li
+            v-for="bookmark in sortedBookmarks"
+            :key="bookmark.id"
+            class="tile"
+            :class="[bookmark.attributes.payload.color, sortedBookmarks.length > 3 ? '':  'cw-tile-margin']"
+            >
+                <a :href="getElementUrl(bookmark)" :title="bookmark.attributes.title">
+                    <div
+                        class="preview-image"
+                        :class="[hasImage(child) ? '' : 'default-image']"
+                        :style="getChildStyle(bookmark)"
+                    ></div>
+                    <div class="description">
+                        <header
+                            :class="[bookmark.attributes.purpose !== '' ? 'description-icon-' + bookmark.attributes.purpose : '']"
+                        >
+                            {{ bookmark.attributes.title }}
+                        </header>
+                        <div class="description-text-wrapper">
+                            <p>{{ bookmark.attributes.payload.description }}</p>
+                        </div>
+                        <footer>
+                            <span v-if="bookmark.relationships.course">
+                                <studip-icon shape="seminar" role="info_alt"/> {{ getCourseName(bookmark.relationships.course.data.id) }}
+                            </span>
+                            <span v-if="bookmark.relationships.user">
+                                <studip-icon shape="headache" role="info_alt"/> {{ getUserName(bookmark.relationships.user.data.id) }}
+                            </span>
+                        </footer>
+                    </div>
+                </a>
+            </li>
+        </ul>
+    </div>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import StudipIcon from '../StudipIcon.vue'
+export default {
+    name: 'courseware-content-bookmarks',
+    components: {
+        StudipIcon
+    },
+    computed: {
+        ...mapGetters({
+            courseById: 'courses/byId',
+            userById: 'users/byId',
+            userId: 'userId',
+            bookmarks: 'courseware-structural-elements/all',
+            bookmarkFilter: 'bookmarkFilter'
+        }),
+        sortedBookmarks() {
+            if (this.bookmarks) {
+                if (this.bookmarkFilter === 'all') {
+                    return this.bookmarks;
+                }
+                return this.bookmarks.filter(bookmark => {
+                    return bookmark.relationships.course.data.id === this.bookmarkFilter;
+                });
+            }
+        }
+    },
+    methods: {
+        ...mapActions({
+            loadUser: 'users/loadById',
+        }),
+        getCourseName(cid) {
+            const course = this.courseById({id: cid});
+
+            return course.attributes.title;
+        },
+        async getUserName(userId) {
+            await this.loadUser({id: userId});
+            const user = this.userById({id: userId});
+
+            return user.attributes['formatted-name'];
+        },
+        getElementUrl(element) {
+            if (element.relationships.course.data) {
+                let cid = element.relationships.course.data.id;
+                return STUDIP.URLHelper.base_url + 'dispatch.php/course/courseware/?cid='+ cid +'#/structural_element/' + element.id;
+            }
+
+            return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/courseware#/structural_element/' + element.id;
+        },
+        getChildStyle(element) {
+            let url = element.relationships?.image?.meta?.['download-url'];
+
+            if (url) {
+                return {'background-image': 'url(' + url + ')'};
+            } else {
+                return {};
+            }
+        },
+        hasImage(child) {
+            return child.relationships?.image?.data !== null;
+        },
+    },
+
+
+
+}
+</script>
diff --git a/resources/vue/components/courseware/CoursewareContentOverviewActionWidget.vue b/resources/vue/components/courseware/CoursewareContentOverviewActionWidget.vue
new file mode 100755
index 00000000000..a5983cbe020
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareContentOverviewActionWidget.vue
@@ -0,0 +1,23 @@
+<template>
+    <ul class="widget-list widget-links cw-action-widget">
+        <li class="cw-action-widget-add" @click="addElement">
+            <translate>Neues Lernmaterial anlegen</translate>
+        </li>
+    </ul>
+</template>
+
+<script>
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-content-overview-action-widget',
+    methods: {
+        ...mapActions({
+            setShowOverviewElementAddDialog: 'setShowOverviewElementAddDialog'
+        }),
+        addElement() {
+            this.setShowOverviewElementAddDialog(true);
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/CoursewareContentOverviewElements.vue b/resources/vue/components/courseware/CoursewareContentOverviewElements.vue
new file mode 100755
index 00000000000..17e661c4bad
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareContentOverviewElements.vue
@@ -0,0 +1,535 @@
+<template>
+<div v-if="root">
+    <ul class="cw-tiles">
+        <li
+            v-for="child in filteredChildren"
+            :key="child.id"
+            class="tile"
+            :class="[child.attributes.payload.color, filteredChildren.length > 3 ? '':  'cw-tile-margin']"
+        >
+            <a :href="getElementUrl(child.id)" :title="child.attributes.title">
+                <div
+                    class="preview-image"
+                    :class="[hasImage(child) ? '' : 'default-image']"
+                    :style="getChildStyle(child)"
+                ></div>
+                <div class="description">
+                    <header
+                        :class="[child.attributes.purpose !== '' ? 'description-icon-' + child.attributes.purpose : '']"
+                    >
+                        {{ child.attributes.title }}
+                    </header>
+                    <div class="description-text-wrapper">
+                        <p>{{ child.attributes.payload.description }}</p>
+                    </div>
+                    <footer>
+                        {{ countChildren(child) }}
+                        <translate
+                            :translate-n="countChildren(child)"
+                            translate-plural="Seiten"
+                        >
+                            Seite
+                        </translate>
+                    </footer>
+                </div>
+            </a>
+        </li>
+    </ul>
+    <courseware-companion-box v-if="children.length !== 0 && filteredChildren.length === 0 && purposeFilter !== 'all'" :msgCompanion="text.emptyFilter" mood="pointing"/>
+    <div v-if="children.length === 0" class="cw-contents-overview-teaser">
+        <div class="cw-contents-overview-teaser-content">
+            <header><translate>Ihre persönlichen Lernmaterialien</translate></header>
+            <p><translate>Erstellen und Verwalten Sie hier ihre eigenen persönlichen Lernmaterialien in Form von ePorfolios,
+                        Vorlagen für Veranstaltungen oder einfach nur persönliche Inhalte für das Studium.
+                        Entwickeln Sie ihre eigenen (Lehr-)Materialien für Studium oder die Lehre und teilen diese mit anderen Nutzenden.</translate></p>
+            <button class="button" @click="addElement">
+                <translate>Neues Lernmaterial anlegen</translate>
+            </button>
+        </div>
+    </div>
+    <studip-dialog
+        v-if="showOverviewElementAddDialog"
+        :title="$gettext('Neues Lernmaterial anlegen')"
+        height="600"
+        width="500"
+        :confirmText="'Erstellen'"
+        :confirmClass="'accept'"
+        :closeText="$gettext('Schließen')"
+        :closeClass="'cancel'"
+        class="cw-structural-element-dialog"
+        @close="closeAddDialog"
+        @confirm="createElement"
+    >
+        <template v-slot:dialogContent>
+
+                <courseware-collapsible-box
+                :title="$gettext('Grundeinstellungen')"
+                :open="true"
+                >
+                    <form class="default" @submit.prevent="">
+                        <label>
+                            <translate>Titel des Lernmaterials</translate><br />
+                            <input v-model="newElement.attributes.title" type="text" />
+                        </label>
+                        <label>
+                            <translate>Zusammenfassung</translate><br />
+                            <textarea v-model="newElement.attributes.payload.description"></textarea>
+                        </label>
+                        <label>
+                            <translate>Bild</translate>
+                            <br>
+                            <input ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" />
+                            <courseware-companion-box
+                                v-if="uploadFileError"
+                                :msgCompanion="uploadFileError"
+                                mood="sad"
+                                class="cw-companion-box-in-form"
+                            />
+                        </label>
+                        <label>
+                            <translate>Art des Lernmaterials</translate>
+                            <select v-model="newElementPurpose">
+                                <option value="content"><translate>Inhalt</translate></option>
+                                <option value="template"><translate>Aufgabenvorlage</translate></option>
+                                <option value="oer"><translate>OER-Material</translate></option>
+                                <option value="portfolio"><translate>ePortfolio</translate></option>
+                                <option value="draft"><translate>Entwurf</translate></option>
+                                <option value="other"><translate>Sonstiges</translate></option>
+                            </select>
+                        </label>
+                        <label>
+                            <translate>Lernmaterialvorlage</translate>
+                            <select v-model="newElementTemplate">
+                                <option :value="null"><translate>ohne Vorlage</translate></option>
+                                <option
+                                    v-for="template in selectableTemplates"
+                                    :key="template.id"
+                                    :value="template"
+                                >
+                                    {{ template.attributes.name }}
+                                </option>
+                            </select>
+                        </label>
+                    </form>
+                </courseware-collapsible-box>
+                <courseware-collapsible-box :title="$gettext('Vorschau')">
+                    <div v-if="currentTemplateStructure" class="cw-template-preview">
+                        <div
+                            class="cw-template-preview-container-wrapper"
+                            v-for="container in currentTemplateStructure.containers"
+                            :key="container.id"
+                            :class="['cw-template-preview-container-' + container.attributes.payload.colspan]"
+                        >
+                            <div class="cw-template-preview-container-content">
+                                <header class="cw-template-preview-container-title">
+                                    {{ container.attributes.title }} | {{ container.attributes.width }}
+                                </header>
+                                <div class="cw-template-preview-blocks" v-for="block in container.blocks" :key="block.id">
+                                    <header class="cw-template-preview-blocks-title">
+                                        {{ block.attributes.title }}
+                                    </header>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <courseware-companion-box
+                        v-else
+                        :msgCompanion="$gettext('Sie können eine Lernmaterialvorlage auswählen und hier eine Vorschau betrachten. Ohne Vorlage wird eine leere Seite erzeugt.')"
+                    />
+                </courseware-collapsible-box>
+                <courseware-collapsible-box
+                    :title="$gettext('Zusatzangaben')"
+                >
+                    <form class="default" @submit.prevent="">
+                        <label>
+                            <translate>Lizenztyp</translate>
+                            <select v-model="newElement.attributes.payload.license_type">
+                                <option v-for="license in licenses" :key="license.id" :value="license.id">
+                                    {{ license.name }}
+                                </option>
+                            </select>
+                        </label>
+                        <label>
+                            <translate>Geschätzter zeitlicher Aufwand</translate>
+                            <input type="text" v-model="newElement.attributes.payload.required_time" />
+                        </label>
+                        <label>
+                            <translate>Niveau</translate><br />
+                            <translate>von</translate>
+                            <select v-model="newElement.attributes.payload.difficulty_start">
+                                <option
+                                    v-for="difficulty_start in 12"
+                                    :key="difficulty_start"
+                                    :value="difficulty_start"
+                                >
+                                    {{ difficulty_start }}
+                                </option>
+                            </select>
+                            <translate>bis</translate>
+                            <select v-model="newElement.attributes.payload.difficulty_end">
+                                <option
+                                    v-for="difficulty_end in 12"
+                                    :key="difficulty_end"
+                                    :value="difficulty_end"
+                                >
+                                    {{ difficulty_end }}
+                                </option>
+                            </select>
+                        </label>
+                        <label>
+                            <translate>Farbe</translate>
+                            <v-select
+                                v-model="newElement.attributes.payload.color"
+                                :options="colors"
+                                :reduce="(color) => color.class"
+                                label="class"
+                                class="cw-vs-select"
+                            >
+                                <template #open-indicator="selectAttributes">
+                                    <span v-bind="selectAttributes"
+                                        ><studip-icon shape="arr_1down" size="10"
+                                    /></span>
+                                </template>
+                                <template #no-options="{ search, searching, loading }">
+                                    <translate>Es steht keine Auswahl zur Verfügung.</translate>
+                                </template>
+                                <template #selected-option="{ name, hex }">
+                                    <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                                    ><span>{{ name }}</span>
+                                </template>
+                                <template #option="{ name, hex }">
+                                    <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                                    ><span>{{ name }}</span>
+                                </template>
+                            </v-select>
+                        </label>
+                    </form>
+                </courseware-collapsible-box>
+
+        </template>
+    </studip-dialog>
+    <courseware-companion-overlay />
+</div>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
+import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
+import StudipDialog from '../StudipDialog.vue';
+
+export default {
+    name: 'courseware-content-overview-elements',
+    components: {
+        CoursewareCollapsibleBox,
+        CoursewareCompanionOverlay,
+        CoursewareCompanionBox,
+        StudipDialog
+    },
+    data() {
+        return {
+            text: {
+                emptyFilter: this.$gettext('Für diese Auswahl wurden keine Lernmaterialien gefunden.'),
+                empty: this.$gettext('Es wurden keine Lernmaterialien gefunden.'),
+            },
+            newElement: {
+                attributes: {
+                    payload: {},
+                },
+            },
+            newElementPurpose: 'content',
+            newElementTemplate: null,
+            uploadFileError: '',
+        }
+    },
+    computed: {
+        ...mapGetters({
+            getElement: 'courseware-structural-elements/byId',
+            licenses: 'licenses',
+            purposeFilter: 'purposeFilter',
+            showOverviewElementAddDialog: 'showOverviewElementAddDialog',
+            templates: 'courseware-templates/all',
+        }),
+        root() {
+            return this.getElement({id: STUDIP.COURSEWARE_USERS_ROOT_ID});
+        },
+        children() {
+            let view = this;
+            let children = [];
+            if(this.root?.relationships?.children?.data) {
+                this.root.relationships.children.data.forEach(function(child){
+                    let element = view.getElement({id: child.id});
+                    children.push(element);
+                });
+            }
+
+            return children;
+        },
+        filteredChildren() {
+            if (this.purposeFilter !== 'all') {
+                return this.children.filter(child => { return child.attributes.purpose === this.purposeFilter});
+            }
+            return this.children;
+        },
+        colors() {
+            const colors = [
+                {
+                    name: this.$gettext('Schwarz'),
+                    class: 'black',
+                    hex: '#000000',
+                    level: 100,
+                    icon: 'black',
+                    darkmode: true,
+                },
+                {
+                    name: this.$gettext('Weiß'),
+                    class: 'white',
+                    hex: '#ffffff',
+                    level: 100,
+                    icon: 'white',
+                    darkmode: false,
+                },
+
+                {
+                    name: this.$gettext('Blau'),
+                    class: 'studip-blue',
+                    hex: '#28497c',
+                    level: 100,
+                    icon: 'blue',
+                    darkmode: true,
+                },
+                {
+                    name: this.$gettext('Hellblau'),
+                    class: 'studip-lightblue',
+                    hex: '#e7ebf1',
+                    level: 40,
+                    icon: 'lightblue',
+                    darkmode: false,
+                },
+                {
+                    name: this.$gettext('Rot'),
+                    class: 'studip-red',
+                    hex: '#d60000',
+                    level: 100,
+                    icon: 'red',
+                    darkmode: false,
+                },
+                {
+                    name: this.$gettext('Grün'),
+                    class: 'studip-green',
+                    hex: '#008512',
+                    level: 100,
+                    icon: 'green',
+                    darkmode: true,
+                },
+                {
+                    name: this.$gettext('Gelb'),
+                    class: 'studip-yellow',
+                    hex: '#ffbd33',
+                    level: 100,
+                    icon: 'yellow',
+                    darkmode: false,
+                },
+                {
+                    name: this.$gettext('Grau'),
+                    class: 'studip-gray',
+                    hex: '#636a71',
+                    level: 100,
+                    icon: 'grey',
+                    darkmode: true,
+                },
+
+                {
+                    name: this.$gettext('Holzkohle'),
+                    class: 'charcoal',
+                    hex: '#3c454e',
+                    level: 100,
+                    icon: false,
+                    darkmode: true,
+                },
+                {
+                    name: this.$gettext('Königliches Purpur'),
+                    class: 'royal-purple',
+                    hex: '#8656a2',
+                    level: 80,
+                    icon: false,
+                    darkmode: true,
+                },
+                {
+                    name: this.$gettext('Leguangrün'),
+                    class: 'iguana-green',
+                    hex: '#66b570',
+                    level: 60,
+                    icon: false,
+                    darkmode: true,
+                },
+                {
+                    name: this.$gettext('Königin blau'),
+                    class: 'queen-blue',
+                    hex: '#536d96',
+                    level: 80,
+                    icon: false,
+                    darkmode: true,
+                },
+                {
+                    name: this.$gettext('Helles Seegrün'),
+                    class: 'verdigris',
+                    hex: '#41afaa',
+                    level: 80,
+                    icon: false,
+                    darkmode: true,
+                },
+                {
+                    name: this.$gettext('Maulbeere'),
+                    class: 'mulberry',
+                    hex: '#bf5796',
+                    level: 80,
+                    icon: false,
+                    darkmode: true,
+                },
+                {
+                    name: this.$gettext('Kürbis'),
+                    class: 'pumpkin',
+                    hex: '#f26e00',
+                    level: 100,
+                    icon: false,
+                    darkmode: true,
+                },
+                {
+                    name: this.$gettext('Sonnenschein'),
+                    class: 'sunglow',
+                    hex: '#ffca5c',
+                    level: 80,
+                    icon: false,
+                    darkmode: false,
+                },
+                {
+                    name: this.$gettext('Apfelgrün'),
+                    class: 'apple-green',
+                    hex: '#8bbd40',
+                    level: 80,
+                    icon: false,
+                    darkmode: true,
+                },
+            ];
+            let elementColors = [];
+            colors.forEach((color) => {
+                if (color.darkmode) {
+                    elementColors.push(color);
+                }
+            });
+
+            return elementColors;
+        },
+        selectableTemplates() {
+            return this.templates.filter(template => {
+                return template.attributes.purpose === this.newElementPurpose
+            });
+        },
+        currentTemplateStructure() {
+            if(this.newElementTemplate === null) {
+                return null;
+            }
+
+            return JSON.parse(this.newElementTemplate.attributes.structure);
+        }
+    },
+    methods: {
+        ...mapActions({
+            createStructuralElement: 'createStructuralElement',
+            createStructuralElementWithTemplate: 'createStructuralElementWithTemplate',
+            loadElement: 'courseware-structural-elements/loadById',
+            setShowOverviewElementAddDialog: 'setShowOverviewElementAddDialog',
+            uploadImageForStructuralElement: 'uploadImageForStructuralElement',
+            companionInfo: 'companionInfo',
+        }),
+        getChildStyle(child) {
+            let url = child.relationships?.image?.meta?.['download-url'];
+
+            if(url) {
+                return {'background-image': 'url(' + url + ')'};
+            } else {
+                return {};
+            }
+        },
+        hasImage(child) {
+            return child.relationships?.image?.data !== null;
+        },
+        getElementUrl(element_id) {
+            return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/courseware#/structural_element/' + element_id;
+        },
+        addElement() {
+            this.setShowOverviewElementAddDialog(true);
+        },
+        closeAddDialog() {
+            this.setShowOverviewElementAddDialog(false);
+            this.initNewElement();
+        },
+        async createElement() {
+            this.setShowOverviewElementAddDialog(false);
+            const file = this.$refs?.upload_image?.files[0];
+            this.newElement.attributes.purpose = this.newElementPurpose;
+            await this.createStructuralElementWithTemplate({
+                attributes: this.newElement.attributes,
+                templateId: this.newElementTemplate ? this.newElementTemplate.id : null,
+                parentId: this.root.id,
+                currentId: this.root.id,
+            });
+            let newStructuralElement = this.$store.getters['courseware-structural-elements/lastCreated'];
+
+            if (file) {
+                await this.uploadImageForStructuralElement({
+                    structuralElement: newStructuralElement,
+                    file,
+                }).catch((error) => {
+                    console.error(error);
+                    this.companionInfo({ info: this.$gettext('Das Bild für das neue Lernmaterial konnte nicht gespeichert werden.') });
+                });
+                this.loadElement({id: newStructuralElement.id, options: {include: 'children'}});
+            }
+            this.initNewElement();
+
+        },
+        initNewElement() {
+            this.newElement = {
+                attributes: {
+                    payload: {},
+                    purpose: '',
+                },
+                template: ''
+            };
+        },
+        countChildren(element) {
+            let data = element.relationships.children.data;
+            if (data) {
+                return data.length;
+            }
+            return 0;
+        },
+        checkUploadFile() {
+            const file = this.$refs?.upload_image?.files[0];
+            if (file.size > 2097152) {
+                this.uploadFileError = this.$gettext('Diese Datei ist zu groß. Bitte wählen Sie eine Datei aus, die kleiner als 2MB groß ist.');
+            } else if (!file.type.includes('image')) {
+                this.uploadFileError = this.$gettext('Diese Datei ist kein Bild. Bitte wählen Sie ein Bild aus.');
+            } else {
+                this.uploadFileError = '';
+            }
+        },
+    },
+    watch: {
+        root(newRootObject) {
+            let view = this;
+            if (newRootObject) {
+                newRootObject.relationships.children.data.forEach(function(child) {
+                    view.loadElement({id: child.id, options: {include: 'children'}});
+                });
+            }
+        },
+        newElementPurpose() {
+            this.newElementTemplate = null;
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue b/resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue
new file mode 100755
index 00000000000..3fd93ab5067
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue
@@ -0,0 +1,51 @@
+<template>
+    <select v-model="purposeFilter" class="sidebar-selectlist">
+        <option value="all">
+            <translate>alle</translate>
+        </option>
+        <option value="content">
+            <translate>Inhalt</translate>
+        </option>
+        <option value="template">
+            <translate>Aufgabenvorlage</translate>
+        </option>
+        <option value="oer">
+            <translate>OER-Material</translate>
+        </option>
+        <option value="portfolio">
+            <translate>ePortfolio</translate>
+        </option>
+        <option value="draft">
+            <translate>Entwurf</translate>
+        </option>
+        <option value="other">
+            <translate>Sonstiges</translate>
+        </option>
+    </select>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-content-overview-filter-widget',
+    data() {
+        return {
+            purposeFilter: 'all'
+        };
+    },
+    methods: {
+        ...mapActions({
+            setPurposeFilter: 'setPurposeFilter'
+        }),
+        filterPurpose() {
+            this.setPurposeFilter(this.purposeFilter);
+        }
+    },
+    watch: {
+        purposeFilter() {
+            this.filterPurpose();
+        }
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareCourseDashboard.vue b/resources/vue/components/courseware/CoursewareCourseDashboard.vue
index 15dc2930727..5f3b7ebed44 100755
--- a/resources/vue/components/courseware/CoursewareCourseDashboard.vue
+++ b/resources/vue/components/courseware/CoursewareCourseDashboard.vue
@@ -1,24 +1,40 @@
 <template>
-    <div class="cw-dashboard cw-course-dashboard">
-        <courseware-collapsible-box :title="$gettext('Ãœberblick')" :open="true" class="cw-dashboard-box cw-dashboard-box-full">
-            <div class="cw-dashboard-overview">
-                <courseware-oblong :name="textChapterFinished" :icon="'accept'" :size="'small'">
-                    <template v-slot:oblongValue> {{ chapterCounter.finished }} </template>
-                </courseware-oblong>
-                <courseware-oblong :name="textChapterStarted" :icon="'play'" :size="'small'">
-                    <template v-slot:oblongValue> {{ chapterCounter.started }} </template>
-                </courseware-oblong>
-                <courseware-oblong :name="textChapterAhead" :icon="'timetable'" :size="'small'">
-                    <template v-slot:oblongValue> {{ chapterCounter.ahead }} </template>
-                </courseware-oblong>
-            </div>
-        </courseware-collapsible-box>
-        <courseware-collapsible-box :title="$gettext('Fortschritt')" :open="true" class="cw-dashboard-box cw-dashboard-box-half">
-            <courseware-dashboard-progress />
-        </courseware-collapsible-box>
-        <courseware-collapsible-box :title="$gettext('Aktivitäten')" :open="true" class="cw-dashboard-box cw-dashboard-box-half">
-            <courseware-dashboard-activities :activitiesList="activitiesList"></courseware-dashboard-activities>
-        </courseware-collapsible-box>
+    <div class="cw-dashboard-wrapper">
+        <div v-if="defaultView" class="cw-dashboard cw-dashboard-default-view">
+            <courseware-collapsible-box :title="$gettext('Ãœberblick')" :open="true" class="cw-dashboard-box cw-dashboard-box-full">
+                <div class="cw-dashboard-overview">
+                    <courseware-oblong :name="textChapterFinished" icon="accept" size="small">
+                        <template v-slot:oblongValue> {{ chapterCounter.finished }} </template>
+                    </courseware-oblong>
+                    <courseware-oblong :name="textChapterStarted" icon="play" size="small">
+                        <template v-slot:oblongValue> {{ chapterCounter.started }} </template>
+                    </courseware-oblong>
+                    <courseware-oblong :name="textChapterAhead" icon="timetable" size="small">
+                        <template v-slot:oblongValue> {{ chapterCounter.ahead }} </template>
+                    </courseware-oblong>
+                </div>
+            </courseware-collapsible-box>
+            <courseware-collapsible-box :title="$gettext('Fortschritt')" :open="true" class="cw-dashboard-box cw-dashboard-box-half">
+                <courseware-dashboard-progress />
+            </courseware-collapsible-box>
+            <courseware-collapsible-box :title="$gettext('Aktivitäten')" :open="true" class="cw-dashboard-box cw-dashboard-box-half cw-content-loading">
+                <courseware-dashboard-activities />
+            </courseware-collapsible-box>
+            <courseware-collapsible-box :title="$gettext('Aufgaben')" :open="true" class="cw-dashboard-box cw-dashboard-box-full">
+                <courseware-dashboard-tasks v-if="!userIsTeacher && teacherStatusLoaded"/>
+                <courseware-dashboard-students v-if="userIsTeacher && teacherStatusLoaded" />
+            </courseware-collapsible-box>
+        </div>
+        <div v-if="taskView" class="cw-dashboard cw-dashboard-task-view">
+            <courseware-dashboard-tasks v-if="!userIsTeacher && teacherStatusLoaded"/>
+            <courseware-dashboard-students v-if="userIsTeacher && teacherStatusLoaded" />
+        </div>
+        <div v-if="activityView" class="cw-dashboard cw-dashboard-activity-view">
+            <courseware-collapsible-box :title="$gettext('Aktivitäten')" :open="true" class="cw-dashboard-box cw-dashboard-box-full cw-content-loading">
+                <courseware-dashboard-activities />
+            </courseware-collapsible-box>
+        </div>
+        <courseware-companion-overlay />
     </div>
 </template>
 
@@ -26,7 +42,11 @@
 import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
 import CoursewareDashboardProgress from './CoursewareDashboardProgress.vue';
 import CoursewareDashboardActivities from './CoursewareDashboardActivities.vue';
+import CoursewareDashboardTasks from './CoursewareDashboardTasks.vue'
+import CoursewareDashboardStudents from './CoursewareDashboardStudents.vue'
 import CoursewareOblong from './CoursewareOblong.vue';
+import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
+import { mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-course-dashboard',
@@ -35,6 +55,9 @@ export default {
         CoursewareOblong,
         CoursewareDashboardProgress,
         CoursewareDashboardActivities,
+        CoursewareDashboardTasks,
+        CoursewareDashboardStudents,
+        CoursewareCompanionOverlay
     },
     data() {
         return {
@@ -44,14 +67,27 @@ export default {
         };
     },
     computed: {
+        ...mapGetters({
+            dashboardViewMode: 'dashboardViewMode',
+            getCourseById: 'courses/byId',
+            getStructuralElementById: 'courseware-structural-elements/byId',
+            getUserById: 'users/byId',
+            teacherStatusLoaded: 'teacherStatusLoaded',
+            userId: 'userId',
+            userIsTeacher: 'userIsTeacher',
+        }),
         chapterCounter() {
             return STUDIP.courseware_chapter_counter;
         },
-
-        activitiesList() {
-            // todo in 5.1
-            return [];
+        defaultView() {
+            return this.dashboardViewMode === 'default';
         },
-    },
+        taskView() {
+            return this.dashboardViewMode === 'task';
+        },
+        activityView() {
+            return this.dashboardViewMode === 'activity';
+        },
+    }
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareCourseManager.vue b/resources/vue/components/courseware/CoursewareCourseManager.vue
index eea091f8e1e..aed709d1c02 100755
--- a/resources/vue/components/courseware/CoursewareCourseManager.vue
+++ b/resources/vue/components/courseware/CoursewareCourseManager.vue
@@ -1,143 +1,149 @@
 <template>
-    <div class="cw-course-manager">
-        <courseware-tabs class="cw-course-manager-tabs">
-            <courseware-tab :name="$gettext('Diese Courseware')" :selected="true">
-                <courseware-manager-element
-                    type="current"
-                    :currentElement="currentElement"
-                    @selectElement="setCurrentId"
-                    @reloadElement="reloadElements"
-                />
-            </courseware-tab>
-            <courseware-tab :name="$gettext('Export')">
-                <button
-                    class="button"
-                    @click.prevent="doExportCourseware"
-                    :class="{
-                        disabled: exportRunning,
-                    }"
-                >
-                    <translate>Alles exportieren</translate>
-                </button>
-                <courseware-companion-box v-show="exportRunning" :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')" mood="pointing"/>
-                <div v-if="exportRunning" class="cw-import-zip">
-                    <header>{{exportState}}:</header>
-                    <div class="progress-bar-wrapper">
-                        <div class="progress-bar" role="progressbar" :style="{width: exportProgress + '%'}" :aria-valuenow="exportProgress" aria-valuemin="0" aria-valuemax="100">{{ exportProgress }}%</div>
+    <div class="cw-course-manager-wrapper">
+        <div class="cw-course-manager">
+            <courseware-tabs class="cw-course-manager-tabs">
+                <courseware-tab :name="$gettext('Diese Courseware')" :selected="true">
+                    <courseware-manager-element
+                        type="current"
+                        :currentElement="currentElement"
+                        @selectElement="setCurrentId"
+                        @reloadElement="reloadElements"
+                    />
+                </courseware-tab>
+                <courseware-tab :name="$gettext('Export')">
+                    <button
+                        class="button"
+                        @click.prevent="doExportCourseware"
+                        :class="{
+                            disabled: exportRunning,
+                        }"
+                    >
+                        <translate>Alles exportieren</translate>
+                    </button>
+                    <courseware-companion-box v-show="exportRunning" :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')" mood="pointing"/>
+                    <div v-if="exportRunning" class="cw-import-zip">
+                        <header>{{exportState}}:</header>
+                        <div class="progress-bar-wrapper">
+                            <div class="progress-bar" role="progressbar" :style="{width: exportProgress + '%'}" :aria-valuenow="exportProgress" aria-valuemin="0" aria-valuemax="100">{{ exportProgress }}%</div>
+                        </div>
                     </div>
-                </div>
-            </courseware-tab>
-        </courseware-tabs>
+                </courseware-tab>
+            </courseware-tabs>
 
-        <courseware-tabs class="cw-course-manager-tabs">
-            <courseware-tab :name="$gettext('FAQ')">
-                <courseware-collapsible-box :open="true" :title="$gettext('Wie finde ich die gewünschte Stelle?')">
-                    <p><translate>
-                        Wählen Sie auf der linken Seite "Diese Courseware" aus.
-                        Beim laden der Seite ist dies immer gewählt. Die Überschrift
-                        gibt an welche Seite Sie grade ausgewählt haben. Darunter befinden
-                        sich die Abschnitte der Seite und innerhalb dieser dessen Blöcke.
-                        Möchten Sie eine Seite die unterhalb der gewählten liegt bearbeiten,
-                        können Sie diese über die Schaltflächen im Bereich "Seiten" wählen.
-                        Über der Überschrift wird eine Navigation eingeblendet, mit dieser können
-                        Sie beliebig weit hoch in der Hierarchie springen.
-                    </translate></p>
-                </courseware-collapsible-box>
-                <courseware-collapsible-box :title="$gettext('Wie sortiere ich Objekte?')">
-                    <p><translate>
-                        Seiten, Abschnitte und Blöcke lassen sich in ihrer Reihenfolge sortieren.
-                        Hierzu wählen Sie auf der linken Seite unter "Diese Courseware" die Schaltfläche "Seiten sortieren",
-                        "Abschnitte sortieren" oder "Blöcke sortieren".
-                        An den Objekten werden Pfeile angezeigt, mit diesen können die Objekte an die gewünschte
-                        Position gebracht werden. Um die neue Sortierung zu speichern wählen Sie "Sortieren beenden".
-                        Sie können die Änderungen auch rückgängig machen indem Sie "Sortieren abbrechen" wählen.
-                    </translate></p>
-                </courseware-collapsible-box>
-                <courseware-collapsible-box :title="$gettext('Wie verschiebe ich Objekte?')">
-                    <p><translate>
-                        Seiten, Abschnitte und Blöcke lassen sich verschieben.
-                        Hierzu wählen Sie auf der linken Seite unter "Diese Courseware" die Schaltfläche
-                        "Seite an diese Stelle einfügen", "Abschnitt an diese Stelle einfügen" oder
-                        "Block an diese Stelle einfügen". Wählen Sie dann auf der rechten Seite unter
-                        "Verschieben" das Objekt aus das Sie verschieben möchten. Verschiebbare Objekte
-                        erkennen Sie an den zwei nach links zeigenden gelben Pfeilen.
-                    </translate></p>
-                </courseware-collapsible-box>
-                <courseware-collapsible-box :title="$gettext('Wie kopiere ich Objekte?')">
-                    <p><translate>
-                        Seiten, Abschnitte und Blöcke lassen sich aus einer anderen Veranstaltung und Ihren
-                        eigenen Inhalten kopieren.
-                        Hierzu wählen Sie auf der linken Seite unter "Diese Courseware" die Schaltfläche
-                        "Seite an diese Stelle einfügen", "Abschnitt an diese Stelle einfügen" oder
-                        "Block an diese Stelle einfügen". Wählen Sie dann auf der rechten Seite unter
-                        "Kopieren" erst die Veranstaltung aus der Sie kopieren möchten oder Ihre eigenen
-                        Inhalte. Wählen sie dann das Objekt aus das Sie kopieren möchten. Kopierbare Objekte
-                        erkennen Sie an den zwei nach links zeigenden gelben Pfeilen.
-                    </translate></p>
-                </courseware-collapsible-box>
-            </courseware-tab>
-            <courseware-tab :name="$gettext('Verschieben')" :selected="true">
-                <courseware-manager-element
-                type="self"
-                :currentElement="selfElement"
-                :moveSelfPossible="moveSelfPossible"
-                :moveSelfChildPossible="moveSelfChildPossible"
-                @selectElement="setSelfId"
-                @reloadElement="reloadElements"
-                />
-            </courseware-tab>
+            <courseware-tabs class="cw-course-manager-tabs">
+                <courseware-tab :name="$gettext('FAQ')">
+                    <courseware-collapsible-box :open="true" :title="$gettext('Wie finde ich die gewünschte Stelle?')">
+                        <p><translate>
+                            Wählen Sie auf der linken Seite "Diese Courseware" aus.
+                            Beim laden der Seite ist dies immer gewählt. Die Überschrift
+                            gibt an, welche Seite Sie gerade ausgewählt haben. Darunter befinden
+                            sich die Abschnitte der Seite und innerhalb dieser deren Blöcke.
+                            Möchten Sie eine Seite, die unterhalb der gewählten liegt bearbeiten,
+                            können Sie diese über die Schaltflächen im Bereich "Seiten" wählen.
+                            Ãœber der Ãœberschrift wird eine Navigation eingeblendet, mit der Sie beliebig 
+                            weit hoch in der Hierarchie springen können.
+                        </translate></p>
+                    </courseware-collapsible-box>
+                    <courseware-collapsible-box :title="$gettext('Wie sortiere ich Objekte?')">
+                        <p><translate>
+                            Seiten, Abschnitte und Blöcke lassen sich in ihrer Reihenfolge sortieren.
+                            Hierzu wählen Sie auf der linken Seite unter "Diese Courseware" die Schaltfläche "Seiten sortieren",
+                            "Abschnitte sortieren" oder "Blöcke sortieren".
+                            An den Objekten werden Pfeile angezeigt, mit diesen können die Objekte an die gewünschte
+                            Position gebracht werden. Um die neue Sortierung zu speichern, wählen Sie "Sortieren beenden".
+                            Sie können die Änderungen auch rückgängig machen, indem Sie "Sortieren abbrechen" wählen.
+                        </translate></p>
+                    </courseware-collapsible-box>
+                    <courseware-collapsible-box :title="$gettext('Wie verschiebe ich Objekte?')">
+                        <p><translate>
+                            Seiten, Abschnitte und Blöcke lassen sich verschieben.
+                            Hierzu wählen Sie auf der linken Seite unter "Diese Courseware" die Schaltfläche
+                            "Seite an diese Stelle einfügen", "Abschnitt an diese Stelle einfügen" oder
+                            "Block an diese Stelle einfügen". Wählen Sie dann auf der rechten Seite unter
+                            "Verschieben" das Objekt aus, das Sie verschieben möchten. Verschiebbare Objekte
+                            erkennen Sie an den zwei nach links zeigenden gelben Pfeilen.
+                        </translate></p>
+                    </courseware-collapsible-box>
+                    <courseware-collapsible-box :title="$gettext('Wie kopiere ich Objekte?')">
+                        <p><translate>
+                            Seiten, Abschnitte und Blöcke lassen sich aus einer anderen Veranstaltung und Ihren
+                            eigenen Inhalten kopieren.
+                            Hierzu wählen Sie auf der linken Seite unter "Diese Courseware" die Schaltfläche
+                            "Seite an diese Stelle einfügen", "Abschnitt an diese Stelle einfügen" oder
+                            "Block an diese Stelle einfügen". Wählen Sie dann auf der rechten Seite unter
+                            "Kopieren" erst die Veranstaltung aus der Sie kopieren möchten oder Ihre eigenen
+                            Inhalte. Wählen sie dann das Objekt aus, das Sie kopieren möchten. Kopierbare Objekte
+                            erkennen Sie an den zwei nach links zeigenden gelben Pfeilen.
+                        </translate></p>
+                    </courseware-collapsible-box>
+                </courseware-tab>
+                <courseware-tab name="Verschieben" :selected="true">
+                    <courseware-manager-element
+                    type="self"
+                    :currentElement="selfElement"
+                    :moveSelfPossible="moveSelfPossible"
+                    :moveSelfChildPossible="moveSelfChildPossible"
+                    @selectElement="setSelfId"
+                    @reloadElement="reloadElements"
+                    />
+                </courseware-tab>
 
-            <courseware-tab :name="$gettext('Kopieren')">
-                <courseware-manager-copy-selector @loadSelf="reloadElements" @reloadElement="reloadElements" />
-            </courseware-tab>
+                <courseware-tab :name="$gettext('Kopieren')">
+                    <courseware-manager-copy-selector @loadSelf="reloadElements" @reloadElement="reloadElements" />
+                </courseware-tab>
 
-            <courseware-tab :name="$gettext('Importieren')">
-                <courseware-companion-box v-show="!importRunning && importDone" :msgCompanion="$gettext('Import erfolgreich!')" mood="special"/>
-                <courseware-companion-box v-show="importRunning" :msgCompanion="$gettext('Import läuft. Bitte verlassen Sie die Seite nicht bis der Import abgeschlossen wurde.')" mood="pointing"/>
-                <button
-                    v-show="!importRunning"
-                    class="button"
-                    @click.prevent="chooseFile"
-                >
-                    <translate>Importdatei auswählen</translate>
-                </button>
+                <courseware-tab :name="$gettext('Importieren')">
+                    <courseware-companion-box v-show="!importRunning && importDone" :msgCompanion="$gettext('Import erfolgreich!')" mood="special"/>
+                    <courseware-companion-box v-show="importRunning" :msgCompanion="$gettext('Import läuft. Bitte verlassen Sie die Seite nicht bis der Import abgeschlossen wurde.')" mood="pointing"/>
+                    <button
+                        v-show="!importRunning"
+                        class="button"
+                        @click.prevent="chooseFile"
+                    >
+                        <translate>Importdatei auswählen</translate>
+                    </button>
 
-                <div v-if="importZip" class="cw-import-zip">
-                    <header>{{ importZip.name }}</header>
-                    <p><translate>Größe</translate>: {{ getFileSizeText(importZip.size) }}</p>
-                </div>
+                    <div v-if="importZip" class="cw-import-zip">
+                        <header>{{ importZip.name }}</header>
+                        <p><translate>Größe</translate>: {{ getFileSizeText(importZip.size) }}</p>
+                    </div>
 
-                <div v-if="importRunning" class="cw-import-zip">
-                    <header><translate>Importiere Dateien</translate>:</header>
-                    <div class="progress-bar-wrapper">
-                        <div class="progress-bar" role="progressbar" :style="{width: importFilesProgress + '%'}" :aria-valuenow="importFilesProgress" aria-valuemin="0" aria-valuemax="100">{{ importFilesProgress }}%</div>
+                    <div v-if="importRunning" class="cw-import-zip">
+                        <header><translate>Importiere Dateien</translate>:</header>
+                        <div class="progress-bar-wrapper">
+                            <div class="progress-bar" role="progressbar" :style="{width: importFilesProgress + '%'}" :aria-valuenow="importFilesProgress" aria-valuemin="0" aria-valuemax="100">{{ importFilesProgress }}%</div>
+                        </div>
+                        {{ importFilesState }}
                     </div>
-                    {{ importFilesState }}
-                </div>
 
-                <div v-if="fileImportDone && importRunning" class="cw-import-zip">
-                    <header><translate>Importiere Elemente</translate>:</header>
-                    <div class="progress-bar-wrapper">
-                        <div class="progress-bar" role="progressbar" :style="{width: importStructuresProgress + '%'}" :aria-valuenow="importStructuresProgress" aria-valuemin="0" aria-valuemax="100">{{ importStructuresProgress }}%</div>
+                    <div v-if="fileImportDone && importRunning" class="cw-import-zip">
+                        <header><translate>Importiere Elemente</translate>:</header>
+                        <div class="progress-bar-wrapper">
+                            <div class="progress-bar" role="progressbar" :style="{width: importStructuresProgress + '%'}" :aria-valuenow="importStructuresProgress" aria-valuemin="0" aria-valuemax="100">{{ importStructuresProgress }}%</div>
+                        </div>
+                        {{ importStructuresState }}
                     </div>
-                    {{ importStructuresState }}
-                </div>
 
-                <button
-                    v-show="importZip && !importRunning"
-                    class="button"
-                    @click.prevent="doImportCourseware"
-                >
-                    <translate>Alles importieren</translate>
-                </button>
+                    <button
+                        v-show="importZip && !importRunning"
+                        class="button"
+                        @click.prevent="doImportCourseware"
+                    >
+                        <translate>Alles importieren</translate>
+                    </button>
 
-                <ul v-if="importErrors.length > 0">
-                    <li v-for="error in importErrors"> {{error}} </li>
-                </ul>
+                    <ul v-if="importErrors.length > 0">
+                        <li v-for="(index, error) in importErrors" :key="index"> {{error}} </li>
+                    </ul>
 
-                <input ref="importFile" type="file" accept=".zip" @change="setImport" style="visibility: hidden" />
-            </courseware-tab>
-        </courseware-tabs>
+                    <input ref="importFile" type="file" accept=".zip" @change="setImport" style="visibility: hidden" />
+                </courseware-tab>
+                <courseware-tab v-if="context.type === 'courses'" :name="$gettext('Aufgabe verteilen')">
+                    <courseware-manager-task-distributor />
+                </courseware-tab>
+            </courseware-tabs>
+        </div>
+        <courseware-companion-overlay />
     </div>
 </template>
 <script>
@@ -146,6 +152,8 @@ import CoursewareTab from './CoursewareTab.vue';
 import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
 import CoursewareManagerElement from './CoursewareManagerElement.vue';
 import CoursewareManagerCopySelector from './CoursewareManagerCopySelector.vue';
+import CoursewareManagerTaskDistributor from './CoursewareManagerTaskDistributor.vue';
+import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
 import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
 import CoursewareImport from '@/vue/mixins/courseware/import.js';
 import CoursewareExport from '@/vue/mixins/courseware/export.js';
@@ -162,7 +170,9 @@ export default {
         CoursewareCollapsibleBox,
         CoursewareManagerElement,
         CoursewareManagerCopySelector,
+        CoursewareCompanionOverlay,
         CoursewareCompanionBox,
+        CoursewareManagerTaskDistributor
     },
 
     mixins: [CoursewareImport, CoursewareExport],
@@ -183,6 +193,7 @@ export default {
     computed: {
         ...mapGetters({
             courseware: 'courseware',
+            context: 'context',
             structuralElementById: 'courseware-structural-elements/byId',
             importFilesState: 'importFilesState',
             importFilesProgress: 'importFilesProgress',
diff --git a/resources/vue/components/courseware/CoursewareDashboardActivities.vue b/resources/vue/components/courseware/CoursewareDashboardActivities.vue
index 3e9a8ee44a1..d068d3a8da7 100755
--- a/resources/vue/components/courseware/CoursewareDashboardActivities.vue
+++ b/resources/vue/components/courseware/CoursewareDashboardActivities.vue
@@ -1,19 +1,110 @@
 <template>
-    <ul class="cw-dashboard-activities">
-        <courseware-activity-item v-for="(item, index) in activitiesList" :key="index" :item="item" />
-    </ul>
+    <div class="cw-dashboard-activities-wrapper">
+        <span v-if="loading">
+            <div class="loading-indicator">
+                <span class="load-1"></span>
+                <span class="load-2"></span>
+                <span class="load-3"></span>
+            </div>
+        </span>
+        <courseware-companion-box
+            v-if="activitiesList.length === 0 && !loading"
+            mood="sad"
+            :msgCompanion="$gettext('Es wurden keine Aktivitäten gefunden.')"
+        />
+        <ul class="cw-dashboard-activities">
+            <courseware-activity-item v-for="(item, index) in activitiesList" :key="index" :item="item" />
+        </ul>
+    </div>
 </template>
 
 <script>
 import CoursewareActivityItem from './CoursewareActivityItem.vue';
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
+import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-dashboard-activities',
     components: {
         CoursewareActivityItem,
+        CoursewareCompanionBox,
     },
     props: {
-        activitiesList: Array,
     },
+    data() {
+        return {
+            activitiesList: [],
+            loading: false
+        }
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            getUserById: 'users/byId',
+            context: 'context',
+            getStructuralElementById: 'courseware-structural-elements/byId',
+        }),
+    },
+    created: function () {
+           this.getActivities();
+    },
+    methods: {
+        ...mapActions([
+            'loadCoursewareActivities'
+        ]),
+
+        async getActivities() {
+            this.loading = true;
+            let activities = await this.loadCoursewareActivities({ userId: this.userId, courseId: this.context.id});
+            this.activitiesList = [];
+
+            activities.forEach(activity => {
+                if(activity.type === 'activities') {
+                    let username = this.getUserById({ id: activity.relationships.actor.data.id }).attributes['formatted-name'];
+                    const date = new Date(activity.attributes.mkdate);
+                    const activityStructuralElement = this.getStructuralElementById({ id: activity.relationships.object.meta["object-id"] });
+
+                    let breadcrumb = activityStructuralElement.attributes.title;
+                    let completeBreadcrumb = activityStructuralElement.attributes.title;
+                    let currentStructuralElement = activityStructuralElement;
+                    if (currentStructuralElement === undefined) {
+                        return;
+                    }
+                    let i = 1; //max breadcrumb navigation depth check
+                    while (currentStructuralElement.relationships.parent.data !== null) {
+                        currentStructuralElement = this.getStructuralElementById({ id: currentStructuralElement.relationships.parent.data.id });
+                        if (currentStructuralElement === undefined) {
+                            break;
+                        }
+                        completeBreadcrumb = currentStructuralElement.attributes.title + '/' + completeBreadcrumb;
+                        
+                        if(++i <= 3) {
+                            breadcrumb = currentStructuralElement.attributes.title + '/' + breadcrumb;
+                            
+                            if(i == 3) {
+                                breadcrumb = '.../' + breadcrumb;
+                            }
+                        }
+                    }
+                    let options = { year: 'numeric', month: '2-digit', day: '2-digit' };
+                    let data = {
+                        username: username,
+                        date: date.toLocaleString('de-DE', options),
+                        type: activity.attributes.verb,
+                        text: activity.attributes.title,
+                        complete_breadcrumb: completeBreadcrumb,
+                        element_breadcrumb: breadcrumb,
+                        element_id: activity.relationships.object.meta["object-id"],
+                        context_id: activity.relationships.context.data.id,
+                        content: activity.attributes.content
+                    }
+
+                    this.activitiesList.push(data);
+                }
+            });
+
+            this.loading = false;
+        }
+    }
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareDashboardProgress.vue b/resources/vue/components/courseware/CoursewareDashboardProgress.vue
index e011c889195..0c32d825ea5 100755
--- a/resources/vue/components/courseware/CoursewareDashboardProgress.vue
+++ b/resources/vue/components/courseware/CoursewareDashboardProgress.vue
@@ -27,8 +27,11 @@
                 :chapterId="chapter.id"
                 @selectChapter="selectChapter"
             />
-            <div v-if="!children.length">
-                <translate>Dieses Seite enthält keine darunter liegenden Seiten</translate>
+            <div v-if="!children.length" class="cw-dashboard-empty-info">
+                <courseware-companion-box 
+                    mood="sad"
+                    :msgCompanion="$gettext('Diese Seite enthält keine darunter liegenden Seiten.')"
+                />
             </div>
         </div>
     </div>
@@ -36,12 +39,14 @@
 
 <script>
 import StudipIcon from '../StudipIcon.vue';
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
 import CoursewareDashboardProgressItem from './CoursewareDashboardProgressItem.vue';
 import CoursewareProgressCircle from './CoursewareProgressCircle.vue';
 
 export default {
     name: 'courseware-dashboard-progress',
     components: {
+        CoursewareCompanionBox,
         CoursewareDashboardProgressItem,
         CoursewareProgressCircle,
         StudipIcon,
diff --git a/resources/vue/components/courseware/CoursewareDashboardStudents.vue b/resources/vue/components/courseware/CoursewareDashboardStudents.vue
new file mode 100755
index 00000000000..4e49327716b
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareDashboardStudents.vue
@@ -0,0 +1,377 @@
+<template>
+    <div class="cw-dashboard-students-wrapper">
+        <table v-if="tasks.length > 0" class="default">
+            <colgroup>
+                <col />
+            </colgroup>
+            <thead>
+                <tr>
+                    <th><translate>Status</translate></th>
+                    <th><translate>Aufgabentitel</translate></th>
+                    <th><translate>Teilnehmende/Gruppen</translate></th>
+                    <th><translate class="responsive-hidden">Seite</translate></th>
+                    <th><translate>bearbeitet</translate></th>
+                    <th><translate>Abgabefrist</translate></th>
+                    <th><translate>Abgabe</translate></th>
+                    <th class="responsive-hidden renewal"><translate>Verlängerungsanfrage</translate></th>
+                    <th class="responsive-hidden feedback"><translate>Feedback</translate></th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-for="{ task, taskGroup, status, element, user, group, feedback } in tasks" :key="task.id">
+                    <td>
+                        <studip-icon
+                            v-if="status.shape !== undefined"
+                            :shape="status.shape"
+                            :role="status.role"
+                            :title="status.description"
+                        />
+                    </td>
+                    <td>
+                        {{ taskGroup && taskGroup.attributes.title }}
+                    </td>
+                    <td>
+                        <span v-if="user">
+                            <studip-icon shape="person2" role="info" :title="$gettext('Teilnehmer/-in')" />
+                            {{ user.attributes['formatted-name'] }}
+                        </span>
+                        <span v-if="group">
+                            <studip-icon shape="group2" role="info" :title="$gettext('Gruppe')" />
+                            {{ group.attributes['name'] }}
+                        </span>
+                    </td>
+                    <td class="responsive-hidden">
+                        <a v-if="task.attributes.submitted" :href="getLinkToElement(element.id)">
+                            {{ element.attributes.title }}
+                        </a>
+                        <span v-else>{{ element.attributes.title }}</span>
+                    </td>
+                    <td>{{ task.attributes.progress }}%</td>
+                    <td>{{ getReadableDate(task.attributes['submission-date']) }}</td>
+                    <td>
+                        <studip-icon v-if="task.attributes.submitted" shape="accept" role="status-green" />
+                    </td>
+                    <td class="responsive-hidden">
+                        <button
+                            v-show="task.attributes.renewal === 'pending'"
+                            class="button"
+                            @click="solveRenewalRequest(task)"
+                        >
+                            <translate>Anfrage bearbeiten</translate>
+                        </button>
+                        <span v-show="task.attributes.renewal === 'declined'">
+                            <studip-icon shape="decline" role="status-red" />
+                            <translate>Anfrage abgelehnt</translate>
+                        </span>
+                        <span v-show="task.attributes.renewal === 'granted'">
+                            <translate>verlängert bis</translate>:
+                            {{ getReadableDate(task.attributes['renewal-date']) }}
+                        </span>
+                        <studip-icon
+                            v-if="task.attributes.renewal === 'declined' || task.attributes.renewal === 'granted'"
+                            :title="$gettext('Anfrage bearbeiten')"
+                            class="edit"
+                            shape="edit"
+                            role="clickable"
+                            @click="solveRenewalRequest(task)"
+                        />
+                    </td>
+                    <td class="responsive-hidden">
+                        <span
+                            v-if="feedback"
+                            :title="
+                                $gettext('Feedback geschrieben am:') +
+                                ' ' +
+                                getReadableDate(feedback.attributes['chdate'])
+                            "
+                        >
+                            <studip-icon shape="accept" role="status-green" />
+                            <translate>Feedback gegeben</translate>
+                            <studip-icon
+                                :title="$gettext('Feedback bearbeiten')"
+                                class="edit"
+                                shape="edit"
+                                role="clickable"
+                                @click="editFeedback(feedback)"
+                            />
+                        </span>
+
+                        <button
+                            v-show="!feedback && task.attributes.submitted"
+                            class="button"
+                            @click="addFeedback(task)"
+                        >
+                            <translate>Feedback geben</translate>
+                        </button>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+        <div v-else>
+            <courseware-companion-box 
+                mood="pointing"
+                :msgCompanion="
+                    $gettext('Es wurden bisher keine Aufgaben gestellt.') + '<br>' + 
+                    $gettext('Wenn Sie eine Aufgabe stellen möchten, nutzen Sie bitte in der Verwaltung der Courseware die Funktion &quot;Aufgabe verteilen&quot;.')
+                "
+            >
+                <template v-slot:companionActions>
+                <a class="button" :href="managerUrl"><translate>Zur Verwaltung</translate></a>
+            </template>
+            </courseware-companion-box>
+        </div>
+        <studip-dialog
+            v-if="showRenewalDialog"
+            :title="text.renewalDialog.title"
+            :confirmText="text.renewalDialog.confirm"
+            :confirmClass="'accept'"
+            :closeText="text.renewalDialog.close"
+            :closeClass="'cancel'"
+            height="350"
+            @close="
+                showRenewalDialog = false;
+                currentDialogTask = {};
+            "
+            @confirm="updateRenewal"
+        >
+            <template v-slot:dialogContent>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        <translate>Fristverlängerung</translate>
+                        <select v-model="currentDialogTask.attributes.renewal">
+                            <option value="declined">
+                                <translate>ablehnen</translate>
+                            </option>
+                            <option value="granted">
+                                <translate>gewähren</translate>
+                            </option>
+                        </select>
+                    </label>
+                    <label v-if="currentDialogTask.attributes.renewal === 'granted'">
+                        <translate>neue Frist</translate>
+                        <courseware-date-input v-model="currentDialogTask.attributes['renewal-date']" class="size-l" />
+                    </label>
+                </form>
+            </template>
+        </studip-dialog>
+        <studip-dialog
+            v-if="showEditFeedbackDialog"
+            :title="text.editFeedbackDialog.title"
+            :confirmText="text.editFeedbackDialog.confirm"
+            :confirmClass="'accept'"
+            :closeText="text.editFeedbackDialog.close"
+            :closeClass="'cancel'"
+            height="420"
+            @close="
+                showEditFeedbackDialog = false;
+                currentDialogFeedback = {};
+            "
+            @confirm="updateFeedback"
+        >
+            <template v-slot:dialogContent>
+                <courseware-companion-box
+                    v-if="currentDialogFeedback.attributes.content === ''"
+                    mood="pointing"
+                    :msgCompanion="
+                        $gettext('Sie haben kein Feedback geschrieben, beim Speichern wird dieses Feedback gelöscht!')
+                    "
+                />
+                <form class="default" @submit.prevent="">
+                    <label>
+                        <translate>Feedback</translate>
+                        <textarea v-model="currentDialogFeedback.attributes.content" />
+                    </label>
+                </form>
+            </template>
+        </studip-dialog>
+        <studip-dialog
+            v-if="showAddFeedbackDialog"
+            :title="text.addFeedbackDialog.title"
+            :confirmText="text.addFeedbackDialog.confirm"
+            :confirmClass="'accept'"
+            :closeText="text.addFeedbackDialog.close"
+            :closeClass="'cancel'"
+            @close="
+                showAddFeedbackDialog = false;
+                currentDialogFeedback = {};
+            "
+            @confirm="createFeedback"
+        >
+            <template v-slot:dialogContent>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        <translate>Feedback</translate>
+                        <textarea v-model="currentDialogFeedback.attributes.content" />
+                    </label>
+                </form>
+            </template>
+        </studip-dialog>
+    </div>
+</template>
+
+<script>
+import StudipIcon from './../StudipIcon.vue';
+import StudipDialog from './../StudipDialog.vue';
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
+import CoursewareDateInput from './CoursewareDateInput.vue';
+import taskHelperMixin from '../../mixins/courseware/task-helper.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-dashboard-students',
+    mixins: [taskHelperMixin],
+    components: {
+        CoursewareCompanionBox,
+        CoursewareDateInput,
+        StudipIcon,
+        StudipDialog,
+    },
+    data() {
+        return {
+            showRenewalDialog: false,
+            showAddFeedbackDialog: false,
+            showEditFeedbackDialog: false,
+            currentDialogTask: {},
+            currentDialogFeedback: {},
+            text: {
+                renewalDialog: {
+                    title: this.$gettext('Verlängerungsanfrage bearbeiten'),
+                    confirm: this.$gettext('Speichern'),
+                    close: this.$gettext('Schließen'),
+                },
+                editFeedbackDialog: {
+                    title: this.$gettext('Feedback zur Aufgabe ändern'),
+                    confirm: this.$gettext('Speichern'),
+                    close: this.$gettext('Schließen'),
+                },
+                addFeedbackDialog: {
+                    title: this.$gettext('Feedback zur Aufgabe geben'),
+                    confirm: this.$gettext('Speichern'),
+                    close: this.$gettext('Schließen'),
+                },
+            },
+        };
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            allTasks: 'courseware-tasks/all',
+            userById: 'users/byId',
+            statusGroupById: 'status-groups/byId',
+            getElementById: 'courseware-structural-elements/byId',
+            getFeedbackById: 'courseware-task-feedback/byId',
+            relatedTaskGroups: 'courseware-task-groups/related',
+        }),
+        tasks() {
+            return this.allTasks.map((task) => {
+                const result = {
+                    task,
+                    taskGroup: this.relatedTaskGroups({ parent: task, relationship: 'task-group' }),
+                    status: this.getStatus(task),
+                    element: this.getElementById({ id: task.relationships['structural-element'].data.id }),
+                    user: null,
+                    group: null,
+                    feedback: null,
+                };
+                let solver = task.relationships.solver.data;
+                if (solver.type === 'users') {
+                    result.user = this.userById({ id: solver.id });
+                }
+                if (solver.type === 'status-groups') {
+                    result.group = this.statusGroupById({ id: solver.id });
+                }
+
+                const feedbackId = task.relationships['task-feedback'].data?.id;
+                if (feedbackId) {
+                    result.feedback = this.getFeedbackById({ id: feedbackId });
+                }
+
+                return result;
+            });
+        },
+        managerUrl() {
+            return STUDIP.URLHelper.getURL('dispatch.php/course/courseware/manager', {cid: this.context.id});
+        }
+    },
+    methods: {
+        ...mapActions({
+            updateTask: 'updateTask',
+            createTaskFeedback: 'createTaskFeedback',
+            updateTaskFeedback: 'updateTaskFeedback',
+            deleteTaskFeedback: 'deleteTaskFeedback',
+            loadRemoteCoursewareStructure: 'loadRemoteCoursewareStructure',
+            copyStructuralElement: 'copyStructuralElement',
+            companionSuccess: 'companionSuccess',
+            companionError: 'companionError',
+        }),
+        addFeedback(task) {
+            this.currentDialogFeedback.attributes = {};
+            this.currentDialogFeedback.attributes.content = '';
+            this.currentDialogFeedback.relationships = {};
+            this.currentDialogFeedback.relationships.task = {};
+            this.currentDialogFeedback.relationships.task.data = {};
+            this.currentDialogFeedback.relationships.task.data.id = task.id;
+            this.currentDialogFeedback.relationships.task.data.type = task.type;
+            this.showAddFeedbackDialog = true;
+        },
+        createFeedback() {
+            if (this.currentDialogFeedback.attributes.content === '') {
+                this.companionError({
+                    info: this.$gettext('Bitte schreiben Sie ein Feedback.'),
+                });
+                return false;
+            }
+            this.showAddFeedbackDialog = false;
+            this.createTaskFeedback({
+                taskFeedback: this.currentDialogFeedback,
+            });
+            this.currentDialogFeedback = {};
+        },
+        editFeedback(feedback) {
+            this.currentDialogFeedback = _.cloneDeep(feedback);
+            this.showEditFeedbackDialog = true;
+        },
+        async updateFeedback() {
+            this.showEditFeedbackDialog = false;
+            let attributes = {};
+            attributes.content = this.currentDialogFeedback.attributes.content;
+            if (attributes.content === '') {
+                await this.deleteTaskFeedback({
+                    taskFeedbackId: this.currentDialogFeedback.id,
+                });
+                this.companionSuccess({
+                    info: this.$gettext('Feedback wurde gelöscht.'),
+                });
+            } else {
+                await this.updateTaskFeedback({
+                    attributes: attributes,
+                    taskFeedbackId: this.currentDialogFeedback.id,
+                });
+                this.companionSuccess({
+                    info: this.$gettext('Feedback wurde gespeichert.'),
+                });
+            }
+
+            this.currentDialogFeedback = {};
+        },
+        solveRenewalRequest(task) {
+            this.currentDialogTask = _.cloneDeep(task);
+            this.showRenewalDialog = true;
+        },
+        updateRenewal() {
+            this.showRenewalDialog = false;
+            let attributes = {};
+            attributes.renewal = this.currentDialogTask.attributes.renewal;
+            if (attributes.renewal === 'granted') {
+                attributes['renewal-date'] = new Date(this.currentDialogTask.attributes['renewal-date']).toISOString();
+            }
+
+            this.updateTask({
+                attributes: attributes,
+                taskId: this.currentDialogTask.id,
+            });
+            this.currentDialogTask = {};
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/CoursewareDashboardTasks.vue b/resources/vue/components/courseware/CoursewareDashboardTasks.vue
new file mode 100755
index 00000000000..02cc37e2525
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareDashboardTasks.vue
@@ -0,0 +1,264 @@
+<template>
+    <div class="cw-dashboard-tasks-wrapper">
+        <table v-if="tasks.length > 0" class="default">
+            <colgroup>
+                <col />
+            </colgroup>
+            <thead>
+                <tr>
+                    <th><translate>Status</translate></th>
+                    <th class="responsive-hidden"><translate>Aufgabentitel</translate></th>
+                    <th><translate>Seite</translate></th>
+                    <th><translate>bearbeitet</translate></th>
+                    <th><translate>Abgabefrist</translate></th>
+                    <th><translate>Abgabe</translate></th>
+                    <th class="responsive-hidden"><translate>Verlängerungsanfrage</translate></th>
+                    <th class="responsive-hidden"><translate>Feedback</translate></th>
+                    <th><translate>Aktionen</translate></th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-for="{ task, taskGroup, status, element, feedback } in tasks" :key="task.id">
+                    <td>
+                        <studip-icon
+                            v-if="status.shape !== undefined"
+                            :shape="status.shape"
+                            :role="status.role"
+                            :title="status.description"
+                        />
+                    </td>
+                    <td class="responsive-hidden">
+                        <studip-icon
+                            v-if="task.attributes['solver-type'] === 'group'"
+                            shape="group2"
+                            role="info"
+                            :title="$gettext('Gruppenaufgabe')"
+                        />
+                        {{ taskGroup.attributes.title }}
+                    </td>
+                    <td>
+                        <a :href="getLinkToElement(element.id)">{{ element.attributes.title }}</a>
+                    </td>
+                    <td>{{ task.attributes.progress }}%</td>
+                    <td>{{ getReadableDate(task.attributes['submission-date']) }}</td>
+                    <td>
+                        <studip-icon v-if="task.attributes.submitted" shape="accept" role="status-green" />
+                    </td>
+                    <td class="responsive-hidden">
+                        <span v-show="task.attributes.renewal === 'declined'">
+                            <studip-icon shape="decline" role="status-red" />
+                            <translate>Anfrage abgelehnt</translate>
+                        </span>
+                        <span v-show="task.attributes.renewal === 'pending'">
+                            <studip-icon shape="date" role="status-yellow" />
+                            <translate>Anfrage wird bearbeitet</translate>
+                        </span>
+                        <span v-show="task.attributes.renewal === 'granted'">
+                            <translate>verlängert bis</translate>: {{getReadableDate(task.attributes['renewal-date'])}}
+                        </span>
+                    </td>
+                    <td class="responsive-hidden">
+                        <studip-icon
+                            v-if="feedback"
+                            :title="$gettext('Feedback anzeigen')"
+                            class="display-feedback"
+                            shape="consultation"
+                            role="clickable"
+                            @click="displayFeedback(feedback)"
+                        />
+                    </td>
+                    <td class="actions">
+                        <studip-action-menu
+                            :items="getTaskMenuItems(task, status)"
+                            @submitTask="displaySubmitDialog(task)"
+                            @renewalRequest="renewalRequest(task)"
+                            @copyContent="copyContent(element)"
+                        />
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+        <div v-else>
+            <courseware-companion-box 
+                mood="sad"
+                :msgCompanion="$gettext('Es wurden bisher keine Aufgaben gestellt.')"
+            />
+        </div>
+        <studip-dialog
+            v-if="showFeedbackDialog"
+            :message="currentTaskFeedback"
+            :title="text.feedbackDialog.title"
+            @close="
+                showFeedbackDialog = false;
+                currentTaskFeedback = '';
+            "
+        />
+        <studip-dialog
+            v-if="showSubmitDialog"
+            :title="text.submitDialog.title"
+            :question="text.submitDialog.question"
+            height="200"
+            width="420"
+            @confirm="submitTask"
+            @close="closeSubmitDialog"
+        />
+    </div>
+</template>
+<script>
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
+import StudipIcon from './../StudipIcon.vue';
+import StudipActionMenu from './../StudipActionMenu.vue';
+import StudipDialog from './../StudipDialog.vue';
+import taskHelperMixin from '../../mixins/courseware/task-helper.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-dashboard-tasks',
+    mixins: [taskHelperMixin],
+    components: {
+        CoursewareCompanionBox,
+        StudipIcon,
+        StudipActionMenu,
+        StudipDialog,
+    },
+    data() {
+        return {
+            showFeedbackDialog: false,
+            showSubmitDialog: false,
+            currentTask: null,
+            currentTaskFeedback: '',
+            text: {
+                feedbackDialog: {
+                    title: this.$gettext('Feedback'),
+                },
+                submitDialog: {
+                    title: this.$gettext('Aufgabe abgeben'),
+                    question: this.$gettext(
+                        'Änderungen sind nach Abgabe nicht mehr möglich. Möchten Sie diese Aufgabe jetzt wirklich abgeben?'
+                    ),
+                },
+            },
+        };
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            allTasks: 'courseware-tasks/all',
+            userId: 'userId',
+            userById: 'users/byId',
+            statusGroupById: 'status-groups/byId',
+            getElementById: 'courseware-structural-elements/byId',
+            getFeedbackById: 'courseware-task-feedback/byId',
+            getTaskGroupById: 'courseware-task-groups/byId',
+        }),
+        tasks() {
+            return this.allTasks.map((task) => {
+                const result = {
+                    task,
+                    taskGroup: this.getTaskGroupById({ id: task.relationships['task-group'].data.id }),
+                    status: this.getStatus(task),
+                    element: this.getElementById({ id: task.relationships['structural-element'].data.id }),
+                    feedback: null,
+                };
+                const feedbackId = task.relationships['task-feedback'].data?.id;
+                if (feedbackId) {
+                    result.feedback = this.getFeedbackById({ id: feedbackId });
+                }
+
+                return result;
+            });
+        },
+    },
+    methods: {
+        ...mapActions({
+            updateTask: 'updateTask',
+            loadRemoteCoursewareStructure: 'loadRemoteCoursewareStructure',
+            copyStructuralElement: 'copyStructuralElement',
+            companionSuccess: 'companionSuccess',
+            companionError: 'companionError',
+        }),
+        getTaskMenuItems(task, status) {
+            let menuItems = [];
+            if (!task.attributes.submitted && status.canSubmit) {
+                menuItems.push({ id: 1, label: this.$gettext('Aufgabe abgeben'), icon: 'service', emit: 'submitTask' });
+            }
+
+            if (!task.attributes.submitted && !task.attributes.renewal) {
+                menuItems.push({
+                    id: 2,
+                    label: this.$gettext('Verlängerung beantragen'),
+                    icon: 'date',
+                    emit: 'renewalRequest',
+                });
+            }
+            if (task.attributes.submitted) {
+                menuItems.push({ id: 3, label: this.$gettext('Inhalt kopieren'), icon: 'export', emit: 'copyContent' });
+            }
+
+            return menuItems;
+        },
+        async renewalRequest(task) {
+            let attributes = {};
+            attributes.renewal = 'pending';
+            await this.updateTask({
+                attributes: attributes,
+                taskId: task.id,
+            });
+            this.companionSuccess({
+                info: this.$gettext('Ihre Anfrage wurde eingereicht.'),
+            });
+        },
+        displaySubmitDialog(task) {
+            this.showSubmitDialog = true;
+            this.currentTask = task;
+        },
+        closeSubmitDialog() {
+            this.showSubmitDialog = false;
+            this.currentTask = null;
+        },
+        async submitTask() {
+            this.showSubmitDialog = false;
+            let attributes = {};
+            attributes.submitted = true;
+            await this.updateTask({
+                attributes: attributes,
+                taskId: this.currentTask.id,
+            });
+            this.companionSuccess({
+                info:
+                    '"' +
+                    this.currentTask.attributes.title +
+                    '" ' +
+                    this.$gettext('wurde erfolgreich abgegeben.'),
+            });
+            this.currentTask = null;
+        },
+        async copyContent(element) {
+            let ownCoursewareInstance = await this.loadRemoteCoursewareStructure({
+                rangeId: this.userId,
+                rangeType: 'users',
+            });
+            if (ownCoursewareInstance !== null) {
+                await this.copyStructuralElement({
+                    parentId: ownCoursewareInstance.relationships.root.data.id,
+                    element: element,
+                    removeType: true,
+                });
+                this.companionSuccess({
+                    info: this.$gettext('Die Inhalte wurden zu Ihren persönlichen Lernmaterialien hinzugefügt.'),
+                });
+            } else {
+                this.companionError({
+                    info: this.$gettext(
+                        'Die Inhalte konnten nicht zu Ihren persönlichen Lernmaterialien hinzugefügt werden.'
+                    ),
+                });
+            }
+        },
+        displayFeedback(feedback) {
+            this.showFeedbackDialog = true;
+            this.currentTaskFeedback = feedback.attributes.content;
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/CoursewareDashboardViewWidget.vue b/resources/vue/components/courseware/CoursewareDashboardViewWidget.vue
new file mode 100755
index 00000000000..4b72b5b9f8a
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareDashboardViewWidget.vue
@@ -0,0 +1,59 @@
+<template>
+    <ul class="widget-list widget-links sidebar-views cw-view-widget">
+        <li
+            :class="{ active: defaultView }"
+            @click="setDefaultView"
+        >
+            <translate>Standard</translate>
+        </li>
+        <li
+            :class="{ active: taskView }"
+            @click="setTaskView"
+        >
+            <translate>Aufgaben</translate>
+        </li>
+        <li 
+            :class="{ active: activityView }"
+            @click="setActivityView"
+        >
+            <translate>Aktivitäten</translate>
+        </li>
+    </ul>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-dashboard-view-widget',
+    computed: {
+        ...mapGetters({
+            dashboardViewMode: 'dashboardViewMode',
+            context: 'context',
+        }),
+        defaultView() {
+            return this.dashboardViewMode === 'default';
+        },
+        taskView() {
+            return this.dashboardViewMode === 'task';
+        },
+        activityView() {
+            return this.dashboardViewMode === 'activity';
+        },
+    },
+    methods: {
+        ...mapActions({
+            setDashboardViewMode: 'setDashboardViewMode'
+        }),
+        setDefaultView() {
+            this.setDashboardViewMode('default');
+        },
+        setTaskView() {
+            this.setDashboardViewMode('task');
+        },
+        setActivityView() {
+            this.setDashboardViewMode('activity');
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/CoursewareDateInput.vue b/resources/vue/components/courseware/CoursewareDateInput.vue
new file mode 100644
index 00000000000..57a5619fbee
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareDateInput.vue
@@ -0,0 +1,34 @@
+<template>
+    <input :value="formattedDate" @input="onInput" type="date" />
+</template>
+
+<script>
+const fromISO8601 = (string) => new Date(string);
+const toISO8601 = (date) => date.toISOString();
+const pad = (what, length = 2) => `00000000${what}`.substr(-length);
+
+export default {
+    props: ['value'],
+    data: () => ({
+        date: new Date(),
+    }),
+    computed: {
+        formattedDate() {
+            return `${this.date.getFullYear()}-${pad(this.date.getMonth() + 1)}-${pad(this.date.getDate())}`;
+        },
+    },
+    methods: {
+        onInput({ target }) {
+            const newValue = toISO8601(target.valueAsDate);
+            if (newValue !== this.value) {
+                this.$emit('input', newValue);
+            }
+        },
+    },
+    beforeMount() {
+        if (this.value) {
+            this.date = fromISO8601(this.value);
+        }
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/CoursewareDefaultBlock.vue
index 02d2a228c10..458739901c6 100755
--- a/resources/vue/components/courseware/CoursewareDefaultBlock.vue
+++ b/resources/vue/components/courseware/CoursewareDefaultBlock.vue
@@ -2,6 +2,7 @@
     <div v-if="block.attributes.visible || canEdit" class="cw-default-block">
         <div class="cw-content-wrapper" :class="[showEditMode ? 'cw-content-wrapper-active' : '']">
             <header v-if="showEditMode" class="cw-block-header">
+                <span class="cw-sortable-handle"></span>
                 <span v-if="!block.attributes.visible" class="cw-default-block-invisible-info">
                     <studip-icon shape="visibility-invisible" />
                 </span>
@@ -14,8 +15,6 @@
                     :canEdit="canEdit"
                     :deleteOnly="deleteOnly"
                     @editBlock="displayFeature('Edit')"
-                    @showFeedback="displayFeature('Feedback')"
-                    @showComments="displayFeature('Comments')"
                     @showInfo="displayFeature('Info')"
                     @showExportOptions="displayFeature('ExportOptions')"
                     @deleteBlock="displayDeleteDialog()"
@@ -25,21 +24,6 @@
                 <slot name="content" />
             </div>
             <div v-if="showFeatures" class="cw-block-features cw-block-features-default">
-                <courseware-block-feedback
-                    v-if="canEdit && showFeedback"
-                    :block="block"
-                    :canEdit="canEdit"
-                    :isTeacher="isTeacher"
-                    @close="displayFeature(false)"
-                />
-                <courseware-block-comments
-                    v-if="showComments"
-                    :block="block"
-                    :comments="currentComments"
-                    @postComment="updateComments"
-                    @close="displayFeature(false)"
-                    ref="comments"
-                />
                 <courseware-block-export-options
                     v-if="canEdit && showExportOptions"
                     :block="block"
@@ -62,6 +46,12 @@
                 </courseware-block-info>
             </div>
         </div>
+        <div v-if="discussView" class="cw-discuss-wrapper">
+            <courseware-block-discussion
+                :block="block"
+                :canEdit="canEdit"
+            />
+        </div>
         <studip-dialog
             v-if="showDeleteDialog"
             :title="textDeleteTitle"
@@ -81,10 +71,13 @@ import CoursewareBlockExportOptions from './CoursewareBlockExportOptions.vue';
 import CoursewareBlockFeedback from './CoursewareBlockFeedback.vue';
 import CoursewareBlockInfo from './CoursewareBlockInfo.vue';
 import CoursewareBlockActions from './CoursewareBlockActions.vue';
+import CoursewareTabs from './CoursewareTabs.vue';
+import CoursewareTab from './CoursewareTab.vue';
 import StudipDialog from '../StudipDialog.vue';
 import StudipIcon from '../StudipIcon.vue';
 import { blockMixin } from './block-mixin.js';
 import { mapActions, mapGetters } from 'vuex';
+import CoursewareBlockDiscussion from './CoursewareBlockDiscussion.vue';
 
 export default {
     name: 'courseware-default-block',
@@ -96,8 +89,11 @@ export default {
         CoursewareBlockFeedback,
         CoursewareBlockActions,
         CoursewareBlockInfo,
+        CoursewareTabs,
+        CoursewareTab,
         StudipDialog,
         StudipIcon,
+        CoursewareBlockDiscussion,
     },
     props: {
         block: Object,
@@ -111,13 +107,11 @@ export default {
         defaultGrade: {
             type: Boolean,
             default: true,
-        },
+        }
     },
     data() {
         return {
             showFeatures: false,
-            showFeedback: false,
-            showComments: false,
             showExportOptions: false,
             showEdit: false,
             showInfo: false,
@@ -134,7 +128,6 @@ export default {
             blockTypes: 'blockTypes',
             userId: 'userId',
             viewMode: 'viewMode',
-            getComments: 'courseware-block-comments/related',
             containerById: 'courseware-containers/byId',
         }),
         showEditMode() {
@@ -144,6 +137,9 @@ export default {
             }
             return show;
         },
+        discussView() {
+            return this.viewMode === 'discuss';
+        },
         blocked() {
             return this.block?.relationships['edit-blocker'].data !== null;
         },
@@ -178,7 +174,6 @@ export default {
             deleteBlock: 'deleteBlockInContainer',
             lockObject: 'lockObject',
             unlockObject: 'unlockObject',
-            loadComments: 'courseware-block-comments/loadRelated',
             loadContainer: 'loadContainer',
             updateContainer: 'updateContainer',
         }),
@@ -187,8 +182,6 @@ export default {
                 return false;
             }
             this.showFeatures = false;
-            this.showFeedback = false;
-            this.showComments = false;
             this.showExportOptions = false;
             this.showEdit = false;
             this.showInfo = false;
@@ -208,7 +201,6 @@ export default {
 
                             return false;
                         }
-
                         if (!this.preview) {
                             this.showContent = false;
                         }
@@ -229,10 +221,6 @@ export default {
                     this['show' + element] = true;
                     this.showFeatures = true;
                 }
-
-                if (element === 'Comments') {
-                    this.loadComments();
-                }
             }
         },
         async closeEdit() {
@@ -285,26 +273,6 @@ export default {
                 containerId: containerId,
             });
         },
-
-        async loadComments() {
-            const parent = {
-                type: this.block.type,
-                id: this.block.id,
-            };
-            await this.$store.dispatch('courseware-block-comments/loadRelated', {
-                parent,
-                relationship: 'comments',
-                options: {
-                    include: 'user',
-                },
-            });
-
-            this.currentComments = await this.getComments({ parent, relationship: 'comments' });
-        },
-        async updateComments() {
-            await this.loadComments();
-            this.$refs.comments.$refs.comments.scrollTo(0, this.$refs.comments.$refs.comments.scrollHeight);
-        },
     },
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareDefaultBlockElements.vue b/resources/vue/components/courseware/CoursewareDefaultBlockElements.vue
index c798366cd7d..17d8834ee1e 100755
--- a/resources/vue/components/courseware/CoursewareDefaultBlockElements.vue
+++ b/resources/vue/components/courseware/CoursewareDefaultBlockElements.vue
@@ -4,8 +4,6 @@
             :block="block"
             :canEdit="canEdit"
             @editBlock="editBlock"
-            @showFeedback="showFeedback"
-            @showComments="showComments"
             @showExportOptions="showExportOptions"
         />
         <courseware-block-feedback v-if="canEdit" :block="block" :canEdit="canEdit" :isTeacher="isTeacher" />
@@ -43,8 +41,6 @@ export default {
         editBlock() {
             this.$emit('editBlock');
         },
-        showFeedback() {},
-        showComments() {},
         showExportOptions() {},
     },
 };
diff --git a/resources/vue/components/courseware/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/CoursewareDefaultContainer.vue
index 53acc421c2c..edc46207e01 100755
--- a/resources/vue/components/courseware/CoursewareDefaultContainer.vue
+++ b/resources/vue/components/courseware/CoursewareDefaultContainer.vue
@@ -11,6 +11,7 @@
                     :container="container"
                     @editContainer="displayEditDialog"
                     @deleteContainer="displayDeleteDialog"
+                    @sortBlocks="sortBlocks"
                 />
             </header>
             <div class="cw-block-wrapper" :class="{ 'cw-block-wrapper-active': showEditMode }">
@@ -103,6 +104,7 @@ export default {
             deleteContainer: 'deleteContainer',
             lockObject: 'lockObject',
             unlockObject: 'unlockObject',
+            companionInfo: 'companionInfo',
         }),
         async displayEditDialog() {
             if (this.blockedByAnotherUser) {
@@ -152,6 +154,25 @@ export default {
             }
             this.showDeleteDialog = false;
         },
+        async sortBlocks() {
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
+
+                return false;
+            }
+            try {
+                await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
+            } catch(error) {
+                if (error.status === 409) {
+                    this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
+                } else {
+                    console.log(error);
+                }
+
+                return false;
+            }
+            this.$emit('sortBlocks');
+        }
     },
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareHeadlineBlock.vue b/resources/vue/components/courseware/CoursewareHeadlineBlock.vue
index 349ac9545c2..ab203ed62cc 100755
--- a/resources/vue/components/courseware/CoursewareHeadlineBlock.vue
+++ b/resources/vue/components/courseware/CoursewareHeadlineBlock.vue
@@ -69,7 +69,7 @@
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
                             </template>
                             <template #no-options="{ search, searching, loading }">
-                                <translate>Es steht keine Auswahl zur Verfügung</translate>.
+                                <translate>Es steht keine Auswahl zur Verfügung.</translate>
                             </template>
                             <template #selected-option="{name, hex}">
                                 <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
@@ -86,7 +86,7 @@
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
                             </template>
                             <template #no-options="{ search, searching, loading }">
-                                <translate>Es steht keine Auswahl zur Verfügung</translate>.
+                                <translate>Es steht keine Auswahl zur Verfügung.</translate>
                             </template>
                             <template #selected-option="option">
                                 <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
@@ -110,7 +110,7 @@
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
                             </template>
                             <template #no-options="{ search, searching, loading }">
-                                <translate>Es steht keine Auswahl zur Verfügung</translate>.
+                                <translate>Es steht keine Auswahl zur Verfügung.</translate>
                             </template>
                             <template #selected-option="{name, hex}">
                                 <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
@@ -141,7 +141,7 @@
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
                             </template>
                             <template #no-options="{ search, searching, loading }">
-                                <translate>Es steht keine Auswahl zur Verfügung</translate>.
+                                <translate>Es steht keine Auswahl zur Verfügung.</translate>
                             </template>
                             <template #selected-option="{name, hex}">
                                 <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
diff --git a/resources/vue/components/courseware/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/CoursewareImageMapBlock.vue
index 082e2ac9ef3..d79443fb244 100755
--- a/resources/vue/components/courseware/CoursewareImageMapBlock.vue
+++ b/resources/vue/components/courseware/CoursewareImageMapBlock.vue
@@ -72,7 +72,7 @@
                                         <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
                                     </template>
                                     <template #no-options="{ search, searching, loading }">
-                                        <translate>Es steht keine Auswahl zur Verfügung</translate>.
+                                        <translate>Es steht keine Auswahl zur Verfügung.</translate>
                                     </template>
                                     <template #selected-option="{name, rgba}">
                                         <span class="vs__option-color" :style="{'background-color': rgba}"></span><span>{{name}}</span>
diff --git a/resources/vue/components/courseware/CoursewareKeyPointBlock.vue b/resources/vue/components/courseware/CoursewareKeyPointBlock.vue
index 7c57f2a6e5c..d8474fb99a3 100755
--- a/resources/vue/components/courseware/CoursewareKeyPointBlock.vue
+++ b/resources/vue/components/courseware/CoursewareKeyPointBlock.vue
@@ -41,7 +41,7 @@
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
                             </template>
                             <template #no-options="{ search, searching, loading }">
-                                <translate>Es steht keine Auswahl zur Verfügung</translate>.
+                                <translate>Es steht keine Auswahl zur Verfügung.</translate>
                             </template>
                             <template #selected-option="{name, hex}">
                                 <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
@@ -59,7 +59,7 @@
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
                             </template>
                             <template #no-options="{ search, searching, loading }">
-                                <translate>Es steht keine Auswahl zur Verfügung</translate>.
+                                <translate>Es steht keine Auswahl zur Verfügung.</translate>
                             </template>
                             <template #selected-option="option">
                                 <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
diff --git a/resources/vue/components/courseware/CoursewareListContainer.vue b/resources/vue/components/courseware/CoursewareListContainer.vue
index ed60f824e74..8e8506aedc1 100755
--- a/resources/vue/components/courseware/CoursewareListContainer.vue
+++ b/resources/vue/components/courseware/CoursewareListContainer.vue
@@ -5,14 +5,37 @@
         :canEdit="canEdit"
         :isTeacher="isTeacher"
         @storeContainer="storeContainer"
+        @sortBlocks="enableSort"
     >
         <template v-slot:containerContent>
-            <ul class="cw-container-list-block-list">
+            <ul v-if="!sortMode"  class="cw-container-list-block-list">
                 <li v-for="block in blocks" :key="block.id" class="cw-block-item">
                     <component :is="component(block)" :block="block" :canEdit="canEdit" :isTeacher="isTeacher" />
                 </li>
-                <li v-if="showEditMode && canEdit"><courseware-block-adder-area :container="container" :section="0" /></li>
+                <li v-if="showEditMode && canEdit && canAddElements"><courseware-block-adder-area :container="container" :section="0" /></li>
             </ul>
+            <draggable
+                v-if="sortMode && canEdit"
+                class="cw-container-list-block-list cw-container-list-sort-mode"
+                tag="ul"
+                v-model="blockList"
+                v-bind="dragOptions"
+                handle=".cw-sortable-handle"
+                @start="isDragging = true"
+                @end="isDragging = false"
+            >
+                <transition-group type="transition" name="flip-blocks">
+                    <li v-for="block in blockList" :key="block.id" class="cw-block-item cw-block-item-sortable">
+                        <component :is="component(block)" :block="block" :canEdit="canEdit" :isTeacher="isTeacher" />
+                    </li>
+                </transition-group>
+            
+            </draggable>
+            <div v-if="sortMode && canEdit">
+                <button class="button accept" @click="storeSort"><translate>Sortierung speichern</translate></button>
+                <button class="button cancel"  @click="resetSort"><translate>Sortieren abbrechen</translate></button>
+            </div>
+
         </template>
     </courseware-default-container>
 </template>
@@ -20,19 +43,33 @@
 <script>
 import ContainerComponents from './container-components.js';
 import containerMixin from '../../mixins/courseware/container.js';
-import { mapGetters } from 'vuex';
+import draggable from 'vuedraggable';
+import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-list-container',
     mixins: [containerMixin],
-    components: ContainerComponents,
+    components: Object.assign(ContainerComponents, {
+        draggable
+    }),
     props: {
         container: Object,
         canEdit: Boolean,
         isTeacher: Boolean,
+        canAddElements: Boolean,
     },
     data() {
-        return {};
+        return {
+            sortMode: false,
+            isDragging: false,
+            dragOptions: {
+                animation: 0,
+                group: "description",
+                disabled: false,
+                ghostClass: "block-ghost"
+            },
+            blockList: [],
+        };
     },
     computed: {
         ...mapGetters({
@@ -50,8 +87,37 @@ export default {
         },
     },
     methods: {
+        ...mapActions({
+            updateContainer: 'updateContainer',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+        }),
         storeContainer(data) {
         },
+        initCurrentData() {
+            this.blockList = this.blocks;
+        },
+        enableSort() {
+            this.initCurrentData();
+            this.sortMode = true;
+        },
+        async storeSort() {
+            this.sortMode = false;
+
+            let currentContainer = this.container;
+            currentContainer.attributes.payload.sections[0].blocks = this.blockList.map(block => {return block.id});
+            await this.updateContainer({
+                container: currentContainer,
+                structuralElementId: currentContainer.relationships['structural-element'].data.id,
+            });
+            await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            this.initCurrentData();
+        },
+        async resetSort() {
+            await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            this.sortMode = false;
+            this.blockList = this.blocks;
+        },
         component(block) {
             if (block.attributes["block-type"] !== undefined) {
                 return 'courseware-' + block.attributes["block-type"] + '-block';
@@ -59,6 +125,8 @@ export default {
             return null;
         },
     },
-    mounted() {},
+    mounted() {
+        this.initCurrentData();
+    },
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareManagerTaskDistributor.vue b/resources/vue/components/courseware/CoursewareManagerTaskDistributor.vue
new file mode 100755
index 00000000000..839bd7b3c63
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareManagerTaskDistributor.vue
@@ -0,0 +1,316 @@
+<template>
+    <div class="cw-manager-task-distributor">
+        <form class="default" @submit.prevent="">
+            <fieldset>
+                <legend><translate>Aufgabe</translate></legend>
+                <label>
+                    <translate>Aufgabentitel</translate>
+                    <input type="text" v-model="taskTitle" />
+                </label>
+                <label>
+                    <translate>Aufgabenvorlage</translate>
+                    <select v-model="selectedElementId">
+                        <option value="" disabled>
+                            <translate>wählen Sie eine Vorlage aus</translate>
+                        </option>
+                        <option v-for="template in taskTemplates" :key="template.id" :value="template.id">
+                            {{ template.attributes.title }}
+                        </option>
+                    </select>
+                </label>
+                <label>
+                    <translate>Abgabefrist</translate>
+                    <input type="date" v-model="submissionDate" />
+                </label>
+                <label>
+                    <translate>Inhalte ergänzen</translate>
+                    <select class="size-s" v-model="solverMayAddBlocks">
+                        <option value="true"><translate>ja</translate></option>
+                        <option value="false"><translate>nein</translate></option>
+                    </select>
+                </label>
+                <label>
+                    <translate>Type</translate>
+                    <select v-model="taskSolverType">
+                        <option value="autor"><translate>für Studierende</translate></option>
+                        <option value="group"><translate>für Gruppen</translate></option>
+                    </select>
+                </label>
+            </fieldset>
+            <fieldset v-show="taskSolverType === 'autor'" class="cw-manager-task-distributor-task-solvers">
+                <legend><translate>Studierende</translate></legend>
+                <courseware-companion-box
+                    v-show="autor_members.length === 0"
+                    :msgCompanion="$gettext('Es wurden keine Studierenden in dieser Veranstaltung gefunden.')"
+                    mood="pointing"
+                />
+                <table v-show="autor_members.length > 0" class="default">
+                    <thead>
+                        <tr>
+                            <th><translate>Name</translate></th>
+                            <th><translate>Aufgabe zuweisen</translate></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr v-for="user in autor_members" :key="user.user_id">
+                            <td>{{ user.formattedname }}</td>
+                            <td><input type="checkbox" v-model="selectedAutors" :value="user.user_id" /></td>
+                        </tr>
+                    </tbody>
+                </table>
+            </fieldset>
+            <fieldset v-show="taskSolverType === 'group'" class="cw-manager-task-distributor-task-solvers">
+                <legend><translate>Gruppen</translate></legend>
+                <courseware-companion-box
+                    v-show="groups.length === 0"
+                    :msgCompanion="$gettext('Es wurden keine Gruppen in dieser Veranstaltung gefunden.')"
+                    mood="pointing"
+                />
+                <table v-show="groups.length > 0" class="default">
+                    <thead>
+                        <tr>
+                            <th><translate>Gruppenname</translate></th>
+                            <th><translate>Aufgabe zuweisen</translate></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr v-for="group in groups" :key="group.id">
+                            <td>{{ group.name }}</td>
+                            <td><input type="checkbox" v-model="selectedGroups" :value="group.id" /></td>
+                        </tr>
+                    </tbody>
+                </table>
+            </fieldset>
+            <footer>
+                <button class="button" name="create_task" :disabled="!targetSelected" @click="createTask">
+                    <translate>Aufgabe verteilen</translate>
+                </button>
+            </footer>
+        </form>
+    </div>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
+
+export default {
+    name: 'courseware-manager-task-distributor',
+    components: {
+        CoursewareCompanionBox,
+    },
+    data() {
+        return {
+            ownCoursewareInstance: null,
+            ownCoursewareElements: [],
+            taskSolverType: 'autor',
+            selectedElementId: '',
+            selectedAutors: [],
+            selectedGroups: [],
+            taskTitle: '',
+            submissionDate: '',
+            solverMayAddBlocks: true,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            userId: 'userId',
+            structuralElementById: 'courseware-structural-elements/byId',
+            relatedCourseMemberships: 'course-memberships/related',
+            relatedCourseStatusGroups: 'status-groups/related',
+            relatedUser: 'users/related',
+            filingData: 'filingData',
+        }),
+        users() {
+            const parent = { type: 'courses', id: this.context.id };
+            const relationship = 'memberships';
+            const memberships = this.relatedCourseMemberships({ parent, relationship });
+
+            return (
+                memberships?.map((membership) => {
+                    const parent = { type: membership.type, id: membership.id };
+                    const member = this.relatedUser({ parent, relationship: 'user' });
+
+                    return {
+                        user_id: member.id,
+                        formattedname: member.attributes['formatted-name'],
+                        username: member.attributes['username'],
+                        perm: membership.attributes['permission'],
+                    };
+                }) ?? []
+            );
+        },
+        groups() {
+            const parent = { type: 'courses', id: this.context.id };
+            const relationship = 'status-groups';
+            const statusGroups = this.relatedCourseStatusGroups({ parent, relationship });
+
+            return (
+                statusGroups?.map((statusGroup) => {
+                    return {
+                        id: statusGroup.id,
+                        name: statusGroup.attributes['name'],
+                    };
+                }) ?? []
+            );
+        },
+        autor_members() {
+            if (Object.keys(this.users).length === 0 && this.users.constructor === Object) {
+                return [];
+            }
+
+            let members = this.users
+                .filter(function (user) {
+                    return user.perm === 'autor';
+                })
+                .map((obj) => ({ ...obj, active: false }));
+
+            return members;
+        },
+        ownCoursewareRootId() {
+            if (this.ownCoursewareInstance !== null) {
+                return this.ownCoursewareInstance.relationships.root.data.id;
+            } else {
+                return '';
+            }
+        },
+        ownCoursewareRoot() {
+            if (this.ownCoursewareRootId !== '') {
+                return this.structuralElementById({ id: this.ownCoursewareRootId });
+            } else {
+                return null;
+            }
+        },
+        taskTemplates() {
+            let templates = this.ownCoursewareElements.filter((elem) => {
+                return elem.attributes.purpose === 'template';
+            });
+
+            return templates;
+        },
+        targetSelected() {
+            return this.filingData.itemType === 'element';
+        },
+    },
+    methods: {
+        ...mapActions({
+            loadCourseMemberships: 'course-memberships/loadRelated',
+            loadCourseStatusGroups: 'status-groups/loadRelated',
+            loadRemoteCoursewareStructure: 'loadRemoteCoursewareStructure',
+            loadStructuralElementById: 'courseware-structural-elements/loadById',
+            loadStructuralElement: 'loadStructuralElement',
+            createTaskGroup: 'createTaskGroup',
+            companionWarning: 'companionWarning',
+            companionSuccess: 'companionSuccess',
+        }),
+        async loadOwnCourseware() {
+            this.ownCoursewareInstance = await this.loadRemoteCoursewareStructure({
+                rangeId: this.userId,
+                rangeType: 'users',
+            });
+            await this.loadStructuralElementById({ id: this.ownCoursewareRootId, options: { include: 'children' } });
+            let children = this.ownCoursewareRoot.relationships.children.data;
+            for (let i = 0; i < children.length; i++) {
+                this.ownCoursewareElements.push(this.structuralElementById({ id: children[i].id }));
+            }
+        },
+        async createTask() {
+            if (!this.targetSelected) {
+                return;
+            }
+
+            if (this.taskTitle === '') {
+                this.companionWarning({
+                    info: this.$gettext('Bitte wählen Sie einen Aufgabentitel aus.'),
+                });
+
+                return false;
+            }
+            if (this.selectedElementId.trim() === '') {
+                this.companionWarning({
+                    info: this.$gettext('Bitte wählen Sie eine Aufgabenvorlage aus.'),
+                });
+
+                return false;
+            }
+            if (this.submissionDate === '') {
+                this.companionWarning({
+                    info: this.$gettext('Bitte wählen Sie eine Abgabefrist aus.'),
+                });
+
+                return false;
+            }
+            if (!['autor', 'group'].includes(this.taskSolverType)) {
+                this.companionWarning({
+                    info: this.$gettext('Bitte wählen Sie aus, an wen die Aufgabe verteilt werden sollen.'),
+                });
+
+                return false;
+            }
+            if (this.taskSolverType === 'autor') {
+                if (this.selectedAutors.length === 0) {
+                    this.companionWarning({
+                        info: this.$gettext('Bitte wählen Sie mindestens einen Studierenden aus.'),
+                    });
+                    return false;
+                }
+            }
+            if (this.taskSolverType === 'group') {
+                if (this.selectedGroups.length === 0) {
+                    this.companionWarning({
+                        info: this.$gettext('Bitte wählen Sie mindestens eine Gruppe aus.'),
+                    });
+                    return false;
+                }
+            }
+
+            const taskGroup = {
+                attributes: {
+                    title: this.taskTitle,
+                    'submission-date': new Date(this.submissionDate).toISOString(),
+                    'solver-may-add-blocks': this.solverMayAddBlocks,
+                },
+                relationships: {
+                    solvers: {
+                        data: [],
+                    },
+                    target: {
+                        data: {
+                            id: this.filingData.parentItem.id,
+                            type: 'courseware-structural-elements',
+                        },
+                    },
+                    'task-template': {
+                        data: {
+                            id: this.selectedElementId,
+                            type: 'courseware-structural-elements',
+                        },
+                    },
+                },
+            };
+
+            let solvers;
+            if (this.taskSolverType === 'autor') {
+                solvers = this.selectedAutors.map((id) => ({ type: 'users', id }));
+            }
+            if (this.taskSolverType === 'group') {
+                solvers = this.selectedGroups.map((id) => ({ type: 'status-groups', id }));
+            }
+            taskGroup.relationships.solvers.data = solvers;
+
+            await this.createTaskGroup({ taskGroup });
+
+            this.companionSuccess({
+                info: this.$gettext('Aufgaben wurden verteilt.'),
+            });
+        },
+    },
+    mounted() {
+        const parent = { type: 'courses', id: this.context.id };
+        this.loadCourseMemberships({ parent, relationship: 'memberships', options: { include: 'user' } });
+        this.loadCourseStatusGroups({ parent, relationship: 'status-groups' });
+        this.loadOwnCourseware();
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/CoursewareOblong.vue b/resources/vue/components/courseware/CoursewareOblong.vue
index e3a905141a0..b1e601dea8a 100755
--- a/resources/vue/components/courseware/CoursewareOblong.vue
+++ b/resources/vue/components/courseware/CoursewareOblong.vue
@@ -4,7 +4,7 @@
             <slot name="oblongValue"></slot>
         </div>
         <div class="cw-oblong-description">
-            <studip-icon v-if="icon" :shape="icon" :size="24"></studip-icon>{{ name }}
+            <studip-icon v-if="icon" :shape="icon" role="info" :size="iconSize"></studip-icon>{{ name }}
         </div>
     </div>
 </template>
@@ -29,6 +29,16 @@ export default {
                     return 'small';
             }
         },
+        iconSize() {
+            switch (this.size) {
+                case 'large':
+                    return 48;
+                case 'small':
+                    return 24;
+                default:
+                    return 24;
+            }
+        },
     },
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue
index b67001327e3..090f37a6149 100755
--- a/resources/vue/components/courseware/CoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue
@@ -6,7 +6,7 @@
             v-if="validContext"
         >
             <div class="cw-structural-element-content" v-if="structuralElement">
-                <courseware-ribbon :canEdit="canEdit">
+                <courseware-ribbon :canEdit="canEdit && canAddElements">
                     <template #buttons>
                         <router-link v-if="prevElement" :to="'/structural_element/' + prevElement.id">
                             <button class="cw-ribbon-button cw-ribbon-button-prev" :title="textRibbon.perv" />
@@ -26,7 +26,7 @@
                         >
                             <span>
                                 <router-link :to="'/structural_element/' + ancestor.id">
-                                    {{ ancestor.attributes.title }}
+                                    {{ ancestor.attributes.title || "–" }}
                                 </router-link>
                             </span>
                         </li>
@@ -34,7 +34,7 @@
                             class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current"
                             :title="structuralElement.attributes.title"
                         >
-                            <span>{{ structuralElement.attributes.title }}</span>
+                            <span>{{ structuralElement.attributes.title || "–" }}</span>
                         </li>
                     </template>
                     <template #breadcrumbFallback>
@@ -57,38 +57,79 @@
                             @showExportOptions="menuAction('showExportOptions')"
                             @oerCurrentElement="menuAction('oerCurrentElement')"
                             @setBookmark="menuAction('setBookmark')"
+                            @sortContainers="menuAction('sortContainers')"
+                            @pdfExport="menuAction('pdfExport')"
                         />
                     </template>
                 </courseware-ribbon>
 
                 <div
-                    v-if="canVisit"
+                    v-if="canVisit && !sortMode"
                     class="cw-container-wrapper"
-                    :class="{ 'cw-container-wrapper-consume': consumeMode }"
+                    :class="{
+                        'cw-container-wrapper-consume': consumeMode,
+                        'cw-container-wrapper-discuss': discussView,
+                    }"
                 >
                     <div v-if="structuralElementLoaded" class="cw-companion-box-wrapper">
                         <courseware-empty-element-box
-                            v-if="
-                                (empty && !isRoot && canEdit) ||
-                                (empty && !canEdit) ||
-                                (!noContainers && empty && isRoot && canEdit)
-                            "
+                            v-if="showEmptyElementBox"
                             :canEdit="canEdit"
                             :noContainers="noContainers"
                         />
                         <courseware-wellcome-screen v-if="noContainers && isRoot && canEdit" />
                     </div>
+                    <courseware-structural-element-discussion
+                        v-if="!noContainers && discussView"
+                        :structuralElement="structuralElement"
+                        :canEdit="canEdit"
+                    />
                     <component
                         v-for="container in containers"
                         :key="container.id"
                         :is="containerComponent(container)"
                         :container="container"
                         :canEdit="canEdit"
-                        :isTeacher="isTeacher"
+                        :canAddElements="canAddElements"
+                        :isTeacher="userIsTeacher"
                         class="cw-container-item"
                     />
                 </div>
-                <div v-else class="cw-container-wrapper" :class="{ 'cw-container-wrapper-consume': consumeMode }">
+                <div v-if="canVisit && canEdit && sortMode" class="cw-container-wrapper-sort-mode">
+                    <draggable
+                        class="cw-structural-element-list-sort-mode"
+                        tag="ul"
+                        v-model="containerList"
+                        v-bind="dragOptions"
+                        handle=".cw-sortable-handle"
+                        @start="isDragging = true"
+                        @end="isDragging = false"
+                    >
+                        <transition-group type="transition" name="flip-containers">
+                            <li
+                                v-for="container in containerList"
+                                :key="container.id"
+                                class="cw-container-item-sortable"
+                            >
+                                <span class="cw-sortable-handle"></span>
+                                <span>{{ container.attributes.title }} ({{ container.attributes.width }})</span>
+                            </li>
+                        </transition-group>
+                    </draggable>
+                    <div class="cw-container-sort-buttons">
+                        <button class="button accept" @click="storeSort">
+                            <translate>Sortierung speichern</translate>
+                        </button>
+                        <button class="button cancel" @click="resetSort">
+                            <translate>Sortieren abbrechen</translate>
+                        </button>
+                    </div>
+                </div>
+                <div
+                    v-if="!canVisit"
+                    class="cw-container-wrapper"
+                    :class="{ 'cw-container-wrapper-consume': consumeMode }"
+                >
                     <div v-if="structuralElementLoaded" class="cw-companion-box-wrapper">
                         <courseware-companion-box
                             mood="sad"
@@ -147,7 +188,7 @@
                                             /></span>
                                         </template>
                                         <template #no-options="{ search, searching, loading }">
-                                            <translate>Es steht keine Auswahl zur Verfügung</translate>.
+                                            <translate>Es steht keine Auswahl zur Verfügung.</translate>
                                         </template>
                                         <template #selected-option="{ name, hex }">
                                             <span class="vs__option-color" :style="{ 'background-color': hex }"></span
@@ -163,7 +204,7 @@
                                     <translate>Zweck</translate>
                                     <select v-model="currentElement.attributes.purpose">
                                         <option value="content"><translate>Inhalt</translate></option>
-                                        <option value="template"><translate>Vorlage</translate></option>
+                                        <option value="template"><translate>Aufgabenvorlage</translate></option>
                                         <option value="oer"><translate>OER-Material</translate></option>
                                         <option value="portfolio"><translate>ePortfolio</translate></option>
                                         <option value="draft"><translate>Entwurf</translate></option>
@@ -287,7 +328,10 @@
                         </label>
                         <label>
                             <translate>Name der neuen Seite</translate><br />
-                            <input v-model="newChapterName" type="text" />
+                            <input v-model="newChapterName" type="text" required />
+                            <div class="invalid_message" :style="{ display: errorEmptyChapterName ? 'block' : 'none' }">
+                                <translate>Der Name der neuen Seite darf nicht leer sein.</translate>
+                            </div>
                         </label>
                     </form>
                 </template>
@@ -415,7 +459,7 @@
                                 <p>{{ currentLicenseName }}</p>
                             </label>
                             <label>
-                                <translate>Sie können diese Daten unter "Seite bearbeiten" verändern</translate>.
+                                <translate>Sie können diese Daten unter "Seite bearbeiten" verändern.</translate>
                             </label>
                         </fieldset>
                         <fieldset>
@@ -452,6 +496,7 @@
 import ContainerComponents from './container-components.js';
 import CoursewarePluginComponents from './plugin-components.js';
 import CoursewareStructuralElementPermissions from './CoursewareStructuralElementPermissions.vue';
+import CoursewareStructuralElementDiscussion from './CoursewareStructuralElementDiscussion.vue';
 import CoursewareAccordionContainer from './CoursewareAccordionContainer.vue';
 import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
 import CoursewareWellcomeScreen from './CoursewareWellcomeScreen.vue';
@@ -465,11 +510,13 @@ import CoursewareTab from './CoursewareTab.vue';
 import CoursewareExport from '@/vue/mixins/courseware/export.js';
 import IsoDate from './IsoDate.vue';
 import StudipDialog from '../StudipDialog.vue';
+import draggable from 'vuedraggable';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-structural-element',
     components: {
+        CoursewareStructuralElementDiscussion,
         CoursewareStructuralElementPermissions,
         CoursewareRibbon,
         CoursewareListContainer,
@@ -483,6 +530,7 @@ export default {
         CoursewareTab,
         IsoDate,
         StudipDialog,
+        draggable,
     },
     props: ['canVisit', 'orderedStructuralElements', 'structuralElement'],
 
@@ -526,16 +574,27 @@ export default {
             exportRunning: false,
             exportChildren: false,
             oerChildren: true,
+            containerList: [],
+            isDragging: false,
+            dragOptions: {
+                animation: 0,
+                group: 'description',
+                disabled: false,
+                ghostClass: 'container-ghost',
+            },
+            errorEmptyChapterName: false,
         };
     },
 
     computed: {
         ...mapGetters({
             courseware: 'courseware',
+            context: 'context',
             consumeMode: 'consumeMode',
             containerById: 'courseware-containers/byId',
             relatedContainers: 'courseware-containers/related',
             relatedStructuralElements: 'courseware-structural-elements/related',
+            relatedTaskGroups: 'courseware-task-groups/related',
             relatedUsers: 'users/related',
             structuralElementById: 'courseware-structural-elements/byId',
             userIsTeacher: 'userIsTeacher',
@@ -552,6 +611,9 @@ export default {
             exportState: 'exportState',
             exportProgress: 'exportProgress',
             userId: 'userId',
+            sortMode: 'structuralElementSortMode',
+            viewMode: 'viewMode',
+            taskById: 'courseware-tasks/byId',
         }),
 
         currentId() {
@@ -624,7 +686,7 @@ export default {
                 if (!parentId) {
                     return null;
                 }
-                const element = this.structuralElementById({id: parentId});
+                const element = this.structuralElementById({ id: parentId });
                 if (!element) {
                     console.error(`CoursewareStructuralElement#ancestors: Could not find parent by ID: "${parentId}".`);
                 }
@@ -636,11 +698,11 @@ export default {
                 const parent = finder(node);
                 if (parent) {
                     yield parent;
-                    yield *visitAncestors(parent);
+                    yield* visitAncestors(parent);
                 }
             };
 
-            return [...visitAncestors(this.structuralElement)].reverse()
+            return [...visitAncestors(this.structuralElement)].reverse();
         },
         prevElement() {
             const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id);
@@ -697,10 +759,6 @@ export default {
             return this.structuralElement.attributes['can-edit'];
         },
 
-        isTeacher() {
-            return this.userIsTeacher;
-        },
-
         isRoot() {
             return this.structuralElement.relationships.parent.data === null;
         },
@@ -724,8 +782,8 @@ export default {
         },
         menuItems() {
             let menu = [
-                { id: 3, label: this.$gettext('Informationen anzeigen'), icon: 'info', emit: 'showInfo' },
-                { id: 4, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' },
+                { id: 4, label: this.$gettext('Informationen anzeigen'), icon: 'info', emit: 'showInfo' },
+                { id: 5, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' },
             ];
             if (this.canEdit) {
                 menu.push({
@@ -734,20 +792,38 @@ export default {
                     icon: 'edit',
                     emit: 'editCurrentElement',
                 });
-                menu.push({ id: 2, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' });
                 menu.push({
-                    id: 5,
+                    id: 2,
+                    label: this.$gettext('Abschnitte sortieren'),
+                    icon: 'arr_1sort',
+                    emit: 'sortContainers',
+                });
+
+                menu.push({ id: 3, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' });
+            }
+            if ((this.userIsTeacher && this.canEdit) || this.context.type === 'users') {
+                menu.push({
+                    id: 6,
                     label: this.$gettext('Seite exportieren'),
                     icon: 'export',
                     emit: 'showExportOptions',
                 });
             }
-            if (this.canEdit && this.oerEnabled) {
-                menu.push({ id: 6, label: this.textOer.title, icon: 'oer-campus', emit: 'oerCurrentElement' });
-            }
-            if (!this.isRoot && this.canEdit) {
+            if ((this.userIsTeacher || this.canEdit) && this.canVisit) {
                 menu.push({
                     id: 7,
+                    type: 'link',
+                    label: this.$gettext('Seite als pdf-Dokument exportieren'),
+                    icon: 'file-pdf',
+                    url: this.pdfExportURL,
+                });
+            }
+            if (this.canEdit && this.oerEnabled && this.userIsTeacher) {
+                menu.push({ id: 8, label: this.textOer.title, icon: 'oer-campus', emit: 'oerCurrentElement' });
+            }
+            if (!this.isRoot && this.canEdit && !this.isTask) {
+                menu.push({
+                    id: 9,
                     label: this.$gettext('Seite löschen'),
                     icon: 'trash',
                     emit: 'deleteCurrentElement',
@@ -928,6 +1004,56 @@ export default {
         blockedByAnotherUser() {
             return this.blocked && this.userId !== this.blockerId;
         },
+        discussView() {
+            return this.viewMode === 'discuss';
+        },
+        pdfExportURL() {
+            if (this.context.type === 'users') {
+                return STUDIP.URLHelper.getURL(
+                    'dispatch.php/contents/courseware/pdf_export/' + this.structuralElement.id
+                );
+            }
+            if (this.context.type === 'courses') {
+                return STUDIP.URLHelper.getURL(
+                    'dispatch.php/course/courseware/pdf_export/' + this.structuralElement.id
+                );
+            }
+
+            return '';
+        },
+        isTask() {
+            return this.structuralElement?.relationships.task.data !== null;
+        },
+        task() {
+            if (!this.isTask) {
+                return null;
+            }
+
+            return this.taskById({ id: this.structuralElement.relationships.task.data.id });
+        },
+        canAddElements() {
+            if (!this.isTask) {
+                return true;
+            }
+
+            // still loading
+            if (!this.task) {
+                return false;
+            }
+
+            const taskGroup = this.relatedTaskGroups({ parent: this.task, relationship: 'task-group' });
+
+            return taskGroup?.attributes['solver-may-add-blocks'];
+        },
+        showEmptyElementBox() {
+            if (!this.empty) {
+                return false;
+            }
+
+            return (
+                (!this.isRoot && this.canEdit) || !this.canEdit || (!this.noContainers && this.isRoot && this.canEdit)
+            );
+        },
     },
 
     methods: {
@@ -948,6 +1074,10 @@ export default {
             showElementInfoDialog: 'showElementInfoDialog',
             showElementDeleteDialog: 'showElementDeleteDialog',
             showElementOerDialog: 'showElementOerDialog',
+            updateContainer: 'updateContainer',
+            setStructuralElementSortMode: 'setStructuralElementSortMode',
+            sortContainersInStructualElements: 'sortContainersInStructualElements',
+            loadTask: 'loadTask',
         }),
 
         initCurrent() {
@@ -964,7 +1094,7 @@ export default {
                     }
                     try {
                         await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
-                    } catch(error) {
+                    } catch (error) {
                         if (error.status === 409) {
                             this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
                         } else {
@@ -978,6 +1108,7 @@ export default {
                 case 'addElement':
                     this.newChapterName = '';
                     this.newChapterParent = 'descendant';
+                    this.errorEmptyChapterName = false;
                     this.showElementAddDialog(true);
                     break;
                 case 'deleteCurrentElement':
@@ -996,6 +1127,24 @@ export default {
                 case 'setBookmark':
                     this.setBookmark();
                     break;
+                case 'sortContainers':
+                    if (this.blockedByAnotherUser) {
+                        this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
+
+                        return false;
+                    }
+                    try {
+                        await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+                    } catch (error) {
+                        if (error.status === 409) {
+                            this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
+                        } else {
+                            console.log(error);
+                        }
+
+                        return false;
+                    }
+                    this.enableSortContainers();
             }
         },
         async closeEditDialog() {
@@ -1057,6 +1206,25 @@ export default {
             this.initCurrent();
         },
 
+        enableSortContainers() {
+            this.setStructuralElementSortMode(true);
+        },
+
+        storeSort() {
+            this.setStructuralElementSortMode(false);
+
+            this.sortContainersInStructualElements({
+                structuralElement: this.structuralElement,
+                containers: this.containerList,
+            });
+            this.$emit('select', this.currentId);
+        },
+
+        resetSort() {
+            this.setStructuralElementSortMode(false);
+            this.containerList = this.containers;
+        },
+
         async exportCurrentElement(data) {
             if (this.exportRunning) {
                 return;
@@ -1088,10 +1256,14 @@ export default {
                 parentId: this.structuralElement.relationships.parent.data.id,
             });
             this.$router.push(parent_id);
+            this.companionInfo({ info: this.$gettext('Die Seite wurde gelöscht.') });
         },
         async createElement() {
             let title = this.newChapterName; // this is the title of the new element
             let parent_id = this.currentId; // new page is descandant as default
+            if (this.errorEmptyChapterName = title.trim() === '') {
+                return;
+            }
             if (this.newChapterParent === 'sibling') {
                 parent_id = this.structuralElement.relationships.parent.data.id;
             }
@@ -1108,6 +1280,7 @@ export default {
                 info:
                     this.$gettextInterpolate('Die Seite %{ pageTitle } wurde erfolgreich angelegt.', {pageTitle: newElement.attributes.title})
             });
+            this.newChapterName = '';
         },
         containerComponent(container) {
             return 'courseware-' + container.attributes['container-type'] + '-container';
@@ -1130,6 +1303,14 @@ export default {
     watch: {
         structuralElement() {
             this.initCurrent();
+            if (this.isTask) {
+                this.loadTask({
+                    taskId: this.structuralElement.relationships.task.data.id,
+                });
+            }
+        },
+        containers() {
+            this.containerList = this.containers;
         },
     },
 
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementComments.vue b/resources/vue/components/courseware/CoursewareStructuralElementComments.vue
new file mode 100755
index 00000000000..da6de348159
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareStructuralElementComments.vue
@@ -0,0 +1,128 @@
+<template>
+    <section
+        class="cw-structural-element-comments"
+        :class="[emptyComments ? 'cw-structural-element-comments-empty' : '']"
+    >
+        <div class="cw-structural-element-comments-items" v-show="!emptyComments" ref="commentsRef">
+            <courseware-talk-bubble
+                v-for="comment in comments"
+                :key="comment.id"
+                :payload="buildPayload(comment)"
+            />
+        </div>
+        <div class="cw-structural-element-comment-create">
+            <textarea v-model="createComment" :placeholder="placeHolder" spellcheck="true"></textarea>
+            <button class="button" @click="postComment"><translate>Senden</translate></button>
+        </div>
+    </section>
+</template>
+
+<script>
+import CoursewareTalkBubble from './CoursewareTalkBubble.vue';
+import { mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-structural-element-comments',
+    components: {
+        CoursewareTalkBubble,
+    },
+    props: {
+        structuralElement: Object,
+    },
+    data() {
+        return {
+            createComment: '',
+            placeHolder: this.$gettext('Stellen Sie eine Frage oder kommentieren Sie...'),
+        };
+    },
+    computed: {
+        ...mapGetters({
+            relatedUser: 'users/related',
+            userId: 'userId',
+            getComments: 'courseware-structural-element-comments/related',
+        }),
+        comments() {
+            const parent = {
+                type: this.structuralElement.type,
+                id: this.structuralElement.id,
+            };
+
+            return this.getComments({ parent, relationship: 'comments' });
+        },
+        emptyComments() {
+            if (this.comments === null || this.comments.length === 0) {
+                return true;
+            }
+
+            return false;
+        }
+    },
+    methods: {
+        async loadComments() {
+            const parent = {
+                type: this.structuralElement.type,
+                id: this.structuralElement.id,
+            };
+            await this.$store.dispatch('courseware-structural-element-comments/loadRelated', {
+                parent,
+                relationship: 'comments',
+                options: {
+                    include: 'user',
+                },
+            });
+        },
+        async postComment() {
+            const data = {
+                attributes: {
+                    comment: this.createComment
+                },
+                relationships: {
+                    'structural-element': {
+                        data: {
+                            id: this.structuralElement.id,
+                            type: this.structuralElement.type
+                        }
+                    }
+                }
+            };
+
+            await this.$store.dispatch('courseware-structural-element-comments/create', data);
+            this.loadComments();
+            this.createComment = '';
+        },
+        buildPayload(comment) {
+            const commenter = this.relatedUser({
+                parent: { id: comment.id, type: comment.type },
+                relationship: 'user',
+            });
+
+            const payload = {
+                id: comment.id,
+                own: comment.relationships.user.data.id === this.userId,
+                content: comment.attributes.comment,
+                chdate: comment.attributes.chdate,
+                mkdate: comment.attributes.mkdate,
+                user_id: commenter.id,
+                user_name: commenter.attributes['formatted-name'],
+                user_avatar: commenter.meta.avatar.small,
+            };
+
+            return payload;
+        },
+    },
+    mounted() {
+        this.loadComments();
+    },
+    updated() {
+        let ref = this.$refs["commentsRef"];
+        ref.scrollTop = ref.scrollHeight;
+    },
+    watch: {
+        comments() {
+            if (this.comments && this.comments.length > 0) {
+                this.$emit('hasComments');
+            }
+        }
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue b/resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue
new file mode 100755
index 00000000000..fcd275c1b2f
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue
@@ -0,0 +1,60 @@
+<template>
+    <div class="cw-structural-element-discussion">
+        <courseware-collapsible-box
+            :title="text.comments"
+            :open="hasComments"
+        >
+            <courseware-structural-element-comments
+            :structuralElement="structuralElement"
+            @hasComments="hasComments = true"
+            />
+        </courseware-collapsible-box>
+
+        <courseware-collapsible-box
+            v-if="canEdit || userIsTeacher"
+            :title="text.feedback"
+            :open="hasFeedback"
+        >
+            <courseware-structural-element-feedback
+                :structuralElement="structuralElement"
+                :canEdit="canEdit"
+                @hasFeedback="hasFeedback = true"
+            />
+        </courseware-collapsible-box>
+    </div>
+</template>
+
+<script>
+import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
+import CoursewareStructuralElementComments from './CoursewareStructuralElementComments.vue';
+import CoursewareStructuralElementFeedback from './CoursewareStructuralElementFeedback.vue';
+import { mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-structural-element-discussion',
+    components: {
+        CoursewareCollapsibleBox,
+        CoursewareStructuralElementComments,
+        CoursewareStructuralElementFeedback,
+    },
+    props: {
+        structuralElement: Object,
+        canEdit: Boolean
+    },
+    data() {
+        return {
+            hasComments: false,
+            hasFeedback: false,
+            text: {
+                comments: this.$gettext('Kommentare zur Seite'),
+                feedback: this.$gettext('Feedback zur Seite')
+            }
+        }
+    },
+    computed: {
+        ...mapGetters({
+            userIsTeacher: 'userIsTeacher',
+        }),
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue b/resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue
new file mode 100755
index 00000000000..cc079eada2d
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue
@@ -0,0 +1,130 @@
+<template>
+    <section
+        v-if="canEdit || userIsTeacher"
+        class="cw-structural-element-feedback"
+        :class="[emptyFeedback ? 'cw-structural-element-feedback-empty' : '']"
+    >
+        <div class="cw-structural-element-feedback-items" v-show="!emptyFeedback" ref="feedbacks">
+            <courseware-talk-bubble
+                v-for="feedback in feedback"
+                :key="feedback.id"
+                :payload="buildPayload(feedback)"
+            />
+        </div>
+        <courseware-companion-box
+                v-if="!userIsTeacher && feedback.length === 0"
+                :msgCompanion="$gettext('Es wurde noch kein Feedback abgegeben.')"
+                mood="pointing"
+            />
+        <div v-if="userIsTeacher" class="cw-structural-element-feedback-create">
+            <textarea v-model="feedbackText" :placeholder="placeHolder" spellcheck="true"></textarea>
+            <button class="button" @click="postFeedback"><translate>Senden</translate></button>
+        </div>
+    </section>
+</template>
+
+<script>
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
+import CoursewareTalkBubble from './CoursewareTalkBubble.vue';
+import { mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-structural-element-feedback',
+    components: {
+        CoursewareCompanionBox,
+        CoursewareTalkBubble,
+    },
+    props: {
+        structuralElement: Object,
+        canEdit: Boolean,
+    },
+    data() {
+        return {
+            feedbackText: '',
+            placeHolder: this.$gettext('Schreiben Sie ein Feedback...'),
+        };
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            getRelatedFeedback: 'courseware-structural-element-feedback/related',
+            getRelatedUser: 'users/related',
+            userIsTeacher: 'userIsTeacher',
+        }),
+        feedback() {
+            const parent = {
+                type: this.structuralElement.type,
+                id: this.structuralElement.id,
+            };
+
+            return this.getRelatedFeedback({ parent, relationship: 'feedback' });
+        },
+        emptyFeedback() {
+            if (this.feedback === null || this.feedback.length === 0) {
+                return true;
+            }
+
+            return false;
+        }
+    },
+    methods: {
+        buildPayload(feedback) {
+            const { id, type } = feedback;
+            const user = this.getRelatedUser({ parent: { id, type }, relationship: 'user' });
+
+            return {
+                own: user.id === this.userId,
+                content: feedback.attributes.feedback,
+                chdate: feedback.attributes.chdate,
+                mkdate: feedback.attributes.mkdate,
+                user_name: user?.attributes?.['formatted-name'] ?? '',
+                user_avatar: user?.meta?.avatar.small,
+            };
+        },
+        async loadFeedback() {
+            const parent = {
+                type: this.structuralElement.type,
+                id: this.structuralElement.id,
+            };
+            await this.$store.dispatch('courseware-structural-element-feedback/loadRelated', {
+                parent,
+                relationship: 'feedback',
+                options: {
+                    include: 'user',
+                },
+            });
+        },
+        async postFeedback() {
+            const data = {
+                attributes: {
+                    feedback: this.feedbackText,
+                },
+                relationships: {
+                    'structural-element': {
+                        data: {
+                            id: this.structuralElement.id,
+                            type: this.structuralElement.type
+                        }
+                    }
+                },
+            };
+            await this.$store.dispatch('courseware-structural-element-feedback/create', data, { root: true });
+            this.feedbackText = '';
+            this.loadFeedback();
+        }
+    },
+    async mounted() {
+        await this.loadFeedback();
+    },
+    updated() {
+        this.$refs.feedbacks.scrollTop = this.$refs.feedbacks.scrollHeight;
+    },
+    watch: {
+        feedback() {
+            if (this.feedback && this.feedback.length > 0) {
+                this.$emit('hasFeedback');
+            }
+        }
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
index e35e041962f..852f2139ec2 100755
--- a/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
+++ b/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
@@ -45,9 +45,9 @@
                                     <p>{{ child.attributes.payload.description }}</p>
                                 </div>
                                 <footer>
-                                    {{ countChildren }}
+                                    {{ countChildChildren(child) }}
                                     <translate
-                                        :translate-n="countChildren"
+                                        :translate-n="countChildChildren(child)"
                                         translate-plural="Seiten"
                                     >
                                        Seite
@@ -118,22 +118,6 @@ export default {
         },
         style() {
             return this.block?.attributes?.payload?.style;
-        },
-        childSets() {
-            let childSets = [];
-            let childElements = this.childElements;
-            while (childElements.length > 0) {
-                let set = [];
-                for (let i = 0; i < 4; i++) {
-                    let elem = childElements.shift();
-                    if (elem !== undefined) {
-                        set.push(elem);
-                    }
-                }
-                childSets.push(set);
-            }
-
-            return childSets;
         }
     },
     mounted() {
@@ -171,6 +155,9 @@ export default {
                 return {};
             }
         },
+        countChildChildren(child) {
+            return this.childrenById(child.id).length;
+        },
         hasImage(child) {
             return child.relationships?.image?.data !== null;
         },
diff --git a/resources/vue/components/courseware/CoursewareTabsContainer.vue b/resources/vue/components/courseware/CoursewareTabsContainer.vue
index f8502bd8542..c9bb646e7a5 100755
--- a/resources/vue/components/courseware/CoursewareTabsContainer.vue
+++ b/resources/vue/components/courseware/CoursewareTabsContainer.vue
@@ -6,9 +6,10 @@
         :isTeacher="isTeacher"
         @storeContainer="storeContainer"
         @closeEdit="initCurrentData"
+        @sortBlocks="enableSort"
     >
         <template v-slot:containerContent>
-            <courseware-tabs>
+            <courseware-tabs v-if="!sortMode">
                 <courseware-tab
                     v-for="(section, index) in currentSections"
                     :key="index"
@@ -26,12 +27,42 @@
                                 :isTeacher="isTeacher"
                             />
                         </li>
-                        <li v-if="showEditMode">
+                        <li v-if="showEditMode && canAddElements">
                             <courseware-block-adder-area :container="container" :section="index" @updateContainerContent="updateContent"/>
                         </li>
                     </ul>
                 </courseware-tab>
             </courseware-tabs>
+            <div v-if="sortMode && canEdit" class="cw-container-tabs-sort">
+                <courseware-collapsible-box
+                    v-for="(section, index) in currentSections"
+                    :key="index"
+                    :title="section.name"
+                    :icon="section.icon"
+                    :open="index === 0"
+                >
+                    <draggable
+                        class="cw-container-list-block-list cw-container-list-sort-mode"
+                        :class="[section.blocks.length === 0 ? 'cw-container-list-sort-mode-empty' : '']"
+                        tag="ul"
+                        v-model="section.blocks"
+                        v-bind="dragOptions"
+                        handle=".cw-sortable-handle"
+                        @start="isDragging = true"
+                        @end="isDragging = false"
+                    >
+                        <transition-group type="transition" name="flip-blocks" tag="div">
+                            <li v-for="block in section.blocks" :key="block.id" class="cw-block-item cw-block-item-sortable">
+                                <component :is="component(block)" :block="block" :canEdit="canEdit" :isTeacher="isTeacher" />
+                            </li>
+                        </transition-group>
+                    </draggable>
+                </courseware-collapsible-box>
+                <div>
+                    <button class="button accept" @click="storeSort"><translate>Sortierung speichern</translate></button>
+                    <button class="button cancel"  @click="resetSort"><translate>Sortieren abbrechen</translate></button>
+                </div>
+            </div>
         </template>
         <template v-slot:containerEditDialog>
             <form class="default cw-container-dialog-edit-form" @submit.prevent="">
@@ -47,7 +78,7 @@
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
                             </template>
                             <template #no-options="{ search, searching, loading }">
-                                <translate>Es steht keine Auswahl zur Verfügung</translate>.
+                                <translate>Es steht keine Auswahl zur Verfügung.</translate>
                             </template>
                             <template #selected-option="option">
                                 <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
@@ -76,6 +107,7 @@ import containerMixin from '../../mixins/courseware/container.js';
 import contentIcons from './content-icons.js';
 import CoursewareTabs from './CoursewareTabs.vue';
 import CoursewareTab from './CoursewareTab.vue';
+import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
 import StudipIcon from './../StudipIcon.vue';
 
 import { mapGetters, mapActions } from 'vuex';
@@ -86,19 +118,29 @@ export default {
     components: Object.assign(ContainerComponents, {
         CoursewareTabs,
         CoursewareTab,
+        CoursewareCollapsibleBox,
         StudipIcon,
     }),
     props: {
         container: Object,
         canEdit: Boolean,
         isTeacher: Boolean,
+        canAddElements: Boolean,
     },
     data() {
         return {
             currentContainer: null,
             currentSections: [],
             textDeleteSection: this.$gettext('Sektion entfernen'),
-            selectAttributes: {'ref': 'openIndicator', 'role': 'presentation', 'class': 'vs__open-indicator'}
+            selectAttributes: {'ref': 'openIndicator', 'role': 'presentation', 'class': 'vs__open-indicator'},
+            sortMode: false,
+            isDragging: false,
+            dragOptions: {
+                animation: 0,
+                group: "description",
+                disabled: false,
+                ghostClass: "block-ghost"
+            },
         };
     },
     computed: {
@@ -125,6 +167,7 @@ export default {
     methods: {
         ...mapActions({
             updateContainer: 'updateContainer',
+            lockObject: 'lockObject',
             unlockObject: 'unlockObject',
         }),
         initCurrentData() {
@@ -174,6 +217,17 @@ export default {
             await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
             this.initCurrentData();
         },
+        enableSort() {
+            this.sortMode = true;
+        },
+        async storeSort() {
+            this.sortMode = false;
+            this.storeContainer();
+        },
+        async resetSort() {
+            await this.unlockObject({ id: this.currentContainer.id, type: 'courseware-containers' });
+            this.sortMode = false;
+        },
         component(block) {
             if (block.attributes) {
                 return 'courseware-' + block.attributes["block-type"] + '-block';
diff --git a/resources/vue/components/courseware/CoursewareTreeItem.vue b/resources/vue/components/courseware/CoursewareTreeItem.vue
index bf7f522d5b6..05047eec468 100755
--- a/resources/vue/components/courseware/CoursewareTreeItem.vue
+++ b/resources/vue/components/courseware/CoursewareTreeItem.vue
@@ -1,15 +1,43 @@
 <template>
     <li>
-        <div :class="{ 'cw-tree-item-is-root': isRoot, 'cw-tree-item-first-level': isFirstLevel }">
+        <div
+            :class="[
+                isRoot ? 'cw-tree-item-is-root' : '',
+                isFirstLevel ? 'cw-tree-item-first-level' : '',
+                hasPurposeClass ? 'cw-tree-item-' + purposeClass : '',
+            ]"
+        >
             <router-link
                 :to="'/structural_element/' + element.id"
                 class="cw-tree-item-link"
                 :class="{ 'cw-tree-item-link-current': isCurrent }"
             >
-                {{ element.attributes.title }}
+                {{ element.attributes.title || "–" }}
+                <span v-if="task">| {{ solverName }}</span>
+                <span
+                    v-if="hasReleaseOrWithdrawDate"
+                    class="cw-tree-item-flag-date"
+                    :title="$gettext('Diese Seite hat eine zeitlich beschränkte Sichtbarkeit')"
+                ></span>
+                <span
+                    v-if="hasWriteApproval"
+                    class="cw-tree-item-flag-write"
+                    :title="$gettext('Diese Seite kann von Teilnehmenden bearbeitet werden')"
+                ></span>
+                <span
+                    v-if="hasNoReadApproval"
+                    class="cw-tree-item-flag-cant-read"
+                    :title="$gettext('Diese Seite kann von Teilnehmenden nicht gesehen werden')"
+                ></span>
             </router-link>
         </div>
-        <ul v-if="hasChildren" :class="{ 'cw-tree-chapter-list': isRoot }">
+        <ul
+            v-if="hasChildren"
+            :class="{
+                'cw-tree-chapter-list': isRoot,
+                'cw-tree-subchapter-list': isFirstLevel,
+            }"
+        >
             <courseware-tree-item
                 v-for="child in children"
                 :key="child.id"
@@ -23,7 +51,7 @@
 </template>
 
 <script>
-import { mapGetters } from 'vuex';
+import { mapGetters, mapActions } from 'vuex';
 
 export default {
     name: 'courseware-tree-item',
@@ -44,6 +72,10 @@ export default {
         ...mapGetters({
             childrenById: 'courseware-structure/children',
             structuralElementById: 'courseware-structural-elements/byId',
+            context: 'context',
+            taskById: 'courseware-tasks/byId',
+            userById: 'users/byId',
+            groupById: 'status-groups/byId',
         }),
         children() {
             if (!this.element) {
@@ -66,6 +98,88 @@ export default {
         isCurrent() {
             return this.element.id === this.currentElement?.id;
         },
+        hasReleaseOrWithdrawDate() {
+            return (
+                this.element.attributes['release-date'] !== null || this.element.attributes['withdraw-date'] !== null
+            );
+        },
+        hasWriteApproval() {
+            const writeApproval = this.element.attributes['write-approval'];
+
+            if (Object.keys(writeApproval).length === 0) {
+                return false;
+            }
+            return writeApproval.all || writeApproval.groups.length > 0 || writeApproval.users.length > 0;
+        },
+        hasNoReadApproval() {
+            const readApproval = this.element.attributes['read-approval'];
+
+            if (Object.keys(readApproval).length === 0 || this.hasWriteApproval) {
+                return false;
+            }
+            return !readApproval.all && readApproval.groups.length === 0 && readApproval.users.length === 0;
+        },
+        hasPurposeClass() {
+            return this.purposeClass !== '';
+        },
+        purposeClass() {
+            if (
+                (this.isFirstLevel && this.context.type === 'users') ||
+                (this.context.type === 'courses' && this.element.attributes.purpose === 'task')
+            ) {
+                return this.element.attributes.purpose;
+            }
+            return '';
+        },
+        task() {
+            if (this.element.relationships.task.data) {
+                return this.taskById({
+                    id: this.element.relationships.task.data.id,
+                });
+            }
+
+            return null;
+        },
+        taskProgress() {
+            return this.task ? this.task.attributes.progress + '%' : '';
+        },
+        solver() {
+            if (this.task) {
+                const solver = this.task.relationships.solver.data;
+                if (solver.type === 'users') {
+                    return this.userById({ id: solver.id });
+                }
+                if (solver.type === 'status-groups') {
+                    return this.groupById({ id: solver.id });
+                }
+            }
+
+            return null;
+        },
+        solverName() {
+            if (this.solver) {
+                if (this.solver.type === 'users') {
+                    return this.solver.attributes['formatted-name'];
+                }
+                if (this.solver.type === 'status-groups') {
+                    return this.solver.attributes.name;
+                }
+            }
+
+            return '';
+        },
+    },
+    methods: {
+        ...mapActions({
+            loadTask: 'loadTask',
+        }),
+    },
+    mounted() {
+        if (this.element.relationships.task.data) {
+            this.loadTask({
+                taskId: this.element.relationships.task.data.id,
+            });
+        }
     },
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareViewWidget.vue b/resources/vue/components/courseware/CoursewareViewWidget.vue
index ff18ff52b4a..fd3ee9ff7b5 100755
--- a/resources/vue/components/courseware/CoursewareViewWidget.vue
+++ b/resources/vue/components/courseware/CoursewareViewWidget.vue
@@ -1,21 +1,53 @@
 <template>
     <ul class="widget-list widget-links sidebar-views cw-view-widget">
-        <li :class="{ active: readView }" @click="setReadView"><translate>Lesen</translate></li>
-        <li :class="{ active: editView }" @click="setEditView"><translate>Bearbeiten</translate></li>
+        <li
+            :class="{ active: readView }"
+            @click="setReadView"
+        >
+            <translate>Lesen</translate>
+        </li>
+        <li
+            v-if="canEdit"
+            :class="{ active: editView }"
+            @click="setEditView"
+        >
+            <translate>Bearbeiten</translate>
+        </li>
+        <li 
+            v-if="context.type === 'courses' && canVisit"
+            :class="{ active: discussView }"
+            @click="setDiscussView"
+        >
+            <translate>Diskutieren</translate>
+        </li>
     </ul>
 </template>
 
 <script>
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-view-widget',
+    props: ['structuralElement', 'canVisit'],
     computed: {
+        ...mapGetters({
+            viewMode: 'viewMode',
+            context: 'context',
+        }),
         readView() {
-            return this.$store.getters.viewMode === 'read';
+            return this.viewMode === 'read';
         },
         editView() {
-            return this.$store.getters.viewMode === 'edit';
+            return this.viewMode === 'edit';
+        },
+        discussView() {
+            return this.viewMode === 'discuss';
+        },
+        canEdit() {
+            if (!this.structuralElement) {
+                return false;
+            }
+            return this.structuralElement.attributes['can-edit'];
         },
     },
     methods: {
@@ -29,6 +61,9 @@ export default {
         setEditView() {
             this.$store.dispatch('coursewareViewMode', 'edit');
         },
+        setDiscussView() {
+            this.$store.dispatch('coursewareViewMode', 'discuss');
+        },
     },
 };
 </script>
diff --git a/resources/vue/components/courseware/DashboardApp.vue b/resources/vue/components/courseware/DashboardApp.vue
index 4338be2f192..34290c55863 100755
--- a/resources/vue/components/courseware/DashboardApp.vue
+++ b/resources/vue/components/courseware/DashboardApp.vue
@@ -1,11 +1,36 @@
 <template>
-    <courseware-course-dashboard></courseware-course-dashboard>
+    <div class="cw-dashboard-wrapper">
+        <courseware-course-dashboard></courseware-course-dashboard>
+        <MountingPortal mountTo="#courseware-dashboard-view-widget" name="sidebar-views">
+            <courseware-dashboard-view-widget></courseware-dashboard-view-widget>
+        </MountingPortal>
+    </div>
 </template>
 
 <script>
 import CoursewareCourseDashboard from './CoursewareCourseDashboard.vue';
+import CoursewareDashboardViewWidget from './CoursewareDashboardViewWidget.vue';
+import { mapActions, mapGetters } from 'vuex';
 
 export default {
-    components: { CoursewareCourseDashboard },
+    components: {
+        CoursewareCourseDashboard,
+        CoursewareDashboardViewWidget
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+        }),
+    },
+    methods: {
+        ...mapActions({
+            loadCoursewareStructure: 'courseware-structure/load',
+            loadTeacherStatus: 'loadTeacherStatus',
+        }),
+    },
+    async mounted() {
+        await this.loadCoursewareStructure();
+        await this.loadTeacherStatus(this.userId);
+    }
 };
 </script>
diff --git a/resources/vue/components/courseware/IndexApp.vue b/resources/vue/components/courseware/IndexApp.vue
index 7209dedffad..427e363a1d7 100755
--- a/resources/vue/components/courseware/IndexApp.vue
+++ b/resources/vue/components/courseware/IndexApp.vue
@@ -7,10 +7,10 @@
             @select="selectStructuralElement"
         ></courseware-structural-element>
         <MountingPortal mountTo="#courseware-action-widget" name="sidebar-actions">
-            <courseware-action-widget :structural-element="selected"></courseware-action-widget>
+            <courseware-action-widget :structural-element="selected" :canVisit="canVisit"></courseware-action-widget>
         </MountingPortal>
         <MountingPortal mountTo="#courseware-view-widget" name="sidebar-views">
-            <courseware-view-widget></courseware-view-widget>
+            <courseware-view-widget :structural-element="selected" :canVisit="canVisit"></courseware-view-widget>
         </MountingPortal>
     </div>
     <studip-progress-indicator
diff --git a/resources/vue/courseware-admin-app.js b/resources/vue/courseware-admin-app.js
new file mode 100755
index 00000000000..2ca74549a14
--- /dev/null
+++ b/resources/vue/courseware-admin-app.js
@@ -0,0 +1,42 @@
+import AdminApp from './components/courseware/AdminApp.vue';
+import { mapResourceModules } from '@elan-ev/reststate-vuex';
+import Vuex from 'vuex';
+import CoursewareAdminModule from './store/courseware/courseware-admin.module';
+import axios from 'axios';
+
+const mountApp = (STUDIP, createApp, element) => {
+    const getHttpClient = () =>
+    axios.create({
+        baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
+        headers: {
+            'Content-Type': 'application/vnd.api+json',
+        },
+    });
+
+    const httpClient = getHttpClient();
+
+    const store = new Vuex.Store({
+        modules: {
+            courseware: CoursewareAdminModule,
+            ...mapResourceModules({
+                names: [
+                    'courseware-templates',
+                ],
+                httpClient,
+            }),
+        },
+    });
+
+    store.dispatch('courseware-templates/loadAll');
+
+    const app = createApp({
+        render: (h) => h(AdminApp),
+        store
+    });
+
+    app.$mount(element);
+
+    return app;
+}
+
+export default mountApp;
\ No newline at end of file
diff --git a/resources/vue/courseware-content-bookmark-app.js b/resources/vue/courseware-content-bookmark-app.js
new file mode 100755
index 00000000000..8e9a43c7bfe
--- /dev/null
+++ b/resources/vue/courseware-content-bookmark-app.js
@@ -0,0 +1,81 @@
+import ContentBookmarkApp from './components/courseware/ContentBookmarkApp.vue';
+import { mapResourceModules } from '@elan-ev/reststate-vuex';
+import Vuex from 'vuex';
+import CoursewareModule from './store/courseware/courseware.module';
+import axios from 'axios';
+
+const mountApp = (STUDIP, createApp, element) => {
+    const getHttpClient = () =>
+    axios.create({
+        baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
+        headers: {
+            'Content-Type': 'application/vnd.api+json',
+        },
+    });
+
+    const httpClient = getHttpClient();
+
+    const store = new Vuex.Store({
+        modules: {
+            courseware: CoursewareModule,
+            ...mapResourceModules({
+                names: [
+                    'activities',
+                    'users',
+                    'courses',
+                    'course-memberships',
+                    'courseware-blocks',
+                    'courseware-block-comments',
+                    'courseware-block-feedback',
+                    'courseware-containers',
+                    'courseware-instances',
+                    'courseware-structural-elements',
+                    'courseware-user-data-fields',
+                    'courseware-user-progresses',
+                    'users',
+                    'institutes',
+                    'semesters',
+                    'sem-classes',
+                    'sem-types',
+                    'status-groups',
+                ],
+                httpClient,
+            }),
+        },
+    });
+    let entry_id = null;
+    let entry_type = null;
+    let elem;
+
+    if ((elem = document.getElementById(element.substring(1))) !== undefined) {
+        if (elem.attributes !== undefined) {
+            if (elem.attributes['entry-type'] !== undefined) {
+                entry_type = elem.attributes['entry-type'].value;
+            }
+
+            if (elem.attributes['entry-id'] !== undefined) {
+                entry_id = elem.attributes['entry-id'].value;
+            }
+        }
+    }
+
+    store.dispatch('setUserId', STUDIP.USER_ID);
+    store.dispatch('users/loadById', {id: STUDIP.USER_ID});
+    store.dispatch('loadUsersBookmarks', STUDIP.USER_ID);
+    store.dispatch('setHttpClient', httpClient);
+    store.dispatch('coursewareContext', {
+        id: entry_id,
+        type: entry_type,
+    });
+
+    const app = createApp({
+        render: (h) => h(ContentBookmarkApp),
+        store
+    });
+
+    app.$mount(element);
+
+    return app;
+}
+
+export default mountApp;
\ No newline at end of file
diff --git a/resources/vue/courseware-content-overview-app.js b/resources/vue/courseware-content-overview-app.js
new file mode 100755
index 00000000000..e1f15ef2450
--- /dev/null
+++ b/resources/vue/courseware-content-overview-app.js
@@ -0,0 +1,97 @@
+import ContentOverviewApp from './components/courseware/ContentOverviewApp.vue';
+import CoursewareStructureModule from './store/courseware/structure.module';
+import { mapResourceModules } from '@elan-ev/reststate-vuex';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import CoursewareModule from './store/courseware/courseware.module';
+import axios from 'axios';
+import vSelect from 'vue-select';
+import 'vue-select/dist/vue-select.css'
+
+Vue.component('v-select', vSelect);
+
+const mountApp = (STUDIP, createApp, element) => {
+    const getHttpClient = () =>
+    axios.create({
+        baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
+        headers: {
+            'Content-Type': 'application/vnd.api+json',
+        },
+    });
+
+    const httpClient = getHttpClient();
+
+    const store = new Vuex.Store({
+        modules: {
+            courseware: CoursewareModule,
+            'courseware-structure': CoursewareStructureModule,
+            ...mapResourceModules({
+                names: [
+                    'activities',
+                    'users',
+                    'courses',
+                    'course-memberships',
+                    'courseware-blocks',
+                    'courseware-block-comments',
+                    'courseware-block-feedback',
+                    'courseware-containers',
+                    'courseware-instances',
+                    'courseware-structural-elements',
+                    'courseware-templates',
+                    'courseware-user-data-fields',
+                    'courseware-user-progresses',
+                    'file-refs',
+                    'users',
+                    'institutes',
+                    'semesters',
+                    'sem-classes',
+                    'sem-types',
+                    'status-groups',
+                ],
+                httpClient,
+            }),
+        },
+    });
+    let entry_id = null;
+    let entry_type = null;
+    let licenses = null;
+    let elem;
+
+    if ((elem = document.getElementById(element.substring(1))) !== undefined) {
+        if (elem.attributes !== undefined) {
+            if (elem.attributes['entry-type'] !== undefined) {
+                entry_type = elem.attributes['entry-type'].value;
+            }
+
+            if (elem.attributes['entry-id'] !== undefined) {
+                entry_id = elem.attributes['entry-id'].value;
+            }
+
+            if (elem.attributes['licenses'] !== undefined) {
+                licenses = JSON.parse(elem.attributes['licenses'].value);
+            }
+        }
+    }
+
+    store.dispatch('setUserId', STUDIP.USER_ID);
+    store.dispatch('users/loadById', {id: STUDIP.USER_ID});
+    store.dispatch('courseware-structural-elements/loadById',{ id: STUDIP.COURSEWARE_USERS_ROOT_ID, options: { include: 'children'}});
+    store.dispatch('courseware-templates/loadAll');
+    store.dispatch('setHttpClient', httpClient);
+    store.dispatch('licenses', licenses);
+    store.dispatch('coursewareContext', {
+        id: entry_id,
+        type: entry_type,
+    });
+
+    const app = createApp({
+        render: (h) => h(ContentOverviewApp),
+        store
+    });
+
+    app.$mount(element);
+
+    return app;
+}
+
+export default mountApp;
diff --git a/resources/vue/courseware-dashboard-app.js b/resources/vue/courseware-dashboard-app.js
index 3a67756667d..608341b8897 100755
--- a/resources/vue/courseware-dashboard-app.js
+++ b/resources/vue/courseware-dashboard-app.js
@@ -1,8 +1,89 @@
 import DashboardApp from './components/courseware/DashboardApp.vue';
+import { mapResourceModules } from '@elan-ev/reststate-vuex';
+import Vuex from 'vuex';
+import CoursewareModule from './store/courseware/courseware.module';
+import CoursewareStructureModule from './store/courseware/structure.module';
+import axios from 'axios';
 
 const mountApp = (STUDIP, createApp, element) => {
+    const getHttpClient = () =>
+        axios.create({
+            baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
+            headers: {
+                'Content-Type': 'application/vnd.api+json',
+            },
+        });
+
+    const httpClient = getHttpClient();
+
+    const store = new Vuex.Store({
+        modules: {
+            courseware: CoursewareModule,
+            'courseware-structure': CoursewareStructureModule,
+            ...mapResourceModules({
+                names: [
+                    'activities',
+                    'users',
+                    'courses',
+                    'course-memberships',
+                    'courseware-blocks',
+                    'courseware-block-comments',
+                    'courseware-block-feedback',
+                    'courseware-containers',
+                    'courseware-instances',
+                    'courseware-structural-elements',
+                    'courseware-task-feedback',
+                    'courseware-task-groups',
+                    'courseware-tasks',
+                    'courseware-user-data-fields',
+                    'courseware-user-progresses',
+                    'files',
+                    'file-refs',
+                    'folders',
+                    'users',
+                    'institutes',
+                    'semesters',
+                    'sem-classes',
+                    'sem-types',
+                    'status-groups',
+                ],
+                httpClient,
+            }),
+        },
+    });
+    let entry_id = null;
+    let entry_type = null;
+    let elem;
+
+    if ((elem = document.getElementById(element.substring(1))) !== undefined) {
+        if (elem.attributes !== undefined) {
+            if (elem.attributes['entry-type'] !== undefined) {
+                entry_type = elem.attributes['entry-type'].value;
+            }
+
+            if (elem.attributes['entry-id'] !== undefined) {
+                entry_id = elem.attributes['entry-id'].value;
+            }
+        }
+    }
+
+    store.dispatch('setUserId', STUDIP.USER_ID);
+    store.dispatch('users/loadById', { id: STUDIP.USER_ID });
+    store.dispatch('setHttpClient', httpClient);
+    store.dispatch('coursewareContext', {
+        id: entry_id,
+        type: entry_type,
+    });
+    store.dispatch('courseware-tasks/loadAll', {
+        options: {
+            'filter[cid]': entry_id,
+            include: 'solver, structural-element, task-feedback, task-group, task-group.lecturer',
+        },
+    });
+
     const app = createApp({
         render: (h) => h(DashboardApp),
+        store,
     });
 
     app.$mount(element);
diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js
index ffdae6a70c6..4135e2c08ec 100755
--- a/resources/vue/courseware-index-app.js
+++ b/resources/vue/courseware-index-app.js
@@ -97,6 +97,11 @@ const mountApp = (STUDIP, createApp, element) => {
                     'courseware-containers',
                     'courseware-instances',
                     'courseware-structural-elements',
+                    'courseware-structural-element-comments',
+                    'courseware-structural-element-feedback',
+                    'courseware-task-feedback',
+                    'courseware-task-groups',
+                    'courseware-tasks',
                     'courseware-user-data-fields',
                     'courseware-user-progresses',
                     'files',
diff --git a/resources/vue/courseware-manager-app.js b/resources/vue/courseware-manager-app.js
index fc700a22644..fccea2beb5e 100755
--- a/resources/vue/courseware-manager-app.js
+++ b/resources/vue/courseware-manager-app.js
@@ -31,6 +31,8 @@ const mountApp = (STUDIP, createApp, element) => {
                     'courseware-containers',
                     'courseware-instances',
                     'courseware-structural-elements',
+                    'courseware-task-groups',
+                    'courseware-tasks',
                     'courseware-user-data-fields',
                     'courseware-user-progresses',
                     'files',
diff --git a/resources/vue/mixins/courseware/task-helper.js b/resources/vue/mixins/courseware/task-helper.js
new file mode 100755
index 00000000000..ecf4ff30492
--- /dev/null
+++ b/resources/vue/mixins/courseware/task-helper.js
@@ -0,0 +1,66 @@
+export default {
+    methods: {
+        getStatus(task) {
+            let status = {};
+            const now = new Date(Date.now());
+            const submissionDate = new Date(task.attributes['submission-date']);
+            let limit = new Date();
+            limit.setDate(now.getDate() + 3);
+            status.canSubmit = true;
+
+            if (now < submissionDate) {
+                status.shape = 'span-empty';
+                status.role = 'status-green';
+                status.description = this.$gettext('Aufgabe bereit');
+            }
+            if (task.attributes.renewal !== 'granted') {
+                if (limit > submissionDate) {
+                    status.shape = 'span-3quarter';
+                    status.role = 'status-yellow';
+                    status.description = this.$gettext('Aufgabe muss bald abgegeben werden');
+                }
+
+                if (now >= submissionDate) {
+                    status.canSubmit = false;
+                    status.shape = 'span-full';
+                    status.role = 'status-red';
+                    status.description = this.$gettext('Abgabe ist nicht bis zur Abgabefrist erfolgt');
+                }
+            } else {
+                const renewalDate = new Date(task.attributes['renewal-date']);
+                if (limit > renewalDate) {
+                    status.shape = 'span-3quarter';
+                    status.role = 'status-yellow';
+                    status.description = this.$gettext('Aufgabe muss bald abgegeben werden');
+                }
+
+                if (now >= renewalDate) {
+                    status.canSubmit = false;
+                    status.shape = 'span-full';
+                    status.role = 'status-red';
+                    status.description = this.$gettext('Abgabe ist nicht bis zur verlängerten Abgabefrist erfolgt');
+                }
+            }
+
+            if (task.attributes.submitted) {
+                status.shape = 'span-full';
+                status.role = 'status-green';
+                status.description = this.$gettext('Aufgabe abgegeben');
+            }
+
+            return status;
+        },
+        getLinkToElement(elementId) {
+            return (
+                STUDIP.URLHelper.base_url +
+                'dispatch.php/course/courseware/?cid=' +
+                STUDIP.URLHelper.parameters.cid +
+                '#/structural_element/' +
+                elementId
+            );
+        },
+        getReadableDate(date) {
+            return new Date(date).toLocaleDateString();
+        },
+    },
+};
diff --git a/resources/vue/store/courseware/courseware-admin.module.js b/resources/vue/store/courseware/courseware-admin.module.js
new file mode 100755
index 00000000000..ea287468c43
--- /dev/null
+++ b/resources/vue/store/courseware/courseware-admin.module.js
@@ -0,0 +1,47 @@
+const getDefaultState = () => {
+    return {
+        adminViewMode: 'templates',
+        showAddTemplateDialog: false,
+    };
+};
+
+const initialState = getDefaultState();
+
+const getters = {
+    adminViewMode(state) {
+        return state.adminViewMode;
+    },
+    showAddTemplateDialog(state) {
+        return state.showAddTemplateDialog;
+    },
+};
+
+export const state = { ...initialState };
+
+export const actions = {
+    // setters
+    adminViewMode(context, view) {
+        context.commit('setAdminViewMode', view);
+    },
+    showAddTemplateDialog(context, showDialog) {
+        context.commit('setShowAddTemplateDialog', showDialog);
+    },
+
+    // other actions
+};
+
+export const mutations = {
+    setAdminViewMode(state, mode) {
+        state.adminViewMode = mode;
+    },
+    setShowAddTemplateDialog(state, showDialog) {
+        state.showAddTemplateDialog = showDialog;
+    },
+};
+
+export default {
+    state,
+    actions,
+    mutations,
+    getters,
+};
diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js
index 8a4326f0fa6..dd3c61bbbb1 100755
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -23,8 +23,10 @@ const getDefaultState = () => {
         urlHelper: null,
         userId: null,
         viewMode: 'read',
+        dashboardViewMode: 'default',
         filingData: {},
         userIsTeacher: false,
+        teacherStatusLoaded: false,
 
         showStructuralElementEditDialog: false,
         showStructuralElementAddDialog: false,
@@ -33,6 +35,8 @@ const getDefaultState = () => {
         showStructuralElementDeleteDialog: false,
         showStructuralElementOerDialog: false,
 
+        structuralElementSortMode: false,
+
         importFilesState: '',
         importFilesProgress: 0,
         importStructuresState: '',
@@ -41,6 +45,11 @@ const getDefaultState = () => {
 
         exportState: '',
         exportProgress: 0,
+
+        purposeFilter: 'all',
+        showOverviewElementAddDialog: false,
+
+        bookmarkFilter: 'all',
     };
 };
 
@@ -86,6 +95,9 @@ const getters = {
     viewMode(state) {
         return state.viewMode;
     },
+    dashboardViewMode(state) {
+        return state.dashboardViewMode;
+    },
     showToolbar(state) {
         return state.showToolbar;
     },
@@ -119,6 +131,9 @@ const getters = {
     userIsTeacher(state) {
         return state.userIsTeacher;
     },
+    teacherStatusLoaded(state) {
+        return state.teacherStatusLoaded;
+    },
     pluginManager(state) {
         return state.pluginManager;
     },
@@ -143,6 +158,12 @@ const getters = {
     showStructuralElementDeleteDialog(state) {
         return state.showStructuralElementDeleteDialog;
     },
+    showOverviewElementAddDialog(state) {
+        return state.showOverviewElementAddDialog;
+    },
+    structuralElementSortMode(state) {
+        return state.structuralElementSortMode;
+    },
     importFilesState(state) {
         return state.importFilesState;
     },
@@ -164,6 +185,12 @@ const getters = {
     exportProgress(state) {
         return state.exportProgress;
     },
+    purposeFilter(state) {
+        return state.purposeFilter;
+    },
+    bookmarkFilter(state) {
+        return state.bookmarkFilter;
+    },
 };
 
 export const state = { ...initialState };
@@ -198,11 +225,42 @@ export const actions = {
         };
         const relationship = 'file-refs';
 
-        return dispatch('file-refs/loadRelated', { parent, relationship }, { root: true })
-            .then(() => rootGetters['file-refs/related']({
+        return dispatch('file-refs/loadRelated', { parent, relationship }, { root: true }).then(() =>
+            rootGetters['file-refs/related']({
                 parent,
                 relationship,
-            }));
+            })
+        );
+    },
+
+    async loadCoursewareActivities({ dispatch, rootGetters }, { userId, courseId }) {
+        const parent = {
+            type: 'users',
+            id: userId,
+        };
+        const relationship = 'activitystream';
+
+        const options = {
+            'filter[context_type]': 'course',
+            'filter[context_id]': courseId,
+            'filter[object_type]': 'courseware',
+            include: 'actor, context, object',
+        };
+
+        await dispatch('users/loadRelated', { parent, relationship, options }, { root: true });
+
+        const activities = rootGetters['users/all'];
+
+        for (const activity of activities) {
+            //load parents for breadcrumb
+            if (activity.type == 'activities') {
+                await this.dispatch('courseware-structural-elements/loadById', {
+                    id: activity.relationships.object.meta['object-id'],
+                });
+            }
+        }
+
+        return activities;
     },
 
     async createFile(context, { file, filedata, folder }) {
@@ -317,8 +375,8 @@ export const actions = {
             // console.log(resp);
         });
     },
-    async copyStructuralElement({ dispatch, getters, rootGetters }, { parentId, element }) {
-        const copy = { data: { parent_id: parentId, }, };
+    async copyStructuralElement({ dispatch, getters, rootGetters }, { parentId, element, removePurpose }) {
+        const copy = { data: { parent_id: parentId, remove_purpose: removePurpose } };
 
         const result = await state.httpClient.post(`courseware-structural-elements/${element.id}/copy`, copy);
         const id = result.data.data.id;
@@ -456,6 +514,28 @@ export const actions = {
         return dispatch('loadStructuralElement', currentId);
     },
 
+    async createStructuralElementWithTemplate({ dispatch }, { attributes, parentId, currentId, templateId }) {
+        const data = {
+            attributes,
+            relationships: {
+                parent: {
+                    data: {
+                        type: 'courseware-structural-elements',
+                        id: parentId,
+                    },
+                },
+            },
+            templateId: templateId,
+        };
+        await dispatch('courseware-structural-elements/create', data, { root: true });
+
+        const options = {
+            include: 'children',
+        };
+
+        return dispatch('courseware-structural-elements/loadById', { id: currentId, options }, { root: true });
+    },
+
     async deleteStructuralElement({ dispatch }, { id, parentId }) {
         const data = {
             id: id,
@@ -631,6 +711,10 @@ export const actions = {
         context.commit('coursewareViewModeSet', view);
     },
 
+    setDashboardViewMode(context, view) {
+        context.commit('setDashboardViewMode', view);
+    },
+
     coursewareShowToolbar(context, toolbar) {
         context.commit('coursewareShowToolbarSet', toolbar);
     },
@@ -672,50 +756,58 @@ export const actions = {
     },
 
     showElementEditDialog(context, bool) {
-        context.commit('setShowStructuralElementEditDialog', bool)
+        context.commit('setShowStructuralElementEditDialog', bool);
     },
 
     showElementAddDialog(context, bool) {
-        context.commit('setShowStructuralElementAddDialog', bool)
+        context.commit('setShowStructuralElementAddDialog', bool);
     },
 
     showElementExportDialog(context, bool) {
-        context.commit('setShowStructuralElementExportDialog', bool)
+        context.commit('setShowStructuralElementExportDialog', bool);
     },
 
     showElementInfoDialog(context, bool) {
-        context.commit('setShowStructuralElementInfoDialog', bool)
+        context.commit('setShowStructuralElementInfoDialog', bool);
     },
 
     showElementOerDialog(context, bool) {
-        context.commit('setShowStructuralElementOerDialog', bool)
+        context.commit('setShowStructuralElementOerDialog', bool);
     },
 
     showElementDeleteDialog(context, bool) {
-        context.commit('setShowStructuralElementDeleteDialog', bool)
+        context.commit('setShowStructuralElementDeleteDialog', bool);
     },
 
-    setImportFilesState({commit}, state ) {
-        commit('setImportFilesState', state)
+    setShowOverviewElementAddDialog(context, bool) {
+        context.commit('setShowOverviewElementAddDialog', bool);
     },
-    setImportFilesProgress({commit}, percent ) {
-        commit('setImportFilesProgress', percent)
+
+    setStructuralElementSortMode({ commit }, bool) {
+        commit('setStructuralElementSortMode', bool);
     },
-    setImportStructuresState({commit}, state ) {
-        commit('setImportStructuresState', state)
+
+    setImportFilesState({ commit }, state) {
+        commit('setImportFilesState', state);
     },
-    setImportStructuresProgress({commit}, percent ) {
-        commit('setImportStructuresProgress', percent)
+    setImportFilesProgress({ commit }, percent) {
+        commit('setImportFilesProgress', percent);
     },
-    setImportErrors({commit}, errors) {
+    setImportStructuresState({ commit }, state) {
+        commit('setImportStructuresState', state);
+    },
+    setImportStructuresProgress({ commit }, percent) {
+        commit('setImportStructuresProgress', percent);
+    },
+    setImportErrors({ commit }, errors) {
         commit('setImportErrors', errors);
     },
 
-    setExportState({commit}, state) {
-        commit('setExportState', state)
+    setExportState({ commit }, state) {
+        commit('setExportState', state);
     },
-    setExportProgress({commit}, percent) {
-        commit('setExportProgress', percent)
+    setExportProgress({ commit }, percent) {
+        commit('setExportProgress', percent);
     },
 
     addBookmark({ dispatch, rootGetters }, structuralElement) {
@@ -811,7 +903,7 @@ export const actions = {
                     parent,
                     relationship,
                     options: optionsWithPages,
-                    resetRelated: false
+                    resetRelated: false,
                 },
                 { root: true }
             );
@@ -819,6 +911,24 @@ export const actions = {
         } while (rootGetters[`${type}/all`].length < rootGetters[`${type}/lastMeta`].page.total);
     },
 
+    loadUsersBookmarks({ dispatch, rootGetters, state }, userId) {
+        const parent = {
+            type: 'users',
+            id: userId,
+        };
+        const relationship = 'courseware-bookmarks';
+        const options = {
+            include: 'course',
+        };
+
+        return dispatch('loadRelatedPaginated', {
+            type: 'courseware-structural-elements',
+            parent,
+            relationship,
+            options,
+        });
+    },
+
     async loadUsersCourses({ dispatch, rootGetters, state }, { userId, withCourseware }) {
         const parent = {
             type: 'users',
@@ -826,13 +936,13 @@ export const actions = {
         };
         const relationship = 'course-memberships';
         const options = {
-            include: 'course'
+            include: 'course',
         };
         await dispatch('loadRelatedPaginated', {
             type: 'course-memberships',
             parent,
             relationship,
-            options
+            options,
         });
 
         const memberships = rootGetters['course-memberships/related']({
@@ -844,7 +954,7 @@ export const actions = {
         for (let membership of memberships) {
             if (
                 membership.attributes.permission === 'dozent' &&
-                    state.context.id !== membership.relationships.course.data.id
+                state.context.id !== membership.relationships.course.data.id
             ) {
                 const course = rootGetters['courses/related']({ parent: membership, relationship: 'course' });
                 if (!withCourseware) {
@@ -938,6 +1048,77 @@ export const actions = {
 
         return dispatch('loadFeedback', blockId);
     },
+
+    async createTaskGroup({ dispatch, rootGetters }, { taskGroup }) {
+        await dispatch('courseware-task-groups/create', taskGroup, { root: true });
+
+        const id = taskGroup.relationships.target.data.id;
+        const target = rootGetters['courseware-structural-elements/byId']({ id });
+
+        return dispatch('courseware-structure/loadDescendants', { root: target });
+    },
+
+    async loadTask({ dispatch }, { taskId }) {
+        return dispatch(
+            'courseware-tasks/loadById',
+            {
+                id: taskId,
+                options: {
+                    include: 'solver,task-group,task-group.lecturer',
+                },
+            },
+            { root: true }
+        );
+    },
+
+    async updateTask({ dispatch }, { attributes, taskId }) {
+        const task = {
+            type: 'courseware-tasks',
+            attributes: attributes,
+            id: taskId,
+        };
+        await dispatch('courseware-tasks/update', task, { root: true });
+
+        return dispatch('loadTask', { taskId: task.id });
+    },
+
+    async deleteTask({ dispatch }, { task }) {
+        const data = {
+            id: task.id,
+        };
+        await dispatch('courseware-tasks/delete', data, { root: true });
+    },
+
+    async createTaskFeedback({ dispatch }, { taskFeedback }) {
+        await dispatch('courseware-task-feedback/create', taskFeedback, { root: true });
+
+        return dispatch('loadTask', { taskId: taskFeedback.relationships.task.data.id });
+    },
+
+    async updateTaskFeedback({ dispatch }, { attributes, taskFeedbackId }) {
+        const taskFeedback = {
+            type: 'courseware-task-feedback',
+            attributes: attributes,
+            id: taskFeedbackId,
+        };
+        await dispatch('courseware-task-feedback/update', taskFeedback, { root: true });
+
+        return dispatch('courseware-task-feedback/loadById', { id: taskFeedback.id }, { root: true });
+    },
+
+    async deleteTaskFeedback({ dispatch }, { taskFeedbackId }) {
+        const data = {
+            id: taskFeedbackId,
+        };
+        await dispatch('courseware-task-feedback/delete', data, { root: true });
+    },
+
+    setPurposeFilter({ commit }, purpose) {
+        commit('setPurposeFilter', purpose);
+    },
+    setBookmarkFilter({ commit }, course) {
+        commit('setBookmarkFilter', course);
+    },
 };
 
 /* eslint no-param-reassign: ["error", { "props": false }] */
@@ -971,6 +1152,10 @@ export const mutations = {
         state.viewMode = data;
     },
 
+    setDashboardViewMode(state, data) {
+        state.dashboardViewMode = data;
+    },
+
     coursewareShowToolbarSet(state, data) {
         state.showToolbar = data;
     },
@@ -1012,6 +1197,7 @@ export const mutations = {
     },
 
     setUserIsTeacher(state, isTeacher) {
+        state.teacherStatusLoaded = true;
         state.userIsTeacher = isTeacher;
     },
 
@@ -1047,6 +1233,14 @@ export const mutations = {
         state.showStructuralElementDeleteDialog = showDelete;
     },
 
+    setShowOverviewElementAddDialog(state, showAdd) {
+        state.showOverviewElementAddDialog = showAdd;
+    },
+
+    setStructuralElementSortMode(state, mode) {
+        state.structuralElementSortMode = mode;
+    },
+
     setImportFilesState(state, importFilesState) {
         state.importFilesState = importFilesState;
     },
@@ -1071,8 +1265,13 @@ export const mutations = {
     },
     setExportProgress(state, exportProgress) {
         state.exportProgress = exportProgress;
-    }
-
+    },
+    setPurposeFilter(state, purpose) {
+        state.purposeFilter = purpose;
+    },
+    setBookmarkFilter(state, course) {
+        state.bookmarkFilter = course;
+    },
 };
 
 export default {
-- 
GitLab