diff --git a/i18n/messages.en.js b/i18n/messages.en.js index 71f62e5a..66ef20da 100644 --- a/i18n/messages.en.js +++ b/i18n/messages.en.js @@ -85,7 +85,7 @@ module.exports = { }, header_dropdown_menu_navigation: 'Choose semester and course offering to see current information and more about the course, such as course syllabus, study period, and application information.', - header_dropdown_menue: 'Information per course offering', + header_dropdown_menu: 'Information per course offering', header_dropdown_menu_aria_label: 'Information about choosing semester and course offering', header_course_info: 'Course information', header_content: 'Content and learning outcomes', @@ -104,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', @@ -166,6 +166,9 @@ module.exports = { course_department: 'Offered by', course_contact_name: 'Contact ', course_prerequisites: 'Recommended prerequisites', + course_prerequisites_description: + 'Describes the knowledge and skills (in addition to the eligibility requirements) that you need to be able to take the course.', + course_prerequisites_menu_aria_label: 'Information about recommended prerequisites', course_suggested_addon_studies: 'Add-on studies', course_supplemental_information_url: 'Supplementary information link', course_supplemental_information_url_text: 'Supplementary information link text', diff --git a/i18n/messages.se.js b/i18n/messages.se.js index 0b70093a..ffa4ecc4 100644 --- a/i18n/messages.se.js +++ b/i18n/messages.se.js @@ -87,7 +87,7 @@ module.exports = { }, header_dropdown_menu_navigation: 'Välj termin och kursomgång för att se aktuell information och mer om kursen, såsom kursplan, studieperiod och anmälningsinformation.', - header_dropdown_menue: 'Information per kursomgång', + header_dropdown_menu: 'Information per kursomgång', header_dropdown_menu_aria_label: 'Information om val av termin och kursomgång', header_course_info: 'Kursinformation', header_content: 'Innehåll och lärandemål', @@ -106,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 ', @@ -170,6 +170,9 @@ module.exports = { course_department: 'Ges av', course_contact_name: 'Kontaktperson', course_prerequisites: 'Rekommenderade förkunskaper', + course_prerequisites_description: + 'Beskriver vilka kunskaper och färdigheter (utöver behörighetskraven) som du behöver för att kunna ta till dig kursen.', + course_prerequisites_menu_aria_label: 'Information om rekommenderade förkunskaper', course_suggested_addon_studies: 'Påbyggnad', course_supplemental_information_url: 'Övrig information - länk', course_supplemental_information_url_text: 'Övrig information - länk text', diff --git a/package-lock.json b/package-lock.json index f0b9660a..7ff254b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "reactstrap": "^9.2.2", "sanitize-html": "^2.13.0", "victory": "^36.9.2", - "xlsx": "^0.18.5" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz" }, "devDependencies": { "@babel/cli": "^7.24.6", @@ -5231,15 +5231,6 @@ "node": ">=0.4.0" } }, - "node_modules/adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -6012,6 +6003,7 @@ "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", "content-type": "~1.0.5", @@ -6308,19 +6300,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/cfb": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "crc-32": "~1.2.0" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -6653,15 +6632,6 @@ "node": ">= 0.12.0" } }, - "node_modules/codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -6923,21 +6893,21 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", "license": "MIT", "dependencies": { - "cookie": "0.4.1", + "cookie": "0.7.2", "cookie-signature": "1.0.6" }, "engines": { @@ -6976,18 +6946,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -7954,6 +7912,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -9029,16 +8988,17 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -9128,12 +9088,12 @@ } }, "node_modules/express-session": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", - "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", "license": "MIT", "dependencies": { - "cookie": "0.6.0", + "cookie": "0.7.2", "cookie-signature": "1.0.7", "debug": "2.6.9", "depd": "~2.0.0", @@ -9146,15 +9106,6 @@ "node": ">= 0.8.0" } }, - "node_modules/express-session/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express-session/node_modules/cookie-signature": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", @@ -9177,9 +9128,9 @@ "license": "MIT" }, "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -9346,6 +9297,7 @@ "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": "~2.0.0", @@ -9514,15 +9466,6 @@ "node": ">= 0.6" } }, - "node_modules/frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -10051,9 +9994,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", @@ -14032,6 +13975,7 @@ "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" } @@ -14066,6 +14010,7 @@ "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", "picomatch": "^2.3.1" @@ -14993,7 +14938,8 @@ "node_modules/path-to-regexp": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -15672,6 +15618,7 @@ "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.6" }, @@ -16561,6 +16508,7 @@ "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", "depd": "2.0.0", @@ -16597,6 +16545,7 @@ "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" } @@ -16620,6 +16569,7 @@ "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": "~2.0.0", "escape-html": "~1.0.3", @@ -16866,18 +16816,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "license": "Apache-2.0", - "dependencies": { - "frac": "~1.1.2" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/stack-chain": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", @@ -18627,9 +18565,9 @@ } }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "dependencies": { "@types/estree": "^1.0.5", @@ -18927,24 +18865,6 @@ "dev": true, "license": "MIT" }, - "node_modules/wmf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/word": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -19143,19 +19063,10 @@ } }, "node_modules/xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "version": "0.20.2", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz", + "integrity": "sha512-+nKZ39+nvK7Qq6i0PvWWRA4j/EkfWOtkP/YhMtupm+lJIiHxUrgTr1CcKv1nBk1rHtkRRQ3O2+Ih/q/sA+FXZA==", "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - }, "bin": { "xlsx": "bin/xlsx.njs" }, diff --git a/package.json b/package.json index 3e705114..4b2d7a1a 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "reactstrap": "^9.2.2", "sanitize-html": "^2.13.0", "victory": "^36.9.2", - "xlsx": "^0.18.5" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz" }, "devDependencies": { "@babel/cli": "^7.24.6", diff --git a/public/css/_shared.scss b/public/css/_shared.scss index b1dda206..2bea5ad9 100644 --- a/public/css/_shared.scss +++ b/public/css/_shared.scss @@ -16,6 +16,9 @@ } } } + .kth-local-navigation--mobile { + padding: 0; + } label:has(+ .form-group), label:has(+ input) { @include typography.font-heading-xs; diff --git a/public/js/app/components/CourseSection.jsx b/public/js/app/components/CourseSection.jsx new file mode 100644 index 00000000..bab42bab --- /dev/null +++ b/public/js/app/components/CourseSection.jsx @@ -0,0 +1,43 @@ +/* eslint-disable react/no-danger */ +import React from 'react' +import { FaAsterisk } from 'react-icons/fa' +import InfoModal from './InfoModal' + +const syllabusMarker = (data, syllabusMarkerAriaLabel) => ( +

+ {data.header} + {data.syllabusMarker && ( + + + + )} + {data.infoModal && ( + + )} +

+) + +// eslint-disable-next-line arrow-body-style +const CourseSection = ({ sectionHeader: header = '', courseData = [], sectionId = '', syllabusMarkerAriaLabel }) => { + return ( +
+ {header.length ?

{header}

: null} + {courseData.map(data => + data.text ? ( + + {data.header && syllabusMarker(data, syllabusMarkerAriaLabel)} +
+ + ) : null + )} +
+ ) +} + +export default CourseSection diff --git a/public/js/app/components/CourseSectionList.jsx b/public/js/app/components/CourseSectionList.jsx index 211da55d..04cd51f9 100644 --- a/public/js/app/components/CourseSectionList.jsx +++ b/public/js/app/components/CourseSectionList.jsx @@ -2,7 +2,7 @@ import React from 'react' import { useLanguage } from '../hooks/useLanguage' import { useMissingInfo } from '../hooks/useMissingInfo' -import CourseSection from './CourseSections' +import CourseSection from './CourseSection' import { SyllabusInformation } from './SyllabusInformation' function CourseSectionList({ courseInfo = {}, partToShow, syllabus = {}, syllabusName }) { @@ -61,7 +61,15 @@ function CourseSectionList({ courseInfo = {}, partToShow, syllabus = {}, syllabu const during = [ ...eligibility, - { header: translation.courseInformation.course_prerequisites, text: courseInfo.course_prerequisites }, + { + header: translation.courseInformation.course_prerequisites, + text: courseInfo.course_recommended_prerequisites, + infoModal: { + description: translation.courseInformation.course_prerequisites_description, + closeLabel: translation.courseLabels.label_close, + ariaLabel: translation.courseInformation.course_prerequisites_menu_aria_label, + }, + }, { header: translation.courseInformation.course_required_equipment, text: courseRequiredEquipment }, { header: translation.courseInformation.course_literature, text: literatureText }, ] diff --git a/public/js/app/components/CourseSections.jsx b/public/js/app/components/CourseSections.jsx deleted file mode 100644 index 694ef276..00000000 --- a/public/js/app/components/CourseSections.jsx +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable react/no-danger */ -import React from 'react' -import { FaAsterisk } from 'react-icons/fa' - -const syllabusMarker = (data, syllabusMarkerAriaLabel) => ( -

- {data.header} - {data.syllabusMarker && ( - - - - )} -

-) - -const CourseSection = ({ sectionHeader: header = '', courseData = [], sectionId = '', syllabusMarkerAriaLabel }) => ( -
- {header.length ?

{header}

: null} - {courseData.map(data => - data.text ? ( - - {data.header && syllabusMarker(data, syllabusMarkerAriaLabel)} -
- - ) : null - )} -
-) - -export default CourseSection diff --git a/public/js/app/components/RoundInformation/CourseMemoLink.jsx b/public/js/app/components/RoundInformation/CourseMemoLink.jsx index bae4e02a..ff7a73c0 100644 --- a/public/js/app/components/RoundInformation/CourseMemoLink.jsx +++ b/public/js/app/components/RoundInformation/CourseMemoLink.jsx @@ -10,7 +10,7 @@ export const CourseMemoLink = ({ courseCode, courseRound }) => { return (
{round_memoFile ? ( - + {translation.courseLabels.label_link_course_memo} ) : ( diff --git a/public/js/app/components/RoundInformation/RoundInformation.jsx b/public/js/app/components/RoundInformation/RoundInformation.jsx index dddc929a..638f7d4c 100644 --- a/public/js/app/components/RoundInformation/RoundInformation.jsx +++ b/public/js/app/components/RoundInformation/RoundInformation.jsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import Alert from '../../components-shared/Alert' import BankIdAlert from '../../components/BankIdAlert' @@ -42,8 +42,22 @@ function RoundInformation({ courseCode, courseData, courseRound, semesterRoundSt 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}

diff --git a/public/js/app/components/RoundSelector/RoundSelector.jsx b/public/js/app/components/RoundSelector/RoundSelector.jsx index 4c9559ea..2f619f90 100644 --- a/public/js/app/components/RoundSelector/RoundSelector.jsx +++ b/public/js/app/components/RoundSelector/RoundSelector.jsx @@ -26,9 +26,9 @@ export const RoundSelector = ({ activeSemesters, semesterRoundState }) => {

- {translation.courseLabels.header_dropdown_menue} + {translation.courseLabels.header_dropdown_menu} ', () => { test('renders course offering memo link correctly if there is an available course memo as pdf', () => { - useWebContext.mockReturnValue({ lang: 'en', browserConfig: { memoStorageURI: 'https://test.com/' } }) + useWebContext.mockReturnValue({ lang: 'en', browserConfig: { memoStorageUri: 'https://test.com/' } }) const [translate] = i18n.messages // en const propsWithMemoFile = { @@ -26,11 +26,11 @@ describe('Component ', () => { }) test('renders course offering memo link correctly to web-based course memos', () => { - useWebContext.mockReturnValue({ lang: 'en', browserConfig: { memoStorageURI: 'https://test.com/' } }) + useWebContext.mockReturnValue({ lang: 'en', browserConfig: { memoStorageUri: 'https://test.com/' } }) const [translate] = i18n.messages // en const propsWithMemoFile = { courseCode: 'ABC123', - memoStorageURI: 'https://test.com/', + memoStorageUri: 'https://test.com/', courseRound: { round_memoFile: { fileName: 'test', fileDate: '1970-01-01' } }, } const propsWithMemoWebPage = { @@ -53,7 +53,7 @@ describe('Component ', () => { }) test('renders course offering memo link to pdf if it exists among other data and correctly prioriterized', () => { - useWebContext.mockReturnValue({ lang: 'en', browserConfig: { memoStorageURI: 'https://test.com/' } }) + useWebContext.mockReturnValue({ lang: 'en', browserConfig: { memoStorageUri: 'https://test.com/' } }) const [translate] = i18n.messages // en const propsWithMemoFileAndOtherInfo = { courseCode: 'KIP1111', diff --git a/public/js/app/components/__tests__/CourseSections.test.js b/public/js/app/components/__tests__/CourseSections.test.js index 23b0bec6..2f8483c6 100644 --- a/public/js/app/components/__tests__/CourseSections.test.js +++ b/public/js/app/components/__tests__/CourseSections.test.js @@ -3,16 +3,16 @@ import React from 'react' import '@testing-library/jest-dom' import { render, screen } from '@testing-library/react' -import CourseSections from '../CourseSections' +import CourseSection from '../CourseSection' import i18n from '../../../../../i18n' const [translationEN] = i18n.messages -describe('Component ', () => { +describe('Component ', () => { test('render text with a syllabus marker correctly', () => { const mockData = [{ header: 'First test header', text: 'Text for test from test syllabus', syllabusMarker: true }] render( - ', () => { test('render text without a syllabus marker correctly', () => { const mockData = [{ header: 'Test header chosen by user', text: 'Text for test not from syllabys' }] render( - ', () => { expect(header.querySelector('sup')).toBeNull() }) + test('render an info button correctly', () => { + const mockData = [ + { + header: 'First test header', + text: 'Text for test from test syllabus', + syllabusMarker: true, + infoModal: { + description: translationEN.courseInformation.course_prerequisites_description, + closeLabel: translationEN.courseLabels.label_close, + ariaLabel: translationEN.courseInformation.course_prerequisites_menu_aria_label, + }, + }, + ] + render( + + ) + + const modal = screen.getByLabelText(mockData[0].infoModal.ariaLabel) + expect(modal).toBeInTheDocument() + }) + test('render array of several headers with text correctly', () => { const mockData = [ { header: 'First test header', text: 'Text for test from test syllabus', syllabusMarker: true }, { header: 'Second test header', text: 'Text for test not from syllabys' }, ] render( - ', () => { test('renders study pace correctly', () => { const propsWithStudyPace = { - memoStorageURI: '', + memoStorageUri: '', semesterRoundState: defaultSemesterRoundState, courseData: {}, courseRound: { @@ -65,7 +65,7 @@ describe('Component ', () => { const responsiblesData = 'Responsibles’ data' const teachersData = 'Teachers’ data' const propsWithEmployees = { - memoStorageURI: '', + memoStorageUri: '', semesterRoundState: defaultSemesterRoundState, courseData: {}, courseCode: 'SF1624', @@ -95,7 +95,7 @@ describe('Component ', () => { test('renders information about missing course employees in course offering because it contains empty string', () => { const propsWithEmptyEmployees = { - memoStorageURI: '', + memoStorageUri: '', semesterRoundState: defaultSemesterRoundState, courseData: {}, courseCode: 'SF1624', @@ -113,7 +113,7 @@ describe('Component ', () => { test('renders information about missing course employees in course offering because no data about employees is provided', () => { const propsWithoutEmployees = { - memoStorageURI: '', + memoStorageUri: '', semesterRoundState: defaultSemesterRoundState, courseData: {}, courseCode: 'SF1624', @@ -131,7 +131,7 @@ describe('Component ', () => { test('renders course offering number of places correctly if all data is available', async () => { const propsWithSeatsNum = { - memoStorageURI: '', + memoStorageUri: '', semesterRoundState: defaultSemesterRoundState, courseData: {}, courseRound: { @@ -169,7 +169,7 @@ describe('Component ', () => { test('renders default text and hide info icon if a course offering number of places is not provided', () => { const propsWithoutSeatsNum = { - memoStorageURI: '', + memoStorageUri: '', semesterRoundState: defaultSemesterRoundState, courseData: {}, courseRound: { @@ -198,7 +198,7 @@ describe('Component ', () => { test('renders course offering number of places correctly and default text in modal if selection criteria is empty', async () => { const propsWithEmptyCriteria = { - memoStorageURI: '', + memoStorageUri: '', semesterRoundState: defaultSemesterRoundState, courseData: {}, courseRound: { diff --git a/server/apiCalls/getFilteredData.js b/server/apiCalls/getFilteredData.js index b5fae0b4..6494f9c4 100644 --- a/server/apiCalls/getFilteredData.js +++ b/server/apiCalls/getFilteredData.js @@ -39,8 +39,6 @@ function _parseCourseDefaultInformation(koppsCourseDetails, ladokCourse, languag ? parseSemesterIntoYearSemesterNumberArray(koppsCourse.lastExamTerm.term) : [], course_literature: parseOrSetEmpty(koppsCourse.courseLiterature, language), - course_supplemental_information_url: parseOrSetEmpty(koppsCourse.supplementaryInfoUrl, language), - course_supplemental_information_url_text: parseOrSetEmpty(koppsCourse.supplementaryInfoUrlName, language), course_state: parseOrSetEmpty(koppsCourse.state, language, true), // TODO(Ladok-POC): Following should be removed (KUI-1387) set to emport for now @@ -250,13 +248,15 @@ const getFilteredData = async ({ courseCode, language, memoList }) => { //* **** Course information that is static on the course side *****// const courseDefaultInformation = _parseCourseDefaultInformation(koppsCourseDetails, ladokCourse, language) - const { sellingText, courseDisposition, supplementaryInfo, imageInfo } = await courseApi.getCourseInfo(courseCode) + const { sellingText, courseDisposition, recommendedPrerequisites, supplementaryInfo, imageInfo } = + await courseApi.getCourseInfo(courseCode) const courseInfo = { ...courseDefaultInformation, sellingText: resolveText(sellingText, language), imageFromAdmin: imageInfo, course_disposition: resolveText(courseDisposition, language), + course_recommended_prerequisites: resolveText(recommendedPrerequisites, language), course_supplemental_information: resolveText(supplementaryInfo, language), } diff --git a/server/apiCalls/kursinfoApi.js b/server/apiCalls/kursinfoApi.js index 2de5f603..0e964f74 100644 --- a/server/apiCalls/kursinfoApi.js +++ b/server/apiCalls/kursinfoApi.js @@ -14,6 +14,7 @@ async function _getCourseInfo(courseCode) { imageInfo: '', supplementaryInfo: { sv: '', en: '' }, courseDisposition: { sv: '', en: '' }, + recommendedPrerequisites: { sv: '', en: '' }, } if (res.statusCode === 200 && res.body) { @@ -21,6 +22,7 @@ async function _getCourseInfo(courseCode) { return { sellingText: body.sellingText ?? defaultValues.sellingText, courseDisposition: body.courseDisposition ?? defaultValues.courseDisposition, + recommendedPrerequisites: body.recommendedPrerequisites ?? defaultValues.recommendedPrerequisites, supplementaryInfo: body.supplementaryInfo ?? defaultValues.supplementaryInfo, imageInfo: body.imageInfo ?? defaultValues.imageInfo, } diff --git a/server/controllers/__tests__/courseCtrl.test.js b/server/controllers/__tests__/courseCtrl.test.js index da77d0d8..c94b1087 100644 --- a/server/controllers/__tests__/courseCtrl.test.js +++ b/server/controllers/__tests__/courseCtrl.test.js @@ -55,7 +55,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', }), })) @@ -142,12 +141,11 @@ describe('Discontinued course to test', () => { "course_possibility_to_addition": "Ingen information tillagd", "course_possibility_to_completions": "Ingen information tillagd", "course_prerequisites": "Ingen information tillagd", + "course_recommended_prerequisites": "", "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,