Merge fix/progress-bar into feat/232-assignments-pagina-ui-ux
This commit is contained in:
commit
368130c431
149 changed files with 4429 additions and 1120 deletions
54
frontend/src/assets/common.css
Normal file
54
frontend/src/assets/common.css
Normal file
|
@ -0,0 +1,54 @@
|
|||
.h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
font-size: 50px;
|
||||
padding-left: 1%;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: bold !important;
|
||||
background-color: #0e6942;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.table thead th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
.table thead th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(odd) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(even) {
|
||||
background-color: #f6faf2;
|
||||
}
|
||||
|
||||
.table td,
|
||||
.table th {
|
||||
border-bottom: 1px solid #0e6942;
|
||||
border-top: 1px solid #0e6942;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 90%;
|
||||
padding-top: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 850px) {
|
||||
.h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
import { AGE_TO_THEMES, THEMESITEMS } from "@/utils/constants.ts";
|
||||
import { useThemeQuery } from "@/queries/themes.ts";
|
||||
import type { Theme } from "@/data-objects/theme.ts";
|
||||
import authService from "@/services/auth/auth-service";
|
||||
|
||||
const props = defineProps({
|
||||
selectedTheme: { type: String, required: true },
|
||||
|
@ -33,6 +34,8 @@
|
|||
cards.value = themes;
|
||||
}
|
||||
});
|
||||
|
||||
const isTeacher = computed(() => authService.authState.activeRole === "teacher");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -57,6 +60,39 @@
|
|||
</div>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="4"
|
||||
class="d-flex"
|
||||
>
|
||||
<ThemeCard
|
||||
path="/learningPath/search"
|
||||
:is-absolute-path="true"
|
||||
:title="t('searchAllLearningPathsTitle')"
|
||||
:description="t('searchAllLearningPathsDescription')"
|
||||
icon="mdi-magnify"
|
||||
class="fill-height grey-bg-card"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="isTeacher"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="4"
|
||||
class="d-flex"
|
||||
>
|
||||
<ThemeCard
|
||||
path="/my-content"
|
||||
:is-absolute-path="true"
|
||||
:title="t('ownLearningContentTitle')"
|
||||
:description="t('ownLearningContentDescription')"
|
||||
icon="mdi-pencil"
|
||||
class="fill-height grey-bg-card"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-for="card in cards"
|
||||
:key="card.key"
|
||||
|
@ -74,24 +110,13 @@
|
|||
class="fill-height"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="4"
|
||||
class="d-flex"
|
||||
>
|
||||
<ThemeCard
|
||||
path="/learningPath/search"
|
||||
:is-absolute-path="true"
|
||||
:title="t('searchAllLearningPathsTitle')"
|
||||
:description="t('searchAllLearningPathsDescription')"
|
||||
icon="mdi-magnify"
|
||||
class="fill-height"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.grey-bg-card {
|
||||
background-color: #f6faf2;
|
||||
border: 2px solid #0e6942;
|
||||
}
|
||||
</style>
|
||||
|
|
63
frontend/src/components/ButtonWithConfirmation.vue
Normal file
63
frontend/src/components/ButtonWithConfirmation.vue
Normal file
|
@ -0,0 +1,63 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
text: string;
|
||||
prependIcon?: string;
|
||||
appendIcon?: string;
|
||||
confirmQueryText: string;
|
||||
variant?: "flat" | "text" | "elevated" | "tonal" | "outlined" | "plain" | undefined;
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ (e: "confirm"): void }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
function confirm(): void {
|
||||
emit("confirm");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog max-width="500">
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
v-bind="activatorProps"
|
||||
:text="props.text"
|
||||
:prependIcon="props.prependIcon"
|
||||
:appendIcon="props.appendIcon"
|
||||
:variant="props.variant"
|
||||
:color="color"
|
||||
:disabled="props.disabled"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card :title="t('confirmDialogTitle')">
|
||||
<v-card-text>
|
||||
{{ props.confirmQueryText }}
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn
|
||||
:text="t('yes')"
|
||||
@click="
|
||||
confirm();
|
||||
isActive.value = false;
|
||||
"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
:text="t('cancel')"
|
||||
@click="isActive.value = false"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -31,4 +31,9 @@
|
|||
></v-text-field>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.search-field {
|
||||
width: 25%;
|
||||
min-width: 300px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -53,9 +53,9 @@
|
|||
white-space: normal;
|
||||
}
|
||||
.results-grid {
|
||||
margin: 20px;
|
||||
margin: 20px auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
|
|
@ -7,13 +7,17 @@
|
|||
|
||||
// Import assets
|
||||
import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg";
|
||||
import { useLocale } from "vuetify";
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { current: vuetifyLocale } = useLocale();
|
||||
|
||||
const role = auth.authState.activeRole;
|
||||
const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable
|
||||
|
||||
const name: string = auth.authState.user!.profile.name!;
|
||||
const username = auth.authState.user!.profile.preferred_username!;
|
||||
const email = auth.authState.user!.profile.email;
|
||||
const initials: string = name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
|
@ -30,6 +34,7 @@
|
|||
// Logic to change the language of the website to the selected language
|
||||
function changeLanguage(langCode: string): void {
|
||||
locale.value = langCode;
|
||||
vuetifyLocale.value = langCode;
|
||||
localStorage.setItem("user-lang", langCode);
|
||||
}
|
||||
|
||||
|
@ -90,31 +95,34 @@
|
|||
<!-- >-->
|
||||
<!-- {{ t("discussions") }}-->
|
||||
<!-- </v-btn>-->
|
||||
<v-menu open-on-hover>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon
|
||||
variant="text"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-translate"
|
||||
size="small"
|
||||
color="#0e6942"
|
||||
></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(language, index) in languages"
|
||||
:key="index"
|
||||
@click="changeLanguage(language.code)"
|
||||
>
|
||||
<v-list-item-title>{{ language.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-toolbar-items>
|
||||
<v-menu
|
||||
open-on-hover
|
||||
open-on-click
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon
|
||||
variant="text"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-translate"
|
||||
size="small"
|
||||
color="#0e6942"
|
||||
></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(language, index) in languages"
|
||||
:key="index"
|
||||
@click="changeLanguage(language.code)"
|
||||
>
|
||||
<v-list-item-title>{{ language.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-dialog max-width="500">
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
|
@ -158,12 +166,48 @@
|
|||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
<v-avatar
|
||||
size="large"
|
||||
color="#0e6942"
|
||||
class="user-button"
|
||||
>{{ initials }}</v-avatar
|
||||
>
|
||||
<v-menu min-width="200px">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
icon
|
||||
v-bind="props"
|
||||
>
|
||||
<v-avatar
|
||||
color="#0e6942"
|
||||
size="large"
|
||||
class="user-button"
|
||||
>
|
||||
<span>{{ initials }}</span>
|
||||
</v-avatar>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<div class="mx-auto text-center">
|
||||
<v-avatar
|
||||
color="#0e6942"
|
||||
size="large"
|
||||
class="user-button mb-3"
|
||||
>
|
||||
<span>{{ initials }}</span>
|
||||
</v-avatar>
|
||||
<h3>{{ name }}</h3>
|
||||
<p class="text-caption mt-1">{{ username }}</p>
|
||||
<p class="text-caption mt-1">{{ email }}</p>
|
||||
<v-divider class="my-3"></v-divider>
|
||||
<v-btn
|
||||
variant="text"
|
||||
rounded
|
||||
append-icon="mdi-logout"
|
||||
@click="performLogout"
|
||||
to="/login"
|
||||
>{{ t("logout") }}</v-btn
|
||||
>
|
||||
<v-divider class="my-3"></v-divider>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
|
@ -248,6 +292,12 @@
|
|||
text-transform: none;
|
||||
}
|
||||
|
||||
.translate-button {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.menu {
|
||||
display: none;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import type { AnswersResponse } from "@/controllers/answers";
|
||||
import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer";
|
||||
import authService from "@/services/auth/auth-service";
|
||||
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||
|
||||
const props = defineProps<{
|
||||
question: QuestionDTO;
|
||||
|
@ -80,7 +81,7 @@
|
|||
{{ question.content }}
|
||||
</div>
|
||||
<div
|
||||
v-if="authService.authState.activeRole === 'teacher'"
|
||||
v-if="authService.authState.activeRole === AccountType.Teacher"
|
||||
class="answer-input-container"
|
||||
>
|
||||
<input
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<div v-if="isError">
|
||||
<v-empty-state
|
||||
icon="mdi-alert-circle-outline"
|
||||
:text="errorMessage"
|
||||
:text="t(errorMessage)"
|
||||
:title="t('error_title')"
|
||||
></v-empty-state>
|
||||
</div>
|
||||
|
|
|
@ -1,49 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { deadlineRules } from "@/utils/assignment-rules.ts";
|
||||
|
||||
const date = ref("");
|
||||
const time = ref("23:59");
|
||||
const emit = defineEmits(["update:deadline"]);
|
||||
const emit = defineEmits<(e: "update:deadline", value: Date) => void>();
|
||||
|
||||
const formattedDeadline = computed(() => {
|
||||
if (!date.value || !time.value) return "";
|
||||
return `${date.value} ${time.value}`;
|
||||
});
|
||||
const datetime = ref("");
|
||||
|
||||
function updateDeadline(): void {
|
||||
if (date.value && time.value) {
|
||||
emit("update:deadline", formattedDeadline.value);
|
||||
// Watch the datetime value and emit the update
|
||||
watch(datetime, (val) => {
|
||||
const newDate = new Date(val);
|
||||
if (!isNaN(newDate.getTime())) {
|
||||
emit("update:deadline", newDate);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="date"
|
||||
label="Select Deadline Date"
|
||||
type="date"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="deadlineRules"
|
||||
required
|
||||
@update:modelValue="updateDeadline"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="time"
|
||||
label="Select Deadline Time"
|
||||
type="time"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
@update:modelValue="updateDeadline"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
</div>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="datetime"
|
||||
type="datetime-local"
|
||||
label="Select Deadline"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="deadlineRules"
|
||||
required
|
||||
/>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -37,6 +37,33 @@ export abstract class BaseController {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a POST-request with a form-data body with the given file.
|
||||
*
|
||||
* @param path Relative path in the api to send the request to.
|
||||
* @param formFieldName The name of the form field in which the file should be.
|
||||
* @param file The file to upload.
|
||||
* @param queryParams The query parameters.
|
||||
* @returns The response the POST request generated.
|
||||
*/
|
||||
protected async postFile<T>(
|
||||
path: string,
|
||||
formFieldName: string,
|
||||
file: File,
|
||||
queryParams?: QueryParams,
|
||||
): Promise<T> {
|
||||
const formData = new FormData();
|
||||
formData.append(formFieldName, file);
|
||||
const response = await apiClient.post<T>(this.absolutePathFor(path), formData, {
|
||||
params: queryParams,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
BaseController.assertSuccessResponse(response);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async delete<T>(path: string, queryParams?: QueryParams): Promise<T> {
|
||||
const response = await apiClient.delete<T>(this.absolutePathFor(path), { params: queryParams });
|
||||
BaseController.assertSuccessResponse(response);
|
||||
|
|
|
@ -14,4 +14,16 @@ export class LearningObjectController extends BaseController {
|
|||
async getHTML(hruid: string, language: Language, version: number): Promise<Document> {
|
||||
return this.get<Document>(`/${hruid}/html`, { language, version }, "document");
|
||||
}
|
||||
|
||||
async getAllAdministratedBy(admin: string): Promise<LearningObject[]> {
|
||||
return this.get<LearningObject[]>("/", { admin });
|
||||
}
|
||||
|
||||
async upload(learningObjectZip: File): Promise<LearningObject> {
|
||||
return this.postFile<LearningObject>("/", "learningObject", learningObjectZip);
|
||||
}
|
||||
|
||||
async deleteLearningObject(hruid: string, language: Language, version: number): Promise<LearningObject> {
|
||||
return this.delete<LearningObject>(`/${hruid}`, { language, version });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { BaseController } from "@/controllers/base-controller.ts";
|
||||
import { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import { single } from "@/utils/response-assertions.ts";
|
||||
import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto.ts";
|
||||
import { LearningPath } from "@/data-objects/learning-paths/learning-path";
|
||||
import { NotFoundException } from "@/exception/not-found-exception";
|
||||
import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
|
||||
export class LearningPathController extends BaseController {
|
||||
constructor() {
|
||||
|
@ -24,10 +24,13 @@ export class LearningPathController extends BaseController {
|
|||
assignmentNo: forGroup?.assignmentNo,
|
||||
classId: forGroup?.classId,
|
||||
});
|
||||
return LearningPath.fromDTO(single(dtos));
|
||||
if (dtos.length === 0) {
|
||||
throw new NotFoundException("learningPathNotFound");
|
||||
}
|
||||
return LearningPath.fromDTO(dtos[0]);
|
||||
}
|
||||
async getAllByTheme(theme: string): Promise<LearningPath[]> {
|
||||
const dtos = await this.get<LearningPathDTO[]>("/", { theme });
|
||||
async getAllByThemeAndLanguage(theme: string, language: Language): Promise<LearningPath[]> {
|
||||
const dtos = await this.get<LearningPathDTO[]>("/", { theme, language });
|
||||
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
||||
}
|
||||
|
||||
|
@ -36,4 +39,20 @@ export class LearningPathController extends BaseController {
|
|||
const dtos = await this.get<LearningPathDTO[]>("/", query);
|
||||
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
||||
}
|
||||
|
||||
async getAllByAdminRaw(admin: string): Promise<LearningPathDTO[]> {
|
||||
return await this.get<LearningPathDTO[]>("/", { admin });
|
||||
}
|
||||
|
||||
async postLearningPath(learningPath: Partial<LearningPathDTO>): Promise<LearningPathDTO> {
|
||||
return await this.post<LearningPathDTO>("/", learningPath);
|
||||
}
|
||||
|
||||
async putLearningPath(learningPath: Partial<LearningPathDTO>): Promise<LearningPathDTO> {
|
||||
return await this.put<LearningPathDTO>(`/${learningPath.hruid}/${learningPath.language}`, learningPath);
|
||||
}
|
||||
|
||||
async deleteLearningPath(hruid: string, language: string): Promise<LearningPathDTO> {
|
||||
return await this.delete<LearningPathDTO>(`/${hruid}/${language}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { BaseController } from "@/controllers/base-controller.ts";
|
||||
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
import type { AssignmentsResponse } from "./assignments";
|
||||
|
@ -45,10 +44,6 @@ export class TeacherController extends BaseController {
|
|||
return this.get<StudentsResponse>(`/${username}/students`, { full });
|
||||
}
|
||||
|
||||
async getQuestions(username: string, full = false): Promise<QuestionsResponse> {
|
||||
return this.get<QuestionsResponse>(`/${username}/questions`, { full });
|
||||
}
|
||||
|
||||
async getStudentJoinRequests(username: string, classId: string): Promise<JoinRequestsResponse> {
|
||||
return this.get<JoinRequestsResponse>(`/${username}/joinRequests/${classId}`);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Language } from "@/data-objects/language.ts";
|
||||
import type { LearningPathNodeDTO } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { LearningObjectNode as LearningPathNodeDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
|
||||
export class LearningPathNode {
|
||||
public readonly learningobjectHruid: string;
|
||||
|
@ -14,7 +14,7 @@ export class LearningPathNode {
|
|||
learningobjectHruid: string;
|
||||
version: number;
|
||||
language: Language;
|
||||
transitions: { next: LearningPathNode; default: boolean }[];
|
||||
transitions: { next: LearningPathNode; default?: boolean }[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
done?: boolean;
|
||||
|
@ -22,7 +22,7 @@ export class LearningPathNode {
|
|||
this.learningobjectHruid = options.learningobjectHruid;
|
||||
this.version = options.version;
|
||||
this.language = options.language;
|
||||
this.transitions = options.transitions;
|
||||
this.transitions = options.transitions.map((it) => ({ next: it.next, default: it.default ?? false }));
|
||||
this.createdAt = options.createdAt;
|
||||
this.updatedAt = options.updatedAt;
|
||||
this.done = options.done || false;
|
||||
|
@ -50,8 +50,8 @@ export class LearningPathNode {
|
|||
return undefined;
|
||||
})
|
||||
.filter((it) => it !== undefined),
|
||||
createdAt: new Date(dto.created_at),
|
||||
updatedAt: new Date(dto.updatedAt),
|
||||
createdAt: dto.created_at ? new Date(dto.created_at) : new Date(),
|
||||
updatedAt: dto.updatedAt ? new Date(dto.updatedAt) : new Date(),
|
||||
done: dto.done,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { Language } from "@/data-objects/language.ts";
|
||||
import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts";
|
||||
import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto.ts";
|
||||
import type { LearningObjectNode, LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
|
||||
export interface LearningPathNodeDTO {
|
||||
_id: string;
|
||||
|
@ -77,20 +77,26 @@ export class LearningPath {
|
|||
hruid: dto.hruid,
|
||||
title: dto.title,
|
||||
description: dto.description,
|
||||
amountOfNodes: dto.num_nodes,
|
||||
amountOfNodesLeft: dto.num_nodes_left,
|
||||
amountOfNodes: dto.num_nodes ?? dto.nodes.length,
|
||||
amountOfNodesLeft: dto.num_nodes_left ?? dto.nodes.length,
|
||||
keywords: dto.keywords.split(" "),
|
||||
targetAges: { min: dto.min_age, max: dto.max_age },
|
||||
targetAges: {
|
||||
min: dto.min_age ?? NaN,
|
||||
max: dto.max_age ?? NaN,
|
||||
},
|
||||
startNode: LearningPathNode.fromDTOAndOtherNodes(LearningPath.getStartNode(dto), dto.nodes),
|
||||
image: dto.image,
|
||||
});
|
||||
}
|
||||
|
||||
static getStartNode(dto: LearningPathDTO): LearningPathNodeDTO {
|
||||
static getStartNode(dto: LearningPathDTO): LearningObjectNode {
|
||||
const startNodeDtos = dto.nodes.filter((it) => it.start_node === true);
|
||||
if (startNodeDtos.length < 1) {
|
||||
// The learning path has no starting node -> use the first node.
|
||||
return dto.nodes[0];
|
||||
if (dto.nodes.length > 0) {
|
||||
return dto.nodes[0];
|
||||
}
|
||||
throw new Error("emptyLearningPath");
|
||||
} // The learning path has 1 or more starting nodes -> use the first start node.
|
||||
return startNodeDtos[0];
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.",
|
||||
"invalidFormat": "Ungültiges Format",
|
||||
"submitCode": "senden",
|
||||
"submit": "senden",
|
||||
"members": "Mitglieder",
|
||||
"themes": "Themen",
|
||||
"choose-theme": "Wählen Sie ein Thema",
|
||||
|
@ -68,10 +69,10 @@
|
|||
"pick-class": "Wählen Sie eine klasse",
|
||||
"choose-students": "Studenten auswählen",
|
||||
"create-group": "Gruppe erstellen",
|
||||
"class": "klasse",
|
||||
"class": "Klasse",
|
||||
"delete": "löschen",
|
||||
"view-assignment": "Auftrag anzeigen",
|
||||
"code": "code",
|
||||
"code": "Code",
|
||||
"invitations": "Einladungen",
|
||||
"createClass": "Klasse erstellen",
|
||||
"createClassInstructions": "Geben Sie einen Namen für Ihre Klasse ein und klicken Sie auf „Erstellen“. Es erscheint ein Fenster mit einem Code, den Sie kopieren können. Geben Sie diesen Code an Ihre Schüler weiter und sie können Ihrer Klasse beitreten.",
|
||||
|
@ -83,7 +84,7 @@
|
|||
"onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
|
||||
"close": "schließen",
|
||||
"copied": "kopiert!",
|
||||
"accept": "akzeptieren",
|
||||
"accept": "Akzeptieren",
|
||||
"deny": "ablehnen",
|
||||
"sent": "sent",
|
||||
"failed": "fehlgeschlagen",
|
||||
|
@ -104,13 +105,12 @@
|
|||
"assignLearningPath": "Als Aufgabe geben",
|
||||
"group": "Gruppe",
|
||||
"description": "Beschreibung",
|
||||
"no-submission": "keine vorlage",
|
||||
"submission": "Einreichung",
|
||||
"progress": "Fortschritte",
|
||||
"remove": "entfernen",
|
||||
"students": "Studenten",
|
||||
"classJoinRequests": "Beitrittsanfragen",
|
||||
"reject": "ablehnen",
|
||||
"reject": "Ablehnen",
|
||||
"areusure": "Sind Sie sicher?",
|
||||
"yes": "ja",
|
||||
"teachers": "Lehrer",
|
||||
|
@ -121,5 +121,49 @@
|
|||
"invite": "einladen",
|
||||
"assignmentIndicator": "AUFGABE",
|
||||
"searchAllLearningPathsTitle": "Alle Lernpfade durchsuchen",
|
||||
"searchAllLearningPathsDescription": "Nicht gefunden, was Sie gesucht haben? Klicken Sie hier, um unsere gesamte Lernpfad-Datenbank zu durchsuchen."
|
||||
"searchAllLearningPathsDescription": "Nicht gefunden, was Sie gesucht haben? Klicken Sie hier, um unsere gesamte Lernpfad-Datenbank zu durchsuchen.",
|
||||
"no-students-found": "Diese Klasse hat keine Schüler.",
|
||||
"no-invitations-found": "Sie haben keine ausstehenden Einladungen.",
|
||||
"no-join-requests-found": "Es gibt keine ausstehenden Beitrittsanfragen für diese Klasse.",
|
||||
"no-classes-found": "Sie sind noch keinem Kurs beigetreten.",
|
||||
"classCreated": "Klasse erstellt!",
|
||||
"success": "Erfolg",
|
||||
"submitted": "eingereicht",
|
||||
"see-submission": "Einsendung anzeigen",
|
||||
"view-submissions": "Einsendungen anzeigen",
|
||||
"valid-username": "Bitte geben Sie einen gültigen Benutzernamen ein",
|
||||
"creationFailed": "Erstellung fehlgeschlagen, bitte versuchen Sie es erneut",
|
||||
"no-assignments": "Derzeit gibt es keine Zuweisungen.",
|
||||
"deadline": "deadline",
|
||||
"learningObjects": "Lernobjekte",
|
||||
"learningPaths": "Lernpfade",
|
||||
"hruid": "HRUID",
|
||||
"language": "Sprache",
|
||||
"version": "Version",
|
||||
"previewFor": "Vorschau für ",
|
||||
"upload": "Hochladen",
|
||||
"learningObjectUploadTitle": "Lernobjekt hochladen",
|
||||
"uploadFailed": "Hochladen fehlgeschlagen",
|
||||
"invalidZip": "Dies ist keine gültige ZIP-Datei.",
|
||||
"emptyZip": "Diese ZIP-Datei ist leer.",
|
||||
"missingMetadata": "Dieses Lernobjekt enthält keine metadata.json-Datei.",
|
||||
"missingContent": "Dieses Lernobjekt enthält keine content.*-Datei.",
|
||||
"open": "öffnen",
|
||||
"editLearningPath": "Lernpfad bearbeiten",
|
||||
"newLearningPath": "Neuen Lernpfad erstellen",
|
||||
"saveChanges": "Änderungen speichern",
|
||||
"newLearningObject": "Lernobjekt hochladen",
|
||||
"confirmDialogTitle": "Bitte bestätigen",
|
||||
"learningPathDeleteQuery": "Möchten Sie diesen Lernpfad wirklich löschen?",
|
||||
"learningObjectDeleteQuery": "Möchten Sie dieses Lernobjekt wirklich löschen?",
|
||||
"learningPathCantModifyId": "Der HRUID oder die Sprache eines Lernpfads kann nicht geändert werden.",
|
||||
"error": "Fehler",
|
||||
"ownLearningContentTitle": "Eigene Lerninhalte",
|
||||
"ownLearningContentDescription": "Erstellen und verwalten Sie eigene Lernobjekte und Lernpfade. Nur für fortgeschrittene Nutzer.",
|
||||
"learningPathNotFound": "Dieser Lernpfad konnte nicht gefunden werden.",
|
||||
"emptyLearningPath": "Dieser Lernpfad enthält keine Lernobjekte.",
|
||||
"pathContainsNonExistingLearningObjects": "Mindestens eines der in diesem Pfad referenzierten Lernobjekte existiert nicht.",
|
||||
"targetAgesMandatory": "Zielalter müssen angegeben werden.",
|
||||
"hintRemoveIfUnconditionalTransition": "(entfernen, wenn dies ein bedingungsloser Übergang sein soll)",
|
||||
"hintKeywordsSeparatedBySpaces": "Schlüsselwörter durch Leerzeichen getrennt"
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
|
||||
"invalidFormat": "Invalid format.",
|
||||
"submitCode": "submit",
|
||||
"submit": "submit",
|
||||
"members": "Members",
|
||||
"themes": "Themes",
|
||||
"choose-theme": "Select a theme",
|
||||
|
@ -68,21 +69,21 @@
|
|||
"pick-class": "Pick a class",
|
||||
"choose-students": "Select students",
|
||||
"create-group": "Create group",
|
||||
"class": "class",
|
||||
"class": "Class",
|
||||
"delete": "delete",
|
||||
"view-assignment": "View assignment",
|
||||
"code": "code",
|
||||
"invitations": "invitations",
|
||||
"createClass": "create class",
|
||||
"code": "Code",
|
||||
"invitations": "Invitations",
|
||||
"createClass": "Create class",
|
||||
"classname": "classname",
|
||||
"EnterNameOfClass": "Enter a classname.",
|
||||
"create": "create",
|
||||
"sender": "sender",
|
||||
"sender": "Sender",
|
||||
"nameIsMandatory": "classname is mandatory",
|
||||
"onlyUse": "only use letters, numbers, dashes (-) and underscores (_)",
|
||||
"close": "close",
|
||||
"copied": "copied!",
|
||||
"accept": "accept",
|
||||
"accept": "Accept",
|
||||
"deny": "deny",
|
||||
"createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join.",
|
||||
"sent": "sent",
|
||||
|
@ -103,17 +104,16 @@
|
|||
"assignLearningPath": "assign",
|
||||
"group": "Group",
|
||||
"description": "Description",
|
||||
"no-submission": "no submission",
|
||||
"submission": "Submission",
|
||||
"progress": "Progress",
|
||||
"created": "created",
|
||||
"remove": "remove",
|
||||
"students": "students",
|
||||
"classJoinRequests": "join requests",
|
||||
"reject": "reject",
|
||||
"students": "Students",
|
||||
"classJoinRequests": "Join requests",
|
||||
"reject": "Reject",
|
||||
"areusure": "Are you sure?",
|
||||
"yes": "yes",
|
||||
"teachers": "teachers",
|
||||
"teachers": "Teachers",
|
||||
"accepted": "accepted",
|
||||
"rejected": "rejected",
|
||||
"enterUsername": "enter the username of the teacher you would like to invite",
|
||||
|
@ -121,6 +121,50 @@
|
|||
"invite": "invite",
|
||||
"assignmentIndicator": "ASSIGNMENT",
|
||||
"searchAllLearningPathsTitle": "Search all learning paths",
|
||||
"not-in-group-message": "You are not part of a group yet",
|
||||
"searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths.",
|
||||
"not-in-group-message": "You are not part of a group yet"
|
||||
"no-students-found": "This class has no students.",
|
||||
"no-invitations-found": "You have no pending invitations.",
|
||||
"no-join-requests-found": "There are no pending join requests for this class.",
|
||||
"no-classes-found": "You are not yet part of a class.",
|
||||
"classCreated": "class created!",
|
||||
"success": "success",
|
||||
"submitted": "submitted",
|
||||
"see-submission": "view submission",
|
||||
"view-submissions": "view submissions",
|
||||
"valid-username": "please enter a valid username",
|
||||
"creationFailed": "creation failed, please try again",
|
||||
"no-assignments": "There are currently no assignments.",
|
||||
"deadline": "deadline",
|
||||
"learningObjects": "Learning objects",
|
||||
"learningPaths": "Learning paths",
|
||||
"hruid": "HRUID",
|
||||
"language": "Language",
|
||||
"version": "Version",
|
||||
"previewFor": "Preview for ",
|
||||
"upload": "Upload",
|
||||
"learningObjectUploadTitle": "Upload a learning object",
|
||||
"uploadFailed": "Upload failed",
|
||||
"invalidZip": "This is not a valid zip file.",
|
||||
"emptyZip": "This zip file is empty",
|
||||
"missingMetadata": "This learning object is missing a metadata.json file.",
|
||||
"missingContent": "This learning object is missing a content.* file.",
|
||||
"open": "open",
|
||||
"editLearningPath": "Edit learning path",
|
||||
"newLearningPath": "Create a new learning path",
|
||||
"saveChanges": "Save changes",
|
||||
"newLearningObject": "Upload learning object",
|
||||
"confirmDialogTitle": "Please confirm",
|
||||
"learningPathDeleteQuery": "Are you sure you want to delete this learning path?",
|
||||
"learningObjectDeleteQuery": "Are you sure you want to delete this learning object?",
|
||||
"learningPathCantModifyId": "The HRUID or language of a learning path cannot be modified.",
|
||||
"error": "Error",
|
||||
"ownLearningContentTitle": "Own learning content",
|
||||
"ownLearningContentDescription": "Create and administrate your own learning objects and learning paths. For advanced users only.",
|
||||
"learningPathNotFound": "This learning path could not be found.",
|
||||
"emptyLearningPath": "This learning path does not contain any learning objects.",
|
||||
"pathContainsNonExistingLearningObjects": "At least one of the learning objects referenced in this path does not exist.",
|
||||
"targetAgesMandatory": "Target ages must be specified.",
|
||||
"hintRemoveIfUnconditionalTransition": "(remove this if this should be an unconditional transition)",
|
||||
"hintKeywordsSeparatedBySpaces": "Keywords separated by spaces"
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
|
||||
"invalidFormat": "Format non valide.",
|
||||
"submitCode": "envoyer",
|
||||
"submit": "envoyer",
|
||||
"members": "Membres",
|
||||
"themes": "Thèmes",
|
||||
"choose-theme": "Choisis un thème",
|
||||
|
@ -68,22 +69,22 @@
|
|||
"pick-class": "Choisissez une classe",
|
||||
"choose-students": "Sélectionnez des élèves",
|
||||
"create-group": "Créer un groupe",
|
||||
"class": "classe",
|
||||
"class": "Classe",
|
||||
"delete": "supprimer",
|
||||
"view-assignment": "Voir le travail",
|
||||
"code": "code",
|
||||
"invitations": "invitations",
|
||||
"createClass": "créer une classe",
|
||||
"code": "Code",
|
||||
"invitations": "Invitations",
|
||||
"createClass": "Créer une classe",
|
||||
"createClassInstructions": "Entrez un nom pour votre classe et cliquez sur créer. Une fenêtre apparaît avec un code que vous pouvez copier. Donnez ce code à vos élèves et ils pourront rejoindre votre classe.",
|
||||
"classname": "nom de classe",
|
||||
"EnterNameOfClass": "saisir un nom de classe.",
|
||||
"create": "créer",
|
||||
"sender": "expéditeur",
|
||||
"sender": "Expéditeur",
|
||||
"nameIsMandatory": "le nom de classe est obligatoire",
|
||||
"onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
|
||||
"close": "fermer",
|
||||
"copied": "copié!",
|
||||
"accept": "accepter",
|
||||
"accept": "Accepter",
|
||||
"deny": "refuser",
|
||||
"sent": "envoyé",
|
||||
"failed": "échoué",
|
||||
|
@ -104,16 +105,16 @@
|
|||
"assignLearningPath": "donner comme tâche",
|
||||
"group": "Groupe",
|
||||
"description": "Description",
|
||||
"no-submission": "aucune soumission",
|
||||
"submission": "Soumission",
|
||||
"progress": "Progrès",
|
||||
"remove": "supprimer",
|
||||
"students": "étudiants",
|
||||
"classJoinRequests": "demandes d'adhésion",
|
||||
"reject": "rejeter",
|
||||
"students": "Étudiants",
|
||||
|
||||
"classJoinRequests": "Demandes d'adhésion",
|
||||
"reject": "Rejeter",
|
||||
"areusure": "Êtes-vous sûr?",
|
||||
"yes": "oui",
|
||||
"teachers": "enseignants",
|
||||
"teachers": "Enseignants",
|
||||
"accepted": "acceptée",
|
||||
"rejected": "rejetée",
|
||||
"enterUsername": "entrez le nom d'utilisateur de l'enseignant que vous souhaitez inviter",
|
||||
|
@ -121,5 +122,49 @@
|
|||
"invite": "inviter",
|
||||
"assignmentIndicator": "DEVOIR",
|
||||
"searchAllLearningPathsTitle": "Rechercher tous les parcours d'apprentissage",
|
||||
"searchAllLearningPathsDescription": "Vous n'avez pas trouvé ce que vous cherchiez ? Cliquez ici pour rechercher dans toute notre base de données de parcours d'apprentissage disponibles."
|
||||
"searchAllLearningPathsDescription": "Vous n'avez pas trouvé ce que vous cherchiez ? Cliquez ici pour rechercher dans toute notre base de données de parcours d'apprentissage disponibles.",
|
||||
"no-students-found": "Cette classe n'a pas d'élèves.",
|
||||
"no-invitations-found": "Vous n'avez aucune invitation en attente.",
|
||||
"no-join-requests-found": "Il n'y a aucune demande d'adhésion en attente pour cette classe.",
|
||||
"no-classes-found": "Vous ne faites pas encore partie d'une classe.",
|
||||
"classCreated": "Classe créée !",
|
||||
"success": "succès",
|
||||
"submitted": "soumis",
|
||||
"see-submission": "voir la soumission",
|
||||
"view-submissions": "voir les soumissions",
|
||||
"valid-username": "veuillez entrer un nom d'utilisateur valide",
|
||||
"creationFailed": "échec de la création, veuillez réessayer",
|
||||
"no-assignments": "Il n'y a actuellement aucun travail.",
|
||||
"deadline": "délai",
|
||||
"learningObjects": "Objets d’apprentissage",
|
||||
"learningPaths": "Parcours d’apprentissage",
|
||||
"hruid": "HRUID",
|
||||
"language": "Langue",
|
||||
"version": "Version",
|
||||
"previewFor": "Aperçu de ",
|
||||
"upload": "Téléverser",
|
||||
"learningObjectUploadTitle": "Téléverser un objet d’apprentissage",
|
||||
"uploadFailed": "Échec du téléversement",
|
||||
"invalidZip": "Ce n’est pas un fichier ZIP valide.",
|
||||
"emptyZip": "Ce fichier ZIP est vide.",
|
||||
"missingMetadata": "Il manque un fichier metadata.json à cet objet d’apprentissage.",
|
||||
"missingContent": "Il manque un fichier content.* à cet objet d’apprentissage.",
|
||||
"open": "ouvrir",
|
||||
"editLearningPath": "Modifier le parcours",
|
||||
"newLearningPath": "Créer un nouveau parcours",
|
||||
"saveChanges": "Enregistrer les modifications",
|
||||
"newLearningObject": "Téléverser un objet d’apprentissage",
|
||||
"confirmDialogTitle": "Veuillez confirmer",
|
||||
"learningPathDeleteQuery": "Voulez-vous vraiment supprimer ce parcours d’apprentissage ?",
|
||||
"learningObjectDeleteQuery": "Voulez-vous vraiment supprimer cet objet d’apprentissage ?",
|
||||
"learningPathCantModifyId": "Le HRUID ou la langue d’un parcours ne peuvent pas être modifiés.",
|
||||
"error": "Erreur",
|
||||
"ownLearningContentTitle": "Contenu d’apprentissage personnel",
|
||||
"ownLearningContentDescription": "Créez et gérez vos propres objets et parcours d’apprentissage. Réservé aux utilisateurs avancés.",
|
||||
"learningPathNotFound": "Ce parcours d'apprentissage est introuvable.",
|
||||
"emptyLearningPath": "Ce parcours d'apprentissage ne contient aucun objet d'apprentissage.",
|
||||
"pathContainsNonExistingLearningObjects": "Au moins un des objets d’apprentissage référencés dans ce chemin n’existe pas.",
|
||||
"targetAgesMandatory": "Les âges cibles doivent être spécifiés.",
|
||||
"hintRemoveIfUnconditionalTransition": "(supprimer ceci s’il s’agit d’une transition inconditionnelle)",
|
||||
"hintKeywordsSeparatedBySpaces": "Mots-clés séparés par des espaces"
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
|
||||
"invalidFormat": "Ongeldig formaat.",
|
||||
"submitCode": "verzenden",
|
||||
"submit": "verzenden",
|
||||
"members": "Leden",
|
||||
"themes": "Lesthema's",
|
||||
"choose-theme": "Kies een thema",
|
||||
|
@ -68,22 +69,22 @@
|
|||
"pick-class": "Kies een klas",
|
||||
"choose-students": "Studenten selecteren",
|
||||
"create-group": "Groep aanmaken",
|
||||
"class": "klas",
|
||||
"class": "Klas",
|
||||
"delete": "verwijderen",
|
||||
"view-assignment": "Opdracht bekijken",
|
||||
"code": "code",
|
||||
"invitations": "uitnodigingen",
|
||||
"createClass": "klas aanmaken",
|
||||
"code": "Code",
|
||||
"invitations": "Uitnodigingen",
|
||||
"createClass": "Klas aanmaken",
|
||||
"createClassInstructions": "Voer een naam in voor je klas en klik op create. Er verschijnt een venster met een code die je kunt kopiëren. Geef deze code aan je leerlingen en ze kunnen deelnemen aan je klas.",
|
||||
"classname": "klasnaam",
|
||||
"EnterNameOfClass": "Geef een klasnaam op.",
|
||||
"create": "aanmaken",
|
||||
"sender": "afzender",
|
||||
"sender": "Afzender",
|
||||
"nameIsMandatory": "klasnaam is verplicht",
|
||||
"onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
|
||||
"close": "sluiten",
|
||||
"copied": "gekopieerd!",
|
||||
"accept": "accepteren",
|
||||
"accept": "Accepteren",
|
||||
"deny": "weigeren",
|
||||
"sent": "verzonden",
|
||||
"failed": "mislukt",
|
||||
|
@ -104,16 +105,15 @@
|
|||
"assignLearningPath": "Als opdracht geven",
|
||||
"group": "Groep",
|
||||
"description": "Beschrijving",
|
||||
"no-submission": "geen indiening",
|
||||
"submission": "Indiening",
|
||||
"progress": "Vooruitgang",
|
||||
"remove": "verwijder",
|
||||
"students": "studenten",
|
||||
"classJoinRequests": "deelname verzoeken",
|
||||
"reject": "weiger",
|
||||
"students": "Studenten",
|
||||
"classJoinRequests": "Deelname verzoeken",
|
||||
"reject": "Weiger",
|
||||
"areusure": "Bent u zeker?",
|
||||
"yes": "ja",
|
||||
"teachers": "leerkrachten",
|
||||
"teachers": "Leerkrachten",
|
||||
"accepted": "geaccepteerd",
|
||||
"rejected": "geweigerd",
|
||||
"enterUsername": "vul de gebruikersnaam van de leerkracht die je wilt uitnodigen in",
|
||||
|
@ -121,5 +121,49 @@
|
|||
"invite": "uitnodigen",
|
||||
"assignmentIndicator": "OPDRACHT",
|
||||
"searchAllLearningPathsTitle": "Alle leerpaden doorzoeken",
|
||||
"searchAllLearningPathsDescription": "Niet gevonden waar je naar op zoek was? Klik hier om onze volledige databank van beschikbare leerpaden te doorzoeken."
|
||||
"searchAllLearningPathsDescription": "Niet gevonden waar je naar op zoek was? Klik hier om onze volledige databank van beschikbare leerpaden te doorzoeken.",
|
||||
"no-students-found": "Deze klas heeft geen leerlingen.",
|
||||
"no-invitations-found": "U heeft geen openstaande uitnodigingen.",
|
||||
"no-join-requests-found": "Er zijn geen openstaande verzoeken om lid te worden van deze klas.",
|
||||
"no-classes-found": "U maakt nog geen deel uit van een klas.",
|
||||
"classCreated": "Klas aangemaakt!",
|
||||
"success": "succes",
|
||||
"submitted": "ingediend",
|
||||
"see-submission": "inzending bekijken",
|
||||
"view-submissions": "inzendingen bekijken",
|
||||
"valid-username": "voer een geldige gebruikersnaam in",
|
||||
"creationFailed": "aanmaak mislukt, probeer het opnieuw",
|
||||
"no-assignments": "Er zijn momenteel geen opdrachten.",
|
||||
"deadline": "deadline",
|
||||
"learningObjects": "Leerobjecten",
|
||||
"learningPaths": "Leerpaden",
|
||||
"hruid": "HRUID",
|
||||
"language": "Taal",
|
||||
"version": "Versie",
|
||||
"previewFor": "Voorbeeld van ",
|
||||
"upload": "Uploaden",
|
||||
"learningObjectUploadTitle": "Leerobject uploaden",
|
||||
"uploadFailed": "Upload mislukt",
|
||||
"invalidZip": "Dit is geen geldig zipbestand.",
|
||||
"emptyZip": "Dit zipbestand is leeg.",
|
||||
"missingMetadata": "Dit leerobject mist een metadata.json-bestand.",
|
||||
"missingContent": "Dit leerobject mist een content.*-bestand.",
|
||||
"open": "openen",
|
||||
"editLearningPath": "Leerpad bewerken",
|
||||
"newLearningPath": "Nieuw leerpad aanmaken",
|
||||
"saveChanges": "Wijzigingen opslaan",
|
||||
"newLearningObject": "Leerobject uploaden",
|
||||
"confirmDialogTitle": "Bevestig alstublieft",
|
||||
"learningPathDeleteQuery": "Weet u zeker dat u dit leerpad wilt verwijderen?",
|
||||
"learningObjectDeleteQuery": "Weet u zeker dat u dit leerobject wilt verwijderen?",
|
||||
"learningPathCantModifyId": "De HRUID of taal van een leerpad kan niet worden gewijzigd.",
|
||||
"error": "Fout",
|
||||
"ownLearningContentTitle": "Eigen leerinhoud",
|
||||
"ownLearningContentDescription": "Maak en beheer je eigen leerobjecten en leerpads. Alleen voor gevorderde gebruikers.",
|
||||
"learningPathNotFound": "Dit leerpad kon niet gevonden worden.",
|
||||
"emptyLearningPath": "Dit leerpad bevat geen leerobjecten.",
|
||||
"pathContainsNonExistingLearningObjects": "Ten minste één van de leerobjecten in dit pad bestaat niet.",
|
||||
"targetAgesMandatory": "Doelleeftijden moeten worden opgegeven.",
|
||||
"hintRemoveIfUnconditionalTransition": "(verwijder dit voor onvoorwaardelijke overgangen)",
|
||||
"hintKeywordsSeparatedBySpaces": "Trefwoorden gescheiden door spaties"
|
||||
}
|
||||
|
|
|
@ -7,15 +7,20 @@ import * as components from "vuetify/components";
|
|||
import * as directives from "vuetify/directives";
|
||||
import i18n from "./i18n/i18n.ts";
|
||||
|
||||
// JSON-editor
|
||||
import JsonEditorVue from "json-editor-vue";
|
||||
|
||||
// Components
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { aliases, mdi } from "vuetify/iconsets/mdi";
|
||||
import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query";
|
||||
import { de, en, fr, nl } from "vuetify/locale";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
app.use(JsonEditorVue, {});
|
||||
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
|
@ -32,6 +37,11 @@ const vuetify = createVuetify({
|
|||
mdi,
|
||||
},
|
||||
},
|
||||
locale: {
|
||||
locale: i18n.global.locale,
|
||||
fallback: "en",
|
||||
messages: { nl, en, de, fr },
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
|
|
|
@ -15,6 +15,7 @@ import { invalidateAllGroupKeys } from "./groups";
|
|||
import { invalidateAllSubmissionKeys } from "./submissions";
|
||||
import type { TeachersResponse } from "@/controllers/teachers";
|
||||
import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations";
|
||||
import { studentClassesQueryKey } from "@/queries/students.ts";
|
||||
|
||||
const classController = new ClassController();
|
||||
|
||||
|
@ -171,6 +172,8 @@ export function useClassDeleteStudentMutation(): UseMutationReturnType<
|
|||
await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) });
|
||||
await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, true) });
|
||||
await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, false) });
|
||||
await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, false) });
|
||||
await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, true) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import { type MaybeRefOrGetter, toValue } from "vue";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query";
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseMutationReturnType,
|
||||
type UseQueryReturnType,
|
||||
} from "@tanstack/vue-query";
|
||||
import { getLearningObjectController } from "@/controllers/controllers.ts";
|
||||
import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts";
|
||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { AxiosError } from "axios";
|
||||
|
||||
export const LEARNING_OBJECT_KEY = "learningObject";
|
||||
const learningObjectController = getLearningObjectController();
|
||||
|
@ -24,15 +31,15 @@ export function useLearningObjectMetadataQuery(
|
|||
}
|
||||
|
||||
export function useLearningObjectHTMLQuery(
|
||||
hruid: MaybeRefOrGetter<string>,
|
||||
language: MaybeRefOrGetter<Language>,
|
||||
version: MaybeRefOrGetter<number>,
|
||||
hruid: MaybeRefOrGetter<string | undefined>,
|
||||
language: MaybeRefOrGetter<Language | undefined>,
|
||||
version: MaybeRefOrGetter<number | undefined>,
|
||||
): UseQueryReturnType<Document, Error> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_OBJECT_KEY, "html", hruid, language, version],
|
||||
queryFn: async () => {
|
||||
const [hruidVal, languageVal, versionVal] = [toValue(hruid), toValue(language), toValue(version)];
|
||||
return learningObjectController.getHTML(hruidVal, languageVal, versionVal);
|
||||
return learningObjectController.getHTML(hruidVal!, languageVal!, versionVal!);
|
||||
},
|
||||
enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)) && Boolean(toValue(version)),
|
||||
});
|
||||
|
@ -55,3 +62,49 @@ export function useLearningObjectListForPathQuery(
|
|||
enabled: () => Boolean(toValue(learningPath)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useLearningObjectListForAdminQuery(
|
||||
admin: MaybeRefOrGetter<string | undefined>,
|
||||
): UseQueryReturnType<LearningObject[], Error> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_OBJECT_KEY, "forAdmin", admin],
|
||||
queryFn: async () => {
|
||||
const adminVal = toValue(admin);
|
||||
return await learningObjectController.getAllAdministratedBy(adminVal!);
|
||||
},
|
||||
enabled: () => toValue(admin) !== undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUploadLearningObjectMutation(): UseMutationReturnType<
|
||||
LearningObject,
|
||||
AxiosError,
|
||||
{ learningObjectZip: File },
|
||||
unknown
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ learningObjectZip }) => await learningObjectController.upload(learningObjectZip),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: [LEARNING_OBJECT_KEY, "forAdmin"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteLearningObjectMutation(): UseMutationReturnType<
|
||||
LearningObject,
|
||||
AxiosError,
|
||||
{ hruid: string; language: Language; version: number },
|
||||
unknown
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ hruid, language, version }) =>
|
||||
await learningObjectController.deleteLearningObject(hruid, language, version),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: [LEARNING_OBJECT_KEY, "forAdmin"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
import { type MaybeRefOrGetter, toValue } from "vue";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query";
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseMutationReturnType,
|
||||
type UseQueryReturnType,
|
||||
} from "@tanstack/vue-query";
|
||||
import { getLearningPathController } from "@/controllers/controllers";
|
||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { AxiosError } from "axios";
|
||||
import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path";
|
||||
|
||||
export const LEARNING_PATH_KEY = "learningPath";
|
||||
const learningPathController = getLearningPathController();
|
||||
|
@ -22,16 +30,69 @@ export function useGetLearningPathQuery(
|
|||
});
|
||||
}
|
||||
|
||||
export function useGetAllLearningPathsByThemeQuery(
|
||||
export function useGetAllLearningPathsByThemeAndLanguageQuery(
|
||||
theme: MaybeRefOrGetter<string>,
|
||||
language: MaybeRefOrGetter<Language>,
|
||||
): UseQueryReturnType<LearningPath[], Error> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme],
|
||||
queryFn: async () => learningPathController.getAllByTheme(toValue(theme)),
|
||||
queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme, language],
|
||||
queryFn: async () => learningPathController.getAllByThemeAndLanguage(toValue(theme), toValue(language)),
|
||||
enabled: () => Boolean(toValue(theme)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetAllLearningPathsByAdminQuery(
|
||||
admin: MaybeRefOrGetter<string | undefined>,
|
||||
): UseQueryReturnType<LearningPathDTO[], AxiosError> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_PATH_KEY, "getAllByAdmin", admin],
|
||||
queryFn: async () => learningPathController.getAllByAdminRaw(toValue(admin)!),
|
||||
enabled: () => Boolean(toValue(admin)),
|
||||
});
|
||||
}
|
||||
|
||||
export function usePostLearningPathMutation(): UseMutationReturnType<
|
||||
LearningPathDTO,
|
||||
AxiosError,
|
||||
{ learningPath: Partial<LearningPathDTO> },
|
||||
unknown
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ learningPath }) => learningPathController.postLearningPath(learningPath),
|
||||
onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function usePutLearningPathMutation(): UseMutationReturnType<
|
||||
LearningPathDTO,
|
||||
AxiosError,
|
||||
{ learningPath: Partial<LearningPathDTO> },
|
||||
unknown
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ learningPath }) => learningPathController.putLearningPath(learningPath),
|
||||
onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteLearningPathMutation(): UseMutationReturnType<
|
||||
LearningPathDTO,
|
||||
AxiosError,
|
||||
{ hruid: string; language: Language },
|
||||
unknown
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ hruid, language }) => learningPathController.deleteLearningPath(hruid, language),
|
||||
onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSearchLearningPathQuery(
|
||||
query: MaybeRefOrGetter<string | undefined>,
|
||||
language: MaybeRefOrGetter<string | undefined>,
|
||||
|
|
|
@ -33,7 +33,7 @@ function studentsQueryKey(full: boolean): [string, boolean] {
|
|||
function studentQueryKey(username: string): [string, string] {
|
||||
return ["student", username];
|
||||
}
|
||||
function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] {
|
||||
export function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] {
|
||||
return ["student-classes", username, full];
|
||||
}
|
||||
function studentAssignmentsQueryKey(username: string, full: boolean): [string, string, boolean] {
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts";
|
||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts";
|
||||
import type { AssignmentResponse, AssignmentsResponse } from "@/controllers/assignments";
|
||||
|
@ -38,10 +37,6 @@ function teacherStudentsQueryKey(username: string, full: boolean): [string, stri
|
|||
return ["teacher-students", username, full];
|
||||
}
|
||||
|
||||
function teacherQuestionsQueryKey(username: string, full: boolean): [string, string, boolean] {
|
||||
return ["teacher-questions", username, full];
|
||||
}
|
||||
|
||||
export function teacherClassJoinRequests(classId: string): [string, string] {
|
||||
return ["teacher-class-join-requests", classId];
|
||||
}
|
||||
|
@ -96,17 +91,6 @@ export function useTeacherStudentsQuery(
|
|||
});
|
||||
}
|
||||
|
||||
export function useTeacherQuestionsQuery(
|
||||
username: MaybeRefOrGetter<string | undefined>,
|
||||
full: MaybeRefOrGetter<boolean> = false,
|
||||
): UseQueryReturnType<QuestionsResponse, Error> {
|
||||
return useQuery({
|
||||
queryKey: computed(() => teacherQuestionsQueryKey(toValue(username)!, toValue(full))),
|
||||
queryFn: async () => teacherController.getQuestions(toValue(username)!, toValue(full)),
|
||||
enabled: () => Boolean(toValue(username)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useTeacherJoinRequestsQuery(
|
||||
username: MaybeRefOrGetter<string | undefined>,
|
||||
classId: MaybeRefOrGetter<string | undefined>,
|
||||
|
|
|
@ -14,6 +14,8 @@ import UserHomePage from "@/views/homepage/UserHomePage.vue";
|
|||
import SingleTheme from "@/views/SingleTheme.vue";
|
||||
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
|
||||
import authService from "@/services/auth/auth-service";
|
||||
import OwnLearningContentPage from "@/views/own-learning-content/OwnLearningContentPage.vue";
|
||||
import { allowRedirect, Redirect } from "@/utils/redirect.ts";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
@ -105,6 +107,12 @@ const router = createRouter({
|
|||
component: SingleDiscussion,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/my-content",
|
||||
name: "OwnLearningContentPage",
|
||||
component: OwnLearningContentPage,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/learningPath",
|
||||
children: [
|
||||
|
@ -143,7 +151,11 @@ router.beforeEach(async (to, _from, next) => {
|
|||
// Verify if user is logged in before accessing certain routes
|
||||
if (to.meta.requiresAuth) {
|
||||
if (!authService.isLoggedIn.value && !(await authService.loadUser())) {
|
||||
next("/login");
|
||||
const path = to.fullPath;
|
||||
if (allowRedirect(path)) {
|
||||
localStorage.setItem(Redirect.AFTER_LOGIN_KEY, path);
|
||||
}
|
||||
next(Redirect.LOGIN);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
|
12
frontend/src/utils/redirect.ts
Normal file
12
frontend/src/utils/redirect.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export enum Redirect {
|
||||
AFTER_LOGIN_KEY = "redirectAfterLogin",
|
||||
HOME = "/user",
|
||||
LOGIN = "/login",
|
||||
ROOT = "/",
|
||||
}
|
||||
|
||||
const NOT_ALLOWED_REDIRECTS = new Set<Redirect>([Redirect.HOME, Redirect.ROOT, Redirect.LOGIN]);
|
||||
|
||||
export function allowRedirect(path: string): boolean {
|
||||
return !NOT_ALLOWED_REDIRECTS.has(path as Redirect);
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
import { useI18n } from "vue-i18n";
|
||||
import { onMounted, ref, type Ref } from "vue";
|
||||
import auth from "../services/auth/auth-service.ts";
|
||||
import { Redirect } from "@/utils/redirect.ts";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -10,10 +11,20 @@
|
|||
|
||||
const errorMessage: Ref<string | null> = ref(null);
|
||||
|
||||
async function redirectPage(): Promise<void> {
|
||||
const redirectUrl = localStorage.getItem(Redirect.AFTER_LOGIN_KEY);
|
||||
if (redirectUrl) {
|
||||
localStorage.removeItem(Redirect.AFTER_LOGIN_KEY);
|
||||
await router.replace(redirectUrl);
|
||||
} else {
|
||||
await router.replace(Redirect.HOME);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await auth.handleLoginCallback();
|
||||
await router.replace("/user"); // Redirect to theme page
|
||||
await redirectPage();
|
||||
} catch (error) {
|
||||
errorMessage.value = `${t("loginUnexpectedError")}: ${error}`;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
alt="Dwengo logo"
|
||||
style="align-self: center"
|
||||
/>
|
||||
<h1>{{ t("homeTitle") }}</h1>
|
||||
<h1 class="h1">{{ t("homeTitle") }}</h1>
|
||||
<p class="info">
|
||||
{{ t("homeIntroduction1") }}
|
||||
</p>
|
||||
|
@ -84,7 +84,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="container_right">
|
||||
<v-menu open-on-hover>
|
||||
<v-menu
|
||||
open-on-hover
|
||||
open-on-click
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
|
|
|
@ -1,17 +1,28 @@
|
|||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg";
|
||||
import auth from "@/services/auth/auth-service.ts";
|
||||
import { watch } from "vue";
|
||||
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
watch(
|
||||
() => auth.isLoggedIn.value,
|
||||
async (newVal) => {
|
||||
if (newVal) {
|
||||
await router.push("/user");
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function loginAsStudent(): Promise<void> {
|
||||
await auth.loginAs("student");
|
||||
await auth.loginAs(AccountType.Student);
|
||||
}
|
||||
|
||||
async function loginAsTeacher(): Promise<void> {
|
||||
await auth.loginAs("teacher");
|
||||
}
|
||||
|
||||
async function performLogout(): Promise<void> {
|
||||
await auth.logout();
|
||||
await auth.loginAs(AccountType.Teacher);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -65,13 +76,6 @@
|
|||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="auth.isLoggedIn.value">
|
||||
<p>
|
||||
You are currently logged in as {{ auth.authState.user!.profile.name }} ({{ auth.authState.activeRole }})
|
||||
</p>
|
||||
<v-btn @click="performLogout">Logout</v-btn>
|
||||
<v-btn to="/user">home</v-btn>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import LearningPathsGrid from "@/components/LearningPathsGrid.vue";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import { useGetAllLearningPathsByThemeQuery } from "@/queries/learning-paths.ts";
|
||||
import { useGetAllLearningPathsByThemeAndLanguageQuery } from "@/queries/learning-paths.ts";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useThemeQuery } from "@/queries/themes.ts";
|
||||
import type { Language } from "@/data-objects/language";
|
||||
|
||||
const props = defineProps<{ theme: string }>();
|
||||
|
||||
|
@ -16,7 +17,10 @@
|
|||
|
||||
const currentThemeInfo = computed(() => themeQueryResult.data.value?.find((it) => it.key === props.theme));
|
||||
|
||||
const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeQuery(() => props.theme);
|
||||
const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeAndLanguageQuery(
|
||||
() => props.theme,
|
||||
() => locale.value as Language,
|
||||
);
|
||||
|
||||
const { t } = useI18n();
|
||||
const searchFilter = ref("");
|
||||
|
@ -31,13 +35,14 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="container d-flex flex-column align-items-center justify-center">
|
||||
<using-query-result :query-result="themeQueryResult">
|
||||
<h1>{{ currentThemeInfo!!.title }}</h1>
|
||||
<p>{{ currentThemeInfo!!.description }}</p>
|
||||
<div class="search-field-container">
|
||||
<br />
|
||||
<div class="search-field-container mt-sm-6">
|
||||
<v-text-field
|
||||
class="search-field"
|
||||
class="search-field mx-auto"
|
||||
:label="t('search')"
|
||||
append-inner-icon="mdi-magnify"
|
||||
v-model="searchFilter"
|
||||
|
@ -56,13 +61,15 @@
|
|||
|
||||
<style scoped>
|
||||
.search-field-container {
|
||||
display: block;
|
||||
margin: 20px;
|
||||
justify-content: center !important;
|
||||
}
|
||||
.search-field {
|
||||
max-width: 300px;
|
||||
width: 25%;
|
||||
min-width: 300px;
|
||||
}
|
||||
.container {
|
||||
padding: 20px;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,72 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import auth from "@/services/auth/auth-service.ts";
|
||||
import { computed, type Ref, ref, watchEffect } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import StudentAssignment from "@/views/assignments/StudentAssignment.vue";
|
||||
import TeacherAssignment from "@/views/assignments/TeacherAssignment.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||
|
||||
const role = auth.authState.activeRole;
|
||||
const isTeacher = computed(() => role === "teacher");
|
||||
const isTeacher = computed(() => role === AccountType.Teacher);
|
||||
|
||||
const route = useRoute();
|
||||
const classId = ref<string>(route.params.classId as string);
|
||||
const assignmentId = ref(Number(route.params.id));
|
||||
|
||||
function useGroupsWithProgress(
|
||||
groups: Ref<GroupDTO[]>,
|
||||
hruid: Ref<string>,
|
||||
language: Ref<string>,
|
||||
): { groupProgressMap: Map<number, number> } {
|
||||
const groupProgressMap: Map<number, number> = new Map<number, number>();
|
||||
|
||||
watchEffect(() => {
|
||||
// Clear existing entries to avoid stale data
|
||||
groupProgressMap.clear();
|
||||
|
||||
const lang = ref(language.value as Language);
|
||||
|
||||
groups.value.forEach((group) => {
|
||||
const groupKey = group.groupNumber;
|
||||
const forGroup = ref({
|
||||
forGroup: groupKey,
|
||||
assignmentNo: assignmentId,
|
||||
classId: classId,
|
||||
});
|
||||
|
||||
const query = useGetLearningPathQuery(hruid.value, lang, forGroup);
|
||||
|
||||
const data = query.data.value;
|
||||
|
||||
groupProgressMap.set(groupKey, data ? calculateProgress(data) : 0);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
groupProgressMap,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateProgress(lp: LearningPath): number {
|
||||
return ((lp.amountOfNodes - lp.amountOfNodesLeft) / lp.amountOfNodes) * 100;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TeacherAssignment
|
||||
:class-id="classId"
|
||||
:assignment-id="assignmentId"
|
||||
:use-groups-with-progress="useGroupsWithProgress"
|
||||
v-if="isTeacher"
|
||||
>
|
||||
</TeacherAssignment>
|
||||
<StudentAssignment
|
||||
:class-id="classId"
|
||||
:assignment-id="assignmentId"
|
||||
:use-groups-with-progress="useGroupsWithProgress"
|
||||
v-else
|
||||
>
|
||||
</StudentAssignment>
|
||||
|
|
20
frontend/src/views/classes/ClassDisplay.vue
Normal file
20
frontend/src/views/classes/ClassDisplay.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import { useClassQuery } from "@/queries/classes";
|
||||
import { defineProps } from "vue";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
|
||||
const props = defineProps({
|
||||
classId: String,
|
||||
});
|
||||
|
||||
const classQuery = useClassQuery(props.classId);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<using-query-result
|
||||
:query-result="classQuery"
|
||||
v-slot="{ data: classResponse }"
|
||||
>
|
||||
<span>{{ classResponse?.class.displayName }}</span>
|
||||
</using-query-result>
|
||||
</template>
|
|
@ -13,6 +13,7 @@
|
|||
import { useCreateTeacherInvitationMutation } from "@/queries/teacher-invitations";
|
||||
import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation";
|
||||
import { useDisplay } from "vuetify";
|
||||
import "../../assets/common.css";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -76,7 +77,7 @@
|
|||
},
|
||||
onError: (e) => {
|
||||
dialog.value = false;
|
||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
||||
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -104,7 +105,7 @@
|
|||
}
|
||||
},
|
||||
onError: (e) => {
|
||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
||||
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -112,7 +113,7 @@
|
|||
|
||||
function sentInvite(): void {
|
||||
if (!usernameTeacher.value) {
|
||||
showSnackbar(t("please enter a valid username"), "error");
|
||||
showSnackbar(t("valid-username"), "error");
|
||||
return;
|
||||
}
|
||||
const data: TeacherInvitationData = {
|
||||
|
@ -125,7 +126,7 @@
|
|||
usernameTeacher.value = "";
|
||||
},
|
||||
onError: (e) => {
|
||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
||||
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -186,7 +187,7 @@
|
|||
v-slot="classResponse: { data: ClassResponse }"
|
||||
>
|
||||
<div>
|
||||
<h1 class="title">{{ classResponse.data.class.displayName }}</h1>
|
||||
<h1 class="h1">{{ classResponse.data.class.displayName }}</h1>
|
||||
<using-query-result
|
||||
:query-result="getStudents"
|
||||
v-slot="studentsResponse: { data: StudentsResponse }"
|
||||
|
@ -211,16 +212,31 @@
|
|||
<th class="header"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tbody v-if="studentsResponse.data.students.length">
|
||||
<tr
|
||||
v-for="s in studentsResponse.data.students as StudentDTO[]"
|
||||
:key="s.id"
|
||||
>
|
||||
<td>{{ s.firstName + " " + s.lastName }}</td>
|
||||
<td>
|
||||
{{ s.firstName + " " + s.lastName }}
|
||||
<v-btn @click="showPopup(s)">{{ t("remove") }}</v-btn>
|
||||
</td>
|
||||
<td>
|
||||
<v-btn @click="showPopup(s)"> {{ t("remove") }} </v-btn>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td
|
||||
colspan="2"
|
||||
class="empty-message"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
>
|
||||
</v-icon>
|
||||
{{ t("no-students-found") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -242,7 +258,7 @@
|
|||
<th class="header">{{ t("accept") + "/" + t("reject") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody v-if="joinRequests.data.joinRequests.length">
|
||||
<tr
|
||||
v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]"
|
||||
:key="(jr.class, jr.requester, jr.status)"
|
||||
|
@ -287,6 +303,21 @@
|
|||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td
|
||||
colspan="2"
|
||||
class="empty-message"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
>
|
||||
</v-icon>
|
||||
{{ t("no-join-requests-found") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
</using-query-result>
|
||||
|
@ -356,49 +387,6 @@
|
|||
</main>
|
||||
</template>
|
||||
<style scoped>
|
||||
.header {
|
||||
font-weight: bold !important;
|
||||
background-color: #0e6942;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table thead th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
.table thead th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(odd) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(even) {
|
||||
background-color: #f6faf2;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 1px solid #0e6942;
|
||||
border-top: 1px solid #0e6942;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 90%;
|
||||
padding-top: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
padding-top: 2%;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
|
@ -407,6 +395,7 @@
|
|||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 1%;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
@ -416,16 +405,7 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { useI18n } from "vue-i18n";
|
||||
import authState from "@/services/auth/auth-service.ts";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { validate, version } from "uuid";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students";
|
||||
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
||||
|
@ -12,8 +12,10 @@
|
|||
import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes";
|
||||
import type { StudentsResponse } from "@/controllers/students";
|
||||
import type { TeachersResponse } from "@/controllers/teachers";
|
||||
import "../../assets/common.css";
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
// Username of logged in student
|
||||
const username = ref<string | undefined>(undefined);
|
||||
|
@ -37,6 +39,11 @@
|
|||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
const queryCode = route.query.code as string | undefined;
|
||||
if (queryCode) {
|
||||
code.value = queryCode;
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch all classes of the logged in student
|
||||
|
@ -74,11 +81,15 @@
|
|||
|
||||
// The code a student sends in to join a class needs to be formatted as v4 to be valid
|
||||
// These rules are used to display a message to the user if they use a code that has an invalid format
|
||||
function codeRegex(value: string): boolean {
|
||||
return /^[a-zA-Z0-9]{6}$/.test(value);
|
||||
}
|
||||
|
||||
const codeRules = [
|
||||
(value: string | undefined): string | boolean => {
|
||||
if (value === undefined || value === "") {
|
||||
return true;
|
||||
} else if (value !== undefined && validate(value) && version(value) === 4) {
|
||||
} else if (codeRegex(value)) {
|
||||
return true;
|
||||
}
|
||||
return t("invalidFormat");
|
||||
|
@ -91,7 +102,7 @@
|
|||
// Function called when a student submits a code to join a class
|
||||
function submitCode(): void {
|
||||
// Check if the code is valid
|
||||
if (code.value !== undefined && validate(code.value) && version(code.value) === 4) {
|
||||
if (code.value !== undefined && codeRegex(code.value)) {
|
||||
mutate(
|
||||
{ username: username.value!, classId: code.value },
|
||||
{
|
||||
|
@ -99,7 +110,7 @@
|
|||
showSnackbar(t("sent"), "success");
|
||||
},
|
||||
onError: (e) => {
|
||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
||||
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -135,7 +146,7 @@
|
|||
></v-empty-state>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ t("classes") }}</h1>
|
||||
<h1 class="h1">{{ t("classes") }}</h1>
|
||||
<using-query-result
|
||||
:query-result="classesQuery"
|
||||
v-slot="classResponse: { data: ClassesResponse }"
|
||||
|
@ -161,7 +172,7 @@
|
|||
<th class="header">{{ t("members") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody v-if="classResponse.data.classes.length">
|
||||
<tr
|
||||
v-for="c in classResponse.data.classes as ClassDTO[]"
|
||||
:key="c.id"
|
||||
|
@ -181,6 +192,21 @@
|
|||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td
|
||||
colspan="3"
|
||||
class="empty-message"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
>
|
||||
</v-icon>
|
||||
{{ t("no-classes-found") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@ -244,7 +270,7 @@
|
|||
<v-text-field
|
||||
label="CODE"
|
||||
v-model="code"
|
||||
placeholder="XXXXXXXX-XXXX-4XXX-XXXX-XXXXXXXXXXXX"
|
||||
placeholder="XXXXXX"
|
||||
:rules="codeRules"
|
||||
variant="outlined"
|
||||
></v-text-field>
|
||||
|
@ -271,49 +297,6 @@
|
|||
</main>
|
||||
</template>
|
||||
<style scoped>
|
||||
.header {
|
||||
font-weight: bold !important;
|
||||
background-color: #0e6942;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table thead th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
.table thead th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(odd) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(even) {
|
||||
background-color: #f6faf2;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 1px solid #0e6942;
|
||||
border-top: 1px solid #0e6942;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 90%;
|
||||
padding-top: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
padding-top: 2%;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
|
@ -321,6 +304,7 @@
|
|||
|
||||
.join {
|
||||
display: flex;
|
||||
margin-left: 1%;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
|
@ -331,16 +315,7 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
|
|
|
@ -8,13 +8,15 @@
|
|||
import { useTeacherClassesQuery } from "@/queries/teachers";
|
||||
import type { ClassesResponse } from "@/controllers/classes";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import { useClassesQuery, useCreateClassMutation } from "@/queries/classes";
|
||||
import { useCreateClassMutation } from "@/queries/classes";
|
||||
import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations";
|
||||
import {
|
||||
useRespondTeacherInvitationMutation,
|
||||
useTeacherInvitationsReceivedQuery,
|
||||
} from "@/queries/teacher-invitations";
|
||||
import { useDisplay } from "vuetify";
|
||||
import "../../assets/common.css";
|
||||
import ClassDisplay from "@/views/classes/ClassDisplay.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -40,7 +42,6 @@
|
|||
|
||||
// Fetch all classes of the logged in teacher
|
||||
const classesQuery = useTeacherClassesQuery(username, true);
|
||||
const allClassesQuery = useClassesQuery();
|
||||
const { mutate } = useCreateClassMutation();
|
||||
const getInvitationsQuery = useTeacherInvitationsReceivedQuery(username);
|
||||
const { mutate: respondToInvitation } = useRespondTeacherInvitationMutation();
|
||||
|
@ -69,7 +70,7 @@
|
|||
await getInvitationsQuery.refetch();
|
||||
},
|
||||
onError: (e) => {
|
||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
||||
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -112,7 +113,7 @@
|
|||
dialog.value = true;
|
||||
}
|
||||
if (!className.value || className.value === "") {
|
||||
showSnackbar(t("name is mandatory"), "error");
|
||||
showSnackbar(t("nameIsMandatory"), "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,10 +132,12 @@
|
|||
// Show the teacher, copying of the code was a successs
|
||||
const copied = ref(false);
|
||||
|
||||
// Copy the generated code to the clipboard
|
||||
async function copyToClipboard(): Promise<void> {
|
||||
await navigator.clipboard.writeText(code.value);
|
||||
copied.value = true;
|
||||
async function copyToClipboard(code: string, isDialog = false, isLink = false): Promise<void> {
|
||||
const content = isLink ? `${window.location.origin}/user/class?code=${code}` : code;
|
||||
await navigator.clipboard.writeText(content);
|
||||
copied.value = isDialog;
|
||||
|
||||
if (!isDialog) showSnackbar(t("copied"), "white");
|
||||
}
|
||||
|
||||
// Custom breakpoints
|
||||
|
@ -162,6 +165,7 @@
|
|||
// Code display dialog logic
|
||||
const viewCodeDialog = ref(false);
|
||||
const selectedCode = ref("");
|
||||
|
||||
function openCodeDialog(codeToView: string): void {
|
||||
selectedCode.value = codeToView;
|
||||
viewCodeDialog.value = true;
|
||||
|
@ -183,7 +187,7 @@
|
|||
></v-empty-state>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ t("classes") }}</h1>
|
||||
<h1 class="h1">{{ t("classes") }}</h1>
|
||||
<using-query-result
|
||||
:query-result="classesQuery"
|
||||
v-slot="classesResponse: { data: ClassesResponse }"
|
||||
|
@ -212,7 +216,7 @@
|
|||
<th class="header">{{ t("members") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody v-if="classesResponse.data.classes.length">
|
||||
<tr
|
||||
v-for="c in classesResponse.data.classes as ClassDTO[]"
|
||||
:key="c.id"
|
||||
|
@ -223,22 +227,58 @@
|
|||
variant="text"
|
||||
>
|
||||
{{ c.displayName }}
|
||||
<v-icon end> mdi-menu-right </v-icon>
|
||||
<v-icon end> mdi-menu-right</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="!isMdAndDown">{{ c.id }}</span>
|
||||
<v-row
|
||||
v-if="!isMdAndDown"
|
||||
dense
|
||||
align="center"
|
||||
no-gutters
|
||||
>
|
||||
<v-btn
|
||||
variant="text"
|
||||
append-icon="mdi-content-copy"
|
||||
@click="copyToClipboard(c.id)"
|
||||
>
|
||||
{{ c.id }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="copyToClipboard(c.id, false, true)"
|
||||
>
|
||||
<v-icon>mdi-link-variant</v-icon>
|
||||
</v-btn>
|
||||
</v-row>
|
||||
<span
|
||||
v-else
|
||||
style="cursor: pointer"
|
||||
@click="openCodeDialog(c.id)"
|
||||
><v-icon icon="mdi-eye"></v-icon
|
||||
></span>
|
||||
>
|
||||
<v-icon icon="mdi-eye"></v-icon>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>{{ c.students.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td
|
||||
colspan="3"
|
||||
class="empty-message"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
>
|
||||
</v-icon>
|
||||
{{ t("no-classes-found") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
<v-col
|
||||
|
@ -270,8 +310,8 @@
|
|||
type="submit"
|
||||
@click="createClass"
|
||||
block
|
||||
>{{ t("create") }}</v-btn
|
||||
>
|
||||
>{{ t("create") }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</v-sheet>
|
||||
<v-container>
|
||||
|
@ -280,14 +320,29 @@
|
|||
max-width="400px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">code</v-card-title>
|
||||
<v-card-title class="headline">{{ t("code") }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="code"
|
||||
readonly
|
||||
append-inner-icon="mdi-content-copy"
|
||||
@click:append-inner="copyToClipboard"
|
||||
></v-text-field>
|
||||
>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="copyToClipboard(code, true)"
|
||||
>
|
||||
<v-icon>mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="copyToClipboard(code, true, true)"
|
||||
>
|
||||
<v-icon>mdi-link-variant</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-slide-y-transition>
|
||||
<div
|
||||
v-if="copied"
|
||||
|
@ -318,7 +373,7 @@
|
|||
</v-container>
|
||||
</using-query-result>
|
||||
|
||||
<h1 class="title">
|
||||
<h1 class="h1">
|
||||
{{ t("invitations") }}
|
||||
</h1>
|
||||
<v-container
|
||||
|
@ -338,20 +393,13 @@
|
|||
:query-result="getInvitationsQuery"
|
||||
v-slot="invitationsResponse: { data: TeacherInvitationsResponse }"
|
||||
>
|
||||
<using-query-result
|
||||
:query-result="allClassesQuery"
|
||||
v-slot="classesResponse: { data: ClassesResponse }"
|
||||
>
|
||||
<template v-if="invitationsResponse.data.invitations.length">
|
||||
<tr
|
||||
v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]"
|
||||
:key="i.classId"
|
||||
>
|
||||
<td>
|
||||
{{
|
||||
(classesResponse.data.classes as ClassDTO[]).filter(
|
||||
(c) => c.id == i.classId,
|
||||
)[0].displayName
|
||||
}}
|
||||
<ClassDisplay :classId="i.classId" />
|
||||
</td>
|
||||
<td>
|
||||
{{
|
||||
|
@ -393,11 +441,27 @@
|
|||
color="red"
|
||||
variant="text"
|
||||
>
|
||||
</v-btn></div
|
||||
></span>
|
||||
</v-btn>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</using-query-result>
|
||||
</template>
|
||||
<template v-else>
|
||||
<tr>
|
||||
<td
|
||||
colspan="3"
|
||||
class="empty-message"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
>
|
||||
</v-icon>
|
||||
{{ t("no-invitations-found") }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</using-query-result>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
@ -420,9 +484,24 @@
|
|||
<v-text-field
|
||||
v-model="selectedCode"
|
||||
readonly
|
||||
append-inner-icon="mdi-content-copy"
|
||||
@click:append-inner="copyToClipboard"
|
||||
></v-text-field>
|
||||
>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="copyToClipboard(selectedCode, true)"
|
||||
>
|
||||
<v-icon>mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="copyToClipboard(selectedCode, true, true)"
|
||||
>
|
||||
<v-icon>mdi-link-variant</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-slide-y-transition>
|
||||
<div
|
||||
v-if="copied"
|
||||
|
@ -449,49 +528,6 @@
|
|||
</main>
|
||||
</template>
|
||||
<style scoped>
|
||||
.header {
|
||||
font-weight: bold !important;
|
||||
background-color: #0e6942;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table thead th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
.table thead th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(odd) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(even) {
|
||||
background-color: #f6faf2;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 1px solid #0e6942;
|
||||
border-top: 1px solid #0e6942;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 90%;
|
||||
padding-top: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
padding-top: 2%;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
|
@ -509,16 +545,7 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 850px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
|
@ -541,10 +568,6 @@
|
|||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.responsive-col {
|
||||
max-width: 100% !important;
|
||||
flex-basis: 100% !important;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import authState from "@/services/auth/auth-service.ts";
|
||||
import TeacherClasses from "./TeacherClasses.vue";
|
||||
import StudentClasses from "./StudentClasses.vue";
|
||||
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||
|
||||
// Determine if role is student or teacher to render correct view
|
||||
const role: string = authState.authState.activeRole!;
|
||||
|
@ -9,7 +10,7 @@
|
|||
|
||||
<template>
|
||||
<main>
|
||||
<TeacherClasses v-if="role === 'teacher'"></TeacherClasses>
|
||||
<TeacherClasses v-if="role === AccountType.Teacher"></TeacherClasses>
|
||||
<StudentClasses v-else></StudentClasses>
|
||||
</main>
|
||||
</template>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { useI18n } from "vue-i18n";
|
||||
import { THEMESITEMS, AGE_TO_THEMES } from "@/utils/constants.ts";
|
||||
import BrowseThemes from "@/components/BrowseThemes.vue";
|
||||
import "../../assets/common.css";
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
|
@ -46,7 +47,7 @@
|
|||
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<h1 class="title">{{ t("themes") }}</h1>
|
||||
<h1 class="h1">{{ t("themes") }}</h1>
|
||||
<v-container class="dropdowns">
|
||||
<v-select
|
||||
class="v-select"
|
||||
|
@ -77,24 +78,6 @@
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-container {
|
||||
min-height: 100vh;
|
||||
min-width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.title {
|
||||
max-width: 50rem;
|
||||
margin-left: 1rem;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dropdowns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -107,12 +90,6 @@
|
|||
min-width: 100px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.dropdowns {
|
||||
flex-direction: column;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
import QuestionNotification from "@/components/QuestionNotification.vue";
|
||||
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
@ -235,8 +236,10 @@
|
|||
</p>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="query.classId && query.assignmentNo && authService.authState.activeRole === 'teacher'"
|
||||
<v-list-itemF
|
||||
v-if="
|
||||
query.classId && query.assignmentNo && authService.authState.activeRole === AccountType.Teacher
|
||||
"
|
||||
>
|
||||
<template v-slot:default>
|
||||
<learning-path-group-selector
|
||||
|
@ -245,9 +248,9 @@
|
|||
v-model="forGroupQueryParam"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list-itemF>
|
||||
<v-divider></v-divider>
|
||||
<div v-if="props.learningObjectHruid">
|
||||
<div>
|
||||
<using-query-result
|
||||
:query-result="learningObjectListQueryResult"
|
||||
v-slot="learningObjects: { data: LearningObject[] }"
|
||||
|
@ -259,7 +262,9 @@
|
|||
:title="node.title"
|
||||
:active="node.key === props.learningObjectHruid"
|
||||
:key="node.key"
|
||||
v-if="!node.teacherExclusive || authService.authState.activeRole === 'teacher'"
|
||||
v-if="
|
||||
!node.teacherExclusive || authService.authState.activeRole === AccountType.Teacher
|
||||
"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon
|
||||
|
@ -283,10 +288,12 @@
|
|||
</using-query-result>
|
||||
</div>
|
||||
<v-spacer></v-spacer>
|
||||
<v-list-item v-if="authService.authState.activeRole === 'teacher'">
|
||||
<v-list-item v-if="authService.authState.activeRole === AccountType.Teacher">
|
||||
<template v-slot:default>
|
||||
<v-btn
|
||||
class="button-in-nav"
|
||||
width="100%"
|
||||
:color="COLORS.teacherExclusive"
|
||||
@click="assign()"
|
||||
>{{ t("assignLearningPath") }}</v-btn
|
||||
>
|
||||
|
@ -294,7 +301,7 @@
|
|||
</v-list-item>
|
||||
<v-list-item>
|
||||
<div
|
||||
v-if="authService.authState.activeRole === 'student' && pathIsAssignment"
|
||||
v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment"
|
||||
class="assignment-indicator"
|
||||
>
|
||||
{{ t("assignmentIndicator") }}
|
||||
|
@ -323,7 +330,7 @@
|
|||
></learning-object-view>
|
||||
</div>
|
||||
<div
|
||||
v-if="authService.authState.activeRole === 'student' && pathIsAssignment"
|
||||
v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment"
|
||||
class="question-box"
|
||||
>
|
||||
<div class="input-wrapper">
|
||||
|
|
|
@ -17,32 +17,37 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-field-container">
|
||||
<learning-path-search-field class="search-field"></learning-path-search-field>
|
||||
</div>
|
||||
<div class="search-page-container d-flex flex-column align-items-center justify-center">
|
||||
<div class="search-field-container">
|
||||
<learning-path-search-field class="mx-auto" />
|
||||
</div>
|
||||
|
||||
<using-query-result
|
||||
:query-result="searchQueryResults"
|
||||
v-slot="{ data }: { data: LearningPath[] }"
|
||||
>
|
||||
<learning-paths-grid :learning-paths="data"></learning-paths-grid>
|
||||
</using-query-result>
|
||||
<div content="empty-state-container">
|
||||
<v-empty-state
|
||||
<using-query-result
|
||||
:query-result="searchQueryResults"
|
||||
v-slot="{ data }: { data: LearningPath[] }"
|
||||
>
|
||||
<learning-paths-grid :learning-paths="data" />
|
||||
</using-query-result>
|
||||
|
||||
<div
|
||||
v-if="!query"
|
||||
icon="mdi-magnify"
|
||||
:title="t('enterSearchTerm')"
|
||||
:text="t('enterSearchTermDescription')"
|
||||
></v-empty-state>
|
||||
class="empty-state-container"
|
||||
>
|
||||
<v-empty-state
|
||||
icon="mdi-magnify"
|
||||
:title="t('enterSearchTerm')"
|
||||
:text="t('enterSearchTermDescription')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-field-container {
|
||||
display: block;
|
||||
margin: 20px;
|
||||
.search-page-container {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
.search-field {
|
||||
max-width: 300px;
|
||||
.search-field-container {
|
||||
justify-content: center !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
|
||||
import LearningObjectContentView from "@/views/learning-paths/learning-object/content/LearningObjectContentView.vue";
|
||||
import LearningObjectSubmissionsView from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue";
|
||||
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||
|
||||
const _isStudent = computed(() => authService.authState.activeRole === "student");
|
||||
const _isStudent = computed(() => authService.authState.activeRole === AccountType.Student);
|
||||
|
||||
const props = defineProps<{
|
||||
hruid: string;
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -31,7 +32,7 @@
|
|||
mutate: submitSolution,
|
||||
} = useCreateSubmissionMutation();
|
||||
|
||||
const isStudent = computed(() => authService.authState.activeRole === "student");
|
||||
const isStudent = computed(() => authService.authState.activeRole === AccountType.Student);
|
||||
|
||||
const isSubmitDisabled = computed(() => {
|
||||
if (!props.submissionData || props.submissions === undefined) {
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
<script setup lang="ts">
|
||||
import { useLearningObjectListForAdminQuery } from "@/queries/learning-objects.ts";
|
||||
import OwnLearningObjectsView from "@/views/own-learning-content/learning-objects/OwnLearningObjectsView.vue";
|
||||
import OwnLearningPathsView from "@/views/own-learning-content/learning-paths/OwnLearningPathsView.vue";
|
||||
import authService from "@/services/auth/auth-service.ts";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import type { LearningObject } from "@/data-objects/learning-objects/learning-object";
|
||||
import { ref, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useGetAllLearningPathsByAdminQuery } from "@/queries/learning-paths";
|
||||
import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const learningObjectsQuery = useLearningObjectListForAdminQuery(
|
||||
authService.authState.user?.profile.preferred_username,
|
||||
);
|
||||
|
||||
const learningPathsQuery = useGetAllLearningPathsByAdminQuery(
|
||||
authService.authState.user?.profile.preferred_username,
|
||||
);
|
||||
|
||||
type Tab = "learningObjects" | "learningPaths";
|
||||
const tab: Ref<Tab> = ref("learningObjects");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tab-pane-container">
|
||||
<h1 class="title">{{ t("ownLearningContentTitle") }}</h1>
|
||||
|
||||
<v-tabs v-model="tab">
|
||||
<v-tab value="learningObjects">{{ t("learningObjects") }}</v-tab>
|
||||
<v-tab value="learningPaths">{{ t("learningPaths") }}</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-tabs-window
|
||||
v-model="tab"
|
||||
class="main-content"
|
||||
>
|
||||
<v-tabs-window-item
|
||||
value="learningObjects"
|
||||
class="main-content"
|
||||
>
|
||||
<using-query-result
|
||||
:query-result="learningObjectsQuery"
|
||||
v-slot="response: { data: LearningObject[] }"
|
||||
>
|
||||
<own-learning-objects-view :learningObjects="response.data"></own-learning-objects-view>
|
||||
</using-query-result>
|
||||
</v-tabs-window-item>
|
||||
<v-tabs-window-item value="learningPaths">
|
||||
<using-query-result
|
||||
:query-result="learningPathsQuery"
|
||||
v-slot="response: { data: LearningPathDTO[] }"
|
||||
>
|
||||
<own-learning-paths-view :learningPaths="response.data" />
|
||||
</using-query-result>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
.tab-pane-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
.main-content {
|
||||
flex: 1 1;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import type { LearningObject } from "@/data-objects/learning-objects/learning-object";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import LearningObjectContentView from "../../learning-paths/learning-object/content/LearningObjectContentView.vue";
|
||||
import ButtonWithConfirmation from "@/components/ButtonWithConfirmation.vue";
|
||||
import { useDeleteLearningObjectMutation, useLearningObjectHTMLQuery } from "@/queries/learning-objects";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
selectedLearningObject?: LearningObject;
|
||||
}>();
|
||||
|
||||
const learningObjectQueryResult = useLearningObjectHTMLQuery(
|
||||
() => props.selectedLearningObject?.key,
|
||||
() => props.selectedLearningObject?.language,
|
||||
() => props.selectedLearningObject?.version,
|
||||
);
|
||||
|
||||
const { isPending, mutate } = useDeleteLearningObjectMutation();
|
||||
|
||||
function deleteLearningObject(): void {
|
||||
if (props.selectedLearningObject) {
|
||||
mutate({
|
||||
hruid: props.selectedLearningObject.key,
|
||||
language: props.selectedLearningObject.language,
|
||||
version: props.selectedLearningObject.version,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
v-if="selectedLearningObject"
|
||||
:title="t('previewFor') + selectedLearningObject.title"
|
||||
>
|
||||
<template v-slot:text>
|
||||
<using-query-result
|
||||
:query-result="learningObjectQueryResult"
|
||||
v-slot="response: { data: Document }"
|
||||
>
|
||||
<learning-object-content-view :learning-object-content="response.data"></learning-object-content-view>
|
||||
</using-query-result>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<button-with-confirmation
|
||||
@confirm="deleteLearningObject"
|
||||
prepend-icon="mdi mdi-delete"
|
||||
color="red"
|
||||
:text="t('delete')"
|
||||
:confirmQueryText="t('learningObjectDeleteQuery')"
|
||||
/>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,82 @@
|
|||
<script setup lang="ts">
|
||||
import { useUploadLearningObjectMutation } from "@/queries/learning-objects";
|
||||
import { ref, watch, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { VFileUpload } from "vuetify/labs/VFileUpload";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogOpen = ref(false);
|
||||
|
||||
interface ContainsErrorString {
|
||||
error: string;
|
||||
}
|
||||
|
||||
const fileToUpload: Ref<File | undefined> = ref(undefined);
|
||||
|
||||
const { isPending, error, isError, isSuccess, mutate } = useUploadLearningObjectMutation();
|
||||
|
||||
watch(isSuccess, (newIsSuccess) => {
|
||||
if (newIsSuccess) {
|
||||
dialogOpen.value = false;
|
||||
fileToUpload.value = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
function uploadFile() {
|
||||
if (fileToUpload.value) {
|
||||
mutate({ learningObjectZip: fileToUpload.value });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<v-dialog
|
||||
max-width="500"
|
||||
v-model="dialogOpen"
|
||||
>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
prepend-icon="mdi mdi-plus"
|
||||
:text="t('newLearningObject')"
|
||||
v-bind="activatorProps"
|
||||
color="rgb(14, 105, 66)"
|
||||
size="large"
|
||||
>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card :title="t('learningObjectUploadTitle')">
|
||||
<v-card-text>
|
||||
<v-file-upload
|
||||
icon="mdi-upload"
|
||||
v-model="fileToUpload"
|
||||
:disabled="isPending"
|
||||
></v-file-upload>
|
||||
<v-alert
|
||||
v-if="error"
|
||||
icon="mdi mdi-alert-circle"
|
||||
type="error"
|
||||
:title="t('uploadFailed')"
|
||||
:text="t((error.response?.data as ContainsErrorString).error ?? error.message)"
|
||||
></v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
:text="t('cancel')"
|
||||
@click="isActive.value = false"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
:text="t('upload')"
|
||||
@click="uploadFile()"
|
||||
:loading="isPending"
|
||||
:disabled="!fileToUpload"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<style scoped></style>
|
|
@ -0,0 +1,79 @@
|
|||
<script setup lang="ts">
|
||||
import type { LearningObject } from "@/data-objects/learning-objects/learning-object";
|
||||
import LearningObjectUploadButton from "@/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue";
|
||||
import LearningObjectPreviewCard from "./LearningObjectPreviewCard.vue";
|
||||
import { computed, ref, watch, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
learningObjects: LearningObject[];
|
||||
}>();
|
||||
|
||||
const tableHeaders = [
|
||||
{ title: t("hruid"), width: "250px", key: "key" },
|
||||
{ title: t("language"), width: "50px", key: "language" },
|
||||
{ title: t("version"), width: "50px", key: "version" },
|
||||
{ title: t("title"), key: "title" },
|
||||
];
|
||||
|
||||
const selectedLearningObjects: Ref<LearningObject[]> = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.learningObjects,
|
||||
() => (selectedLearningObjects.value = []),
|
||||
);
|
||||
|
||||
const selectedLearningObject = computed(() =>
|
||||
selectedLearningObjects.value ? selectedLearningObjects.value[0] : undefined,
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="root">
|
||||
<div class="table-container">
|
||||
<learning-object-upload-button />
|
||||
<v-data-table
|
||||
class="table"
|
||||
v-model="selectedLearningObjects"
|
||||
:items="props.learningObjects"
|
||||
:headers="tableHeaders"
|
||||
select-strategy="single"
|
||||
show-select
|
||||
return-object
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="preview-container"
|
||||
v-if="selectedLearningObject"
|
||||
>
|
||||
<learning-object-preview-card
|
||||
class="preview"
|
||||
:selectedLearningObject="selectedLearningObject"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.root {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.preview-container {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
}
|
||||
.table-container {
|
||||
flex: 1;
|
||||
}
|
||||
.preview {
|
||||
width: 100%;
|
||||
}
|
||||
.table {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,147 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed, ref, watch, type Ref } from "vue";
|
||||
import JsonEditorVue from "json-editor-vue";
|
||||
import ButtonWithConfirmation from "@/components/ButtonWithConfirmation.vue";
|
||||
import {
|
||||
useDeleteLearningPathMutation,
|
||||
usePostLearningPathMutation,
|
||||
usePutLearningPathMutation,
|
||||
} from "@/queries/learning-paths";
|
||||
import { Language } from "@/data-objects/language";
|
||||
import type { LearningPath } from "@dwengo-1/common/interfaces/learning-content";
|
||||
import type { AxiosError } from "axios";
|
||||
import { parse } from "uuid";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
selectedLearningPath?: LearningPath;
|
||||
}>();
|
||||
|
||||
const { isPending, mutate, error: deleteError, isSuccess: deleteSuccess } = useDeleteLearningPathMutation();
|
||||
|
||||
const DEFAULT_LEARNING_PATH: Partial<LearningPath> = {
|
||||
language: "en",
|
||||
hruid: "...",
|
||||
title: "...",
|
||||
description: "...",
|
||||
nodes: [
|
||||
{
|
||||
learningobject_hruid: "...",
|
||||
language: Language.English,
|
||||
version: 1,
|
||||
start_node: true,
|
||||
transitions: [
|
||||
{
|
||||
default: true,
|
||||
condition: t("hintRemoveIfUnconditionalTransition"),
|
||||
next: {
|
||||
hruid: "...",
|
||||
version: 1,
|
||||
language: "...",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { isPending: isPostPending, error: postError, mutate: doPost } = usePostLearningPathMutation();
|
||||
const { isPending: isPutPending, error: putError, mutate: doPut } = usePutLearningPathMutation();
|
||||
|
||||
const learningPath: Ref<Partial<LearningPath> | string> = ref(DEFAULT_LEARNING_PATH);
|
||||
|
||||
const parsedLearningPath = computed(() =>
|
||||
typeof learningPath.value === "string" ? (JSON.parse(learningPath.value) as LearningPath) : learningPath.value,
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.selectedLearningPath,
|
||||
() => (learningPath.value = props.selectedLearningPath ?? DEFAULT_LEARNING_PATH),
|
||||
);
|
||||
|
||||
function uploadLearningPath(): void {
|
||||
if (props.selectedLearningPath) {
|
||||
doPut({ learningPath: parsedLearningPath.value });
|
||||
} else {
|
||||
doPost({ learningPath: parsedLearningPath.value });
|
||||
}
|
||||
}
|
||||
|
||||
function deleteLearningPath(): void {
|
||||
if (props.selectedLearningPath) {
|
||||
mutate({
|
||||
hruid: props.selectedLearningPath.hruid,
|
||||
language: props.selectedLearningPath.language as Language,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function extractErrorMessage(error: AxiosError): string {
|
||||
return (error.response?.data as { error: string }).error ?? error.message;
|
||||
}
|
||||
|
||||
const isIdModified = computed(
|
||||
() =>
|
||||
props.selectedLearningPath !== undefined &&
|
||||
(props.selectedLearningPath.hruid !== parsedLearningPath.value.hruid ||
|
||||
props.selectedLearningPath.language !== parsedLearningPath.value.language),
|
||||
);
|
||||
|
||||
function getErrorMessage(): string | null {
|
||||
if (postError.value) {
|
||||
return t(extractErrorMessage(postError.value));
|
||||
} else if (putError.value) {
|
||||
return t(extractErrorMessage(putError.value));
|
||||
} else if (deleteError.value) {
|
||||
return t(extractErrorMessage(deleteError.value));
|
||||
} else if (isIdModified.value) {
|
||||
return t("learningPathCantModifyId");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card :title="props.selectedLearningPath ? t('editLearningPath') : t('newLearningPath')">
|
||||
<template v-slot:text>
|
||||
<json-editor-vue v-model="learningPath"></json-editor-vue>
|
||||
<v-alert
|
||||
v-if="postError || putError || deleteError || isIdModified"
|
||||
icon="mdi mdi-alert-circle"
|
||||
type="error"
|
||||
:title="t('error')"
|
||||
:text="getErrorMessage()!"
|
||||
></v-alert>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<v-btn
|
||||
@click="uploadLearningPath"
|
||||
prependIcon="mdi mdi-check"
|
||||
:loading="isPostPending || isPutPending"
|
||||
:disabled="parsedLearningPath.hruid === DEFAULT_LEARNING_PATH.hruid || isIdModified"
|
||||
>
|
||||
{{ props.selectedLearningPath ? t("saveChanges") : t("create") }}
|
||||
</v-btn>
|
||||
<button-with-confirmation
|
||||
@confirm="deleteLearningPath"
|
||||
:disabled="!props.selectedLearningPath"
|
||||
:text="t('delete')"
|
||||
color="red"
|
||||
prependIcon="mdi mdi-delete"
|
||||
:confirmQueryText="t('learningPathDeleteQuery')"
|
||||
/>
|
||||
<v-btn
|
||||
:href="`/learningPath/${props.selectedLearningPath?.hruid}/${props.selectedLearningPath?.language}/start`"
|
||||
target="_blank"
|
||||
prepend-icon="mdi mdi-open-in-new"
|
||||
:disabled="!props.selectedLearningPath"
|
||||
>
|
||||
{{ t("open") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,77 @@
|
|||
<script setup lang="ts">
|
||||
import LearningPathPreviewCard from "./LearningPathPreviewCard.vue";
|
||||
import { computed, ref, watch, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
learningPaths: LearningPathDTO[];
|
||||
}>();
|
||||
|
||||
const tableHeaders = [
|
||||
{ title: t("hruid"), width: "250px", key: "hruid" },
|
||||
{ title: t("language"), width: "50px", key: "language" },
|
||||
{ title: t("title"), key: "title" },
|
||||
];
|
||||
|
||||
const selectedLearningPaths: Ref<LearningPathDTO[]> = ref([]);
|
||||
|
||||
const selectedLearningPath = computed(() =>
|
||||
selectedLearningPaths.value ? selectedLearningPaths.value[0] : undefined,
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.learningPaths,
|
||||
() => (selectedLearningPaths.value = []),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="root">
|
||||
<div class="table-container">
|
||||
<v-data-table
|
||||
class="table"
|
||||
v-model="selectedLearningPaths"
|
||||
:items="props.learningPaths"
|
||||
:headers="tableHeaders"
|
||||
select-strategy="single"
|
||||
show-select
|
||||
return-object
|
||||
/>
|
||||
</div>
|
||||
<div class="preview-container">
|
||||
<learning-path-preview-card
|
||||
class="preview"
|
||||
:selectedLearningPath="selectedLearningPath"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fab {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
.root {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.preview-container {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
}
|
||||
.table-container {
|
||||
flex: 1;
|
||||
}
|
||||
.preview {
|
||||
width: 100%;
|
||||
}
|
||||
.table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue