Merge branch 'dev' into feat/home-data

This commit is contained in:
Gabriellvl 2025-03-02 12:41:24 +01:00 committed by GitHub
commit 1531fa36fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 931 additions and 35 deletions

View file

@ -14,16 +14,17 @@
"test:unit": "vitest"
},
"dependencies": {
"@mikro-orm/core": "6.4.6",
"@mikro-orm/postgresql": "6.4.6",
"@mikro-orm/core": "^6.4.6",
"@mikro-orm/postgresql": "^6.4.6",
"@mikro-orm/reflection": "^6.4.6",
"@types/js-yaml": "^4.0.9",
"axios": "^1.8.1",
"@mikro-orm/sqlite": "6.4.6",
"@mikro-orm/reflection": "6.4.6",
"dotenv": "^16.4.7",
"express": "^5.0.1",
"uuid": "^11.1.0",
"express": "^5.0.1",
"js-yaml": "^4.1.0",
"@types/js-yaml": "^4.0.9",
"@types/js-yaml": "^4.0.9"
},
"devDependencies": {
"@mikro-orm/cli": "^6.4.6",

View file

@ -3,14 +3,16 @@ import { initORM } from './orm.js';
import { EnvVars, getNumericEnvVar } from './util/envvars.js';
import themeRoutes from './routes/themes.js';
import learningPathRoutes from './routes/learningPaths.js';
import learningObjectRoutes from './routes/learningObjects.js';
import studentRouter from './routes/student';
import groupRouter from './routes/group';
import assignmentRouter from './routes/assignment';
import submissionRouter from './routes/submission';
import classRouter from './routes/class';
import questionRouter from './routes/question';
import loginRouter from './routes/login';
import studentRouter from './routes/student.js';
import groupRouter from './routes/group.js';
import assignmentRouter from './routes/assignment.js';
import submissionRouter from './routes/submission.js';
import classRouter from './routes/class.js';
import questionRouter from './routes/question.js';
import loginRouter from './routes/login.js';
const app: Express = express();
const port: string | number = getNumericEnvVar(EnvVars.Port);
@ -32,6 +34,8 @@ app.use('/question', questionRouter);
app.use('/login', loginRouter);
app.use('/theme', themeRoutes);
app.use('/learningPath', learningPathRoutes);
app.use('/learningObject', learningObjectRoutes);
async function startServer() {
await initORM();

10
backend/src/config.ts Normal file
View 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';

View 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' });
}
}

View 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' });
}
}

View file

