diff --git a/i18n/messages.en.js b/i18n/messages.en.js index 18e988de..b7b668c2 100644 --- a/i18n/messages.en.js +++ b/i18n/messages.en.js @@ -39,6 +39,10 @@ module.exports = { menu_panel_close: 'Close', menu_panel_menu: 'Menu', }, + showMoreContent: { + show: 'Show more', + hide: 'Hide', + }, bankIdAlertText: `Please note that Students not located in Sweden may have problems attending a course at KTH.
You could meet obstacles if you're required to pay fees or if you do not have a Swedish Mobile BankID. `, courseLabels: { label_course_description: 'Introduction to course', @@ -100,7 +104,7 @@ module.exports = { 'Course offering may be cancelled if number of admitted are less than minimum of places. If there are more applicants than number of places selection will be made.', round_seats_info: 'The selection results are based on:', syllabus_info: - '

• A course goes different course offerings. To see information about a specific course offering, choose semester and course offering. The course syllabus information will be updated depending on the chosen semester. Information from the course syllabus is marked with *.

• Please note: regulations in course syllabus are rules that are generally applicable and binding for both employees and students.

• If you have not chosen semester and course offering, you will see course information from the current or future course syllabus. The valid period of the course syllabus is stated on the page.

', + '

A course goes different course offerings. To see information about a specific course offering, choose semester and course offering. The course syllabus information will be updated depending on the chosen semester. Information from the course syllabus is marked with *

', sideMenu: { aria_label: 'Sub menu', page_about_course: 'About course', diff --git a/i18n/messages.se.js b/i18n/messages.se.js index 93ebd0ca..230d3360 100644 --- a/i18n/messages.se.js +++ b/i18n/messages.se.js @@ -40,6 +40,10 @@ module.exports = { menu_panel_close: 'Stäng', menu_panel_menu: 'Meny', }, + showMoreContent: { + show: 'Visa mer', + hide: 'Dölj', + }, bankIdAlertText: 'Du behöver ett KTH-konto för att läsa en kurs på KTH, kontot aktiveras med Mobilt BankID eller genom att besöka KTH:s campus. Det enda sättet att starta en kurs utan att besöka campus, är om du har Mobilt BankID.', courseLabels: { @@ -102,7 +106,7 @@ module.exports = { 'Kursomgången kan komma att ställas in om antalet antagna understiger minimiantalet platser. Vid fler sökande än platser kommer urval att ske.', round_seats_info: 'Urvalet sker baserat på:', syllabus_info: - '

• En kurs undervisas i olika kursomgångar. För att se information om en specifik kursomgång behöver du välja termin och kursomgång. Information från kursplan kommer att uppdateras beroende på vald termin. Information från kursplan är markerad med *.

• Observera: bestämmelser i kursplaner är regler som är generellt tillämpbara och bindande för såväl anställda som studenter.

• Har du inte valt termin och kursomgång ser du kursinformation från nuvarande eller kommande kursplan. På sidan anges den period som information från kursplan gäller för.

', + '

En kurs undervisas i olika kursomgångar. För att se information om en specifik kursomgång behöver du välja termin och kursomgång. Information från kursplan kommer att uppdateras beroende på vald termin. Information från kursplan är markerad med *

', sideMenu: { aria_label: 'Undermeny', page_about_course: 'Om kursen ', diff --git a/package-lock.json b/package-lock.json index 4f745037..0cc1ebb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@kth/monitor": "^4.3.1", "@kth/server": "^4.1.0", "@kth/session": "^3.0.9", - "@kth/style": "^0.18.12", + "@kth/style": "^1.4.2", "@kth/ug-rest-api-helper": "^1.0.26", "axios": "^1.7.2", "body-parser": "^1.20.2", @@ -3539,9 +3539,9 @@ } }, "node_modules/@kth/style": { - "version": "0.18.12", - "resolved": "https://registry.npmjs.org/@kth/style/-/style-0.18.12.tgz", - "integrity": "sha512-QtRdFEJcpHNgtUdqrpCn9EVXOQw9Iu6tYLsFO0qgCfgbWWMzSq8d52NoV6rJ6JSNeqhA8tnwdXHprhtLTstA/w==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@kth/style/-/style-1.4.2.tgz", + "integrity": "sha512-Z6EhjUVzhw+QITpnFsdLu51xogsgEXQiB1E+hEI2HsaFxTOrxkq6qbnN/uK1WWeTuKfi+A6szaVubPXswWRtyw==", "peerDependencies": { "react": "*" } @@ -4339,17 +4339,6 @@ "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -6007,9 +5996,9 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -6020,7 +6009,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -7950,18 +7939,18 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/enhanced-resolve": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", - "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "license": "MIT", "dependencies": { @@ -9032,37 +9021,37 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -9347,13 +9336,13 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -14036,10 +14025,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "license": "MIT" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -14068,9 +14060,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -14993,9 +14985,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "license": "MIT" }, "node_modules/path-type": { @@ -15672,12 +15664,12 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -16562,9 +16554,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -16600,6 +16592,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -16617,15 +16618,15 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -18628,13 +18629,12 @@ } }, "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -18643,7 +18643,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", diff --git a/package.json b/package.json index 9c41771e..07a4a1c8 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@kth/monitor": "^4.3.1", "@kth/server": "^4.1.0", "@kth/session": "^3.0.9", - "@kth/style": "^0.18.12", + "@kth/style": "^1.4.2", "@kth/ug-rest-api-helper": "^1.0.26", "axios": "^1.7.2", "body-parser": "^1.20.2", diff --git a/public/css/_shared.scss b/public/css/_shared.scss index e3581ddc..2bea5ad9 100644 --- a/public/css/_shared.scss +++ b/public/css/_shared.scss @@ -16,6 +16,15 @@ } } } + .kth-local-navigation--mobile { + padding: 0; + } + label:has(+ .form-group), + label:has(+ input) { + @include typography.font-heading-xs; + display: block; + margin-bottom: 0.25rem; + } } #mainContent { @@ -159,10 +168,3 @@ p { .select-wrapper::after { background-color: var(--color-primary); } - -label:has(+ .form-group), -label:has(+ input) { - @include typography.font-heading-xs; - display: block; - margin-bottom: 0.25rem; -} diff --git a/public/css/kursinfo-web.scss b/public/css/kursinfo-web.scss index d3a6665a..8a4aefd8 100644 --- a/public/css/kursinfo-web.scss +++ b/public/css/kursinfo-web.scss @@ -93,19 +93,35 @@ gap: 1rem; flex-direction: column; - align-items: flex-start; + align-items: stretch; + + .select-wrapper { + width: 100%; + } @media (min-width: 768px) { flex-direction: row; align-items: flex-end; + .select-wrapper { + width: 360px; + } } } } .roundInformation { + position: relative; padding: 20px; margin-bottom: 30px; background-color: var(--color-background-alt); + .person { + min-height: 31px; + img { + max-width: 40px; + overflow: hidden; + white-space: nowrap; + } + } h3 { margin-block-end: 1rem; @@ -140,11 +156,81 @@ margin-bottom: 1.5rem; } + .roundInformation__infoGridItem { + display: flex; + flex-direction: column; + + .roundInformation__infoGridItemContent { + position: relative; + overflow: hidden; + max-height: 96px; + transition: max-height 200ms ease-in; + + &.hidden::after { + content: ''; + position: absolute; + bottom: 0; + height: 24px; + width: 100%; + background: linear-gradient(to bottom, transparent, var(--color-background-alt)); + display: block; + } + } + + .roundInformation__infoGridItemShowMoreButton { + background-color: transparent; + border: none; + padding: 0; + margin-top: 4px; + color: var(--color-tertiary); + cursor: pointer; + align-self: flex-end; + + &:hover { + text-decoration: underline; + } + } + } + .kth-alert { background-color: var(--color-background); } } +.shimmer-effect::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(-45deg, var(--color-background-alt) 40%, #fafafa 50%, var(--color-background-alt) 60%); + background-size: 300%; + background-position-x: 100%; + animation: shimmer 1s infinite linear; + z-index: 10; + pointer-events: none; +} +.fadeIn > * { + animation: fadeIn 1.5s ease forwards; +} + +@keyframes shimmer { + to { + background-position-x: 0%; + } +} +@keyframes fadeIn { + 0% { + opacity: 0; + } + 33.33% { + opacity: 0; + } + 100% { + opacity: 1; + } +} .course-section-list { @include prose.prose; diff --git a/public/js/app/components/CourseSectionList.jsx b/public/js/app/components/CourseSectionList.jsx index 8c656c18..58e84920 100644 --- a/public/js/app/components/CourseSectionList.jsx +++ b/public/js/app/components/CourseSectionList.jsx @@ -165,16 +165,6 @@ function CourseSectionList({ courseInfo = {}, partToShow, syllabus = {}, syllabu header: translation.courseInformation.course_supplemental_information, text: courseInfo.course_supplemental_information, }) - if (!isMissingInfoLabel(courseInfo.course_supplemental_information_url)) - prepare.push({ - header: translation.courseInformation.course_supplemental_information_url, - text: courseInfo.course_supplemental_information_url, - }) - if (!isMissingInfoLabel(courseInfo.course_supplemental_information_url_text)) - prepare.push({ - header: translation.courseInformation.course_supplemental_information_url_text, - text: courseInfo.course_supplemental_information_url_text, - }) if (!isContractEducation() && syllabus.course_additional_regulations !== '') prepare.push({ diff --git a/public/js/app/components/RoundInformation/PlannedModules.jsx b/public/js/app/components/RoundInformation/PlannedModules.jsx deleted file mode 100644 index 07190655..00000000 --- a/public/js/app/components/RoundInformation/PlannedModules.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import { usePlannedModules } from '../../hooks/usePlannedModules' - -export const PlannedModules = ({ courseCode, courseRound, selectedSemester }) => { - const { plannedModules } = usePlannedModules({ - courseCode, - semester: selectedSemester, - applicationCode: courseRound.round_application_code, - }) - return -} diff --git a/public/js/app/components/RoundInformation/RoundInformation.jsx b/public/js/app/components/RoundInformation/RoundInformation.jsx index 75addb3c..638f7d4c 100644 --- a/public/js/app/components/RoundInformation/RoundInformation.jsx +++ b/public/js/app/components/RoundInformation/RoundInformation.jsx @@ -1,9 +1,11 @@ -import React from 'react' +import React, { useEffect, useMemo, useState } from 'react' import Alert from '../../components-shared/Alert' import BankIdAlert from '../../components/BankIdAlert' import { useLanguage } from '../../hooks/useLanguage' import { useRoundUtils } from '../../hooks/useRoundUtils' +import { useCourseEmployees } from '../../hooks/useCourseEmployees' +import { usePlannedModules } from '../../hooks/usePlannedModules' import { RoundInformationInfoGrid } from './RoundInformationInfoGrid' import { RoundInformationContacts } from './RoundInformationContacts' @@ -14,13 +16,57 @@ function RoundInformation({ courseCode, courseData, courseRound, semesterRoundSt const selectedRoundHeader = createRoundHeader(courseRound) const { selectedSemester } = semesterRoundState + const memoizedCourseRound = useMemo(() => courseRound, [courseRound]) + + const memoizedParams = useMemo( + () => ({ + courseCode, + selectedSemester, + applicationCode: memoizedCourseRound?.round_application_code, + }), + [courseCode, selectedSemester, memoizedCourseRound?.round_application_code] + ) + + const { + courseRoundEmployees, + isError: courseEmployeesError, + isLoading: courseEmployeesLoading, + } = useCourseEmployees(memoizedParams) + + const { + plannedModules, + isError: plannedModulesError, + isLoading: plannedModulesIsLoading, + } = usePlannedModules(memoizedParams) + + const isLoading = courseEmployeesLoading || plannedModulesIsLoading + const isError = courseEmployeesError || plannedModulesError + + const [isLoaderVisible, setIsLoaderVisible] = useState(false) + + useEffect(() => { + let timer + if (isLoading) { + setIsLoaderVisible(true) + } else { + timer = setTimeout(() => { + setIsLoaderVisible(false) + }, 300) + } + return () => clearTimeout(timer) + }, [isLoading]) + return ( -
+

{translation.courseRoundInformation.round_header} {selectedRoundHeader}

- + @@ -29,12 +75,7 @@ function RoundInformation({ courseCode, courseData, courseRound, semesterRoundSt )}

{translation.courseLabels.header_contact}

- +
) } diff --git a/public/js/app/components/RoundInformation/RoundInformationContacts.jsx b/public/js/app/components/RoundInformation/RoundInformationContacts.jsx index a80bfdde..2c9a100e 100644 --- a/public/js/app/components/RoundInformation/RoundInformationContacts.jsx +++ b/public/js/app/components/RoundInformation/RoundInformationContacts.jsx @@ -1,16 +1,10 @@ import React from 'react' import { useLanguage } from '../../hooks/useLanguage' import { useMissingInfo } from '../../hooks/useMissingInfo' -import { useCourseEmployees } from '../../hooks/useCourseEmployees' -function RoundInformationContacts({ courseCode, courseData, courseRound, selectedSemester }) { +function RoundInformationContacts({ courseData, courseRoundEmployees }) { const { translation } = useLanguage() const { missingInfoLabel } = useMissingInfo() - const { courseRoundEmployees } = useCourseEmployees({ - courseCode, - selectedSemester, - applicationCode: courseRound?.round_application_code, - }) return (
diff --git a/public/js/app/components/RoundInformation/RoundInformationInfoGrid.jsx b/public/js/app/components/RoundInformation/RoundInformationInfoGrid.jsx index 8ab06e78..cdd2cb98 100644 --- a/public/js/app/components/RoundInformation/RoundInformationInfoGrid.jsx +++ b/public/js/app/components/RoundInformation/RoundInformationInfoGrid.jsx @@ -1,12 +1,48 @@ -import React from 'react' +import React, { useEffect, useState, useRef } from 'react' import { useLanguage } from '../../hooks/useLanguage' import InfoModal from '../InfoModal' import { CourseMemoLink } from './CourseMemoLink' import { CourseScheduleLink } from './CourseScheduleLink' -import { PlannedModules } from './PlannedModules' + +// Calculates if a "Show more" button should be displayed, and creates props for content and button elements. +const useShowMoreContent = content => { + const ref = useRef(null) + const [hasMoreContent, setHasMoreContent] = useState(false) + const [contentHeight, setHeight] = useState(undefined) + const { translation } = useLanguage() + + const showMoreContent = hasMoreContent && !!contentHeight + const hideMoreContent = hasMoreContent && !showMoreContent + useEffect(() => { + if (ref.current) { + const el = ref.current + const isOverflowing = el.clientHeight < el.scrollHeight + setHasMoreContent(isOverflowing) + } + }, [content]) + + const contentProps = { + ref, + style: { maxHeight: contentHeight }, // set max height for animation from the max value specified in css to actual content height + className: `roundInformation__infoGridItemContent ${hideMoreContent ? 'hidden' : ''}`, + } + + const showMoreButtonProps = hasMoreContent + ? { + onClick: () => { + setHeight(showMoreContent ? undefined : ref.current.scrollHeight) + }, + children: showMoreContent ? translation.showMoreContent.hide : translation.showMoreContent.show, + className: 'roundInformation__infoGridItemShowMoreButton', + } + : undefined + + return { contentProps, showMoreButtonProps } +} const Item = ({ children, html, title, infoModalContent }) => { const { translation } = useLanguage() + const { contentProps, showMoreButtonProps } = useShowMoreContent(html ?? children) return (
@@ -21,12 +57,19 @@ const Item = ({ children, html, title, infoModalContent }) => { /> )} - {html ?
:
{children}
} +
+ {html ? ( +
+ ) : ( +
{children}
+ )} + {showMoreButtonProps &&
) } -function RoundInformationInfoGrid({ courseCode, courseRound, selectedSemester }) { +function RoundInformationInfoGrid({ courseCode, courseRound, plannedModules }) { const { translation } = useLanguage() return ( @@ -73,9 +116,7 @@ function RoundInformationInfoGrid({ courseCode, courseRound, selectedSemester })

{courseRound.round_seats || translation.courseRoundInformation.round_no_seats_limit}

- - - + diff --git a/public/js/app/components/RoundSelector/RoundSelector.jsx b/public/js/app/components/RoundSelector/RoundSelector.jsx index b469bcf6..4c9559ea 100644 --- a/public/js/app/components/RoundSelector/RoundSelector.jsx +++ b/public/js/app/components/RoundSelector/RoundSelector.jsx @@ -14,6 +14,7 @@ export const RoundSelector = ({ activeSemesters, semesterRoundState }) => { showRoundData, selectedSemester, activeSemesterOnlyHasOneRound, + hasOnlyOneRound, roundsForSelectedSemester, activeRound, } = semesterRoundState @@ -34,13 +35,13 @@ export const RoundSelector = ({ activeSemesters, semesterRoundState }) => { ariaLabel={translation.courseLabels.header_dropdown_menu_aria_label} /> -

- {hasActiveSemesters ? ( - translation.courseLabels.header_dropdown_menu_navigation - ) : ( + {hasActiveSemesters ? ( + !hasOnlyOneRound &&

{translation.courseLabels.header_dropdown_menu_navigation}

+ ) : ( +

{translation.courseLabels.lable_no_rounds} - )} -

+

+ )}
diff --git a/public/js/app/components/__tests__/RoundInformation.test.js b/public/js/app/components/__tests__/RoundInformation.test.js index 047838c2..1cc2adeb 100644 --- a/public/js/app/components/__tests__/RoundInformation.test.js +++ b/public/js/app/components/__tests__/RoundInformation.test.js @@ -25,7 +25,7 @@ const defaultSemesterRoundState = { } describe('Component ', () => { - beforeAll(() => { + beforeEach(() => { usePlannedModules.mockReturnValue({ plannedModules: 'somePlannedModules', }) @@ -72,7 +72,7 @@ describe('Component ', () => { courseRound: mockCourseRound, } - useCourseEmployees.mockReturnValueOnce({ + useCourseEmployees.mockReturnValue({ courseRoundEmployees: { examiners: `${examinersData}'`, responsibles: `${responsiblesData}`, diff --git a/public/js/app/components/__tests__/RoundInformationContacts.test.js b/public/js/app/components/__tests__/RoundInformationContacts.test.js index 201aea16..4871bcfe 100644 --- a/public/js/app/components/__tests__/RoundInformationContacts.test.js +++ b/public/js/app/components/__tests__/RoundInformationContacts.test.js @@ -22,14 +22,7 @@ const withinNextSibling = element => within(nextSibling(element)) describe('Component ', () => { describe('examiners, responsibles and teachers', () => { test('shoud show headers with "missing info" text when data is missing', () => { - render( - - ) + render() const examinerLabel = screen.getByText('Examiner') expect(nextSibling(examinerLabel)).toHaveTextContent('No information inserted') @@ -41,19 +34,14 @@ describe('Component ', () => { }) test('shoud show headers with data inserted as html', () => { - useCourseEmployees.mockReturnValue({ - courseRoundEmployees: { - examiners: '

Test examiners

', - responsibles: '

Test responsibles

', - teachers: '

Test teachers

', - }, - }) render( Test examiners

', + responsibles: '

Test responsibles

', + teachers: '

Test teachers

', + }} /> ) const examinerLabel = screen.getByText('Examiner') @@ -71,58 +59,12 @@ describe('Component ', () => { expect(teacherLink).toHaveTextContent('Test teachers') expect(teacherLink).toHaveAttribute('href', '/profile/testteachers/') }) - - test('should update data when prop changes', () => { - useCourseEmployees.mockImplementation(({ applicationCode }) => - applicationCode === '1111' - ? { courseRoundEmployees: { examiners: 'Examiner for 1111' } } - : applicationCode === '3333' - ? { courseRoundEmployees: { examiners: 'Examiner for 3333' } } - : { courseRoundEmployees: { examiners: '' } } - ) - - const { rerender } = render( - - ) - - expect(nextSibling(screen.getByText('Examiner'))).toHaveTextContent('Examiner for 1111') - - rerender( - - ) - expect(nextSibling(screen.getByText('Examiner'))).toHaveTextContent('No information inserted') - - rerender( - - ) - expect(nextSibling(screen.getByText('Examiner'))).toHaveTextContent('Examiner for 3333') - }) }) describe('cource contact', () => { test('should show header and content for course contact name', () => { render( - + ) const contactLabel = screen.getByText('Contact') expect(contactLabel).toBeInTheDocument() @@ -131,12 +73,7 @@ describe('Component ', () => { "shoud NOT show header if contact name is '%s'", contactNameArg => { render( - + ) const contactLabel = screen.queryByText('Contact') expect(contactLabel).not.toBeInTheDocument() diff --git a/public/js/app/hooks/__tests__/useApi.test.js b/public/js/app/hooks/__tests__/useApi.test.js index 13f6cfcd..04d8e07a 100644 --- a/public/js/app/hooks/__tests__/useApi.test.js +++ b/public/js/app/hooks/__tests__/useApi.test.js @@ -39,9 +39,10 @@ describe('useApi', () => { }) test('calls apiToCall with given parameters', async () => { - const { result } = renderHook(() => useApi(apiToCall, { one: 'one', two: 2 }, null, null)) + const apiParams1 = { one: 'one', two: 2 } + const { result } = renderHook(() => useApi(apiToCall, apiParams1, null, null)) - expect(apiToCall).toHaveBeenLastCalledWith({ one: 'one', two: 2 }) + expect(apiToCall).toHaveBeenLastCalledWith(apiParams1) await waitFor(() => expect(result.current.data).toStrictEqual('somePlannedModules')) @@ -50,15 +51,18 @@ describe('useApi', () => { data: 'someData', }) - const { result: result2 } = renderHook(() => useApi(anotherApiToCall, { two: 'two', three: 3 }, null, null)) + const apiParams2 = { two: 'two', three: 3 } - expect(anotherApiToCall).toHaveBeenLastCalledWith({ two: 'two', three: 3 }) + const { result: result2 } = renderHook(() => useApi(anotherApiToCall, apiParams2, null, null)) + + expect(anotherApiToCall).toHaveBeenLastCalledWith(apiParams2) await waitFor(() => expect(result2.current.data).toStrictEqual('someData')) }) test('returns data from apiToCall', async () => { - const { result } = renderHook(() => useApi(apiToCall, {}, null, null)) + const params = {} + const { result } = renderHook(() => useApi(apiToCall, params, null, null)) await waitFor(() => expect(result.current.data).toStrictEqual('somePlannedModules')) }) @@ -90,28 +94,28 @@ describe('useApi', () => { }) ) - const { result } = renderHook(() => useApi(apiToCall, defaultParams, defaultValue, null)) + const { result: firstResult } = renderHook(() => useApi(apiToCall, defaultParams, defaultValue, null)) - expect(result.current.data).toStrictEqual(defaultValue) - expect(result.current.isError).toStrictEqual(false) + expect(firstResult.current.data).toStrictEqual(defaultValue) + expect(firstResult.current.isError).toStrictEqual(false) jest.advanceTimersByTime(100) expect(apiToCall).toHaveBeenCalledTimes(1) - await waitFor(() => expect(result.current.data).toStrictEqual('someNewPlannedModules')) - await waitFor(() => expect(result.current.isError).toStrictEqual(false)) + await waitFor(() => expect(firstResult.current.data).toStrictEqual('someNewPlannedModules')) + await waitFor(() => expect(firstResult.current.isError).toStrictEqual(false)) - result.current.setApiParams({ other: 'params' }) + const { result: secodResult } = renderHook(() => useApi(apiToCall, defaultParams, defaultValue, null)) - await waitFor(() => expect(result.current.data).toStrictEqual(defaultValue)) - await waitFor(() => expect(result.current.isError).toStrictEqual(false)) + await waitFor(() => expect(secodResult.current.data).toStrictEqual(defaultValue)) + await waitFor(() => expect(secodResult.current.isError).toStrictEqual(false)) jest.advanceTimersByTime(100) expect(apiToCall).toHaveBeenCalledTimes(2) - await waitFor(() => expect(result.current.data).toStrictEqual('someOtherPlannedModules')) - await waitFor(() => expect(result.current.isError).toStrictEqual(false)) + await waitFor(() => expect(secodResult.current.data).toStrictEqual('someOtherPlannedModules')) + await waitFor(() => expect(secodResult.current.isError).toStrictEqual(false)) jest.useRealTimers() }) @@ -186,18 +190,19 @@ describe('useApi', () => { }) ) - const { result } = renderHook(() => useApi(apiToCall, defaultParams, null, null)) + const { result: firstResult } = renderHook(() => useApi(apiToCall, defaultParams, null, null)) jest.advanceTimersByTime(100) - await waitFor(() => expect(result.current.isError).toStrictEqual(true)) + await waitFor(() => expect(firstResult.current.isError).toStrictEqual(true)) - result.current.setApiParams({ ...defaultParams, applicationCode: 54321 }) + const newParams = { ...defaultParams, applicationCode: 54321 } + const { result: secodResult } = renderHook(() => useApi(apiToCall, newParams, null, null)) - await waitFor(() => expect(result.current.isError).toStrictEqual(false)) + await waitFor(() => expect(secodResult.current.isError).toStrictEqual(false)) jest.advanceTimersByTime(100) - await waitFor(() => expect(result.current.isError).toStrictEqual(true)) + await waitFor(() => expect(secodResult.current.isError).toStrictEqual(true)) jest.useRealTimers() }) diff --git a/public/js/app/hooks/__tests__/useSemesterRoundState.test.js b/public/js/app/hooks/__tests__/useSemesterRoundState.test.js index 6f1b158a..a8420778 100644 --- a/public/js/app/hooks/__tests__/useSemesterRoundState.test.js +++ b/public/js/app/hooks/__tests__/useSemesterRoundState.test.js @@ -477,4 +477,59 @@ describe('useSemesterRoundsLogic', () => { await waitFor(() => expect(result.current.showRoundData).toBe(false)) await waitFor(() => expect(result.current.activeRound).toEqual({})) }) + + describe('set hasOnlyOneRound and activeSemesterOnlyHasOneRound correctly', () => { + test('one semester with one round', async () => { + const { result } = renderHook(() => + useSemesterRoundState({ + initiallySelectedRoundIndex: undefined, + initiallySelectedSemester: 20222, + roundsBySemester: { 20222: roundsBySemester[20222] }, + syllabusList, + activeSemesters: [{ year: '2022', semesterNumber: '2', semester: '20222' }], + }) + ) + expect(result.current.activeSemesterOnlyHasOneRound).toBe(true) + expect(result.current.hasOnlyOneRound).toBe(true) + }) + test('one semester with multiple rounds', async () => { + const { result } = renderHook(() => + useSemesterRoundState({ + initiallySelectedRoundIndex: undefined, + initiallySelectedSemester: 20232, + roundsBySemester: { 20232: roundsBySemester[20232] }, + syllabusList, + activeSemesters: [{ year: '2023', semesterNumber: '2', semester: '20232' }], + }) + ) + expect(result.current.activeSemesterOnlyHasOneRound).toBe(false) + expect(result.current.hasOnlyOneRound).toBe(false) + }) + test('multiple semesters - selected has multiple rounds', async () => { + const { result } = renderHook(() => + useSemesterRoundState({ + initiallySelectedRoundIndex: undefined, + initiallySelectedSemester: 20232, + roundsBySemester, + syllabusList, + activeSemesters, + }) + ) + expect(result.current.activeSemesterOnlyHasOneRound).toBe(false) + expect(result.current.hasOnlyOneRound).toBe(false) + }) + test('multiple semesters - selected has one round', async () => { + const { result } = renderHook(() => + useSemesterRoundState({ + initiallySelectedRoundIndex: undefined, + initiallySelectedSemester: 20222, + roundsBySemester, + syllabusList, + activeSemesters, + }) + ) + expect(result.current.activeSemesterOnlyHasOneRound).toBe(true) + expect(result.current.hasOnlyOneRound).toBe(false) + }) + }) }) diff --git a/public/js/app/hooks/useApi.js b/public/js/app/hooks/useApi.js index f8b70421..2dd6d7c0 100644 --- a/public/js/app/hooks/useApi.js +++ b/public/js/app/hooks/useApi.js @@ -1,20 +1,26 @@ import { useEffect, useState } from 'react' import { STATUS } from './api/status' -export const useApi = (apiToCall, initialApiParams, defaultValue, defaulValueIfNullResponse) => { - const [apiParams, setApiParams] = useState(initialApiParams) +export const useApi = (apiToCall, apiParams, defaultValue, defaulValueIfNullResponse) => { const [data, setData] = useState(defaultValue) const [isError, setIsError] = useState(false) + const [isLoading, setIsLoading] = useState(true) useEffect(() => { const fetchData = async () => { + setIsLoading(true) setData(defaultValue) setIsError(false) - - const result = await apiToCall(apiParams) - - setData(result.data || defaulValueIfNullResponse) - setIsError(result.status === STATUS.ERROR) + try { + const result = await apiToCall(apiParams) + setData(result.data || defaulValueIfNullResponse) + setIsError(result.status === STATUS.ERROR) + } catch (error) { + setIsError(true) + setData(defaulValueIfNullResponse) + } finally { + setIsLoading(false) + } } fetchData() @@ -27,6 +33,6 @@ export const useApi = (apiToCall, initialApiParams, defaultValue, defaulValueIfN return { data, isError, - setApiParams, + isLoading, } } diff --git a/public/js/app/hooks/useCourseEmployees.js b/public/js/app/hooks/useCourseEmployees.js index 0b0a0df8..fb0186bf 100644 --- a/public/js/app/hooks/useCourseEmployees.js +++ b/public/js/app/hooks/useCourseEmployees.js @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { useMemo } from 'react' import { useWebContext } from '../context/WebContext' import { useApi } from './useApi' import { getCourseEmployees } from './api/getCourseEmployees' @@ -8,24 +8,21 @@ export const useCourseEmployees = ({ courseCode, selectedSemester, applicationCo const { uri } = context.paths.api.employees - const { data, isError, setApiParams } = useApi( - getCourseEmployees, - { + const requestData = useMemo( + () => ({ uri, courseCode, selectedSemester, applicationCode, - }, - {}, - {} + }), + [uri, courseCode, selectedSemester, applicationCode] ) - useEffect(() => { - setApiParams({ uri, courseCode, selectedSemester, applicationCode }) - }, [applicationCode, courseCode, selectedSemester, setApiParams, uri]) + const { data, isError, isLoading } = useApi(getCourseEmployees, requestData) return { courseRoundEmployees: data, isError, + isLoading, } } diff --git a/public/js/app/hooks/usePlannedModules.js b/public/js/app/hooks/usePlannedModules.js index 018dce15..5132ff96 100644 --- a/public/js/app/hooks/usePlannedModules.js +++ b/public/js/app/hooks/usePlannedModules.js @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { useMemo } from 'react' import { useWebContext } from '../context/WebContext' import { getPlannedModules } from './api/getPlannedModules' import { useApi } from './useApi' @@ -6,27 +6,29 @@ import { useMissingInfo } from './useMissingInfo' const MISSING_INFO = '' -export const usePlannedModules = ({ courseCode, semester, applicationCode }) => { +export const usePlannedModules = ({ courseCode, selectedSemester, applicationCode }) => { const context = useWebContext() const { missingInfoLabel } = useMissingInfo() const basePath = context.paths.api.plannedSchemaModules.uri - const { data, isError, setApiParams } = useApi( - getPlannedModules, - { basePath, courseCode, semester, applicationCode }, - null, - MISSING_INFO + const requestData = useMemo( + () => ({ + basePath, + courseCode, + semester: selectedSemester, + applicationCode, + }), + [basePath, courseCode, selectedSemester, applicationCode] ) - useEffect(() => { - setApiParams({ basePath, courseCode, semester, applicationCode }) - }, [applicationCode, basePath, courseCode, semester, setApiParams]) + const { data, isError, isLoading } = useApi(getPlannedModules, requestData, null, MISSING_INFO) const plannedModules = data === MISSING_INFO ? missingInfoLabel : data return { plannedModules, isError, + isLoading, } } diff --git a/public/js/app/hooks/useSemesterRoundState.js b/public/js/app/hooks/useSemesterRoundState.js index 6447d274..d796b2a6 100644 --- a/public/js/app/hooks/useSemesterRoundState.js +++ b/public/js/app/hooks/useSemesterRoundState.js @@ -34,6 +34,10 @@ const useSemesterRoundState = ({ () => determineSemesterOnlyHasOneRound(roundsBySemester, selectedSemester), [roundsBySemester, selectedSemester] ) + const hasOnlyOneRound = useMemo(() => { + if (activeSemesters.length !== 1) return false + return activeSemesterOnlyHasOneRound + }, [activeSemesterOnlyHasOneRound, activeSemesters.length]) const showRoundData = useMemo(() => { if (isSetSelectedRoundIndex || activeSemesterOnlyHasOneRound) { @@ -93,6 +97,7 @@ const useSemesterRoundState = ({ roundsForSelectedSemester, selectedSemester, showRoundData, + hasOnlyOneRound, activeSemesterOnlyHasOneRound, hasActiveSemesters, activeSyllabus, diff --git a/server/apiCalls/getFilteredData.js b/server/apiCalls/getFilteredData.js index 6005c783..b0c28b72 100644 --- a/server/apiCalls/getFilteredData.js +++ b/server/apiCalls/getFilteredData.js @@ -47,8 +47,6 @@ function _parseCourseDefaultInformation(courseDetails, language) { course_recruitment_text: parseOrSetEmpty(course.recruitmentText, language, true), course_required_equipment: parseOrSetEmpty(course.requiredEquipment, language), course_suggested_addon_studies: parseOrSetEmpty(course.addOn, language), - course_supplemental_information_url: parseOrSetEmpty(course.supplementaryInfoUrl, language), - course_supplemental_information_url_text: parseOrSetEmpty(course.supplementaryInfoUrlName, language), course_state: parseOrSetEmpty(course.state, language, true), } } diff --git a/server/controllers/__tests__/courseCtrl.test.js b/server/controllers/__tests__/courseCtrl.test.js index 9ff0e364..66ce6126 100644 --- a/server/controllers/__tests__/courseCtrl.test.js +++ b/server/controllers/__tests__/courseCtrl.test.js @@ -48,7 +48,6 @@ jest.mock('../../apiCalls/kursinfoApi', () => ({ getCourseInfo: () => ({ sellingText: { sv: '

Fantastisk kurs

', en: '

This course is awesome

' }, courseDisposition: { sv: '

Kursupplägg på svenska

', en: '

Course Disposition in english

' }, - supplementaryInfo: { sv: '

Övrig info

', en: '

Extra info

' }, imageInfo: 'own_image', }), })) @@ -145,9 +144,7 @@ describe('Discontinued course to test', () => { "course_required_equipment": "Ingen information tillagd", "course_state": "ESTABLISHED", "course_suggested_addon_studies": "Ingen information tillagd", - "course_supplemental_information": "

Övrig info

", - "course_supplemental_information_url": "Ingen information tillagd", - "course_supplemental_information_url_text": "Ingen information tillagd", + "course_supplemental_information": "", "imageFromAdmin": "own_image", "sellingText": "

Fantastisk kurs

", }, diff --git a/server/controllers/mocks/mockedDiscontinuedCourse.js b/server/controllers/mocks/mockedDiscontinuedCourse.js index ee6f4011..c535ad6d 100644 --- a/server/controllers/mocks/mockedDiscontinuedCourse.js +++ b/server/controllers/mocks/mockedDiscontinuedCourse.js @@ -11,7 +11,6 @@ const mockedDiscontinuedCourse = { }, educationalLevelCode: 'RESEARCH', gradeScaleCode: 'PF', - supplementaryInfo: '

Ersätter kurs FBB3640.

', title: 'Kolhydratteknik inom glykovetenskap', titleOther: 'Carbohydrate Technologies in Glycoscience', cancelled: false, diff --git a/server/views/helpers/index.js b/server/views/helpers/index.js index 66658a90..ee1d44ff 100644 --- a/server/views/helpers/index.js +++ b/server/views/helpers/index.js @@ -1,5 +1,5 @@ 'use strict' - +const handlebars = require('handlebars') const registerHeaderContentHelper = require('@kth/kth-node-web-common/lib/handlebars/helpers/headerContent') const { registerBreadcrumbHelper } = require('@kth/kth-node-web-common/lib/handlebars/helpers/breadcrumbs') const { registerLanguageLinkHelper } = require('@kth/kth-node-web-common/lib/handlebars/helpers/languageLink') @@ -41,3 +41,5 @@ require('@kth/kth-node-web-common/lib/handlebars/helpers/contentedit') const i18n = require('../../../i18n') require('@kth/kth-node-web-common/lib/handlebars/helpers/createI18nHelper')(i18n) require('@kth/kth-node-web-common/lib/handlebars/helpers/safe') + +handlebars.registerHelper('eq', (var1, var2) => var1.toString() === var2.toString()) diff --git a/server/views/partials/headerLogotype.handlebars b/server/views/partials/headerLogotype.handlebars index f9e8d7b7..6239219e 100644 --- a/server/views/partials/headerLogotype.handlebars +++ b/server/views/partials/headerLogotype.handlebars @@ -1,6 +1,9 @@
- Till KTH:s startsida + {{#if (eq theme "external")}} Till KTH:s startsida + {{else}} + Till KTH:s startsida + {{/if}}
\ No newline at end of file