Merge branch 'dev' into feat/endpoints-beschermen-met-authenticatie-#105
This commit is contained in:
commit
5893933d4b
32 changed files with 1155 additions and 6559 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;
|
||||
}
|
||||
}
|
|
@ -57,6 +57,22 @@
|
|||
</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-for="card in cards"
|
||||
:key="card.key"
|
||||
|
@ -74,24 +90,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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
@ -40,10 +39,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}`);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
@ -110,7 +111,7 @@
|
|||
"remove": "entfernen",
|
||||
"students": "Studenten",
|
||||
"classJoinRequests": "Beitrittsanfragen",
|
||||
"reject": "ablehnen",
|
||||
"reject": "Ablehnen",
|
||||
"areusure": "Sind Sie sicher?",
|
||||
"yes": "ja",
|
||||
"teachers": "Lehrer",
|
||||
|
@ -121,5 +122,18 @@
|
|||
"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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
@ -108,12 +109,12 @@
|
|||
"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,5 +122,18 @@
|
|||
"invite": "invite",
|
||||
"assignmentIndicator": "ASSIGNMENT",
|
||||
"searchAllLearningPathsTitle": "Search all learning paths",
|
||||
"searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths."
|
||||
"searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths.",
|
||||
"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"
|
||||
}
|
||||
|
|
|
@ -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é",
|
||||
|
@ -108,12 +109,13 @@
|
|||
"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 +123,18 @@
|
|||
"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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
@ -108,12 +109,12 @@
|
|||
"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 +122,18 @@
|
|||
"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"
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
@ -33,10 +32,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];
|
||||
}
|
||||
|
@ -80,17 +75,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>,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
// Disable combobox when learningPath prop is passed
|
||||
const lpIsSelected = route.query.hruid !== undefined;
|
||||
const deadline = ref(null);
|
||||
const deadline = ref(new Date());
|
||||
const description = ref("");
|
||||
const groups = ref<string[][]>([]);
|
||||
|
||||
|
@ -86,6 +86,7 @@
|
|||
title: assignmentTitle.value,
|
||||
description: description.value,
|
||||
learningPath: lp || "",
|
||||
deadline: deadline.value,
|
||||
language: language.value,
|
||||
groups: groups.value,
|
||||
};
|
||||
|
@ -96,7 +97,7 @@
|
|||
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<h1 class="title">{{ t("new-assignment") }}</h1>
|
||||
<h1 class="h1">{{ t("new-assignment") }}</h1>
|
||||
<v-card class="form-card">
|
||||
<v-form
|
||||
ref="form"
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import { asyncComputed } from "@vueuse/core";
|
||||
import { useDeleteAssignmentMutation } from "@/queries/assignments.ts";
|
||||
import "../../assets/common.css";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const role = ref(auth.authState.activeRole);
|
||||
|
@ -27,30 +28,47 @@
|
|||
classesQueryResults = useStudentClassesQuery(username, true);
|
||||
}
|
||||
|
||||
//TODO: remove later
|
||||
const classController = new ClassController();
|
||||
|
||||
//TODO: replace by query that fetches all user's assignment
|
||||
const assignments = asyncComputed(async () => {
|
||||
const classes = classesQueryResults?.data?.value?.classes;
|
||||
if (!classes) return [];
|
||||
const result = await Promise.all(
|
||||
(classes as ClassDTO[]).map(async (cls) => {
|
||||
const { assignments } = await classController.getAssignments(cls.id);
|
||||
return assignments.map((a) => ({
|
||||
id: a.id,
|
||||
class: cls,
|
||||
title: a.title,
|
||||
description: a.description,
|
||||
learningPath: a.learningPath,
|
||||
language: a.language,
|
||||
groups: a.groups,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
const assignments = asyncComputed(
|
||||
async () => {
|
||||
const classes = classesQueryResults?.data?.value?.classes;
|
||||
if (!classes) return [];
|
||||
|
||||
return result.flat();
|
||||
}, []);
|
||||
const result = await Promise.all(
|
||||
(classes as ClassDTO[]).map(async (cls) => {
|
||||
const { assignments } = await classController.getAssignments(cls.id);
|
||||
return assignments.map((a) => ({
|
||||
id: a.id,
|
||||
class: cls,
|
||||
title: a.title,
|
||||
description: a.description,
|
||||
learningPath: a.learningPath,
|
||||
language: a.language,
|
||||
deadline: a.deadline,
|
||||
groups: a.groups,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
// Order the assignments by deadline
|
||||
return result.flat().sort((a, b) => {
|
||||
const now = Date.now();
|
||||
const aTime = new Date(a.deadline).getTime();
|
||||
const bTime = new Date(b.deadline).getTime();
|
||||
|
||||
const aIsPast = aTime < now;
|
||||
const bIsPast = bTime < now;
|
||||
|
||||
if (aIsPast && !bIsPast) return 1;
|
||||
if (!aIsPast && bIsPast) return -1;
|
||||
|
||||
return aTime - bTime;
|
||||
});
|
||||
},
|
||||
[],
|
||||
{ evaluating: true },
|
||||
);
|
||||
|
||||
async function goToCreateAssignment(): Promise<void> {
|
||||
await router.push("/assignment/create");
|
||||
|
@ -72,6 +90,35 @@
|
|||
mutate({ cid: clsId, an: num });
|
||||
}
|
||||
|
||||
function formatDate(date?: string | Date): string {
|
||||
if (!date) return "–";
|
||||
const d = new Date(date);
|
||||
|
||||
// Choose locale based on selected language
|
||||
const currentLocale = locale.value;
|
||||
|
||||
return d.toLocaleDateString(currentLocale, {
|
||||
weekday: "short",
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function getDeadlineClass(deadline?: string | Date): string {
|
||||
if (!deadline) return "";
|
||||
|
||||
const date = new Date(deadline);
|
||||
const now = new Date();
|
||||
const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
if (date.getTime() < now.getTime()) return "deadline-passed";
|
||||
if (date.getTime() <= in24Hours.getTime()) return "deadline-in24hours";
|
||||
return "deadline-upcoming";
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const user = await auth.loadUser();
|
||||
username.value = user?.profile?.preferred_username ?? "";
|
||||
|
@ -80,7 +127,7 @@
|
|||
|
||||
<template>
|
||||
<div class="assignments-container">
|
||||
<h1>{{ t("assignments") }}</h1>
|
||||
<h1 class="h1">{{ t("assignments") }}</h1>
|
||||
|
||||
<v-btn
|
||||
v-if="isTeacher"
|
||||
|
@ -107,6 +154,13 @@
|
|||
{{ assignment.class.displayName }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="assignment-deadline"
|
||||
:class="getDeadlineClass(assignment.deadline)"
|
||||
>
|
||||
{{ t("deadline") }}:
|
||||
<span>{{ formatDate(assignment.deadline) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
@ -131,6 +185,13 @@
|
|||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="assignments.length === 0">
|
||||
<v-col cols="12">
|
||||
<div class="no-assignments">
|
||||
{{ t("no-assignments") }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -139,25 +200,32 @@
|
|||
.assignments-container {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 2% 4%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
.center-btn {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin: 0 auto 2rem auto;
|
||||
font-weight: 600;
|
||||
background-color: #10ad61;
|
||||
color: white;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.center-btn:hover {
|
||||
background-color: #0e6942;
|
||||
}
|
||||
|
||||
.assignment-card {
|
||||
padding: 1rem;
|
||||
padding: 1.25rem;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
background-color: white;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
.assignment-card:hover {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.top-content {
|
||||
|
@ -165,6 +233,35 @@
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.assignment-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.4rem;
|
||||
color: #0e6942;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.assignment-class,
|
||||
.assignment-deadline {
|
||||
font-size: 0.95rem;
|
||||
color: #444;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.class-name {
|
||||
font-weight: 600;
|
||||
color: #097180;
|
||||
}
|
||||
|
||||
.assignment-deadline.deadline-passed {
|
||||
color: #d32f2f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.assignment-deadline.deadline-in24hours {
|
||||
color: #f57c00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
@ -172,24 +269,14 @@
|
|||
.button-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.assignment-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.assignment-class {
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.class-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
.no-assignments {
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
color: #777;
|
||||
padding: 3rem 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -188,7 +189,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 }"
|
||||
|
@ -213,16 +214,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>
|
||||
|
@ -244,7 +260,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)"
|
||||
|
@ -289,6 +305,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>
|
||||
|
@ -358,49 +389,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;
|
||||
|
@ -409,6 +397,7 @@
|
|||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 1%;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
@ -418,16 +407,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;
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
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();
|
||||
|
||||
|
@ -135,7 +136,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 +162,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 +182,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>
|
||||
|
@ -271,49 +287,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 +294,7 @@
|
|||
|
||||
.join {
|
||||
display: flex;
|
||||
margin-left: 1%;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
|
@ -331,16 +305,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;
|
||||
|
|
|
@ -1,171 +1,180 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import authState from "@/services/auth/auth-service.ts";
|
||||
import { onMounted, ref, watchEffect } from "vue";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import type { TeacherInvitationData, TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
|
||||
import { useTeacherClassesQuery } from "@/queries/teachers";
|
||||
import type { ClassesResponse } from "@/controllers/classes";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import { useClassesQuery, useCreateClassMutation } from "@/queries/classes";
|
||||
import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations";
|
||||
import {
|
||||
useRespondTeacherInvitationMutation,
|
||||
useTeacherInvitationsReceivedQuery,
|
||||
} from "@/queries/teacher-invitations";
|
||||
import { useDisplay } from "vuetify";
|
||||
import ClassDisplay from "@/views/classes/ClassDisplay.vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import authState from '@/services/auth/auth-service.ts';
|
||||
import { onMounted, ref, watchEffect } from 'vue';
|
||||
import type { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
||||
import type { ClassDTO } from '@dwengo-1/common/interfaces/class';
|
||||
import type { TeacherInvitationData, TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation';
|
||||
import { useTeacherClassesQuery } from '@/queries/teachers';
|
||||
import type { ClassesResponse } from '@/controllers/classes';
|
||||
import UsingQueryResult from '@/components/UsingQueryResult.vue';
|
||||
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();
|
||||
const { t } = useI18n();
|
||||
|
||||
// Username of logged in teacher
|
||||
const username = ref<string | undefined>(undefined);
|
||||
const isLoading = ref(false);
|
||||
const isError = ref(false);
|
||||
const errorMessage = ref<string>("");
|
||||
// Username of logged in teacher
|
||||
const username = ref<string | undefined>(undefined);
|
||||
const isLoading = ref(false);
|
||||
const isError = ref(false);
|
||||
const errorMessage = ref<string>('');
|
||||
|
||||
// Load current user before rendering the page
|
||||
onMounted(async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const userObject = await authState.loadUser();
|
||||
username.value = userObject!.profile.preferred_username;
|
||||
} catch (error) {
|
||||
isError.value = true;
|
||||
errorMessage.value = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
// Load current user before rendering the page
|
||||
onMounted(async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const userObject = await authState.loadUser();
|
||||
username.value = userObject!.profile.preferred_username;
|
||||
} catch (error) {
|
||||
isError.value = true;
|
||||
errorMessage.value = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch all classes of the logged in teacher
|
||||
const classesQuery = useTeacherClassesQuery(username, true);
|
||||
const { mutate } = useCreateClassMutation();
|
||||
const getInvitationsQuery = useTeacherInvitationsReceivedQuery(username);
|
||||
const { mutate: respondToInvitation } = useRespondTeacherInvitationMutation();
|
||||
|
||||
// Boolean that handles visibility for dialogs
|
||||
// Creating a class will generate a popup with the generated code
|
||||
const dialog = ref(false);
|
||||
|
||||
// Code generated when new class was created
|
||||
const code = ref<string>('');
|
||||
|
||||
// Function to handle an invitation request
|
||||
function handleInvitation(ti: TeacherInvitationDTO, accepted: boolean): void {
|
||||
const data: TeacherInvitationData = {
|
||||
sender: (ti.sender as TeacherDTO).id,
|
||||
receiver: (ti.receiver as TeacherDTO).id,
|
||||
class: ti.classId,
|
||||
accepted: accepted,
|
||||
};
|
||||
respondToInvitation(data, {
|
||||
onSuccess: async () => {
|
||||
if (accepted) {
|
||||
await classesQuery.refetch();
|
||||
}
|
||||
|
||||
await getInvitationsQuery.refetch();
|
||||
},
|
||||
onError: (e) => {
|
||||
showSnackbar(t('failed') + ': ' + e.response.data.error || e.message, 'error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch all classes of the logged in teacher
|
||||
const classesQuery = useTeacherClassesQuery(username, true);
|
||||
const { mutate } = useCreateClassMutation();
|
||||
const getInvitationsQuery = useTeacherInvitationsReceivedQuery(username);
|
||||
const { mutate: respondToInvitation } = useRespondTeacherInvitationMutation();
|
||||
// Teacher should be able to set a displayname when making a class
|
||||
const className = ref<string>('');
|
||||
|
||||
// Boolean that handles visibility for dialogs
|
||||
// Creating a class will generate a popup with the generated code
|
||||
const dialog = ref(false);
|
||||
// The name can only contain dash, underscore letters and numbers
|
||||
// These rules are used to display a message to the user if the name is not valid
|
||||
const nameRules = [
|
||||
(value: string | undefined): string | boolean => {
|
||||
if (!value) return true;
|
||||
if (value && /^[a-zA-Z0-9_-]+$/.test(value)) return true;
|
||||
return t('onlyUse');
|
||||
},
|
||||
];
|
||||
|
||||
// Code generated when new class was created
|
||||
const code = ref<string>("");
|
||||
|
||||
// Function to handle an invitation request
|
||||
function handleInvitation(ti: TeacherInvitationDTO, accepted: boolean): void {
|
||||
const data: TeacherInvitationData = {
|
||||
sender: (ti.sender as TeacherDTO).id,
|
||||
receiver: (ti.receiver as TeacherDTO).id,
|
||||
class: ti.classId,
|
||||
accepted: accepted,
|
||||
// Function called when a teacher creates a class
|
||||
async function createClass(): Promise<void> {
|
||||
// Check if the class name is valid
|
||||
if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) {
|
||||
const classDto: ClassDTO = {
|
||||
id: '',
|
||||
displayName: className.value,
|
||||
teachers: [username.value!],
|
||||
students: [],
|
||||
};
|
||||
respondToInvitation(data, {
|
||||
onSuccess: async () => {
|
||||
if (accepted) {
|
||||
await classesQuery.refetch();
|
||||
}
|
||||
|
||||
await getInvitationsQuery.refetch();
|
||||
mutate(classDto, {
|
||||
onSuccess: async (classResponse) => {
|
||||
showSnackbar(t('classCreated'), 'success');
|
||||
const createdClass: ClassDTO = classResponse.class;
|
||||
code.value = createdClass.id;
|
||||
await classesQuery.refetch();
|
||||
},
|
||||
onError: (e) => {
|
||||
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||
onError: (err) => {
|
||||
showSnackbar(t('creationFailed') + ': ' + err.message, 'error');
|
||||
},
|
||||
});
|
||||
dialog.value = true;
|
||||
}
|
||||
|
||||
// Teacher should be able to set a displayname when making a class
|
||||
const className = ref<string>("");
|
||||
|
||||
// The name can only contain dash, underscore letters and numbers
|
||||
// These rules are used to display a message to the user if the name is not valid
|
||||
const nameRules = [
|
||||
(value: string | undefined): string | boolean => {
|
||||
if (!value) return true;
|
||||
if (value && /^[a-zA-Z0-9_-]+$/.test(value)) return true;
|
||||
return t("onlyUse");
|
||||
},
|
||||
];
|
||||
|
||||
// Function called when a teacher creates a class
|
||||
async function createClass(): Promise<void> {
|
||||
// Check if the class name is valid
|
||||
if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) {
|
||||
const classDto: ClassDTO = {
|
||||
id: "",
|
||||
displayName: className.value,
|
||||
teachers: [username.value!],
|
||||
students: [],
|
||||
};
|
||||
|
||||
mutate(classDto, {
|
||||
onSuccess: async (classResponse) => {
|
||||
showSnackbar(t("classCreated"), "success");
|
||||
const createdClass: ClassDTO = classResponse.class;
|
||||
code.value = createdClass.id;
|
||||
await classesQuery.refetch();
|
||||
},
|
||||
onError: (err) => {
|
||||
showSnackbar(t("creationFailed") + ": " + err.message, "error");
|
||||
},
|
||||
});
|
||||
dialog.value = true;
|
||||
}
|
||||
if (!className.value || className.value === "") {
|
||||
showSnackbar(t("name is mandatory"), "error");
|
||||
}
|
||||
if (!className.value || className.value === '') {
|
||||
showSnackbar(t('nameIsMandatory'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
const snackbar = ref({
|
||||
visible: false,
|
||||
message: "",
|
||||
color: "success",
|
||||
});
|
||||
const snackbar = ref({
|
||||
visible: false,
|
||||
message: '',
|
||||
color: 'success',
|
||||
});
|
||||
|
||||
function showSnackbar(message: string, color: string): void {
|
||||
snackbar.value.message = message;
|
||||
snackbar.value.color = color;
|
||||
snackbar.value.visible = true;
|
||||
}
|
||||
function showSnackbar(message: string, color: string): void {
|
||||
snackbar.value.message = message;
|
||||
snackbar.value.color = color;
|
||||
snackbar.value.visible = true;
|
||||
}
|
||||
|
||||
// Show the teacher, copying of the code was a successs
|
||||
const copied = ref(false);
|
||||
// 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;
|
||||
}
|
||||
// Copy the generated code to the clipboard
|
||||
async function copyToClipboard(): Promise<void> {
|
||||
await navigator.clipboard.writeText(code.value);
|
||||
copied.value = true;
|
||||
}
|
||||
|
||||
// Custom breakpoints
|
||||
const customBreakpoints = {
|
||||
xs: 0,
|
||||
sm: 500,
|
||||
md: 1370,
|
||||
lg: 1400,
|
||||
xl: 1600,
|
||||
};
|
||||
async function copyCode(selectedCode: string): Promise<void> {
|
||||
code.value = selectedCode;
|
||||
await copyToClipboard();
|
||||
showSnackbar(t('copied'), 'white');
|
||||
copied.value = false;
|
||||
}
|
||||
|
||||
// Logic for small screens
|
||||
const display = useDisplay();
|
||||
// Custom breakpoints
|
||||
const customBreakpoints = {
|
||||
xs: 0,
|
||||
sm: 500,
|
||||
md: 1370,
|
||||
lg: 1400,
|
||||
xl: 1600,
|
||||
};
|
||||
|
||||
// Reactive variables to hold custom logic based on breakpoints
|
||||
const isMdAndDown = ref(false);
|
||||
const isSmAndDown = ref(false);
|
||||
// Logic for small screens
|
||||
const display = useDisplay();
|
||||
|
||||
watchEffect(() => {
|
||||
// Custom breakpoint logic
|
||||
isMdAndDown.value = display.width.value < customBreakpoints.md;
|
||||
isSmAndDown.value = display.width.value < customBreakpoints.sm;
|
||||
});
|
||||
// Reactive variables to hold custom logic based on breakpoints
|
||||
const isMdAndDown = ref(false);
|
||||
const isSmAndDown = ref(false);
|
||||
|
||||
// Code display dialog logic
|
||||
const viewCodeDialog = ref(false);
|
||||
const selectedCode = ref("");
|
||||
function openCodeDialog(codeToView: string): void {
|
||||
selectedCode.value = codeToView;
|
||||
viewCodeDialog.value = true;
|
||||
}
|
||||
watchEffect(() => {
|
||||
// Custom breakpoint logic
|
||||
isMdAndDown.value = display.width.value < customBreakpoints.md;
|
||||
isSmAndDown.value = display.width.value < customBreakpoints.sm;
|
||||
});
|
||||
|
||||
// Code display dialog logic
|
||||
const viewCodeDialog = ref(false);
|
||||
const selectedCode = ref('');
|
||||
|
||||
function openCodeDialog(codeToView: string): void {
|
||||
selectedCode.value = codeToView;
|
||||
viewCodeDialog.value = true;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<main>
|
||||
|
@ -183,7 +192,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 }"
|
||||
|
@ -204,40 +213,62 @@
|
|||
>
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("classes") }}</th>
|
||||
<th class="header">
|
||||
{{ t("code") }}
|
||||
</th>
|
||||
<th class="header">{{ t("members") }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="header">{{ t('classes') }}</th>
|
||||
<th class="header">
|
||||
{{ t('code') }}
|
||||
</th>
|
||||
<th class="header">{{ t('members') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="c in classesResponse.data.classes as ClassDTO[]"
|
||||
:key="c.id"
|
||||
>
|
||||
<td>
|
||||
<v-btn
|
||||
:to="`/class/${c.id}`"
|
||||
variant="text"
|
||||
>
|
||||
{{ c.displayName }}
|
||||
<v-icon end> mdi-menu-right </v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="!isMdAndDown">{{ c.id }}</span>
|
||||
<span
|
||||
v-else
|
||||
style="cursor: pointer"
|
||||
@click="openCodeDialog(c.id)"
|
||||
><v-icon icon="mdi-eye"></v-icon
|
||||
></span>
|
||||
</td>
|
||||
<tbody v-if="classesResponse.data.classes.length">
|
||||
<tr
|
||||
v-for="c in classesResponse.data.classes as ClassDTO[]"
|
||||
:key="c.id"
|
||||
>
|
||||
<td>
|
||||
<v-btn
|
||||
:to="`/class/${c.id}`"
|
||||
variant="text"
|
||||
>
|
||||
{{ c.displayName }}
|
||||
<v-icon end> mdi-menu-right</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>
|
||||
<v-btn
|
||||
v-if="!isMdAndDown"
|
||||
variant="text"
|
||||
append-icon="mdi-content-copy"
|
||||
@click="copyCode(c.id)"
|
||||
>
|
||||
{{ c.id }}
|
||||
</v-btn>
|
||||
<span
|
||||
v-else
|
||||
style="cursor: pointer"
|
||||
@click="openCodeDialog(c.id)"
|
||||
><v-icon icon="mdi-eye"></v-icon
|
||||
></span>
|
||||
</td>
|
||||
|
||||
<td>{{ c.students.length }}</td>
|
||||
</tr>
|
||||
<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>
|
||||
|
@ -248,13 +279,13 @@
|
|||
class="responsive-col"
|
||||
>
|
||||
<div>
|
||||
<h2>{{ t("createClass") }}</h2>
|
||||
<h2>{{ t('createClass') }}</h2>
|
||||
|
||||
<v-sheet
|
||||
class="pa-4 sheet"
|
||||
max-width="600px"
|
||||
>
|
||||
<p>{{ t("createClassInstructions") }}</p>
|
||||
<p>{{ t('createClassInstructions') }}</p>
|
||||
<v-form @submit.prevent>
|
||||
<v-text-field
|
||||
class="mt-4"
|
||||
|
@ -270,7 +301,8 @@
|
|||
type="submit"
|
||||
@click="createClass"
|
||||
block
|
||||
>{{ t("create") }}</v-btn
|
||||
>{{ t('create') }}
|
||||
</v-btn
|
||||
>
|
||||
</v-form>
|
||||
</v-sheet>
|
||||
|
@ -293,7 +325,7 @@
|
|||
v-if="copied"
|
||||
class="text-center mt-2"
|
||||
>
|
||||
{{ t("copied") }}
|
||||
{{ t('copied') }}
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
</v-card-text>
|
||||
|
@ -306,7 +338,7 @@
|
|||
copied = false;
|
||||
"
|
||||
>
|
||||
{{ t("close") }}
|
||||
{{ t('close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
@ -318,8 +350,8 @@
|
|||
</v-container>
|
||||
</using-query-result>
|
||||
|
||||
<h1 class="title">
|
||||
{{ t("invitations") }}
|
||||
<h1 class="h1">
|
||||
{{ t('invitations') }}
|
||||
</h1>
|
||||
<v-container
|
||||
fluid
|
||||
|
@ -327,17 +359,18 @@
|
|||
>
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("class") }}</th>
|
||||
<th class="header">{{ t("sender") }}</th>
|
||||
<th class="header">{{ t("accept") + "/" + t("reject") }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="header">{{ t('class') }}</th>
|
||||
<th class="header">{{ t('sender') }}</th>
|
||||
<th class="header">{{ t('accept') + '/' + t('reject') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<using-query-result
|
||||
:query-result="getInvitationsQuery"
|
||||
v-slot="invitationsResponse: { data: TeacherInvitationsResponse }"
|
||||
>
|
||||
<using-query-result
|
||||
:query-result="getInvitationsQuery"
|
||||
v-slot="invitationsResponse: { data: TeacherInvitationsResponse }"
|
||||
>
|
||||
<template v-if="invitationsResponse.data.invitations.length">
|
||||
<tr
|
||||
v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]"
|
||||
:key="i.classId"
|
||||
|
@ -346,48 +379,65 @@
|
|||
<ClassDisplay :classId="i.classId" />
|
||||
</td>
|
||||
<td>
|
||||
{{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}
|
||||
{{ (i.sender as TeacherDTO).firstName + ' ' + (i.sender as TeacherDTO).lastName }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span v-if="!isSmAndDown">
|
||||
<div>
|
||||
<v-btn
|
||||
color="green"
|
||||
@click="handleInvitation(i, true)"
|
||||
class="mr-2"
|
||||
>
|
||||
{{ t("accept") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="handleInvitation(i, false)"
|
||||
>
|
||||
{{ t("deny") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</span>
|
||||
<span v-if="!isSmAndDown">
|
||||
<div>
|
||||
<v-btn
|
||||
color="green"
|
||||
@click="handleInvitation(i, true)"
|
||||
class="mr-2"
|
||||
>
|
||||
{{ t('accept') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="handleInvitation(i, false)"
|
||||
>
|
||||
{{ t('deny') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>
|
||||
<div>
|
||||
<v-btn
|
||||
@click="handleInvitation(i, true)"
|
||||
class="mr-2"
|
||||
icon="mdi-check-circle"
|
||||
color="green"
|
||||
variant="text"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="handleInvitation(i, false)"
|
||||
class="mr-2"
|
||||
icon="mdi-close-circle"
|
||||
color="red"
|
||||
variant="text"
|
||||
>
|
||||
</v-btn></div
|
||||
></span>
|
||||
<div>
|
||||
<v-btn
|
||||
@click="handleInvitation(i, true)"
|
||||
class="mr-2"
|
||||
icon="mdi-check-circle"
|
||||
color="green"
|
||||
variant="text"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="handleInvitation(i, false)"
|
||||
class="mr-2"
|
||||
icon="mdi-close-circle"
|
||||
color="red"
|
||||
variant="text"
|
||||
>
|
||||
</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>
|
||||
</v-container>
|
||||
|
@ -404,7 +454,7 @@
|
|||
max-width="400px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">{{ t("code") }}</v-card-title>
|
||||
<v-card-title class="headline">{{ t('code') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="selectedCode"
|
||||
|
@ -417,7 +467,7 @@
|
|||
v-if="copied"
|
||||
class="text-center mt-2"
|
||||
>
|
||||
{{ t("copied") }}
|
||||
{{ t('copied') }}
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
</v-card-text>
|
||||
|
@ -430,7 +480,7 @@
|
|||
copied = false;
|
||||
"
|
||||
>
|
||||
{{ t("close") }}
|
||||
{{ t('close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
@ -438,105 +488,49 @@
|
|||
</main>
|
||||
</template>
|
||||
<style scoped>
|
||||
.header {
|
||||
font-weight: bold !important;
|
||||
background-color: #0e6942;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
h2 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
table thead th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.link {
|
||||
color: #0b75bb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 850px) {
|
||||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0b75bb;
|
||||
text-decoration: underline;
|
||||
.sheet {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 850px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.custom-breakpoint {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.responsive-col {
|
||||
max-width: 100% !important;
|
||||
flex-basis: 100% !important;
|
||||
}
|
||||
.custom-breakpoint {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.responsive-col {
|
||||
max-width: 100% !important;
|
||||
flex-basis: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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,31 +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;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
.dropdowns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -114,12 +90,6 @@
|
|||
min-width: 100px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.dropdowns {
|
||||
flex-direction: column;
|
||||
|
|
|
@ -17,32 +17,52 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-field-container">
|
||||
<learning-path-search-field class="search-field"></learning-path-search-field>
|
||||
</div>
|
||||
<v-container class="search-page-container">
|
||||
<v-row
|
||||
justify="center"
|
||||
class="mb-6"
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="8"
|
||||
md="6"
|
||||
lg="4"
|
||||
>
|
||||
<learning-path-search-field class="search-field" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<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
|
||||
v-if="!query"
|
||||
icon="mdi-magnify"
|
||||
:title="t('enterSearchTerm')"
|
||||
:text="t('enterSearchTermDescription')"
|
||||
></v-empty-state>
|
||||
</div>
|
||||
<v-row justify="center">
|
||||
<v-col cols="12">
|
||||
<using-query-result
|
||||
:query-result="searchQueryResults"
|
||||
v-slot="{ data }: { data: LearningPath[] }"
|
||||
>
|
||||
<learning-paths-grid :learning-paths="data" />
|
||||
</using-query-result>
|
||||
|
||||
<div
|
||||
v-if="!query"
|
||||
class="empty-state-container"
|
||||
>
|
||||
<v-empty-state
|
||||
icon="mdi-magnify"
|
||||
:title="t('enterSearchTerm')"
|
||||
:text="t('enterSearchTermDescription')"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-field-container {
|
||||
display: block;
|
||||
margin: 20px;
|
||||
.search-page-container {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
max-width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue