feat(frontend): UI vragen & antwoorden verbeterd.
This commit is contained in:
		
							parent
							
								
									10a329bed3
								
							
						
					
					
						commit
						97f6a603f5
					
				
					 9 changed files with 164 additions and 217 deletions
				
			
		|  | @ -15,7 +15,6 @@ | |||
|             <div | ||||
|                 v-for="question in questions" | ||||
|                 :key="(question.sequenceNumber, question.content)" | ||||
|                 class="border rounded-2xl p-4 shadow-sm bg-white" | ||||
|             > | ||||
|                 <SingleQuestion :question="question"></SingleQuestion> | ||||
|             </div> | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ | |||
|         language: Language; | ||||
|         learningObjectHruid?: string; | ||||
|         forGroup?: GroupDTOId | undefined; | ||||
|         withTitle?: boolean; | ||||
|     }>(); | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
|  | @ -65,6 +66,8 @@ | |||
|     const createQuestionMutation = useCreateQuestionMutation(loID); | ||||
|     const groupsQueryResult = useStudentGroupsQuery(authService.authState.user?.profile.preferred_username); | ||||
| 
 | ||||
|     const showQuestionBox = computed(() => authService.authState.activeRole === AccountType.Student && pathIsAssignment.value); | ||||
| 
 | ||||
