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" |         "test:unit": "vitest" | ||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@mikro-orm/core": "6.4.6", |         "@mikro-orm/core": "^6.4.6", | ||||||
|         "@mikro-orm/postgresql": "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/sqlite": "6.4.6", | ||||||
|         "@mikro-orm/reflection": "6.4.6", |  | ||||||
|         "dotenv": "^16.4.7", |         "dotenv": "^16.4.7", | ||||||
|         "express": "^5.0.1", |         "express": "^5.0.1", | ||||||
|         "uuid": "^11.1.0", |         "uuid": "^11.1.0", | ||||||
|         "express": "^5.0.1", |  | ||||||
|         "js-yaml": "^4.1.0", |         "js-yaml": "^4.1.0", | ||||||
|         "@types/js-yaml": "^4.0.9", |         "@types/js-yaml": "^4.0.9" | ||||||
|     }, |     }, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "@mikro-orm/cli": "^6.4.6", |         "@mikro-orm/cli": "^6.4.6", | ||||||
|  |  | ||||||
|  | @ -3,14 +3,16 @@ import { initORM } from './orm.js'; | ||||||
| import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | ||||||
| 
 | 
 | ||||||
| import themeRoutes from './routes/themes.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 studentRouter from './routes/student.js'; | ||||||
| import groupRouter from './routes/group'; | import groupRouter from './routes/group.js'; | ||||||
| import assignmentRouter from './routes/assignment'; | import assignmentRouter from './routes/assignment.js'; | ||||||
| import submissionRouter from './routes/submission'; | import submissionRouter from './routes/submission.js'; | ||||||
| import classRouter from './routes/class'; | import classRouter from './routes/class.js'; | ||||||
| import questionRouter from './routes/question'; | import questionRouter from './routes/question.js'; | ||||||
| import loginRouter from './routes/login'; | import loginRouter from './routes/login.js'; | ||||||
| 
 | 
 | ||||||
| const app: Express = express(); | const app: Express = express(); | ||||||
| const port: string | number = getNumericEnvVar(EnvVars.Port); | const port: string | number = getNumericEnvVar(EnvVars.Port); | ||||||
|  | @ -32,6 +34,8 @@ app.use('/question', questionRouter); | ||||||
| app.use('/login', loginRouter); | app.use('/login', loginRouter); | ||||||
| 
 | 
 | ||||||
| app.use('/theme', themeRoutes); | app.use('/theme', themeRoutes); | ||||||
|  | app.use('/learningPath', learningPathRoutes); | ||||||
|  | app.use('/learningObject', learningObjectRoutes); | ||||||
| 
 | 
 | ||||||
| async function startServer() { | async function startServer() { | ||||||
|     await initORM(); |     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 { Request, Response } from 'express'; | ||||||
| import { themes } from '../data/themes.js'; | import { themes } from '../data/themes.js'; | ||||||
| import { loadTranslations } from "../util/translationHelper.js"; | import { loadTranslations } from "../util/translationHelper.js"; | ||||||
|  | import { FALLBACK_LANG } from '../config.js'; | ||||||
| 
 | 
 | ||||||
| interface Translations { | interface Translations { | ||||||
|     curricula_page: { |     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) { | export function getThemes(req: Request, res: Response) { | ||||||
|     const language = (req.query.language as string)?.toLowerCase() || 'nl'; |     const language = (req.query.language as string)?.toLowerCase() || 'nl'; | ||||||
|     const translations = loadTranslations<Translations>(language); |     const translations = loadTranslations<Translations>(language); | ||||||
| 
 |  | ||||||
|     const themeList = themes.map((theme) => { |     const themeList = themes.map((theme) => { | ||||||
|         return { |         return { | ||||||
|             key: theme.title, |             key: theme.title, | ||||||
|  | @ -28,9 +25,6 @@ export function getThemes(req: Request, res: Response) { | ||||||
|     res.json(themeList); |     res.json(themeList); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** |  | ||||||
|  * GET /themes/:theme → Geeft de HRUIDs terug voor een specifiek thema |  | ||||||
|  */ |  | ||||||
| export function getThemeByTitle(req: Request, res: Response) { | export function getThemeByTitle(req: Request, res: Response) { | ||||||
|     const themeKey = req.params.theme; |     const themeKey = req.params.theme; | ||||||
|     const theme = themes.find((t) => { |     const theme = themes.find((t) => { | ||||||
|  | @ -40,6 +34,6 @@ export function getThemeByTitle(req: Request, res: Response) { | ||||||
|     if (theme) { |     if (theme) { | ||||||
|         res.json(theme.hruids); |         res.json(theme.hruids); | ||||||
|     } else { |     } 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(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | // Query: language
 | ||||||
|  | //  Route to fetch list of {key, title, description, image} themes in their respective language
 | ||||||
| router.get('/', getThemes); | router.get('/', getThemes); | ||||||
|  | 
 | ||||||
|  | // Arg: theme (key)
 | ||||||
|  | //  Route to fetch list of hruids based on theme
 | ||||||
| router.get('/:theme', getThemeByTitle); | router.get('/:theme', getThemeByTitle); | ||||||
| 
 | 
 | ||||||
| export default router; | 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"> | <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> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <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> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <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> | </style> | ||||||
|  |  | ||||||
|  | @ -14,6 +14,11 @@ const app = createApp(App); | ||||||
| 
 | 
 | ||||||
| app.use(router); | 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({ | const vuetify = createVuetify({ | ||||||
|     components, |     components, | ||||||
|     directives, |     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" |                 "frontend" | ||||||
|             ], |             ], | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@types/js-yaml": "^4.0.9" |                 "@types/js-yaml": "^4.0.9", | ||||||
|                 "vue-i18n": "^10.0.5" |                 "vue-i18n": "^10.0.5" | ||||||
|             }, |             }, | ||||||
|             "devDependencies": { |             "devDependencies": { | ||||||
|  | @ -32,13 +32,16 @@ | ||||||
|             "name": "dwengo-1-backend", |             "name": "dwengo-1-backend", | ||||||
|             "version": "0.0.1", |             "version": "0.0.1", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@mikro-orm/core": "6.4.6", |                 "@mikro-orm/core": "^6.4.6", | ||||||
|                 "@mikro-orm/postgresql": "6.4.6", |                 "@mikro-orm/postgresql": "^6.4.6", | ||||||
|                 "@mikro-orm/reflection": "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/sqlite": "6.4.6", | ||||||
|  | 
 | ||||||
|                 "dotenv": "^16.4.7", |                 "dotenv": "^16.4.7", | ||||||
|                 "express": "^5.0.1", |                 "express": "^5.0.1", | ||||||
|                 "uuid": "^11.1.0" |                 "uuid": "^11.1.0", | ||||||
|                 "js-yaml": "^4.1.0", |                 "js-yaml": "^4.1.0", | ||||||
|                 "@types/js-yaml": "^4.0.9", |                 "@types/js-yaml": "^4.0.9", | ||||||
|             }, |             }, | ||||||
|  | @ -3391,9 +3394,19 @@ | ||||||
|             "version": "0.4.0", |             "version": "0.4.0", | ||||||
|             "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", |             "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", | ||||||
|             "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", |             "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT" |             "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": { |         "node_modules/balanced-match": { | ||||||
|             "version": "1.0.2", |             "version": "1.0.2", | ||||||
|             "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", |             "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", | ||||||
|  | @ -3437,6 +3450,12 @@ | ||||||
|                 "url": "https://github.com/sponsors/antfu" |                 "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": { |         "node_modules/bl": { | ||||||
|             "version": "4.1.0", |             "version": "4.1.0", | ||||||
|             "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", |             "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", | ||||||
|  | @ -3884,7 +3903,6 @@ | ||||||
|             "version": "1.0.8", |             "version": "1.0.8", | ||||||
|             "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", |             "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", | ||||||
|             "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", |             "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "delayed-stream": "~1.0.0" |                 "delayed-stream": "~1.0.0" | ||||||
|  | @ -4180,7 +4198,6 @@ | ||||||
|             "version": "1.0.0", |             "version": "1.0.0", | ||||||
|             "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", |             "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", | ||||||
|             "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", |             "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">=0.4.0" |                 "node": ">=0.4.0" | ||||||
|  | @ -4453,7 +4470,6 @@ | ||||||
|             "version": "2.1.0", |             "version": "2.1.0", | ||||||
|             "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", |             "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", | ||||||
|             "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", |             "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "es-errors": "^1.3.0", |                 "es-errors": "^1.3.0", | ||||||
|  | @ -5212,6 +5228,26 @@ | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "ISC" |             "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": { |         "node_modules/foreground-child": { | ||||||
|             "version": "3.3.0", |             "version": "3.3.0", | ||||||
|             "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", |             "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", | ||||||
|  | @ -5233,7 +5269,6 @@ | ||||||
|             "version": "4.0.2", |             "version": "4.0.2", | ||||||
|             "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", |             "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", | ||||||
|             "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", |             "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "asynckit": "^0.4.0", |                 "asynckit": "^0.4.0", | ||||||
|  | @ -5249,7 +5284,6 @@ | ||||||
|             "version": "1.52.0", |             "version": "1.52.0", | ||||||
|             "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", |             "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", | ||||||
|             "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", |             "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">= 0.6" |                 "node": ">= 0.6" | ||||||
|  | @ -5259,7 +5293,6 @@ | ||||||
|             "version": "2.1.35", |             "version": "2.1.35", | ||||||
|             "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", |             "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", | ||||||
|             "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", |             "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "mime-db": "1.52.0" |                 "mime-db": "1.52.0" | ||||||
|  | @ -5654,7 +5687,6 @@ | ||||||
|             "version": "1.0.2", |             "version": "1.0.2", | ||||||
|             "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", |             "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", | ||||||
|             "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", |             "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "has-symbols": "^1.0.3" |                 "has-symbols": "^1.0.3" | ||||||
|  | @ -5808,6 +5840,12 @@ | ||||||
|                 "node": ">=0.10.0" |                 "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": { |         "node_modules/ieee754": { | ||||||
|             "version": "1.2.1", |             "version": "1.2.1", | ||||||
|             "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", |             "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", | ||||||
|  | @ -6080,6 +6118,12 @@ | ||||||
|                 "url": "https://github.com/sponsors/sindresorhus" |                 "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": { |         "node_modules/is-what": { | ||||||
|             "version": "4.1.16", |             "version": "4.1.16", | ||||||
|             "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", |             "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", | ||||||
|  | @ -7010,6 +7054,47 @@ | ||||||
|                 "node": ">= 0.6" |                 "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": { |         "node_modules/node-abi": { | ||||||
|             "version": "3.74.0", |             "version": "3.74.0", | ||||||
|             "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", |             "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", | ||||||
|  | @ -7331,6 +7416,15 @@ | ||||||
|                 "url": "https://github.com/sponsors/sindresorhus" |                 "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": { |         "node_modules/optionator": { | ||||||
|             "version": "0.9.4", |             "version": "0.9.4", | ||||||
|             "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", |             "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", | ||||||
|  | @ -7961,6 +8055,12 @@ | ||||||
|                 "node": ">= 0.10" |                 "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": { |         "node_modules/pump": { | ||||||
|             "version": "3.0.2", |             "version": "3.0.2", | ||||||
|             "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", |             "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", | ||||||
|  | @ -8118,6 +8218,12 @@ | ||||||
|             "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", |             "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", | ||||||
|             "license": "Apache-2.0" |             "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": { |         "node_modules/require-directory": { | ||||||
|             "version": "2.1.1", |             "version": "2.1.1", | ||||||
|             "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", |             "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", | ||||||
|  | @ -9091,6 +9197,30 @@ | ||||||
|                 "node": ">=8.0.0" |                 "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": { |         "node_modules/tildify": { | ||||||
|             "version": "2.0.0", |             "version": "2.0.0", | ||||||
|             "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", |             "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", | ||||||
|  | @ -10493,6 +10623,12 @@ | ||||||
|                 "node": ">=18" |                 "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": { |         "node_modules/webidl-conversions": { | ||||||
|             "version": "7.0.0", |             "version": "7.0.0", | ||||||
|             "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", |             "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", | ||||||
|  | @ -10818,6 +10954,15 @@ | ||||||
|             "funding": { |             "funding": { | ||||||
|                 "url": "https://github.com/sponsors/sindresorhus" |                 "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" |         "typescript-eslint": "^8.24.1" | ||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@types/js-yaml": "^4.0.9" |         "@types/js-yaml": "^4.0.9", | ||||||
|         "vue-i18n": "^10.0.5" |         "vue-i18n": "^10.0.5" | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl