f
@@ -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)
|
||||
|
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>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
|
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)
|
||||
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 364 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
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
|
||||
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
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; }
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||