|     function submitQuestion(): void { | ||||
|         const assignments = studentAssignmentsQueryResult.data.value?.assignments as AssignmentDTO[]; | ||||
|         const assignment = assignments.find( | ||||
|  | @ -93,24 +96,26 @@ | |||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div | ||||
|         v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment" | ||||
|         class="question-box" | ||||
|     > | ||||
|         <div class="input-wrapper"> | ||||
|             <input | ||||
|                 type="text" | ||||
|                 :placeholder="`${t(`question-input-placeholder`)}`" | ||||
|                 class="question-input" | ||||
|                 v-model="questionInput" | ||||
|             /> | ||||
|             <button | ||||
|                 @click="submitQuestion" | ||||
|                 class="send-button" | ||||
|             > | ||||
|                 ▶ | ||||
|             </button> | ||||
|         </div> | ||||
|     <h3 v-if="props.withTitle && showQuestionBox">{{ t('askAQuestion') }}:</h3> | ||||
|     <div class="question-box" v-if="showQuestionBox"> | ||||
|         <v-textarea | ||||
|             :label="t('question-input-placeholder')" | ||||
|             v-model="questionInput" | ||||
|             class="question-field" | ||||
|             density="compact" | ||||
|             rows="1" | ||||
|             variant="outlined" | ||||
|             auto-grow> | ||||
|             <template v-slot:append-inner> | ||||
|                 <v-btn | ||||
|                     icon="mdi mdi-send" | ||||
|                     size="small" | ||||
|                     variant="plain" | ||||
|                     class="question-button" | ||||
|                     @click="submitQuestion" | ||||
|                     /> | ||||
|             </template> | ||||
|         </v-textarea> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -119,40 +124,5 @@ | |||
|         width: 100%; | ||||
|         max-width: 400px; | ||||
|         margin: 20px auto; | ||||
|         font-family: sans-serif; | ||||
|     } | ||||
| 
 | ||||
|     .input-wrapper { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         border: 1px solid #ccc; | ||||
|         border-radius: 999px; | ||||
|         padding: 8px 12px; | ||||
|         box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||||
|     } | ||||
| 
 | ||||
|     .question-input { | ||||
|         flex: 1; | ||||
|         border: none; | ||||
|         outline: none; | ||||
|         font-size: 14px; | ||||
|         background-color: transparent; | ||||
|     } | ||||
| 
 | ||||
|     .question-input::placeholder { | ||||
|         color: #999; | ||||
|     } | ||||
| 
 | ||||
|     .send-button { | ||||
|         background: none; | ||||
|         border: none; | ||||
|         cursor: pointer; | ||||
|         font-size: 16px; | ||||
|         color: #555; | ||||
|         transition: color 0.2s ease; | ||||
|     } | ||||
| 
 | ||||
|     .send-button:hover { | ||||
|         color: #000; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -5,9 +5,10 @@ | |||
|     import UsingQueryResult from "./UsingQueryResult.vue"; | ||||
|     import type { AnswersResponse } from "@/controllers/answers"; | ||||
|     import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer"; | ||||
|     import type { UserDTO } from "@dwengo-1/common/interfaces/user"; | ||||
|     import authService from "@/services/auth/auth-service"; | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import { AccountType } from "@dwengo-1/common/util/account-types" | ||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|  | @ -65,161 +66,119 @@ | |||
|             createAnswerMutation.mutate(answerData, { | ||||
|                 onSuccess: async () => { | ||||
|                     answer.value = ""; | ||||
|                     expanded.value = true; | ||||
|                     await answersQuery.refetch(); | ||||
|                 }, | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function displayNameFor(user: UserDTO) { | ||||
|         if (user.firstName && user.lastName) { | ||||
|             return `${user.firstName} ${user.lastName}`; | ||||
|         } else { | ||||
|             return user.username; | ||||
|         } | ||||
|     } | ||||
| </script> | ||||
| <template> | ||||
|     <div class="space-y-4"> | ||||
|         <div | ||||
|             class="flex justify-between items-center mb-2" | ||||
|             style=" | ||||
|                 margin-right: 5px; | ||||
|                 margin-left: 5px; | ||||
|                 font-weight: bold; | ||||
|                 display: flex; | ||||
|                 flex-direction: row; | ||||
|                 justify-content: space-between; | ||||
|             " | ||||
|         <v-card | ||||
|             class="question-card" | ||||
|         > | ||||
|             <span class="font-semibold text-lg text-gray-800">{{ | ||||
|                 question.author.firstName + " " + question.author.lastName | ||||
|             }}</span> | ||||
|             <span class="text-sm text-gray-500">{{ formatDate(question.timestamp) }}</span> | ||||
|         </div> | ||||
| 
 | ||||
|         <div | ||||
|             class="text-gray-700 mb-3" | ||||
|             style="margin-left: 10px" | ||||
|         > | ||||
|             {{ question.content }} | ||||
|         </div> | ||||
|         <div | ||||
|             v-if="authService.authState.activeRole === AccountType.Teacher" | ||||
|             class="answer-input-container" | ||||
|         > | ||||
|             <input | ||||
|                 v-model="answer" | ||||
|                 type="text" | ||||
|                 :placeholder="t('answer-input-placeholder')" | ||||
|                 class="answer-input" | ||||
|             /> | ||||
|             <button | ||||
|                 @click="submitAnswer" | ||||
|                 class="submit-button" | ||||
|             <v-card-title class="author-title">{{ displayNameFor(question.author) }}</v-card-title> | ||||
|             <v-card-subtitle>{{ formatDate(question.timestamp) }}</v-card-subtitle> | ||||
|             <v-card-text> | ||||
|                 {{ question.content }} | ||||
|             </v-card-text> | ||||
|             <template v-slot:actions | ||||
|                 v-if="authService.authState.activeRole === AccountType.Teacher || answersQuery.data?.value?.answers?.length > 0" | ||||
|             > | ||||
|                 ▶ | ||||
|             </button> | ||||
|         </div> | ||||
|         <using-query-result | ||||
|             :query-result="answersQuery" | ||||
|             v-slot="answersResponse: { data: AnswersResponse }" | ||||
|         > | ||||
|             <button | ||||
|                 v-if="answersResponse.data.answers && answersResponse.data.answers.length > 0" | ||||
|                 @click="toggle()" | ||||
|                 class="toggle-answers-btn" | ||||
|             > | ||||
|                 {{ expanded ? t("answers-toggle-hide") : t("answers-toggle-show") }} | ||||
|             </button> | ||||
| 
 | ||||
|             <div | ||||
|                 v-show="expanded" | ||||
|                 ref="answersContainer" | ||||
|                 class="mt-3 pl-4 border-l-2 border-blue-200 space-y-2" | ||||
|             > | ||||
|                 <div | ||||
|                     v-for="(answer, answerIndex) in answersResponse.data.answers as AnswerDTO[]" | ||||
|                     :key="answerIndex" | ||||
|                     class="text-gray-600" | ||||
|                 > | ||||
|                     <v-divider :thickness="2" /> | ||||
|                     <div | ||||
|                         class="flex justify-between items-center mb-2" | ||||
|                         style=" | ||||
|                             margin-right: 5px; | ||||
|                             margin-left: 5px; | ||||
|                             font-weight: bold; | ||||
|                             display: flex; | ||||
|                             flex-direction: row; | ||||
|                             justify-content: space-between; | ||||
|                         " | ||||
|                 <div class="question-actions-container"> | ||||
|                     <v-textarea | ||||
|                         v-if="authService.authState.activeRole === AccountType.Teacher" | ||||
|                         :label="t('answer-input-placeholder')" | ||||
|                         v-model="answer" | ||||
|                         class="answer-field" | ||||
|                         density="compact" | ||||
|                         rows="1" | ||||
|                         variant="outlined" | ||||
|                         auto-grow> | ||||
|                         <template v-slot:append-inner> | ||||
|                             <v-btn | ||||
|                                 icon="mdi mdi-send" | ||||
|                                 size="small" | ||||
|                                 variant="plain" | ||||
|                                 class="answer-button" | ||||
|                                 @click="submitAnswer" | ||||
|                                 /> | ||||
|                         </template> | ||||
|                     </v-textarea> | ||||
|                     <using-query-result | ||||
|                         :query-result="answersQuery" | ||||
|                         v-slot="answersResponse: { data: AnswersResponse }" | ||||
|                     > | ||||
|                         <span class="font-semibold text-lg text-gray-800">{{ answer.author.username }}</span> | ||||
|                         <span class="text-sm text-gray-500">{{ formatDate(answer.timestamp) }}</span> | ||||
|                     </div> | ||||
|                         <v-btn | ||||
|                             v-if="answersResponse.data.answers && answersResponse.data.answers.length > 0" | ||||
|                             @click="toggle()" | ||||
|                         > | ||||
|                             {{ expanded ? t("answers-toggle-hide") : t("answers-toggle-show") }} | ||||
|                         </v-btn> | ||||
| 
 | ||||
|                     <div | ||||
|                         class="text-gray-700 mb-3" | ||||
|                         style="margin-left: 10px" | ||||
|                     > | ||||
|                         {{ answer.content }} | ||||
|                     </div> | ||||
|                         <div | ||||
|                             v-show="expanded" | ||||
|                             ref="answersContainer" | ||||
|                             class="mt-3 pl-4 border-l-2 border-blue-200 space-y-2" | ||||
|                         > | ||||
|                             <v-card | ||||
|                                 v-for="(answer, answerIndex) in answersResponse.data.answers as AnswerDTO[]" | ||||
|                                 :key="answerIndex" | ||||
|                                 class="answer-card" | ||||
|                             > | ||||
|                                 <v-card-title class="author-title">{{ displayNameFor(answer.author) }}</v-card-title> | ||||
|                                 <v-card-subtitle>{{ formatDate(answer.timestamp) }}</v-card-subtitle> | ||||
|                                 <v-card-text> | ||||
|                                     {{ answer.content }} | ||||
|                                 </v-card-text> | ||||
|                             </v-card> | ||||
|                         </div> | ||||
|                     </using-query-result> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </using-query-result> | ||||
|             </template> | ||||
|         </v-card> | ||||
|     </div> | ||||
| </template> | ||||
| <style scoped> | ||||
|     .toggle-answers-btn { | ||||
|         font-size: 0.875rem; | ||||
|         text-decoration: none; | ||||
|         background-color: #f0f4ff; /* subtle blue background */ | ||||
|         border: none; | ||||
|         border-radius: 4px; | ||||
|         padding: 6px 12px; | ||||
|         cursor: pointer; | ||||
|         transition: background-color 0.2s ease; | ||||
| 
 | ||||
|     .answer-field { | ||||
|         max-width: 500px; | ||||
|     } | ||||
| 
 | ||||
|     .toggle-answers-btn:hover { | ||||
|         background-color: #e0eaff; /* slightly darker on hover */ | ||||
|         text-decoration: underline; | ||||
|     .answer-button { | ||||
|         margin: auto; | ||||
|     } | ||||
| 
 | ||||
|     .answer-input-container { | ||||
|         margin: 5px; | ||||
|     } | ||||
|     .answer-input { | ||||
|         flex-grow: 1; | ||||
|         outline: none; | ||||
|         border: none; | ||||
|         background: transparent; | ||||
|         color: #374151; /* gray-700 */ | ||||
|         font-size: 0.875rem; /* smaller font size */ | ||||
| 
 | ||||
|     .question-card { | ||||
|         margin: 10px; | ||||
|     } | ||||
| 
 | ||||
|     .answer-input::placeholder { | ||||
|         color: #9ca3af; /* gray-400 */ | ||||
|     } | ||||
| 
 | ||||
|     .submit-button { | ||||
|         margin-left: 0.25rem; | ||||
|         padding: 0.25rem; | ||||
|         background-color: #f3f4f6; /* gray-100 */ | ||||
|         border-radius: 9999px; | ||||
|         transition: background-color 0.2s; | ||||
|         border: none; | ||||
|         cursor: pointer; | ||||
|     } | ||||
| 
 | ||||
|     .submit-button:hover { | ||||
|         background-color: #e5e7eb; /* gray-200 */ | ||||
|     } | ||||
| 
 | ||||
|     .submit-icon { | ||||
|         width: 0.75rem; | ||||
|         height: 0.75rem; | ||||
|         color: #4b5563; /* gray-600 */ | ||||
|     } | ||||
|     .answer-input-container { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         border: 1px solid #d1d5db; /* gray-300 */ | ||||
|         border-radius: 9999px; | ||||
|         padding: 0.5rem 1rem; | ||||
|         max-width: 28rem; | ||||
|     .question-actions-container { | ||||
|         width: 100%; | ||||
|         margin-left: 10px; | ||||
|         margin-right: 10px; | ||||
|     } | ||||
| 
 | ||||
|     .answer-card { | ||||
|         margin-top: 10px; | ||||
|         margin-bottom: 10px; | ||||
|     } | ||||
| 
 | ||||
|     .author-title { | ||||
|         font-size: 14pt; | ||||
|         margin-bottom: -10px; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -169,10 +169,12 @@ | |||
|     "hintKeywordsSeparatedBySpaces": "Schlüsselwörter durch Leerzeichen getrennt", | ||||
|     "questions": "Fragen", | ||||
|     "view-questions": "Fragen anzeigen auf ", | ||||
|     "question-input-placeholder": "Frage...", | ||||
|     "answer-input-placeholder": "Antwort...", | ||||
|     "question-input-placeholder": "Ihre Frage...", | ||||
|     "answer-input-placeholder": "Ihre Antwort...", | ||||
|     "answers-toggle-hide": "Antworten verstecken", | ||||
|     "answers-toggle-show": "Antworten anzeigen", | ||||
|     "no-questions": "Keine Fragen", | ||||
|     "no-discussion-tip": "Wählen Sie ein Lernobjekt aus, um dessen Fragen anzuzeigen" | ||||
|     "no-discussion-tip": "Wählen Sie ein Lernobjekt aus, um dessen Fragen anzuzeigen", | ||||
|     "askAQuestion": "Eine Frage stellen", | ||||
|     "questionsCapitalized": "Fragen" | ||||
| } | ||||
|  |  | |||
|  | @ -169,10 +169,12 @@ | |||
|     "hintKeywordsSeparatedBySpaces": "Keywords separated by spaces", | ||||
|     "questions": "questions", | ||||
|     "view-questions": "View questions in ", | ||||
|     "question-input-placeholder": "question...", | ||||
|     "answer-input-placeholder": "answer...", | ||||
|     "question-input-placeholder": "Your question...", | ||||
|     "answer-input-placeholder": "Your answer...", | ||||
|     "answers-toggle-hide": "Hide answers", | ||||
|     "answers-toggle-show": "Show answers", | ||||
|     "no-questions": "No questions asked yet", | ||||
|     "no-discussion-tip": "Choose a learning object to view its questions" | ||||
|     "no-discussion-tip": "Choose a learning object to view its questions", | ||||
|     "askAQuestion": "Ask a question", | ||||
|     "questionsCapitalized": "Questions" | ||||
| } | ||||
|  |  | |||
|  | @ -170,10 +170,12 @@ | |||
|     "hintKeywordsSeparatedBySpaces": "Mots-clés séparés par des espaces", | ||||
|     "questions": "Questions", | ||||
|     "view-questions": "Voir les questions dans ", | ||||
|     "question-input-placeholder": "question...", | ||||
|     "answer-input-placeholder": "réponse...", | ||||
|     "question-input-placeholder": "Votre question...", | ||||
|     "answer-input-placeholder": "Votre réponse...", | ||||
|     "answers-toggle-hide": "Masquer réponses", | ||||
|     "answers-toggle-show": "Afficher réponse", | ||||
|     "no-questions": "Aucune question trouvée", | ||||
|     "no-discussion-tip": "Sélectionnez un objet d'apprentissage pour afficher les questions qui s'y rapportent" | ||||
|     "no-discussion-tip": "Sélectionnez un objet d'apprentissage pour afficher les questions qui s'y rapportent", | ||||
|     "askAQuestion": "Pose une question", | ||||
|     "questionsCapitalized": "Questions" | ||||
| } | ||||
|  |  | |||
|  | @ -169,10 +169,12 @@ | |||
|     "hintKeywordsSeparatedBySpaces": "Trefwoorden gescheiden door spaties", | ||||
|     "questions": "vragen", | ||||
|     "view-questions": "Bekijk vragen in ", | ||||
|     "question-input-placeholder": "vraag...", | ||||
|     "answer-input-placeholder": "antwoord...", | ||||
|     "question-input-placeholder": "Uw vraag...", | ||||
|     "answer-input-placeholder": "Uw antwoord...", | ||||
|     "answers-toggle-hide": "Verberg antwoorden", | ||||
|     "answers-toggle-show": "Toon antwoorden", | ||||
|     "no-questions": "Nog geen vragen gesteld", | ||||
|     "no-discussion-tip": "Kies een leerobject om zijn vragen te bekijken" | ||||
|     "no-discussion-tip": "Kies een leerobject om zijn vragen te bekijken", | ||||
|     "askAQuestion": "Stel een vraag", | ||||
|     "questionsCapitalized": "Vragen" | ||||
| } | ||||
|  |  | |||
|  | @ -15,6 +15,9 @@ | |||
|     import type { QuestionDTO } from "@dwengo-1/common/interfaces/question"; | ||||
|     import DiscussionsSideBar from "@/components/DiscussionsSideBar.vue"; | ||||
|     import QuestionBox from "@/components/QuestionBox.vue"; | ||||
|     import { useI18n } from "vue-i18n"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
| 
 | ||||
|  | @ -120,21 +123,29 @@ | |||
| 
 | ||||
| <template> | ||||
|     <DiscussionsSideBar></DiscussionsSideBar> | ||||
|     <QuestionBox | ||||
|         :hruid="props.hruid" | ||||
|         :language="props.language" | ||||
|         :learningObjectHruid="props.learningObjectHruid" | ||||
|         :forGroup="forGroup" | ||||
|     /> | ||||
|     <using-query-result | ||||
|         :query-result="getQuestionsQuery" | ||||
|         v-slot="questionsResponse: { data: QuestionsResponse }" | ||||
|     > | ||||
|         <QandA :questions="(questionsResponse.data.questions as QuestionDTO[]) ?? []" /> | ||||
|     </using-query-result> | ||||
|     <div class="discussions-container"> | ||||
|         <QuestionBox | ||||
|             :hruid="props.hruid" | ||||
|             :language="props.language" | ||||
|             :learningObjectHruid="props.learningObjectHruid" | ||||
|             :forGroup="forGroup" | ||||
|             withTitle | ||||
|         /> | ||||
|         <h3>{{ t("questionsCapitalized") }}:</h3> | ||||
|         <using-query-result | ||||
|             :query-result="getQuestionsQuery" | ||||
|             v-slot="questionsResponse: { data: QuestionsResponse }" | ||||
|         > | ||||
|             <QandA :questions="(questionsResponse.data.questions as QuestionDTO[]) ?? []" /> | ||||
|         </using-query-result> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|     .discussions-container { | ||||
|         margin: 20px; | ||||
|     } | ||||
| 
 | ||||
|     .learning-path-title { | ||||
|         white-space: normal; | ||||
|     } | ||||
|  |  | |||
|  | @ -308,12 +308,6 @@ | |||
|                 v-if="currentNode" | ||||
|             ></learning-object-view> | ||||
|         </div> | ||||
|         <QuestionBox | ||||
|             :hruid="props.hruid" | ||||
|             :language="props.language" | ||||
|             :learningObjectHruid="props.learningObjectHruid" | ||||
|             :forGroup="forGroup" | ||||
|         /> | ||||
|         <div class="navigation-buttons-container"> | ||||
|             <v-btn | ||||
|                 prepend-icon="mdi-chevron-left" | ||||
|  | @ -333,6 +327,7 @@ | |||
|             </v-btn> | ||||
|         </div> | ||||
|         <using-query-result | ||||
|             v-if="forGroup" | ||||
|             :query-result="getQuestionsQuery" | ||||
|             v-slot="questionsResponse: { data: QuestionsResponse }" | ||||
|         > | ||||
|  | @ -346,7 +341,12 @@ | |||
|                     </router-link> | ||||
|                 </span> | ||||
|             </div> | ||||
| 
 | ||||
|             <QuestionBox | ||||
|                 :hruid="props.hruid" | ||||
|                 :language="props.language" | ||||
|                 :learningObjectHruid="props.learningObjectHruid" | ||||
|                 :forGroup="forGroup" | ||||
|             /> | ||||
|             <QandA :questions="(questionsResponse.data.questions as QuestionDTO[]) ?? []" /> | ||||
|         </using-query-result> | ||||
|     </using-query-result> | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger