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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue