Merging origin/dev into feat/assignment-page
This commit is contained in:
		
						commit
						2868435c62
					
				
					 87 changed files with 989 additions and 1002 deletions
				
			
		|  | @ -19,7 +19,16 @@ See [Vite Configuration Reference](https://vite.dev/config/). | |||
| ## Project Setup | ||||
| 
 | ||||
| ```sh | ||||
| # Install dependencies | ||||
| npm install | ||||
| 
 | ||||
| # Start necessary services for development | ||||
| cd ../ # Go to the root of the repository | ||||
| docker compose up -d | ||||
| # Start the backend | ||||
| cd backend | ||||
| cp .env.development.example .env.development.local | ||||
| npm run dev # or npm run build && npm run start | ||||
| ``` | ||||
| 
 | ||||
| ### Compile and Hot-Reload for Development | ||||
|  |  | |||
|  | @ -42,7 +42,7 @@ | |||
|         "jsdom": "^26.0.0", | ||||
|         "npm-run-all2": "^7.0.2", | ||||
|         "typescript": "~5.7.3", | ||||
|         "vite": "^6.1.0", | ||||
|         "vite": "^6.1.2", | ||||
|         "vite-plugin-vue-devtools": "^7.7.2", | ||||
|         "vitest": "^3.0.5", | ||||
|         "vue-tsc": "^2.2.2" | ||||
|  |  | |||
|  | @ -1,10 +1,26 @@ | |||
| <script setup lang="ts"> | ||||
|     import auth from "@/services/auth/auth-service.ts"; | ||||
|     import MenuBar from "@/components/MenuBar.vue"; | ||||
|     import { useRoute } from "vue-router"; | ||||
|     import { computed } from "vue"; | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
|     auth.loadUser(); | ||||
| 
 | ||||
|     interface RouteMeta { | ||||
|         requiresAuth?: boolean; | ||||
|     } | ||||
| 
 | ||||
|     const showMenuBar = computed(() => (route.meta as RouteMeta).requiresAuth && auth.authState.user); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <router-view /> | ||||
|     <v-app> | ||||
|         <menu-bar v-if="showMenuBar"></menu-bar> | ||||
|         <v-main> | ||||
|             <router-view /> | ||||
|         </v-main> | ||||
|     </v-app> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
|  |  | |||
|  | @ -1,47 +1,56 @@ | |||
| <script setup lang="ts"> | ||||
| import ThemeCard from "@/components/ThemeCard.vue"; | ||||
| import { ref, watchEffect, computed } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { AGE_TO_THEMES, THEMESITEMS } from "@/utils/constants.ts"; | ||||
| import { useThemeQuery } from "@/queries/themes.ts"; | ||||
|     import ThemeCard from "@/components/ThemeCard.vue"; | ||||
|     import { ref, watchEffect, computed } from "vue"; | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import { AGE_TO_THEMES, THEMESITEMS } from "@/utils/constants.ts"; | ||||
|     import { useThemeQuery } from "@/queries/themes.ts"; | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|     selectedTheme: { type: String, required: true }, | ||||
|     selectedAge: { type: String, required: true } | ||||
| }); | ||||
|     const props = defineProps({ | ||||
|         selectedTheme: { type: String, required: true }, | ||||
|         selectedAge: { type: String, required: true }, | ||||
|     }); | ||||
| 
 | ||||
| const { locale } = useI18n(); | ||||
| const language = computed(() => locale.value); | ||||
|     const { locale } = useI18n(); | ||||
|     const language = computed(() => locale.value); | ||||
| 
 | ||||
| const { data: allThemes, isLoading, error } = useThemeQuery(language); | ||||
|     const { data: allThemes, isLoading, error } = useThemeQuery(language); | ||||
| 
 | ||||
| const allCards = ref([]); | ||||
| const cards = ref([]); | ||||
|     const allCards = ref([]); | ||||
|     const cards = ref([]); | ||||
| 
 | ||||
| watchEffect(() => { | ||||
|     const themes = allThemes.value ?? []; | ||||
|     allCards.value = themes; | ||||
|     watchEffect(() => { | ||||
|         const themes = allThemes.value ?? []; | ||||
|         allCards.value = themes; | ||||
| 
 | ||||
|     if (props.selectedTheme) { | ||||
|         cards.value = themes.filter((theme) => | ||||
|             THEMESITEMS[props.selectedTheme]?.includes(theme.key) && | ||||
|             AGE_TO_THEMES[props.selectedAge]?.includes(theme.key) | ||||
|         ); | ||||
|     } else { | ||||
|         cards.value = themes; | ||||
|     } | ||||
| }); | ||||
|         if (props.selectedTheme) { | ||||
|             cards.value = themes.filter( | ||||
|                 (theme) => | ||||
|                     THEMESITEMS[props.selectedTheme]?.includes(theme.key) && | ||||
|                     AGE_TO_THEMES[props.selectedAge]?.includes(theme.key), | ||||
|             ); | ||||
|         } else { | ||||
|             cards.value = themes; | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <template> | ||||
|     <v-container> | ||||
|         <div v-if="isLoading" class="text-center py-10"> | ||||
|             <v-progress-circular indeterminate color="primary" /> | ||||
|         <div | ||||
|             v-if="isLoading" | ||||
|             class="text-center py-10" | ||||
|         > | ||||
|             <v-progress-circular | ||||
|                 indeterminate | ||||
|                 color="primary" | ||||
|             /> | ||||
|             <p>Loading...</p> | ||||
|         </div> | ||||
| 
 | ||||
|         <div v-else-if="error" class="text-center py-10 text-error"> | ||||
|         <div | ||||
|             v-else-if="error" | ||||
|             class="text-center py-10 text-error" | ||||
|         > | ||||
|             <v-icon large>mdi-alert-circle</v-icon> | ||||
|             <p>Error loading: {{ error.message }}</p> | ||||
|         </div> | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
|         { name: "English", code: "en" }, | ||||
|         { name: "Nederlands", code: "nl" }, | ||||
|         { name: "Français", code: "fr" }, | ||||
|         { name: "Deutsch", code: "de" } | ||||
|         { name: "Deutsch", code: "de" }, | ||||
|     ]); | ||||
| 
 | ||||
|     // Logic to change the language of the website to the selected language | ||||
|  | @ -31,79 +31,95 @@ | |||
|         localStorage.setItem("user-lang", langCode); | ||||
|     }; | ||||
| 
 | ||||
