diff --git a/lib/models/Courseware/BlockTypes/BiographyAchievements.json b/lib/models/Courseware/BlockTypes/BiographyAchievements.json new file mode 100755 index 0000000000000000000000000000000000000000..ae55410c74455a8204bf93f211ac691ab4586c60 --- /dev/null +++ b/lib/models/Courseware/BlockTypes/BiographyAchievements.json @@ -0,0 +1,29 @@ +{ + "title": "Payload schema of Courseware\\BlockType\\BiographyAchievements", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "date": { + "type": "integer", + "minimum": 0 + }, + "end_date": { + "type": "integer", + "minimum": 0 + }, + "role": { + "type": "string" + }, + "description": { + "type": "string" + } + + }, + "required": [], + "additionalProperties": false +} diff --git a/lib/models/Courseware/BlockTypes/BiographyAchievements.php b/lib/models/Courseware/BlockTypes/BiographyAchievements.php new file mode 100755 index 0000000000000000000000000000000000000000..7c7c38e150fad315857c94c222473ca0d02aabdc --- /dev/null +++ b/lib/models/Courseware/BlockTypes/BiographyAchievements.php @@ -0,0 +1,66 @@ +<?php + +namespace Courseware\BlockTypes; + +use Opis\JsonSchema\Schema; + +/** + * This class represents the content of a Courseware biography achievements block. + * + * @author Robin Winkler <rwinkler@uos.de> + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.2 + */ +class BiographyAchievements extends BlockType +{ + public static function getType(): string + { + return 'biography-achievements'; + } + + public static function getTitle(): string + { + return _('Erfolge'); + } + + public static function getDescription(): string + { + return _('Zeigt verschiedene Arten von erreichten Erfolgen an.'); + } + + public function initialPayload(): array + { + return [ + 'type' => 'certificate', + 'title' => '', + 'date' => time() * 1000, + 'end_date' => time() * 1000, + 'role' => '', + 'description' => '' + ]; + } + + public static function getJsonSchema(): Schema + { + $schemaFile = __DIR__.'/BiographyAchievements.json'; + + return Schema::fromJsonString(file_get_contents($schemaFile)); + } + + public static function getCategories(): array + { + return ['biography']; + } + + public static function getContentTypes(): array + { + return ['text']; + } + + public static function getFileTypes(): array + { + return []; + } +} diff --git a/lib/models/Courseware/BlockTypes/BiographyCareer.json b/lib/models/Courseware/BlockTypes/BiographyCareer.json new file mode 100755 index 0000000000000000000000000000000000000000..798d04e3509b96614d99862ea178a21e28a7606e --- /dev/null +++ b/lib/models/Courseware/BlockTypes/BiographyCareer.json @@ -0,0 +1,14 @@ +{ + "title": "Payload schema of Courseware\\BlockType\\BiographyCareer", + "type": "object", + "properties": { + "sort": { + "type": "string" + }, + "items": { + "type": "array" + } + }, + "required": [], + "additionalProperties": false +} diff --git a/lib/models/Courseware/BlockTypes/BiographyCareer.php b/lib/models/Courseware/BlockTypes/BiographyCareer.php new file mode 100755 index 0000000000000000000000000000000000000000..ec443df3eb64f2ee4a45a0a70b22b00f4dfab853 --- /dev/null +++ b/lib/models/Courseware/BlockTypes/BiographyCareer.php @@ -0,0 +1,72 @@ +<?php + +namespace Courseware\BlockTypes; + +use Opis\JsonSchema\Schema; + +/** + * This class represents the content of a Courseware biography career block. + * + * @author Robin Winkler <rwinkler@uos.de> + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.2 + */ +class BiographyCareer extends BlockType +{ + public static function getType(): string + { + return 'biography-career'; + } + + public static function getTitle(): string + { + return _('Karriere'); + } + + public static function getDescription(): string + { + return _('Stellt die Stationen Ihrer schulischen, akademischen und beruflichen Qualifikationen, sowie Ihre Berufserfahrung dar.'); + } + + public function initialPayload(): array + { + return [ + 'sort' => 'asc', + 'items' => [[ + 'date' => '', + 'title' => '', + 'description' => '', + 'type' => 'school', + 'qualification' => '', + 'focus' => '', + 'skills' => '', + 'employer' => '', + 'job' => '', + ]] + ]; + } + + public static function getJsonSchema(): Schema + { + $schemaFile = __DIR__.'/BiographyCareer.json'; + + return Schema::fromJsonString(file_get_contents($schemaFile)); + } + + public static function getCategories(): array + { + return ['biography']; + } + + public static function getContentTypes(): array + { + return ['data']; + } + + public static function getFileTypes(): array + { + return []; + } +} diff --git a/lib/models/Courseware/BlockTypes/BiographyGoals.json b/lib/models/Courseware/BlockTypes/BiographyGoals.json new file mode 100755 index 0000000000000000000000000000000000000000..0c93910f38d1629a62003239359caf4d3685c3f6 --- /dev/null +++ b/lib/models/Courseware/BlockTypes/BiographyGoals.json @@ -0,0 +1,14 @@ +{ + "title": "Payload schema of Courseware\\BlockType\\BiographyGoals", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["type"], + "additionalProperties": false +} diff --git a/lib/models/Courseware/BlockTypes/BiographyGoals.php b/lib/models/Courseware/BlockTypes/BiographyGoals.php new file mode 100755 index 0000000000000000000000000000000000000000..7dc46384b5ca88e4f1c066456166164c00588516 --- /dev/null +++ b/lib/models/Courseware/BlockTypes/BiographyGoals.php @@ -0,0 +1,62 @@ +<?php + +namespace Courseware\BlockTypes; + +use Opis\JsonSchema\Schema; + +/** + * This class represents the content of a Courseware biography personal goals block. + * + * @author Robin Winkler <rwinkler@uos.de> + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.2 + */ +class BiographyGoals extends BlockType +{ + public static function getType(): string + { + return 'biography-goals'; + } + + public static function getTitle(): string + { + return _('Ziele'); + } + + public static function getDescription(): string + { + return _('Präsentiert eines Ihrer Ziele.'); + } + + public function initialPayload(): array + { + return [ + 'type' => 'personal', + 'description' => '' + ]; + } + + public static function getJsonSchema(): Schema + { + $schemaFile = __DIR__.'/BiographyGoals.json'; + + return Schema::fromJsonString(file_get_contents($schemaFile)); + } + + public static function getCategories(): array + { + return ['biography']; + } + + public static function getContentTypes(): array + { + return ['text']; + } + + public static function getFileTypes(): array + { + return []; + } +} diff --git a/lib/models/Courseware/BlockTypes/BiographyPersonalInformation.json b/lib/models/Courseware/BlockTypes/BiographyPersonalInformation.json new file mode 100755 index 0000000000000000000000000000000000000000..1a7bbac230ee03758afcfc34e6bfd79c9055637d --- /dev/null +++ b/lib/models/Courseware/BlockTypes/BiographyPersonalInformation.json @@ -0,0 +1,24 @@ +{ + "title": "Payload schema of Courseware\\BlockType\\BiographyPersonalInformation", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "birthplace": { + "type": "string" + }, + "birthday": { + "type": "integer", + "minimum": 0 + }, + "gender": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [], + "additionalProperties": false +} diff --git a/lib/models/Courseware/BlockTypes/BiographyPersonalInformation.php b/lib/models/Courseware/BlockTypes/BiographyPersonalInformation.php new file mode 100755 index 0000000000000000000000000000000000000000..5c637fa125221f6a7393caacd30bc7fc207272fc --- /dev/null +++ b/lib/models/Courseware/BlockTypes/BiographyPersonalInformation.php @@ -0,0 +1,65 @@ +<?php + +namespace Courseware\BlockTypes; + +use Opis\JsonSchema\Schema; + +/** + * This class represents the content of a Courseware biography personal information block. + * + * @author Robin Winkler <rwinkler@uos.de> + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.2 + */ +class BiographyPersonalInformation extends BlockType +{ + public static function getType(): string + { + return 'biography-personal-information'; + } + + public static function getTitle(): string + { + return _('Persönliche Informationen'); + } + + public static function getDescription(): string + { + return _('Zeigt persönliche Daten an.'); + } + + public function initialPayload(): array + { + return [ + 'name' => '', + 'birthplace' => '', + 'birthday' => time() * 1000, + 'gender' => 'none', + 'status' => 'none' + ]; + } + + public static function getJsonSchema(): Schema + { + $schemaFile = __DIR__.'/BiographyPersonalInformation.json'; + + return Schema::fromJsonString(file_get_contents($schemaFile)); + } + + public static function getCategories(): array + { + return ['biography']; + } + + public static function getContentTypes(): array + { + return ['text']; + } + + public static function getFileTypes(): array + { + return []; + } +} diff --git a/lib/models/Courseware/BlockTypes/BlockType.php b/lib/models/Courseware/BlockTypes/BlockType.php index 64cd5cc7bf8fbefae27fee0f5cbac7b533e681da..13364d56d4d3c9cd8bb24341cfbbbf03f3728d75 100644 --- a/lib/models/Courseware/BlockTypes/BlockType.php +++ b/lib/models/Courseware/BlockTypes/BlockType.php @@ -88,6 +88,10 @@ abstract class BlockType $blockTypes = [ Audio::class, BeforeAfter::class, + BiographyAchievements::class, + BiographyCareer::class, + BiographyGoals::class, + BiographyPersonalInformation::class, Canvas::class, Chart::class, Code::class, @@ -106,6 +110,7 @@ abstract class BlockType Link::class, TableOfContents::class, Text::class, + Timeline::class, Typewriter::class, Video::class, ]; diff --git a/lib/models/Courseware/BlockTypes/Timeline.json b/lib/models/Courseware/BlockTypes/Timeline.json new file mode 100755 index 0000000000000000000000000000000000000000..51b7ae06ec385541efe2481d793961da4a38db01 --- /dev/null +++ b/lib/models/Courseware/BlockTypes/Timeline.json @@ -0,0 +1,17 @@ +{ + "title": "Payload schema of Courseware\\BlockType\\Timeline", + "type": "object", + "properties": { + "sort": { + "type": "string" + }, + "dateformat": { + "type": "string" + }, + "items": { + "type": "array" + } + }, + "required": [], + "additionalProperties": false +} diff --git a/lib/models/Courseware/BlockTypes/Timeline.php b/lib/models/Courseware/BlockTypes/Timeline.php new file mode 100755 index 0000000000000000000000000000000000000000..16491e45665b69ac65197a858533d4211df936b3 --- /dev/null +++ b/lib/models/Courseware/BlockTypes/Timeline.php @@ -0,0 +1,69 @@ +<?php + +namespace Courseware\BlockTypes; + +use Opis\JsonSchema\Schema; + +/** + * This class represents the content of a Courseware timeline block. + * + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.0 + */ +class Timeline extends BlockType +{ + public static function getType(): string + { + return 'timeline'; + } + + public static function getTitle(): string + { + return _('Zeitstrahl'); + } + + public static function getDescription(): string + { + return _('Kann beliebig viele Ereignisse in zeitlicher Reihenfolge darstellen.'); + } + + public function initialPayload(): array + { + return [ + 'sort' => 'asc', + 'dateformat' => 'year', + 'items' => [[ + 'date' => '', + 'time' => '', + 'color' => 'studip-blue', + 'icon' => 'courseware', + 'title' => '', + 'description' => '' + ]] + ]; + } + + public static function getJsonSchema(): Schema + { + $schemaFile = __DIR__.'/Timeline.json'; + + return Schema::fromJsonString(file_get_contents($schemaFile)); + } + + public static function getCategories(): array + { + return ['layout']; + } + + public static function getContentTypes(): array + { + return ['data']; + } + + public static function getFileTypes(): array + { + return []; + } +} diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index 412b5c3c0e5e9e6ed5114f17e6076971643bece1..ed57ee883aa253c71b07d626d57a96c76bbf6d52 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -74,11 +74,32 @@ $blockadder-items: ( link: link-extern, table-of-contents: table-of-contents, text: edit, + timeline: date-cycle, typewriter: block-typewriter, video: video2, accordion: block-accordion, list: view-list, - tabs: block-tabs + tabs: block-tabs, + biography-achievements: medal, + biography-career: ranking, + biography-personal-information: own-license, + biography-goals: radar +); + +$achievement-types: ( + certificate: file-text, + accreditation: vcard, + award: medal, + book: literature, + publication: news, + membership: group3, +); + +$goals-types: ( + personal: person2, + school: doctoral-cap, + academic: doctoral-cap, + professional: tools, ); $media-buttons: ( @@ -1690,6 +1711,10 @@ $icons: ( display: block; height: unset; } + + form.default { + padding-left: 10px; + } } &.cw-course-manager-tabs { .cw-tab { @@ -4030,6 +4055,104 @@ i m a g e m a p b l o c k i m a g e m a p b l o c k e n d * * * * * * * * * * * * * * * * */ +/* * * * * * * * * * * * * * +b i o g r a p h y b l o c k +* * * * * * * * * * * * * */ + +.cw-block-biography { + .cw-block-biography-content { + display: flex; + min-height: 200px; + flex-direction: row; + padding: 2em 2em 2em 1em; + border: 2px solid $base-color; + + .cw-block-biography-type { + margin: auto 1em auto 0; + padding-top: 96px; + min-width: 192px; + max-width: 192px; + text-align: center; + background-repeat: no-repeat; + background-position: center top; + } + + .cw-block-biography-details { + h2, h3 { + margin-top: 0; + } + } + + } +} + +/* * * * * * * * * * * * * * * * * * +b i o g r a p h y b l o c k e n d +* * * * * * * * * * * * * * * * * */ + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * +b i o g r a p h y a c h i e v e m e n t s b l o c k +* * * * * * * * * * * * * * * * * * * * * * * * * * */ + +.cw-block-biography-achievements { + @each $type, $icon in $achievement-types { + .cw-block-biography-achievements-type-#{$type} { + @include background-icon($icon, clickable, 96); + } + } +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +b i o g r a p h y a c h i e v e m e n t s b l o c k e n d +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/* * * * * * * * * * * * ** * * * * * * +b i o g r a p h y g o a l s b l o c k +* * * * * * * * * * * * * * * * * * * */ + +.cw-block-biography-goals { + @each $type, $icon in $goals-types { + .cw-block-biography-goals-type-#{$type} { + @include background-icon($icon, clickable, 96); + } + } +} + +/* * * * * * * * * * * * * * * * * * * * * * * +b i o g r a p h y g o a l s b l o c k e n d +* * * * * * * * * * * * * * * * * * * * * * */ + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +b i o g r a p h y p e r s o n a l i n f o r m a t i o n b l o c k +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +.cw-block-biography-personal-information { + .cw-block-biography-content { + min-height: 140px; + + .cw-block-biography-personal-information-type { + @include background-icon(person2, clickable, 96); + } + + .cw-block-biography-personal-information-details { + display: grid; + max-height: 7em; + grid-template-columns: max-content 1fr; + grid-gap: 5px 10px; + + .preface { + grid-column-start: 1; + grid-column-end: 3; + } + } + } +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +b i o g r a p h y p e r s o n a l i n f o r m a t i o n b l o c k e n d +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + /* * * * * * * * * * l i n k b l o c k * * * * * * * * * */ @@ -4689,6 +4812,257 @@ text block text block end */ +/* * * * * * * * + t i m e l i n e +* * * * * * * */ +// Mixins and Placeholders +%clearfix { + &:after, &:before { + content: ''; + display: block; + width: 100%; + clear: both; + } +} + +// Timeline +.cw-timeline { + list-style: none; + width: 100%; + margin: 30px auto; + position: relative; + padding: 0; + transition: all 0.4s ease; + + &:before { + content:""; + width: 3px; + height: 100%; + background: $content-color-40; + left: 50%; + top: 0; + position: absolute; + } + + &:after { + content: ""; + clear: both; + display: table; + width: 100%; + } + + .cw-timeline-item { + margin-bottom: 50px; + position: relative; + @extend %clearfix; + + .cw-timeline-item-icon { + background: $white; + width: 50px; + height: 50px; + position: absolute; + top: 0; + left: 50%; + overflow: hidden; + margin-left: -25px; + @each $name, $color in $tile-colors { + &.cw-timeline-item-icon-color-#{"" + $name} { + border: solid 2px $color; + } + } + + border-radius: 50%; + + img { + position: relative; + top: 9px; + left: 9px; + + &.charcoal { + filter: brightness(0) saturate(100%) invert(22%) sepia(29%) saturate(364%) hue-rotate(168deg) brightness(87%) contrast(79%); + } + &.royal-purple { + filter: brightness(0) saturate(100%) invert(35%) sepia(43%) saturate(658%) hue-rotate(234deg) brightness(100%) contrast(87%); + } + &.iguana-green { + filter: brightness(0) saturate(100%) invert(74%) sepia(9%) saturate(1885%) hue-rotate(76deg) brightness(86%) contrast(88%); + } + &.queen-blue { + filter: brightness(0) saturate(100%) invert(44%) sepia(10%) saturate(2086%) hue-rotate(178deg) brightness(88%) contrast(80%); + } + &.verdigris { + filter: brightness(0) saturate(100%) invert(64%) sepia(11%) saturate(4959%) hue-rotate(131deg) brightness(103%) contrast(49%); + } + &.mulberry { + filter: brightness(0) saturate(100%) invert(49%) sepia(16%) saturate(1665%) hue-rotate(271deg) brightness(88%) contrast(95%); + } + &.pumpkin { + filter: brightness(0) saturate(100%) invert(38%) sepia(86%) saturate(1993%) hue-rotate(13deg) brightness(104%) contrast(108%); + } + &.sunglow { + filter: brightness(0) saturate(100%) invert(93%) sepia(69%) saturate(6824%) hue-rotate(313deg) brightness(102%) contrast(100%); + } + &.apple-green { + filter: brightness(0) saturate(100%) invert(69%) sepia(5%) saturate(5203%) hue-rotate(42deg) brightness(100%) contrast(84%); + } + &.studip-blue { + filter: brightness(0) saturate(100%) invert(26%) sepia(19%) saturate(1783%) hue-rotate(177deg) brightness(96%) contrast(93%); + } + &.studip-lightblue { + filter: brightness(0) saturate(100%) invert(91%) sepia(12%) saturate(190%) hue-rotate(190deg) brightness(104%) contrast(89%); + } + &.studip-red { + filter: brightness(0) saturate(100%) invert(8%) sepia(95%) saturate(6904%) hue-rotate(1deg) brightness(95%) contrast(109%); + } + &.studip-green { + filter: brightness(0) saturate(100%) invert(27%) sepia(85%) saturate(1531%) hue-rotate(109deg) brightness(95%) contrast(101%); + } + &.studip-yellow { + filter: brightness(0) saturate(100%) invert(94%) sepia(14%) saturate(7314%) hue-rotate(330deg) brightness(103%) contrast(101%); + } + &.studip-gray { + filter: brightness(0) saturate(100%) invert(46%) sepia(1%) saturate(2621%) hue-rotate(169deg) brightness(87%) contrast(87%); + } + } + } + + .cw-timeline-item-content { + width: 40%; + background: $white; + padding: 20px; + transition: all 0.3s ease; + + h3 { + padding: 15px; + color: #fff; + margin: -20px -20px 0 -20px; + font-weight: 700; + min-height: 1.1em; + } + + article { + min-height: 2em; + border: solid thin $content-color-20; + border-top: none; + margin: 0 -20px; + padding: 15px; + + header { + font-weight: 700; + font-size: 1.1em; + margin: 0.5em 0; + } + } + + &:before { + content: ''; + position: absolute; + left: calc(40% + 40px); + top: 18px; + width: 0; + height: 0; + border-top: 7px solid transparent; + border-bottom: 7px solid transparent; + } + &.left { + h3 { + text-align: right; + } + } + &.right { + float: right; + + h3 { + text-align: left; + } + + &:before { + content: ''; + right: calc(40% + 40px); + left: inherit; + border-left: 0; + } + } + + @each $name, $color in $tile-colors { + &.cw-timeline-item-content-color-#{"" + $name} { + border-color: $color; + h3 { + background-color: $color; + } + &.left { + &:before { + border-left: 7px solid $color; + } + } + &.right { + &:before { + border-right: 7px solid $color; + } + } + } + } + } + } +} +@mixin oneSidedTimeline() { + .cw-timeline { + &:before { + left: 25px; + } + .cw-timeline-item { + .cw-timeline-item-icon { + left: 25px; + } + .cw-timeline-item-content { + width: stretch; + margin-left: 70px; + &.left { + float: unset; + + h3 { + text-align: left; + } + + &:before { + content: ''; + right: calc(100% - 70px); + left: inherit; + border-left: 0; + } + } + &.right { + float: unset; + &:before { + right: calc(100% - 70px); + } + } + + @each $name, $color in $tile-colors { + &.cw-timeline-item-content-color-#{"" + $name} { + &.left { + &:before { + border-right: 7px solid $color; + border-left: none; + } + } + } + } + } + } + } +} +.cw-container-colspan-half { + @include oneSidedTimeline(); +} + +@media only screen and (max-width: 1070px) { + @include oneSidedTimeline(); +} +/* * * * * * * * * * * * + t i m e l i n e e n d +* * * * * * * * * * * */ + /* cw tiles */ diff --git a/resources/vue/components/courseware/CoursewareBiographyAchievementsBlock.vue b/resources/vue/components/courseware/CoursewareBiographyAchievementsBlock.vue new file mode 100755 index 0000000000000000000000000000000000000000..7a3ce6dc82edd5618d5c54b63d478880b4f96f5f --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBiographyAchievementsBlock.vue @@ -0,0 +1,202 @@ +<template> + <div class="cw-block cw-block-biography cw-block-biography-achievements"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @showEdit="setShowEdit" + @closeEdit="initCurrentData" + > + <template #content> + <div class="cw-block-biography-content" > + <div class="cw-block-biography-type" :class="'cw-block-biography-achievements-type-' + currentData.type"> + <h2>{{ typeName }}</h2> + </div> + <div class="cw-block-biography-details"> + <h3> + <translate>Titel</translate>: {{currentData.title}} + </h3> + <h4> + <span v-show="currentData.type !== 'membership'"><translate>Datum</translate>:</span> + <span v-show="currentData.type === 'membership'"><translate>Startdatum</translate>:</span> + {{ getReadableDate(currentData.date) }} + </h4> + <h4 v-show="hasEndDate"> + <translate>Enddatum</translate>: {{ getReadableDate(currentData.end_date)}} + </h4> + <h4 v-show="hasParticipation"> + <translate>Beteiligung</translate>: <span v-html="currentData.role"></span> + </h4> + <div> + <h4><translate>Beschreibung</translate>:</h4> + <p v-html="currentData.description"></p> + </div> + </div> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Type</translate> + <select v-model="currentData.type"> + <option value="certificate"><translate>Zertifikat</translate></option> + <option value="accreditation"><translate>Akkreditierung</translate></option> + <option value="award"><translate>Auszeichnung</translate></option> + <option value="book"><translate>Buch</translate></option> + <option value="publication"><translate>Veröffentlichung</translate></option> + <option value="membership"><translate>Mitgliedschaft</translate></option> + </select> + </label> + <label> + <translate>Titel</translate> + <input type="text" v-model="currentData.title"> + </label> + <label> + <span v-show="!hasEndDate"><translate>Datum</translate></span> + <span v-show="hasEndDate"><translate>Startdatum</translate></span> + <input type="date" v-model="currentData.date" /> + </label> + <label v-show="hasEndDate"> + <translate>Enddatum</translate> + <input type="date" v-model="currentData.end_date" /> + </label> + <label v-show="hasParticipation"> + <translate>Beteiligung</translate> + <input type="text" v-model="currentData.role"> + </label> + <label> + <translate>Beschreibung</translate> + <studip-wysiwyg v-model="currentData.description"></studip-wysiwyg> + </label> + </form> + </template> + <template #info><translate>Informationen zum Erfolge-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import { mapActions } from 'vuex'; +import { blockMixin } from './block-mixin.js'; +import StudipIcon from '../StudipIcon.vue'; +import StudipWysiwyg from '../StudipWysiwyg.vue'; + +export default { + name: 'courseware-biography-achievements-block', + mixins: [blockMixin], + components: { + CoursewareDefaultBlock, + StudipIcon, + StudipWysiwyg, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentData: {}, + showEdit: false, + } + }, + computed: { + payload() { + return this.block?.attributes?.payload; + }, + typeName() { + switch (this.currentData.type) { + case 'certificate': + return this.$gettext('Zertifikat'); + case 'accreditation': + return this.$gettext('Akkreditierung'); + case 'award': + return this.$gettext('Auszeichnung'); + case 'book': + return this.$gettext('Buch'); + case 'publication': + return this.$gettext('Veröffentlichung'); + case 'membership': + return this.$gettext('Mitgliedschaft'); + default: + return ''; + } + }, + achievementClass() { + switch (this.currentData.type) { + case 'certificate': + case 'accreditation': + case 'award': + return 'certificate'; + case 'book': + case 'publication': + return 'publication'; + case 'membership': + return 'membership'; + default: + return ''; + } + }, + hasParticipation() { + return ['book', 'publication'].includes(this.currentData.type); + }, + hasEndDate() { + return this.currentData.type === 'membership'; + }, + }, + mounted() { + this.initCurrentData(); + }, + updated() { + this.updateCanvas(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + if (this.payload) { + this.currentData = this.payload; + this.currentData.date = this.getInputDate(this.currentData.date); + this.currentData.end_date = this.getInputDate(this.currentData.end_date); + } + }, + setShowEdit(state) { + this.showEdit = state; + }, + getInputDate(inputDate) { + let date = new Date(inputDate); + return date.getFullYear() + '-' + + ('0' + (date.getMonth() + 1)).slice(-2) + '-' + + ('0' + date.getDate()).slice(-2); + }, + storeBlock() { + let attributes = {}; + attributes.payload = { + type: this.currentData.type, + title: this.currentData.title, + date: new Date(this.currentData.date).getTime(), + end_date: new Date(this.currentData.end_date).getTime(), + role: this.currentData.role, + description: this.currentData.description + }; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, + watch: { + payload() { + if (!this.showEdit) { + this.initCurrentData(); + } + }, + } +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBiographyCareerBlock.vue b/resources/vue/components/courseware/CoursewareBiographyCareerBlock.vue new file mode 100755 index 0000000000000000000000000000000000000000..fb20809899ac1d0ee3388e8b8a658758d6af089b --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBiographyCareerBlock.vue @@ -0,0 +1,262 @@ +<template> + <div class="cw-block cw-block-biography-career"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @closeEdit="initCurrentData" + @showEdit="setShowEdit" + @storeEdit="storeBlock" + > + <template #content> + <ol class="cw-timeline"> + <li + v-for="(item, index) in sortedItems" + :key="index" + class="cw-timeline-item" + > + <div class="cw-timeline-item-icon cw-timeline-item-icon-color-studip-blue"> + <studip-icon v-if="item.type === 'school'" shape="doctoral-cap" role="clickable" size="32"/> + <studip-icon v-if="item.type === 'experience'" shape="tools" role="clickable" size="32"/> + </div> + <div + class="cw-timeline-item-content cw-timeline-item-content-color-studip-blue" + :class="[index % 2 === 0 ? 'left' : 'right',]" + > + <h3>{{ item.date ? getReadableDate(item.date) : ''}}{{ item.enddate ? ' - ' + getReadableDate(item.enddate) : '' }}</h3> + <article> + <header>{{ getItemTypeName(item.type) }}</header> + <div v-if="item.type === 'school'"> + <p><translate>Bezeichnung der Qualifikation</translate>: {{ item.qualification }}</p> + <p><translate>Hauptfächer / Schwerpunkt</translate>: {{ item.focus }}</p> + <p><translate>berufliche Fähigkeiten</translate>: {{ item.skills }}</p> + </div> + <div v-if="item.type === 'experience'"> + <p><translate>Name des Arbeitgebers</translate>: {{ item.employer }}</p> + <p><translate>Beruf / Funktion</translate>: {{ item.job }}</p> + </div> + </article> + </div> + </li> + </ol> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Zeitliche Sortierung</translate> + <select v-model="currentSort"> + <option value="none"><translate>Keine</translate></option> + <option value="asc"><translate>Aufsteigend</translate></option> + <option value="desc"><translate>Absteigend</translate></option> + </select> + </label> + </form> + <button class="button add" @click="addItem"><translate>Ereignis hinzufügen</translate></button> + <courseware-tabs + v-if="currentItems.length > 0" + :setSelected="setItemTab" + @selectTab="setItemTab = (parseInt($event.name.replace($gettext('Ereignis') + ' ', '')) - 1)" + > + <courseware-tab + v-for="(item, index) in currentItems" + :key="index" + :index="index" + :name="$gettext('Ereignis') + ' ' + (index + 1).toString()" + :selected="index === 0" + canBeEmpty + > + <form class="default" @submit.prevent=""> + <label> + <translate>Startdatum</translate> + <input type="date" v-model="item.date" required /> + </label> + <label> + <translate>Enddatum</translate> + <input type="date" v-model="item.enddate" /> + </label> + <label> + <translate>Art</translate> + <select v-model="item.type"> + <option value="school"><translate>Schul- und Berufsbildung</translate></option> + <option value="experience"><translate>Berufserfahrung</translate></option> + </select> + </label> + <div v-show="item.type === 'school'"> + <label> + <translate>Bezeichnung der Qualifikation</translate> + <input type="text" v-model="item.qualification" /> + </label> + <label> + <translate>Hauptfächer / Schwerpunkt</translate> + <input type="text" v-model="item.focus" /> + </label> + <label> + <translate>berufliche Fähigkeiten</translate> + <input type="text" v-model="item.skills" /> + </label> + </div> + <div v-show="item.type === 'experience'"> + <label> + <translate>Name des Arbeitgebers</translate> + <input type="text" v-model="item.employer" /> + </label> + <label> + <translate>Beruf / Funktion</translate> + <input type="text" v-model="item.job" /> + </label> + </div> + <label> + <translate>Beschreibung</translate> + <textarea v-model="item.description" /> + </label> + <label v-if="currentItems.length > 1"> + <button class="button trash" @click="removeItem(index)"> + <translate>Ereignis entfernen</translate> + </button> + </label> + </form> + </courseware-tab> + </courseware-tabs> + </template> + <template #info><translate>Informationen zum Karriere-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareTabs from './CoursewareTabs.vue'; +import CoursewareTab from './CoursewareTab.vue'; +import { mapActions } from 'vuex'; +import { blockMixin } from './block-mixin.js'; +import StudipIcon from '../StudipIcon.vue'; + +export default { + name: 'courseware-biography-career-block', + mixins: [blockMixin], + components: { + CoursewareDefaultBlock, + CoursewareTabs, + CoursewareTab, + StudipIcon, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + showEdit: false, + setItemTab: 0, + currentItems: [], + currentSort: '', + } + }, + computed: { + items() { + return this.block?.attributes?.payload?.items; + }, + sort() { + return this.block?.attributes?.payload?.sort; + }, + sortedItems() { + if (this.currentSort === 'none') { + return this.currentItems; + } + let view = this; + let items = _.cloneDeep(this.currentItems); + return items.sort((a, b) => { + let dateA = null; + let dateB = null; + + if (a.time) { + dateA = new Date(a.date + 'T' + a.time); + } else { + dateA = new Date(a.date); + } + if (b.time) { + dateB = new Date(b.date + 'T' + b.time); + } else { + dateB = new Date(b.date); + } + if (view.currentSort === 'asc') { + return dateA > dateB ? 1 : dateA < dateB ? -1 : 0; + } + if (view.currentSort === 'desc') { + return dateA < dateB ? 1 : dateA > dateB ? -1 : 0; + } + }); + } + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + setShowEdit(state) { + this.showEdit = state; + }, + initCurrentData() { + this.currentItems = this.items; + this.currentSort = this.sort; + this.setItemTab = 0; + }, + addItem() { + this.currentItems.push({ + title: '', + description: '', + date: '', + enddate: '', + type: 'school', + qualification: '', + focus: '', + skills: '', + employer: '', + job: '', + }); + this.$nextTick(() => { this.setItemTab = this.currentItems.length - 1; }); + }, + removeItem(itemIndex){ + this.currentItems = this.currentItems.filter((val, index) => { + return !(index === itemIndex); + }); + this.$nextTick(() => { this.setItemTab = 0; }); + }, + getItemTypeName(type) { + switch (type) { + case 'school': + return this.$gettext('Schul- und Berufsbildung'); + case 'experience': + return this.$gettext('Berufserfahrung'); + } + + return ''; + }, + storeBlock() { + let attributes = { + payload: { + sort: this.currentSort, + items: this.currentItems + } + }; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, + watch: { + items() { + if (!this.showEdit) { + this.initCurrentData(); + } + }, + } +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBiographyGoalsBlock.vue b/resources/vue/components/courseware/CoursewareBiographyGoalsBlock.vue new file mode 100755 index 0000000000000000000000000000000000000000..4ff944ba4a20a8ede88dbec4341b090a3ecb8299 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBiographyGoalsBlock.vue @@ -0,0 +1,134 @@ +<template> + <div class="cw-block cw-block-biography cw-block-biography-goals"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @closeEdit="initCurrentData" + @showEdit="setShowEdit" + @storeEdit="storeBlock" + > + <template #content> + <div class="cw-block-biography-content" > + <div class="cw-block-biography-type" :class="'cw-block-biography-goals-type-' + currentData.type"> + <h2>{{ goalTypeName }}</h2> + </div> + <div class="cw-block-biography-details formatted-content" v-html="currentData.description"> + </div> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default"> + <label for="type"> + <span><translate>Ziel</translate></span> + <select name="type" class="type" v-model="currentData.type"> + <option value="personal"><translate>Persönliches Ziel</translate></option> + <option value="school"><translate>Schulisches Ziel</translate></option> + <option value="academic"><translate>Akademisches Ziel</translate></option> + <option value="professional"><translate>Berufliches Ziel</translate></option> + </select> + </label> + <label for="description"> + <span><translate>Beschreibung</translate></span> + <studip-wysiwyg name="description" v-model="currentData.description"></studip-wysiwyg> + </label> + </form> + </template> + <template #info><translate>Informationen zum Ziele-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import { mapActions } from 'vuex'; +import { blockMixin } from './block-mixin.js'; +import StudipWysiwyg from '../StudipWysiwyg.vue'; + +export default { + name: 'courseware-biography-goals-block', + mixins: [blockMixin], + components: { + CoursewareDefaultBlock, + StudipWysiwyg, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + showEdit: false, + currentData: { + type: '', + description: '' + }, + } + }, + computed: { + type() { + return this.block?.attributes?.payload?.type; + }, + description() { + return this.block?.attributes?.payload?.description; + }, + goalTypeName() { + switch (this.currentData.type) { + case 'personal': + return this.$gettext('Persönliches Ziel'); + case 'school': + return this.$gettext('Schulisches Ziel'); + case 'academic': + return this.$gettext('Akademisches Ziel'); + case 'professional': + return this.$gettext('Berufliches Ziel'); + } + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentData = { + type: this.type, + description: this.description + }; + }, + setShowEdit(state) { + this.showEdit = state; + }, + storeBlock() { + let attributes = { + payload: { + type: this.currentData.type, + description: this.currentData.description + } + }; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, + watch: { + type() { + if (!this.showEdit) { + this.currentData.type = this.type; + } + }, + description() { + if (!this.showEdit) { + this.currentData.description = this.description; + } + }, + } +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBiographyPersonalInformationBlock.vue b/resources/vue/components/courseware/CoursewareBiographyPersonalInformationBlock.vue new file mode 100755 index 0000000000000000000000000000000000000000..e8ee5b40ea535b53b0cf06185c0918ce0e9f1a0a --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBiographyPersonalInformationBlock.vue @@ -0,0 +1,187 @@ +<template> + <div class="cw-block cw-block-biography cw-block-biography-personal-information"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @closeEdit="initCurrentData" + @showEdit="setShowEdit" + @storeEdit="storeBlock" + > + <template #content> + <div class="cw-block-biography-content" > + <div class="cw-block-biography-type cw-block-biography-personal-information-type"> + <h2>{{currentData.name}}</h2> + </div> + <div class="cw-block-biography-details cw-block-biography-personal-information-details"> + <span><translate>Geburtsort</translate>:</span> + <span>{{currentData.birthplace}}</span> + + <span><translate>Geburtsdatum</translate>:</span> + <span>{{ getReadableDate(currentData.birthday) }}</span> + + <span><translate>Geschlecht</translate>:</span> + <span>{{displayGenderText(currentData.gender)}}</span> + + <span><translate>Familienstand</translate>:</span> + <span>{{ displayStatusText(currentData.status) }}</span> + </div> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Name</translate> + <input type="text" v-model="currentData.name"> + </label> + <label> + <translate>Geburtsort</translate> + <input type="text" v-model="currentData.birthplace"> + </label> + <label> + <translate>Geburtsdatum</translate> + <input type="date" v-model="currentData.birthday" /> + </label> + <label> + <translate>Geschlecht</translate> + <select v-model="currentData.gender"> + <option value="none">{{ displayGenderText('none') }}</option> + <option value="male">{{ displayGenderText('male') }}</option> + <option value="female">{{ displayGenderText('female') }}</option> + <option value="diverse">{{ displayGenderText('diverse') }}</option> + </select> + </label> + <label> + <translate>Familienstand</translate> + <select v-model="currentData.status"> + <option value="none">{{ displayStatusText('none') }}</option> + <option value="single">{{ displayStatusText('single') }}</option> + <option value="married">{{ displayStatusText('married') }}</option> + <option value="widowed">{{ displayStatusText('widowed') }}</option> + <option value="divorced">{{ displayStatusText('divorced') }}</option> + <option value="registered-civil-partnership">{{ displayStatusText('registered-civil-partnership') }}</option> + <option value="annulled-civil-partnership">{{ displayStatusText('annulled-civil-partnership') }}</option> + </select> + </label> + </form> + </template> + <template #info><translate>Informationen zum Persönlichen-Informationen-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import { mapActions } from 'vuex'; +import { blockMixin } from './block-mixin.js'; +import StudipIcon from '../StudipIcon.vue'; + +export default { + name: 'courseware-biography-personal-information-block', + mixins: [blockMixin], + components: { + CoursewareDefaultBlock, + StudipIcon, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + showEdit: false, + currentData: {}, + } + }, + computed: { + payload() { + return this.block?.attributes?.payload; + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + changeDate(date) { + this.currentData.birthday = date; + }, + initCurrentData() { + if (this.payload) { + this.currentData = this.payload; + this.currentData.birthday = this.getInputDate(this.currentData.birthday); + } + }, + getInputDate(inputDate) { + let date = new Date(inputDate); + return date.getFullYear() + '-' + + ('0' + (date.getMonth() + 1)).slice(-2) + '-' + + ('0' + date.getDate()).slice(-2); + }, + displayGenderText(gender) { + switch (gender) { + case 'none': + return this.$gettext('keine Angabe'); + case 'male': + return this.$gettext('männlich'); + case 'female': + return this.$gettext('Weiblich'); + case 'diverse': + return this.$gettext('divers'); + default: + return ''; + } + }, + displayStatusText(status) { + switch (status) { + case 'none': + return this.$gettext('keine Angabe'); + case 'single': + return this.$gettext('ledig'); + case 'married': + return this.$gettext('verheiratet'); + case 'widowed': + return this.$gettext('verwitwet'); + case 'divorced': + return this.$gettext('geschieden'); + case 'registered-civil-partnership': + return this.$gettext('eingetragene Lebenspartnerschaft'); + case 'annulled-civil-partnership': + return this.$gettext('aufgehobene Lebenspartnerschaft'); + default: + return ''; + } + }, + setShowEdit(state) { + this.showEdit = state; + }, + storeBlock() { + let attributes = {}; + attributes.payload = { + name: this.currentData.name, + birthplace: this.currentData.birthplace, + birthday: new Date(this.currentData.birthday).getTime(), + gender: this.currentData.gender, + status: this.currentData.status + }; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, + watch: { + payload() { + if (!this.showEdit) { + this.initCurrentData(); + } + }, + } +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareTimelineBlock.vue b/resources/vue/components/courseware/CoursewareTimelineBlock.vue new file mode 100755 index 0000000000000000000000000000000000000000..451478ecad9931330d0b7f8b55a1827539661ac5 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareTimelineBlock.vue @@ -0,0 +1,328 @@ +<template> + <div class="cw-block cw-block-timeline"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @closeEdit="initCurrentData" + @showEdit="setShowEdit" + @storeEdit="storeBlock" + > + <template #content> + <ol class="cw-timeline"> + <li + v-for="(item, index) in sortedItems" + :key="index" + class="cw-timeline-item" + > + <div class="cw-timeline-item-icon" :class="'cw-timeline-item-icon-color-' + item.color"> + <studip-icon :shape="item.icon" role="info" size="32" :class="item.color"/> + </div> + <div + class="cw-timeline-item-content" + :class="[index % 2 === 0 ? 'left' : 'right', 'cw-timeline-item-content-color-' + item.color]" + > + <h3 v-if="currentDateFormat !== 'none'">{{ getItemDate(item) }}</h3> + <h3 v-else>{{ item.title }}</h3> + <article> + <header v-if="currentDateFormat !== 'none'">{{ item.title }}</header> + <p>{{ item.description }}</p> + </article> + </div> + </li> + </ol> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Zeitliche Sortierung</translate> + <select v-model="currentSort"> + <option value="none"><translate>Keine</translate></option> + <option value="asc"><translate>Aufsteigend</translate></option> + <option value="desc"><translate>Absteigend</translate></option> + </select> + </label> + + <label> + <translate>Zeitangabe</translate> + <select v-model="currentDateFormat"> + <option value="year"><translate>Jahr</translate></option> + <option value="date"><translate>Datum</translate></option> + <option value="time"><translate>Zeit</translate></option> + <option value="datetime"><translate>Datum und Zeit</translate></option> + <option value="none"><translate>Keine</translate></option> + </select> + </label> + </form> + <button class="button add" @click="addItem"><translate>Ereignis hinzufügen</translate></button> + <courseware-tabs + v-if="currentItems.length > 0" + :setSelected="setItemTab" + @selectTab="setItemTab = (parseInt($event.name.replace($gettext('Ereignis') + ' ', '')) - 1)" + > + <courseware-tab + v-for="(item, index) in currentItems" + :key="index" + :index="index" + :name="$gettext('Ereignis') + ' ' + (index + 1).toString()" + :selected="index === 0" + canBeEmpty + > + <form class="default" @submit.prevent=""> + <label> + <translate>Titel</translate> + <input type="text" v-model="item.title" /> + </label> + <label> + <translate>Beschreibung</translate> + <textarea v-model="item.description" /> + </label> + <label> + <translate>Farbe</translate> + <studip-select + :options="colors" + label="icon" + :clearable="false" + :reduce="option => option.class" + v-model="item.color" + > + <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> + </studip-select> + </label> + <label> + <translate>Icon</translate> + <studip-select :options="icons" :clearable="false" v-model="item.icon"> + <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="option"> + <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span> + </template> + <template #option="option"> + <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span> + </template> + </studip-select> + </label> + <label> + <translate>Datum</translate> + <input type="date" v-model="item.date" required/> + </label> + <label> + <translate>Zeit</translate> + <input type="time" v-model="item.time" /> + </label> + <label v-if="currentItems.length > 1"> + <button class="button trash" @click="removeItem(index)"> + <translate>Ereignis entfernen</translate> + </button> + </label> + </form> + </courseware-tab> + </courseware-tabs> + </template> + <template #info><translate>Informationen zum Zeitstrahl-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareTabs from './CoursewareTabs.vue'; +import CoursewareTab from './CoursewareTab.vue'; +import { mapActions } from 'vuex'; +import { blockMixin } from './block-mixin.js'; +import contentIcons from './content-icons.js'; +import StudipIcon from '../StudipIcon.vue'; + +export default { + name: 'courseware-timeline-block', + mixins: [blockMixin], + components: { + CoursewareDefaultBlock, + CoursewareTabs, + CoursewareTab, + StudipIcon, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + showEdit: false, + setItemTab: 0, + currentItems: [], + currentSort: '', + currentDateFormat: '', + } + }, + computed: { + items() { + return this.block?.attributes?.payload?.items; + }, + sort() { + return this.block?.attributes?.payload?.sort; + }, + dateformat() { + return this.block?.attributes?.payload?.dateformat; + }, + icons() { + return contentIcons; + }, + colors() { + const colors = [ + {name: this.$gettext('Schwarz'), class: 'black', hex: '#000000', level: 100, icon: 'black', darkmode: true}, + + {name: this.$gettext('Blau'), class: 'studip-blue', hex: '#28497c', level: 100, icon: 'blue', darkmode: true}, + {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('Apfelgrün'), class: 'apple-green', hex: '#8bbd40', level: 80, icon: false, darkmode: true}, + ]; + + return colors; + }, + sortedItems() { + if (this.currentSort === 'none') { + return this.currentItems; + } + let view = this; + let items = _.cloneDeep(this.currentItems); + return items.sort((a, b) => { + let dateA = null; + let dateB = null; + + if (a.time) { + dateA = new Date(a.date + 'T' + a.time); + } else { + dateA = new Date(a.date); + } + if (b.time) { + dateB = new Date(b.date + 'T' + b.time); + } else { + dateB = new Date(b.date); + } + if (view.currentSort === 'asc') { + return dateA > dateB ? 1 : dateA < dateB ? -1 : 0; + } + if (view.currentSort === 'desc') { + return dateA < dateB ? 1 : dateA > dateB ? -1 : 0; + } + }); + } + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + setShowEdit(state) { + this.showEdit = state; + }, + initCurrentData() { + this.currentItems = this.items; + this.currentSort = this.sort; + this.currentDateFormat = this.dateformat; + this.setItemTab = 0; + }, + getItemDate(item) { + switch (this.currentDateFormat) { + case 'year': + if (item.date) { + return (new Date(item.date)).getFullYear(); + } + break; + case 'date': + if (item.date) { + return this.getReadableDate(item.date); + } + break; + case 'time': + if (item.time) { + return item.time; + } + break; + case 'datetime': + if (item.date && item.time) { + return this.getReadableDate(item.date) + ' ' + item.time; + } + if (!item.date && item.time) { + return '--.--.---- ' + item.time; + } + if (item.date && !item.time) { + return this.getReadableDate(item.date) + ' --:--'; + } + return '--.--.---- --:--' + } + + return ''; + }, + addItem() { + this.currentItems.push({ + title: '', + description: '', + date: '', + time: '', + color: 'studip-blue', + icon: 'courseware' + }); + this.$nextTick(() => { this.setItemTab = this.currentItems.length - 1; }); + }, + removeItem(itemIndex){ + this.currentItems = this.currentItems.filter((val, index) => { + return !(index === itemIndex); + }); + this.$nextTick(() => { this.setItemTab = 0; }); + }, + storeBlock() { + let attributes = { + payload: { + dateformat: this.currentDateFormat, + sort: this.currentSort, + items: this.currentItems + } + }; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, + watch: { + items() { + if (!this.showEdit) { + this.initCurrentData(); + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareToolsBlockadder.vue b/resources/vue/components/courseware/CoursewareToolsBlockadder.vue index 3ab0d2d223a985bae245517cd0089f44453633de..dde956fa20fdbe57c9e2bebd6f390b6f047b39b8 100644 --- a/resources/vue/components/courseware/CoursewareToolsBlockadder.vue +++ b/resources/vue/components/courseware/CoursewareToolsBlockadder.vue @@ -169,6 +169,7 @@ export default { { title: this.$gettext('Gestaltung'), type: 'layout' }, { title: this.$gettext('Dateien'), type: 'files' }, { title: this.$gettext('Externe Inhalte'), type: 'external' }, + { title: this.$gettext('Biografie'), type: 'biography' }, ]; }, maxHeight() { diff --git a/resources/vue/components/courseware/block-mixin.js b/resources/vue/components/courseware/block-mixin.js index 2e084baf116885a4362940dd1618c28d575d47f3..45eb6316b0aeaa73d7f477dfb332f165387ca115 100644 --- a/resources/vue/components/courseware/block-mixin.js +++ b/resources/vue/components/courseware/block-mixin.js @@ -20,5 +20,13 @@ export const blockMixin = { ...mapActions({ updateUserProgress: 'courseware-user-progresses/update', }), + getReadableDate(date) { + let locale = navigator.language ? navigator.language : 'de-DE'; + return new Date(date).toLocaleDateString(locale, { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + }, }, }; diff --git a/resources/vue/components/courseware/container-components.js b/resources/vue/components/courseware/container-components.js index 7ce04e483fc8e83a1e07e5eb0fc2892f82f8415e..2926a186e2555f04a6fa4d685147a8a1eca185b0 100644 --- a/resources/vue/components/courseware/container-components.js +++ b/resources/vue/components/courseware/container-components.js @@ -23,8 +23,14 @@ import CoursewareKeyPointBlock from './CoursewareKeyPointBlock.vue'; import CoursewareLinkBlock from './CoursewareLinkBlock.vue'; import CoursewareTableOfContentsBlock from './CoursewareTableOfContentsBlock.vue'; import CoursewareTextBlock from './CoursewareTextBlock.vue'; +import CoursewareTimelineBlock from './CoursewareTimelineBlock.vue'; import CoursewareTypewriterBlock from './CoursewareTypewriterBlock.vue'; import CoursewareVideoBlock from './CoursewareVideoBlock.vue'; +import CoursewareBiographyAchievementsBlock from './CoursewareBiographyAchievementsBlock.vue'; +import CoursewareBiographyCareerBlock from './CoursewareBiographyCareerBlock.vue'; +import CoursewareBiographyGoalsBlock from './CoursewareBiographyGoalsBlock.vue'; +import CoursewareBiographyPersonalInformationBlock from './CoursewareBiographyPersonalInformationBlock.vue'; + const ContainerComponents = { CoursewareDefaultBlock, @@ -52,8 +58,13 @@ const ContainerComponents = { CoursewareLinkBlock, CoursewareTableOfContentsBlock, CoursewareTextBlock, + CoursewareTimelineBlock, CoursewareTypewriterBlock, CoursewareVideoBlock, + CoursewareBiographyAchievementsBlock, + CoursewareBiographyCareerBlock, + CoursewareBiographyGoalsBlock, + CoursewareBiographyPersonalInformationBlock, }; export default ContainerComponents;