diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cd0a094b..9dc299c9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,8 @@ "name": "vite-vue-typescript-starter", "version": "0.0.0", "dependencies": { + "@vuelidate/core": "^2.0.3", + "@vuelidate/validators": "^2.0.4", "@vueuse/core": "^10.9.0", "axios": "^1.6.8", "js-cookie": "^3.0.5", @@ -19,7 +21,8 @@ "primevue": "^3.50.0", "vue": "^3.4.18", "vue-i18n": "^9.10.2", - "vue-router": "^4.3.0" + "vue-router": "^4.3.0", + "vuelidate": "^0.7.7" }, "devDependencies": { "@types/js-cookie": "^3.0.6", @@ -1606,6 +1609,90 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz", "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==" }, + "node_modules/@vuelidate/core": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@vuelidate/core/-/core-2.0.3.tgz", + "integrity": "sha512-AN6l7KF7+mEfyWG0doT96z+47ljwPpZfi9/JrNMkOGLFv27XVZvKzRLXlmDPQjPl/wOB1GNnHuc54jlCLRNqGA==", + "dependencies": { + "vue-demi": "^0.13.11" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^2.0.0 || >=3.0.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vuelidate/core/node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vuelidate/validators": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@vuelidate/validators/-/validators-2.0.4.tgz", + "integrity": "sha512-odTxtUZ2JpwwiQ10t0QWYJkkYrfd0SyFYhdHH44QQ1jDatlZgTh/KRzrWVmn/ib9Gq7H4hFD4e8ahoo5YlUlDw==", + "dependencies": { + "vue-demi": "^0.13.11" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^2.0.0 || >=3.0.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vuelidate/validators/node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@vueuse/core": { "version": "10.9.0", "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz", @@ -7237,6 +7324,15 @@ "typescript": "*" } }, + "node_modules/vuelidate": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/vuelidate/-/vuelidate-0.7.7.tgz", + "integrity": "sha512-pT/U2lDI67wkIqI4tum7cMSIfGcAMfB+Phtqh2ttdXURwvHRBJEAQ0tVbUsW9Upg83Q5QH59bnCoXI7A9JDGnA==", + "engines": { + "node": ">= 4.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index b0422179..f46b10ad 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,8 @@ "lint-fix": "eslint src --ext .ts,.vue --fix" }, "dependencies": { + "@vuelidate/core": "^2.0.3", + "@vuelidate/validators": "^2.0.4", "@vueuse/core": "^10.9.0", "axios": "^1.6.8", "js-cookie": "^3.0.5", @@ -26,7 +28,8 @@ "primevue": "^3.50.0", "vue": "^3.4.18", "vue-i18n": "^9.10.2", - "vue-router": "^4.3.0" + "vue-router": "^4.3.0", + "vuelidate": "^0.7.7" }, "devDependencies": { "@types/js-cookie": "^3.0.6", diff --git a/frontend/src/assets/lang/en.json b/frontend/src/assets/lang/en.json index 4eead9da..5a5da5d3 100644 --- a/frontend/src/assets/lang/en.json +++ b/frontend/src/assets/lang/en.json @@ -22,7 +22,8 @@ "courses": "My courses", "projects": "Current projects", "no_projects": "No projects available for this academic year.", - "no_courses": "No courses available for this academic year." + "no_courses": "No courses available for this academic year.", + "select_course": "Select the course for which you want to create a project:" }, "login": { "title": "Login", @@ -38,9 +39,17 @@ "title": "Calendar" }, "projects": { - "deadline": "Deadline", - "submissionStatus": "Indienstatus", - "groupMembers": "Group members" + "deadline": "Deadline", + "submissionStatus": "Indienstatus", + "groupMembers": "Group members", + "create": "Create new project", + "name": "Project name", + "description": "Description", + "start_date": "Start project", + "group_size": "Number of students in a group (1 for an individual project)", + "max_score": "Maximum score that can be achieved", + "visibility": "Make project visible to students", + "score_visibility": "Make score, when uploaded, automatically visible to students" }, "courses": { "create": "Create course", @@ -85,6 +94,10 @@ "unknown": "An unknown error has occurred." } }, + "validations": { + "required": "This field is required", + "deadline": "The deadline must be after the start date" + }, "primevue": { "startsWith": "Starts with", "contains": "Contains", diff --git a/frontend/src/assets/lang/nl.json b/frontend/src/assets/lang/nl.json index 31ec1044..38c5579c 100644 --- a/frontend/src/assets/lang/nl.json +++ b/frontend/src/assets/lang/nl.json @@ -22,7 +22,8 @@ "courses": "Mijn vakken", "projects": "Lopende projecten", "no_projects": "Geen projecten gevonden.", - "no_courses": "Geen vakken gevonden." + "no_courses": "Geen vakken gevonden.", + "select_course": "Selecteer het vak waarvoor je een project wil maken:" }, "login": { "title": "Inloggen", @@ -40,7 +41,15 @@ "projects": { "deadline": "Deadline", "submissionStatus": "Indienstatus", - "groupMembers": "Groepsleden" + "groupMembers": "Groepsleden", + "create": "Creëer nieuw project", + "name": "Projectnaam", + "description": "Beschrijving", + "start_date": "Start project", + "group_size": "Aantal studenten per groep (1 voor individueel project)", + "max_score": "Maximale te behalen score", + "visibility": "Project zichtbaar maken voor studenten", + "score_visibility": "Maak score, wanneer ingevuld, automatisch zichtbaar voor studenten" }, "courses": { "create": "Creëer vak", @@ -110,6 +119,10 @@ } } }, + "validations": { + "required": "Dit veld is verplicht", + "deadline": "De deadline moet na de startdatum liggen" + }, "primevue": { "accept": "Ja", "addRule": "Voeg regel toe", diff --git a/frontend/src/components/forms/ErrorMessage.vue b/frontend/src/components/forms/ErrorMessage.vue new file mode 100644 index 00000000..349dcb75 --- /dev/null +++ b/frontend/src/components/forms/ErrorMessage.vue @@ -0,0 +1,14 @@ + + + + {{ props.field.$errors[0].$message }} + + + diff --git a/frontend/src/components/projects/ProjectCreateButton.vue b/frontend/src/components/projects/ProjectCreateButton.vue new file mode 100644 index 00000000..164e5fba --- /dev/null +++ b/frontend/src/components/projects/ProjectCreateButton.vue @@ -0,0 +1,72 @@ + + + + + + + + + + + {{ course.name }} + + + + + + diff --git a/frontend/src/composables/services/project.service.ts b/frontend/src/composables/services/project.service.ts index 4e39187f..e29d2519 100644 --- a/frontend/src/composables/services/project.service.ts +++ b/frontend/src/composables/services/project.service.ts @@ -84,7 +84,7 @@ export function useProject(): ProjectState { visible: projectData.visible, archived: projectData.archived, locked_groups: projectData.locked_groups, - start_data: projectData.start_date, + start_date: projectData.start_date, deadline: projectData.deadline, max_score: projectData.max_score, score_visible: projectData.score_visible, diff --git a/frontend/src/router/router.ts b/frontend/src/router/router.ts index d0d87a26..095f2842 100644 --- a/frontend/src/router/router.ts +++ b/frontend/src/router/router.ts @@ -1,9 +1,7 @@ -// import { useUserStore } from '@/stores/userStore'; -// TODO: after pinia setup is done - import DashboardView from '@/views/dashboard/DashboardView.vue'; import CourseView from '@/views/courses/CourseView.vue'; import CreateCourseView from '@/views/courses/CreateCourseView.vue'; +import CreateProjectView from '@/views/projects/CreateProjectView.vue'; import Dummy from '@/components/Dummy.vue'; import LoginView from '@/views/authentication/LoginView.vue'; import CalendarView from '@/views/calendar/CalendarView.vue'; @@ -36,11 +34,7 @@ const routes: RouteRecordRaw[] = [ path: '/courses', children: [ { path: '', component: SearchCourseView, name: 'courses' }, - { - path: 'create', - component: CreateCourseView, - name: 'course-create', - }, + { path: 'create', component: CreateCourseView, name: 'course-create' }, // Single course { path: ':courseId', @@ -52,35 +46,15 @@ const routes: RouteRecordRaw[] = [ path: 'projects', children: [ { path: '', component: Dummy, name: 'projects' }, - { - path: 'create', - component: Dummy, - name: 'project-create', - }, + { path: 'create', component: CreateProjectView, name: 'project-create' }, // Single project { path: ':projectId', children: [ - { - path: '', - component: ProjectView, - name: 'project', - }, - { - path: 'edit', - component: Dummy, - name: 'project-edit', - }, - { - path: 'groups', - component: Dummy, - name: 'project-groups', - }, - { - path: 'submit', - component: Dummy, - name: 'project-submit', - }, + { path: '', component: ProjectView, name: 'project' }, + { path: 'edit', component: Dummy, name: 'project-edit' }, + { path: 'groups', component: Dummy, name: 'project-groups' }, + { path: 'submit', component: Dummy, name: 'project-submit' }, ], }, ], @@ -138,10 +112,7 @@ const routes: RouteRecordRaw[] = [ { path: '/notifications/:id', component: Dummy, name: 'notification' }, // Authentication - { - path: '/auth/', - children: [{ path: 'login', component: LoginView, name: 'login' }], - }, + { path: '/auth/', children: [{ path: 'login', component: LoginView, name: 'login' }] }, // Page not found: redirect to dashboard { path: '/:pathMatch(.*)*', redirect: { name: 'dashboard' } }, diff --git a/frontend/src/views/courses/CreateCourseView.vue b/frontend/src/views/courses/CreateCourseView.vue index 93a519a9..50f5b7cd 100644 --- a/frontend/src/views/courses/CreateCourseView.vue +++ b/frontend/src/views/courses/CreateCourseView.vue @@ -2,39 +2,66 @@ import Calendar from 'primevue/calendar'; import BaseLayout from '@/components/layout/BaseLayout.vue'; import Title from '@/components/layout/Title.vue'; -import { ref } from 'vue'; +import { reactive, computed } from 'vue'; import { useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; +import { useAuthStore } from '@/store/authentication.store.ts'; +import { storeToRefs } from 'pinia'; import InputText from 'primevue/inputtext'; import Textarea from 'primevue/textarea'; import Button from 'primevue/button'; import { Course } from '@/types/Course'; import { useCourses } from '@/composables/services/courses.service'; +import { required, helpers } from '@vuelidate/validators'; +import { useVuelidate } from '@vuelidate/core'; +import ErrorMessage from '@/components/forms/ErrorMessage.vue'; +import { User } from '@/types/users/User.ts'; /* Composable injections */ const { t } = useI18n(); const { push } = useRouter(); +const { user } = storeToRefs(useAuthStore()); /* Service injection */ const { createCourse } = useCourses(); -const courseName = ref(''); -const courseDescription = ref(''); -const courseYear = ref(new Date()); +/* Form content */ +const form = reactive({ + name: '', + description: '', + year: user.value !== null ? new Date(User.getAcademicYear(new Date()), 0, 1) : new Date(), +}); + +// Define validation rules for each form field +const rules = computed(() => { + return { + name: { required: helpers.withMessage(t('validations.required'), required) }, + year: { required: helpers.withMessage(t('validations.required'), required) }, + }; +}); + +// useVuelidate function to perform form validation +const v$ = useVuelidate(rules, form); const submitCourse = async (): Promise => { - // Pass the course data to the service - await createCourse( - new Course( - '', // ID not needed for creation, will be generated by the backend - courseName.value, - courseDescription.value, - courseYear.value.getFullYear(), - ), - ); + // Validate the form + const result = await v$.value.$validate(); + + // Only submit the form if the validation was successful + if (result) { + // Pass the course data to the service + await createCourse( + new Course( + '', // ID not needed for creation, will be generated by the backend + form.name, + form.description, + form.year.getFullYear(), + ), + ); - // Redirect to the dashboard overview - push({ name: 'dashboard' }); + // Redirect to the dashboard overview + push({ name: 'dashboard' }); + } }; @@ -50,19 +77,27 @@ const submitCourse = async (): Promise => { {{ t('views.courses.name') }} - + + {{ t('views.courses.description') }} - + {{ t('views.courses.year') }} - + + + + {{ form.year.getFullYear() }} - {{ form.year.getFullYear() + 1 }} + + + + diff --git a/frontend/src/views/dashboard/DashboardView.vue b/frontend/src/views/dashboard/DashboardView.vue index a6dc5a87..d3a6855b 100644 --- a/frontend/src/views/dashboard/DashboardView.vue +++ b/frontend/src/views/dashboard/DashboardView.vue @@ -3,7 +3,11 @@ import BaseLayout from '@/components/layout/BaseLayout.vue'; import { useAuthStore } from '@/store/authentication.store.ts'; import { storeToRefs } from 'pinia'; import StudentDashboardView from '@/views/dashboard/roles/StudentDashboardView.vue'; +import TeacherDashboardView from './roles/TeacherDashboardView.vue'; +import AssistantDashboardView from './roles/AssistantDashboardView.vue'; import { type Student } from '@/types/users/Student'; +import { type Teacher } from '@/types/users/Teacher'; +import { type Assistant } from '@/types/users/Assistant'; /* Service injection */ const { user } = storeToRefs(useAuthStore()); @@ -12,6 +16,8 @@ const { user } = storeToRefs(useAuthStore()); + + diff --git a/frontend/src/views/dashboard/roles/AssistantDashboardView.vue b/frontend/src/views/dashboard/roles/AssistantDashboardView.vue new file mode 100644 index 00000000..18763b15 --- /dev/null +++ b/frontend/src/views/dashboard/roles/AssistantDashboardView.vue @@ -0,0 +1,64 @@ + + + + + + + {{ t('views.dashboard.courses') }} + + + + + + + + + + {{ t('views.dashboard.projects') }} + + + + + + + + + diff --git a/frontend/src/views/dashboard/roles/StudentDashboardView.vue b/frontend/src/views/dashboard/roles/StudentDashboardView.vue index e88bfdc8..767c49e3 100644 --- a/frontend/src/views/dashboard/roles/StudentDashboardView.vue +++ b/frontend/src/views/dashboard/roles/StudentDashboardView.vue @@ -1,12 +1,9 @@ + + + + + + {{ t('views.dashboard.courses') }} + + + + + + + + + + + + + + + + + {{ t('views.dashboard.projects') }} + + + + + + + + + diff --git a/frontend/src/views/projects/CreateProjectView.vue b/frontend/src/views/projects/CreateProjectView.vue new file mode 100644 index 00000000..13f83181 --- /dev/null +++ b/frontend/src/views/projects/CreateProjectView.vue @@ -0,0 +1,189 @@ + + + + + + + + {{ t('views.projects.create') }} + + + + + + {{ t('views.projects.name') }} + + + + + + + {{ t('views.projects.description') }} + + + + + + {{ t('views.projects.start_date') }} + + + + + + + {{ t('views.projects.deadline') }} + + + + + + + {{ t('views.projects.group_size') }} + + + + + + + {{ t('views.projects.max_score') }} + + + + + + + {{ t('views.projects.visibility') }} + + + + + + {{ t('views.projects.score_visibility') }} + + + + + + + + + + + + + +