|     // contains functionality to let the collapsed menu appear and disappear | ||||
|     // when the screen size varies | ||||
|     // Contains functionality to let the collapsed menu appear and disappear | ||||
|     // When the screen size varies | ||||
|     const drawer = ref(false); | ||||
| 
 | ||||
|     // when the user wants to logout, a popup is shown to verify this | ||||
|     // if verified, the user should be logged out | ||||
|     // When the user wants to logout, a popup is shown to verify this | ||||
|     // If verified, the user should be logged out | ||||
|     const performLogout = () => { | ||||
|         auth.logout(); | ||||
|     }; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <main> | ||||
|         <v-app class="menu_collapsed"> | ||||
|             <v-app-bar | ||||
|                 app | ||||
|                 style="background-color: #f6faf2" | ||||
|     <v-app-bar | ||||
|         class="app-bar" | ||||
|         app | ||||
|     > | ||||
|         <v-app-bar-nav-icon | ||||
|             class="menu_collapsed" | ||||
|             @click="drawer = !drawer" | ||||
|         /> | ||||
|         <router-link | ||||
|             to="/user" | ||||
|             class="dwengo_home" | ||||
|         > | ||||
|             <div> | ||||
|                 <img | ||||
|                     class="dwengo_logo" | ||||
|                     alt="Dwengo logo" | ||||
|                     :src="dwengoLogo" | ||||
|                 /> | ||||
|                 <p class="caption"> | ||||
|                     {{ t(`${role}`) }} | ||||
|                 </p> | ||||
|             </div> | ||||
|         </router-link> | ||||
|         <v-toolbar-items class="menu"> | ||||
|             <v-btn | ||||
|                 class="menu_item" | ||||
|                 variant="text" | ||||
|                 to="/user/assignment" | ||||
|             > | ||||
|                 <template v-slot:prepend> | ||||
|                     <v-app-bar-nav-icon @click="drawer = !drawer" /> | ||||
|                 </template> | ||||
| 
 | ||||
|                 <v-app-bar-title> | ||||
|                     <router-link | ||||
|                         to="/user" | ||||
|                         class="dwengo_home" | ||||
|                 {{ t("assignments") }} | ||||
|             </v-btn> | ||||
|             <v-btn | ||||
|                 class="menu_item" | ||||
|                 variant="text" | ||||
|                 to="/user/class" | ||||
|             > | ||||
|                 {{ t("classes") }} | ||||
|             </v-btn> | ||||
|             <v-btn | ||||
|                 class="menu_item" | ||||
|                 variant="text" | ||||
|                 to="/user/discussion" | ||||
|             > | ||||
|                 {{ t("discussions") }} | ||||
|             </v-btn> | ||||
|             <v-menu open-on-hover> | ||||
|                 <template v-slot:activator="{ props }"> | ||||
|                     <v-btn | ||||
|                         v-bind="props" | ||||
|                         icon | ||||
|                         variant="text" | ||||
|                     > | ||||
|                         <div> | ||||
|                             <img | ||||
|                                 class="dwengo_logo" | ||||
|                                 :src="dwengoLogo" | ||||
|                                 style="width: 100px" | ||||
|                             /> | ||||
|                             <p | ||||
|                                 class="caption" | ||||
|                                 style="font-size: smaller" | ||||
|                             > | ||||
|                                 {{ t(`${role}`) }} | ||||
|                             </p> | ||||
|                         </div> | ||||
|                     </router-link> | ||||
|                 </v-app-bar-title> | ||||
| 
 | ||||
|                 <v-spacer></v-spacer> | ||||
| 
 | ||||
|                 <v-menu open-on-hover> | ||||
|                     <template v-slot:activator="{ props }"> | ||||
|                         <v-btn | ||||
|                             v-bind="props" | ||||
|                             icon | ||||
|                             variant="text" | ||||
|                         > | ||||
|                             <v-icon | ||||
|                                 icon="mdi-translate" | ||||
|                                 size="small" | ||||
|                                 color="#0e6942" | ||||
|                             ></v-icon> | ||||
|                         </v-btn> | ||||
|                     </template> | ||||
|                     <v-list> | ||||
|                         <v-list-item | ||||
|                             v-for="(language, index) in languages" | ||||
|                             :key="index" | ||||
|                             @click="changeLanguage(language.code)" | ||||
|                         > | ||||
|                             <v-list-item-title>{{ language.name }}</v-list-item-title> | ||||
|                         </v-list-item> | ||||
|                     </v-list> | ||||
|                 </v-menu> | ||||
| 
 | ||||
|                         <v-icon | ||||
|                             icon="mdi-translate" | ||||
|                             size="small" | ||||
|                             color="#0e6942" | ||||
|                         ></v-icon> | ||||
|                     </v-btn> | ||||
|                 </template> | ||||
|                 <v-list> | ||||
|                     <v-list-item | ||||
|                         v-for="(language, index) in languages" | ||||
|                         :key="index" | ||||
|                         @click="changeLanguage(language.code)" | ||||
|                     > | ||||
|                         <v-list-item-title>{{ language.name }}</v-list-item-title> | ||||
|                     </v-list-item> | ||||
|                 </v-list> | ||||
|             </v-menu> | ||||
|         </v-toolbar-items> | ||||
|         <v-spacer></v-spacer> | ||||
|         <v-dialog max-width="500"> | ||||
|             <template v-slot:activator="{ props: activatorProps }"> | ||||
|                 <v-btn | ||||
|                     @click="performLogout" | ||||
|                     text | ||||
|                     v-bind="activatorProps" | ||||
|                     :rounded="true" | ||||
|                     variant="text" | ||||
|                 > | ||||
|                     <v-tooltip | ||||
|                         :text="t('logout')" | ||||
|  | @ -115,201 +131,81 @@ | |||
|                                 icon="mdi-logout" | ||||
|                                 size="x-large" | ||||
|                                 color="#0e6942" | ||||
|                             /> | ||||
|                             > | ||||
|                             </v-icon> | ||||
|                         </template> | ||||
|                     </v-tooltip> | ||||
|                 </v-btn> | ||||
|             </v-app-bar> | ||||
|             </template> | ||||
| 
 | ||||
|             <v-navigation-drawer | ||||
|                 v-model="drawer" | ||||
|                 app | ||||
|             > | ||||
|                 <v-list> | ||||
|                     <v-list-item | ||||
|                         to="/user/assignment" | ||||
|                         link | ||||
|                     > | ||||
|                         <v-list-item-content> | ||||
|                             <v-list-item-title class="menu_item">{{ t("assignments") }}</v-list-item-title> | ||||
|                         </v-list-item-content> | ||||
|                     </v-list-item> | ||||
|             <template v-slot:default="{ isActive }"> | ||||
|                 <v-card :title="t('logoutVerification')"> | ||||
|                     <v-card-actions> | ||||
|                         <v-spacer></v-spacer> | ||||
| 
 | ||||
|                     <v-list-item | ||||
|                         to="/user/class" | ||||
|                         link | ||||
|                     > | ||||
|                         <v-list-item-content> | ||||
|                             <v-list-item-title class="menu_item">{{ t("classes") }}</v-list-item-title> | ||||
|                         </v-list-item-content> | ||||
|                     </v-list-item> | ||||
| 
 | ||||
|                     <v-list-item | ||||
|                         to="/user/discussion" | ||||
|                         link | ||||
|                     > | ||||
|                         <v-list-item-content> | ||||
|                             <v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title> | ||||
|                         </v-list-item-content> | ||||
|                     </v-list-item> | ||||
|                 </v-list> | ||||
|             </v-navigation-drawer> | ||||
|         </v-app> | ||||
| 
 | ||||
