diff --git a/.github/workflows/cypress-testing.yml b/.github/workflows/cypress-testing.yml index ca7777461d..df7a2595e6 100644 --- a/.github/workflows/cypress-testing.yml +++ b/.github/workflows/cypress-testing.yml @@ -132,7 +132,7 @@ jobs: - name: Cypress run uses: cypress-io/github-action@v6 - timeout-minutes: 20 + timeout-minutes: 60 with: install: false wait-on: 'http://127.0.0.1:3000/api/graphql, http://127.0.0.1:3001, http://127.0.0.1:3002, http://127.0.0.1:3003, http://127.0.0.1:3010' diff --git a/.github/workflows/test-graphql.yml b/.github/workflows/test-graphql.yml index 9c66b33ff4..00a11eb7d3 100644 --- a/.github/workflows/test-graphql.yml +++ b/.github/workflows/test-graphql.yml @@ -27,5 +27,13 @@ jobs: - name: Test functions shell: bash run: | - cd packages/graphql + cd packages/prisma + pnpm run build + cd ../types + pnpm run build + cd ../util + pnpm run build + cd ../grading + pnpm run build + cd ../graphql pnpm run test diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c3559d5ea..9ebad03817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -377,7 +377,7 @@ All notable changes to this project will be documented in this file. See [standa ### Bug Fixes -* ensure that validation in quesiton edit modal works properly ([#4251](https://github.com/uzh-bf/klicker-uzh/issues/4251)) ([5e26453](https://github.com/uzh-bf/klicker-uzh/commit/5e2645381465ccfbf26dac17d2f36b380ef9e4bf)) +* ensure that validation in question edit modal works properly ([#4251](https://github.com/uzh-bf/klicker-uzh/issues/4251)) ([5e26453](https://github.com/uzh-bf/klicker-uzh/commit/5e2645381465ccfbf26dac17d2f36b380ef9e4bf)) * require that the user specifies sample solutions for open questions when activated ([#4252](https://github.com/uzh-bf/klicker-uzh/issues/4252)) ([0c5aa6b](https://github.com/uzh-bf/klicker-uzh/commit/0c5aa6b1c15e00813f4485e9380d53728ca527c7)) ## [3.2.0-alpha.20](https://github.com/uzh-bf/klicker-uzh/compare/v3.2.0-alpha.19...v3.2.0-alpha.20) (2024-09-06) diff --git a/README.md b/README.md index ec1a837338..394d2d09f4 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ KlickerUZH v3.0 uses multiple different web applications and services, which communicate with each other: - [Frontend PWA](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/frontend-pwa) is the student frontend of KlickerUZH, which contains the student views for live quizzes, microlearnings, practice quizzes, leaderboards and more. -- [Frontend Manage](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/frontend-manage) is the lecturer frontend of KlickerUZH, which provides all the functionalities that lecturers need, including but not limited to question management, session management, course management and analytics. -- [Fontend Control](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/frontend-control) is a minimal controller frontend, which allows to control live sessions from mobile devices in an optimized layout. Soon, this app will also be available as a [PowerPoint integration](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/office-addin) (work in progress) for catalyst users. +- [Frontend Manage](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/frontend-manage) is the lecturer frontend of KlickerUZH, which provides all the functionalities that lecturers need, including but not limited to question management, activity management, course management and analytics. +- [Frontend Control](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/frontend-control) is a minimal controller frontend, which allows to control live quizzes from mobile devices in an optimized layout. Soon, this app will also be available as a [PowerPoint integration](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/office-addin) (work in progress) for catalyst users. - [Fontend Authentication](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/auth) is the authentication frontend of KlickerUZH, providing login functionalities through Edu-ID accounts and delegated logins to the manage frontend. - [Backend Docker](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/backend-docker) is the main backend service of KlickerUZH. -- [Backend Responses](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/func-incoming-responses) is a service that handles incoming student responses during a live session and puts them into an Azure queue for improved load handling. +- [Backend Responses](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/func-incoming-responses) is a service that handles incoming student responses during a live quizzes and puts them into an Azure queue for improved load handling. - [Backend Response Processor](https://github.com/uzh-bf/klicker-uzh/tree/v3/apps/func-response-processor) accesses queued elements from the aforementioned service and processes them by computing scores and experience points, updating the cache, etc. In addition to the key application components, this repository also includes the codebases for our landing page and documentation at [www.klicker.uzh.ch](https://www.klicker.uzh.ch/), as well as deployment scripts for Helm/Kubernetes. An updated deployment documentation for self-hosting KlickerUZH v3.0 will be added until the end of the year. diff --git a/_down_macos.sh b/_down_macos.sh new file mode 100755 index 0000000000..fbccb2dffc --- /dev/null +++ b/_down_macos.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +docker compose down postgres redis_exec redis_cache reverse_proxy_macos diff --git a/apps/auth/package.json b/apps/auth/package.json index ac66c6d873..4a994b8ef7 100644 --- a/apps/auth/package.json +++ b/apps/auth/package.json @@ -8,7 +8,7 @@ "@klicker-uzh/prisma": "workspace:*", "@klicker-uzh/shared-components": "workspace:*", "@next-auth/prisma-adapter": "1.0.7", - "@uzh-bf/design-system": "3.0.0-alpha.34", + "@uzh-bf/design-system": "3.0.0-alpha.35", "axios": "1.7.7", "bcryptjs": "2.4.3", "js-cookie": "3.0.5", @@ -17,7 +17,6 @@ "next": "15.0.0", "next-auth": "4.24.8", "next-intl": "3.21.1", - "nookies": "2.5.2", "react": "18.3.1", "react-dom": "18.3.1", "sharp": "0.33.5", diff --git a/apps/backend-docker/package.json b/apps/backend-docker/package.json index 0af6e108ed..7e1c4bce03 100644 --- a/apps/backend-docker/package.json +++ b/apps/backend-docker/package.json @@ -56,7 +56,7 @@ "@types/passport": "^1.0.16", "@types/passport-jwt": "^4.0.1", "@types/ws": "^8.5.12", - "@uzh-bf/design-system": "3.0.0-alpha.34", + "@uzh-bf/design-system": "3.0.0-alpha.35", "axios": "~1.7.7", "cross-env": "~7.0.3", "dotenv": "~16.4.5", diff --git a/apps/backend-docker/scripts/2023-11-13_upgrade_question_data.ts b/apps/backend-docker/scripts/2023-11-13_upgrade_question_data.ts index 5fdfcf57f2..ae3607d12c 100644 --- a/apps/backend-docker/scripts/2023-11-13_upgrade_question_data.ts +++ b/apps/backend-docker/scripts/2023-11-13_upgrade_question_data.ts @@ -1,3 +1,5 @@ +// @ts-nocheck + import type { PrismaMigrationClient } from '@klicker-uzh/graphql/src/types/app.js' // import { PrismaClient } from '@klicker-uzh/prisma' diff --git a/apps/backend-docker/scripts/checkRedisConsistency.ts b/apps/backend-docker/scripts/checkRedisConsistency.ts index e734e0de0e..7668877995 100644 --- a/apps/backend-docker/scripts/checkRedisConsistency.ts +++ b/apps/backend-docker/scripts/checkRedisConsistency.ts @@ -1,4 +1,4 @@ -import { PrismaClient } from '@klicker-uzh/prisma' +import { PrismaClient, PublicationStatus } from '@klicker-uzh/prisma' import { Redis } from 'ioredis' async function run() { @@ -12,18 +12,18 @@ async function run() { tls: process.env.REDIS_TLS ? {} : undefined, }) - const sessions = await prisma.liveSession.findMany({ + const quizzes = await prisma.liveQuiz.findMany({ where: { status: { - not: 'RUNNING', + not: PublicationStatus.PUBLISHED, }, }, }) let count = 0 - for (const session of sessions) { - const invalidKeys = await redisExec.keys(`s:${session.id}:*`) + for (const quiz of quizzes) { + const invalidKeys = await redisExec.keys(`lq:${quiz.id}:*`) if (invalidKeys.length > 0) { count += invalidKeys.length diff --git a/apps/backend-docker/scripts/fixPointsInconsistency.ts b/apps/backend-docker/scripts/fixPointsInconsistency.ts index dbd77b972f..f4b5e9e04c 100644 --- a/apps/backend-docker/scripts/fixPointsInconsistency.ts +++ b/apps/backend-docker/scripts/fixPointsInconsistency.ts @@ -12,17 +12,17 @@ const redisExec = new Redis({ tls: process.env.REDIS_TLS ? {} : undefined, }) -// deduct points from course leaderboard entries: 1x points in sessionLB +// deduct points from course leaderboard entries: 1x points in quizLB const FAILURES = 1 const COURSE_ID = '' -const SESSION_ID = '' +const QUIZ_ID = '' -const sessionLB = await redisExec.hgetall(`s:${SESSION_ID}:lb`) -const sessionXP = await redisExec.hgetall(`s:${SESSION_ID}:xp`) +const quizLB = await redisExec.hgetall(`lq:${QUIZ_ID}:lb`) +const quizXP = await redisExec.hgetall(`lq:${QUIZ_ID}:xp`) const results = await Promise.allSettled( - Object.entries(sessionLB).map(async ([participantId, score]) => { + Object.entries(quizLB).map(async ([participantId, score]) => { const lbEntry = await prisma.leaderboardEntry.findFirst({ where: { participantId, diff --git a/apps/backend-docker/scripts/fixQuestionData.ts b/apps/backend-docker/scripts/fixQuestionData.ts deleted file mode 100644 index 3ea09a31ca..0000000000 --- a/apps/backend-docker/scripts/fixQuestionData.ts +++ /dev/null @@ -1,41 +0,0 @@ -// pn exec:prod scripts/fixQuestionData.ts - -import { PrismaClient } from '@klicker-uzh/prisma' - -const prisma = new PrismaClient() - -const questionInstances = await prisma.questionInstance.findMany() - -console.log(questionInstances.length) - -questionInstances.forEach((questionInstance) => { - const expectedKeys = [ - 'id', - 'name', - 'type', - 'content', - 'options', - 'ownerId', - 'createdAt', - 'isDeleted', - 'updatedAt', - 'isArchived', - 'originalId', - 'displayMode', // TODO: will be moved to options - 'explanation', - 'pointsMultiplier', // TODO: will be moved to options - 'hasSampleSolution', // TODO: will be moved to options - 'hasAnswerFeedbacks', // TODO: will be moved to options - 'attachments', // TODO: could be purged from existing questionData - 'contentPlain', // TODO: could be purged from existing questionData - ] - - const existingKeys = Object.keys(questionInstance.questionData) - - const difference = existingKeys.filter((key) => !expectedKeys.includes(key)) - if (difference.length !== 0) { - console.log(difference) - } -}) - -process.exit(0) diff --git a/apps/docs/docs/gamification/experience.mdx b/apps/docs/docs/gamification/experience.mdx index 102fffb867..680038b7ec 100644 --- a/apps/docs/docs/gamification/experience.mdx +++ b/apps/docs/docs/gamification/experience.mdx @@ -2,7 +2,7 @@ title: XP and Levels --- -While participants collect points for correct answers on a course level, experience points (XP) are collected across courses and allow students to level up. This represents an important part of our gamification concept, making sure that students keep making progress across different courses and activities. However, students are not only awarded experience points for answering questions correctly (in live settings as well as learning elements and micro sessions), but also when obtaining achievements, solving group challenges and many more activities. +While participants collect points for correct answers on a course level, experience points (XP) are collected across courses and allow students to level up. This represents an important part of our gamification concept, making sure that students keep making progress across different courses and activities. However, students are not only awarded experience points for answering questions correctly (in live settings as well as practice quizzes and microlearnings), but also when obtaining achievements, solving group challenges and many more activities. As usual, levelling up will be faster in the beginning and then require a growing amount of additional points later on. KlickerUZH currently uses the following formula to compute the required amount of experience points for some level $i$ as $\text{XP}_{\text{req}}(i)$: diff --git a/apps/docs/docs/gamification/grading_logic.mdx b/apps/docs/docs/gamification/grading_logic.mdx index 05d8437d3d..f035fe2dd1 100644 --- a/apps/docs/docs/gamification/grading_logic.mdx +++ b/apps/docs/docs/gamification/grading_logic.mdx @@ -4,7 +4,7 @@ title: Grading Logic In order to distribute points, XP and achievements based on the performance of the participants in various activities, an automated grading logic has been implemented in KlickerUZH. This chapter provides a detailed explanation of the approaches employed for different element and activity types, including practice quizzes, microlearnings and live quizzes. -The presented grading approach assumes that all considered questions have a defined correct solution (or possibly multiple solutions), as this is required in asynchronous activities, except for free-text questions. During live sessions where questions without sample solutions were used, the participants are awarded a fixed amount of 10 points for taking part in each poll. Please also note that the [grading approach for live quizzes](#grading-in-live-quizzes) slightly extends the logic of asynchronous activities and that some element types are only available in select activities. +The presented grading approach assumes that all considered questions have a defined correct solution (or possibly multiple solutions), as this is required in asynchronous activities, except for free-text questions. During live quizzes where questions without sample solutions were used, the participants are awarded a fixed amount of 10 points for taking part in each poll. Please also note that the [grading approach for live quizzes](#grading-in-live-quizzes) slightly extends the logic of asynchronous activities and that some element types are only available in select activities. ## Grading by Question Type @@ -14,7 +14,7 @@ The grading logic will first be described on the example of practice quizzes and The point multipliers, which can be specified both on a question and activity level, are combined during the creation of the activity and applied to the total awarded points (including basic points and potential bonuses). If no multipliers were specified, this factor defaults to 1. -Multipliers can be used to weigh questions according to their difficulty and reward participation in certain quizzes with more points (e.g. quiz on exam level during the last lecture of the semester vs. an introductory quiz in the first lecture). The same multiplier concept also applies to live sessions. +Multipliers can be used to weigh questions according to their difficulty and reward participation in certain quizzes with more points (e.g. quiz on exam level during the last lecture of the semester vs. an introductory quiz in the first lecture). The same multiplier concept also applies to live quizzes. ### Grading Single Choice Questions @@ -70,14 +70,14 @@ For free text questions, you can currently specify a selection of correct respon During synchronous live quizzes, every participant will receive 10 points for each submitted answer, independent of its correctness. If the submitted answer is correct, 5 additional points will be awarded. For partially correct answers, these 5 points are multiplied with a factor described in the sections above. If no sample solution is defined, students will not receive any time-dependent bonus, but only the fixed amount of 10 points (no multiplication with multiplier). -To incentivize fast and correct answers during live sessions, additional bonus points are awarded. Starting time with the first correct answer, players will receive up to 45 points (default setting), depending on the time that passed between the first correct answer and theirs. By default, the slope to zero points is implemented with a duration of 20 seconds. +To incentivize fast and correct answers during live quizzes, additional bonus points are awarded. Starting time with the first correct answer, players will receive up to 45 points (default setting), depending on the time that passed between the first correct answer and theirs. By default, the slope to zero points is implemented with a duration of 20 seconds. -The corresponding resulting point curves for correct and wrong answers during live sessions, when starting time with the first correct response, are shown in the plot below (not considering multipliers). +The corresponding resulting point curves for correct and wrong answers during live quizzes, when starting time with the first correct response, are shown in the plot below (not considering multipliers).
Illustration of Grading in Live Sessions
diff --git a/apps/docs/docs/tutorials/course_management.mdx b/apps/docs/docs/tutorials/course_management.mdx index f78a9c9e95..4e1354b5cc 100644 --- a/apps/docs/docs/tutorials/course_management.mdx +++ b/apps/docs/docs/tutorials/course_management.mdx @@ -87,6 +87,6 @@ This process helps ensure that groups are formed **fairly and efficiently**. The 1. Participants that are **alone** in a group are automatically added to the random assignment pool, while their group is resolved. 2. Students remaining in the assignment pool are evenly distributed into **new groups** to match the preferred group size. -5. **Manual group creation** during the creation period: Lecturers can manually finalize group assignments at any time through the course's **group overview page** (ahead of the group formation deadline). This allows you to use randomized group formation in a single in-class session instead of across a longer timespan. To do so, you can select the "**Assign random groups**" button within the group overview on the course page (screenshot below). This will close group creation and distribute students from the pool into existing groups as described in the previous step. To reopen group formation after manual finalization, lecturers can **extend the deadline** in the course settings. +5. **Manual group creation** during the creation period: Lecturers can manually finalize group assignments at any time through the course's **group overview page** (ahead of the group formation deadline). This allows you to use randomized group formation in a single in-class quizzes instead of across a longer timespan. To do so, you can select the "**Assign random groups**" button within the group overview on the course page (screenshot below). This will close group creation and distribute students from the pool into existing groups as described in the previous step. To reopen group formation after manual finalization, lecturers can **extend the deadline** in the course settings. ![Random Group Creation - Lecturer View](/img_v3/18_random_groups_lecturer.png) diff --git a/apps/docs/docs/tutorials/element_management.mdx b/apps/docs/docs/tutorials/element_management.mdx index aaf3c77654..b2248ff243 100644 --- a/apps/docs/docs/tutorials/element_management.mdx +++ b/apps/docs/docs/tutorials/element_management.mdx @@ -96,5 +96,5 @@ Elements (questions, flashcards, and content elements) are the building blocks o - **Delete**: Remove unwanted elements (note: this action is irreversible) :::info -Most elements can be used in any learning activity, with some restrictions. For detailed information on embedding elements into different activities, refer to the following sections. Remember, elements are persistent within existing sessions, even if deleted from your element pool. +Most elements can be used in any learning activity, with some restrictions. For detailed information on embedding elements into different activities, refer to the following sections. Remember, elements are persistent within existing quizzes, even if deleted from your element pool. ::: diff --git a/apps/docs/docs/tutorials/group_activity.mdx b/apps/docs/docs/tutorials/group_activity.mdx index 0d1671961f..cc1d1fc7d6 100644 --- a/apps/docs/docs/tutorials/group_activity.mdx +++ b/apps/docs/docs/tutorials/group_activity.mdx @@ -35,7 +35,7 @@ To create a group activity, navigate to the question pool and use the button at In a first step, you will be asked to provide general information about the group activity. The following fields are available for customization: -- **Name**: The name of the group activity allows the user to distinguish the particular session from others. It is therefore only visible to the users themselves. +- **Name**: The name of the group activity allows the user to distinguish the particular activity from others. It is therefore only visible to the users themselves. - **Display Name**: This name will be shown to the participants while the group activity is being performed. - **Description**: A description of the group activity can optionally be added and will be displayed to the participants as an introduction to the group activity. While not required, we highly recommend to provide general information about the group activity here. diff --git a/apps/docs/docs/tutorials/live_qa.mdx b/apps/docs/docs/tutorials/live_qa.mdx index c84dd8b0bb..af8cb5225e 100644 --- a/apps/docs/docs/tutorials/live_qa.mdx +++ b/apps/docs/docs/tutorials/live_qa.mdx @@ -24,7 +24,7 @@ Live Quizzes in KlickerUZH can be coupled with a Live Q&A that enables students title="KlickerUZH - Live Q&A" > -A Live Q&A is embedded in the Live Quiz and can be activated during a Live Quiz session. For more details on the Live Quiz, review the dedicated [tutorial on live quiz creation](/tutorials/live_quiz/). +A Live Q&A is embedded in the Live Quiz and can be activated during its execution. For more details on the Live Quiz, review the dedicated [tutorial on live quiz creation](/tutorials/live_quiz/). 1. Activate Live Q&A: As soon as you activate a question block (displayed in green), the Live Q&A can be activated. 2. Manage questions: By clicking on the eye symbol, you can make the question visible for all the participants. The question can then not only be seen by others, but also be upvoted. Furthermore, you can sort the incoming questions or delete them. diff --git a/apps/docs/docs/tutorials/live_quiz.mdx b/apps/docs/docs/tutorials/live_quiz.mdx index 09a6a521b9..f2691c9fa8 100644 --- a/apps/docs/docs/tutorials/live_quiz.mdx +++ b/apps/docs/docs/tutorials/live_quiz.mdx @@ -28,9 +28,9 @@ Live Quizzes in KlickerUZH involve real-time interaction during lectures to fost **Add a description:** -1. Name: The name of the Live Quiz allows the user to distinguish the particular session from others. It is therefore only visible to the users themselves. +1. Name: The name of the Live Quiz allows the user to distinguish the particular quiz from others. It is therefore only visible to the users themselves. 2. Display Name: This name will be shown to the participants while the Live Quiz is being performed. -3. Description: A description of the Live Quiz can optionally be added and will be displayed to the participants at the beginning of the session. +3. Description: A description of the Live Quiz can optionally be added and will be displayed to the participants at the beginning of the live quiz. **Adjust the settings:** @@ -69,9 +69,9 @@ For this step, existing questions are required. If you need help with creating q title="KlickerUZH - Live Quiz Execution" > -The prepared Live Quiz can be found under Sessions. Here it is possible to edit the session before it is started. Once Start Session is clicked, the session can no longer be edited. Also note that once Live Quizzes are closed, they cannot be reopened. +The prepared Live Quiz can be found under "Live Quizzes". Here it is possible to edit the quiz before it is started. Once the live quiz is started, it can no longer be edited. Also note that once Live Quizzes are closed, they cannot be reopened. -Once the session is stared, there are the following steps to take: +Once the quiz is stared, there are the following steps to take: 1. QR Code: To share the QR code, you can either click on "Present QR code" and copy the link (recommended) or take a screenshot of the QR code and share it. 2. Start first/next block: In order for the participants to see the particular block of questions, click the Start first/next block button. @@ -83,7 +83,7 @@ Once the session is stared, there are the following steps to take: ## How can I edit a Live Quiz? -It is possible to edit a Live Quiz only before it has been started. Once Start Session is clicked, the session can no longer be edited. +It is possible to edit a Live Quiz only before it has been started. Once the quiz is started, it can no longer be edited. ## How can participants join a Live Quiz? @@ -103,7 +103,7 @@ It is possible to edit a Live Quiz only before it has been started. Once Start S title="KlickerUZH - Join Live Quiz" > -Once the session is started, click on the QR code button in the top bar of the session page. You can send the direct link to your participants or show them the QR code (Present QR code displays the QR code on a separate page). +Once the live quiz is started, click on the QR code button in the top bar of the quiz page. You can send the direct link to your participants or show them the QR code (Present QR code displays the QR code on a separate page). ## What should I look out for when creating a gamified Live Quiz? diff --git a/apps/docs/docs/tutorials/lti_integration.mdx b/apps/docs/docs/tutorials/lti_integration.mdx index 2f0a156112..60b19b4634 100644 --- a/apps/docs/docs/tutorials/lti_integration.mdx +++ b/apps/docs/docs/tutorials/lti_integration.mdx @@ -22,7 +22,7 @@ Create a KlickerUZH structure with the following LTI pages: 1. **Course overview and documentation**: At the very top of your KlickerUZH course page, you can add a description entailing an overview of how the KlickerUZH will be used in your course, including the timing and purpose of its features. This section can also include information on initial login and account setup. The information given in this text box can then be integrated into your OLAT course, providing your students with an overview and guidance. -2. **Live Quiz and Q&A**: Make sure to integrate a page where your students can access the live quizzes and Q&A sessions (if you are using them) directly from OLAT. +2. **Live Quiz and Q&A**: Make sure to integrate a page where your students can access the live quizzes and Q&A quizzes (if you are using them) directly from OLAT. 3. **Leaderboard**: If your course includes gamification elements and/or groups and group activities, it makes sense to dedicate a page to the leaderboard (where students can also manage their groups). Motivate students by presenting the leaderboard that tracks individual and group points from time to time. diff --git a/apps/docs/docs/tutorials/microlearning.mdx b/apps/docs/docs/tutorials/microlearning.mdx index 36142bfe7f..04a9b52413 100644 --- a/apps/docs/docs/tutorials/microlearning.mdx +++ b/apps/docs/docs/tutorials/microlearning.mdx @@ -31,19 +31,19 @@ Microlearning offers short learning units with a limited number of questions, pu **Add a description:** -1. Name: The name of the Microlearning session allows the user to distinguish the particular session from others. It is therefore only visible to the users themselves. -2. Display Name: This name will be shown to the participants while the Microlearning session is being performed. -3. Description: A description of the Microlearning session can optionally be added and will be displayed to the participants at the beginning of the session. +1. Name: The name of the microlearning allows the user to distinguish the particular activity from others. It is therefore only visible to the users themselves. +2. Display Name: This name will be shown to the participants while the microlearning is being performed. +3. Description: A description of the microlearning can optionally be added and will be displayed to the participants at the beginning of the activity. **Adjust the settings:** -4. Course: In this field, the Microlearning session has to be assigned to a course. Note that this is different from the Live Quiz, which can be run independently of a course. If you need help creating a course, please review the [tutorial on course management](/tutorials/course_management). -5. Start/End date: Microlearnings are limited in time. These fields determine the starting and ending points at which the participants have access to the session. +4. Course: In this field, the microlearning has to be assigned to a course. Note that this is different from the Live Quiz, which can be run independently of a course. If you need help creating a course, please review the [tutorial on course management](/tutorials/course_management). +5. Start/End date: Microlearnings are limited in time. These fields determine the starting and ending points at which the participants have access to the activity. 6. Multiplier: The multiplier is a factor with which the points are multiplied when a question is answered. A factor above 1 is only used if gamification is activated . **Choose your questions:** -For this step, existing questions are required. Note that Microlearning and Practice Quizzes support all element types except from free-text questions, as long as a valid sample solution is specified. If you need help with creating questions or content elements / flashcards, please review the [tutorial on element management](/tutorials/element_management/). +For this step, existing questions are required. Note that microlearning and Practice Quizzes support all element types except from free-text questions, as long as a valid sample solution is specified. If you need help with creating questions or content elements / flashcards, please review the [tutorial on element management](/tutorials/element_management/). When adding questions, you can choose to have one or multiple questions per stack. It makes sense to have several questions per stack if you provide your students with a task / case study that builds up and the questions are connected. There are multiple options to add a question to a stack: @@ -55,7 +55,7 @@ There are multiple options to add a question to a stack: **End of preparation:** -11. With a click on Create your first Microlearning session is prepared! +11. With a click on Create your first microlearning is prepared! ## How can I publish a Microlearning and make it accessible to participants? @@ -75,12 +75,12 @@ There are multiple options to add a question to a stack: title="KlickerUZH - Publish Microlearning" > -1. After you create a Microlearning session, you can find it under Courses. Then select the course to which you added the session. +1. After you create a microlearning, you can find it under Courses. Then select the course to which you added the activity. 2. In the course you have multiple options. You can... - ...copy the access link and provide it to your participants. - ...copy the access link to integrate the microlearning into your learning management system through LTI. - - ...edit your session before publishing it. + - ...edit your microlearning before publishing it. - ...extend the duration of your microlearning, while it is still running. - ...publish it. Note that the publishing process makes the microlearning available to all participants after the scheduled start date. Until then, the microlearning will have a "Scheduled" state and can be unpublished and edited again. The change from the scheduled state to the "Published" state is performed automatically at the scheduled start date and is irreversible. - ...delete your microlearning with certain restrictions. -3. After publishing your session, your participants can see the session in their account by joining the course. In the app, the Microlearning session is displayed to the participants as shown in the screenshots below. +3. After publishing your microlearning, your participants can see it in their account by joining the course. In the app, the microlearning is displayed to the participants as shown in the screenshots below. diff --git a/apps/docs/docs/tutorials/practice_quiz.mdx b/apps/docs/docs/tutorials/practice_quiz.mdx index 521107daf3..7d2436085b 100644 --- a/apps/docs/docs/tutorials/practice_quiz.mdx +++ b/apps/docs/docs/tutorials/practice_quiz.mdx @@ -33,21 +33,21 @@ The Practice Quiz activity consists of longer question sets that specifically ta **Add a description:** -1. Name: The name of the Practice Quiz allows the user to distinguish the particular session from others. It is therefore only visible to the users themselves. -2. Display Name: This name will be shown to the participants while the Practice Quiz is being performed. -3. Description: A description of the Practice Quiz can optionally be added and will be displayed to the participants at the beginning of the session. +1. Name: The name of the practice quiz allows the user to distinguish the particular activity from others. It is therefore only visible to the users themselves. +2. Display Name: This name will be shown to the participants while the practice quiz is being performed. +3. Description: A description of the practice quiz can optionally be added and will be displayed to the participants at the beginning of the quiz. **Adjust the settings:** -4. Course: In this field, the Practice Quiz has to be assigned to a course. Note that this is different from the Live Quiz, which can be run independently of a course. If you need help creating a course, please review the [tutorial on course management](/tutorials/course_management/). +4. Course: In this field, the practice quiz has to be assigned to a course. Note that this is different from the Live Quiz, which can be run independently of a course. If you need help creating a course, please review the [tutorial on course management](/tutorials/course_management/). 5. Multiplier: The multiplier is a factor with which the awarded points are multiplied when a question is answered. This field is only available if the practice quiz is included in a gamified course and participants collect points for correct answers. -6. Repetition interval: Practice Quizzes are characterized by their spaced repetition. Here, the period is chosen after which the participants can repeat the Quiz. +6. Repetition interval: practice quizzes are characterized by their spaced repetition. Here, the period is chosen after which the participants can repeat the Quiz. 7. Order: The order in which the questions are to be solved by the participants is selected here. It is possible to choose between the following setups: Last response first, sequential or shuffled. 8. Availability (optional): Specify an optional start date from which the practice quiz is available after publication (default: immediately after publication). If the chosen date lies in the future and the practice quiz is published before, it will be available to students per the set start date. Should you not wish to use this option, simply leave the field value at its default value and the practice quiz will become available at the moment you publish it on the course overview after creation. **Choose your questions:** -For this step, existing questions are required. Note that Microlearning and Practice Quizzes support all element types except from free-text questions, as long as a valid sample solution is specified. If you need help with creating questions or content elements / flashcards, please review the [tutorial on element management](/tutorials/element_management/). +For this step, existing questions are required. Note that microlearnings and practice quizzes support all element types except from free-text questions, as long as a valid sample solution is specified. If you need help with creating questions or content elements / flashcards, please review the [tutorial on element management](/tutorials/element_management/). When adding questions, you can choose to have one or multiple questions per stack. It makes sense to have several questions per stack if you provide your students with a task / case study that builds up and the questions are connected. Stacks in practice quizzes can optionally be ordered according to a spaced repetition logic based on the student’s individual responses, while questions within a stack will always be shown in sequence. There are multiple options to add a question to a stack: @@ -59,7 +59,7 @@ There are multiple options to add a question to a stack: **End of preparation:** -13. With a click on Create your first Practice Quiz is prepared! +13. With a click on Create your first practice quiz is prepared! ## How can I publish a Practice Quiz and make it accessible to participants? @@ -79,10 +79,10 @@ There are multiple options to add a question to a stack: title="KlickerUZH - Publish Practice Quiz" > -1. After you create a Practice Quiz, you can find it under Courses. Then select the course to which you added the session. +1. After you create a practice quiz, you can find it under Courses. Then select the course to which you added the quiz. 2. In the course you have multiple options. You can... - ...copy the access link and provide it to your participants. - - ...edit your session before publishing it. - - ...publish it. Not that publishing a Practice Quiz or Microlearning session makes the item visible to all participants. This process cannot be undone. Changes to the content of an item cannot be made after publishing. - - ...delete your session. -3. After publishing your session, your participants can see the session in their account by joining the course. In the app, the Practice Quiz is displayed to the participants as shown in the screenshots below. + - ...edit your quiz before publishing it. + - ...publish it. Not that publishing a practice quiz or microlearning makes the item visible to all participants. This process cannot be undone. Changes to the content of an item cannot be made after publishing. + - ...delete your quiz. +3. After publishing your practice quiz, your participants can see it in their account by joining the course. In the app, the practice quiz is displayed to the participants as shown in the screenshots below. diff --git a/apps/docs/package.json b/apps/docs/package.json index 666b4554e5..e280aecf3a 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -23,7 +23,7 @@ "@mdx-js/react": "~3.1.0", "@tailwindcss/typography": "~0.5.15", "@types/react": "^18.3.11", - "@uzh-bf/design-system": "3.0.0-alpha.34", + "@uzh-bf/design-system": "3.0.0-alpha.35", "autoprefixer": "~10.4.20", "cross-env": "~7.0.3", "nodemon": "~3.1.7", diff --git a/apps/docs/src/constants.tsx b/apps/docs/src/constants.tsx index 935f4badb6..e145fcbc26 100644 --- a/apps/docs/src/constants.tsx +++ b/apps/docs/src/constants.tsx @@ -285,12 +285,11 @@ export const USE_CASES = { into OLAT, even solely for live quizzes, is strongly recommended.
  • - Number of uses per session: In the events organized by the DBF, + Number of users per live quiz: In the events organized by the DBF, KlickerUZH is mainly utilized in bachelor-level lectures with a large number of students (150-800 students). In a single semester (14 weeks), KlickerUZH was used between three to seven times in - these sessions, with an average of three questions asked per - session. + these sessions, with an average of three questions asked per quiz.
  • Gamification: Incorporating gamification in live quizzes works @@ -488,7 +487,7 @@ export const USE_CASES = { Grading: It could be an option to make participation in sessions and/or passing of quizzes before or during sessions mandatory or part of the grade. However, this could also negatively influence the - openness of the discussions. Mi + openness of the discussions.
  • diff --git a/apps/docs/src/pages/development.tsx b/apps/docs/src/pages/development.tsx index 2a6c6ddad7..1e2622e151 100644 --- a/apps/docs/src/pages/development.tsx +++ b/apps/docs/src/pages/development.tsx @@ -64,7 +64,7 @@ const tileContent = [ { title: 'Learning Analytics', content: - 'Analysis functionalities allow lecturers to evaluate their sessions and questions in terms of different quality dimensions, as well as students to reflect on their learning progress.', + 'Analysis functionalities allow lecturers to evaluate their quizzes and questions in terms of different quality dimensions, as well as students to reflect on their learning progress.', useCases: [ { content: 'Learning Analytics for Lecturers', diff --git a/apps/frontend-control/package.json b/apps/frontend-control/package.json index 9229a81fa1..f1d114d652 100644 --- a/apps/frontend-control/package.json +++ b/apps/frontend-control/package.json @@ -17,8 +17,7 @@ "@klicker-uzh/prisma": "workspace:*", "@klicker-uzh/shared-components": "workspace:*", "@socialgouv/matomo-next": "1.9.1", - "@uzh-bf/design-system": "3.0.0-alpha.34", - "body-parser": "1.20.3", + "@uzh-bf/design-system": "3.0.0-alpha.35", "cross-env": "7.0.3", "dayjs": "1.11.13", "deepmerge": "4.3.1", @@ -31,7 +30,6 @@ "localforage": "1.10.0", "next": "15.0.0", "next-intl": "3.21.1", - "nookies": "2.5.2", "react": "18.3.1", "react-dom": "18.3.1", "remeda": "2.15.0", @@ -43,13 +41,11 @@ "@tailwindcss/aspect-ratio": "~0.4.2", "@tailwindcss/forms": "~0.5.9", "@tailwindcss/typography": "~0.5.15", - "@types/body-parser": "^1.19.2", "@types/js-cookie": "^3.0.6", "@types/jsonwebtoken": "^9.0.7", "@types/node": "^20.16.1", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", - "@types/web-push": "^3.6.3", "autoprefixer": "~10.4.20", "cross-env": "~7.0.3", "cssnano": "~6.0.1", diff --git a/apps/frontend-control/src/components/Layout.tsx b/apps/frontend-control/src/components/Layout.tsx index 0fb4c161f6..5da244d85f 100644 --- a/apps/frontend-control/src/components/Layout.tsx +++ b/apps/frontend-control/src/components/Layout.tsx @@ -10,11 +10,11 @@ import MobileMenuBar from './layout/MobileMenuBar' interface LayoutProps { title: string children: React.ReactNode - sessionId?: string + quizId?: string className?: string } -function Layout({ title, children, sessionId, className }: LayoutProps) { +function Layout({ title, children, quizId, className }: LayoutProps) { const router = useRouter() const { loading: loadingUser, @@ -50,7 +50,7 @@ function Layout({ title, children, sessionId, className }: LayoutProps) {
    - +
    diff --git a/apps/frontend-control/src/components/common/LoginForm.tsx b/apps/frontend-control/src/components/common/LoginForm.tsx index be1030b3ac..ad5e574318 100644 --- a/apps/frontend-control/src/components/common/LoginForm.tsx +++ b/apps/frontend-control/src/components/common/LoginForm.tsx @@ -33,7 +33,7 @@ interface LoginFormProps { installIOS?: string } -export function LoginForm({ +function LoginForm({ header, labelIdentifier, fieldIdentifier, diff --git a/apps/frontend-control/src/components/layout/MobileMenuBar.tsx b/apps/frontend-control/src/components/layout/MobileMenuBar.tsx index 9eb567a6dd..112c5de6e7 100644 --- a/apps/frontend-control/src/components/layout/MobileMenuBar.tsx +++ b/apps/frontend-control/src/components/layout/MobileMenuBar.tsx @@ -7,14 +7,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useTranslations } from 'next-intl' import { useRouter } from 'next/router' import { useState } from 'react' -import EmbeddingModal from '../../components/sessions/EmbeddingModal' +import EmbeddingModal from '../liveQuizzes/EmbeddingModal' import MenuButton from './MenuButton' interface MobileMenuBarProps { - sessionId?: string + quizId?: string } -function MobileMenuBar({ sessionId }: MobileMenuBarProps) { +function MobileMenuBar({ quizId }: MobileMenuBarProps) { const t = useTranslations() const router = useRouter() const [embedModalOpen, setEmbedModalOpen] = useState(false) @@ -39,18 +39,18 @@ function MobileMenuBar({ sessionId }: MobileMenuBarProps) { } onClick={() => setEmbedModalOpen(true)} - disabled={!sessionId} + disabled={!quizId} data={{ cy: 'ppt-button' }} > PPT - {sessionId && ( + {quizId && ( )} diff --git a/apps/frontend-control/src/components/sessions/EmbeddingModal.tsx b/apps/frontend-control/src/components/liveQuizzes/EmbeddingModal.tsx similarity index 65% rename from apps/frontend-control/src/components/sessions/EmbeddingModal.tsx rename to apps/frontend-control/src/components/liveQuizzes/EmbeddingModal.tsx index 4f92294a73..79ec9c4be7 100644 --- a/apps/frontend-control/src/components/sessions/EmbeddingModal.tsx +++ b/apps/frontend-control/src/components/liveQuizzes/EmbeddingModal.tsx @@ -2,8 +2,8 @@ import { useQuery } from '@apollo/client' import { faClipboard } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { - GetSessionHmacDocument, - GetSingleLiveSessionDocument, + GetLiveQuizHmacDocument, + GetSingleLiveQuizDocument, } from '@klicker-uzh/graphql/dist/ops' import { Button, H2, Modal } from '@uzh-bf/design-system' import { useTranslations } from 'next-intl' @@ -13,29 +13,23 @@ import { useMemo } from 'react' interface EmbeddingModalProps { open: boolean setOpen: (newValue: boolean) => void - sessionId: string + quizId: string } -function LazyHMACLink({ - sessionId, - params, -}: { - sessionId: string - params: string -}) { - const sessionHMAC = useQuery(GetSessionHmacDocument, { +function LazyHMACLink({ quizId, params }: { quizId: string; params: string }) { + const quizHMAC = useQuery(GetLiveQuizHmacDocument, { variables: { - id: sessionId, + id: quizId, }, }) - if (sessionHMAC.loading || !sessionHMAC.data?.sessionHMAC) { + if (quizHMAC.loading || !quizHMAC.data?.liveQuizHMAC) { return <> } const link = `${ process.env.NEXT_PUBLIC_MANAGE_URL - }/sessions/${sessionId}/evaluation?hmac=${sessionHMAC.data?.sessionHMAC}${ + }/sessions/${quizId}/evaluation?hmac=${quizHMAC.data?.liveQuizHMAC}${ params ? `&${params}` : '' }` @@ -47,25 +41,23 @@ function LazyHMACLink({ onClick={() => navigator?.clipboard?.writeText(link)} /> - {link} + {link} ) } -function EmbeddingModal({ open, setOpen, sessionId }: EmbeddingModalProps) { +function EmbeddingModal({ open, setOpen, quizId }: EmbeddingModalProps) { const t = useTranslations() - const { data: dataLiveSession } = useQuery(GetSingleLiveSessionDocument, { - variables: { sessionId: sessionId || '' }, - skip: !sessionId, + const { data: dataLiveQuiz } = useQuery(GetSingleLiveQuizDocument, { + variables: { quizId: quizId || '' }, + skip: !quizId, }) const questions = useMemo( () => - dataLiveSession?.liveSession?.blocks?.flatMap( - (block) => block.instances - ) || [], - [dataLiveSession?.liveSession?.blocks] + dataLiveQuiz?.liveQuiz?.blocks?.flatMap((block) => block.elements) || [], + [dataLiveQuiz?.liveQuiz?.blocks] ) return ( @@ -90,17 +82,16 @@ function EmbeddingModal({ open, setOpen, sessionId }: EmbeddingModalProps) { >

    {t('control.course.pptEmbedding')}

    - {questions?.map((question: any, ix: number) => { + {questions?.map((element, ix) => { + if (!element || !element.elementData) return null + return ( -
    +
    {`${ix + 1}. ${ - question.questionData.name + element.elementData.name }`}
    - +
    ) @@ -108,7 +99,7 @@ function EmbeddingModal({ open, setOpen, sessionId }: EmbeddingModalProps) {
    {t('shared.generic.leaderboard')}:
    - +
    ) diff --git a/apps/frontend-control/src/components/sessions/SessionBlock.tsx b/apps/frontend-control/src/components/liveQuizzes/LiveQuizBlock.tsx similarity index 76% rename from apps/frontend-control/src/components/sessions/SessionBlock.tsx rename to apps/frontend-control/src/components/liveQuizzes/LiveQuizBlock.tsx index b8b384f3e6..1297937c72 100644 --- a/apps/frontend-control/src/components/sessions/SessionBlock.tsx +++ b/apps/frontend-control/src/components/liveQuizzes/LiveQuizBlock.tsx @@ -1,9 +1,9 @@ import { faClock, faPlay } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { - QuestionInstance, - SessionBlockStatus, - SessionBlock as SessionBlockType, + ElementBlock, + ElementBlockStatus, + ElementInstance, } from '@klicker-uzh/graphql/dist/ops' import { CycleCountdown, UserNotification } from '@uzh-bf/design-system' import dayjs from 'dayjs' @@ -11,24 +11,24 @@ import { useTranslations } from 'next-intl' import { useMemo } from 'react' import { twMerge } from 'tailwind-merge' -interface SessionBlockProps { - block?: Omit & { - instances?: - | (Omit & { - questionData?: { name: string } | null +interface LiveQuizBlockProps { + block?: Omit & { + elements?: + | (Omit & { + elementData?: { name: string } | null })[] | null } active?: boolean } -function SessionBlock({ block, active = false }: SessionBlockProps) { +function LiveQuizBlock({ block, active = false }: LiveQuizBlockProps) { const t = useTranslations() // compute the time until expiration in seconds + 20 seconds buffer from now const untilExpiration = useMemo(() => { if (!block) return -1 - if (block.status === SessionBlockStatus.Executed) { + if (block.status === ElementBlockStatus.Executed) { return -1 } return block.expiresAt @@ -73,12 +73,12 @@ function SessionBlock({ block, active = false }: SessionBlockProps) { key={`${block.expiresAt}-${block.status}`} overrideSize={15} isStatic={ - !block.expiresAt || block.status === SessionBlockStatus.Executed + !block.expiresAt || block.status === ElementBlockStatus.Executed } expiresAt={expirationTime} strokeWidthRem={0.2} totalDuration={ - block.status !== SessionBlockStatus.Executed + block.status !== ElementBlockStatus.Executed ? untilExpiration : 0 } @@ -88,18 +88,18 @@ function SessionBlock({ block, active = false }: SessionBlockProps) { }} /> )} - {block.status === SessionBlockStatus.Scheduled && ( + {block.status === ElementBlockStatus.Scheduled && ( )} - {block.status === SessionBlockStatus.Active && ( + {block.status === ElementBlockStatus.Active && ( )}
    - {block.instances?.map((instance) => ( + {block.elements?.map((instance) => (
    - - {instance.questionData?.name} + - {instance.elementData?.name}
    ))}
    @@ -107,4 +107,4 @@ function SessionBlock({ block, active = false }: SessionBlockProps) { ) } -export default SessionBlock +export default LiveQuizBlock diff --git a/apps/frontend-control/src/components/sessions/SessionLists.tsx b/apps/frontend-control/src/components/liveQuizzes/LiveQuizLists.tsx similarity index 65% rename from apps/frontend-control/src/components/sessions/SessionLists.tsx rename to apps/frontend-control/src/components/liveQuizzes/LiveQuizLists.tsx index 9e860a81a6..13ac42804c 100644 --- a/apps/frontend-control/src/components/sessions/SessionLists.tsx +++ b/apps/frontend-control/src/components/liveQuizzes/LiveQuizLists.tsx @@ -12,43 +12,46 @@ import ErrorStartToast from '../toasts/ErrorStartToast' import EmbeddingModal from './EmbeddingModal' import StartModal from './StartModal' -interface SessionListsProps { - runningSessions: { id: string; name: string }[] - plannedSessions: { id: string; name: string }[] +interface LiveQuizListsProps { + runningLiveQuizzes: { id: string; name: string }[] + plannedLiveQuizzes: { id: string; name: string }[] } -function SessionLists({ runningSessions, plannedSessions }: SessionListsProps) { +function LiveQuizLists({ + runningLiveQuizzes, + plannedLiveQuizzes, +}: LiveQuizListsProps) { const t = useTranslations() const [startModalOpen, setStartModalOpen] = useState(false) const [errorToast, setErrorToast] = useState(false) const [startId, setStartId] = useState('') const [startName, setStartName] = useState('') const [embedOpen, setEmbedOpen] = useState(false) - const [sessionId, setSessionId] = useState('') + const [quizId, setQuizId] = useState('') return ( <> -

    {t('control.course.runningSessions')}

    - {runningSessions.length > 0 ? ( +

    {t('control.course.runningLiveQuizzes')}

    + {runningLiveQuizzes.length > 0 ? (
    - {runningSessions.map((session) => ( -
    + {runningLiveQuizzes.map((quiz) => ( +
    ) : ( -
    {t('control.course.noRunningSessions')}
    +
    {t('control.course.noRunningLiveQuizzes')}
    )}

    - {t('control.course.plannedSessions')} + {t('control.course.plannedLiveQuizzes')}

    - {plannedSessions.length > 0 ? ( + {plannedLiveQuizzes.length > 0 ? (
    - {plannedSessions.map((session) => ( -
    + {plannedLiveQuizzes.map((quiz) => ( +
    { setStartModalOpen(true) - setStartId(session.id) - setStartName(session.name) + setStartId(quiz.id) + setStartName(quiz.name) }} - data={{ cy: `start-session-${session.name}` }} + data={{ cy: `start-live-quiz-${quiz.name}` }} />
    ) : ( -
    {t('control.course.noPlannedSessions')}
    +
    {t('control.course.noPlannedLiveQuizzes')}
    )} setEmbedOpen(newValue)} - sessionId={sessionId} + quizId={quizId} /> void + setErrorToast: (open: boolean) => void +} + +function StartModal({ + quizId, + quizName, + startModalOpen, + setStartModalOpen, + setErrorToast, +}: StartModalProps) { + const t = useTranslations() + const router = useRouter() + const [startLiveQuiz, { loading: startingLiveQuiz }] = useMutation( + StartLiveQuizDocument, + { + optimisticResponse: { + startLiveQuiz: { + __typename: 'LiveQuizMeta', + id: quizId, + name: quizName, + status: PublicationStatus.Published, + }, + }, + update(cache) { + const data = cache.readQuery({ + query: GetUnassignedLiveQuizzesDocument, + }) + cache.writeQuery({ + query: GetUnassignedLiveQuizzesDocument, + data: { + unassignedLiveQuizzes: + data?.unassignedLiveQuizzes?.map((quiz) => + quiz.id === quizId + ? { + id: quizId, + name: quizName, + status: PublicationStatus.Published, + } + : quiz + ) ?? [], + }, + }) + }, + } + ) + + return ( + setStartModalOpen(false)} + onPrimaryAction={ + + } + onSecondaryAction={ + + } + className={{ content: 'mx-auto my-auto h-max w-max md:min-w-[30rem]' }} + hideCloseButton + > +

    {t('control.course.startLiveQuiz')}

    +
    + {t('control.course.confirmStartLiveQuiz')} +
    {quizName}
    +
    +
    + {t('control.course.explanationStartLiveQuiz')} +
    +
    + ) +} + +export default StartModal diff --git a/apps/frontend-control/src/components/sessions/StartModal.tsx b/apps/frontend-control/src/components/sessions/StartModal.tsx deleted file mode 100644 index 9137f49f2a..0000000000 --- a/apps/frontend-control/src/components/sessions/StartModal.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useMutation } from '@apollo/client' -import { StartSessionDocument } from '@klicker-uzh/graphql/dist/ops' -import { Button, H3, Modal } from '@uzh-bf/design-system' -import { useTranslations } from 'next-intl' -import { useRouter } from 'next/router' - -interface StartModalProps { - startId: string - startName: string - startModalOpen: boolean - setStartModalOpen: (open: boolean) => void - setErrorToast: (open: boolean) => void -} - -function StartModal({ - startId, - startName, - startModalOpen, - setStartModalOpen, - setErrorToast, -}: StartModalProps) { - const t = useTranslations() - const router = useRouter() - const [startSession, { loading: startingSession }] = - useMutation(StartSessionDocument) - - return ( - setStartModalOpen(false)} - onPrimaryAction={ - - } - onSecondaryAction={ - - } - className={{ content: 'mx-auto my-auto h-max w-max md:min-w-[30rem]' }} - hideCloseButton - > -

    {t('control.course.startSession')}

    -
    - {t('control.course.confirmStartSession')} -
    {startName}
    -
    -
    - {t('control.course.explanationStartSession')} -
    -
    - ) -} - -export default StartModal diff --git a/apps/frontend-control/src/components/toasts/ErrorStartToast.tsx b/apps/frontend-control/src/components/toasts/ErrorStartToast.tsx index 659a0f3c31..31a1e2b89e 100644 --- a/apps/frontend-control/src/components/toasts/ErrorStartToast.tsx +++ b/apps/frontend-control/src/components/toasts/ErrorStartToast.tsx @@ -17,7 +17,7 @@ function ErrorStartToast({ open, setOpen }: ErrorStartToastProps) { type="error" duration={5000} > - {t('control.course.sessionStartFailed')} + {t('control.course.liveQuizStartFailed')} ) } diff --git a/apps/frontend-control/src/pages/course/[id].tsx b/apps/frontend-control/src/pages/course/[id].tsx index c88eafeee5..23b274a816 100644 --- a/apps/frontend-control/src/pages/course/[id].tsx +++ b/apps/frontend-control/src/pages/course/[id].tsx @@ -1,7 +1,7 @@ import { useQuery } from '@apollo/client' import { GetControlCourseDocument, - SessionStatus, + PublicationStatus, } from '@klicker-uzh/graphql/dist/ops' import Loader from '@klicker-uzh/shared-components/src/Loader' import { UserNotification } from '@uzh-bf/design-system' @@ -10,7 +10,7 @@ import { useTranslations } from 'next-intl' import { useRouter } from 'next/router' import { useEffect } from 'react' import Layout from '../../components/Layout' -import SessionLists from '../../components/sessions/SessionLists' +import LiveQuizLists from '../../components/liveQuizzes/LiveQuizLists' function Course() { const t = useTranslations() @@ -49,24 +49,24 @@ function Course() { const { controlCourse } = data - const runningSessions = controlCourse.sessions?.filter( - (session) => session.status === SessionStatus.Running + const runningQuizzes = controlCourse.liveQuizzes?.filter( + (quiz) => quiz.status === PublicationStatus.Published ) - const plannedSessions = controlCourse.sessions?.filter( - (session) => - session.status === SessionStatus.Prepared || - session.status === SessionStatus.Scheduled + const plannedQuizzes = controlCourse.liveQuizzes?.filter( + (quiz) => + quiz.status === PublicationStatus.Draft || + quiz.status === PublicationStatus.Scheduled ) return ( -
    - {t('control.course.completedSessionsHint')} + {t('control.course.completedLiveQuizzesHint')}
    ) diff --git a/apps/frontend-control/src/pages/course/unassigned.tsx b/apps/frontend-control/src/pages/course/unassigned.tsx index 3985dec999..a648163b04 100644 --- a/apps/frontend-control/src/pages/course/unassigned.tsx +++ b/apps/frontend-control/src/pages/course/unassigned.tsx @@ -1,7 +1,7 @@ import { useQuery } from '@apollo/client' import { - GetUnassignedSessionsDocument, - SessionStatus, + GetUnassignedLiveQuizzesDocument, + PublicationStatus, } from '@klicker-uzh/graphql/dist/ops' import Loader from '@klicker-uzh/shared-components/src/Loader' import { UserNotification } from '@uzh-bf/design-system' @@ -9,54 +9,50 @@ import { GetStaticPropsContext } from 'next' import { useTranslations } from 'next-intl' import { useMemo } from 'react' import Layout from '../../components/Layout' -import SessionLists from '../../components/sessions/SessionLists' +import LiveQuizLists from '../../components/liveQuizzes/LiveQuizLists' -function UnassignedSessions() { +function UnassignedLiveQuizzes() { const t = useTranslations() - const { - loading: loadingSessions, - error: errorSessions, - data: dataSessions, - } = useQuery(GetUnassignedSessionsDocument) + const { data, loading, error } = useQuery(GetUnassignedLiveQuizzesDocument) - const runningSessions = useMemo(() => { - return dataSessions?.unassignedSessions?.filter( - (session) => session.status === SessionStatus.Running + const runningQuizzes = useMemo(() => { + return data?.unassignedLiveQuizzes?.filter( + (quiz) => quiz.status === PublicationStatus.Published ) - }, [dataSessions]) + }, [data]) - const plannedSessions = useMemo(() => { - return dataSessions?.unassignedSessions?.filter( - (session) => - session.status === SessionStatus.Scheduled || - session.status === SessionStatus.Prepared + const plannedQuizzes = useMemo(() => { + return data?.unassignedLiveQuizzes?.filter( + (quiz) => + quiz.status === PublicationStatus.Scheduled || + quiz.status === PublicationStatus.Draft ) - }, [dataSessions]) + }, [data]) - if (loadingSessions) { + if (loading) { return ( - + ) } - if (errorSessions || !dataSessions) { + if (error || !data) { return ( - + ) } return ( - - + ) @@ -70,4 +66,4 @@ export async function getStaticProps({ locale }: GetStaticPropsContext) { } } -export default UnassignedSessions +export default UnassignedLiveQuizzes diff --git a/apps/frontend-control/src/pages/index.tsx b/apps/frontend-control/src/pages/index.tsx index 20547f65fb..6c777681c4 100644 --- a/apps/frontend-control/src/pages/index.tsx +++ b/apps/frontend-control/src/pages/index.tsx @@ -68,13 +68,13 @@ function Index() { )}
    -

    {t('control.home.sessionsNoCourse')}

    +

    {t('control.home.liveQuizzesNoCourse')}

    diff --git a/apps/frontend-control/src/pages/session/[id].tsx b/apps/frontend-control/src/pages/session/[id].tsx index 3bc5867c2e..d9fd2bffb8 100644 --- a/apps/frontend-control/src/pages/session/[id].tsx +++ b/apps/frontend-control/src/pages/session/[id].tsx @@ -2,12 +2,12 @@ import { useMutation, useQuery } from '@apollo/client' import { faArrowDown, faEllipsis } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { - ActivateSessionBlockDocument, - DeactivateSessionBlockDocument, - EndSessionDocument, - GetControlSessionDocument, - GetUserRunningSessionsDocument, - SessionBlockStatus, + ActivateLiveQuizBlockDocument, + DeactivateLiveQuizBlockDocument, + ElementBlockStatus, + EndLiveQuizDocument, + GetControlLiveQuizDocument, + GetUnassignedLiveQuizzesDocument, } from '@klicker-uzh/graphql/dist/ops' import Loader from '@klicker-uzh/shared-components/src/Loader' import { Button, H3, UserNotification } from '@uzh-bf/design-system' @@ -17,33 +17,46 @@ import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { sort } from 'remeda' import Layout from '../../components/Layout' -import SessionBlock from '../../components/sessions/SessionBlock' +import LiveQuizBlock from '../../components/liveQuizzes/LiveQuizBlock' -function RunningSession() { +function RunningLiveQuiz() { const t = useTranslations() const router = useRouter() const [nextBlockOrder, setNextBlockOrder] = useState(-1) const [currentBlockOrder, setCurrentBlockOrder] = useState< number | undefined >(undefined) - const [activateSessionBlock, { loading: activatingBlock }] = useMutation( - ActivateSessionBlockDocument + const [activateLiveQuizBlock, { loading: activatingBlock }] = useMutation( + ActivateLiveQuizBlockDocument ) - const [deactivateSessionBlock, { loading: deactivatingBlock }] = useMutation( - DeactivateSessionBlockDocument + const [deactivateLiveQuizBlock, { loading: deactivatingBlock }] = useMutation( + DeactivateLiveQuizBlockDocument ) - const [endSession, { loading: endingLiveQuiz }] = useMutation( - EndSessionDocument, + const [endLiveQuiz, { loading: endingLiveQuiz }] = useMutation( + EndLiveQuizDocument, { - refetchQueries: [{ query: GetUserRunningSessionsDocument }], + update(cache, res) { + const data = cache.readQuery({ + query: GetUnassignedLiveQuizzesDocument, + }) + cache.writeQuery({ + query: GetUnassignedLiveQuizzesDocument, + data: { + unassignedLiveQuizzes: + data?.unassignedLiveQuizzes?.filter( + (q) => q.id !== res.data?.endLiveQuiz?.id + ) ?? [], + }, + }) + }, } ) const { - loading: sessionLoading, - error: sessionError, - data: sessionData, - } = useQuery(GetControlSessionDocument, { + loading: quizLoading, + error: quizError, + data: quizData, + } = useQuery(GetControlLiveQuizDocument, { variables: { id: router.query.id as string, }, @@ -52,56 +65,53 @@ function RunningSession() { }) useEffect(() => { - setCurrentBlockOrder(sessionData?.controlSession?.activeBlock?.order) - }, [ - sessionData?.controlSession?.id, - sessionData?.controlSession?.activeBlock, - ]) + setCurrentBlockOrder(quizData?.controlLiveQuiz?.activeBlock?.order) + }, [quizData?.controlLiveQuiz?.id, quizData?.controlLiveQuiz?.activeBlock]) useEffect(() => { - if (!sessionData?.controlSession?.blocks) return + if (!quizData?.controlLiveQuiz?.blocks) return const sortedBlocks = sort( - sessionData?.controlSession?.blocks, + quizData?.controlLiveQuiz?.blocks, (a, b) => a.order - b.order ) if (!sortedBlocks) return const scheduledNext = sortedBlocks.find( - (block) => block.status === SessionBlockStatus.Scheduled + (block) => block.status === ElementBlockStatus.Scheduled ) setNextBlockOrder( typeof scheduledNext === 'undefined' ? -1 : scheduledNext.order ) - }, [sessionData?.controlSession?.blocks]) + }, [quizData?.controlLiveQuiz?.blocks]) - if (sessionLoading) { + if (quizLoading) { return ( - + ) } - if (!sessionData?.controlSession || sessionError) { + if (!quizData?.controlLiveQuiz || quizError) { return ( - + ) } - const { id, name, course, blocks } = sessionData?.controlSession + const { id, name, course, blocks } = quizData?.controlLiveQuiz if (!blocks) { return ( ) @@ -109,15 +119,15 @@ function RunningSession() { return (
    {typeof currentBlockOrder !== 'undefined' ? (
    -

    {t('control.session.activeBlock')}

    +

    {t('control.liveQuiz.activeBlock')}

    - block.order === currentBlockOrder)} active /> @@ -131,7 +141,7 @@ function RunningSession() { size="2xl" /> - block.order === nextBlockOrder )} @@ -141,10 +151,10 @@ function RunningSession() {
    ) : nextBlockOrder !== -1 ? (
    -

    {t('control.session.nextBlock')}

    +

    {t('control.liveQuiz.nextBlock')}

    {nextBlockOrder > 0 && ( )} - block.order === nextBlockOrder)} /> {nextBlockOrder < blocks.length - 1 && ( @@ -183,10 +193,10 @@ function RunningSession() { loading={activatingBlock} onClick={async () => { { - await activateSessionBlock({ + await activateLiveQuizBlock({ variables: { - sessionId: id, - sessionBlockId: + quizId: id, + blockId: blocks.find((block) => block.order === nextBlockOrder) ?.id || -1, }, @@ -200,7 +210,7 @@ function RunningSession() { }} data={{ cy: 'activate-next-block' }} > - {t('control.session.activateBlockN', { + {t('control.liveQuiz.activateBlockN', { number: nextBlockOrder + 1, })} @@ -209,13 +219,13 @@ function RunningSession() {
    )} {typeof currentBlockOrder !== 'undefined' && nextBlockOrder == -1 && ( )} @@ -256,4 +266,4 @@ export function getStaticPaths() { } } -export default RunningSession +export default RunningLiveQuiz diff --git a/apps/frontend-manage/package.json b/apps/frontend-manage/package.json index 1926c3b9e4..76c49073df 100644 --- a/apps/frontend-manage/package.json +++ b/apps/frontend-manage/package.json @@ -21,8 +21,7 @@ "@socialgouv/matomo-next": "1.9.1", "@tanstack/react-table": "8.20.5", "@uidotdev/usehooks": "2.4.1", - "@uzh-bf/design-system": "3.0.0-alpha.34", - "d3": "7.9.0", + "@uzh-bf/design-system": "3.0.0-alpha.35", "dayjs": "1.11.13", "deepmerge": "4.3.1", "formik": "2.4.6", @@ -30,7 +29,6 @@ "graphql": "16.9.0", "graphql-codegen-persisted-query-ids": "0.2.0", "graphql-ws": "5.16.0", - "html-to-image": "1.11.11", "is-hotkey": "0.2.0", "js-cookie": "3.0.5", "js-search": "2.0.1", @@ -39,7 +37,6 @@ "nanoid": "5.0.8", "next": "15.0.0", "next-intl": "3.21.1", - "nookies": "2.5.2", "react": "18.3.1", "react-csv-downloader": "3.1.1", "react-d3-speedometer": "2.2.1", @@ -48,9 +45,7 @@ "react-dom": "18.3.1", "react-dropzone": "14.2.9", "react-qrcode-logo": "3.0.0", - "react-resizable-panels": "2.1.4", "react-select": "5.8.1", - "react-sizeme": "3.0.2", "react-tagcloud": "2.3.3", "recharts": "2.13.0", "remark-slate": "1.8.6", @@ -67,7 +62,6 @@ "@tailwindcss/container-queries": "~0.1.1", "@tailwindcss/forms": "~0.5.9", "@tailwindcss/typography": "~0.5.15", - "@types/d3": "^7.4.3", "@types/is-hotkey": "^0.1.10", "@types/js-cookie": "^3.0.6", "@types/js-search": "^1.4.4", diff --git a/apps/frontend-manage/src/components/sessions/creation/ElementCreation.tsx b/apps/frontend-manage/src/components/activities/ElementCreation.tsx similarity index 87% rename from apps/frontend-manage/src/components/sessions/creation/ElementCreation.tsx rename to apps/frontend-manage/src/components/activities/ElementCreation.tsx index eff20e3970..6e20f7ad8e 100644 --- a/apps/frontend-manage/src/components/sessions/creation/ElementCreation.tsx +++ b/apps/frontend-manage/src/components/activities/ElementCreation.tsx @@ -4,22 +4,21 @@ import { Element, GetActiveUserCoursesDocument, GetGroupActivityDocument, - GetSingleLiveSessionDocument, + GetSingleLiveQuizDocument, GetSingleMicroLearningDocument, GetSinglePracticeQuizDocument, GroupActivity, MicroLearning, PracticeQuiz, PublicationStatus, - Session, } from '@klicker-uzh/graphql/dist/ops' import Loader from '@klicker-uzh/shared-components/src/Loader' import { useTranslations } from 'next-intl' import { useMemo } from 'react' -import GroupActivityWizard from './groupActivity/GroupActivityWizard' -import LiveSessionWizard from './liveQuiz/LiveSessionWizard' -import MicroLearningWizard from './microLearning/MicroLearningWizard' -import PracticeQuizWizard from './practiceQuiz/PracticeQuizWizard' +import GroupActivityWizard from './creation/groupActivity/GroupActivityWizard' +import LiveQuizWizard from './creation/liveQuiz/LiveQuizWizard' +import MicroLearningWizard from './creation/microLearning/MicroLearningWizard' +import PracticeQuizWizard from './creation/practiceQuiz/PracticeQuizWizard' export enum WizardMode { LiveQuiz = 'liveQuiz', @@ -42,7 +41,7 @@ export type ElementSelectCourse = { interface ElementCreationProps { creationMode: WizardMode closeWizard: () => void - elementId?: string + activityId?: string editMode?: string duplicationMode?: WizardMode conversionMode?: string @@ -53,7 +52,7 @@ interface ElementCreationProps { function ElementCreation({ creationMode, closeWizard, - elementId, + activityId, editMode, duplicationMode, conversionMode, @@ -61,12 +60,12 @@ function ElementCreation({ resetSelection, }: ElementCreationProps) { const t = useTranslations() - const { data: dataLiveSession, loading: liveLoading } = useQuery( - GetSingleLiveSessionDocument, + const { data: dataLiveQuiz, loading: liveLoading } = useQuery( + GetSingleLiveQuizDocument, { - variables: { sessionId: elementId || '' }, + variables: { quizId: activityId || '' }, skip: - !elementId || + !activityId || (editMode !== WizardMode.LiveQuiz && duplicationMode !== WizardMode.LiveQuiz) || conversionMode === 'microLearningToPracticeQuiz', @@ -75,9 +74,9 @@ function ElementCreation({ const { data: dataMicroLearning, loading: microLoading } = useQuery( GetSingleMicroLearningDocument, { - variables: { id: elementId || '' }, + variables: { id: activityId || '' }, skip: - !elementId || + !activityId || (editMode !== WizardMode.Microlearning && duplicationMode !== WizardMode.Microlearning && conversionMode !== 'microLearningToPracticeQuiz'), @@ -86,9 +85,9 @@ function ElementCreation({ const { data: dataPracticeQuiz, loading: learningLoading } = useQuery( GetSinglePracticeQuizDocument, { - variables: { id: elementId || '' }, + variables: { id: activityId || '' }, skip: - !elementId || + !activityId || (editMode !== WizardMode.PracticeQuiz && duplicationMode !== WizardMode.PracticeQuiz) || conversionMode === 'microLearningToPracticeQuiz', @@ -97,9 +96,9 @@ function ElementCreation({ const { data: dataGroupActivity, loading: groupActivityLoading } = useQuery( GetGroupActivityDocument, { - variables: { id: elementId || '' }, + variables: { id: activityId || '' }, skip: - !elementId || + !activityId || (editMode !== WizardMode.GroupActivity && duplicationMode !== WizardMode.GroupActivity), } @@ -140,23 +139,23 @@ function ElementCreation({ if ( (!errorCourses && loadingCourses) || - (elementId && + (activityId && (editMode === WizardMode.LiveQuiz || duplicationMode === WizardMode.LiveQuiz) && liveLoading) || - (elementId && + (activityId && (editMode === WizardMode.Microlearning || duplicationMode === WizardMode.Microlearning) && microLoading) || - (elementId && + (activityId && (editMode === WizardMode.PracticeQuiz || duplicationMode === WizardMode.PracticeQuiz) && learningLoading) || - (elementId && + (activityId && (editMode === WizardMode.GroupActivity || duplicationMode === WizardMode.GroupActivity) && groupActivityLoading) || - (elementId && + (activityId && conversionMode === 'microLearningToPracticeQuiz' && microLoading) ) { @@ -200,18 +199,18 @@ function ElementCreation({
    {creationMode === WizardMode.LiveQuiz && ( - void selection?: Record resetSelection?: () => void acceptedTypes: ElementType[] } +interface AddBlockButtonProps { + type: 'block' + push: (value: ElementBlockFormValues) => void + selection?: Record + resetSelection?: () => void + acceptedTypes: ElementType[] +} + function AddStackButton({ + type, push, selection, resetSelection, acceptedTypes, -}: AddStackButtonProps) { +}: AddStackButtonProps | AddBlockButtonProps) { const t = useTranslations() const [{ isOver }, drop] = useDrop( () => ({ accept: acceptedTypes, drop: (item: QuestionDragDropTypes) => { - push({ - displayName: '', - description: '', - elements: [ - { - id: item.id, - title: item.title, - type: item.questionType, - hasSampleSolution: item.hasSampleSolution, - }, - ], - }) + const initialElements = [ + { + id: item.id, + title: item.title, + type: item.questionType, + hasSampleSolution: item.hasSampleSolution, + }, + ] + + if (type === 'block') { + push({ + timeLimit: undefined, + elements: initialElements, + }) + } else { + push({ + displayName: '', + description: '', + elements: initialElements, + }) + } }, collect: (monitor) => ({ isOver: !!monitor.isOver(), @@ -58,23 +77,28 @@ function AddStackButton({ root: 'flex max-w-[135px] flex-1 flex-col justify-center gap-1 border-orange-300 bg-orange-100 text-sm hover:border-orange-400 hover:bg-orange-200 hover:text-orange-900', }} onClick={() => { - const stackElements = Object.values(selection).map( - (question) => ({ - id: question.id, - title: question.name, - type: question.type, - hasSampleSolution: - 'options' in question - ? (question.options.hasSampleSolution ?? false) - : true, - }) - ) + const elements = Object.values(selection).map((question) => ({ + id: question.id, + title: question.name, + type: question.type, + hasSampleSolution: + 'options' in question + ? (question.options.hasSampleSolution ?? false) + : true, + })) - push({ - displayName: '', - description: '', - elements: stackElements, - }) + if (type === 'block') { + push({ + timeLimit: undefined, + elements, + }) + } else { + push({ + displayName: '', + description: '', + elements, + }) + } resetSelection?.() }} data={{ cy: 'add-stack-with-selected' }} @@ -84,9 +108,13 @@ function AddStackButton({ - {t('manage.sessionForms.newStackSelected', { - count: Object.keys(selection).length, - })} + {type === 'block' + ? t('manage.activityWizard.newStackSelected', { + count: Object.keys(selection).length, + }) + : t('manage.activityWizard.newBlockSelected', { + count: Object.keys(selection).length, + })}
    - {t('manage.sessionForms.pasteSingleElementsStack', { - count: Object.keys(selection).length, - })} + {t( + type === 'block' + ? 'manage.activityWizard.pasteSingleElementsBlock' + : 'manage.activityWizard.pasteSingleElementsStack', + { + count: Object.keys(selection).length, + } + )}
    @@ -136,17 +178,28 @@ function AddStackButton({ 'hover:bg-primary-20 flex w-full cursor-pointer flex-col items-center justify-center rounded border border-solid p-2 text-center md:w-16', isOver && 'bg-primary-20' )} - onClick={() => - push({ - displayName: '', - description: '', - elements: [], - }) - } + onClick={() => { + if (type === 'block') { + push({ + timeLimit: undefined, + elements: [], + }) + } else { + push({ + displayName: '', + description: '', + elements: [], + }) + } + }} data-cy="drop-elements-add-stack" > -
    {t('manage.sessionForms.newStack')}
    +
    + {type === 'block' + ? t('manage.activityWizard.newBlock') + : t('manage.activityWizard.newStack')} +
    )}
    diff --git a/apps/frontend-manage/src/components/sessions/creation/CompletionStep.tsx b/apps/frontend-manage/src/components/activities/creation/CompletionStep.tsx similarity index 85% rename from apps/frontend-manage/src/components/sessions/creation/CompletionStep.tsx rename to apps/frontend-manage/src/components/activities/creation/CompletionStep.tsx index b54440870b..37faac4e24 100644 --- a/apps/frontend-manage/src/components/sessions/creation/CompletionStep.tsx +++ b/apps/frontend-manage/src/components/activities/creation/CompletionStep.tsx @@ -40,8 +40,8 @@ function CompletionStep({ {completionSuccessMessage ? completionSuccessMessage(name) : editMode - ? t('manage.sessionForms.changesSaved') - : t('manage.sessionForms.elementCreated')} + ? t('manage.activityWizard.changesSaved') + : t('manage.activityWizard.elementCreated')}
    {children} @@ -56,7 +56,7 @@ function CompletionStep({ - {t('manage.sessionForms.openPreview')} + {t('manage.activityWizard.openPreview')} @@ -64,13 +64,15 @@ function CompletionStep({ @@ -84,7 +86,9 @@ function CompletionStep({ - {t('manage.sessionForms.closeWizard')} + + {t('manage.activityWizard.closeWizard')} + ) : ( )} diff --git a/apps/frontend-manage/src/components/sessions/creation/CourseChangeMonitor.tsx b/apps/frontend-manage/src/components/activities/creation/CourseChangeMonitor.tsx similarity index 96% rename from apps/frontend-manage/src/components/sessions/creation/CourseChangeMonitor.tsx rename to apps/frontend-manage/src/components/activities/creation/CourseChangeMonitor.tsx index 0ba5a14da8..2507a87f78 100644 --- a/apps/frontend-manage/src/components/sessions/creation/CourseChangeMonitor.tsx +++ b/apps/frontend-manage/src/components/activities/creation/CourseChangeMonitor.tsx @@ -1,6 +1,6 @@ import { FormikErrors, FormikTouched } from 'formik' import { SetStateAction, useEffect } from 'react' -import { ElementSelectCourse } from './ElementCreation' +import { ElementSelectCourse } from '../ElementCreation' import { GroupActivityFormValues } from './WizardLayout' function CourseChangeMonitor({ diff --git a/apps/frontend-manage/src/components/sessions/creation/CourseSelectionMonitorMicrolearning.tsx b/apps/frontend-manage/src/components/activities/creation/CourseSelectionMonitorMicrolearning.tsx similarity index 96% rename from apps/frontend-manage/src/components/sessions/creation/CourseSelectionMonitorMicrolearning.tsx rename to apps/frontend-manage/src/components/activities/creation/CourseSelectionMonitorMicrolearning.tsx index c1f1ac2eec..60a3431a52 100644 --- a/apps/frontend-manage/src/components/sessions/creation/CourseSelectionMonitorMicrolearning.tsx +++ b/apps/frontend-manage/src/components/activities/creation/CourseSelectionMonitorMicrolearning.tsx @@ -1,6 +1,6 @@ import { FormikErrors, FormikTouched } from 'formik' import { SetStateAction, useEffect } from 'react' -import { ElementSelectCourse } from './ElementCreation' +import { ElementSelectCourse } from '../ElementCreation' import { MicroLearningFormValues } from './WizardLayout' function CourseSelectionMonitorMicrolearning({ diff --git a/apps/frontend-manage/src/components/sessions/creation/CourseSelectionMonitorPracticeQuiz.tsx b/apps/frontend-manage/src/components/activities/creation/CourseSelectionMonitorPracticeQuiz.tsx similarity index 96% rename from apps/frontend-manage/src/components/sessions/creation/CourseSelectionMonitorPracticeQuiz.tsx rename to apps/frontend-manage/src/components/activities/creation/CourseSelectionMonitorPracticeQuiz.tsx index 14c3480dfc..37279d0a20 100644 --- a/apps/frontend-manage/src/components/sessions/creation/CourseSelectionMonitorPracticeQuiz.tsx +++ b/apps/frontend-manage/src/components/activities/creation/CourseSelectionMonitorPracticeQuiz.tsx @@ -1,6 +1,6 @@ import { FormikErrors, FormikTouched } from 'formik' import { SetStateAction, useEffect } from 'react' -import { ElementSelectCourse } from './ElementCreation' +import { ElementSelectCourse } from '../ElementCreation' import { PracticeQuizFormValues } from './WizardLayout' function CourseSelectionMonitorPracticeQuiz({ diff --git a/apps/frontend-manage/src/components/sessions/creation/CreationButton.tsx b/apps/frontend-manage/src/components/activities/creation/CreationButton.tsx similarity index 100% rename from apps/frontend-manage/src/components/sessions/creation/CreationButton.tsx rename to apps/frontend-manage/src/components/activities/creation/CreationButton.tsx diff --git a/apps/frontend-manage/src/components/sessions/creation/CreationFormValidator.tsx b/apps/frontend-manage/src/components/activities/creation/CreationFormValidator.tsx similarity index 100% rename from apps/frontend-manage/src/components/sessions/creation/CreationFormValidator.tsx rename to apps/frontend-manage/src/components/activities/creation/CreationFormValidator.tsx diff --git a/apps/frontend-manage/src/components/sessions/creation/DateChangeMonitor.tsx b/apps/frontend-manage/src/components/activities/creation/DateChangeMonitor.tsx similarity index 100% rename from apps/frontend-manage/src/components/sessions/creation/DateChangeMonitor.tsx rename to apps/frontend-manage/src/components/activities/creation/DateChangeMonitor.tsx diff --git a/apps/frontend-manage/src/components/sessions/creation/DescriptionStep.tsx b/apps/frontend-manage/src/components/activities/creation/DescriptionStep.tsx similarity index 97% rename from apps/frontend-manage/src/components/sessions/creation/DescriptionStep.tsx rename to apps/frontend-manage/src/components/activities/creation/DescriptionStep.tsx index be8e70fee4..0b7ee0691e 100644 --- a/apps/frontend-manage/src/components/sessions/creation/DescriptionStep.tsx +++ b/apps/frontend-manage/src/components/activities/creation/DescriptionStep.tsx @@ -6,7 +6,7 @@ import CreationFormValidator from './CreationFormValidator' import EditorField from './EditorField' import WizardNavigation from './WizardNavigation' import { GroupActivityWizardStepProps } from './groupActivity/GroupActivityWizard' -import { LiveQuizWizardStepProps } from './liveQuiz/LiveSessionWizard' +import { LiveQuizWizardStepProps } from './liveQuiz/LiveQuizWizard' import { MicroLearningWizardStepProps } from './microLearning/MicroLearningWizard' import { PracticeQuizWizardStepProps } from './practiceQuiz/PracticeQuizWizard' @@ -95,7 +95,7 @@ function DescriptionStep({ required autoComplete="off" name="displayName" - label={t('manage.sessionForms.displayName')} + label={t('manage.activityWizard.displayName')} tooltip={displayNameTooltip} className={{ root: 'mb-1 w-full md:w-1/2', diff --git a/apps/frontend-manage/src/components/activities/creation/DropElementsStack.tsx b/apps/frontend-manage/src/components/activities/creation/DropElementsStack.tsx new file mode 100644 index 0000000000..22fd2ae25a --- /dev/null +++ b/apps/frontend-manage/src/components/activities/creation/DropElementsStack.tsx @@ -0,0 +1,34 @@ +import { faPlus } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { ReactElement } from 'react' +import { ConnectableElement } from 'react-dnd' +import { twMerge } from 'tailwind-merge' + +function DropElementsStack({ + type, + drop, + isOver, + index, +}: { + type: 'block' | 'stack' + drop: ( + elementOrNode: ConnectableElement, + options?: any + ) => ReactElement | null + isOver: boolean + index: number +}) { + return drop( +
    + +
    + ) +} + +export default DropElementsStack diff --git a/apps/frontend-manage/src/components/sessions/creation/EditorField.tsx b/apps/frontend-manage/src/components/activities/creation/EditorField.tsx similarity index 95% rename from apps/frontend-manage/src/components/sessions/creation/EditorField.tsx rename to apps/frontend-manage/src/components/activities/creation/EditorField.tsx index 2910a634d3..d0045aa3d6 100644 --- a/apps/frontend-manage/src/components/sessions/creation/EditorField.tsx +++ b/apps/frontend-manage/src/components/activities/creation/EditorField.tsx @@ -68,7 +68,9 @@ function EditorField({ helpers.setTouched(true) }} showToolbarOnFocus={showToolbarOnFocus} - placeholder={placeholder ?? t('manage.sessionForms.enterContentHere')} + placeholder={ + placeholder ?? t('manage.activityWizard.enterContentHere') + } className={{ ...className?.input, root: twMerge('w-full', className?.input?.root), diff --git a/apps/frontend-manage/src/components/sessions/creation/MultiplierSelector.tsx b/apps/frontend-manage/src/components/activities/creation/MultiplierSelector.tsx similarity index 60% rename from apps/frontend-manage/src/components/sessions/creation/MultiplierSelector.tsx rename to apps/frontend-manage/src/components/activities/creation/MultiplierSelector.tsx index dadedc5567..18cbdfa31c 100644 --- a/apps/frontend-manage/src/components/sessions/creation/MultiplierSelector.tsx +++ b/apps/frontend-manage/src/components/activities/creation/MultiplierSelector.tsx @@ -20,35 +20,35 @@ function MultiplierSelector({ disabled={disabled} name={name} label={t('shared.generic.multiplier')} - tooltip={t('manage.sessionForms.liveQuizMultiplier')} - placeholder={t('manage.sessionForms.multiplierDefault')} + tooltip={t('manage.activityWizard.liveQuizMultiplier')} + placeholder={t('manage.activityWizard.multiplierDefault')} items={[ { - label: t('manage.sessionForms.multiplier1'), + label: t('manage.activityWizard.multiplier1'), value: '1', data: { - cy: `select-multiplier-${t('manage.sessionForms.multiplier1')}`, + cy: `select-multiplier-${t('manage.activityWizard.multiplier1')}`, }, }, { - label: t('manage.sessionForms.multiplier2'), + label: t('manage.activityWizard.multiplier2'), value: '2', data: { - cy: `select-multiplier-${t('manage.sessionForms.multiplier2')}`, + cy: `select-multiplier-${t('manage.activityWizard.multiplier2')}`, }, }, { - label: t('manage.sessionForms.multiplier3'), + label: t('manage.activityWizard.multiplier3'), value: '3', data: { - cy: `select-multiplier-${t('manage.sessionForms.multiplier3')}`, + cy: `select-multiplier-${t('manage.activityWizard.multiplier3')}`, }, }, { - label: t('manage.sessionForms.multiplier4'), + label: t('manage.activityWizard.multiplier4'), value: '4', data: { - cy: `select-multiplier-${t('manage.sessionForms.multiplier4')}`, + cy: `select-multiplier-${t('manage.activityWizard.multiplier4')}`, }, }, ]} diff --git a/apps/frontend-manage/src/components/activities/creation/PasteSelectionButton.tsx b/apps/frontend-manage/src/components/activities/creation/PasteSelectionButton.tsx new file mode 100644 index 0000000000..8681fa7d4b --- /dev/null +++ b/apps/frontend-manage/src/components/activities/creation/PasteSelectionButton.tsx @@ -0,0 +1,71 @@ +import { faBars } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Element } from '@klicker-uzh/graphql/dist/ops' +import { Button } from '@uzh-bf/design-system' +import { useTranslations } from 'next-intl' +import { ElementBlockFormValues, ElementStackFormValues } from './WizardLayout' + +interface BaseProps { + index: number + selection: Record + resetSelection: (() => void) | undefined +} + +interface StackProps extends BaseProps { + stack: ElementStackFormValues + replace: (index: number, value: ElementStackFormValues) => void +} + +interface BlockProps extends BaseProps { + stack: ElementBlockFormValues + replace: (index: number, value: ElementBlockFormValues) => void +} + +function PasteSelectionButton({ + index, + selection, + resetSelection, + stack, + replace, +}: StackProps | BlockProps) { + const t = useTranslations() + + return ( + + ) +} + +export default PasteSelectionButton diff --git a/apps/frontend-manage/src/components/sessions/creation/PropertyList.tsx b/apps/frontend-manage/src/components/activities/creation/PropertyList.tsx similarity index 100% rename from apps/frontend-manage/src/components/sessions/creation/PropertyList.tsx rename to apps/frontend-manage/src/components/activities/creation/PropertyList.tsx diff --git a/apps/frontend-manage/src/components/activities/creation/StackBlockCreation.tsx b/apps/frontend-manage/src/components/activities/creation/StackBlockCreation.tsx new file mode 100644 index 0000000000..63b7294388 --- /dev/null +++ b/apps/frontend-manage/src/components/activities/creation/StackBlockCreation.tsx @@ -0,0 +1,258 @@ +import { faCommentDots } from '@fortawesome/free-regular-svg-icons' +import { + faArrowLeft, + faArrowRight, + faCircleExclamation, + faTrash, + faWarning, +} from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Element, ElementType } from '@klicker-uzh/graphql/dist/ops' +import { Button, Tooltip } from '@uzh-bf/design-system' +import { useTranslations } from 'next-intl' +import { useState } from 'react' +import { useDrop } from 'react-dnd' +import { isEmpty } from 'remeda' +import { twMerge } from 'tailwind-merge' +import { QuestionDragDropTypes } from '../../questions/Question' +import DropElementsStack from './DropElementsStack' +import PasteSelectionButton from './PasteSelectionButton' +import StackCreationErrors from './StackCreationErrors' +import StackDescriptionModal from './StackDescriptionModal' +import WizardElementList from './WizardElementList' +import { ElementStackErrorValues, ElementStackFormValues } from './WizardLayout' + +interface StackBlockCreationProps { + stackIx: number + stack: ElementStackFormValues + acceptedTypes: ElementType[] + replace: (stackIx: number, value: ElementStackFormValues) => void + selection?: Record + resetSelection?: () => void + singleStackMode?: boolean + className?: string +} + +interface StackBlockCreationMultipleProps extends StackBlockCreationProps { + numOfStacks: number + remove: (stackIx: number) => void + move: (from: number, to: number) => void + highlightFTNoSL?: boolean + error?: ElementStackErrorValues[] +} + +interface StackBlockCreationSingleProps extends StackBlockCreationProps { + numOfStacks?: never + remove?: never + move?: never + highlightFTNoSL?: never + error?: ElementStackErrorValues +} + +function StackBlockCreation({ + stackIx, + stack, + numOfStacks = 1, + acceptedTypes, + remove, + move, + replace, + selection, + resetSelection, + error, + highlightFTNoSL = false, + singleStackMode = false, + className, +}: + | StackBlockCreationMultipleProps + | StackBlockCreationSingleProps): React.ReactElement { + const t = useTranslations() + const [stackDescriptionModal, setStackDescriptionModal] = useState(false) + + const [{ isOver }, drop] = useDrop( + () => ({ + accept: acceptedTypes, + drop: (item: QuestionDragDropTypes) => { + replace(stackIx, { + ...stack, + elements: [ + ...stack.elements, + { + id: item.id, + title: item.title, + type: item.questionType, + hasSampleSolution: item.hasSampleSolution, + }, + ], + }) + }, + collect: (monitor) => ({ + isOver: !!monitor.isOver(), + }), + }), + [] + ) + + const FTQuestionNoSLCount = highlightFTNoSL + ? stack.elements.filter( + (element) => + element.type === ElementType.FreeText && + !element.hasSampleSolution && + typeof element.hasSampleSolution !== 'undefined' + ).length + : 0 + + return ( +
    +
    +
    +
    + {singleStackMode + ? t('shared.generic.questions') + : t('shared.generic.stackN', { number: stackIx + 1 })} +
    + {highlightFTNoSL && FTQuestionNoSLCount > 0 && ( + + + + )} + {error && + !singleStackMode && + Array.isArray(error) && + error.length > stackIx && + typeof error[stackIx] !== 'undefined' && ( + } + delay={0} + className={{ tooltip: 'z-20 max-w-[30rem] text-sm' }} + > + + + )} + {error && !Array.isArray(error) && singleStackMode && ( + } + delay={0} + className={{ tooltip: 'z-20 max-w-[30rem] text-sm' }} + > + + + )} +
    +
    + {!singleStackMode && typeof move !== 'undefined' && ( + + )} + {!singleStackMode && typeof move !== 'undefined' && ( + + )} + {!singleStackMode && ( + + )} + {!singleStackMode && typeof remove !== 'undefined' && ( + + )} +
    +
    + + + + {selection && !isEmpty(selection) && ( + + )} + + + +
    + ) +} + +export default StackBlockCreation diff --git a/apps/frontend-manage/src/components/sessions/creation/StackCreationErrors.tsx b/apps/frontend-manage/src/components/activities/creation/StackCreationErrors.tsx similarity index 100% rename from apps/frontend-manage/src/components/sessions/creation/StackCreationErrors.tsx rename to apps/frontend-manage/src/components/activities/creation/StackCreationErrors.tsx diff --git a/apps/frontend-manage/src/components/sessions/creation/StackCreationStep.tsx b/apps/frontend-manage/src/components/activities/creation/StackCreationStep.tsx similarity index 95% rename from apps/frontend-manage/src/components/sessions/creation/StackCreationStep.tsx rename to apps/frontend-manage/src/components/activities/creation/StackCreationStep.tsx index 83d8136335..9ec9943700 100644 --- a/apps/frontend-manage/src/components/sessions/creation/StackCreationStep.tsx +++ b/apps/frontend-manage/src/components/activities/creation/StackCreationStep.tsx @@ -1,5 +1,5 @@ import { Element, ElementType } from '@klicker-uzh/graphql/dist/ops' -import { FieldArray, FieldArrayRenderProps, Form, Formik } from 'formik' +import { FieldArray, Form, Formik } from 'formik' import AddStackButton from './AddStackButton' import CreationFormValidator from './CreationFormValidator' import StackBlockCreation from './StackBlockCreation' @@ -56,13 +56,13 @@ function StackCreationStep({
    - {({ push, remove, move, replace }: FieldArrayRenderProps) => ( + {({ push, remove, move, replace }) => (
    {values.stacks.map( (stack: ElementStackFormValues, index: number) => ( e.id).join('-')}`} - index={index} + stackIx={index} stack={stack} numOfStacks={values.stacks.length} acceptedTypes={acceptedTypes} @@ -77,6 +77,7 @@ function StackCreationStep({ ) )} setModalOpen(false)} - title={t('manage.sessionForms.stackDescriptionTitle', { + title={t('manage.activityWizard.stackDescriptionTitle', { stackIx: stackIx + 1, })} className={{ @@ -28,16 +28,16 @@ function StackDescriptionModal({ > void diff --git a/apps/frontend-manage/src/components/activities/creation/WizardElementList.tsx b/apps/frontend-manage/src/components/activities/creation/WizardElementList.tsx new file mode 100644 index 0000000000..7aef3b46df --- /dev/null +++ b/apps/frontend-manage/src/components/activities/creation/WizardElementList.tsx @@ -0,0 +1,164 @@ +import { + faArrowDown, + faArrowUp, + faCircleExclamation, + faTrash, + faWarning, +} from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { ElementType } from '@klicker-uzh/graphql/dist/ops' +import { Ellipsis } from '@klicker-uzh/markdown' +import { Button } from '@uzh-bf/design-system' +import { swapIndices } from 'remeda' +import { + ElementBlockFormValues, + ElementStackErrorValues, + ElementStackFormValues, +} from './WizardLayout' + +interface BaseProps { + stackIx: number +} + +interface StackWizardElementListProps extends BaseProps { + stack: ElementStackFormValues + error: ElementStackErrorValues | ElementStackErrorValues[] | undefined + replace: (index: number, value: ElementStackFormValues) => void + highlightFTNoSL?: boolean +} + +interface BlockWizardElementListProps extends BaseProps { + stack: ElementBlockFormValues + error: ElementBlockFormValues | undefined + replace: (index: number, value: ElementBlockFormValues) => void + highlightFTNoSL?: never +} + +function WizardElementList({ + stackIx, + stack, + error, + replace, + highlightFTNoSL, +}: StackWizardElementListProps | BlockWizardElementListProps) { + return ( +
    + {stack.elements.map((element, elementIdx) => { + const errors = + error && Array.isArray(error) + ? error.length > stackIx + ? error[stackIx]?.elements + : undefined + : error?.elements + + return ( +
    +
    + + {element.title} + +
    +
    + {errors?.[elementIdx] && ( + + )} + {highlightFTNoSL && + element.type === ElementType.FreeText && + !element.hasSampleSolution && ( + + )} + + +
    + +
    + ) + })} +
    + ) +} + +export default WizardElementList diff --git a/apps/frontend-manage/src/components/sessions/creation/WizardLayout.tsx b/apps/frontend-manage/src/components/activities/creation/WizardLayout.tsx similarity index 88% rename from apps/frontend-manage/src/components/sessions/creation/WizardLayout.tsx rename to apps/frontend-manage/src/components/activities/creation/WizardLayout.tsx index 9ac6833d28..2133edbb87 100644 --- a/apps/frontend-manage/src/components/sessions/creation/WizardLayout.tsx +++ b/apps/frontend-manage/src/components/activities/creation/WizardLayout.tsx @@ -25,18 +25,26 @@ interface CommonFormValues { multiplier: string } -export interface LiveQuizBlockFormValues { - questionIds: number[] - titles: string[] - types: ElementType[] +export interface ElementBlockFormValues { timeLimit?: number + elements: { + id: number + title: string + type: ElementType + hasSampleSolution: boolean + }[] } -export interface LiveQuizBlockErrorValues { - questionIds?: string[] - titles?: string[] - types?: string[] +export interface ElememntBlockErrorValues { timeLimit?: string + elements?: + | string + | { + id: string + title: string + type: string + hasSampleSolution: string + }[] } export interface ElementStackFormValues { @@ -63,8 +71,8 @@ export interface ElementStackErrorValues { }[] } -export interface LiveSessionFormValues extends CommonFormValues { - blocks: LiveQuizBlockFormValues[] +export interface LiveQuizFormValues extends CommonFormValues { + blocks: ElementBlockFormValues[] isGamificationEnabled: boolean isConfusionFeedbackEnabled: boolean isLiveQAEnabled: boolean @@ -93,7 +101,7 @@ export interface GroupActivityFormValues extends CommonFormValues { } export type CreationFormValues = - | LiveSessionFormValues + | LiveQuizFormValues | MicroLearningFormValues | PracticeQuizFormValues | GroupActivityFormValues diff --git a/apps/frontend-manage/src/components/sessions/creation/WizardNavigation.tsx b/apps/frontend-manage/src/components/activities/creation/WizardNavigation.tsx similarity index 95% rename from apps/frontend-manage/src/components/sessions/creation/WizardNavigation.tsx rename to apps/frontend-manage/src/components/activities/creation/WizardNavigation.tsx index 585995bfe0..35ad2d1fd9 100644 --- a/apps/frontend-manage/src/components/sessions/creation/WizardNavigation.tsx +++ b/apps/frontend-manage/src/components/activities/creation/WizardNavigation.tsx @@ -37,7 +37,7 @@ function WizardNavigation({ {typeof onPrevStep !== 'undefined' && ( setOpen(false)} - title={t('manage.sessionForms.groupActivityAddClue')} + title={t('manage.activityWizard.groupActivityAddClue')} className={{ content: 'w-[40rem]' }} > (
    - {t('manage.sessionForms.groupActivityCluesDescription')} + {t('manage.activityWizard.groupActivityCluesDescription')}
    ) : null}
    - {t('manage.sessionForms.groupActivityIntroductionName')} + {t('manage.activityWizard.groupActivityIntroductionName')}
    ( ( ( e.id).join('-')}`} stack={values.stack} acceptedTypes={acceptedTypes} @@ -85,7 +85,7 @@ function GroupActivityStackClues({ label={t('shared.generic.clues')} labelType="small" tooltip={t( - 'manage.sessionForms.groupActivityCluesDescription' + 'manage.activityWizard.groupActivityCluesDescription' )} className={{ label: 'mt-0' }} /> diff --git a/apps/frontend-manage/src/components/sessions/creation/groupActivity/GroupActivityWizard.tsx b/apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivityWizard.tsx similarity index 87% rename from apps/frontend-manage/src/components/sessions/creation/groupActivity/GroupActivityWizard.tsx rename to apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivityWizard.tsx index f0df31364b..0fde467460 100644 --- a/apps/frontend-manage/src/components/sessions/creation/groupActivity/GroupActivityWizard.tsx +++ b/apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivityWizard.tsx @@ -16,8 +16,8 @@ import { useRouter } from 'next/router' import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react' import * as yup from 'yup' import ElementCreationErrorToast from '../../../toasts/ElementCreationErrorToast' +import { ElementSelectCourse } from '../../ElementCreation' import CompletionStep from '../CompletionStep' -import { ElementSelectCourse } from '../ElementCreation' import WizardLayout, { GroupActivityClueFormValues, GroupActivityFormValues, @@ -93,25 +93,25 @@ function GroupActivityWizard({ const nameValidationSchema = yup.object().shape({ name: yup .string() - .required(t('manage.sessionForms.groupActivityNameError')), + .required(t('manage.activityWizard.groupActivityNameError')), }) const descriptionValidationSchema = yup.object().shape({ displayName: yup .string() - .required(t('manage.sessionForms.groupActivityDisplayNameError')), + .required(t('manage.activityWizard.groupActivityDisplayNameError')), description: yup .string() - .required(t('manage.sessionForms.groupActivityDescriptionError')), + .required(t('manage.activityWizard.groupActivityDescriptionError')), }) const settingsValidationSchema = yup.object().shape({ startDate: yup .date() - .required(t('manage.sessionForms.groupActivityStartDate')) + .required(t('manage.activityWizard.groupActivityStartDate')) .test( 'afterCourseStart', - t('manage.sessionForms.groupActivityStartAfterCourseStart'), + t('manage.activityWizard.groupActivityStartAfterCourseStart'), (value, context) => context.parent.courseStartDate ? dayjs(value) > dayjs(context.parent.courseStartDate) @@ -119,7 +119,7 @@ function GroupActivityWizard({ ) .test( 'afterGroupDeadline', - t('manage.sessionForms.groupActivityStartAfterGroupDeadline'), + t('manage.activityWizard.groupActivityStartAfterGroupDeadline'), (value, context) => context.parent.courseGroupDeadline ? dayjs(value) > dayjs(context.parent.courseGroupDeadline) @@ -127,11 +127,11 @@ function GroupActivityWizard({ ), endDate: yup .date() - .required(t('manage.sessionForms.groupActivityEndDate')) - .min(yup.ref('startDate'), t('manage.sessionForms.endAfterStart')) + .required(t('manage.activityWizard.groupActivityEndDate')) + .min(yup.ref('startDate'), t('manage.activityWizard.endAfterStart')) .test( 'beforeCourseEnd', - t('manage.sessionForms.groupActivityEndBeforeCourseEnd'), + t('manage.activityWizard.groupActivityEndBeforeCourseEnd'), (value, context) => context.parent.courseEndDate ? dayjs(value) < dayjs(context.parent.courseEndDate) @@ -139,17 +139,17 @@ function GroupActivityWizard({ ), multiplier: yup .string() - .matches(/^[0-9]+$/, t('manage.sessionForms.validMultiplicator')), + .matches(/^[0-9]+$/, t('manage.activityWizard.validMultiplicator')), courseId: yup .string() - .required(t('manage.sessionForms.groupActivityCourse')), + .required(t('manage.activityWizard.groupActivityCourse')), }) const stackCluesValiationSchema = yup.object().shape({ stack: yup.object().shape({ elements: yup .array() - .min(1, t('manage.sessionForms.minOneQuestionGroupActivity')) + .min(1, t('manage.activityWizard.minOneQuestionGroupActivity')) .of( yup.object().shape({ id: yup.number(), @@ -158,7 +158,7 @@ function GroupActivityWizard({ .string() .oneOf( acceptedTypes, - t('manage.sessionForms.groupActivityTypes') + t('manage.activityWizard.groupActivityTypes') ), }) ), @@ -169,9 +169,9 @@ function GroupActivityWizard({ yup.object().shape({ name: yup .string() - .required(t('manage.sessionForms.clueNameMissing')) + .required(t('manage.activityWizard.clueNameMissing')) .test({ - message: t('manage.sessionForms.groupActivityCluesUniqueNames'), + message: t('manage.activityWizard.groupActivityCluesUniqueNames'), test: function (value) { const { from } = this const clues = from?.[1].value @@ -183,17 +183,17 @@ function GroupActivityWizard({ }), displayName: yup .string() - .required(t('manage.sessionForms.clueDisplayNameMissing')), + .required(t('manage.activityWizard.clueDisplayNameMissing')), type: yup .string() .oneOf([ParameterType.String, ParameterType.Number]), value: yup .string() - .required(t('manage.sessionForms.clueValueMissing')), + .required(t('manage.activityWizard.clueValueMissing')), unit: yup.string(), }) ) - .min(2, t('manage.sessionForms.groupActivityMin2Clues')), + .min(2, t('manage.activityWizard.groupActivityMin2Clues')), }) const formDefaultValues = { @@ -218,24 +218,24 @@ function GroupActivityWizard({ const workflowItems = [ { title: t('shared.generic.information'), - tooltip: t('manage.sessionForms.groupActivityInformation'), + tooltip: t('manage.activityWizard.groupActivityInformation'), completed: stepValidity[0], }, { title: t('shared.generic.description'), - tooltip: t('manage.sessionForms.groupActivityDescription'), + tooltip: t('manage.activityWizard.groupActivityDescription'), completed: stepValidity[1], }, { title: t('shared.generic.settings'), - tooltip: t('manage.sessionForms.groupActivitySettings'), - tooltipDisabled: t('manage.sessionForms.checkValues'), + tooltip: t('manage.activityWizard.groupActivitySettings'), + tooltipDisabled: t('manage.activityWizard.checkValues'), completed: stepValidity[2], }, { title: t('shared.generic.questions'), - tooltip: t('manage.sessionForms.groupActivityQuestions'), - tooltipDisabled: t('manage.sessionForms.checkValues'), + tooltip: t('manage.activityWizard.groupActivityQuestions'), + tooltipDisabled: t('manage.activityWizard.checkValues'), completed: stepValidity[3], }, ] @@ -319,11 +319,11 @@ function GroupActivityWizard({ completionSuccessMessage={(elementName) => (
    {editMode - ? t.rich('manage.sessionForms.groupActivityEdited', { + ? t.rich('manage.activityWizard.groupActivityEdited', { b: (text) => {text}, name: elementName, }) - : t.rich('manage.sessionForms.groupActivityCreated', { + : t.rich('manage.activityWizard.groupActivityCreated', { b: (text) => {text}, name: elementName, })} @@ -434,8 +434,8 @@ function GroupActivityWizard({ setOpen={setErrorToastOpen} error={ editMode - ? t('manage.sessionForms.groupActivityEditingFailed') - : t('manage.sessionForms.groupActivityCreationFailed') + ? t('manage.activityWizard.groupActivityEditingFailed') + : t('manage.activityWizard.groupActivityCreationFailed') } /> diff --git a/apps/frontend-manage/src/components/sessions/creation/groupActivity/submitGroupActivityForm.ts b/apps/frontend-manage/src/components/activities/creation/groupActivity/submitGroupActivityForm.ts similarity index 100% rename from apps/frontend-manage/src/components/sessions/creation/groupActivity/submitGroupActivityForm.ts rename to apps/frontend-manage/src/components/activities/creation/groupActivity/submitGroupActivityForm.ts diff --git a/apps/frontend-manage/src/components/sessions/creation/liveQuiz/AdvancedLiveQuizSettings.tsx b/apps/frontend-manage/src/components/activities/creation/liveQuiz/AdvancedLiveQuizSettings.tsx similarity index 83% rename from apps/frontend-manage/src/components/sessions/creation/liveQuiz/AdvancedLiveQuizSettings.tsx rename to apps/frontend-manage/src/components/activities/creation/liveQuiz/AdvancedLiveQuizSettings.tsx index 3d9dbcdd66..eb1eea8ceb 100644 --- a/apps/frontend-manage/src/components/sessions/creation/liveQuiz/AdvancedLiveQuizSettings.tsx +++ b/apps/frontend-manage/src/components/activities/creation/liveQuiz/AdvancedLiveQuizSettings.tsx @@ -45,7 +45,7 @@ function AdvancedLiveQuizSettings({ } - title={t('manage.sessionForms.liveQuizAdvancedSettings')} + title={t('manage.activityWizard.liveQuizAdvancedSettings')} className={{ content: '!w-full max-w-[60rem] !pb-2' }} dataCloseButton={{ cy: 'live-quiz-advanced-settings-close' }} > @@ -55,8 +55,8 @@ function AdvancedLiveQuizSettings({ required precision={0} name="maxBonusPoints" - label={t('manage.sessionForms.liveQuizMaxBonusPoints')} - tooltip={t('manage.sessionForms.liveQuizMaxBonusPointsTooltip', { + label={t('manage.activityWizard.liveQuizMaxBonusPoints')} + tooltip={t('manage.activityWizard.liveQuizMaxBonusPointsTooltip', { defaultValue: LQ_MAX_BONUS_POINTS, })} data={{ @@ -67,8 +67,8 @@ function AdvancedLiveQuizSettings({ required precision={0} name="timeToZeroBonus" - label={t('manage.sessionForms.liveQuizTimeToZeroBonus')} - tooltip={t('manage.sessionForms.liveQuizTimeToZeroBonusTooltip', { + label={t('manage.activityWizard.liveQuizTimeToZeroBonus')} + tooltip={t('manage.activityWizard.liveQuizTimeToZeroBonusTooltip', { defaultValue: LQ_TIME_TO_ZERO_BONUS, })} data={{ @@ -78,7 +78,7 @@ function AdvancedLiveQuizSettings({
    - {t('manage.sessionForms.liveQuizTotalPointsCorrect')} + {t('manage.activityWizard.liveQuizTotalPointsCorrect')}
    - {t('manage.sessionForms.liveQuizIntroductionName')} + {t('manage.activityWizard.liveQuizIntroductionName')}
    ( -
    - {text} - - ), - }), + richText: t.rich( + 'manage.activityWizard.liveQuizUseCase', + { + link: (text) => ( + + {text} + + ), + } + ), }, { icon: faBookOpen, iconColor: 'text-uzh-blue-100', richText: t.rich( - 'manage.sessionForms.liveQuizLecturerDocs', + 'manage.activityWizard.liveQuizLecturerDocs', { link: (text) => ( ( resetSelection: () => void } +// TODO: update accepted types in live quiz to include flashcards and content elements +const acceptedTypes = [ + ElementType.Sc, + ElementType.Mc, + ElementType.Kprim, + ElementType.Numerical, + ElementType.FreeText, + // ElementType.Flashcard, + // ElementType.Content, +] + function LiveQuizQuestionsStep({ editMode, formRef, @@ -45,28 +55,29 @@ function LiveQuizQuestionsStep({
    - {({ push, remove, move, replace }: FieldArrayRenderProps) => ( + {({ push, remove, move, replace }) => (
    - {values.blocks.map( - (block: LiveQuizBlockFormValues, index: number) => ( - - ) - )} - ( + e.id).join('-')}`} + blockIx={index} + block={block} + numOfBlocks={values.blocks.length} + acceptedTypes={acceptedTypes} + remove={remove} + move={move} + replace={replace} + selection={selection} + resetSelection={resetSelection} + error={errors.blocks as any} + /> + ))} +
    )} diff --git a/apps/frontend-manage/src/components/sessions/creation/liveQuiz/LiveQuizSettingsStep.tsx b/apps/frontend-manage/src/components/activities/creation/liveQuiz/LiveQuizSettingsStep.tsx similarity index 91% rename from apps/frontend-manage/src/components/sessions/creation/liveQuiz/LiveQuizSettingsStep.tsx rename to apps/frontend-manage/src/components/activities/creation/liveQuiz/LiveQuizSettingsStep.tsx index 39cdcf0136..24ef2a2562 100644 --- a/apps/frontend-manage/src/components/sessions/creation/liveQuiz/LiveQuizSettingsStep.tsx +++ b/apps/frontend-manage/src/components/activities/creation/liveQuiz/LiveQuizSettingsStep.tsx @@ -14,7 +14,7 @@ import MultiplierSelector from '../MultiplierSelector' import WizardNavigation from '../WizardNavigation' import AdvancedLiveQuizSettings from './AdvancedLiveQuizSettings' import LiveQuizCourseMonitor from './LiveQuizCourseMonitor' -import { LiveQuizWizardStepProps } from './LiveSessionWizard' +import { LiveQuizWizardStepProps } from './LiveQuizWizard' function LiveQuizSettingsStep({ editMode, @@ -87,8 +87,8 @@ function LiveQuizSettingsStep({
    diff --git a/apps/frontend-manage/src/components/sessions/creation/liveQuiz/LiveSessionWizard.tsx b/apps/frontend-manage/src/components/activities/creation/liveQuiz/LiveQuizWizard.tsx similarity index 66% rename from apps/frontend-manage/src/components/sessions/creation/liveQuiz/LiveSessionWizard.tsx rename to apps/frontend-manage/src/components/activities/creation/liveQuiz/LiveQuizWizard.tsx index e4fb66d48c..db49e6c2d2 100644 --- a/apps/frontend-manage/src/components/sessions/creation/liveQuiz/LiveSessionWizard.tsx +++ b/apps/frontend-manage/src/components/activities/creation/liveQuiz/LiveQuizWizard.tsx @@ -2,12 +2,13 @@ import { useMutation } from '@apollo/client' import { faPlay } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { - CreateSessionDocument, - EditSessionDocument, + CreateLiveQuizDocument, + EditLiveQuizDocument, Element, ElementType, - Session, - StartSessionDocument, + GetUserRunningLiveQuizzesDocument, + LiveQuiz, + StartLiveQuizDocument, } from '@klicker-uzh/graphql/dist/ops' import { LQ_MAX_BONUS_POINTS, @@ -22,43 +23,57 @@ import { useRouter } from 'next/router' import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react' import * as yup from 'yup' import ElementCreationErrorToast from '../../../toasts/ElementCreationErrorToast' +import { ElementSelectCourse } from '../../ElementCreation' import CompletionStep from '../CompletionStep' -import { ElementSelectCourse } from '../ElementCreation' -import WizardLayout, { LiveSessionFormValues } from '../WizardLayout' +import WizardLayout, { LiveQuizFormValues } from '../WizardLayout' import LiveQuizDescriptionStep from './LiveQuizDescriptionStep' import LiveQuizInformationStep from './LiveQuizInformationStep' import LiveQuizQuestionsStep from './LiveQuizQuestionsStep' import LiveQuizSettingsStep from './LiveQuizSettingsStep' -import submitLiveSessionForm from './submitLiveSessionForm' +import submitLiveQuizForm from './submitLiveQuizForm' export interface LiveQuizWizardStepProps { editMode: boolean formRef: any - formData: LiveSessionFormValues + formData: LiveQuizFormValues continueDisabled: boolean activeStep: number stepValidity: boolean[] validationSchema: any gamifiedCourses?: ElementSelectCourse[] nonGamifiedCourses?: ElementSelectCourse[] - onSubmit?: (newValues: LiveSessionFormValues) => void + onSubmit?: (newValues: LiveQuizFormValues) => void setStepValidity: Dispatch> - onNextStep?: (newValues: LiveSessionFormValues) => void - onPrevStep?: (newValues: LiveSessionFormValues) => void + onNextStep?: (newValues: LiveQuizFormValues) => void + onPrevStep?: (newValues: LiveQuizFormValues) => void closeWizard: () => void } -interface LiveSessionWizardProps { +interface LiveQuizWizardProps { title: string courses: ElementSelectCourse[] - initialValues?: Partial + initialValues?: Pick< + LiveQuiz, + | 'id' + | 'name' + | 'displayName' + | 'description' + | 'pointsMultiplier' + | 'maxBonusPoints' + | 'timeToZeroBonus' + | 'isConfusionFeedbackEnabled' + | 'isGamificationEnabled' + | 'isLiveQAEnabled' + | 'isModerationEnabled' + | 'blocks' + > & { course?: { id: string } | null } selection: Record resetSelection: () => void closeWizard: () => void editMode: boolean } -function LiveSessionWizard({ +function LiveQuizWizard({ title, courses, initialValues, @@ -66,7 +81,7 @@ function LiveSessionWizard({ resetSelection, closeWizard, editMode, -}: LiveSessionWizardProps) { +}: LiveQuizWizardProps) { const router = useRouter() const t = useTranslations() @@ -76,39 +91,39 @@ function LiveSessionWizard({ const [stepValidity, setStepValidity] = useState( Array(4).fill(!!initialValues) ) - const formRef = useRef>(null) + const formRef = useRef>(null) const { gamifiedCourses, nonGamifiedCourses } = useCoursesGamificationSplit({ courseSelection: courses, }) const nameValidationSchema = yup.object().shape({ - name: yup.string().required(t('manage.sessionForms.sessionName')), + name: yup.string().required(t('manage.activityWizard.activityName')), }) const descriptionValidationSchema = yup.object().shape({ displayName: yup .string() - .required(t('manage.sessionForms.sessionDisplayName')), + .required(t('manage.activityWizard.activityDisplayName')), description: yup.string(), }) const settingsValidationSchema = yup.object().shape({ multiplier: yup .string() - .matches(/^[0-9]+$/, t('manage.sessionForms.validMultiplicator')), + .matches(/^[0-9]+$/, t('manage.activityWizard.validMultiplicator')), courseId: yup.string(), isGamificationEnabled: yup .boolean() - .required(t('manage.sessionForms.liveQuizGamified')), + .required(t('manage.activityWizard.liveQuizGamified')), maxBonusPoints: yup .number() - .required(t('manage.sessionForms.liveQuizMaxBonusPointsReq')) - .min(0, t('manage.sessionForms.liveQuizMaxBonusPointsMin')), + .required(t('manage.activityWizard.liveQuizMaxBonusPointsReq')) + .min(0, t('manage.activityWizard.liveQuizMaxBonusPointsMin')), timeToZeroBonus: yup .number() - .required(t('manage.sessionForms.liveQuizTimeToZeroBonusReq')) - .min(1, t('manage.sessionForms.liveQuizTimeToZeroBonusMin')), + .required(t('manage.activityWizard.liveQuizTimeToZeroBonusReq')) + .min(1, t('manage.activityWizard.liveQuizTimeToZeroBonusMin')), }) const questionsValidationSchema = yup.object().shape({ @@ -129,12 +144,12 @@ function LiveSessionWizard({ ElementType.Numerical, ElementType.FreeText, ], - t('manage.sessionForms.liveQuizTypes') + t('manage.activityWizard.liveQuizTypes') ) ), timeLimit: yup .number() - .min(1, t('manage.sessionForms.liveQuizTimeRestriction')), + .min(1, t('manage.activityWizard.liveQuizTimeRestriction')), }) ), }) @@ -143,7 +158,7 @@ function LiveSessionWizard({ name: '', displayName: '', description: '', - blocks: [{ questionIds: [], titles: [], types: [], timeLimit: undefined }], + blocks: [{ timeLimit: undefined, elements: [] }], courseId: '', multiplier: '1', maxBonusPoints: LQ_MAX_BONUS_POINTS, @@ -157,49 +172,51 @@ function LiveSessionWizard({ const workflowItems = [ { title: t('shared.generic.information'), - tooltip: t('manage.sessionForms.liveQuizInformation'), + tooltip: t('manage.activityWizard.liveQuizInformation'), completed: stepValidity[0], }, { title: t('shared.generic.description'), - tooltip: t('manage.sessionForms.liveQuizDescription'), - tooltipDisabled: t('manage.sessionForms.liveQuizDescription'), + tooltip: t('manage.activityWizard.liveQuizDescription'), + tooltipDisabled: t('manage.activityWizard.liveQuizDescription'), completed: stepValidity[1], }, { title: t('shared.generic.settings'), - tooltip: t('manage.sessionForms.liveQuizSettings'), - tooltipDisabled: t('manage.sessionForms.checkValues'), + tooltip: t('manage.activityWizard.liveQuizSettings'), + tooltipDisabled: t('manage.activityWizard.checkValues'), completed: stepValidity[2], }, { - title: t('manage.sessionForms.liveQuizBlocks'), - tooltip: t('manage.sessionForms.liveQuizDragDrop'), - tooltipDisabled: t('manage.sessionForms.checkValues'), + title: t('manage.activityWizard.liveQuizBlocks'), + tooltip: t('manage.activityWizard.liveQuizDragDrop'), + tooltipDisabled: t('manage.activityWizard.checkValues'), completed: stepValidity[3], }, ] - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ name: initialValues?.name || formDefaultValues.name, displayName: initialValues?.displayName || formDefaultValues.displayName, description: initialValues?.description || formDefaultValues.description, - blocks: - initialValues?.blocks?.map((block) => { - return { - questionIds: - block.instances?.map( - (instance) => instance.questionData!.questionId! - ) ?? [], - titles: - block.instances?.map((instance) => instance.questionData!.name!) ?? - [], - types: - block.instances?.map((instance) => instance.questionData!.type) ?? - [], - timeLimit: block.timeLimit ?? undefined, - } - }) || formDefaultValues.blocks, + blocks: initialValues?.blocks + ? initialValues.blocks.map((block) => { + return { + timeLimit: block.timeLimit ?? undefined, + elements: block.elements!.map((element) => { + return { + id: parseInt(element.elementData.id), + title: element.elementData.name, + type: element.elementData.type, + hasSampleSolution: + 'options' in element.elementData + ? (element.elementData.options.hasSampleSolution ?? false) + : true, + } + }), + } + }) + : formDefaultValues.blocks, courseId: initialValues?.course?.id || formDefaultValues.courseId, multiplier: initialValues?.pointsMultiplier ? String(initialValues?.pointsMultiplier) @@ -221,22 +238,43 @@ function LiveSessionWizard({ formDefaultValues.isModerationEnabled, }) - const [editSession] = useMutation(EditSessionDocument) - const [createSession, { data }] = useMutation(CreateSessionDocument) - const [startSession] = useMutation(StartSessionDocument) + const [editLiveQuiz] = useMutation(EditLiveQuizDocument) + const [createLiveQuiz, { data }] = useMutation(CreateLiveQuizDocument) + const [startLiveQuiz] = useMutation(StartLiveQuizDocument, { + update(cache, res) { + const data = cache.readQuery({ + query: GetUserRunningLiveQuizzesDocument, + }) + cache.writeQuery({ + query: GetUserRunningLiveQuizzesDocument, + data: { + userRunningLiveQuizzes: res.data?.startLiveQuiz + ? [ + ...(data?.userRunningLiveQuizzes ?? []), + { + id: res.data?.startLiveQuiz?.id, + name: res.data.startLiveQuiz.name ?? '', + }, + ] + : (data?.userRunningLiveQuizzes ?? []), + }, + }) + }, + }) + const handleSubmit = useCallback( - async (values: LiveSessionFormValues) => { - submitLiveSessionForm({ + async (values: LiveQuizFormValues) => { + submitLiveQuizForm({ id: initialValues?.id, editMode, values, - createLiveSession: createSession, - editLiveSession: editSession, + createLiveQuiz, + editLiveQuiz, setIsWizardCompleted, setErrorToastOpen, }) }, - [createSession, editMode, editSession, initialValues?.id] + [createLiveQuiz, editMode, editLiveQuiz, initialValues?.id] ) return ( @@ -254,11 +292,11 @@ function LiveSessionWizard({ completionSuccessMessage={(elementName) => (
    {editMode - ? t.rich('manage.sessionForms.liveQuizUpdated', { + ? t.rich('manage.activityWizard.liveQuizUpdated', { b: (text) => {text}, name: elementName, }) - : t.rich('manage.sessionForms.liveQuizCreated', { + : t.rich('manage.activityWizard.liveQuizCreated', { b: (text) => {text}, name: elementName, })} @@ -275,16 +313,16 @@ function LiveSessionWizard({ setStepNumber={setActiveStep} onCloseWizard={closeWizard} > - {!editMode && data?.createSession?.id && ( + {!editMode && data?.createLiveQuiz?.id ? ( - )} + ) : null} } steps={[ @@ -309,7 +347,7 @@ function LiveSessionWizard({ stepValidity={stepValidity} validationSchema={nameValidationSchema} setStepValidity={setStepValidity} - onNextStep={(newValues: Partial) => { + onNextStep={(newValues: Partial) => { setFormData((prev) => ({ ...prev, ...newValues })) setActiveStep((currentStep) => currentStep + 1) }} @@ -325,11 +363,11 @@ function LiveSessionWizard({ stepValidity={stepValidity} validationSchema={descriptionValidationSchema} setStepValidity={setStepValidity} - onNextStep={(newValues: Partial) => { + onNextStep={(newValues: Partial) => { setFormData((prev) => ({ ...prev, ...newValues })) setActiveStep((currentStep) => currentStep + 1) }} - onPrevStep={(newValues: Partial) => { + onPrevStep={(newValues: Partial) => { setFormData((prev) => ({ ...prev, ...newValues })) setActiveStep((currentStep) => currentStep - 1) }} @@ -347,11 +385,11 @@ function LiveSessionWizard({ gamifiedCourses={gamifiedCourses} nonGamifiedCourses={nonGamifiedCourses} setStepValidity={setStepValidity} - onNextStep={(newValues: Partial) => { + onNextStep={(newValues: Partial) => { setFormData((prev) => ({ ...prev, ...newValues })) setActiveStep((currentStep) => currentStep + 1) }} - onPrevStep={(newValues: Partial) => { + onPrevStep={(newValues: Partial) => { setFormData((prev) => ({ ...prev, ...newValues })) setActiveStep((currentStep) => currentStep - 1) }} @@ -369,10 +407,10 @@ function LiveSessionWizard({ stepValidity={stepValidity} validationSchema={questionsValidationSchema} setStepValidity={setStepValidity} - onSubmit={(newValues: LiveSessionFormValues) => + onSubmit={(newValues: LiveQuizFormValues) => handleSubmit({ ...formData, ...newValues }) } - onPrevStep={(newValues: Partial) => { + onPrevStep={(newValues: Partial) => { setFormData((prev) => ({ ...prev, ...newValues })) setActiveStep((currentStep) => currentStep - 1) }} @@ -388,12 +426,12 @@ function LiveSessionWizard({ setOpen={setErrorToastOpen} error={ editMode - ? t('manage.sessionForms.liveQuizEditingFailed') - : t('manage.sessionForms.liveQuizCreationFailed') + ? t('manage.activityWizard.liveQuizEditingFailed') + : t('manage.activityWizard.liveQuizCreationFailed') } /> ) } -export default LiveSessionWizard +export default LiveQuizWizard diff --git a/apps/frontend-manage/src/components/sessions/creation/liveQuiz/submitLiveSessionForm.tsx b/apps/frontend-manage/src/components/activities/creation/liveQuiz/submitLiveQuizForm.tsx similarity index 73% rename from apps/frontend-manage/src/components/sessions/creation/liveQuiz/submitLiveSessionForm.tsx rename to apps/frontend-manage/src/components/activities/creation/liveQuiz/submitLiveQuizForm.tsx index 40155ac51e..9eecbd46b4 100644 --- a/apps/frontend-manage/src/components/sessions/creation/liveQuiz/submitLiveSessionForm.tsx +++ b/apps/frontend-manage/src/components/activities/creation/liveQuiz/submitLiveQuizForm.tsx @@ -1,48 +1,51 @@ import { GetSingleCourseDocument, - GetUserSessionsDocument, + GetUserLiveQuizzesDocument, } from '@klicker-uzh/graphql/dist/ops' -import { LiveSessionFormValues } from '../WizardLayout' +import { ElementBlockFormValues, LiveQuizFormValues } from '../WizardLayout' -interface LiveSessionFormProps { +interface LiveQuizFormProps { id?: string editMode: boolean - values: LiveSessionFormValues - createLiveSession: any - editLiveSession: any + values: LiveQuizFormValues + createLiveQuiz: any + editLiveQuiz: any setIsWizardCompleted: (isCompleted: boolean) => void setErrorToastOpen: (isOpen: boolean) => void } -async function submitLiveSessionForm({ +async function submitLiveQuizForm({ id, editMode, values, - createLiveSession, - editLiveSession, + createLiveQuiz, + editLiveQuiz, setIsWizardCompleted, setErrorToastOpen, -}: LiveSessionFormProps) { - const blockQuestions = values.blocks - .filter((block) => block.questionIds.length > 0) - .map((block) => { +}: LiveQuizFormProps) { + const blockSubmission = values.blocks.map( + (block: ElementBlockFormValues, ix) => { return { - questionIds: block.questionIds, + order: ix, timeLimit: block.timeLimit, + elements: block.elements.map((element, ix) => { + return { elementId: element.id, order: ix } + }), } - }) + } + ) try { let success = false if (editMode && id) { - const session = await editLiveSession({ + const liveQuiz = await editLiveQuiz({ variables: { id: id, name: values.name, displayName: values.displayName, description: values.description, - blocks: blockQuestions, + blocks: blockSubmission, courseId: values.courseId, multiplier: values.courseId !== '' ? parseInt(values.multiplier) : 1, maxBonusPoints: parseInt(String(values.maxBonusPoints)), @@ -55,7 +58,7 @@ async function submitLiveSessionForm({ }, refetchQueries: [ { - query: GetUserSessionsDocument, + query: GetUserLiveQuizzesDocument, }, ...(values.courseId ? [ @@ -69,14 +72,14 @@ async function submitLiveSessionForm({ : []), ], }) - success = Boolean(session.data?.editSession) + success = Boolean(liveQuiz.data?.editLiveQuiz) } else { - const session = await createLiveSession({ + const liveQuiz = await createLiveQuiz({ variables: { name: values.name, displayName: values.displayName, description: values.description, - blocks: blockQuestions, + blocks: blockSubmission, courseId: values.courseId, multiplier: parseInt(values.multiplier), maxBonusPoints: parseInt(String(values.maxBonusPoints)), @@ -89,7 +92,7 @@ async function submitLiveSessionForm({ }, refetchQueries: [ { - query: GetUserSessionsDocument, + query: GetUserLiveQuizzesDocument, }, ...(values.courseId ? [ @@ -103,7 +106,7 @@ async function submitLiveSessionForm({ : []), ], }) - success = Boolean(session.data?.createSession) + success = Boolean(liveQuiz.data?.createLiveQuiz) } if (success) { @@ -115,4 +118,4 @@ async function submitLiveSessionForm({ } } -export default submitLiveSessionForm +export default submitLiveQuizForm diff --git a/apps/frontend-manage/src/components/sessions/creation/microLearning/MicroLearningDescriptionStep.tsx b/apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningDescriptionStep.tsx similarity index 87% rename from apps/frontend-manage/src/components/sessions/creation/microLearning/MicroLearningDescriptionStep.tsx rename to apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningDescriptionStep.tsx index c10518e101..244814f2bd 100644 --- a/apps/frontend-manage/src/components/sessions/creation/microLearning/MicroLearningDescriptionStep.tsx +++ b/apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningDescriptionStep.tsx @@ -19,8 +19,8 @@ function MicroLearningDescriptionStep({ return ( ) : (
    - {t('manage.sessionForms.microLearningIntroductionName')} + {t('manage.activityWizard.microLearningIntroductionName')}
    )} (
    ( ( context.parent.courseStartDate ? dayjs(value) > dayjs(context.parent.courseStartDate) @@ -113,16 +113,20 @@ function MicroLearningWizard({ ), endDate: yup .date() - .required(t('manage.sessionForms.endDate')) - .test('checkDateInPast', t('manage.sessionForms.endInFuture'), (date) => { - return !!(date && date > new Date()) - }) + .required(t('manage.activityWizard.endDate')) + .test( + 'checkDateInPast', + t('manage.activityWizard.endInFuture'), + (date) => { + return !!(date && date > new Date()) + } + ) .when('startDate', (startDate, schema) => - schema.min(startDate, t('manage.sessionForms.endAfterStart')) + schema.min(startDate, t('manage.activityWizard.endAfterStart')) ) .test( 'beforeCourseEnd', - t('manage.sessionForms.microlearningEndBeforeCourseEnd'), + t('manage.activityWizard.microlearningEndBeforeCourseEnd'), (value, context) => context.parent.courseEndDate ? dayjs(value) < dayjs(context.parent.courseEndDate) @@ -130,10 +134,10 @@ function MicroLearningWizard({ ), multiplier: yup .string() - .matches(/^[0-9]+$/, t('manage.sessionForms.validMultiplicator')), + .matches(/^[0-9]+$/, t('manage.activityWizard.validMultiplicator')), courseId: yup .string() - .required(t('manage.sessionForms.microlearningCourse')), + .required(t('manage.activityWizard.microlearningCourse')), }) const stackValiationSchema = yup.object().shape({ @@ -145,7 +149,7 @@ function MicroLearningWizard({ description: yup.string(), elements: yup .array() - .min(1, t('manage.sessionForms.minOneElementPerStack')) + .min(1, t('manage.activityWizard.minOneElementPerStack')) .of( yup.object().shape({ id: yup.number(), @@ -154,12 +158,14 @@ function MicroLearningWizard({ .string() .oneOf( acceptedTypes, - t('manage.sessionForms.microlearningTypes') + t('manage.activityWizard.microlearningTypes') ), hasSampleSolution: yup.boolean().when('type', { is: (type: ElementType) => type !== ElementType.FreeText, then: (schema) => - schema.isTrue(t('manage.sessionForms.elementSolutionReq')), + schema.isTrue( + t('manage.activityWizard.elementSolutionReq') + ), }), }) ), @@ -190,25 +196,25 @@ function MicroLearningWizard({ const workflowItems = [ { title: t('shared.generic.information'), - tooltip: t('manage.sessionForms.microLearningInformation'), + tooltip: t('manage.activityWizard.microLearningInformation'), completed: stepValidity[0], }, { title: t('shared.generic.description'), - tooltip: t('manage.sessionForms.microlearningDescription'), - tooltipDisabled: t('manage.sessionForms.microlearningDescription'), + tooltip: t('manage.activityWizard.microlearningDescription'), + tooltipDisabled: t('manage.activityWizard.microlearningDescription'), completed: stepValidity[1], }, { title: t('shared.generic.settings'), - tooltip: t('manage.sessionForms.microlearningSettings'), - tooltipDisabled: t('manage.sessionForms.checkValues'), + tooltip: t('manage.activityWizard.microlearningSettings'), + tooltipDisabled: t('manage.activityWizard.checkValues'), completed: stepValidity[2], }, { title: t('shared.generic.questions'), - tooltip: t('manage.sessionForms.microlearningQuestions'), - tooltipDisabled: t('manage.sessionForms.checkValues'), + tooltip: t('manage.activityWizard.microlearningQuestions'), + tooltipDisabled: t('manage.activityWizard.checkValues'), completed: stepValidity[3], }, ] @@ -289,11 +295,11 @@ function MicroLearningWizard({ completionSuccessMessage={(elementName) => (
    {editMode - ? t.rich('manage.sessionForms.microlearningCreated', { + ? t.rich('manage.activityWizard.microlearningCreated', { b: (text) => {text}, name: elementName, }) - : t.rich('manage.sessionForms.microlearningEdited', { + : t.rich('manage.activityWizard.microlearningEdited', { b: (text) => {text}, name: elementName, })} @@ -407,8 +413,8 @@ function MicroLearningWizard({ setOpen={setErrorToastOpen} error={ editMode - ? t('manage.sessionForms.microlearningEditingFailed') - : t('manage.sessionForms.microlearningCreationFailed') + ? t('manage.activityWizard.microlearningEditingFailed') + : t('manage.activityWizard.microlearningCreationFailed') } /> diff --git a/apps/frontend-manage/src/components/sessions/creation/microLearning/submitMicrolearningForm.ts b/apps/frontend-manage/src/components/activities/creation/microLearning/submitMicrolearningForm.ts similarity index 100% rename from apps/frontend-manage/src/components/sessions/creation/microLearning/submitMicrolearningForm.ts rename to apps/frontend-manage/src/components/activities/creation/microLearning/submitMicrolearningForm.ts diff --git a/apps/frontend-manage/src/components/sessions/creation/practiceQuiz/PracticeQuizDescriptionStep.tsx b/apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizDescriptionStep.tsx similarity index 87% rename from apps/frontend-manage/src/components/sessions/creation/practiceQuiz/PracticeQuizDescriptionStep.tsx rename to apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizDescriptionStep.tsx index febaed99d2..117736855d 100644 --- a/apps/frontend-manage/src/components/sessions/creation/practiceQuiz/PracticeQuizDescriptionStep.tsx +++ b/apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizDescriptionStep.tsx @@ -19,8 +19,8 @@ function PracticeQuizDescriptionStep({ return ( ) : null}
    - {t('manage.sessionForms.practiceQuizIntroductionName')} + {t('manage.activityWizard.practiceQuizIntroductionName')}
    (
    ( ( @@ -98,7 +100,7 @@ function PracticeQuizSettingsStep({ ) : ( { return { value: order, - label: t(`manage.sessionForms.practiceQuiz${order}`), + label: t(`manage.activityWizard.practiceQuiz${order}`), data: { cy: `select-order-${t( - `manage.sessionForms.practiceQuiz${order}` + `manage.activityWizard.practiceQuiz${order}` )}`, }, } diff --git a/apps/frontend-manage/src/components/sessions/creation/practiceQuiz/PracticeQuizWizard.tsx b/apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizWizard.tsx similarity index 89% rename from apps/frontend-manage/src/components/sessions/creation/practiceQuiz/PracticeQuizWizard.tsx rename to apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizWizard.tsx index 783bf5837f..324bdf4b96 100644 --- a/apps/frontend-manage/src/components/sessions/creation/practiceQuiz/PracticeQuizWizard.tsx +++ b/apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizWizard.tsx @@ -16,8 +16,8 @@ import { useRouter } from 'next/router' import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react' import * as yup from 'yup' import ElementCreationErrorToast from '../../../toasts/ElementCreationErrorToast' +import { ElementSelectCourse } from '../../ElementCreation' import CompletionStep from '../CompletionStep' -import { ElementSelectCourse } from '../ElementCreation' import StackCreationStep from '../StackCreationStep' import WizardLayout, { PracticeQuizFormValues } from '../WizardLayout' import PracticeQuizDescriptionStep from './PracticeQuizDescriptionStep' @@ -101,34 +101,37 @@ function PracticeQuizWizard({ }) const nameValidationSchema = yup.object().shape({ - name: yup.string().required(t('manage.sessionForms.sessionName')), + name: yup.string().required(t('manage.activityWizard.activityName')), }) const descriptionValidationSchema = yup.object().shape({ displayName: yup .string() - .required(t('manage.sessionForms.sessionDisplayName')), + .required(t('manage.activityWizard.activityDisplayName')), description: yup.string(), }) const settingsValidationSchema = yup.object().shape({ multiplier: yup .string() - .matches(/^[0-9]+$/, t('manage.sessionForms.validMultiplicator')), + .matches(/^[0-9]+$/, t('manage.activityWizard.validMultiplicator')), courseId: yup .string() - .required(t('manage.sessionForms.practiceQuizSelectCourse')), + .required(t('manage.activityWizard.practiceQuizSelectCourse')), order: yup .string() .required() .oneOf( Object.values(ElementOrderType), - t('manage.sessionForms.practiceQuizOrder') + t('manage.activityWizard.practiceQuizOrder') ), resetTimeDays: yup .string() - .required(t('manage.sessionForms.practiceQuizResetDays')) - .matches(/^[0-9]+$/, t('manage.sessionForms.practiceQuizValidResetDays')), + .required(t('manage.activityWizard.practiceQuizResetDays')) + .matches( + /^[0-9]+$/, + t('manage.activityWizard.practiceQuizValidResetDays') + ), }) const stackValiationSchema = yup.object().shape({ @@ -140,7 +143,7 @@ function PracticeQuizWizard({ description: yup.string(), elements: yup .array() - .min(1, t('manage.sessionForms.minOneElementPerStack')) + .min(1, t('manage.activityWizard.minOneElementPerStack')) .of( yup.object().shape({ id: yup.number(), @@ -149,12 +152,14 @@ function PracticeQuizWizard({ .string() .oneOf( acceptedTypes, - t('manage.sessionForms.practiceQuizTypes') + t('manage.activityWizard.practiceQuizTypes') ), hasSampleSolution: yup.boolean().when('type', { is: (type: ElementType) => type !== ElementType.FreeText, then: (schema) => - schema.isTrue(t('manage.sessionForms.elementSolutionReq')), + schema.isTrue( + t('manage.activityWizard.elementSolutionReq') + ), }), }) ), @@ -184,24 +189,24 @@ function PracticeQuizWizard({ const workflowItems = [ { title: t('shared.generic.information'), - tooltip: t('manage.sessionForms.practiceQuizInformation'), + tooltip: t('manage.activityWizard.practiceQuizInformation'), completed: stepValidity[0], }, { title: t('shared.generic.description'), - tooltip: t('manage.sessionForms.practiceQuizDescription'), + tooltip: t('manage.activityWizard.practiceQuizDescription'), completed: stepValidity[1], }, { title: t('shared.generic.settings'), - tooltip: t('manage.sessionForms.practiceQuizSettings'), - tooltipDisabled: t('manage.sessionForms.checkValues'), + tooltip: t('manage.activityWizard.practiceQuizSettings'), + tooltipDisabled: t('manage.activityWizard.checkValues'), completed: stepValidity[2], }, { title: t('shared.generic.questions'), - tooltip: t('manage.sessionForms.practiceQuizContent'), - tooltipDisabled: t('manage.sessionForms.checkValues'), + tooltip: t('manage.activityWizard.practiceQuizContent'), + tooltipDisabled: t('manage.activityWizard.checkValues'), completed: stepValidity[3], }, ] @@ -278,11 +283,11 @@ function PracticeQuizWizard({ completionSuccessMessage={(elementName) => (
    {editMode - ? t.rich('manage.sessionForms.practiceQuizUpdated', { + ? t.rich('manage.activityWizard.practiceQuizUpdated', { b: (text) => {text}, name: elementName, }) - : t.rich('manage.sessionForms.practiceQuizCreated', { + : t.rich('manage.activityWizard.practiceQuizCreated', { b: (text) => {text}, name: elementName, })} @@ -396,8 +401,8 @@ function PracticeQuizWizard({ setOpen={setErrorToastOpen} error={ editMode - ? t('manage.sessionForms.practiceQuizEditingFailed') - : t('manage.sessionForms.practiceQuizCreationFailed') + ? t('manage.activityWizard.practiceQuizEditingFailed') + : t('manage.activityWizard.practiceQuizCreationFailed') } /> diff --git a/apps/frontend-manage/src/components/sessions/creation/practiceQuiz/submitPracticeQuizForm.ts b/apps/frontend-manage/src/components/activities/creation/practiceQuiz/submitPracticeQuizForm.ts similarity index 100% rename from apps/frontend-manage/src/components/sessions/creation/practiceQuiz/submitPracticeQuizForm.ts rename to apps/frontend-manage/src/components/activities/creation/practiceQuiz/submitPracticeQuizForm.ts diff --git a/apps/frontend-manage/src/components/common/ContentInput.tsx b/apps/frontend-manage/src/components/common/ContentInput.tsx index aac51b362a..64905da173 100644 --- a/apps/frontend-manage/src/components/common/ContentInput.tsx +++ b/apps/frontend-manage/src/components/common/ContentInput.tsx @@ -547,7 +547,7 @@ const MarkButton = ({ ) } -export const SlateButton = React.forwardRef< +const SlateButton = React.forwardRef< HTMLSpanElement, PropsWithChildren<{ active: boolean diff --git a/apps/frontend-manage/src/components/common/Header.tsx b/apps/frontend-manage/src/components/common/Header.tsx index de97d1c1ff..923667fd52 100644 --- a/apps/frontend-manage/src/components/common/Header.tsx +++ b/apps/frontend-manage/src/components/common/Header.tsx @@ -6,7 +6,7 @@ import { } from '@fortawesome/free-regular-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { - GetUserRunningSessionsDocument, + GetUserRunningLiveQuizzesDocument, User, } from '@klicker-uzh/graphql/dist/ops' import { Navigation } from '@uzh-bf/design-system' @@ -25,7 +25,8 @@ function Header({ user }: HeaderProps): React.ReactElement { const t = useTranslations() const [showSupportModal, setShowSupportModal] = useState(false) - const { data } = useQuery(GetUserRunningSessionsDocument) + const { data } = useQuery(GetUserRunningLiveQuizzesDocument) + const quizzes = data?.userRunningLiveQuizzes const navigationItems = [ { @@ -36,9 +37,9 @@ function Header({ user }: HeaderProps): React.ReactElement { }, { href: '/sessions', - label: t('manage.general.sessions'), + label: t('manage.general.liveQuizzes'), active: router.pathname == '/sessions', - cy: 'sessions', + cy: 'live-quizzes', }, { href: '/courses', @@ -87,23 +88,20 @@ function Header({ user }: HeaderProps): React.ReactElement { root: 'group h-10 w-2', icon: twMerge( 'text-uzh-grey-80', - data?.userRunningSessions?.length !== 0 && 'text-green-600' + quizzes?.length !== 0 && 'text-green-600' ), disabled: '!text-gray-400', dropdown: 'gap-0 p-1.5', }} - disabled={data?.userRunningSessions?.length === 0} + disabled={quizzes?.length === 0} > - {data?.userRunningSessions && - data?.userRunningSessions.length > 0 ? ( - data?.userRunningSessions.map((session) => { + {quizzes && quizzes.length > 0 ? ( + quizzes.map((quiz) => { return ( - router.push(`/sessions/${session.id}/cockpit`) - } + key={quiz.id} + title={quiz.name} + onClick={() => router.push(`/sessions/${quiz.id}/cockpit`)} className={{ title: 'text-base font-bold', root: 'p-2' }} /> ) diff --git a/apps/frontend-manage/src/components/courses/CourseGamificationInfos.tsx b/apps/frontend-manage/src/components/courses/CourseGamificationInfos.tsx index 3e27169e21..8533da2cb8 100644 --- a/apps/frontend-manage/src/components/courses/CourseGamificationInfos.tsx +++ b/apps/frontend-manage/src/components/courses/CourseGamificationInfos.tsx @@ -9,7 +9,7 @@ import IndividualLeaderboard, { } from './IndividualLeaderboard' interface CourseGamificationInfosProps { - course: Omit & { + course: Omit & { leaderboard?: InvididualLeaderboardEntry[] | null } tabValue: string diff --git a/apps/frontend-manage/src/components/courses/CourseOverviewHeader.tsx b/apps/frontend-manage/src/components/courses/CourseOverviewHeader.tsx index 37c92ce92a..6f93476247 100644 --- a/apps/frontend-manage/src/components/courses/CourseOverviewHeader.tsx +++ b/apps/frontend-manage/src/components/courses/CourseOverviewHeader.tsx @@ -12,7 +12,7 @@ import { Button, Dropdown, H1, Toast } from '@uzh-bf/design-system' import dayjs from 'dayjs' import { useTranslations } from 'next-intl' import { useState } from 'react' -import CourseQRModal from '../sessions/cockpit/CourseQRModal' +import CourseQRModal from '../liveQuiz/cockpit/CourseQRModal' import { getLTIAccessLink } from './PracticeQuizElement' import CourseManipulationModal, { CourseManipulationFormData, @@ -21,7 +21,7 @@ import CourseManipulationModal, { interface CourseOverviewHeaderProps { course: Omit< Course, - 'leaderboard' | 'sessions' | 'practiceQuizzes' | 'microLearnings' + 'leaderboard' | 'liveQuizzes' | 'practiceQuizzes' | 'microLearnings' > name: string pinCode: number diff --git a/apps/frontend-manage/src/components/courses/GroupActivityElement.tsx b/apps/frontend-manage/src/components/courses/GroupActivityElement.tsx index fe53c50b27..cd64f2543c 100644 --- a/apps/frontend-manage/src/components/courses/GroupActivityElement.tsx +++ b/apps/frontend-manage/src/components/courses/GroupActivityElement.tsx @@ -10,17 +10,14 @@ import { faPlay, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { - GroupActivity, - GroupActivityStatus, -} from '@klicker-uzh/graphql/dist/ops' +import { GroupActivity, PublicationStatus } from '@klicker-uzh/graphql/dist/ops' import { Ellipsis } from '@klicker-uzh/markdown' import { Dropdown } from '@uzh-bf/design-system' import dayjs from 'dayjs' import { useTranslations } from 'next-intl' import { useRouter } from 'next/router' import React, { useState } from 'react' -import { WizardMode } from '../sessions/creation/ElementCreation' +import { WizardMode } from '../activities/ElementCreation' import StatusTag from './StatusTag' import PublishGroupActivityButton from './actions/PublishGroupActivityButton' import GroupActivityExtensionButton from './groupActivity/GroupActivityExtensionButton' @@ -52,62 +49,61 @@ function GroupActivityElement({ const [endingModal, setEndingModal] = useState(false) const [startingModal, setStartingModal] = useState(false) - const statusMap: Record = { - [GroupActivityStatus.Draft]: ( + const statusMap: Record = { + [PublicationStatus.Draft]: ( ), - [GroupActivityStatus.Scheduled]: ( + [PublicationStatus.Scheduled]: ( ), - [GroupActivityStatus.Published]: ( + [PublicationStatus.Published]: ( ), - [GroupActivityStatus.Ended]: ( + [PublicationStatus.Ended]: ( ), - - [GroupActivityStatus.Graded]: ( + [PublicationStatus.Graded]: (
    - {groupActivity.status === GroupActivityStatus.Draft && ( + {groupActivity.status === PublicationStatus.Draft && ( <> )} - {groupActivity.status === GroupActivityStatus.Scheduled && ( + {groupActivity.status === PublicationStatus.Scheduled && ( <> )} - {groupActivity.status === GroupActivityStatus.Published && ( + {groupActivity.status === PublicationStatus.Published && ( <> )} - {groupActivity.status === GroupActivityStatus.Ended && ( + {groupActivity.status === PublicationStatus.Ended && ( <> )} - {groupActivity.status === GroupActivityStatus.Graded && ( + {groupActivity.status === PublicationStatus.Graded && ( <> -
    - {statusMap[groupActivity.status ?? GroupActivityStatus.Draft]} -
    +
    {statusMap[groupActivity.status ?? PublicationStatus.Draft]}
    , - SCHEDULED: , - RUNNING: , - COMPLETED: , -} - -interface LiveQuizElementProps { - session: Pick< - Session, - | 'id' - | 'status' - | 'name' - | 'numOfBlocks' - | 'numOfQuestions' - | 'isGamificationEnabled' - | 'accessMode' - > -} +export type LiveQuizListElementType = Pick< + LiveQuiz, + | 'id' + | 'status' + | 'name' + | 'numOfBlocks' + | 'numOfInstances' + | 'isGamificationEnabled' + | 'accessMode' +> -function LiveQuizElement({ session }: LiveQuizElementProps) { +function LiveQuizElement({ quiz }: { quiz: LiveQuizListElementType }) { const t = useTranslations() const router = useRouter() @@ -65,8 +55,40 @@ function LiveQuizElement({ session }: LiveQuizElementProps) { fetchPolicy: 'cache-only', }) + const statusTagMap: Record = { + [PublicationStatus.Draft]: ( + + ), + [PublicationStatus.Scheduled]: ( + + ), + [PublicationStatus.Published]: ( + + ), + [PublicationStatus.Ended]: ( + + ), + [PublicationStatus.Graded]: null, + } + const [deleteLiveQuiz] = useMutation(DeleteLiveQuizDocument, { - variables: { id: session.id }, + variables: { id: quiz.id }, update(cache, res) { const data = cache.readQuery({ query: GetSingleCourseDocument, @@ -81,8 +103,8 @@ function LiveQuizElement({ session }: LiveQuizElementProps) { data: { course: { ...data.course, - sessions: data.course.sessions?.filter( - (session) => session.id !== res.data!.deleteLiveQuiz!.id + liveQuizzes: data.course.liveQuizzes?.filter( + (quiz) => quiz.id !== res.data!.deleteLiveQuiz!.id ), }, }, @@ -90,8 +112,8 @@ function LiveQuizElement({ session }: LiveQuizElementProps) { }, optimisticResponse: { deleteLiveQuiz: { - __typename: 'Session', - id: session.id, + __typename: 'LiveQuiz', + id: quiz.id, }, }, refetchQueries: [GetSingleCourseDocument], @@ -101,182 +123,198 @@ function LiveQuizElement({ session }: LiveQuizElementProps) { return (
    -
    -
    -
    {statusMap[session.status]}
    - - - {session.name} - +
    +
    +
    + + {quiz.name} + +
    +
    + {t('manage.liveQuizzes.nBlocksQuestions', { + blocks: quiz.numOfBlocks, + questions: quiz.numOfInstances, + })} +
    -
    - {t('manage.sessions.nBlocksQuestions', { - blocks: session.numOfBlocks, - questions: session.numOfQuestions, - })} + +
    +
    + {(quiz.status === PublicationStatus.Scheduled || + quiz.status === PublicationStatus.Draft) && ( + <> + + + +
    {t('manage.liveQuizzes.editLiveQuiz')}
    +
    + ), + onClick: () => + router.push({ + pathname: '/', + query: { + elementId: quiz.id, + editMode: WizardMode.LiveQuiz, + }, + }), + data: { cy: `edit-live-quiz-${quiz.name}` }, + }, + getActivityDuplicationAction({ + id: quiz.id, + text: t('manage.liveQuizzes.duplicateLiveQuiz'), + wizardMode: WizardMode.LiveQuiz, + router: router, + data: { cy: `duplicate-live-quiz-${quiz.name}` }, + }), + { + label: ( +
    + +
    {t('manage.liveQuizzes.deleteLiveQuiz')}
    +
    + ), + onClick: () => setDeletionModal(true), + data: { cy: `delete-live-quiz-${quiz.name}` }, + }, + ].flat()} + triggerIcon={faHandPointer} + /> + + )} + {quiz.status === PublicationStatus.Published && ( + <> + + + + )} + {quiz.status === PublicationStatus.Ended && ( + <> + + + +
    {t('manage.liveQuizzes.deleteLiveQuiz')}
    +
    + ), + onClick: () => setDeletionModal(true), + data: { cy: `delete-live-quiz-${quiz.name}` }, + }, + getActivityDuplicationAction({ + id: quiz.id, + text: t('manage.liveQuizzes.duplicateLiveQuiz'), + wizardMode: WizardMode.LiveQuiz, + router: router, + data: { cy: `duplicate-live-quiz-${quiz.name}` }, + }), + ]} + triggerIcon={faHandPointer} + /> + + )} +
    + + +
    -
    -
    - {(session.status === SessionStatus.Scheduled || - session.status === SessionStatus.Prepared) && ( - <> - - - -
    {t('manage.sessions.editSession')}
    -
    - ), - onClick: () => - router.push({ - pathname: '/', - query: { - elementId: session.id, - editMode: WizardMode.LiveQuiz, - }, - }), - data: { cy: `edit-live-quiz-${session.name}` }, - }, - getActivityDuplicationAction({ - id: session.id, - text: t('manage.sessions.duplicateSession'), - wizardMode: WizardMode.LiveQuiz, - router: router, - data: { cy: `duplicate-live-quiz-${session.name}` }, - }), - { - label: ( -
    - -
    {t('manage.sessions.deleteSession')}
    -
    - ), - onClick: () => setDeletionModal(true), - data: { cy: `delete-live-quiz-${session.name}` }, - }, - ].flat()} - triggerIcon={faHandPointer} - /> - - )} - {session.status === SessionStatus.Running && ( - <> - - - - )} - {session.status === SessionStatus.Completed && ( - <> - - - -
    {t('manage.sessions.deleteSession')}
    -
    - ), - onClick: () => setDeletionModal(true), - data: { cy: `delete-live-quiz-${session.name}` }, - }, - getActivityDuplicationAction({ - id: session.id, - text: t('manage.sessions.duplicateSession'), - wizardMode: WizardMode.LiveQuiz, - router: router, - data: { cy: `duplicate-live-quiz-${session.name}` }, - }), - ]} - triggerIcon={faHandPointer} - /> - - )} -
    +
    - {session.isGamificationEnabled && ( + {quiz.isGamificationEnabled && ( )} - {session.accessMode === SessionAccessMode.Public && ( + {quiz.accessMode === LiveQuizAccessMode.Public && ( )} - {session.accessMode === SessionAccessMode.Restricted && ( + {quiz.accessMode === LiveQuizAccessMode.Restricted && ( )}
    +
    {statusTagMap[quiz.status]}
    - - -
    ) } diff --git a/apps/frontend-manage/src/components/courses/LiveQuizList.tsx b/apps/frontend-manage/src/components/courses/LiveQuizList.tsx index a85c2fbc2b..9cf9e795bd 100644 --- a/apps/frontend-manage/src/components/courses/LiveQuizList.tsx +++ b/apps/frontend-manage/src/components/courses/LiveQuizList.tsx @@ -1,47 +1,40 @@ -import { Session, SessionStatus } from '@klicker-uzh/graphql/dist/ops' +import { PublicationStatus } from '@klicker-uzh/graphql/dist/ops' import { useTranslations } from 'next-intl' import { sort } from 'remeda' -import LiveQuizElement from './LiveQuizElement' +import LiveQuizElement, { LiveQuizListElementType } from './LiveQuizElement' -const sortingOrderSessions: Record = { - [SessionStatus.Running]: 0, - [SessionStatus.Scheduled]: 1, - [SessionStatus.Prepared]: 2, - [SessionStatus.Completed]: 3, +const sortingOrderLiveQuizzes: Record = { + [PublicationStatus.Published]: 0, + [PublicationStatus.Scheduled]: 1, + [PublicationStatus.Draft]: 2, + [PublicationStatus.Ended]: 3, + [PublicationStatus.Graded]: 4, } -interface LiveQuizListProps { - sessions: Pick< - Session, - | 'id' - | 'status' - | 'name' - | 'numOfBlocks' - | 'numOfQuestions' - | 'isGamificationEnabled' - | 'accessMode' - >[] -} - -function LiveQuizList({ sessions }: LiveQuizListProps) { +function LiveQuizList({ + liveQuizzes, +}: { + liveQuizzes: LiveQuizListElementType[] +}) { const t = useTranslations() return (
    - {sessions && sessions.length > 0 ? ( + {liveQuizzes && liveQuizzes.length > 0 ? (
    - {sort(sessions, (a, b) => { + {sort(liveQuizzes, (a, b) => { if (!a.status || !b.status) return 0 return ( - sortingOrderSessions[a.status] - sortingOrderSessions[b.status] + sortingOrderLiveQuizzes[a.status] - + sortingOrderLiveQuizzes[b.status] ) - }).map((session) => ( - + }).map((quiz) => ( + ))}
    ) : ( -
    {t('manage.course.noSessions')}
    +
    {t('manage.course.noLiveQuizzes')}
    )}
    ) diff --git a/apps/frontend-manage/src/components/courses/MicroLearningElement.tsx b/apps/frontend-manage/src/components/courses/MicroLearningElement.tsx index f602104fca..9d84cdc504 100644 --- a/apps/frontend-manage/src/components/courses/MicroLearningElement.tsx +++ b/apps/frontend-manage/src/components/courses/MicroLearningElement.tsx @@ -2,13 +2,13 @@ import { useMutation, useQuery } from '@apollo/client' import { faCalendar, faClock, + faHandPointer, faTrashCan, } from '@fortawesome/free-regular-svg-icons' import { faArrowsRotate, faCheck, faFlagCheckered, - faHandPointer, faHourglassEnd, faHourglassStart, faLock, @@ -28,7 +28,7 @@ import dayjs from 'dayjs' import { useTranslations } from 'next-intl' import { useRouter } from 'next/router' import React, { useState } from 'react' -import { WizardMode } from '../sessions/creation/ElementCreation' +import { WizardMode } from '../activities/ElementCreation' import CopyConfirmationToast from '../toasts/CopyConfirmationToast' import { getAccessLink, getLTIAccessLink } from './PracticeQuizElement' import StatusTag from './StatusTag' @@ -79,7 +79,7 @@ function MicroLearningElement({ variables: { id: microLearning.id }, }) - const statusMap: Record = { + const statusMap: Record = { [PublicationStatus.Draft]: ( ), + [PublicationStatus.Ended]: null, + [PublicationStatus.Graded]: null, } const deletionElement = { diff --git a/apps/frontend-manage/src/components/courses/PracticeQuizElement.tsx b/apps/frontend-manage/src/components/courses/PracticeQuizElement.tsx index a4c2ea8233..654602278c 100644 --- a/apps/frontend-manage/src/components/courses/PracticeQuizElement.tsx +++ b/apps/frontend-manage/src/components/courses/PracticeQuizElement.tsx @@ -1,8 +1,11 @@ import { useMutation, useQuery } from '@apollo/client' -import { faClock, faTrashCan } from '@fortawesome/free-regular-svg-icons' import { - faCopy, + faClock, faHandPointer, + faTrashCan, +} from '@fortawesome/free-regular-svg-icons' +import { + faCopy, faHourglassStart, faLink, faLock, @@ -23,7 +26,7 @@ import dayjs from 'dayjs' import { useTranslations } from 'next-intl' import { useRouter } from 'next/router' import React, { useState } from 'react' -import { WizardMode } from '../sessions/creation/ElementCreation' +import { WizardMode } from '../activities/ElementCreation' import CopyConfirmationToast from '../toasts/CopyConfirmationToast' import StatusTag from './StatusTag' import PracticeQuizAccessLink from './actions/PracticeQuizAccessLink' @@ -136,7 +139,7 @@ function PracticeQuizElement({ const href = `${process.env.NEXT_PUBLIC_PWA_URL}/course/${courseId}/quiz/${practiceQuiz.id}/` const evaluationHref = `/practiceQuiz/${practiceQuiz.id}/evaluation` - const statusMap: Record = { + const statusMap: Record = { [PublicationStatus.Draft]: ( ), + [PublicationStatus.Ended]: null, + [PublicationStatus.Graded]: null, } const deletionItem = { diff --git a/apps/frontend-manage/src/components/courses/actions/EvaluationLinkLiveQuiz.tsx b/apps/frontend-manage/src/components/courses/actions/EvaluationLinkLiveQuiz.tsx index 114f95e8fa..bd7b7512b7 100644 --- a/apps/frontend-manage/src/components/courses/actions/EvaluationLinkLiveQuiz.tsx +++ b/apps/frontend-manage/src/components/courses/actions/EvaluationLinkLiveQuiz.tsx @@ -1,11 +1,11 @@ import { faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { Session } from '@klicker-uzh/graphql/dist/ops' +import { LiveQuiz } from '@klicker-uzh/graphql/dist/ops' import { useTranslations } from 'next-intl' import Link from 'next/link' interface EvaluationLinkLiveQuizProps { - liveQuiz: Partial + liveQuiz: Pick } function EvaluationLinkLiveQuiz({ liveQuiz }: EvaluationLinkLiveQuizProps) { @@ -21,7 +21,7 @@ function EvaluationLinkLiveQuiz({ liveQuiz }: EvaluationLinkLiveQuizProps) { passHref legacyBehavior > -
    + {t('shared.generic.evaluation')} diff --git a/apps/frontend-manage/src/components/courses/actions/RunningLiveQuizLink.tsx b/apps/frontend-manage/src/components/courses/actions/RunningLiveQuizLink.tsx index 004229257b..bf53760e60 100644 --- a/apps/frontend-manage/src/components/courses/actions/RunningLiveQuizLink.tsx +++ b/apps/frontend-manage/src/components/courses/actions/RunningLiveQuizLink.tsx @@ -1,11 +1,11 @@ import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { Session } from '@klicker-uzh/graphql/dist/ops' +import { LiveQuiz } from '@klicker-uzh/graphql/dist/ops' import { useTranslations } from 'next-intl' import Link from 'next/link' interface RunningLiveQuizLinkProps { - liveQuiz: Partial + liveQuiz: Pick } function RunningLiveQuizLink({ liveQuiz }: RunningLiveQuizLinkProps) { @@ -15,8 +15,8 @@ function RunningLiveQuizLink({ liveQuiz }: RunningLiveQuizLinkProps) { diff --git a/apps/frontend-manage/src/components/courses/actions/StartLiveQuizButton.tsx b/apps/frontend-manage/src/components/courses/actions/StartLiveQuizButton.tsx index 1a3669f8ae..8c3444ee82 100644 --- a/apps/frontend-manage/src/components/courses/actions/StartLiveQuizButton.tsx +++ b/apps/frontend-manage/src/components/courses/actions/StartLiveQuizButton.tsx @@ -1,28 +1,52 @@ import { useMutation } from '@apollo/client' import { faPlay } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { Session, StartSessionDocument } from '@klicker-uzh/graphql/dist/ops' +import { + GetUserRunningLiveQuizzesDocument, + LiveQuiz, + StartLiveQuizDocument, +} from '@klicker-uzh/graphql/dist/ops' import { Button } from '@uzh-bf/design-system' import { useTranslations } from 'next-intl' import { useRouter } from 'next/router' interface StartLiveQuizButtonProps { - liveQuiz: Partial + liveQuiz: Pick } function StartLiveQuizButton({ liveQuiz }: StartLiveQuizButtonProps) { const t = useTranslations() const router = useRouter() - const [startSession, { loading: startingSession }] = - useMutation(StartSessionDocument) + const [startLiveQuiz, { loading: startingQuiz }] = useMutation( + StartLiveQuizDocument, + { + update(cache) { + const data = cache.readQuery({ + query: GetUserRunningLiveQuizzesDocument, + }) + cache.writeQuery({ + query: GetUserRunningLiveQuizzesDocument, + data: { + userRunningLiveQuizzes: + liveQuiz.id && liveQuiz.name + ? [ + ...(data?.userRunningLiveQuizzes ?? []), + { id: liveQuiz.id, name: liveQuiz.name }, + ] + : (data?.userRunningLiveQuizzes ?? []), + }, + }) + }, + } + ) return ( ) } diff --git a/apps/frontend-manage/src/components/courses/actions/getActivityDuplicationAction.tsx b/apps/frontend-manage/src/components/courses/actions/getActivityDuplicationAction.tsx index 1a6fc01cbb..c64869b1dd 100644 --- a/apps/frontend-manage/src/components/courses/actions/getActivityDuplicationAction.tsx +++ b/apps/frontend-manage/src/components/courses/actions/getActivityDuplicationAction.tsx @@ -1,7 +1,7 @@ -import { WizardMode } from '@components/sessions/creation/ElementCreation' import { faCopy } from '@fortawesome/free-regular-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { NextRouter } from 'next/router' +import { WizardMode } from '../../activities/ElementCreation' interface getActivityDuplicationActionProps { id: string diff --git a/apps/frontend-manage/src/components/courses/groupActivity/GroupActivityGradingStack.tsx b/apps/frontend-manage/src/components/courses/groupActivity/GroupActivityGradingStack.tsx index 03c4f949b4..475d3f341c 100644 --- a/apps/frontend-manage/src/components/courses/groupActivity/GroupActivityGradingStack.tsx +++ b/apps/frontend-manage/src/components/courses/groupActivity/GroupActivityGradingStack.tsx @@ -10,7 +10,7 @@ import { GroupActivityInstance, } from '@klicker-uzh/graphql/dist/ops' import StudentElement, { - StudentResponseType, + StackStudentResponseType, } from '@klicker-uzh/shared-components/src/StudentElement' import { Button, @@ -227,7 +227,7 @@ function GroupActivityGradingStack({ (findResponse( element.id, element.elementType - ) as StudentResponseType) ?? [] + ) as StackStudentResponseType) ?? [] } setStudentResponse={() => null} hideReadButton diff --git a/apps/frontend-manage/src/components/courses/modals/CourseManipulationModal.tsx b/apps/frontend-manage/src/components/courses/modals/CourseManipulationModal.tsx index 5f3fd23aee..b85bb4090d 100644 --- a/apps/frontend-manage/src/components/courses/modals/CourseManipulationModal.tsx +++ b/apps/frontend-manage/src/components/courses/modals/CourseManipulationModal.tsx @@ -16,7 +16,7 @@ import { useTranslations } from 'next-intl' import { Dispatch, SetStateAction, useRef, useState } from 'react' import { twMerge } from 'tailwind-merge' import * as yup from 'yup' -import EditorField from '../../sessions/creation/EditorField' +import EditorField from '../../activities/creation/EditorField' import ElementCreationErrorToast from '../../toasts/ElementCreationErrorToast' import CourseDateChangeMonitor from './CourseDateChangeMonitor' import GamificationSettingMonitor from './GamificationSettingMonitor' @@ -239,7 +239,7 @@ function CourseManipulationModal({ /> dayjs(value) > dayjs(courseStartDate) ), })} diff --git a/apps/frontend-manage/src/components/evaluation/ActivityEvaluation.tsx b/apps/frontend-manage/src/components/evaluation/ActivityEvaluation.tsx index 9b32453443..ca69442af8 100644 --- a/apps/frontend-manage/src/components/evaluation/ActivityEvaluation.tsx +++ b/apps/frontend-manage/src/components/evaluation/ActivityEvaluation.tsx @@ -1,28 +1,53 @@ import { - sizeReducer, - TextSizes, -} from '@components/sessions/evaluation/constants' -import { StackEvaluation } from '@klicker-uzh/graphql/dist/ops' + ConfusionTimestep, + Feedback, + StackEvaluation, +} from '@klicker-uzh/graphql/dist/ops' import { ChartType } from '@klicker-uzh/shared-components/src/constants' +import Leaderboard, { + LeaderboardCombinedEntry, +} from '@klicker-uzh/shared-components/src/Leaderboard' +import useEvaluationInitialization from '@lib/hooks/useEvaluationInitialization' +import { UserNotification } from '@uzh-bf/design-system' +import { useTranslations } from 'next-intl' import Head from 'next/head' import { useRouter } from 'next/router' +import Rank1Img from 'public/img/rank1.svg' +import Rank2Img from 'public/img/rank2.svg' +import Rank3Img from 'public/img/rank3.svg' import { useReducer, useState } from 'react' import { twMerge } from 'tailwind-merge' import ElementEvaluation from './ElementEvaluation' import EvaluationFooter from './EvaluationFooter' +import EvaluationConfusion from './feedbacks/EvaluationConfusion' +import EvaluationFeedbacks from './feedbacks/EvaluationFeedbacks' import useChartTypeUpdate from './hooks/useChartTypeUpdate' import useStackInstanceMap from './hooks/useStackInstanceMap' import EvaluationNavigation from './navigation/EvaluationNavigation' +import { sizeReducer, TextSizes } from './textSizes' + +export type ActivityEvaluationType = 'LiveQuiz' | 'Asynchronous' +export type ActiveStackType = number | 'feedbacks' | 'confusion' | 'leaderboard' interface ActivityEvaluationProps { activityName: string stacks: StackEvaluation[] + feedbacks?: Feedback[] | null + confusionFeedbacks?: ConfusionTimestep[] | null + leaderboard?: LeaderboardCombinedEntry[] | null + type?: ActivityEvaluationType } -export type ActiveStackType = number | 'feedbacks' | 'confusion' | 'leaderboard' - -function ActivityEvaluation({ activityName, stacks }: ActivityEvaluationProps) { +function ActivityEvaluation({ + activityName, + stacks, + feedbacks, + confusionFeedbacks, + leaderboard, + type = 'Asynchronous', +}: ActivityEvaluationProps) { const router = useRouter() + const t = useTranslations() const [activeStack, setActiveStack] = useState(0) const [activeInstance, setActiveInstance] = useState(0) const [showSolution, setShowSolution] = useState(false) @@ -31,13 +56,24 @@ function ActivityEvaluation({ activityName, stacks }: ActivityEvaluationProps) { const instanceResults = stacks.flatMap((stack) => stack.instances) + // automatically switch to correct instance and use correct settings depending on URL params + useEvaluationInitialization({ + setActiveInstance, + setActiveStack, + setShowSolution, + questionIx: router.query.questionIx as string | null, + showLeaderboard: router.query.leaderboard === 'true', + showSolution: router.query.showSolution === 'true', + type, + }) + // compute a map between stack and instance indices {stackIx: [instanceIx1, instanceIx2], ...} const stackInstanceMap = useStackInstanceMap({ stacks }) // update the chart type as soon as the active instance changes useChartTypeUpdate({ activeInstance, - activeElementType: instanceResults[activeInstance].type, + activeElementType: instanceResults[activeInstance]?.type, chartType, setChartType, }) @@ -63,6 +99,11 @@ function ActivityEvaluation({ activityName, stacks }: ActivityEvaluationProps) { activeInstance={activeInstance} setActiveInstance={setActiveInstance} numOfInstances={instanceResults.length} + type={type} + leaderboardAvailable={leaderboard !== null} + feedbacksAvailable={ + feedbacks !== null && confusionFeedbacks !== null + } />
    )} @@ -79,46 +120,47 @@ function ActivityEvaluation({ activityName, stacks }: ActivityEvaluationProps) { ? showSolution : false } + type={type} /> )} - {/* {showLeaderboard && !showConfusion && !showFeedbacks && ( -
    -
    -
    - {data.sessionLeaderboard && - data.sessionLeaderboard.length > 0 ? ( - - ) : ( - - )} + {type === 'LiveQuiz' && + leaderboard !== null && + activeStack === 'leaderboard' && ( +
    +
    +
    + {leaderboard && leaderboard.length > 0 ? ( + + ) : ( + + )} +
    -
    - )} */} + )} - {/* {!showLeaderboard && - !showConfusion && - showFeedbacks && - data.sessionEvaluation && ( + {type === 'LiveQuiz' && + feedbacks !== null && + activeStack === 'feedbacks' && (
    {feedbacks && feedbacks.length > 0 ? ( ) : (
    - )} */} + )} - {/* {!showLeaderboard && showConfusion && !showFeedbacks && ( -
    -
    -
    - {confusionFeedbacks && confusionFeedbacks.length > 0 ? ( - - ) : ( - - )} + {type === 'LiveQuiz' && + confusionFeedbacks !== null && + activeStack === 'confusion' && ( +
    +
    +
    + {confusionFeedbacks && confusionFeedbacks.length > 0 ? ( + + ) : ( + + )} +
    -
    - )} */} + )}
    -} - -function BarChart({ - data, - showSolution, - textSize, -}: BarChartProps): React.ReactElement { - const t = useTranslations() - - // add labelIn and labelOut attributes to data, set labelIn to votes if votes/totalResponses > SMALL_BAR_THRESHOLD and set labelOut to votes otherwise - const questionData = data.questionData - const dataWithLabels = useMemo(() => { - const labeledData = Object.values( - data.results as Record - ).map((result, idx) => { - const labelIn = - result.count / data.participants > SMALL_BAR_THRESHOLD - ? result.count - : undefined - const labelOut = - result.count / data.participants <= SMALL_BAR_THRESHOLD - ? result.count - : undefined - const xLabel = - questionData.type === 'NUMERICAL' - ? Math.round(parseFloat(result.value) * 100) / 100 - : String.fromCharCode(Number(idx) + 65) - return { count: result.count, labelIn, labelOut, xLabel } - }) - return labeledData.length > 0 - ? labeledData - : [ - { - count: 0, - labelIn: undefined, - labelOut: undefined, - xLabel: '0', - }, - ] - }, [data.results, data.participants, questionData.type]) - - return ( - - - - { - const rounded = Math.ceil(dataMax * 1.1) - if (rounded % 2 === 0) { - return rounded - } - return rounded + 1 - }, - ]} - label={{ - angle: -90, - position: 'insideLeft', - value: t('shared.generic.responses'), - className: textSize.textXl, - }} - /> - - - - - {questionData.__typename === 'ChoicesQuestionData' && - questionData.options.choices.map( - (choice: Choice): React.ReactElement => ( - - ) - )} - - - - ) -} - -export default BarChart diff --git a/apps/frontend-manage/src/components/evaluation/Chart.tsx b/apps/frontend-manage/src/components/evaluation/Chart.tsx deleted file mode 100644 index 477bff0804..0000000000 --- a/apps/frontend-manage/src/components/evaluation/Chart.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { - InstanceResult, - NumericalQuestionData, -} from '@klicker-uzh/graphql/dist/ops' -import Histogram from '@klicker-uzh/shared-components/src/Histogram' -import { useTranslations } from 'next-intl' -import React from 'react' -import { TextSizeType } from '../sessions/evaluation/constants' -import BarChart from './BarChart' -import TableChart from './TableChart' -import Wordcloud from './Wordcloud' - -interface ChartProps { - chartType: string - data: InstanceResult - showSolution: boolean - textSize: TextSizeType - statisticsShowSolution?: { - mean?: boolean - median?: boolean - q1?: boolean - q3?: boolean - sd?: boolean - } -} - -function Chart({ - chartType, - data, - showSolution, - textSize, - statisticsShowSolution, -}: ChartProps): React.ReactElement { - const t = useTranslations() - - if (chartType === 'table') { - // TODO: add resizing possibility with sizeMe: {({ size }) => } - return ( -
    - -
    - ) - } else if (chartType === 'histogram') { - return ( - - ) - } else if (chartType === 'wordCloud') { - return ( - - ) - } else if (chartType === 'barChart') { - return ( - - ) - } else { - return
    {t('manage.evaluation.noChartsAvailable')}
    - } -} - -export default Chart diff --git a/apps/frontend-manage/src/components/evaluation/ElementChart.tsx b/apps/frontend-manage/src/components/evaluation/ElementChart.tsx index adf5467817..a7e4ab1d9b 100644 --- a/apps/frontend-manage/src/components/evaluation/ElementChart.tsx +++ b/apps/frontend-manage/src/components/evaluation/ElementChart.tsx @@ -1,17 +1,19 @@ import { ElementInstanceEvaluation } from '@klicker-uzh/graphql/dist/ops' +import ElementBarChart from '@klicker-uzh/shared-components/src/charts/ElementBarChart' +import ElementHistogram from '@klicker-uzh/shared-components/src/charts/ElementHistogram' +import ElementTableChart from '@klicker-uzh/shared-components/src/charts/ElementTableChart' +import ElementWordcloud from '@klicker-uzh/shared-components/src/charts/ElementWordcloud' import { ChartType } from '@klicker-uzh/shared-components/src/constants' import { useTranslations } from 'next-intl' import React from 'react' -import { TextSizeType } from '../sessions/evaluation/constants' -import ElementBarChart from './charts/ElementBarChart' -import ElementHistogram from './charts/ElementHistogram' -import ElementTableChart from './charts/ElementTableChart' -import ElementWordcloud from './charts/ElementWordcloud' +import { ShowStatisticsType } from './elements/NREvaluation' +import { TextSizeType } from './textSizes' interface ElementChartProps { chartType: string instanceEvaluation: ElementInstanceEvaluation showSolution: boolean + showStatistics?: ShowStatisticsType textSize: TextSizeType } @@ -19,6 +21,7 @@ function ElementChart({ chartType, instanceEvaluation, showSolution, + showStatistics, textSize, }: ElementChartProps): React.ReactElement { const t = useTranslations() @@ -35,10 +38,23 @@ function ElementChart({ chartType === ChartType.HISTOGRAM && instanceEvaluation.__typename === 'NumericalElementInstanceEvaluation' ) { + const responses = instanceEvaluation.results.responseValues.map( + (response) => ({ + value: response.value, + count: response.count, + }) + ) + return ( ) diff --git a/apps/frontend-manage/src/components/evaluation/ElementEvaluation.tsx b/apps/frontend-manage/src/components/evaluation/ElementEvaluation.tsx index f4b2c5b163..a2aca63a92 100644 --- a/apps/frontend-manage/src/components/evaluation/ElementEvaluation.tsx +++ b/apps/frontend-manage/src/components/evaluation/ElementEvaluation.tsx @@ -1,6 +1,7 @@ import { ElementInstanceEvaluation } from '@klicker-uzh/graphql/dist/ops' import { ChartType } from '@klicker-uzh/shared-components/src/constants' import { twMerge } from 'tailwind-merge' +import { ActivityEvaluationType } from './ActivityEvaluation' import CTEvaluation from './elements/CTEvaluation' import ChoicesEvaluation from './elements/ChoicesEvaluation' import FCEvaluation from './elements/FCEvaluation' @@ -15,6 +16,7 @@ interface ElementEvaluationProps { textSize: TextSizeType chartType: ChartType showSolution: boolean + type: ActivityEvaluationType className?: string } @@ -24,6 +26,7 @@ function ElementEvaluation({ textSize, chartType, showSolution, + type, className, }: ElementEvaluationProps) { return ( @@ -42,6 +45,7 @@ function ElementEvaluation({ textSize={textSize} chartType={chartType} showSolution={showSolution} + type={type} /> )} {currentInstance.__typename === @@ -51,6 +55,7 @@ function ElementEvaluation({ textSize={textSize} chartType={chartType} showSolution={showSolution} + type={type} /> )} {currentInstance.__typename === 'FreeElementInstanceEvaluation' && ( @@ -59,6 +64,7 @@ function ElementEvaluation({ textSize={textSize} chartType={chartType} showSolution={showSolution} + type={type} /> )} {currentInstance.__typename === diff --git a/apps/frontend-manage/src/components/evaluation/EvaluationFooter.tsx b/apps/frontend-manage/src/components/evaluation/EvaluationFooter.tsx index c220116dbf..4b3a27bd5d 100644 --- a/apps/frontend-manage/src/components/evaluation/EvaluationFooter.tsx +++ b/apps/frontend-manage/src/components/evaluation/EvaluationFooter.tsx @@ -41,7 +41,7 @@ function EvaluationFooter({