This commit is contained in:
Alvin-Zilverstand
2026-09-09 10:45:31 +02:00
parent f3e79a7aeb
commit d420c317ab
88 changed files with 36601 additions and 0 deletions
@@ -0,0 +1 @@
*/**.env
@@ -0,0 +1,10 @@
# Dependencies
/node_modules
/client/node_modules
# Environment variables
.env
server/.env
# Build output
/client/build
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
<path d="M4 1h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2zm0 1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/>
<path d="M5 5h6v1H5V5zm0 2h6v1H5V7zm0 2h4v1H5V9z"/>
</svg>

After

Width:  |  Height:  |  Size: 297 B

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Azure Document Analyzer</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
@@ -0,0 +1,91 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #282c34;
color: white;
}
.App {
text-align: center;
}
.App-header {
background-color: #1a1d24;
padding: 20px;
color: white;
}
h1 {
font-size: 2.5rem;
}
main {
padding: 20px;
}
.upload-section {
margin-bottom: 20px;
}
input[type="file"] {
margin-right: 10px;
}
button {
background-color: #61dafb;
color: #282c34;
border: none;
padding: 10px 20px;
font-size: 1rem;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #4fa8c5;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.error-message {
color: #ff6b6b;
margin: 20px 0;
}
.results-section {
margin-top: 20px;
text-align: left;
background-color: #20232a;
padding: 20px;
border-radius: 8px;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'Courier New', Courier, monospace;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.clear-button {
background-color: #ff6b6b;
color: white;
}
.clear-button:hover {
background-color: #e05252;
}
@@ -0,0 +1,102 @@
import React, { useState, useRef } from 'react';
import './App.css';
function App() {
const [file, setFile] = useState(null);
const [analysisResult, setAnalysisResult] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const fileInputRef = useRef(null);
const handleFileChange = (e) => {
setFile(e.target.files[0]);
};
const handleAnalyzeClick = async () => {
if (!file) {
setError('Please select a file first.');
return;
}
const formData = new FormData();
formData.append('document', file);
setLoading(true);
setError(null);
setAnalysisResult(null);
try {
const response = await fetch('/api/analyze', {
method: 'POST',
body: formData,
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Failed to analyze document.');
}
setAnalysisResult(result);
// Trigger download
const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `analysis-result.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleClear = () => {
setFile(null);
setAnalysisResult(null);
setError(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<div className="App">
<header className="App-header">
<h1>Azure Document Analyzer</h1>
<p>Upload a document (PDF, Word, Excel) to analyze its content.</p>
<p style={{fontSize: '0.8rem', opacity: 0.8}}>(Recommended formats: .pdf, .docx, .xlsx)</p>
</header>
<main>
<div className="upload-section">
<input
type="file"
onChange={handleFileChange}
accept=".pdf,.docx,.xlsx,.pptx,.jpg,.png"
ref={fileInputRef}
/>
<button onClick={handleAnalyzeClick} disabled={loading || !file}>
{loading ? 'Analyzing...' : 'Analyze Document'}
</button>
</div>
{error && <div className="error-message">{error}</div>}
{analysisResult && (
<div className="results-section">
<div className="results-header">
<h2>Analysis Result</h2>
<button onClick={handleClear} className="clear-button">Clear</button>
</div>
<pre>{JSON.stringify(analysisResult, null, 2)}</pre>
</div>
)}
</main>
</div>
);
}
export default App;
@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
@@ -0,0 +1,330 @@
{
"name": "document-analyzer",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "document-analyzer",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"concurrently": "^9.2.1"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/concurrently": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "4.1.2",
"rxjs": "7.8.2",
"shell-quote": "1.8.3",
"supports-color": "8.1.1",
"tree-kill": "1.2.2",
"yargs": "17.7.2"
},
"bin": {
"conc": "dist/bin/concurrently.js",
"concurrently": "dist/bin/concurrently.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"license": "MIT",
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
}
}
}
@@ -0,0 +1,16 @@
[{
"order": {
"Naam":"",
"Datum": "yyyy-mm-dd",
"Relatiecode":"",
"Referentie":"",
"regels": [
{
"productcode": 0,
"omschrijving": "",
"aantal": 0,
"prijs": 0.00
}
]
}
}]
@@ -0,0 +1,6 @@
# Azure Document Intelligence credentials
AZURE_ENDPOINT="https://alvinzilverstand-8432-resource.cognitiveservices.azure.com/"
AZURE_API_KEY="YOUR_AZURE_API_KEY"
# Server port (optional)
PORT=3001
@@ -0,0 +1,213 @@
#!/usr/bin/env node
require('dotenv').config();
const fs = require('fs').promises;
const path = require('path');
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const { DocumentAnalysisClient, AzureKeyCredential } = require('@azure/ai-form-recognizer');
const formatDate = (dateStr) => {
if (!dateStr) return "yyyy-mm-dd";
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.toISOString().split('T')[0];
}
const match = dateStr.match(/(\d{1,2})[-/](\d{1,2})[-/](\d{4})/);
if (match) {
return `${match[3]}-${match[2].padStart(2, '0')}-${match[1].padStart(2, '0')}`;
}
return dateStr;
};
const parseNumber = (text, isFloat = false) => {
if (!text) return isFloat ? 0.00 : 0;
const cleanText = text.replace(/[^\d.,-]/g, '').replace(',', '.');
const val = isFloat ? parseFloat(cleanText) : parseInt(cleanText);
return isNaN(val) ? (isFloat ? 0.00 : 0) : val;
};
async function analyzeDocument(filePath) {
const endpoint = process.env.AZURE_ENDPOINT;
const apiKey = process.env.AZURE_API_KEY;
if (!endpoint || !apiKey) {
throw new Error('Azure credentials not configured. Please check your .env file.');
}
const fileBuffer = await fs.readFile(filePath);
const client = new DocumentAnalysisClient(endpoint, new AzureKeyCredential(apiKey), {
apiVersion: "2023-07-31"
});
const poller = await client.beginAnalyzeDocument('prebuilt-layout', fileBuffer);
const { content, pages, tables, keyValuePairs = [] } = await poller.pollUntilDone();
const findValue = (keys) => {
const kv = keyValuePairs.find(p =>
keys.some(k => p.key.content.toLowerCase().includes(k.toLowerCase())) &&
p.value && p.value.content.trim() !== ""
);
if (kv && kv.value) return kv.value.content;
return "";
};
// Improved Naam detection
let detectedName = findValue(["Naam", "Name", "Klant", "Customer", "Billed to", "Factuur aan"]);
if (!detectedName) {
for (const table of tables) {
const billedToCell = table.cells.find(c => c.content.toLowerCase().includes("billed to") || c.content.toLowerCase().includes("factuur aan"));
if (billedToCell) {
const belowCell = table.cells.find(c => c.rowIndex === billedToCell.rowIndex + 1 && c.columnIndex === billedToCell.columnIndex);
if (belowCell && belowCell.content.trim()) {
detectedName = belowCell.content.trim();
break;
}
const rightCell = table.cells.find(c => c.rowIndex === billedToCell.rowIndex && c.columnIndex === billedToCell.columnIndex + 1);
if (rightCell && rightCell.content.trim()) {
detectedName = rightCell.content.trim();
break;
}
}
}
}
if (!detectedName && content) {
const lines = content.split('\n');
const billedToIndex = lines.findIndex(l => l.toLowerCase().includes("billed to") || l.toLowerCase().includes("factuur aan"));
if (billedToIndex !== -1 && billedToIndex + 1 < lines.length) {
for (let i = billedToIndex + 1; i < Math.min(billedToIndex + 5, lines.length); i++) {
const candidate = lines[i].trim();
if (candidate && !candidate.toLowerCase().includes("from") && !candidate.toLowerCase().includes("invoice") && !candidate.toLowerCase().includes("purchase") && !candidate.toLowerCase().includes("billed by")) {
detectedName = candidate;
break;
}
}
}
}
if (detectedName && detectedName.includes('\n')) {
detectedName = detectedName.split('\n')[0].trim();
}
const orderData = {
Naam: detectedName,
Datum: formatDate(findValue(["Datum", "Date", "Factuurdatum", "Issue Date", "Invoice Date"])),
Relatiecode: findValue(["Relatiecode", "Relatie nr", "Klantnummer", "Customer Code", "Customer ID"]),
Referentie: findValue(["Referentie", "Reference", "Your reference", "Uw referentie", "Betalingskenmerk", "Kenmerk"]),
regels: []
};
// Find the item table
let itemTable = null;
let maxScore = -1;
let columnMap = { productcode: -1, omschrijving: -1, aantal: -1, prijs: -1 };
for (const table of tables) {
let currentColumnMap = { productcode: -1, omschrijving: -1, aantal: -1, prijs: -1 };
let score = 0;
const headerRow = table.cells.filter(c => c.rowIndex === 0);
headerRow.forEach(cell => {
const text = cell.content.toLowerCase();
if (text.includes("description") || text.includes("omschrijving") || text.includes("product") || text.includes("item")) {
currentColumnMap.omschrijving = cell.columnIndex;
score += 10;
} else if (text.includes("code") || text.includes("sku") || text.includes("art")) {
currentColumnMap.productcode = cell.columnIndex;
score += 5;
} else if (text.includes("qty") || text.includes("aantal") || text.includes("quantity") || text.includes("stuks")) {
currentColumnMap.aantal = cell.columnIndex;
score += 10;
} else if (text.includes("price") || text.includes("prijs") || text.includes("unit") || text.includes("rate") || text.includes("cost") || text.includes("kosten")) {
currentColumnMap.prijs = cell.columnIndex;
score += 10;
} else if (text.includes("amount") || text.includes("totaal") || text.includes("subtotal")) {
if (currentColumnMap.prijs === -1) currentColumnMap.prijs = cell.columnIndex;
score += 5;
}
});
if (score > maxScore) {
maxScore = score;
itemTable = table;
columnMap = currentColumnMap;
}
}
if (itemTable && maxScore > 5) {
for (let r = 1; r < itemTable.rowCount; r++) {
const rowCells = itemTable.cells.filter(c => c.rowIndex === r);
if (rowCells.length > 0) {
const row = {
productcode: 0,
omschrijving: "",
aantal: 0,
prijs: 0.00
};
rowCells.forEach(cell => {
const content = cell.content.trim();
if (cell.columnIndex === columnMap.productcode) {
row.productcode = parseNumber(content);
} else if (cell.columnIndex === columnMap.omschrijving) {
row.omschrijving = content;
} else if (cell.columnIndex === columnMap.aantal) {
row.aantal = parseNumber(content);
} else if (cell.columnIndex === columnMap.prijs) {
row.prijs = parseNumber(content, true);
}
});
if (row.omschrijving || row.productcode !== 0 || row.prijs !== 0) {
orderData.regels.push(row);
}
}
}
}
if (orderData.regels.length === 0) {
orderData.regels.push({
productcode: 0,
omschrijving: "",
aantal: 0,
prijs: 0.00
});
}
const standardizedJson = [{ order: orderData }];
return standardizedJson;
}
async function main() {
const argv = yargs(hideBin(process.argv))
.option('file', {
alias: 'f',
describe: 'Path to the document to analyze',
demandOption: true,
type: 'string',
})
.option('output', {
alias: 'o',
describe: 'Optional: Path to save the JSON output file',
type: 'string',
})
.help()
.argv;
try {
console.log(`Analyzing document: ${argv.file}...`);
const result = await analyzeDocument(argv.file);
if (argv.output) {
await fs.writeFile(argv.output, JSON.stringify(result, null, 2));
console.log(`Successfully saved analysis to ${argv.output}`);
} else {
console.log(JSON.stringify(result, null, 2));
}
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
}
main();
@@ -0,0 +1,230 @@
require('dotenv').config();
const express = require('express');
const multer = require('multer');
const cors = require('cors');
const { DocumentAnalysisClient, AzureKeyCredential } = require('@azure/ai-form-recognizer');
const app = express();
const port = process.env.PORT || 3001;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Multer setup for file uploads
const upload = multer({ storage: multer.memoryStorage() });
// Helper functions
const formatDate = (dateStr) => {
if (!dateStr) return "yyyy-mm-dd";
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.toISOString().split('T')[0];
}
const match = dateStr.match(/(\d{1,2})[-/](\d{1,2})[-/](\d{4})/);
if (match) {
return `${match[3]}-${match[2].padStart(2, '0')}-${match[1].padStart(2, '0')}`;
}
return dateStr;
};
const parseNumber = (text, isFloat = false) => {
if (!text) return isFloat ? 0.00 : 0;
const cleanText = text.replace(/[^\d.,-]/g, '').replace(',', '.');
const val = isFloat ? parseFloat(cleanText) : parseInt(cleanText);
return isNaN(val) ? (isFloat ? 0.00 : 0) : val;
};
// Route for handling file analysis
app.post('/api/analyze', upload.single('document'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded.' });
}
const endpoint = process.env.AZURE_ENDPOINT;
const apiKey = process.env.AZURE_API_KEY;
if (!endpoint || !apiKey) {
return res.status(500).json({ error: 'Azure credentials not configured.' });
}
const client = new DocumentAnalysisClient(endpoint, new AzureKeyCredential(apiKey), {
apiVersion: "2024-11-30"
});
console.log(`Starting analysis for file: ${req.file.originalname}, mimetype: ${req.file.mimetype}`);
// Choose the best model based on file type.
// Office files (.docx, .xlsx, .pptx) are ONLY supported by 'prebuilt-layout' and 'prebuilt-read' in many regions.
// PDF and images are supported by 'prebuilt-document' which provides richer key-value pair extraction.
const isOfficeFile = req.file.mimetype.includes('officedocument') ||
req.file.originalname.endsWith('.docx') ||
req.file.originalname.endsWith('.xlsx') ||
req.file.originalname.endsWith('.pptx');
const modelId = isOfficeFile ? 'prebuilt-layout' : 'prebuilt-document';
console.log(`Using model: ${modelId} for ${isOfficeFile ? 'Office file' : 'Standard file'}`);
const poller = await client.beginAnalyzeDocument(modelId, req.file.buffer, {
contentType: req.file.mimetype
});
const { content, pages, tables, keyValuePairs = [] } = await poller.pollUntilDone();
const findValue = (keys) => {
const kv = keyValuePairs.find(p =>
keys.some(k => p.key.content.toLowerCase().includes(k.toLowerCase())) &&
p.value && p.value.content.trim() !== ""
);
if (kv && kv.value) return kv.value.content;
return "";
};
// Improved Naam detection
let detectedName = findValue(["Naam", "Name", "Klant", "Customer", "Billed to", "Factuur aan"]);
// Fallback 1: Check if "Billed to" is in a table
if (!detectedName) {
for (const table of tables) {
const billedToCell = table.cells.find(c => c.content.toLowerCase().includes("billed to") || c.content.toLowerCase().includes("factuur aan"));
if (billedToCell) {
const belowCell = table.cells.find(c => c.rowIndex === billedToCell.rowIndex + 1 && c.columnIndex === billedToCell.columnIndex);
if (belowCell && belowCell.content.trim()) {
detectedName = belowCell.content.trim();
break;
}
const rightCell = table.cells.find(c => c.rowIndex === billedToCell.rowIndex && c.columnIndex === billedToCell.columnIndex + 1);
if (rightCell && rightCell.content.trim()) {
detectedName = rightCell.content.trim();
break;
}
}
}
}
// Fallback 2: Search the raw content for "Billed to" and take the next line
if (!detectedName && content) {
const lines = content.split('\n');
const billedToIndex = lines.findIndex(l => l.toLowerCase().includes("billed to") || l.toLowerCase().includes("factuur aan"));
if (billedToIndex !== -1 && billedToIndex + 1 < lines.length) {
for (let i = billedToIndex + 1; i < Math.min(billedToIndex + 5, lines.length); i++) {
const candidate = lines[i].trim();
if (candidate && !candidate.toLowerCase().includes("from") && !candidate.toLowerCase().includes("invoice") && !candidate.toLowerCase().includes("purchase") && !candidate.toLowerCase().includes("billed by")) {
detectedName = candidate;
break;
}
}
}
}
// Clean up Naam if it contains multiple lines (just take first line as the name)
if (detectedName && detectedName.includes('\n')) {
detectedName = detectedName.split('\n')[0].trim();
}
const orderData = {
Naam: detectedName,
Datum: formatDate(findValue(["Datum", "Date", "Factuurdatum", "Issue Date", "Invoice Date"])),
Relatiecode: findValue(["Relatiecode", "Relatie nr", "Klantnummer", "Customer Code", "Customer ID"]),
Referentie: findValue(["Referentie", "Reference", "Your reference", "Uw referentie", "Betalingskenmerk", "Kenmerk"]),
regels: []
};
// Find the item table
let itemTable = null;
let maxScore = -1;
let columnMap = { productcode: -1, omschrijving: -1, aantal: -1, prijs: -1 };
for (const table of tables) {
let currentColumnMap = { productcode: -1, omschrijving: -1, aantal: -1, prijs: -1 };
let score = 0;
const headerRow = table.cells.filter(c => c.rowIndex === 0);
headerRow.forEach(cell => {
const text = cell.content.toLowerCase();
if (text.includes("description") || text.includes("omschrijving") || text.includes("product") || text.includes("item")) {
currentColumnMap.omschrijving = cell.columnIndex;
score += 10;
} else if (text.includes("code") || text.includes("sku") || text.includes("art")) {
currentColumnMap.productcode = cell.columnIndex;
score += 5;
} else if (text.includes("qty") || text.includes("aantal") || text.includes("quantity") || text.includes("stuks")) {
currentColumnMap.aantal = cell.columnIndex;
score += 10;
} else if (text.includes("price") || text.includes("prijs") || text.includes("unit") || text.includes("rate") || text.includes("cost") || text.includes("kosten")) {
currentColumnMap.prijs = cell.columnIndex;
score += 10;
} else if (text.includes("amount") || text.includes("totaal") || text.includes("subtotal")) {
if (currentColumnMap.prijs === -1) currentColumnMap.prijs = cell.columnIndex;
score += 5;
}
});
if (score > maxScore) {
maxScore = score;
itemTable = table;
columnMap = currentColumnMap;
}
}
if (itemTable && maxScore > 5) {
for (let r = 1; r < itemTable.rowCount; r++) {
const rowCells = itemTable.cells.filter(c => c.rowIndex === r);
if (rowCells.length > 0) {
const row = {
productcode: 0,
omschrijving: "",
aantal: 0,
prijs: 0.00
};
rowCells.forEach(cell => {
const content = cell.content.trim();
if (cell.columnIndex === columnMap.productcode) {
row.productcode = parseNumber(content);
} else if (cell.columnIndex === columnMap.omschrijving) {
row.omschrijving = content;
} else if (cell.columnIndex === columnMap.aantal) {
row.aantal = parseNumber(content);
} else if (cell.columnIndex === columnMap.prijs) {
row.prijs = parseNumber(content, true);
}
});
if (row.omschrijving || row.productcode !== 0 || row.prijs !== 0) {
orderData.regels.push(row);
}
}
}
}
if (orderData.regels.length === 0) {
orderData.regels.push({
productcode: 0,
omschrijving: "",
aantal: 0,
prijs: 0.00
});
}
const standardizedJson = [{ order: orderData }];
res.json(standardizedJson);
} catch (error) {
console.error('--- Analysis Error ---');
console.error('Message:', error.message);
if (error.details) {
console.error('Details:', JSON.stringify(error.details, null, 2));
}
if (error.response) {
console.error('Response Status:', error.response.status);
console.error('Response Data:', JSON.stringify(error.response.data, null, 2));
}
const errorMessage = error.details?.error?.message || error.message || 'Failed to analyze document.';
res.status(error.statusCode || 500).json({ error: errorMessage });
}
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
@@ -0,0 +1,67 @@
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: lightgray;
}
.header-top,
.header-bottom {
position: fixed;
left: 0;
width: 100%;
height: 60px;
background-color: white;
z-index: 10;
}
.header-top {
top: 0;
}
.header-bottom {
bottom: 0;
}
.container {
display: flex;
gap: 20px;
}
.box {
width: 500px;
height: 500px;
background-color: #7b8083;
display: flex;
justify-content: center;
align-items: center;
}
.boxinbox {
width: 400px;
height: 400px;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-size: 28px;
}
.action-button {
padding: 16px 28px;
border: none;
border-radius: 8px;
background-color: #c62828;
color: white;
font-size: 24px;
cursor: pointer;
}
.action-button:hover {
background-color: #a71f1f;
}
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home Page</title>
</head>
<body>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="home.css" />
<header class="header-top"></header>
<header class="header-bottom"></header>
<div class="container">
<div class="box">
<div class="boxinbox">
<button class="action-button" type="button">
Drag file to convert
</button>
</div>
</div>
<div class="box">
<div class="boxinbox">
<button class="action-button" type="button">Download</button>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,18 @@
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(
# This is the default and can be omitted
api_key=os.environ.get("KEY"),
)
response = client.responses.create(
model="gpt-5.2",
instructions="You are a coding assistant that talks like a pirate.",
input="How do I check if a Python object is an instance of a class?",
)
print(response.output_text)
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

@@ -0,0 +1,8 @@
*.env
*bin
*obj
*.exe
*.dll
*.pdb
*Debug
*.json
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
<PackageReference Include="PdfPig" Version="0.1.14" />
</ItemGroup>
</Project>
@@ -0,0 +1,124 @@
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using UglyToad.PdfPig;
using UglyToad.PdfPig.Content;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
static string ExtractText(string filePath)
{
var ext = Path.GetExtension(filePath).ToLowerInvariant();
return ext switch
{
".pdf" => ExtractFromPdf(filePath),
".docx" => ExtractFromDocx(filePath),
_ => throw new NotSupportedException($"Unsupported file type: {ext}")
};
}
static string ExtractFromPdf(string filePath)
{
var sb = new StringBuilder();
using var document = PdfDocument.Open(filePath);
foreach (Page page in document.GetPages())
sb.AppendLine(page.Text);
return sb.ToString();
}
static string ExtractFromDocx(string filePath)
{
var sb = new StringBuilder();
using var doc = WordprocessingDocument.Open(filePath, false);
var body = doc.MainDocumentPart?.Document?.Body;
if (body is null) return string.Empty;
foreach (var para in body.Descendants<Paragraph>())
sb.AppendLine(para.InnerText);
return sb.ToString();
}
var inputFile = args.Length > 0 ? args[0] : throw new Exception("Pass a file path as argument.");
var text = ExtractText(inputFile);
var client = new HttpClient();
client.Timeout = TimeSpan.FromMinutes(10);
var url = "http://192.168.178.214:11434/api/generate";
var body = new
{
model = "qwen3-vl:8b-instruct",
prompt = $@"Extract order data from the document below and return it as JSON.
CRITICAL RULES — read before extracting:
- ONLY use values that are explicitly written in the document
- If a field is not present in the document, use """" for strings or 0 for numbers
- NEVER invent, guess, or infer values that are not clearly stated
- NEVER generate product codes, reference numbers, or relation codes unless they appear word-for-word in the document
- Referentie = PO Number if present, otherwise """"
- Relatiecode = customer/relation code if explicitly labeled as such, otherwise """"
- productcode = numeric code explicitly labeled as a product/article code, otherwise 0
- ONLY return the json data, no other explenations or other text than the json
Output ONLY this JSON structure:
[{{
""order"": {{
""Naam"": ""<customer name from document or empty>"",
""Datum"": ""<date in yyyy-MM-dd or empty>"",
""Relatiecode"": ""<relation code if explicitly present, else empty>"",
""Referentie"": ""<PO number if explicitly present, else empty>"",
""regels"": [
{{
""productcode"": <integer code if explicitly present, else 0>,
""omschrijving"": ""<product description>"",
""aantal"": <quantity as integer>,
""prijs"": <unit price as decimal>
}}
]
}}
}}]
Document:
{text}
",
system = @"You are a strict document-to-JSON data extractor for order processing.
Your job is EXTRACTION ONLY — not inference, not completion, not enrichment.
You ONLY output valid JSON matching the exact schema provided.
You NEVER output explanations, markdown, code blocks, or comments.
You NEVER add fields not in the schema.
Anti-hallucination rules:
- If you are not 100% certain a value appears in the document, leave it empty or 0
- Product codes, reference numbers, and relation codes are NEVER made up — they must appear verbatim in the document
- Do not infer a product code from a product name or description
- Do not infer a relation code from a customer name
- Do not infer a reference from an invoice number unless explicitly labeled as PO or reference
- When in doubt, use empty string or 0
Data type rules:
- Datum must be yyyy-MM-dd format
- productcode and aantal must be integers
- prijs must be a decimal number
- All string values use double quotes
- No trailing commas
Your entire response must be JSON and nothing else.",
keep_alive = "24h",
stream = false
};
var json = JsonSerializer.Serialize(body);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(url, content);
var result = await response.Content.ReadAsStringAsync();
var parsed = JsonSerializer.Deserialize<OllamaResponse>(result);
Console.WriteLine(parsed?.Response);
record OllamaResponse([property: JsonPropertyName("response")] string Response);
@@ -0,0 +1,23 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{435B0DD4-35E4-8754-9856-0EBD55269029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{435B0DD4-35E4-8754-9856-0EBD55269029}.Debug|Any CPU.Build.0 = Debug|Any CPU
{435B0DD4-35E4-8754-9856-0EBD55269029}.Release|Any CPU.ActiveCfg = Release|Any CPU
{435B0DD4-35E4-8754-9856-0EBD55269029}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CFD9A1B3-04B0-4CB5-9F51-6DCD267FC919}
EndGlobalSection
EndGlobal
@@ -0,0 +1,305 @@
using Microsoft.AspNetCore.Mvc;
using System.Text;
using System.Text.Json;
using ExcelDataReader;
using System.Data;
using UglyToad.PdfPig;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
namespace FileConverterAPI.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class UploadController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> UploadFile(IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest("No file uploaded");
string text;
try
{
var ext = Path.GetExtension(file.FileName).ToLower();
text = ext switch
{
".xls" or ".xlsx" => ReadExcel(file),
".pdf" => ReadPdf(file),
".docx" => ReadWord(file),
_ => throw new Exception("Unsupported file type")
};
}
catch (Exception ex)
{
return BadRequest("FILE ERROR: " + ex.Message);
}
// stuurt het hele document zodat de ai alles pakt
var aiResponse = await SendToOllama(text);
var json = ExtractJson(aiResponse);
var finalResult = ParseJson(json);
var prettyJson = JsonSerializer.Serialize(finalResult, new JsonSerializerOptions
{
WriteIndented = true
});
return File(
Encoding.UTF8.GetBytes(prettyJson),
"application/json",
"converted.json"
);
}
// ---------------- OLLAMA ----------------
private async Task<string> SendToOllama(string text)
{
using var client = new HttpClient();
client.Timeout = TimeSpan.FromMinutes(10);
var body = new
{
model = "qwen2.5-coder:3b",
//model = "qwen3-vl:8b-instruct",
messages = new[]
{
new
{
role = "system",
content = $@"Je bent een extreem strikte document-naar-JSON extractor.
Jouw ENIGE taak is het extraheren van gegevens uit een document naar EXACT de toegestane JSON structuur.
ABSOLUTE REGELS:
- Retourneer UITSLUITEND geldige JSON
- Retourneer EXACT de opgegeven structuur
- ALLE andere structuren zijn VERBODEN
- ALLE velden buiten de toegestane structuur zijn VERBODEN
- GEEN extra velden
- GEEN extra objecten
- GEEN extra arrays
- GEEN uitleg
- GEEN markdown
- GEEN comments
- GEEN tekst buiten de JSON
- GEEN Engelse veldnamen
- ALLES moet Nederlands blijven
VERBODEN VELDEN:
- totalAmount
- currency
- invoiceDate
- dueDate
- vatRate
- vatExemptionReason
- customer
- supplier
- paymentTerms
- items
- discounts
- tax
- grossAmount
- netAmount
- deliveryNotes
- bankDetails
- contactInformation
- itemNumber
- quantity
- totalPrice
- unitPrice
- invoiceNumber
- subtotal
- btw
- leverancier
- klant
- adres
- iban
- bic
ALLEEN DEZE VELDEN ZIJN TOEGESTAAN:
- order
- Naam
- Datum
- Relatiecode
- Referentie
- regels
- productcode
- omschrijving
- aantal
- prijs
EXTRACTIE REGELS:
- Gebruik ALLEEN waarden die letterlijk in het document staan
- NOOIT gokken
- NOOIT afleiden
- NOOIT aanvullen
- NOOIT hernoemen van velden
- Indien een waarde ontbreekt:
- gebruik """" voor strings
- gebruik 0 voor integers
- gebruik 0.00 voor decimalen
- productcode mag ALLEEN gevuld worden indien expliciet vermeld als productcode, artikelcode of numerieke code
- Relatiecode mag ALLEEN gevuld worden indien expliciet gelabeld
- Referentie mag ALLEEN gevuld worden indien expliciet gelabeld als referentie of PO-nummer
- Datum MOET yyyy-MM-dd formaat gebruiken
DE OUTPUT MOET EXACT DIT FORMAAT HEBBEN:
[
{{
""order"": {{
""Naam"": """",
""Datum"": """",
""Relatiecode"": """",
""Referentie"": """",
""regels"": [
{{
""productcode"": 0,
""omschrijving"": """",
""aantal"": 0,
""prijs"": 0.00
}}
]
}}
}}
]
IEDERE OUTPUT DIE NIET EXACT DEZE STRUCTUUR GEBRUIKT IS ONGELDIG.
"
},
new
{
role = "user",
content = text
}
},
keep_alive = "24h",
stream = false
};
var json = JsonSerializer.Serialize(body);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync("http://localhost:11434/api/chat", content);
//var response = await client.PostAsync("http://192.168.178.214:11434/api/chat", content);
var raw = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(raw);
if (!doc.RootElement.TryGetProperty("message", out var message))
return "";
if (!message.TryGetProperty("content", out var contentText))
return "";
return contentText.GetString() ?? "";
}
// ---------------- PARSE JSON RESPONSE ----------------
private Dictionary<string, object> ParseJson(string json)
{
var result = new Dictionary<string, object>();
try
{
if (string.IsNullOrWhiteSpace(json))
return result;
using var doc = JsonDocument.Parse(json);
foreach (var prop in doc.RootElement.EnumerateObject())
{
result[prop.Name] = prop.Value.GetRawText();
}
}
catch
{
// als het faalt geef niks terug
}
return result;
}
// ---------------- JSON ----------------
private string ExtractJson(string input)
{
if (string.IsNullOrWhiteSpace(input))
return "";
int start = input.IndexOf('{');
int end = input.LastIndexOf('}');
if (start == -1 || end == -1 || end <= start)
return "";
return input.Substring(start, end - start + 1);
}
// ---------------- EXCEL ----------------
private string ReadExcel(IFormFile file)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
using var stream = new MemoryStream();
file.CopyTo(stream);
stream.Position = 0;
using var reader = ExcelReaderFactory.CreateReader(stream);
var ds = reader.AsDataSet();
var sb = new StringBuilder();
foreach (DataTable table in ds.Tables)
{
foreach (DataRow row in table.Rows)
{
foreach (var cell in row.ItemArray)
sb.Append(cell?.ToString() + " ");
sb.AppendLine();
}
}
return sb.ToString();
}
// ---------------- PDF ----------------
private string ReadPdf(IFormFile file)
{
var sb = new StringBuilder();
using var stream = new MemoryStream();
file.CopyTo(stream);
stream.Position = 0;
using var doc = PdfDocument.Open(stream);
foreach (var page in doc.GetPages())
sb.AppendLine(page.Text);
return sb.ToString();
}
// ---------------- WORD ----------------
private string ReadWord(IFormFile file)
{
var sb = new StringBuilder();
using var stream = new MemoryStream();
file.CopyTo(stream);
stream.Position = 0;
using var doc = WordprocessingDocument.Open(stream, false);
var body = doc.MainDocumentPart.Document.Body;
foreach (var t in body.Descendants<Text>())
sb.Append(t.Text + " ");
return sb.ToString();
}
}
}
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
<PackageReference Include="EPPlus" Version="8.5.1" />
<PackageReference Include="ExcelDataReader" Version="3.8.0" />
<PackageReference Include="ExcelDataReader.DataSet" Version="3.8.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageReference Include="UglyToad.PdfPig" Version="1.7.0-custom-5" />
</ItemGroup>
</Project>
@@ -0,0 +1,6 @@
@FileConverterAPI_HostAddress = http://localhost:5037
GET {{FileConverterAPI_HostAddress}}/weatherforecast/
Accept: application/json
###
@@ -0,0 +1,20 @@
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// CORS (nodig voor frontend)
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll",
policy => policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod());
});
var app = builder.Build();
app.UseCors("AllowAll");
app.MapControllers();
app.Run();
@@ -0,0 +1,153 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>converter</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<nav class="navbar">
<h1>Converter</h1>
</nav>
<!-- Upload Box -->
<div id="dropZone" class="box upload-box">
<div class="upload-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" role="presentation">
<path d="M12 15V5" />
<path d="M8 9L12 5L16 9" />
<path d="M5 13V17C5 18.1 5.9 19 7 19H17C18.1 19 19 18.1 19 17V13" />
</svg>
</div>
<h2>Sleep je bestand hierheen</h2>
<p>of klik om te bladeren</p>
<div class="file-types">
<!-- ✅ FIX: added accept -->
<input id="filePicker" type="file" accept=".xlsx,.xls,.pdf,.docx" hidden />
<button id="openFilePicker" class="tag excel" type="button">
Bestand zoeken
</button>
</div>
</div>
<div class="middle-arrow" aria-hidden="true"></div>
<!-- Result Box -->
<div class="box2 result-box">
<div class="file-icon" aria-hidden="true"></div>
<p id="fileName" class="file-name">bestandsnaam.json</p>
<p id="downloadStatus" class="download-status" aria-live="polite"></p>
<div class="file-separator" aria-hidden="true"></div>
<button id="downloadBtn" class="download-button" type="button">
<svg
class="download-icon"
viewBox="0 0 24 24"
role="presentation"
aria-hidden="true"
>
<path d="M12 15V6" />
<path d="M8.5 11.5L12 15L15.5 11.5" />
<path d="M6 17.5H18" />
</svg>
<span>Download JSON</span>
</button>
</div>
<script>
const openFilePickerButton = document.getElementById("openFilePicker");
const filePickerInput = document.getElementById("filePicker");
const downloadBtn = document.getElementById("downloadBtn");
const dropZone = document.getElementById("dropZone");
const fileNameText = document.getElementById("fileName");
const downloadStatusText = document.getElementById("downloadStatus");
const downloadBtnLabel = downloadBtn.querySelector("span");
let selectedFile = null;
openFilePickerButton.addEventListener("click", () => {
filePickerInput.click();
});
filePickerInput.addEventListener("change", () => {
selectedFile = filePickerInput.files[0];
if (selectedFile) {
fileNameText.textContent = selectedFile.name;
}
});
dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
});
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
selectedFile = e.dataTransfer.files[0];
if (selectedFile) {
fileNameText.textContent = selectedFile.name;
}
});
downloadBtn.addEventListener("click", async () => {
if (!selectedFile) {
alert("Select a file first!");
return;
}
const formData = new FormData();
formData.append("file", selectedFile);
downloadBtn.disabled = true;
downloadBtnLabel.textContent = "Downloaden...";
downloadStatusText.textContent = "Bezig met downloaden...";
try {
const response = await fetch("http://localhost:5037/api/upload", {
method: "POST",
body: formData,
});
// ✅ FIX: show real error
if (!response.ok) {
const errorText = await response.text();
console.error("Backend error:", errorText);
downloadStatusText.textContent = "Download mislukt.";
alert("Upload failed: " + errorText);
return;
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "converted.json";
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
downloadStatusText.textContent = "Download voltooid.";
} catch (error) {
console.error(error);
downloadStatusText.textContent =
"Er ging iets fout tijdens downloaden.";
alert("Error uploading file");
} finally {
downloadBtn.disabled = false;
downloadBtnLabel.textContent = "Download JSON";
}
});
</script>
</body>
</html>
@@ -0,0 +1,228 @@
html,
body {
background-color: rgb(220, 220, 220);
margin: 0;
height: 100%;
min-height: 100vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
gap: 110px;
padding-top: 80px;
font-family: "Segoe UI", Tahoma, sans-serif;
}
.navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 64px;
background-color: rgb(0, 0, 0);
display: flex;
align-items: center;
padding: 0 24px;
box-sizing: border-box;
color: white;
z-index: 1000;
}
.navbar h1 {
margin: 0;
font-size: 24px;
}
.box {
width: 350px;
background-color: rgb(243, 243, 243);
height: 350px;
border: 2px dashed rgb(187, 187, 187);
box-sizing: border-box;
border-radius: 6px;
margin-bottom: 3cm;
}
.box2 {
width: 350px;
background-color: rgb(243, 243, 243);
height: 350px;
border: 2px dashed rgb(187, 187, 187);
box-sizing: border-box;
border-radius: 6px;
margin-bottom: 3cm;
}
.upload-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
.upload-icon {
width: 64px;
height: 64px;
border: 1px solid rgb(174, 212, 226);
border-radius: 5px;
background-color: rgb(221, 236, 244);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14px;
}
.upload-icon svg {
width: 34px;
height: 34px;
}
.upload-icon path {
fill: none;
stroke: rgb(41, 159, 196);
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.upload-box h2 {
margin: 1cm;
color: rgb(40, 40, 40);
font-size: 18px;
font-weight: 7 00;
line-height: 1.25;
}
.upload-box p {
margin: 16px 0 18px;
color: rgb(123, 142, 154);
font-size: 18px;
}
.file-types {
display: flex;
gap: 8px;
}
.tag {
color: white;
font-size: 20px;
font-weight: 700;
line-height: 1;
padding: 7px 13px;
border-radius: 2px;
border: 0;
cursor: pointer;
}
.tag.excel {
background-color: rgb(87, 169, 50);
}
.tag.word {
background-color: rgb(44, 166, 209);
}
.tag:hover {
filter: brightness(0.95);
}
.result-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 58px 34px 30px;
}
.file-icon {
width: 66px;
height: 58px;
border-radius: 4px;
background: linear-gradient(
145deg,
rgb(239, 239, 239),
rgb(229, 229, 229)
);
opacity: 0.7;
}
.file-name {
margin: 18px 0 16px;
color: rgb(0, 0, 0);
font-size: 24px;
font-weight: 700;
}
.download-status {
min-height: 24px;
margin: 0 0 10px;
color: rgb(90, 90, 90);
font-size: 16px;
font-weight: 600;
}
.file-separator {
width: 18px;
background-color: rgb(213, 213, 213);
margin-bottom: 48px;
}
.download-button {
width: 100%;
height: 72px;
border: 0;
border-radius: 4px;
background-color: rgb(180, 180, 180);
color: rgb(250, 250, 250);
font-size: 20px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
cursor: pointer;
}
.download-button:hover {
background-color: rgb(168, 168, 168);
}
.download-button:disabled {
background-color: rgb(145, 145, 145);
cursor: wait;
}
.download-icon {
width: 30px;
height: 30px;
}
.download-icon path {
fill: none;
stroke: rgb(250, 250, 250);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.middle-arrow {
position: relative;
width: 56px;
height: 3px;
background-color: rgb(183, 183, 183);
margin-bottom: 3cm;
}
.middle-arrow::after {
content: "";
position: absolute;
right: -9px;
top: 50%;
transform: translateY(-50%);
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
border-left: 9px solid rgb(183, 183, 183);
}
@@ -0,0 +1 @@
# Einstein Academy externe challenge
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,161 @@
.container {
flex: 100%;
}
html {
scroll-behavior: smooth;
}
.nav-pills .nav-link.blue {
color: #142982;
}
.nav-pills .nav-link.blue:hover {
color: #ABB4D6;
}
.slider-text {
color: #000000;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
margin-left: 10px;
margin-right: 10px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ABB4D6;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #00F064;
}
input:focus + .slider {
box-shadow: 0 0 1px #ABB4D6;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.home {
max-width: 700px;
margin: 0 auto;
padding: 3rem 2rem;
text-align: left;
}
.home p {
font-size: 16px;
line-height: 1.8;
color: #1B2B6B;
margin-bottom: 1.25rem;
}
.home p:first-child {
font-size: 20px;
font-weight: 700;
color: #1B2B6B;
line-height: 1.5;
}
.home p:last-child {
margin-bottom: 0;
font-size: 15px;
color: #4a5568;
}
.about-page p {
color: #0f172a;
}
.about-page p:first-child {
color: #09142f;
}
.about-page p:last-child {
color: #1e293b;
}
hr.thin-line {
border: none;
border-top: 1px solid #dcdcdc;
margin: 0.5rem auto;
max-width: 600px;
}
/* Page roll-up banners (fixed at left and right side of viewport) */
.page-banner-left,
.page-banner-right {
position: fixed;
top: 120px; /* sits below the header area */
width: 140px;
height: auto;
min-height: 420px;
z-index: 1000;
pointer-events: none;
overflow: hidden;
}
.page-banner-left { left: 8px; }
.page-banner-right { right: 8px; }
.page-banner-left object,
.page-banner-right object,
.page-banner-left img,
.page-banner-right img,
.page-banner-left canvas,
.page-banner-right canvas {
width: 100%;
height: auto;
display: block;
border: none;
}
/* Hide banners on small screens */
@media (max-width: 1024px) {
.page-banner-left,
.page-banner-right { display: none; }
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einstein Academy | Contact</title>
<link rel="icon" type="image/x-icon" href="../assets/favicon.ico">
<link rel="stylesheet" href="../css/style.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<div class="container">
<header
class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom"
>
<a
href="../index.html"
class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none"
>
<img src=../assets/EinsteinAcademy_Logo_Blauw_Transparant.png width="40" height="32" aria-hidden="true">
<span class="fs-4">Einstein Academy</span>
</a>
<ul class="nav nav-pills">
<li class="nav-item"><a href="../index.html" class="nav-link blue">Home</a></li>
<li class="nav-item"><a href="./projects.html" class="nav-link blue">Projects</a></li>
<li class="nav-item"><a href="./about.html" class="nav-link blue">About</a></li>
<li class="nav-item"><a href="./contact.html" class="nav-link blue">Contact</a></li>
</ul>
<div class="nav slider-text">
<h5>Nederlands</h5>
<label class="switch">
<input type="checkbox" id="langToggle">
<span class="slider round"></span>
</label>
<h5>English</h5>
</div>
</header>
</div>
<div class="page-banner-left" data-pdf="../docs/EinsteinAcademy_Roll-up-Banner_1_100x200cm.pdf">
<img src="../assets/EinsteinAcademy_Logo_Blauw_Transparant.png" alt="Einstein Academy banner left">
</div>
<div class="page-banner-right" data-pdf="../docs/EinsteinAcademy_Roll-up-Banner_2_100x200cm.pdf">
<img src="../assets/EinsteinAcademy_Logo_Blauw_Transparant.png" alt="Einstein Academy banner right">
</div>
<section class="home about-page" id="Home">
<div class="filler-block" style="text-align:center; margin-top:1rem;">
<hr class="thin-line">
<p data-i18n="aboutDescription1"></p>
<p data-i18n="aboutDescription2"></p>
<p data-i18n="aboutDescription3"></p>
<p data-i18n="aboutDescription4"></p>
<p class="mb-0" data-i18n="aboutDescription5"></p>
<hr class="thin-line">
</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="../js/script.js"></script>
</body>
<footer>
<div class="container">
<footer class="py-3 my-4">
<ul class="nav justify-content-center border-bottom pb-3 mb-3">
<li class="nav-item">
<a href="../index.html" class="nav-link px-2 text-body-secondary">Home</a>
</li>
<li class="nav-item">
<a href="./projects.html" class="nav-link px-2 text-body-secondary">Projects</a>
</li>
<li class="nav-item">
<a href="./about.html" class="nav-link px-2 text-body-secondary">About</a>
</li>
<li class="nav-item">
<a href="./contact.html" class="nav-link px-2 text-body-secondary">Contact</a>
</li>
</ul>
</footer>
</div>
</footer>
</html>
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einstein Academy | Contact</title>
<link rel="icon" type="image/x-icon" href="../assets/favicon.ico">
<link rel="stylesheet" href="../css/style.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<div class="container">
<header
class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom"
>
<a
href="../index.html"
class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none"
>
<img src=../assets/EinsteinAcademy_Logo_Blauw_Transparant.png width="40" height="32" aria-hidden="true">
<span class="fs-4">Einstein Academy</span>
</a>
<ul class="nav nav-pills">
<li class="nav-item"><a href="../index.html" class="nav-link blue">Home</a></li>
<li class="nav-item"><a href="./projects.html" class="nav-link blue">Projects</a></li>
<li class="nav-item"><a href="./about.html" class="nav-link blue">About</a></li>
<li class="nav-item"><a href="./contact.html" class="nav-link blue">Contact</a></li>
</ul>
<div class="nav slider-text">
<h5>Nederlands</h5>
<label class="switch">
<input type="checkbox" id="langToggle">
<span class="slider round"></span>
</label>
<h5>English</h5>
</div>
</header>
</div>
<div class="page-banner-left" data-pdf="../docs/EinsteinAcademy_Roll-up-Banner_1_100x200cm.pdf">
<img src="../assets/EinsteinAcademy_Logo_Blauw_Transparant.png" alt="Einstein Academy banner left">
</div>
<div class="page-banner-right" data-pdf="../docs/EinsteinAcademy_Roll-up-Banner_2_100x200cm.pdf">
<img src="../assets/EinsteinAcademy_Logo_Blauw_Transparant.png" alt="Einstein Academy banner right">
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="../js/script.js"></script>
</body>
<footer>
<div class="container">
<footer class="py-3 my-4">
<ul class="nav justify-content-center border-bottom pb-3 mb-3">
<li class="nav-item">
<a href="../index.html" class="nav-link px-2 text-body-secondary">Home</a>
</li>
<li class="nav-item">
<a href="./projects.html" class="nav-link px-2 text-body-secondary">Projects</a>
</li>
<li class="nav-item">
<a href="./about.html" class="nav-link px-2 text-body-secondary">About</a>
</li>
<li class="nav-item">
<a href="./contact.html" class="nav-link px-2 text-body-secondary">Contact</a>
</li>
</ul>
</footer>
</div>
</footer>
</html>
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einstein Academy | Contact</title>
<link rel="icon" type="image/x-icon" href="../assets/favicon.ico">
<link rel="stylesheet" href="../css/style.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<div class="container">
<header
class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom"
>
<a
href="../index.html"
class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none"
>
<img src=../assets/EinsteinAcademy_Logo_Blauw_Transparant.png width="40" height="32" aria-hidden="true">
<span class="fs-4">Einstein Academy</span>
</a>
<ul class="nav nav-pills">
<li class="nav-item"><a href="../index.html" class="nav-link blue">Home</a></li>
<li class="nav-item"><a href="./projects.html" class="nav-link blue">Projects</a></li>
<li class="nav-item"><a href="./about.html" class="nav-link blue">About</a></li>
<li class="nav-item"><a href="./contact.html" class="nav-link blue">Contact</a></li>
</ul>
<div class="nav slider-text">
<h5>Nederlands</h5>
<label class="switch">
<input type="checkbox" id="langToggle">
<span class="slider round"></span>
</label>
<h5>English</h5>
</div>
</header>
</div>
<div class="page-banner-left" data-pdf="../docs/EinsteinAcademy_Roll-up-Banner_1_100x200cm.pdf">
<img src="../assets/EinsteinAcademy_Logo_Blauw_Transparant.png" alt="Einstein Academy banner left">
</div>
<div class="page-banner-right" data-pdf="../docs/EinsteinAcademy_Roll-up-Banner_3_100x200cm.pdf">
<img src="../assets/EinsteinAcademy_Logo_Blauw_Transparant.png" alt="Einstein Academy banner right">
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="../js/script.js"></script>
</body>
<footer>
<div class="container">
<footer class="py-3 my-4">
<ul class="nav justify-content-center border-bottom pb-3 mb-3">
<li class="nav-item">
<a href="../index.html" class="nav-link px-2 text-body-secondary">Home</a>
</li>
<li class="nav-item">
<a href="./projects.html" class="nav-link px-2 text-body-secondary">Projects</a>
</li>
<li class="nav-item">
<a href="./about.html" class="nav-link px-2 text-body-secondary">About</a>
</li>
<li class="nav-item">
<a href="./contact.html" class="nav-link px-2 text-body-secondary">Contact</a>
</li>
</ul>
</footer>
</div>
</footer>
</html>
@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einstein Academy homepage</title>
<link rel="icon" type="image/x-icon" href="./assets/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="container">
<header
class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom"
>
<a
href="#"
class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none"
>
<img src=./assets/EinsteinAcademy_Logo_Blauw_Transparant.png width="40" height="32" aria-hidden="true">
<span class="fs-4">Einstein Academy</span>
</a>
<ul class="nav nav-pills">
<li class="nav-item"><a href="#" class="nav-link blue">Home</a></li>
<li class="nav-item"><a href="./html/projects.html" class="nav-link blue">Projects</a></li>
<li class="nav-item"><a href="./html/about.html" class="nav-link blue">About</a></li>
<li class="nav-item"><a href="./html/contact.html" class="nav-link blue">Contact</a></li>
</ul>
<div class="nav slider-text">
<h5>Nederlands</h5>
<label class="switch">
<input type="checkbox" id="langToggle">
<span class="slider round"></span>
</label>
<h5>English</h5>
</div>
</header>
</div>
<div class="page-banner-left" data-pdf="./docs/EinsteinAcademy_Roll-up-Banner_1_100x200cm.pdf">
<img src="./assets/EinsteinAcademy_Logo_Blauw_Transparant.png" alt="Einstein Academy banner left">
</div>
<div class="page-banner-right" data-pdf="./docs/EinsteinAcademy_Roll-up-Banner_2_100x200cm.pdf">
<img src="./assets/EinsteinAcademy_Logo_Blauw_Transparant.png" alt="Einstein Academy banner right">
</div>
<section class="home" id="Home">
<p data-i18n="description1"></p>
<p data-i18n="description2"></p>
<p data-i18n="description3"></p>
<p data-i18n="description4"></p>
<p data-i18n="description5"></p>
<div class="filler-block" style="text-align:center; margin-top:1rem;">
<hr class="thin-line">
<div id="measureText" style="visibility:hidden; width:80%; margin:0 auto;">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.
</div>
<div class="video-wrapper">
<video id="fillerVideo" src="./assets/Aftermovie Startschot Einstein Academy (1).mp4" controls playsinline></video>
</div>
<hr class="thin-line">
<script>
document.addEventListener('DOMContentLoaded', function(){
var measure = document.getElementById('measureText');
var vid = document.getElementById('fillerVideo');
if (measure && vid) {
var height = measure.offsetHeight;
var factor = 8.0; // increase video height to ~800%
var newHeight = Math.round(height * factor);
vid.style.height = newHeight + 'px';
vid.style.maxWidth = '100%';
vid.style.display = 'block';
vid.style.margin = '0 auto';
}
});
</script>
</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="./js/script.js"></script>
</body>
<footer>
<div class="container">
<footer class="py-3 my-4">
<ul class="nav justify-content-center border-bottom pb-3 mb-3">
<li class="nav-item">
<a href="#" class="nav-link px-2 text-body-secondary">Home</a>
</li>
<li class="nav-item">
<a href="./html/projects.html" class="nav-link px-2 text-body-secondary">Projects</a>
</li>
<li class="nav-item">
<a href="./html/about.html" class="nav-link px-2 text-body-secondary">About</a>
</li>
<li class="nav-item">
<a href="./html/contact.html" class="nav-link px-2 text-body-secondary">Contact</a>
</li>
</ul>
</footer>
</div>
</footer>
</html>
@@ -0,0 +1,115 @@
let translations = {};
let currentLang = "nl";
function getLanguagePath(lang) {
const isInsideHtmlFolder = window.location.pathname.replace(/\\/g, "/").includes("/html/");
const prefix = isInsideHtmlFolder ? "../" : "./";
return `${prefix}lang/${lang}.json`;
}
async function loadLanguage(lang) {
try {
const res = await fetch(getLanguagePath(lang));
translations = await res.json();
applyTranslations();
currentLang = lang;
localStorage.setItem("lang", lang);
} catch (error) {
console.error("Failed to load language file:", error);
}
}
function applyTranslations() {
document.querySelectorAll("[data-i18n]").forEach(el => {
const key = el.dataset.i18n;
if (translations[key]) {
el.textContent = translations[key];
}
});
}
function initLanguageToggle() {
const toggle = document.getElementById("langToggle");
if (!toggle) return;
toggle.checked = (localStorage.getItem("lang") || "nl") === "en";
toggle.addEventListener("change", () => {
const lang = toggle.checked ? "en" : "nl";
loadLanguage(lang);
});
}
// init on page load
const savedLang = localStorage.getItem("lang") || "nl";
loadLanguage(savedLang);
// Call toggle init when DOM is ready
document.addEventListener("DOMContentLoaded", initLanguageToggle);
// PDF Rendering Logic
async function initPDFRendering() {
const pdfElements = document.querySelectorAll("[data-pdf]");
if (pdfElements.length === 0) return;
// Load PDF.js if not already loaded
if (typeof pdfjsLib === 'undefined') {
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
}
pdfElements.forEach(async (container) => {
const url = container.getAttribute('data-pdf');
if (!url) return;
try {
const loadingTask = pdfjsLib.getDocument(url);
const pdf = await loadingTask.promise;
const page = await pdf.getPage(1);
// Calculate scale to fit the container width
const containerWidth = container.clientWidth || 140;
const unscaledViewport = page.getViewport({ scale: 1 });
const scale = containerWidth / unscaledViewport.width;
const viewport = page.getViewport({ scale: scale * 2 }); // Use 2x for better quality
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.width = '100%';
canvas.style.height = 'auto';
canvas.style.display = 'block';
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
// Clear container and add canvas
container.innerHTML = '';
container.appendChild(canvas);
} catch (error) {
console.error("Error rendering PDF:", url, error);
}
});
}
document.addEventListener("DOMContentLoaded", initPDFRendering);
// Enable transitions after a short delay to avoid initial animation
setTimeout(() => {
const slider = document.querySelector(".slider");
if (slider) {
slider.classList.add("slider-transition");
}
}, 100);
@@ -0,0 +1,12 @@
{
"description1": "The Einstein Academy is a cross-border collaboration program in which educational institutions, research organizations, and businesses work together on the technologies of tomorrow. Its main objective is attracting, developing, and deploying talent for the construction, maintenance, and operation of the Einstein Telescope.",
"description2": "The Einstein Academy connects education, research, and practice in a unique learning environment where students, professionals, and companies work together on realistic technological challenges. Innovation, craftsmanship, and cross-border cooperation are central to this mission.",
"description3": "The Einstein Academy is an initiative of Zuyd University of Applied Sciences, in collaboration with core partners Vista College and Gilde Opleidingen, supplemented by various Euregional educational and knowledge partners. The program is made possible in part by the Province of Limburg and the Ministry of Education, Culture and Science.",
"description4": "The Einstein Academy anticipates the potential arrival of the Einstein Telescope: a world-leading observatory for gravitational waves planned deep underground in the Euregio Meuse-Rhine.",
"description5": "With practice-oriented engineering education, innovative learning paths, and collaboration between vocational (mbo), higher professional (hbo), and university (wo) education alongside the business community, the Einstein Academy offers education centered around the Einstein Telescope that transcends borders, levels, and target groups.",
"aboutDescription1": "The Einstein Academy is a cross-border collaboration program in which educational institutions, research organizations, and businesses work together on the technologies of tomorrow. Its main objective is attracting, developing, and deploying talent for the construction, maintenance, and operation of the Einstein Telescope.",
"aboutDescription2": "The Einstein Academy connects education, research, and practice in a unique learning environment where students, professionals, and companies work together on realistic technological challenges. Innovation, craftsmanship, and cross-border cooperation are central to this mission.",
"aboutDescription3": "The Einstein Academy is an initiative of Zuyd University of Applied Sciences, in collaboration with core partners Vista College and Gilde Opleidingen, supplemented by various Euregional educational and knowledge partners. The program is made possible in part by the Province of Limburg and the Ministry of Education, Culture and Science.",
"aboutDescription4": "The Einstein Academy anticipates the potential arrival of the Einstein Telescope: a world-leading observatory for gravitational waves planned deep underground in the Euregio Meuse-Rhine.",
"aboutDescription5": "With practice-oriented engineering education, innovative learning paths, and collaboration between vocational (mbo), higher professional (hbo), and university (wo) education alongside the business community, the Einstein Academy offers education centered around the Einstein Telescope that transcends borders, levels, and target groups."
}
@@ -0,0 +1,12 @@
{
"description1": "De Einstein Academy is een grensoverstijgend samenwerkingsprogramma waarin onderwijsinstellingen, onderzoeksorganisaties en bedrijven samen werken aan de technologieën van morgen. Met als hoofddoel het aantrekken, ontwikkelen en inzetten van talent voor de bouw, het onderhoud en de werking van de Einstein Telescope.",
"description2": "De Einstein Academy verbindt onderwijs, onderzoek en praktijk in een unieke leeromgeving waar studenten, professionals en bedrijven samen werken aan realistische technologische vraagstukken. Innovatie, vakmanschap en samenwerking over landsgrenzen heen staan hierbij centraal. ",
"description3": "De Einstein Academy is een initiatief van Zuyd Hogeschool, in samenwerking met kernpartners Vista College en Gilde Opleidingen, aangevuld met diverse Euregionale onderwijs- en kennispartners. Het programma wordt mede mogelijk gemaakt door de Provincie Limburg en het ministerie van Onderwijs, Cultuur en Wetenschap. ",
"description4": "De Einstein Academy speelt in op de mogelijke komst van de Einstein Telescope: een wereldwijd toonaangevend observatorium voor zwaartekrachtgolven dat diep onder de grond in de Euregio Maas-Rijn gepland wordt. ",
"description5": "Met praktijkgericht techniekonderwijs, innovatieve leertrajecten en samenwerking tussen mbo, hbo, wo en het bedrijfsleven biedt de Einstein Academy grens-, niveau- en doelgroepoverstijgend onderwijs rondom de Einstein Telescope.",
"aboutDescription1": "De Einstein Academy is een grensoverstijgend samenwerkingsprogramma waarin onderwijsinstellingen, onderzoeksorganisaties en bedrijven samen werken aan de technologieën van morgen. Met als hoofddoel het aantrekken, ontwikkelen en inzetten van talent voor de bouw, het onderhoud en de werking van de Einstein Telescope.",
"aboutDescription2": "De Einstein Academy verbindt onderwijs, onderzoek en praktijk in een unieke leeromgeving waar studenten, professionals en bedrijven samen werken aan realistische technologische vraagstukken. Innovatie, vakmanschap en samenwerking over landsgrenzen heen staan hierbij centraal.",
"aboutDescription3": "De Einstein Academy is een initiatief van Zuyd Hogeschool, in samenwerking met kernpartners Vista College en Gilde Opleidingen, aangevuld met diverse Euregionale onderwijs- en kennispartners. Het programma wordt mede mogelijk gemaakt door de Provincie Limburg en het ministerie van Onderwijs, Cultuur en Wetenschap.",
"aboutDescription4": "De Einstein Academy speelt in op de mogelijke komst van de Einstein Telescope: een wereldwijd toonaangevend observatorium voor zwaartekrachtgolven dat diep onder de grond in de Euregio Maas-Rijn gepland wordt.",
"aboutDescription5": "Met praktijkgericht techniekonderwijs, innovatieve leertrajecten en samenwerking tussen mbo, hbo, wo en het bedrijfsleven biedt de Einstein Academy grens-, niveau- en doelgroepoverstijgend onderwijs rondom de Einstein Telescope."
}