|         <nav class="menu"> | ||||
|             <div class="left"> | ||||
|                 <ul> | ||||
|                     <li> | ||||
|                         <router-link | ||||
|                             to="/user" | ||||
|                             class="dwengo_home" | ||||
|                         > | ||||
|                             <img | ||||
|                                 class="dwengo_logo" | ||||
|                                 :src="dwengoLogo" | ||||
|                             /> | ||||
|                             <p class="caption"> | ||||
|                                 {{ t(`${role}`) }} | ||||
|                             </p> | ||||
|                         </router-link> | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <router-link | ||||
|                             :to="`/user/assignment`" | ||||
|                             class="menu_item" | ||||
|                         > | ||||
|                             {{ t("assignments") }} | ||||
|                         </router-link> | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <router-link | ||||
|                             to="/user/class" | ||||
|                             class="menu_item" | ||||
|                             >{{ t("classes") }}</router-link | ||||
|                         > | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <router-link | ||||
|                             to="/user/discussion" | ||||
|                             class="menu_item" | ||||
|                             >{{ t("discussions") }} | ||||
|                         </router-link> | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <v-menu open-on-hover> | ||||
|                             <template v-slot:activator="{ props }"> | ||||
|                                 <v-btn | ||||
|                                     v-bind="props" | ||||
|                                     icon | ||||
|                                     variant="text" | ||||
|                                 > | ||||
|                                     <v-icon | ||||
|                                         icon="mdi-translate" | ||||
|                                         size="small" | ||||
|                                         color="#0e6942" | ||||
|                                     ></v-icon> | ||||
|                                 </v-btn> | ||||
|                             </template> | ||||
|                             <v-list> | ||||
|                                 <v-list-item | ||||
|                                     v-for="(language, index) in languages" | ||||
|                                     :key="index" | ||||
|                                     @click="changeLanguage(language.code)" | ||||
|                                 > | ||||
|                                     <v-list-item-title>{{ language.name }}</v-list-item-title> | ||||
|                                 </v-list-item> | ||||
|                             </v-list> | ||||
|                         </v-menu> | ||||
|                     </li> | ||||
|                 </ul> | ||||
|             </div> | ||||
|             <div class="right"> | ||||
|                 <li> | ||||
|                     <!-- <v-btn | ||||
|                         @click="performLogout" | ||||
|                         to="/login" | ||||
|                         style="background-color: transparent; box-shadow: none !important" | ||||
|                     > | ||||
|                         <v-tooltip | ||||
|                         <v-btn | ||||
|                             :text="t('cancel')" | ||||
|                             @click="isActive.value = false" | ||||
|                         ></v-btn> | ||||
|                         <v-btn | ||||
|                             :text="t('logout')" | ||||
|                             location="bottom" | ||||
|                         > | ||||
|                             <template v-slot:activator="{ props }"> | ||||
|                                 <v-icon | ||||
|                                     v-bind="props" | ||||
|                                     icon="mdi-logout" | ||||
|                                     size="x-large" | ||||
|                                     color="#0e6942" | ||||
|                                 ></v-icon> | ||||
|                             </template> | ||||
|                         </v-tooltip> | ||||
|                     </v-btn> --> | ||||
|                     <v-dialog max-width="500"> | ||||
|                         <template v-slot:activator="{ props: activatorProps }"> | ||||
|                             <v-btn | ||||
|                                 v-bind="activatorProps" | ||||
|                                 style="background-color: transparent; box-shadow: none !important" | ||||
|                             > | ||||
|                                 <v-tooltip | ||||
|                                     :text="t('logout')" | ||||
|                                     location="bottom" | ||||
|                                 > | ||||
|                                     <template v-slot:activator="{ props }"> | ||||
|                                         <v-icon | ||||
|                                             v-bind="props" | ||||
|                                             icon="mdi-logout" | ||||
|                                             size="x-large" | ||||
|                                             color="#0e6942" | ||||
|                                         > | ||||
|                                         </v-icon> | ||||
|                                     </template> | ||||
|                                 </v-tooltip> | ||||
|                             </v-btn> | ||||
|                         </template> | ||||
|                             @click="performLogout" | ||||
|                             to="/login" | ||||
|                         ></v-btn> | ||||
|                     </v-card-actions> | ||||
|                 </v-card> | ||||
|             </template> | ||||
|         </v-dialog> | ||||
|         <v-avatar | ||||
|             size="large" | ||||
|             color="#0e6942" | ||||
|             class="user-button" | ||||
|             >{{ initials }}</v-avatar | ||||
|         > | ||||
|     </v-app-bar> | ||||
|     <v-navigation-drawer | ||||
|         v-model="drawer" | ||||
|         temporary | ||||
|         app | ||||
|     > | ||||
|         <v-list> | ||||
|             <v-list-item | ||||
|                 to="/user/assignment" | ||||
|                 link | ||||
|             > | ||||
|                 <v-list-item-title class="menu_item">{{ t("assignments") }}</v-list-item-title> | ||||
|             </v-list-item> | ||||
| 
 | ||||
|                         <template v-slot:default="{ isActive }"> | ||||
|                             <v-card :title="t('logoutVerification')"> | ||||
|                                 <v-card-actions> | ||||
|                                     <v-spacer></v-spacer> | ||||
|             <v-list-item | ||||
|                 to="/user/class" | ||||
|                 link | ||||
|             > | ||||
|                 <v-list-item-title class="menu_item">{{ t("classes") }}</v-list-item-title> | ||||
|             </v-list-item> | ||||
| 
 | ||||
|                                     <v-btn | ||||
|                                         :text="t('cancel')" | ||||
|                                         @click="isActive.value = false" | ||||
|                                     ></v-btn> | ||||
|                                     <v-btn | ||||
|                                         :text="t('logout')" | ||||
|                                         @click="performLogout" | ||||
|                                         to="/login" | ||||
|                                     ></v-btn> | ||||
|                                 </v-card-actions> | ||||
|                             </v-card> | ||||
|                         </template> | ||||
|                     </v-dialog> | ||||
|                 </li> | ||||
|                 <li> | ||||
|                     <v-avatar | ||||
|                         size="large" | ||||
|                         color="#0e6942" | ||||
|                         style="font-size: large; font-weight: bold" | ||||
|                         >{{ initials }}</v-avatar | ||||
|                     > | ||||
|                 </li> | ||||
|             </div> | ||||
|         </nav> | ||||
|         <router-view /> | ||||
|     </main> | ||||
|             <v-list-item | ||||
|                 to="/user/discussion" | ||||
|                 link | ||||
|             > | ||||
|                 <v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title> | ||||
|             </v-list-item> | ||||
|         </v-list> | ||||
|     </v-navigation-drawer> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|     .app-bar { | ||||
|         background-color: #f6faf2; | ||||
|     } | ||||
|     .menu { | ||||
|         background-color: #f6faf2; | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|     } | ||||
| 
 | ||||
|     .right { | ||||
|         align-items: center; | ||||
|         padding: 10px; | ||||
|     .user-button { | ||||
|         margin-right: 10px; | ||||
|         font-size: large; | ||||
|         font-weight: bold; | ||||
|     } | ||||
| 
 | ||||
|     .right li { | ||||
|  | @ -347,16 +243,19 @@ | |||
|         color: #0e6942; | ||||
|         text-decoration: none; | ||||
|         font-size: large; | ||||
|     } | ||||
| 
 | ||||
|     nav a.router-link-active { | ||||
|         font-weight: bold; | ||||
|         text-transform: none; | ||||
|     } | ||||
| 
 | ||||
|     @media (max-width: 700px) { | ||||
|         .menu { | ||||
|             display: none; | ||||
|         } | ||||
|         .caption { | ||||
|             font-size: smaller; | ||||
|         } | ||||
|         .dwengo_logo { | ||||
|             width: 100px; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @media (min-width: 701px) { | ||||
|  |  | |||
|  | @ -1,14 +1,14 @@ | |||
| <script setup lang="ts"> | ||||
| import { useI18n } from "vue-i18n"; | ||||
|     import { useI18n } from "vue-i18n"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
| defineProps<{ | ||||
|     path: string; | ||||
|     title: string; | ||||
|     description: string; | ||||
|     image: string; | ||||
| }>(); | ||||
|     defineProps<{ | ||||
|         path: string; | ||||
|         title: string; | ||||
|         description: string; | ||||
|         image: string; | ||||
|     }>(); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|  | @ -31,7 +31,10 @@ defineProps<{ | |||
|         </v-card-title> | ||||
|         <v-card-text class="description flex-grow-1">{{ description }}</v-card-text> | ||||
|         <v-card-actions> | ||||
|             <v-btn :to="`theme/${path}`" variant="text"> | ||||
|             <v-btn | ||||
|                 :to="`theme/${path}`" | ||||
|                 variant="text" | ||||
|             > | ||||
|                 {{ t("read-more") }} | ||||
|             </v-btn> | ||||
|         </v-card-actions> | ||||
|  | @ -39,36 +42,36 @@ defineProps<{ | |||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| .theme-card { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     height: 100%; | ||||
|     padding: 1rem; | ||||
|     cursor: pointer; | ||||
| } | ||||
|     .theme-card { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         height: 100%; | ||||
|         padding: 1rem; | ||||
|         cursor: pointer; | ||||
|     } | ||||
| 
 | ||||
| .theme-card:hover { | ||||
|     background-color: rgba(0, 0, 0, 0.03); | ||||
| } | ||||
|     .theme-card:hover { | ||||
|         background-color: rgba(0, 0, 0, 0.03); | ||||
|     } | ||||
| 
 | ||||
| .title-container { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 10px; | ||||
|     text-align: left; | ||||
|     justify-content: flex-start; | ||||
| } | ||||
|     .title-container { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         gap: 10px; | ||||
|         text-align: left; | ||||
|         justify-content: flex-start; | ||||
|     } | ||||
| 
 | ||||
| .title-image { | ||||
|     flex-shrink: 0; | ||||
|     border-radius: 5px; | ||||
|     margin-left: 0; | ||||
| } | ||||
|     .title-image { | ||||
|         flex-shrink: 0; | ||||
|         border-radius: 5px; | ||||
|         margin-left: 0; | ||||
|     } | ||||
| 
 | ||||
| .title { | ||||
|     flex-grow: 1; | ||||
|     white-space: normal; | ||||
|     overflow-wrap: break-word; | ||||
|     word-break: break-word; | ||||
| } | ||||
|     .title { | ||||
|         flex-grow: 1; | ||||
|         white-space: normal; | ||||
|         overflow-wrap: break-word; | ||||
|         word-break: break-word; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {apiConfig} from "@/config.ts"; | ||||
| import { apiConfig } from "@/config.ts"; | ||||
| 
 | ||||
| export class BaseController { | ||||
|     protected baseUrl: string; | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {ThemeController} from "@/controllers/themes.ts"; | ||||
| import { ThemeController } from "@/controllers/themes.ts"; | ||||
| 
 | ||||
| export function controllerGetter<T>(Factory: new () => T): () => T { | ||||
|     let instance: T | undefined; | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {BaseController} from "@/controllers/base-controller.ts"; | ||||
| import { BaseController } from "@/controllers/base-controller.ts"; | ||||
| 
 | ||||
| export class ThemeController extends BaseController { | ||||
|     constructor() { | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ import i18n from "./i18n/i18n.ts"; | |||
| // Components
 | ||||
| import App from "./App.vue"; | ||||
| import router from "./router"; | ||||
| import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query'; | ||||
| import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query"; | ||||
| 
 | ||||
| const app = createApp(App); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,25 +1,22 @@ | |||
| import { useQuery } from '@tanstack/vue-query'; | ||||
| import { getThemeController } from '@/controllers/controllers'; | ||||
| import {type MaybeRefOrGetter, toValue} from "vue"; | ||||
| import { useQuery } from "@tanstack/vue-query"; | ||||
| import { getThemeController } from "@/controllers/controllers"; | ||||
| import { type MaybeRefOrGetter, toValue } from "vue"; | ||||
| 
 | ||||
| const themeController = getThemeController(); | ||||
| 
 | ||||
| export const useThemeQuery = (language: MaybeRefOrGetter<string>) => { | ||||
|     return useQuery({ | ||||
|         queryKey: ['themes', language], | ||||
| export const useThemeQuery = (language: MaybeRefOrGetter<string>) => | ||||
|     useQuery({ | ||||
|         queryKey: ["themes", language], | ||||
|         queryFn: () => { | ||||
|             const lang = toValue(language); | ||||
|             return themeController.getAll(lang); | ||||
|         }, | ||||
|         enabled: () => !!toValue(language), | ||||
|         enabled: () => Boolean(toValue(language)), | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| export const useThemeHruidsQuery = (themeKey: string | null) => { | ||||
|     return useQuery({ | ||||
|         queryKey: ['theme-hruids', themeKey], | ||||
| export const useThemeHruidsQuery = (themeKey: string | null) => | ||||
|     useQuery({ | ||||
|         queryKey: ["theme-hruids", themeKey], | ||||
|         queryFn: () => themeController.getHruidsByKey(themeKey!), | ||||
|         enabled: !!themeKey, | ||||
|         enabled: Boolean(themeKey), | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,11 +1,14 @@ | |||
| import apiClient from "@/services/api-client.ts"; | ||||
| import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts"; | ||||
| 
 | ||||
| export const AUTH_CONFIG_ENDPOINT = "auth/config"; | ||||
| 
 | ||||
| /** | ||||
|  * Fetch the authentication configuration from the backend. | ||||
|  */ | ||||
| export async function loadAuthConfig() { | ||||
|     const authConfig = (await apiClient.get<FrontendAuthConfig>("auth/config")).data; | ||||
|     const authConfigResponse = await apiClient.get<FrontendAuthConfig>(AUTH_CONFIG_ENDPOINT); | ||||
|     const authConfig = authConfigResponse.data; | ||||
|     return { | ||||
|         student: { | ||||
|             authority: authConfig.student.authority, | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| import { computed, reactive } from "vue"; | ||||
| import type { AuthState, Role, UserManagersForRoles } from "@/services/auth/auth.d.ts"; | ||||
| import { User, UserManager } from "oidc-client-ts"; | ||||
| import { loadAuthConfig } from "@/services/auth/auth-config-loader.ts"; | ||||
| import { AUTH_CONFIG_ENDPOINT, loadAuthConfig } from "@/services/auth/auth-config-loader.ts"; | ||||
| import authStorage from "./auth-storage.ts"; | ||||
| import { loginRoute } from "@/config.ts"; | ||||
| import apiClient from "@/services/api-client.ts"; | ||||
|  | @ -108,7 +108,7 @@ async function logout(): Promise<void> { | |||
| apiClient.interceptors.request.use( | ||||
|     async (reqConfig) => { | ||||
|         const token = authState?.user?.access_token; | ||||
|         if (token) { | ||||
|         if (token && reqConfig.url !== AUTH_CONFIG_ENDPOINT) { | ||||
|             reqConfig.headers.Authorization = `Bearer ${token}`; | ||||
|         } | ||||
|         return reqConfig; | ||||
|  |  | |||
|  | @ -1,33 +1,64 @@ | |||
| export const THEMES_KEYS = [ | ||||
|     "kiks", "art", "socialrobot", "agriculture", "wegostem", | ||||
|     "computational_thinking", "math_with_python", "python_programming", | ||||
|     "stem", "care", "chatbot", "physical_computing", "algorithms", "basics_ai" | ||||
|     "kiks", | ||||
|     "art", | ||||
|     "socialrobot", | ||||
|     "agriculture", | ||||
|     "wegostem", | ||||
|     "computational_thinking", | ||||
|     "math_with_python", | ||||
|     "python_programming", | ||||
|     "stem", | ||||
|     "care", | ||||
|     "chatbot", | ||||
|     "physical_computing", | ||||
|     "algorithms", | ||||
|     "basics_ai", | ||||
| ]; | ||||
| 
 | ||||
| export const THEMESITEMS: Record<string, string[]> = { | ||||
|     "all": THEMES_KEYS, | ||||
|     "culture": ["art", "wegostem", "chatbot"], | ||||
|     all: THEMES_KEYS, | ||||
|     culture: ["art", "wegostem", "chatbot"], | ||||
|     "electricity-and-mechanics": ["socialrobot", "wegostem", "stem", "physical_computing"], | ||||
|     "nature-and-climate": ["kiks", "agriculture"], | ||||
|     "agriculture": ["agriculture"], | ||||
|     "society": ["kiks", "socialrobot", "care", "chatbot"], | ||||
|     "math": ["kiks", "math_with_python", "python_programming", "stem", "care", "basics_ai"], | ||||
|     "technology": ["socialrobot", "wegostem", "computational_thinking", "stem", "physical_computing", "basics_ai"], | ||||
|     "algorithms": ["math_with_python", "python_programming", "stem", "algorithms", "basics_ai"], | ||||
|     agriculture: ["agriculture"], | ||||
|     society: ["kiks", "socialrobot", "care", "chatbot"], | ||||
|     math: ["kiks", "math_with_python", "python_programming", "stem", "care", "basics_ai"], | ||||
|     technology: ["socialrobot", "wegostem", "computational_thinking", "stem", "physical_computing", "basics_ai"], | ||||
|     algorithms: ["math_with_python", "python_programming", "stem", "algorithms", "basics_ai"], | ||||
| }; | ||||
| 
 | ||||
| export const AGEITEMS = ["all", "primary-school", "lower-secondary", "upper-secondary", "high-school", "older"]; | ||||
| 
 | ||||
| export const AGE_TO_THEMES: Record<string, string[]> = { | ||||
|     "all": THEMES_KEYS, | ||||
|     all: THEMES_KEYS, | ||||
|     "primary-school": ["wegostem", "computational_thinking", "physical_computing"], | ||||
|     "lower-secondary": ["socialrobot", "art", "wegostem", "computational_thinking", "physical_computing"], | ||||
|     "upper-secondary": ["kiks", "art", "socialrobot", "agriculture", | ||||
|         "computational_thinking", "math_with_python", "python_programming", | ||||
|         "stem", "care", "chatbot", "algorithms", "basics_ai"], | ||||
|     "high-school": [ | ||||
|         "kiks", "art", "agriculture", "computational_thinking", "math_with_python", "python_programming", | ||||
|         "stem", "care", "chatbot", "algorithms", "basics_ai" | ||||
|     "upper-secondary": [ | ||||
|         "kiks", | ||||
|         "art", | ||||
|         "socialrobot", | ||||
|         "agriculture", | ||||
|         "computational_thinking", | ||||
|         "math_with_python", | ||||
|         "python_programming", | ||||
|         "stem", | ||||
|         "care", | ||||
|         "chatbot", | ||||
|         "algorithms", | ||||
|         "basics_ai", | ||||
|     ], | ||||
|     "older": [ | ||||
|         "kiks", "computational_thinking", "algorithms", "basics_ai" | ||||
|     ] | ||||
|     "high-school": [ | ||||
|         "kiks", | ||||
|         "art", | ||||
|         "agriculture", | ||||
|         "computational_thinking", | ||||
|         "math_with_python", | ||||
|         "python_programming", | ||||
|         "stem", | ||||
|         "care", | ||||
|         "chatbot", | ||||
|         "algorithms", | ||||
|         "basics_ai", | ||||
|     ], | ||||
|     older: ["kiks", "computational_thinking", "algorithms", "basics_ai"], | ||||
| }; | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
|     onMounted(async () => { | ||||
|         try { | ||||
|             await auth.handleLoginCallback(); | ||||
|             await router.replace("/"); // Redirect to home (or dashboard) | ||||
|             await router.replace("/user"); // Redirect to theme page | ||||
|         } catch (error) { | ||||
|             console.error("OIDC callback error:", error); | ||||
|         } | ||||
|  |  | |||
|  | @ -25,9 +25,10 @@ | |||
|             <div class="container_left"> | ||||
|                 <img | ||||
|                     :src="dwengoLogo" | ||||
|                     alt="Dwengo logo" | ||||
|                     style="align-self: center" | ||||
|                 /> | ||||
|                 <h> {{ t("homeTitle") }}</h> | ||||
|                 <h1>{{ t("homeTitle") }}</h1> | ||||
|                 <p class="info"> | ||||
|                     {{ t("homeIntroduction1") }} | ||||
|                 </p> | ||||
|  | @ -55,7 +56,7 @@ | |||
|                         width="125" | ||||
|                         src="/assets/home/innovative.png" | ||||
|                     ></v-img> | ||||
|                     <h class="big">{{ t("innovative") }}</h> | ||||
|                     <h2 class="big">{{ t("innovative") }}</h2> | ||||
|                 </div> | ||||
|                 <div class="img_small"> | ||||
|                     <v-img | ||||
|  | @ -63,7 +64,7 @@ | |||
|                         width="125" | ||||
|                         src="/assets/home/research_based.png" | ||||
|                     ></v-img> | ||||
|                     <h class="big">{{ t("researchBased") }}</h> | ||||
|                     <h2 class="big">{{ t("researchBased") }}</h2> | ||||
|                 </div> | ||||
|                 <div class="img_small"> | ||||
|                     <v-img | ||||
|  | @ -71,7 +72,7 @@ | |||
|                         width="125" | ||||
|                         src="/assets/home/inclusive.png" | ||||
|                     ></v-img> | ||||
|                     <h class="big">{{ t("sociallyRelevant") }}</h> | ||||
|                     <h2 class="big">{{ t("sociallyRelevant") }}</h2> | ||||
|                 </div> | ||||
|                 <div class="img_small"> | ||||
|                     <v-img | ||||
|  | @ -79,7 +80,7 @@ | |||
|                         width="125" | ||||
|                         src="/assets/home/socially_relevant.png" | ||||
|                     ></v-img> | ||||
|                     <h class="big">{{ t("inclusive") }}</h> | ||||
|                     <h2 class="big">{{ t("inclusive") }}</h2> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="container_right"> | ||||
|  | @ -158,7 +159,7 @@ | |||
|         margin-bottom: 10px; | ||||
|     } | ||||
| 
 | ||||
|     h { | ||||
|     h2 { | ||||
|         font-size: large; | ||||
|         font-weight: bold; | ||||
|         align-self: center; | ||||
|  |  | |||
|  | @ -1,11 +1,7 @@ | |||
| <script setup lang="ts"> | ||||
| 
 | ||||
| </script> | ||||
| <script setup lang="ts"></script> | ||||
| 
 | ||||
| <template> | ||||
| <main></main> | ||||
|     <main></main> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| 
 | ||||
| </style> | ||||
| <style scoped></style> | ||||
|  |  | |||
|  | @ -1,13 +1,13 @@ | |||
| <script setup lang="ts"> | ||||
| import {ref, watch} from "vue"; | ||||
|     import {useI18n} from "vue-i18n"; | ||||
|     import {THEMESITEMS, AGE_TO_THEMES} from "@/utils/constants.ts"; | ||||
|     import { ref, watch } from "vue"; | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import { THEMESITEMS, AGE_TO_THEMES } from "@/utils/constants.ts"; | ||||
|     import BrowseThemes from "@/components/BrowseThemes.vue"; | ||||
| 
 | ||||
|     const {t, locale} = useI18n(); | ||||
|     const { t, locale } = useI18n(); | ||||
| 
 | ||||
|     const selectedThemeKey = ref<string>('all'); | ||||
|     const selectedAgeKey = ref<string>('all'); | ||||
|     const selectedThemeKey = ref<string>("all"); | ||||
|     const selectedAgeKey = ref<string>("all"); | ||||
| 
 | ||||
|     const allThemes = ref(Object.keys(THEMESITEMS)); | ||||
|     const availableThemes = ref([...allThemes.value]); | ||||
|  | @ -17,18 +17,17 @@ import {ref, watch} from "vue"; | |||
| 
 | ||||
|     // Reset selection when language changes | ||||
|     watch(locale, () => { | ||||
|         selectedThemeKey.value = 'all'; | ||||
|         selectedAgeKey.value = 'all'; | ||||
|         selectedThemeKey.value = "all"; | ||||
|         selectedAgeKey.value = "all"; | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     watch(selectedThemeKey, () => { | ||||
|         if (selectedThemeKey.value === "all") { | ||||
|             availableAges.value = [...allAges.value]; // Reset to all ages | ||||
|         } else { | ||||
|             const themes = THEMESITEMS[selectedThemeKey.value]; | ||||
|             availableAges.value = allAges.value.filter(age => | ||||
|                 AGE_TO_THEMES[age]?.some(theme => themes.includes(theme)) | ||||
|             availableAges.value = allAges.value.filter((age) => | ||||
|                 AGE_TO_THEMES[age]?.some((theme) => themes.includes(theme)), | ||||
|             ); | ||||
|         } | ||||
|     }); | ||||
|  | @ -38,32 +37,31 @@ import {ref, watch} from "vue"; | |||
|             availableThemes.value = [...allThemes.value]; // Reset to all themes | ||||
|         } else { | ||||
|             const themes = AGE_TO_THEMES[selectedAgeKey.value]; | ||||
|             availableThemes.value = allThemes.value.filter(theme => | ||||
|                 THEMESITEMS[theme]?.some(theme => themes.includes(theme)) | ||||
|             availableThemes.value = allThemes.value.filter((theme) => | ||||
|                 THEMESITEMS[theme]?.some((theme) => themes.includes(theme)), | ||||
|             ); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div class="main-container"> | ||||
|         <h1 class="title">{{ t("themes") }}</h1> | ||||
|         <v-container class="dropdowns"> | ||||
|             <v-select class="v-select" | ||||
|                       :label="t('choose-theme')" | ||||
|                       :items="availableThemes.map(theme => ({ title: t(`theme-options.${theme}`), value: theme }))" | ||||
|                       v-model="selectedThemeKey" | ||||
|                       item-title="title" | ||||
|                       item-value="value" | ||||
|                       variant="outlined" | ||||
|             <v-select | ||||
|                 class="v-select" | ||||
|                 :label="t('choose-theme')" | ||||
|                 :items="availableThemes.map((theme) => ({ title: t(`theme-options.${theme}`), value: theme }))" | ||||
|                 v-model="selectedThemeKey" | ||||
|                 item-title="title" | ||||
|                 item-value="value" | ||||
|                 variant="outlined" | ||||
|             /> | ||||
| 
 | ||||
| 
 | ||||
|             <v-select | ||||
|                 class="v-select" | ||||
|                 :label="t('choose-age')" | ||||
|                 :items="availableAges.map(age => ({ key: age, label: t(`age-options.${age}`), value: age }))" | ||||
|                 :items="availableAges.map((age) => ({ key: age, label: t(`age-options.${age}`), value: age }))" | ||||
|                 v-model="selectedAgeKey" | ||||
|                 item-title="label" | ||||
|                 item-value="key" | ||||
|  | @ -71,55 +69,55 @@ import {ref, watch} from "vue"; | |||
|             ></v-select> | ||||
|         </v-container> | ||||
| 
 | ||||
|         <BrowseThemes :selectedTheme="selectedThemeKey ?? ''" :selectedAge="selectedAgeKey ?? ''"/> | ||||
|         <BrowseThemes | ||||
|             :selectedTheme="selectedThemeKey ?? ''" | ||||
|             :selectedAge="selectedAgeKey ?? ''" | ||||
|         /> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| .main-container { | ||||
|     min-height: 100vh; | ||||
|     min-width: 100vw; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: flex-start; | ||||
|     justify-content: flex-start; | ||||
| } | ||||
| 
 | ||||
| .title { | ||||
|     max-width: 50rem; | ||||
|     margin-left: 1rem; | ||||
|     margin-top: 1rem; | ||||
|     text-align: center; | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| .dropdowns { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     gap: 5rem; | ||||
|     width: 80%; | ||||
| } | ||||
| 
 | ||||
| .v-select { | ||||
|     flex: 1; | ||||
|     min-width: 100px; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @media (max-width: 768px) { | ||||
|     .main-container { | ||||
|         padding: 1rem; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 700px) { | ||||
|     .dropdowns { | ||||
|         min-height: 100vh; | ||||
|         min-width: 100vw; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         gap: 1rem; | ||||
|         align-items: flex-start; | ||||
|         justify-content: flex-start; | ||||
|     } | ||||
| 
 | ||||
|     .title { | ||||
|         max-width: 50rem; | ||||
|         margin-left: 1rem; | ||||
|         margin-top: 1rem; | ||||
|         text-align: center; | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|     } | ||||
| 
 | ||||
|     .dropdowns { | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|         gap: 5rem; | ||||
|         width: 80%; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     .v-select { | ||||
|         flex: 1; | ||||
|         min-width: 100px; | ||||
|     } | ||||
| 
 | ||||
|     @media (max-width: 768px) { | ||||
|         .main-container { | ||||
|             padding: 1rem; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @media (max-width: 700px) { | ||||
|         .dropdowns { | ||||
|             flex-direction: column; | ||||
|             gap: 1rem; | ||||
|             width: 80%; | ||||
|         } | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana