diff --git a/frontend/src/assets/lang/nl.json b/frontend/src/assets/lang/nl.json index fef6810d..60f293f2 100644 --- a/frontend/src/assets/lang/nl.json +++ b/frontend/src/assets/lang/nl.json @@ -21,8 +21,8 @@ "dashboard": { "courses": "Mijn vakken", "projects": "Lopende projecten", - "no_projects": "Geen projecten beschikbaar voor dit academiejaar.", - "no_courses": "Geen vakken beschikbaar voor dit academiejaar." + "no_projects": "Geen projecten gevonden.", + "no_courses": "Geen vakken gevonden." }, "login": { "title": "Inloggen", @@ -48,7 +48,8 @@ "description": "Beschrijving", "year": "Academiejaar", "search": { - + "title": "Zoek een vak", + "results": "{0} vakken gevonden voor ingestelde filters" } } }, diff --git a/frontend/src/components/courses/CourseList.vue b/frontend/src/components/courses/CourseList.vue index b51956a3..ec1d1143 100644 --- a/frontend/src/components/courses/CourseList.vue +++ b/frontend/src/components/courses/CourseList.vue @@ -36,7 +36,7 @@ const { t } = useI18n(); diff --git a/frontend/src/composables/configuration.ts b/frontend/src/composables/configuration.ts deleted file mode 100644 index ef7976b6..00000000 --- a/frontend/src/composables/configuration.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type Ref, ref } from 'vue'; -import { endpoints } from '../config/endpoints.ts'; -import { environment } from '../config/environment.ts'; - -interface Config { - environment: any; - endpoints: any; -} - -export function useConfig(): { config: Ref } { - const config = ref({ - environment, - endpoints, - }); - - return { config }; -} diff --git a/frontend/src/composables/filters/filter.ts b/frontend/src/composables/filters/filter.ts new file mode 100644 index 00000000..92ef191e --- /dev/null +++ b/frontend/src/composables/filters/filter.ts @@ -0,0 +1,15 @@ +import { type Ref, ref } from 'vue'; +import { type Filter } from '@/types/filter/Filter.ts'; + +export interface FilterState { + filter: Ref; +} + +export function useFilter(initial: Filter): FilterState { + /* State */ + const filter = ref(initial); + + return { + filter, + }; +} diff --git a/frontend/src/composables/filters/paginator.ts b/frontend/src/composables/filters/paginator.ts new file mode 100644 index 00000000..98d2bfb4 --- /dev/null +++ b/frontend/src/composables/filters/paginator.ts @@ -0,0 +1,67 @@ +import { computed, type Ref, ref, watch } from 'vue'; +import { type LocationQuery, useRoute, useRouter } from 'vue-router'; + +export interface PaginatorState { + page: Ref; + pageSize: Ref; + first: Ref; + paginate: (newFirst: number) => Promise; +} + +export function usePaginator( + initialPage: number = 1, + initialPageSize: number = 20, +): PaginatorState { + /* Composables */ + const { query } = useRoute(); + const { push } = useRouter(); + + /* State */ + const page = ref(initialPage); + const pageSize = ref(initialPageSize); + + /* Watchers */ + watch( + () => query, + (query: LocationQuery) => { + if (query.page !== undefined) { + page.value = parseInt(query.page as string); + } + + if (query.pageSize !== undefined) { + pageSize.value = parseInt(query.pageSize as string); + } + }, + { immediate: true }, + ); + + /* Computed */ + const first = computed({ + get: () => (page.value - 1) * pageSize.value, + set: (value: number) => + (page.value = Math.floor(value / pageSize.value) + 1), + }); + + /** + * Paginate using a new first item index. + * + * @param newFirst + */ + async function paginate(newFirst: number): Promise { + first.value = newFirst; + + await push({ + query: { + page: page.value, + pageSize: pageSize.value, + }, + }); + } + + return { + page, + pageSize, + first, + paginate, + }; +} diff --git a/frontend/src/composables/services/courses.service.ts b/frontend/src/composables/services/courses.service.ts index 8623850c..bb356e23 100644 --- a/frontend/src/composables/services/courses.service.ts +++ b/frontend/src/composables/services/courses.service.ts @@ -8,15 +8,20 @@ import { deleteId, getPaginatedList, } from '@/composables/services/helpers.ts'; -import { type Filters, type PaginationResponse } from '@/types/Pagination.ts'; +import { type PaginatorResponse } from '@/types/filter/Paginator.ts'; +import { type Filter } from '@/types/filter/Filter.ts'; interface CoursesState { - pagination: Ref | null>; + pagination: Ref | null>; courses: Ref; course: Ref; getCourseByID: (id: string) => Promise; getCourses: () => Promise; - searchCourses: (filters: Filters) => Promise; + searchCourses: ( + filters: Filter, + page: number, + pageSize: number, + ) => Promise; getCoursesByStudent: (studentId: string) => Promise; getCoursesByTeacher: (teacherId: string) => Promise; getCourseByAssistant: (assistantId: string) => Promise; @@ -26,7 +31,7 @@ interface CoursesState { } export function useCourses(): CoursesState { - const pagination = ref | null>(null); + const pagination = ref | null>(null); const courses = ref(null); const course = ref(null); @@ -40,11 +45,17 @@ export function useCourses(): CoursesState { await getList(endpoint, courses, Course.fromJSON); } - async function searchCourses(filters: Filters): Promise { + async function searchCourses( + filters: Filter, + page: number, + pageSize: number, + ): Promise { const endpoint = endpoints.courses.search; await getPaginatedList( endpoint, filters, + page, + pageSize, pagination, Course.fromJSON, ); diff --git a/frontend/src/composables/services/helpers.ts b/frontend/src/composables/services/helpers.ts index cc0649dc..ac77df4f 100644 --- a/frontend/src/composables/services/helpers.ts +++ b/frontend/src/composables/services/helpers.ts @@ -1,9 +1,10 @@ import { type AxiosError } from 'axios'; -import { client } from '@/composables/axios.ts'; +import { client } from '@/config/axios.ts'; import { type Ref } from 'vue'; import { useMessagesStore } from '@/store/messages.store.ts'; -import { i18n } from '../i18n'; -import { type Filters, type PaginationResponse } from '@/types/Pagination.ts'; +import { i18n } from '@/config/i18n.ts'; +import { type PaginatorResponse } from '@/types/filter/Paginator.ts'; +import { type Filter } from '@/types/filter/Filter.ts'; /** * Get an item given its ID. @@ -115,6 +116,7 @@ export async function getList( } catch (error: any) { processError(error); console.error(error); // Log the error for debugging + ref.value = []; // Set the ref to an empty array throw error; // Re-throw the error to the caller } } @@ -123,19 +125,27 @@ export async function getList( * Get a paginated list of items. * * @param endpoint - * @param params + * @param filters + * @param page + * @param pageSize * @param pagination * @param fromJson */ export async function getPaginatedList( endpoint: string, - params: Filters, - pagination: Ref | null>, + filters: Filter, + page: number, + pageSize: number, + pagination: Ref | null>, fromJson: (data: any) => T, ): Promise { try { const response = await client.get(endpoint, { - params, + params: { + ...filters, + page, + page_size: pageSize, + }, }); pagination.value = { @@ -145,6 +155,14 @@ export async function getPaginatedList( } catch (error: any) { processError(error); console.error(error); // Log the error for debugging + + pagination.value = { + // Set the ref to an empty array + ...error.data, + count: 0, + results: [], + }; + throw error; // Re-throw the error to the caller } } @@ -174,6 +192,7 @@ export async function getListMerged( } catch (error: any) { processError(error); console.error(error); // Log the error for debugging + ref.value = []; // Set the ref to an empty array throw error; // Re-throw the error to the caller } } diff --git a/frontend/src/composables/axios.ts b/frontend/src/config/axios.ts similarity index 92% rename from frontend/src/composables/axios.ts rename to frontend/src/config/axios.ts index b5cbe90f..554cb61f 100644 --- a/frontend/src/composables/axios.ts +++ b/frontend/src/config/axios.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import Cookie from 'js-cookie'; -import { i18n } from '@/composables/i18n'; +import { i18n } from '@/config/i18n.ts'; const { locale } = i18n.global; diff --git a/frontend/src/composables/i18n.ts b/frontend/src/config/i18n.ts similarity index 100% rename from frontend/src/composables/i18n.ts rename to frontend/src/config/i18n.ts diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 2bdc5b4a..0eae269f 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -3,7 +3,7 @@ import router from '@/router/router'; import PrimeVue from 'primevue/config'; import ToastService from 'primevue/toastservice'; import Ripple from 'primevue/ripple'; -import { i18n } from '@/composables/i18n'; +import { i18n } from '@/config/i18n.ts'; import { createApp } from 'vue'; import { createPinia } from 'pinia'; diff --git a/frontend/src/store/authentication.store.ts b/frontend/src/store/authentication.store.ts index 2d38c74d..a069588c 100644 --- a/frontend/src/store/authentication.store.ts +++ b/frontend/src/store/authentication.store.ts @@ -3,7 +3,7 @@ import { defineStore } from 'pinia'; import { type Role, User } from '@/types/users/User.ts'; import { endpoints } from '@/config/endpoints.ts'; import { useMessagesStore } from '@/store/messages.store.ts'; -import { client } from '@/composables/axios.ts'; +import { client } from '@/config/axios.ts'; import { useLocalStorage } from '@vueuse/core'; import { computed, ref, watch } from 'vue'; import { useAssistant } from '@/composables/services/assistant.service'; diff --git a/frontend/src/types/Pagination.ts b/frontend/src/types/Pagination.ts deleted file mode 100644 index 8c4a9b79..00000000 --- a/frontend/src/types/Pagination.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface Filters { - search: string; - years?: number[]; - faculties?: string[]; - pagination?: PaginationRequest; -} - -export interface PaginationRequest { - page: number; - page_size: number; -} - -export interface PaginationResponse { - count: number; - next: string | null; - previous: string | null; - results: T[]; -} diff --git a/frontend/src/types/filter/Filter.ts b/frontend/src/types/filter/Filter.ts new file mode 100644 index 00000000..edd36982 --- /dev/null +++ b/frontend/src/types/filter/Filter.ts @@ -0,0 +1,10 @@ +export const COURSE_FILTER = { + search: '', + faculties: [], + years: [], +}; + +export interface Filter { + search: string; + [key: string]: any; +} diff --git a/frontend/src/types/filter/Paginator.ts b/frontend/src/types/filter/Paginator.ts new file mode 100644 index 00000000..4e6972fb --- /dev/null +++ b/frontend/src/types/filter/Paginator.ts @@ -0,0 +1,6 @@ +export interface PaginatorResponse { + count: number; + next: string | null; + previous: string | null; + results: T[]; +} diff --git a/frontend/src/views/authentication/VerifyView.vue b/frontend/src/views/authentication/VerifyView.vue index 68296e02..b2898ff6 100644 --- a/frontend/src/views/authentication/VerifyView.vue +++ b/frontend/src/views/authentication/VerifyView.vue @@ -1,18 +1,17 @@ diff --git a/frontend/src/views/courses/SearchCourseView.vue b/frontend/src/views/courses/SearchCourseView.vue index 273e448e..1818ccbf 100644 --- a/frontend/src/views/courses/SearchCourseView.vue +++ b/frontend/src/views/courses/SearchCourseView.vue @@ -5,45 +5,56 @@ import InputText from 'primevue/inputtext'; import IconField from 'primevue/iconfield'; import InputIcon from 'primevue/inputicon'; import Checkbox from 'primevue/checkbox'; +import Paginator from 'primevue/paginator'; import Title from '@/components/layout/Title.vue'; import BaseLayout from '@/components/layout/BaseLayout.vue'; import CourseList from '@/components/courses/CourseList.vue'; -import { onMounted, ref, watch } from 'vue'; +import { onMounted, watch } from 'vue'; import { useCourses } from '@/composables/services/courses.service.ts'; import { useAuthStore } from '@/store/authentication.store.ts'; import { useFaculty } from '@/composables/services/faculties.service.ts'; -import { type Filters } from '@/types/Pagination.ts'; import { storeToRefs } from 'pinia'; +import { useI18n } from 'vue-i18n'; +import { useFilter } from '@/composables/filters/filter.ts'; +import { usePaginator } from '@/composables/filters/paginator.ts'; import { watchDebounced } from '@vueuse/core'; -import { User } from '@/types/users/User.ts'; +import { COURSE_FILTER } from '@/types/filter/Filter.ts'; /* Composable injections */ +const { t } = useI18n(); const { user } = storeToRefs(useAuthStore()); const { faculties, getFaculties } = useFaculty(); const { pagination, searchCourses } = useCourses(); - -/* State */ -const filters = ref({ - search: '', - faculties: [], - years: [User.getAcademicYear()], -}); - -/* Watch the filters to reset the data */ -watch(filters, () => (pagination.value = null), { deep: true }); - -/* Watch the filters to search for courses */ -watchDebounced( - filters, - async () => { - await searchCourses(filters.value); - }, - { deep: true, immediate: true, debounce: 500 }, -); +const { paginate, page, first, pageSize } = usePaginator(); +const { filter } = useFilter(COURSE_FILTER); /* Fetch the faculties */ onMounted(async () => { await getFaculties(); + + /* Reset current page on filter changes */ + watch( + filter, + () => { + paginate(0); + pagination.value = null; + }, + { deep: true }, + ); + + /* Search courses on page change */ + watch(page, async () => { + await searchCourses(filter.value, page.value, pageSize.value); + }); + + /* Search courses on filter change */ + watchDebounced( + filter, + async () => { + await searchCourses(filter.value, page.value, pageSize.value); + }, + { debounce: 500, immediate: true, deep: true }, + ); }); @@ -56,7 +67,7 @@ onMounted(async () => { @@ -67,7 +78,7 @@ onMounted(async () => { :key="faculty.id" class="flex align-items-center mb-2"> @@ -82,7 +93,7 @@ onMounted(async () => { :key="year" class="flex align-items-center mb-2"> @@ -94,11 +105,22 @@ onMounted(async () => {
- Zoek een vak + + {{ t('views.courses.search.title') }} + +

+ {{ t('views.courses.search.results', [pagination.count]) }} +

+