Merge branch 'dev' into feat/home-data
This commit is contained in:
		
						commit
						1531fa36fe
					
				
					 20 changed files with 931 additions and 35 deletions
				
			
		|  | @ -14,16 +14,17 @@ | |||
|         "test:unit": "vitest" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@mikro-orm/core": "6.4.6", | ||||
|         "@mikro-orm/postgresql": "6.4.6", | ||||
|         "@mikro-orm/core": "^6.4.6", | ||||
|         "@mikro-orm/postgresql": "^6.4.6", | ||||
|         "@mikro-orm/reflection": "^6.4.6", | ||||
|         "@types/js-yaml": "^4.0.9", | ||||
|         "axios": "^1.8.1", | ||||
|         "@mikro-orm/sqlite": "6.4.6", | ||||
|         "@mikro-orm/reflection": "6.4.6", | ||||
|         "dotenv": "^16.4.7", | ||||
|         "express": "^5.0.1", | ||||
|         "uuid": "^11.1.0", | ||||
|         "express": "^5.0.1", | ||||
|         "js-yaml": "^4.1.0", | ||||
|         "@types/js-yaml": "^4.0.9", | ||||
|         "@types/js-yaml": "^4.0.9" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@mikro-orm/cli": "^6.4.6", | ||||
|  |  | |||
|  | @ -3,14 +3,16 @@ import { initORM } from './orm.js'; | |||
| import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | ||||
| 
 | ||||
| import themeRoutes from './routes/themes.js'; | ||||
| import learningPathRoutes from './routes/learningPaths.js'; | ||||
| import learningObjectRoutes from './routes/learningObjects.js'; | ||||
| 
 | ||||
| import studentRouter from './routes/student'; | ||||
| import groupRouter from './routes/group'; | ||||
| import assignmentRouter from './routes/assignment'; | ||||
| import submissionRouter from './routes/submission'; | ||||
| import classRouter from './routes/class'; | ||||
| import questionRouter from './routes/question'; | ||||
| import loginRouter from './routes/login'; | ||||
| import studentRouter from './routes/student.js'; | ||||
| import groupRouter from './routes/group.js'; | ||||
| import assignmentRouter from './routes/assignment.js'; | ||||
| import submissionRouter from './routes/submission.js'; | ||||
| import classRouter from './routes/class.js'; | ||||
| import questionRouter from './routes/question.js'; | ||||
| import loginRouter from './routes/login.js'; | ||||
| 
 | ||||
| const app: Express = express(); | ||||
| const port: string | number = getNumericEnvVar(EnvVars.Port); | ||||
|  | @ -32,6 +34,8 @@ app.use('/question', questionRouter); | |||
| app.use('/login', loginRouter); | ||||
| 
 | ||||
| app.use('/theme', themeRoutes); | ||||
| app.use('/learningPath', learningPathRoutes); | ||||
| app.use('/learningObject', learningObjectRoutes); | ||||
| 
 | ||||
