feat(frontend): UI vragen & antwoorden verbeterd.

This commit is contained in:
Gerald Schmittinger 2025-05-18 14:40:37 +02:00
parent 10a329bed3
commit 97f6a603f5
9 changed files with 164 additions and 217 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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;
}

View file

@ -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>