@ -1,6 +1,7 @@
import { Request, Response } from 'express';
import { themes } from '../data/themes.js';
import { loadTranslations } from "../util/translationHelper.js";
import { FALLBACK_LANG } from '../config.js';
interface Translations {
curricula_page: {
@ -8,13 +9,9 @@ interface Translations {
};
}
/**
* GET /themes Haalt de lijst met thema's op inclusief vertalingen
*/
export function getThemes(req: Request, res: Response) {
const language = (req.query.language as string)?.toLowerCase() || 'nl';
const translations = loadTranslations<Translations>(language);
const themeList = themes.map((theme) => {
return {
key: theme.title,
@ -28,9 +25,6 @@ export function getThemes(req: Request, res: Response) {
res.json(themeList);
}
/**
* GET /themes/:theme Geeft de HRUIDs terug voor een specifiek thema
*/
export function getThemeByTitle(req: Request, res: Response) {
const themeKey = req.params.theme;
const theme = themes.find((t) => {
@ -40,6 +34,6 @@ export function getThemeByTitle(req: Request, res: Response) {
if (theme) {
res.json(theme.hruids);
} else {
res.status(404).json({ error: 'Thema niet gevonden' });
res.status(404).json({ error: 'Theme not found' });
}
}

View 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;
}

View 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;

View 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;

View file

@ -3,7 +3,12 @@ import { getThemes, getThemeByTitle } from '../controllers/themes.js';
const router = express.Router();
// Query: language
// Route to fetch list of {key, title, description, image} themes in their respective language
router.get('/', getThemes);
// Arg: theme (key)
// Route to fetch list of hruids based on theme
router.get('/:theme', getThemeByTitle);
export default router;

View 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[];
}

View 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 ?? [];
}

View 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;
}
}

View file

@ -1,11 +1,186 @@
<script setup lang="ts">
import { ref } from "vue";
import { useRoute } from "vue-router";
import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg";
const route = useRoute();
// Instantiate variables to use in html to render right
// Links and content dependent on the role (student or teacher)
const isTeacher = route.path.includes("teacher");
const userId = route.params.id as string;
const role = isTeacher ? "teacher" : "student";
const name = "Kurt Cobain";
const initials = name
.split(" ")
.map((n) => {
return n[0];
})
.join("");
const languages = ref([
{ name: "English", code: "en" },
{ name: "Nederlands", code: "nl" },
]);
// Logic to change the language of the website to the selected language
const changeLanguage = (langCode: string) => {
console.log(langCode);
};
</script>
<template>
<main></main>
<main>
<nav class="menu">
<div class="left">
<ul>
<li>
<router-link
:to="`/${role}/${userId}`"
class="dwengo_home"
>
<img
class="dwengo_logo"
:src="dwengoLogo"
/>
<p class="caption">
{{ role }}
</p>
</router-link>
</li>
<li>
<router-link
:to="`/${role}/${userId}/assignment`"
class="menu_item"
>
assignments
</router-link>
</li>
<li>
<router-link
:to="`/${role}/${userId}/class`"
class="menu_item"
>classes</router-link
>
</li>
<li>
<router-link
:to="`/${role}/${userId}/discussion`"
class="menu_item"
>discussions</router-link
>
</li>
<li>
<v-menu open-on-hover>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon
variant="text"
>
<v-icon
icon="mdi-translate"
size="small"
color="#0e6942"
></v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
v-for="(language, index) in languages"
:key="index"
@click="changeLanguage(language.code)"
>
<v-list-item-title>{{ language.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</li>
</ul>
</div>
<div class="right">
<li>
<router-link :to="`/login`">
<v-tooltip
text="log out"
location="bottom"
>
<template v-slot:activator="{ props }">
<v-icon
v-bind="props"
icon="mdi-logout"
size="x-large"
color="#0e6942"
></v-icon>
</template>
</v-tooltip>
</router-link>
</li>
<li>
<v-avatar
size="large"
color="#0e6942"
style="font-size: large; font-weight: bold"
>{{ initials }}</v-avatar
>
</li>
</div>
</nav>
</main>
</template>
<style scoped>
.menu {
background-color: #f6faf2;
display: flex;
justify-content: space-between;
}
.right {
align-items: center;
padding: 10px;
}
.right li {
margin-left: 15px;
}
nav ul {
display: flex;
list-style-type: none;
margin: 0;
padding: 0;
gap: 15px;
align-items: center;
}
li {
display: inline;
}
.dwengo_home {
text-align: center;
text-decoration: none;
}
.dwengo_logo {
width: 150px;
}
.caption {
color: black;
margin-top: -25px;
}
.menu_item {
color: #0e6942;
text-decoration: none;
font-size: large;
}
nav a.router-link-active {
font-weight: bold;
}
</style>

View file

@ -14,6 +14,11 @@ const app = createApp(App);
app.use(router);
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css";
document.head.appendChild(link);
const vuetify = createVuetify({
components,
directives,

View 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}`;
}

File diff suppressed because one or more lines are too long

View 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
View file

@ -13,7 +13,7 @@
"frontend"
],
"dependencies": {
"@types/js-yaml": "^4.0.9"
"@types/js-yaml": "^4.0.9",
"vue-i18n": "^10.0.5"
},
"devDependencies": {
@ -32,13 +32,16 @@
"name": "dwengo-1-backend",
"version": "0.0.1",
"dependencies": {
"@mikro-orm/core": "6.4.6",
"@mikro-orm/postgresql": "6.4.6",
"@mikro-orm/reflection": "6.4.6",
"@mikro-orm/core": "^6.4.6",
"@mikro-orm/postgresql": "^6.4.6",
"@mikro-orm/reflection": "^6.4.6",
"@types/js-yaml": "^4.0.9",
"axios": "^1.8.1",
"@mikro-orm/sqlite": "6.4.6",
"dotenv": "^16.4.7",
"express": "^5.0.1",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"js-yaml": "^4.1.0",
"@types/js-yaml": "^4.0.9",
},
@ -3391,9 +3394,19 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/axios": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -3437,6 +3450,12 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/bmp-js": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
"integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==",
"license": "MIT"
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@ -3884,7 +3903,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@ -4180,7 +4198,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@ -4453,7 +4470,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -5212,6 +5228,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
@ -5233,7 +5269,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@ -5249,7 +5284,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@ -5259,7 +5293,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@ -5654,7 +5687,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@ -5808,6 +5840,12 @@
"node": ">=0.10.0"
}
},
"node_modules/idb-keyval": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
"integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==",
"license": "Apache-2.0"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@ -6080,6 +6118,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT"
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
@ -7010,6 +7054,47 @@
"node": ">= 0.6"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
},
"node_modules/node-abi": {
"version": "3.74.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
@ -7331,6 +7416,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/opencollective-postinstall": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
"license": "MIT",
"bin": {
"opencollective-postinstall": "index.js"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -7961,6 +8055,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
@ -8118,6 +8218,12 @@
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0"
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT"
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -9091,6 +9197,30 @@
"node": ">=8.0.0"
}
},
"node_modules/tesseract.js": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.0.tgz",
"integrity": "sha512-tqYCod1HwJzkeZw1l6XWx+ly2hhisGcBtak9MArhYwDAxL0NgeVhLJcUjqPxZMQtpgtVUzWcpZPryi+hnaQGVw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"bmp-js": "^0.1.0",
"idb-keyval": "^6.2.0",
"is-url": "^1.2.4",
"node-fetch": "^2.6.9",
"opencollective-postinstall": "^2.0.3",
"regenerator-runtime": "^0.13.3",
"tesseract.js-core": "^6.0.0",
"wasm-feature-detect": "^1.2.11",
"zlibjs": "^0.3.1"
}
},
"node_modules/tesseract.js-core": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz",
"integrity": "sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==",
"license": "Apache-2.0"
},
"node_modules/tildify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz",
@ -10493,6 +10623,12 @@
"node": ">=18"
}
},
"node_modules/wasm-feature-detect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
"license": "Apache-2.0"
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@ -10818,6 +10954,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zlibjs": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
"integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
"license": "MIT",
"engines": {
"node": "*"
}
}
}
}

View file

@ -37,7 +37,7 @@
"typescript-eslint": "^8.24.1"
},
"dependencies": {
"@types/js-yaml": "^4.0.9"
"@types/js-yaml": "^4.0.9",
"vue-i18n": "^10.0.5"
}
}