| async function startServer() { | ||||
|     await initORM(); | ||||
|  |  | |||
							
								
								
									
										10
									
								
								backend/src/config.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/config.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| // Can be placed in dotenv but found it redundant
 | ||||
| 
 | ||||
| // Import dotenv from "dotenv";
 | ||||
| 
 | ||||
| // Load .env file
 | ||||
| // Dotenv.config();
 | ||||
| 
 | ||||
| export const DWENGO_API_BASE = 'https://dwengo.org/backend/api'; | ||||
| 
 | ||||
| export const FALLBACK_LANG = 'nl'; | ||||
							
								
								
									
										60
									
								
								backend/src/controllers/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								backend/src/controllers/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { | ||||
|     getLearningObjectById, | ||||
|     getLearningObjectIdsFromPath, | ||||
|     getLearningObjectsFromPath, | ||||
| } from '../services/learningObjects.js'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import { FilteredLearningObject } from '../interfaces/learningPath'; | ||||
| 
 | ||||
| export async function getAllLearningObjects( | ||||
|     req: Request, | ||||
|     res: Response | ||||
| ): Promise<void> { | ||||
|     try { | ||||
|         const hruid = req.query.hruid as string; | ||||
|         const full = req.query.full === 'true'; | ||||
|         const language = (req.query.language as string) || FALLBACK_LANG; | ||||
| 
 | ||||
|         if (!hruid) { | ||||
|             res.status(400).json({ error: 'HRUID query is required.' }); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let learningObjects: FilteredLearningObject[] | string[]; | ||||
|         if (full) { | ||||
|             learningObjects = await getLearningObjectsFromPath(hruid, language); | ||||
|         } else { | ||||
|             learningObjects = await getLearningObjectIdsFromPath( | ||||
|                 hruid, | ||||
|                 language | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         res.json(learningObjects); | ||||
|     } catch (error) { | ||||
|         console.error('Error fetching learning objects:', error); | ||||
|         res.status(500).json({ error: 'Internal server error' }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export async function getLearningObject( | ||||
|     req: Request, | ||||
|     res: Response | ||||
| ): Promise<void> { | ||||
|     try { | ||||
|         const { hruid } = req.params; | ||||
|         const language = (req.query.language as string) || FALLBACK_LANG; | ||||
| 
 | ||||
|         if (!hruid) { | ||||
|             res.status(400).json({ error: 'HRUID parameter is required.' }); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const learningObject = await getLearningObjectById(hruid, language); | ||||
|         res.json(learningObject); | ||||
|     } catch (error) { | ||||
|         console.error('Error fetching learning object:', error); | ||||
|         res.status(500).json({ error: 'Internal server error' }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										62
									
								
								backend/src/controllers/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								backend/src/controllers/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { themes } from '../data/themes.js'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import { | ||||
|     fetchLearningPaths, | ||||
|     searchLearningPaths, | ||||
| } from '../services/learningPaths.js'; | ||||
| /** | ||||
|  * Fetch learning paths based on query parameters. | ||||
|  */ | ||||
| export async function getLearningPaths( | ||||
|     req: Request, | ||||
|     res: Response | ||||
| ): Promise<void> { | ||||
|     try { | ||||
|         const hruids = req.query.hruid; | ||||
|         const themeKey = req.query.theme as string; | ||||
|         const searchQuery = req.query.search as string; | ||||
|         const language = (req.query.language as string) || FALLBACK_LANG; | ||||
| 
 | ||||
|         let hruidList; | ||||
| 
 | ||||
|         if (hruids) { | ||||
|             hruidList = Array.isArray(hruids) | ||||
|                 ? hruids.map(String) | ||||
|                 : [String(hruids)]; | ||||
|         } else if (themeKey) { | ||||
|             const theme = themes.find((t) => { | ||||
|                 return t.title === themeKey; | ||||
|             }); | ||||
|             if (theme) { | ||||
|                 hruidList = theme.hruids; | ||||
|             } else { | ||||
|                 res.status(404).json({ | ||||
|                     error: `Theme "${themeKey}" not found.`, | ||||
|                 }); | ||||
|                 return; | ||||
|             } | ||||
|         } else if (searchQuery) { | ||||
|             const searchResults = await searchLearningPaths( | ||||
|                 searchQuery, | ||||
|                 language | ||||
|             ); | ||||
|             res.json(searchResults); | ||||
|             return; | ||||
|         } else { | ||||
|             hruidList = themes.flatMap((theme) => { | ||||
|                 return theme.hruids; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         const learningPaths = await fetchLearningPaths( | ||||
|             hruidList, | ||||
|             language, | ||||
|             `HRUIDs: ${hruidList.join(', ')}` | ||||
|         ); | ||||
|         res.json(learningPaths.data); | ||||
|     } catch (error) { | ||||
|         console.error('❌ Unexpected error fetching learning paths:', error); | ||||
|         res.status(500).json({ error: 'Internal server error' }); | ||||
|     } | ||||
| } | ||||
|  | @ -1,6 +1,7 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { themes } from '../data/themes.js'; | ||||
| import { loadTranslations } from "../util/translationHelper.js"; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| 
 | ||||
| interface Translations { | ||||
|     curricula_page: { | ||||
|  | @ -8,13 +9,9 @@ interface Translations { | |||
|     }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /themes → Haalt de lijst met thema's op inclusief vertalingen | ||||
|  */ | ||||
| export function getThemes(req: Request, res: Response) { | ||||
|     const language = (req.query.language as string)?.toLowerCase() || 'nl'; | ||||
|     const translations = loadTranslations<Translations>(language); | ||||
| 
 | ||||
|     const themeList = themes.map((theme) => { | ||||
|         return { | ||||
|             key: theme.title, | ||||
|  | @ -28,9 +25,6 @@ export function getThemes(req: Request, res: Response) { | |||
|     res.json(themeList); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /themes/:theme → Geeft de HRUIDs terug voor een specifiek thema | ||||
|  */ | ||||
| export function getThemeByTitle(req: Request, res: Response) { | ||||
|     const themeKey = req.params.theme; | ||||
|     const theme = themes.find((t) => { | ||||
|  | @ -40,6 +34,6 @@ export function getThemeByTitle(req: Request, res: Response) { | |||
|     if (theme) { | ||||
|         res.json(theme.hruids); | ||||
|     } else { | ||||
|         res.status(404).json({ error: 'Thema niet gevonden' }); | ||||
|         res.status(404).json({ error: 'Theme not found' }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										98
									
								
								backend/src/interfaces/learningPath.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								backend/src/interfaces/learningPath.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,98 @@ | |||
| export interface Transition { | ||||
|     default: boolean; | ||||
|     _id: string; | ||||
|     next: { | ||||
|         _id: string; | ||||
|         hruid: string; | ||||
|         version: number; | ||||
|         language: string; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export interface LearningObjectNode { | ||||
|     _id: string; | ||||
|     learningobject_hruid: string; | ||||
|     version: number; | ||||
|     language: string; | ||||
|     start_node?: boolean; | ||||
|     transitions: Transition[]; | ||||
|     created_at: string; | ||||
|     updatedAt: string; | ||||
| } | ||||
| 
 | ||||
| export interface LearningPath { | ||||
|     _id: string; | ||||
|     language: string; | ||||
|     hruid: string; | ||||
|     title: string; | ||||
|     description: string; | ||||
|     image?: string; // Image might be missing, so it's optional
 | ||||
|     num_nodes: number; | ||||
|     num_nodes_left: number; | ||||
|     nodes: LearningObjectNode[]; | ||||
|     keywords: string; | ||||
|     target_ages: number[]; | ||||
|     min_age: number; | ||||
|     max_age: number; | ||||
|     __order: number; | ||||
| } | ||||
| 
 | ||||
| export interface EducationalGoal { | ||||
|     source: string; | ||||
|     id: string; | ||||
| } | ||||
| 
 | ||||
| export interface ReturnValue { | ||||
|     callback_url: string; | ||||
|     callback_schema: Record<string, any>; | ||||
| } | ||||
| 
 | ||||
| export interface LearningObjectMetadata { | ||||
|     _id: string; | ||||
|     uuid: string; | ||||
|     hruid: string; | ||||
|     version: number; | ||||
|     language: string; | ||||
|     title: string; | ||||
|     description: string; | ||||
|     difficulty: number; | ||||
|     estimated_time: number; | ||||
|     available: boolean; | ||||
|     teacher_exclusive: boolean; | ||||
|     educational_goals: EducationalGoal[]; | ||||
|     keywords: string[]; | ||||
|     target_ages: number[]; | ||||
|     content_type: string; // Markdown, image, etc.
 | ||||
|     content_location?: string; | ||||
|     skos_concepts?: string[]; | ||||
|     return_value?: ReturnValue; | ||||
| } | ||||
| 
 | ||||
| export interface FilteredLearningObject { | ||||
|     key: string; | ||||
|     _id: string; | ||||
|     uuid: string; | ||||
|     version: number; | ||||
|     title: string; | ||||
|     htmlUrl: string; | ||||
|     language: string; | ||||
|     difficulty: number; | ||||
|     estimatedTime: number; | ||||
|     available: boolean; | ||||
|     teacherExclusive: boolean; | ||||
|     educationalGoals: EducationalGoal[]; | ||||
|     keywords: string[]; | ||||
|     description: string; | ||||
|     targetAges: number[]; | ||||
|     contentType: string; | ||||
|     contentLocation?: string; | ||||
|     skosConcepts?: string[]; | ||||
|     returnValue?: ReturnValue; | ||||
| } | ||||
| 
 | ||||
| export interface LearningPathResponse { | ||||
|     success: boolean; | ||||
|     source: string; | ||||
|     data: LearningPath[] | null; | ||||
|     message?: string; | ||||
| } | ||||
							
								
								
									
										27
									
								
								backend/src/routes/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								backend/src/routes/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| import express from 'express'; | ||||
| import { | ||||
|     getAllLearningObjects, | ||||
|     getLearningObject, | ||||
| } from '../controllers/learningObjects.js'; | ||||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // DWENGO learning objects
 | ||||
| 
 | ||||
| // Queries: hruid(path), full, language
 | ||||
| // Route to fetch list of learning objects based on hruid of learning path
 | ||||
| 
 | ||||
| // Route 1: list of object hruids
 | ||||
| // Example 1: http://localhost:3000/learningObject?hruid=un_artificiele_intelligentie
 | ||||
| 
 | ||||
| // Route 2: list of object data
 | ||||
| // Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie
 | ||||
| router.get('/', getAllLearningObjects); | ||||
| 
 | ||||
| // Parameter: hruid of learning object
 | ||||
| // Query: language
 | ||||
| // Route to fetch data of one learning object based on its hruid
 | ||||
| // Example: http://localhost:3000/learningObject/un_ai7
 | ||||
| router.get('/:hruid', getLearningObject); | ||||
| 
 | ||||
| export default router; | ||||
							
								
								
									
										27
									
								
								backend/src/routes/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								backend/src/routes/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| import express from 'express'; | ||||
| import { getLearningPaths } from '../controllers/learningPaths.js'; | ||||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // DWENGO learning paths
 | ||||
| 
 | ||||
| // Route 1: no query
 | ||||
| // Fetch all learning paths
 | ||||
| // Example 1: http://localhost:3000/learningPath
 | ||||
| 
 | ||||
| // Unified route for fetching learning paths
 | ||||
| // Route 2: Query: hruid (list), language
 | ||||
| // Fetch learning paths based on hruid list
 | ||||
| // Example 2: http://localhost:3000/learningPath?hruid=pn_werking&hruid=art1
 | ||||
| 
 | ||||
| // Query: search, language
 | ||||
| // Route to fetch learning paths based on a searchterm
 | ||||
| // Example 3: http://localhost:3000/learningPath?search=robot
 | ||||
| 
 | ||||
| // Query: theme, anguage
 | ||||
| // Route to fetch learning paths based on a theme
 | ||||
| // Example: http://localhost:3000/learningPath?theme=kiks
 | ||||
| 
 | ||||
| router.get('/', getLearningPaths); | ||||
| 
 | ||||
| export default router; | ||||
|  | @ -3,7 +3,12 @@ import { getThemes, getThemeByTitle } from '../controllers/themes.js'; | |||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // Query: language
 | ||||
| //  Route to fetch list of {key, title, description, image} themes in their respective language
 | ||||
| router.get('/', getThemes); | ||||
| 
 | ||||
| // Arg: theme (key)
 | ||||
| //  Route to fetch list of hruids based on theme
 | ||||
| router.get('/:theme', getThemeByTitle); | ||||
| 
 | ||||
| export default router; | ||||
|  |  | |||
							
								
								
									
										134
									
								
								backend/src/services/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								backend/src/services/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,134 @@ | |||
| import { DWENGO_API_BASE } from '../config.js'; | ||||
| import { fetchWithLogging } from '../util/apiHelper.js'; | ||||
| import { | ||||
|     FilteredLearningObject, | ||||
|     LearningObjectMetadata, | ||||
|     LearningObjectNode, | ||||
|     LearningPathResponse, | ||||
| } from '../interfaces/learningPath.js'; | ||||
| import { fetchLearningPaths } from './learningPaths.js'; | ||||
| 
 | ||||
| function filterData( | ||||
|     data: LearningObjectMetadata, | ||||
|     htmlUrl: string | ||||
| ): FilteredLearningObject { | ||||
|     return { | ||||
|         key: data.hruid, // Hruid learningObject (not path)
 | ||||
|         _id: data._id, | ||||
|         uuid: data.uuid, | ||||
|         version: data.version, | ||||
|         title: data.title, | ||||
|         htmlUrl, // Url to fetch html content
 | ||||
|         language: data.language, | ||||
|         difficulty: data.difficulty, | ||||
|         estimatedTime: data.estimated_time, | ||||
|         available: data.available, | ||||
|         teacherExclusive: data.teacher_exclusive, | ||||
|         educationalGoals: data.educational_goals, // List with learningObjects
 | ||||
|         keywords: data.keywords, // For search
 | ||||
|         description: data.description, // For search (not an actual description)
 | ||||
|         targetAges: data.target_ages, | ||||
|         contentType: data.content_type, // Markdown, image, audio, etc.
 | ||||
|         contentLocation: data.content_location, // If content type extern
 | ||||
|         skosConcepts: data.skos_concepts, | ||||
|         returnValue: data.return_value, // Callback response information
 | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Fetches a single learning object by its HRUID | ||||
|  */ | ||||
| export async function getLearningObjectById( | ||||
|     hruid: string, | ||||
|     language: string | ||||
| ): Promise<FilteredLearningObject | null> { | ||||
|     const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; | ||||
|     const metadata = await fetchWithLogging<LearningObjectMetadata>( | ||||
|         metadataUrl, | ||||
|         `Metadata for Learning Object HRUID "${hruid}" (language ${language})` | ||||
|     ); | ||||
| 
 | ||||
|     if (!metadata) { | ||||
|         console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`; | ||||
|     return filterData(metadata, htmlUrl); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Generic function to fetch learning objects (full data or just HRUIDs) | ||||
|  */ | ||||
| async function fetchLearningObjects( | ||||
|     hruid: string, | ||||
|     full: boolean, | ||||
|     language: string | ||||
| ): Promise<FilteredLearningObject[] | string[]> { | ||||
|     try { | ||||
|         const learningPathResponse: LearningPathResponse = | ||||
|             await fetchLearningPaths( | ||||
|                 [hruid], | ||||
|                 language, | ||||
|                 `Learning path for HRUID "${hruid}"` | ||||
|             ); | ||||
| 
 | ||||
|         if ( | ||||
|             !learningPathResponse.success || | ||||
|             !learningPathResponse.data?.length | ||||
|         ) { | ||||
|             console.error( | ||||
|                 `⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.` | ||||
|             ); | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|         const nodes: LearningObjectNode[] = learningPathResponse.data[0].nodes; | ||||
| 
 | ||||
|         if (!full) { | ||||
|             return nodes.map((node) => { | ||||
|                 return node.learningobject_hruid; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return await Promise.all( | ||||
|             nodes.map(async (node) => { | ||||
|                 return getLearningObjectById( | ||||
|                     node.learningobject_hruid, | ||||
|                     language | ||||
|                 ); | ||||
|             }) | ||||
|         ).then((objects) => { | ||||
|             return objects.filter((obj): obj is FilteredLearningObject => { | ||||
|                 return obj !== null; | ||||
|             }); | ||||
|         }); | ||||
|     } catch (error) { | ||||
|         console.error('❌ Error fetching learning objects:', error); | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Fetch full learning object data (metadata) | ||||
|  */ | ||||
| export async function getLearningObjectsFromPath( | ||||
|     hruid: string, | ||||
|     language: string | ||||
| ): Promise<FilteredLearningObject[]> { | ||||
|     return (await fetchLearningObjects( | ||||
|         hruid, | ||||
|         true, | ||||
|         language | ||||
|     )) as FilteredLearningObject[]; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Fetch only learning object HRUIDs | ||||
|  */ | ||||
| export async function getLearningObjectIdsFromPath( | ||||
|     hruid: string, | ||||
|     language: string | ||||
| ): Promise<string[]> { | ||||
|     return (await fetchLearningObjects(hruid, false, language)) as string[]; | ||||
| } | ||||
							
								
								
									
										61
									
								
								backend/src/services/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								backend/src/services/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | |||
| import { fetchWithLogging } from '../util/apiHelper.js'; | ||||
| import { DWENGO_API_BASE } from '../config.js'; | ||||
| import { | ||||
|     LearningPath, | ||||
|     LearningPathResponse, | ||||
| } from '../interfaces/learningPath.js'; | ||||
| 
 | ||||
| export async function fetchLearningPaths( | ||||
|     hruids: string[], | ||||
|     language: string, | ||||
|     source: string | ||||
| ): Promise<LearningPathResponse> { | ||||
|     if (hruids.length === 0) { | ||||
|         return { | ||||
|             success: false, | ||||
|             source, | ||||
|             data: null, | ||||
|             message: `No HRUIDs provided for ${source}.`, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; | ||||
|     const params = { pathIdList: JSON.stringify({ hruids }), language }; | ||||
| 
 | ||||
|     const learningPaths = await fetchWithLogging<LearningPath[]>( | ||||
|         apiUrl, | ||||
|         `Learning paths for ${source}`, | ||||
|         params | ||||
|     ); | ||||
| 
 | ||||
|     if (!learningPaths || learningPaths.length === 0) { | ||||
|         console.error(`⚠️ WARNING: No learning paths found for ${source}.`); | ||||
|         return { | ||||
|             success: false, | ||||
|             source, | ||||
|             data: [], | ||||
|             message: `No learning paths found for ${source}.`, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         success: true, | ||||
|         source, | ||||
|         data: learningPaths, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export async function searchLearningPaths( | ||||
|     query: string, | ||||
|     language: string | ||||
| ): Promise<LearningPath[]> { | ||||
|     const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; | ||||
|     const params = { all: query, language }; | ||||
| 
 | ||||
|     const searchResults = await fetchWithLogging<LearningPath[]>( | ||||
|         apiUrl, | ||||
|         `Search learning paths with query "${query}"`, | ||||
|         params | ||||
|     ); | ||||
|     return searchResults ?? []; | ||||
| } | ||||
							
								
								
									
										43
									
								
								backend/src/util/apiHelper.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								backend/src/util/apiHelper.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| import axios, { AxiosRequestConfig } from 'axios'; | ||||
| 
 | ||||
| // !!!! when logger is done -> change
 | ||||
| 
 | ||||
| /** | ||||
|  * Utility function to fetch data from an API endpoint with error handling. | ||||
|  * Logs errors but does NOT throw exceptions to keep the system running. | ||||
|  * | ||||
|  * @param url The API endpoint to fetch from. | ||||
|  * @param description A short description of what is being fetched (for logging). | ||||
|  * @param params | ||||
|  * @returns The response data if successful, or null if an error occurs. | ||||
|  */ | ||||
| export async function fetchWithLogging<T>( | ||||
|     url: string, | ||||
|     description: string, | ||||
|     params?: Record<string, any> | ||||
| ): Promise<T | null> { | ||||
|     try { | ||||
|         const config: AxiosRequestConfig = params ? { params } : {}; | ||||
| 
 | ||||
|         const response = await axios.get<T>(url, config); | ||||
|         return response.data; | ||||
|     } catch (error: any) { | ||||
|         if (error.response) { | ||||
|             if (error.response.status === 404) { | ||||
|                 console.error( | ||||
|                     `❌ ERROR: ${description} not found (404) at "${url}".` | ||||
|                 ); | ||||
|             } else { | ||||
|                 console.error( | ||||
|                     `❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")` | ||||
|                 ); | ||||
|             } | ||||
|         } else { | ||||
|             console.error( | ||||
|                 `❌ ERROR: Network or unexpected error when fetching ${description}:`, | ||||
|                 error.message | ||||
|             ); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | @ -1,11 +1,186 @@ | |||
| <script setup lang="ts"> | ||||
|     import { ref } from "vue"; | ||||
|     import { useRoute } from "vue-router"; | ||||
|     import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg"; | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
| 
 | ||||
|     // Instantiate variables to use in html to render right | ||||
|     // Links and content dependent on the role (student or teacher) | ||||
|     const isTeacher = route.path.includes("teacher"); | ||||
| 
 | ||||
|     const userId = route.params.id as string; | ||||
| 
 | ||||
|     const role = isTeacher ? "teacher" : "student"; | ||||
|     const name = "Kurt Cobain"; | ||||
|     const initials = name | ||||
|         .split(" ") | ||||
|         .map((n) => { | ||||
|             return n[0]; | ||||
|         }) | ||||
|         .join(""); | ||||
| 
 | ||||
|     const languages = ref([ | ||||
|         { name: "English", code: "en" }, | ||||
|         { name: "Nederlands", code: "nl" }, | ||||
|     ]); | ||||
| 
 | ||||
|     // Logic to change the language of the website to the selected language | ||||
|     const changeLanguage = (langCode: string) => { | ||||
|         console.log(langCode); | ||||
|     }; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
| <main></main> | ||||
|     <main> | ||||
|         <nav class="menu"> | ||||
|             <div class="left"> | ||||
|                 <ul> | ||||
|                     <li> | ||||
|                         <router-link | ||||
|                             :to="`/${role}/${userId}`" | ||||
|                             class="dwengo_home" | ||||
|                         > | ||||
|                             <img | ||||
|                                 class="dwengo_logo" | ||||
|                                 :src="dwengoLogo" | ||||
|                             /> | ||||
|                             <p class="caption"> | ||||
|                                 {{ role }} | ||||
|                             </p> | ||||
|                         </router-link> | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <router-link | ||||
|                             :to="`/${role}/${userId}/assignment`" | ||||
|                             class="menu_item" | ||||
|                         > | ||||
|                             assignments | ||||
|                         </router-link> | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <router-link | ||||
|                             :to="`/${role}/${userId}/class`" | ||||
|                             class="menu_item" | ||||
|                             >classes</router-link | ||||
|                         > | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <router-link | ||||
|                             :to="`/${role}/${userId}/discussion`" | ||||
|                             class="menu_item" | ||||
|                             >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> | ||||
|                     <router-link :to="`/login`"> | ||||
|                         <v-tooltip | ||||
|                             text="log out" | ||||
|                             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> | ||||
|                     </router-link> | ||||
|                 </li> | ||||
|                 <li> | ||||
|                     <v-avatar | ||||
|                         size="large" | ||||
|                         color="#0e6942" | ||||
|                         style="font-size: large; font-weight: bold" | ||||
|                         >{{ initials }}</v-avatar | ||||
|                     > | ||||
|                 </li> | ||||
|             </div> | ||||
|         </nav> | ||||
|     </main> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|     .menu { | ||||
|         background-color: #f6faf2; | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|     } | ||||
| 
 | ||||
|     .right { | ||||
|         align-items: center; | ||||
|         padding: 10px; | ||||
|     } | ||||
| 
 | ||||
|     .right li { | ||||
|         margin-left: 15px; | ||||
|     } | ||||
| 
 | ||||
|     nav ul { | ||||
|         display: flex; | ||||
|         list-style-type: none; | ||||
|         margin: 0; | ||||
|         padding: 0; | ||||
|         gap: 15px; | ||||
|         align-items: center; | ||||
|     } | ||||
| 
 | ||||
|     li { | ||||
|         display: inline; | ||||
|     } | ||||
| 
 | ||||
|     .dwengo_home { | ||||
|         text-align: center; | ||||
|         text-decoration: none; | ||||
|     } | ||||
| 
 | ||||
|     .dwengo_logo { | ||||
|         width: 150px; | ||||
|     } | ||||
| 
 | ||||
|     .caption { | ||||
|         color: black; | ||||
|         margin-top: -25px; | ||||
|     } | ||||
| 
 | ||||
|     .menu_item { | ||||
|         color: #0e6942; | ||||
|         text-decoration: none; | ||||
|         font-size: large; | ||||
|     } | ||||
| 
 | ||||
|     nav a.router-link-active { | ||||
|         font-weight: bold; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -14,6 +14,11 @@ const app = createApp(App); | |||
| 
 | ||||
| app.use(router); | ||||
| 
 | ||||
| const link = document.createElement("link"); | ||||
| link.rel = "stylesheet"; | ||||
| link.href = "https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css"; | ||||
| document.head.appendChild(link); | ||||
| 
 | ||||
| const vuetify = createVuetify({ | ||||
|     components, | ||||
|     directives, | ||||
|  |  | |||
							
								
								
									
										20
									
								
								frontend/src/utils/base64ToImage.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/src/utils/base64ToImage.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| /** | ||||
|  * Converts a Base64 string to a valid image source URL. | ||||
|  * | ||||
|  * @param base64String - The "image" field from the learning path JSON response. | ||||
|  * @returns A properly formatted data URL for use in an <img> tag. | ||||
|  * | ||||
|  * @example | ||||
|  * // Fetch the learning path data and extract the image
 | ||||
|  * const response = await fetch( learning path route ); | ||||
|  * const data = await response.json(); | ||||
|  * const base64String = data.image; | ||||
|  * | ||||
|  * // Use in an <img> element
 | ||||
|  * <img :src="convertBase64ToImageSrc(base64String)" alt="Learning Path Image" /> | ||||
|  */ | ||||
| export function convertBase64ToImageSrc(base64String: string): string { | ||||
|     return base64String.startsWith("data:image") | ||||
|         ? base64String | ||||
|         : `data:image/png;base64,${base64String}`; | ||||
| } | ||||
							
								
								
									
										1
									
								
								frontend/tests/base64/base64Sample.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/tests/base64/base64Sample.txt
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										24
									
								
								frontend/tests/base64/base64ToImage.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/tests/base64/base64ToImage.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| import { describe, it, expect, beforeAll } from 'vitest'; | ||||
| import { convertBase64ToImageSrc } from '../../src/utils/base64ToImage.js'; | ||||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| 
 | ||||
| let sampleBase64: string; | ||||
| 
 | ||||
| beforeAll(() => { | ||||
|     // Load base64 sample from text file
 | ||||
|     const filePath = path.resolve(__dirname, 'base64Sample.txt'); | ||||
|     sampleBase64 = fs.readFileSync(filePath, 'utf8').trim(); | ||||
| }); | ||||
| 
 | ||||
| describe('convertBase64ToImageSrc', () => { | ||||
|     it('should return the same string if it is already a valid data URL', () => { | ||||
|         const base64Image = `data:image/png;base64,${sampleBase64}`; | ||||
|         expect(convertBase64ToImageSrc(base64Image)).toBe(base64Image); | ||||
|     }); | ||||
| 
 | ||||
|     it('should correctly format a raw Base64 string as a PNG image URL', () => { | ||||
|         expect(convertBase64ToImageSrc(sampleBase64)).toBe(`data:image/png;base64,${sampleBase64}`); | ||||
|     }); | ||||
| 
 | ||||
| }); | ||||
							
								
								
									
										171
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										171
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -13,7 +13,7 @@ | |||
|                 "frontend" | ||||
|             ], | ||||
|             "dependencies": { | ||||
|                 "@types/js-yaml": "^4.0.9" | ||||
|                 "@types/js-yaml": "^4.0.9", | ||||
|                 "vue-i18n": "^10.0.5" | ||||
|             }, | ||||
|             "devDependencies": { | ||||
|  | @ -32,13 +32,16 @@ | |||
|             "name": "dwengo-1-backend", | ||||
|             "version": "0.0.1", | ||||
|             "dependencies": { | ||||
|                 "@mikro-orm/core": "6.4.6", | ||||
|                 "@mikro-orm/postgresql": "6.4.6", | ||||
|                 "@mikro-orm/reflection": "6.4.6", | ||||
|                 "@mikro-orm/core": "^6.4.6", | ||||
|                 "@mikro-orm/postgresql": "^6.4.6", | ||||
|                 "@mikro-orm/reflection": "^6.4.6", | ||||
|                 "@types/js-yaml": "^4.0.9", | ||||
|                 "axios": "^1.8.1", | ||||
|                 "@mikro-orm/sqlite": "6.4.6", | ||||
| 
 | ||||
|                 "dotenv": "^16.4.7", | ||||
|                 "express": "^5.0.1", | ||||
|                 "uuid": "^11.1.0" | ||||
|                 "uuid": "^11.1.0", | ||||
|                 "js-yaml": "^4.1.0", | ||||
|                 "@types/js-yaml": "^4.0.9", | ||||
|             }, | ||||
|  | @ -3391,9 +3394,19 @@ | |||
|             "version": "0.4.0", | ||||
|             "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", | ||||
|             "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", | ||||
|             "dev": true, | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/axios": { | ||||
|             "version": "1.8.1", | ||||
|             "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", | ||||
|             "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "follow-redirects": "^1.15.6", | ||||
|                 "form-data": "^4.0.0", | ||||
|                 "proxy-from-env": "^1.1.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/balanced-match": { | ||||
|             "version": "1.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", | ||||
|  | @ -3437,6 +3450,12 @@ | |||
|                 "url": "https://github.com/sponsors/antfu" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/bmp-js": { | ||||
|             "version": "0.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", | ||||
|             "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/bl": { | ||||
|             "version": "4.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", | ||||
|  | @ -3884,7 +3903,6 @@ | |||
|             "version": "1.0.8", | ||||
|             "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", | ||||
|             "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "delayed-stream": "~1.0.0" | ||||
|  | @ -4180,7 +4198,6 @@ | |||
|             "version": "1.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", | ||||
|             "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">=0.4.0" | ||||
|  | @ -4453,7 +4470,6 @@ | |||
|             "version": "2.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", | ||||
|             "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "es-errors": "^1.3.0", | ||||
|  | @ -5212,6 +5228,26 @@ | |||
|             "dev": true, | ||||
|             "license": "ISC" | ||||
|         }, | ||||
|         "node_modules/follow-redirects": { | ||||
|             "version": "1.15.9", | ||||
|             "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", | ||||
|             "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", | ||||
|             "funding": [ | ||||
|                 { | ||||
|                     "type": "individual", | ||||
|                     "url": "https://github.com/sponsors/RubenVerborgh" | ||||
|                 } | ||||
|             ], | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">=4.0" | ||||
|             }, | ||||
|             "peerDependenciesMeta": { | ||||
|                 "debug": { | ||||
|                     "optional": true | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/foreground-child": { | ||||
|             "version": "3.3.0", | ||||
|             "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", | ||||
|  | @ -5233,7 +5269,6 @@ | |||
|             "version": "4.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", | ||||
|             "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "asynckit": "^0.4.0", | ||||
|  | @ -5249,7 +5284,6 @@ | |||
|             "version": "1.52.0", | ||||
|             "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", | ||||
|             "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">= 0.6" | ||||
|  | @ -5259,7 +5293,6 @@ | |||
|             "version": "2.1.35", | ||||
|             "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", | ||||
|             "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "mime-db": "1.52.0" | ||||
|  | @ -5654,7 +5687,6 @@ | |||
|             "version": "1.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", | ||||
|             "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "has-symbols": "^1.0.3" | ||||
|  | @ -5808,6 +5840,12 @@ | |||
|                 "node": ">=0.10.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/idb-keyval": { | ||||
|             "version": "6.2.1", | ||||
|             "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", | ||||
|             "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==", | ||||
|             "license": "Apache-2.0" | ||||
|         }, | ||||
|         "node_modules/ieee754": { | ||||
|             "version": "1.2.1", | ||||
|             "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", | ||||
|  | @ -6080,6 +6118,12 @@ | |||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/is-url": { | ||||
|             "version": "1.2.4", | ||||
|             "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", | ||||
|             "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/is-what": { | ||||
|             "version": "4.1.16", | ||||
|             "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", | ||||
|  | @ -7010,6 +7054,47 @@ | |||
|                 "node": ">= 0.6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/node-fetch": { | ||||
|             "version": "2.7.0", | ||||
|             "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", | ||||
|             "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "whatwg-url": "^5.0.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": "4.x || >=6.0.0" | ||||
|             }, | ||||
|             "peerDependencies": { | ||||
|                 "encoding": "^0.1.0" | ||||
|             }, | ||||
|             "peerDependenciesMeta": { | ||||
|                 "encoding": { | ||||
|                     "optional": true | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/node-fetch/node_modules/tr46": { | ||||
|             "version": "0.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", | ||||
|             "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/node-fetch/node_modules/webidl-conversions": { | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", | ||||
|             "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", | ||||
|             "license": "BSD-2-Clause" | ||||
|         }, | ||||
|         "node_modules/node-fetch/node_modules/whatwg-url": { | ||||
|             "version": "5.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", | ||||
|             "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "tr46": "~0.0.3", | ||||
|                 "webidl-conversions": "^3.0.0" | ||||
|         }, | ||||
|         "node_modules/node-abi": { | ||||
|             "version": "3.74.0", | ||||
|             "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", | ||||
|  | @ -7331,6 +7416,15 @@ | |||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/opencollective-postinstall": { | ||||
|             "version": "2.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", | ||||
|             "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", | ||||
|             "license": "MIT", | ||||
|             "bin": { | ||||
|                 "opencollective-postinstall": "index.js" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/optionator": { | ||||
|             "version": "0.9.4", | ||||
|             "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", | ||||
|  | @ -7961,6 +8055,12 @@ | |||
|                 "node": ">= 0.10" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/proxy-from-env": { | ||||
|             "version": "1.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", | ||||
|             "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/pump": { | ||||
|             "version": "3.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", | ||||
|  | @ -8118,6 +8218,12 @@ | |||
|             "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", | ||||
|             "license": "Apache-2.0" | ||||
|         }, | ||||
|         "node_modules/regenerator-runtime": { | ||||
|             "version": "0.13.11", | ||||
|             "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", | ||||
|             "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/require-directory": { | ||||
|             "version": "2.1.1", | ||||
|             "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", | ||||
|  | @ -9091,6 +9197,30 @@ | |||
|                 "node": ">=8.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/tesseract.js": { | ||||
|             "version": "6.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.0.tgz", | ||||
|             "integrity": "sha512-tqYCod1HwJzkeZw1l6XWx+ly2hhisGcBtak9MArhYwDAxL0NgeVhLJcUjqPxZMQtpgtVUzWcpZPryi+hnaQGVw==", | ||||
|             "hasInstallScript": true, | ||||
|             "license": "Apache-2.0", | ||||
|             "dependencies": { | ||||
|                 "bmp-js": "^0.1.0", | ||||
|                 "idb-keyval": "^6.2.0", | ||||
|                 "is-url": "^1.2.4", | ||||
|                 "node-fetch": "^2.6.9", | ||||
|                 "opencollective-postinstall": "^2.0.3", | ||||
|                 "regenerator-runtime": "^0.13.3", | ||||
|                 "tesseract.js-core": "^6.0.0", | ||||
|                 "wasm-feature-detect": "^1.2.11", | ||||
|                 "zlibjs": "^0.3.1" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/tesseract.js-core": { | ||||
|             "version": "6.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz", | ||||
|             "integrity": "sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==", | ||||
|             "license": "Apache-2.0" | ||||
|         }, | ||||
|         "node_modules/tildify": { | ||||
|             "version": "2.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", | ||||
|  | @ -10493,6 +10623,12 @@ | |||
|                 "node": ">=18" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/wasm-feature-detect": { | ||||
|             "version": "1.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", | ||||
|             "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", | ||||
|             "license": "Apache-2.0" | ||||
|         }, | ||||
|         "node_modules/webidl-conversions": { | ||||
|             "version": "7.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", | ||||
|  | @ -10818,6 +10954,15 @@ | |||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/zlibjs": { | ||||
|             "version": "0.3.1", | ||||
|             "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", | ||||
|             "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": "*" | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ | |||
|         "typescript-eslint": "^8.24.1" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@types/js-yaml": "^4.0.9" | ||||
|         "@types/js-yaml": "^4.0.9", | ||||
|         "vue-i18n": "^10.0.5" | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl