Initial code commit
@@ -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)
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "concordancer-web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"bootstrap": "^5.2.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.0.0",
|
||||
"react-query": "^3.39.2",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.56.1"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<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>TP | Konkordančnik</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,900;1,100;1,400&display=swap" rel="stylesheet">
|
||||
</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,28 @@
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider
|
||||
} from 'react-query'
|
||||
import ConcordanceIndex from './pages/concordance/ConcordanceIndex';
|
||||
import ConcordanceResults from './pages/concordance/ConcordanceResults';
|
||||
import ListIndex from './pages/list/ListIndex';
|
||||
import ListResults from './pages/list/ListResults';
|
||||
import NoCorpus from './pages/NoCorpus';
|
||||
|
||||
function App() {
|
||||
const queryClient = new QueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Routes>
|
||||
<Route path='/:corpusId/concordance' element={<ConcordanceIndex />} />
|
||||
<Route path='/:corpusId/concordance/search' element={<ConcordanceResults />} />
|
||||
<Route path='/:corpusId/list' element={<ListIndex />} />
|
||||
<Route path='/:corpusId/list/search' element={<ListResults />} />
|
||||
<Route path='/:corpusId/*' element={<ConcordanceIndex />} />
|
||||
<Route path='*' element={<NoCorpus />} />
|
||||
</Routes>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.292893 0.292893C0.683417 -0.0976311 1.31658 -0.0976311 1.70711 0.292893L7 5.58579L12.2929 0.292893C12.6834 -0.0976311 13.3166 -0.0976311 13.7071 0.292893C14.0976 0.683417 14.0976 1.31658 13.7071 1.70711L7.70711 7.70711C7.31658 8.09763 6.68342 8.09763 6.29289 7.70711L0.292893 1.70711C-0.0976311 1.31658 -0.0976311 0.683417 0.292893 0.292893Z" fill="#006cb7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 513 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.29289 0.292893C6.68342 -0.0976311 7.31658 -0.0976311 7.70711 0.292893L13.7071 6.29289C14.0976 6.68342 14.0976 7.31658 13.7071 7.70711C13.3166 8.09763 12.6834 8.09763 12.2929 7.70711L7 2.41421L1.70711 7.70711C1.31658 8.09763 0.683417 8.09763 0.292893 7.70711C-0.0976311 7.31658 -0.0976311 6.68342 0.292893 6.29289L6.29289 0.292893Z" fill="#006cb7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 502 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289Z" fill="#848C90"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="#848C90"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 721 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="#006CB7"/>
|
||||
<path d="M12.5781 6.625V18H11.0703V6.625H12.5781Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="#B6BEC4"/>
|
||||
<path d="M12.5781 6.625V18H11.0703V6.625H12.5781Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 10C10.4477 10 10 10.4477 10 11V20C10 20.5523 10.4477 21 11 21H20C20.5523 21 21 20.5523 21 20V11C21 10.4477 20.5523 10 20 10H11ZM8 11C8 9.34315 9.34315 8 11 8H20C21.6569 8 23 9.34315 23 11V20C23 21.6569 21.6569 23 20 23H11C9.34315 23 8 21.6569 8 20V11Z" fill="#006CB7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3C3.73478 3 3.48043 3.10536 3.29289 3.29289C3.10536 3.48043 3 3.73478 3 4V13C3 13.2652 3.10536 13.5196 3.29289 13.7071C3.48043 13.8946 3.73478 14 4 14H5C5.55228 14 6 14.4477 6 15C6 15.5523 5.55228 16 5 16H4C3.20435 16 2.44129 15.6839 1.87868 15.1213C1.31607 14.5587 1 13.7956 1 13V4C1 3.20435 1.31607 2.44129 1.87868 1.87868C2.44129 1.31607 3.20435 1 4 1H13C13.7956 1 14.5587 1.31607 15.1213 1.87868C15.6839 2.44129 16 3.20435 16 4V5C16 5.55228 15.5523 6 15 6C14.4477 6 14 5.55228 14 5V4C14 3.73478 13.8946 3.48043 13.7071 3.29289C13.5196 3.10536 13.2652 3 13 3H4Z" fill="#006CB7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 2C6.02944 2 2 6.02944 2 11C2 15.9706 6.02944 20 11 20C15.9706 20 20 15.9706 20 11C20 6.02944 15.9706 2 11 2ZM0 11C0 4.92487 4.92487 0 11 0C17.0751 0 22 4.92487 22 11C22 17.0751 17.0751 22 11 22C4.92487 22 0 17.0751 0 11Z" fill="#848C91"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2581 7.02432C10.7926 6.94447 10.3138 7.03195 9.90664 7.27127C9.49944 7.51058 9.19007 7.88629 9.03333 8.33184C8.85006 8.85283 8.27915 9.12661 7.75816 8.94333C7.23717 8.76006 6.96339 8.18915 7.14667 7.66816C7.46014 6.77705 8.07887 6.02563 8.89327 5.547C9.70767 5.06837 10.6652 4.89341 11.5962 5.05311C12.5273 5.2128 13.3718 5.69686 13.9801 6.41953C14.5883 7.14205 14.9213 8.05648 14.92 9.00091C14.9197 10.0711 14.3569 10.889 13.7751 11.4464C13.1931 12.004 12.5031 12.3909 12.0159 12.6243C11.977 12.6429 11.9487 12.6696 11.933 12.6926C11.9257 12.7033 11.9225 12.7113 11.9211 12.7154C11.92 12.719 11.92 12.7205 11.92 12.7207V13C11.92 13.5523 11.4723 14 10.92 14C10.3677 14 9.92 13.5523 9.92 13V12.7208C9.92 11.8509 10.4686 11.1479 11.1518 10.8206C11.5483 10.6307 12.0263 10.3522 12.3915 10.0023C12.7567 9.65234 12.92 9.32103 12.92 9L12.92 8.99851C12.9207 8.52619 12.7542 8.06886 12.4501 7.70753C12.1459 7.34619 11.7236 7.10417 11.2581 7.02432Z" fill="#848C91"/>
|
||||
<path d="M12 16C12 16.5523 11.5523 17 11 17C10.4477 17 10 16.5523 10 16C10 15.4477 10.4477 15 11 15C11.5523 15 12 15.4477 12 16Z" fill="#848C91"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="25" height="22" viewBox="0 0 25 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.1667 0.5C8.36834 0.5 3.66667 5.20167 3.66667 11H0.166672L4.70501 15.5383L4.78667 15.7017L9.50001 11H6.00001C6.00001 6.485 9.65167 2.83333 14.1667 2.83333C18.6817 2.83333 22.3333 6.485 22.3333 11C22.3333 15.515 18.6817 19.1667 14.1667 19.1667C11.915 19.1667 9.87334 18.245 8.40334 16.7633L6.74667 18.42C8.64834 20.3217 11.2617 21.5 14.1667 21.5C19.965 21.5 24.6667 16.7983 24.6667 11C24.6667 5.20167 19.965 0.5 14.1667 0.5ZM13 6.33333V12.1667L17.9583 15.1067L18.8567 13.6133L14.75 11.175V6.33333H13Z" fill="#848C91"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 633 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 2C6.02944 2 2 6.02944 2 11C2 15.9706 6.02944 20 11 20C15.9706 20 20 15.9706 20 11C20 6.02944 15.9706 2 11 2ZM0 11C0 4.92487 4.92487 0 11 0C17.0751 0 22 4.92487 22 11C22 17.0751 17.0751 22 11 22C4.92487 22 0 17.0751 0 11Z" fill="#F5F5F5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 11C0 10.4477 0.447715 10 1 10H21C21.5523 10 22 10.4477 22 11C22 11.5523 21.5523 12 21 12H1C0.447715 12 0 11.5523 0 11Z" fill="#F5F5F5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00023 11C8.06877 14.0748 9.12625 17.0352 11 19.4492C12.8737 17.0352 13.9312 14.0748 13.9998 11C13.9312 7.92516 12.8737 4.96485 11 2.5508C9.12626 4.96485 8.06877 7.92516 8.00023 11ZM11 1L10.2617 0.325577C7.59689 3.24291 6.08251 7.02885 6.00022 10.9792C5.99993 10.9931 5.99993 11.0069 6.00022 11.0208C6.08251 14.9711 7.59689 18.7571 10.2617 21.6744C10.4511 21.8818 10.7191 22 11 22C11.2809 22 11.5489 21.8818 11.7383 21.6744C14.4031 18.7571 15.9175 14.9711 15.9998 11.0208C16.0001 11.0069 16.0001 10.9931 15.9998 10.9792C15.9175 7.02885 14.4031 3.24291 11.7383 0.325577L11 1Z" fill="#F5F5F5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="#006CB7"/>
|
||||
<path d="M14.8828 15.125C14.8828 14.8594 14.8411 14.625 14.7578 14.4219C14.6797 14.2135 14.5391 14.026 14.3359 13.8594C14.138 13.6927 13.862 13.5339 13.5078 13.3828C13.1589 13.2318 12.7161 13.0781 12.1797 12.9219C11.6172 12.7552 11.1094 12.5703 10.6562 12.3672C10.2031 12.1589 9.8151 11.9219 9.49219 11.6562C9.16927 11.3906 8.92188 11.0859 8.75 10.7422C8.57812 10.3984 8.49219 10.0052 8.49219 9.5625C8.49219 9.11979 8.58333 8.71094 8.76562 8.33594C8.94792 7.96094 9.20833 7.63542 9.54688 7.35938C9.89062 7.07812 10.2995 6.85938 10.7734 6.70312C11.2474 6.54688 11.776 6.46875 12.3594 6.46875C13.2135 6.46875 13.9375 6.63281 14.5312 6.96094C15.1302 7.28385 15.5859 7.70833 15.8984 8.23438C16.2109 8.75521 16.3672 9.3125 16.3672 9.90625H14.8672C14.8672 9.47917 14.776 9.10156 14.5938 8.77344C14.4115 8.4401 14.1354 8.17969 13.7656 7.99219C13.3958 7.79948 12.9271 7.70312 12.3594 7.70312C11.8229 7.70312 11.3802 7.78385 11.0312 7.94531C10.6823 8.10677 10.4219 8.32552 10.25 8.60156C10.0833 8.8776 10 9.19271 10 9.54688C10 9.78646 10.0495 10.0052 10.1484 10.2031C10.2526 10.3958 10.4115 10.5755 10.625 10.7422C10.8438 10.9089 11.1198 11.0625 11.4531 11.2031C11.7917 11.3438 12.1953 11.4792 12.6641 11.6094C13.3099 11.7917 13.8672 11.9948 14.3359 12.2188C14.8047 12.4427 15.1901 12.6953 15.4922 12.9766C15.7995 13.2526 16.026 13.5677 16.1719 13.9219C16.3229 14.2708 16.3984 14.6667 16.3984 15.1094C16.3984 15.5729 16.3047 15.9922 16.1172 16.3672C15.9297 16.7422 15.6615 17.0625 15.3125 17.3281C14.9635 17.5938 14.5443 17.7995 14.0547 17.9453C13.5703 18.0859 13.0286 18.1562 12.4297 18.1562C11.9036 18.1562 11.3854 18.0833 10.875 17.9375C10.3698 17.7917 9.90885 17.5729 9.49219 17.2812C9.08073 16.9896 8.75 16.6302 8.5 16.2031C8.25521 15.7708 8.13281 15.2708 8.13281 14.7031H9.63281C9.63281 15.0938 9.70833 15.4297 9.85938 15.7109C10.0104 15.987 10.2161 16.2161 10.4766 16.3984C10.7422 16.5807 11.0417 16.7161 11.375 16.8047C11.7135 16.888 12.0651 16.9297 12.4297 16.9297C12.9557 16.9297 13.401 16.8568 13.7656 16.7109C14.1302 16.5651 14.4062 16.3568 14.5938 16.0859C14.7865 15.8151 14.8828 15.4948 14.8828 15.125Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="#B6BEC4"/>
|
||||
<path d="M14.8828 15.125C14.8828 14.8594 14.8411 14.625 14.7578 14.4219C14.6797 14.2135 14.5391 14.026 14.3359 13.8594C14.138 13.6927 13.862 13.5339 13.5078 13.3828C13.1589 13.2318 12.7161 13.0781 12.1797 12.9219C11.6172 12.7552 11.1094 12.5703 10.6562 12.3672C10.2031 12.1589 9.8151 11.9219 9.49219 11.6562C9.16927 11.3906 8.92188 11.0859 8.75 10.7422C8.57812 10.3984 8.49219 10.0052 8.49219 9.5625C8.49219 9.11979 8.58333 8.71094 8.76562 8.33594C8.94792 7.96094 9.20833 7.63542 9.54688 7.35938C9.89062 7.07812 10.2995 6.85938 10.7734 6.70312C11.2474 6.54688 11.776 6.46875 12.3594 6.46875C13.2135 6.46875 13.9375 6.63281 14.5312 6.96094C15.1302 7.28385 15.5859 7.70833 15.8984 8.23438C16.2109 8.75521 16.3672 9.3125 16.3672 9.90625H14.8672C14.8672 9.47917 14.776 9.10156 14.5938 8.77344C14.4115 8.4401 14.1354 8.17969 13.7656 7.99219C13.3958 7.79948 12.9271 7.70312 12.3594 7.70312C11.8229 7.70312 11.3802 7.78385 11.0312 7.94531C10.6823 8.10677 10.4219 8.32552 10.25 8.60156C10.0833 8.8776 10 9.19271 10 9.54688C10 9.78646 10.0495 10.0052 10.1484 10.2031C10.2526 10.3958 10.4115 10.5755 10.625 10.7422C10.8438 10.9089 11.1198 11.0625 11.4531 11.2031C11.7917 11.3438 12.1953 11.4792 12.6641 11.6094C13.3099 11.7917 13.8672 11.9948 14.3359 12.2188C14.8047 12.4427 15.1901 12.6953 15.4922 12.9766C15.7995 13.2526 16.026 13.5677 16.1719 13.9219C16.3229 14.2708 16.3984 14.6667 16.3984 15.1094C16.3984 15.5729 16.3047 15.9922 16.1172 16.3672C15.9297 16.7422 15.6615 17.0625 15.3125 17.3281C14.9635 17.5938 14.5443 17.7995 14.0547 17.9453C13.5703 18.0859 13.0286 18.1562 12.4297 18.1562C11.9036 18.1562 11.3854 18.0833 10.875 17.9375C10.3698 17.7917 9.90885 17.5729 9.49219 17.2812C9.08073 16.9896 8.75 16.6302 8.5 16.2031C8.25521 15.7708 8.13281 15.2708 8.13281 14.7031H9.63281C9.63281 15.0938 9.70833 15.4297 9.85938 15.7109C10.0104 15.987 10.2161 16.2161 10.4766 16.3984C10.7422 16.5807 11.0417 16.7161 11.375 16.8047C11.7135 16.888 12.0651 16.9297 12.4297 16.9297C12.9557 16.9297 13.401 16.8568 13.7656 16.7109C14.1302 16.5651 14.4062 16.3568 14.5938 16.0859C14.7865 15.8151 14.8828 15.4948 14.8828 15.125Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 2C5.13401 2 2 5.13401 2 9C2 12.866 5.13401 16 9 16C12.866 16 16 12.866 16 9C16 5.13401 12.866 2 9 2ZM0 9C0 4.02944 4.02944 0 9 0C13.9706 0 18 4.02944 18 9C18 13.9706 13.9706 18 9 18C4.02944 18 0 13.9706 0 9Z" fill="#848C91"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9429 13.9429C14.3334 13.5524 14.9666 13.5524 15.3571 13.9429L19.7071 18.2929C20.0976 18.6834 20.0976 19.3166 19.7071 19.7071C19.3166 20.0976 18.6834 20.0976 18.2929 19.7071L13.9429 15.3571C13.5524 14.9666 13.5524 14.3334 13.9429 13.9429Z" fill="#848C91"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 690 B |
@@ -0,0 +1,30 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import ConcordanceAggregationItem from './ConcordanceAggregationItem';
|
||||
|
||||
import styles from './ConcordanceAggregation.module.scss';
|
||||
import chevronDown from '../../assets/chevron-down.svg';
|
||||
import chevronUp from '../../assets/chevron-up.svg'
|
||||
|
||||
const ConcordanceAggregation = (props) => {
|
||||
const expandable = props.items.length > 5;
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const items = expandable && !isExpanded ? props.items.slice(0, 5) : props.items;
|
||||
|
||||
const expandHandler = () => {
|
||||
setIsExpanded((prevState) => !prevState);
|
||||
};
|
||||
|
||||
const headerEl = expandable ? <h2 onClick={expandHandler}>{props.title} <img className='float-end' src={isExpanded ? chevronUp : chevronDown} alt={isExpanded ? 'Collapse' : 'Expand'} /></h2> : <h2>{props.title}</h2>
|
||||
|
||||
return (
|
||||
<div className={styles.aggregation}>
|
||||
{headerEl}
|
||||
<ul>
|
||||
{items.map(x => <ConcordanceAggregationItem key={x.key} filterKey={x.key} title={x.title} count={x.count} />)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceAggregation;
|
||||
@@ -0,0 +1,21 @@
|
||||
@import '../../variables';
|
||||
|
||||
.aggregation {
|
||||
h2 {
|
||||
border-bottom: 1px solid $border-color;
|
||||
color: $primary-color;
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.9rem;
|
||||
padding-bottom: 0.7rem;
|
||||
|
||||
img {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0 0 2.7rem 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import useSearch from '../../hooks/use-search';
|
||||
import styles from './ConcordanceAggregationItem.module.scss';
|
||||
|
||||
const ConcordanceAggregationItem = (props) => {
|
||||
const search = useSearch();
|
||||
const isSelected = search.isFiltered('TextIds', props.filterKey) === true;
|
||||
const link = isSelected ? search.getClearFilterLink('TextIds', props.filterKey) : search.getFilterLink('TextIds', props.filterKey);
|
||||
|
||||
return (
|
||||
<li className={styles.aggregationItem}><Link to={link}>{isSelected && <span>X - </span>}{props.title}<span className='float-end'>{props.count}</span></Link></li>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceAggregationItem;
|
||||
@@ -0,0 +1,12 @@
|
||||
@import '../../variables';
|
||||
|
||||
.aggregationItem {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 0.8rem;
|
||||
padding-bottom: 0.8rem;
|
||||
|
||||
a {
|
||||
color: $text-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ConcordanceAlternateSearch from './ConcordanceAlternateSearch';
|
||||
import ConcordanceAggregation from './ConcordanceAggregation';
|
||||
import styles from './ConcordanceAggregations.module.scss';
|
||||
|
||||
const ConcordanceAggregations = (props) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={styles.aggregations}>
|
||||
<ConcordanceAlternateSearch title={t(`concordance.aggregation${props.lemmasAlternateSearch.type}`)} items={props.lemmasAlternateSearch.items} />
|
||||
{props.aggregations.map(x => <ConcordanceAggregation key={x.type} title={t(`concordance.aggregation${x.type}`)} items={x.items} />)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceAggregations;
|
||||
@@ -0,0 +1,8 @@
|
||||
.aggregations {
|
||||
padding-top: 1.8rem;
|
||||
padding-right: 6.4rem;
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import ConcordanceAlternateSearchItem from './ConcordanceAlternateSearchItem';
|
||||
import styles from './ConcordanceAlternateSearch.module.scss';
|
||||
|
||||
const ConcordanceAlternateSearch = (props) => {
|
||||
return (
|
||||
<div className={styles.alternateSearch}>
|
||||
<h2>{props.title}</h2>
|
||||
<ul>
|
||||
{props.items.map((x,i) => <ConcordanceAlternateSearchItem key={i} filterKey={x.key} title={x.title} count={x.count} search={x.search} />)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceAlternateSearch;
|
||||
@@ -0,0 +1,21 @@
|
||||
@import '../../variables';
|
||||
|
||||
.alternateSearch {
|
||||
h2 {
|
||||
border-bottom: 1px solid $border-color;
|
||||
color: $primary-color;
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.9rem;
|
||||
padding-bottom: 0.7rem;
|
||||
|
||||
img {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0 0 2.7rem 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import useSearch from '../../hooks/use-search';
|
||||
import styles from './ConcordanceAlternateSearchItem.module.scss';
|
||||
|
||||
const ConcordanceAlternateSearchItem = (props) => {
|
||||
const search = useSearch();
|
||||
const link = search.getAlternateSearchLink(props.search);
|
||||
return (
|
||||
<li className={styles.alternateSearchItem}><Link to={link}>{props.title}<span className='float-end'>{props.count}</span></Link></li>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceAlternateSearchItem;
|
||||
@@ -0,0 +1,12 @@
|
||||
@import '../../variables';
|
||||
|
||||
.alternateSearchItem {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 0.8rem;
|
||||
padding-bottom: 0.8rem;
|
||||
|
||||
a {
|
||||
color: $text-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSearch from '../../hooks/use-search';
|
||||
|
||||
import Spinner from '../shared/Spinner';
|
||||
import ConcordanceDetailsParagraph from './ConcordanceDetailsParagraph';
|
||||
import ConcordanceDetailsText from './ConcordanceDetailsText';
|
||||
import ConcordanceDetailsTokens from './ConcordanceDetailsTokens';
|
||||
|
||||
import closeIcon from '../../assets/close.svg';
|
||||
import styles from './ConcordanceDetails.module.scss';
|
||||
|
||||
|
||||
const ConcordanceDetails = (props) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const search = useSearch();
|
||||
const body = search.getDetailsRequest(props.paragraphId, props.tokenOrder);
|
||||
|
||||
const fetchDetails = () => fetch(`${window.baseAppPath}/concordancer/corpus/${search.corpusId}/concordances/details`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then((res) => res.json());
|
||||
|
||||
const { isLoading, isError, data, error } = useQuery(['details', body], fetchDetails, { refetchOnWindowFocus: false });
|
||||
|
||||
const selectParagraphTab = (e) => {
|
||||
e.preventDefault();
|
||||
setActiveTab(0);
|
||||
};
|
||||
|
||||
const selectTokensTab = (e) => {
|
||||
e.preventDefault();
|
||||
setActiveTab(1);
|
||||
}
|
||||
|
||||
if (!isLoading && data) {
|
||||
return (
|
||||
<div className={styles.details}>
|
||||
<div className={styles.tabs}>
|
||||
<ul>
|
||||
<li className={activeTab === 0 ? styles.active : null}><button type='button' className='btn btn-link' onClick={selectParagraphTab}>{t('concordance.detailsParagraph')}</button></li>
|
||||
<li className={activeTab === 1 ? styles.active : null}><button type='button' className='btn btn-link' onClick={selectTokensTab} >{t('concordance.detailsAnnotation')}</button></li>
|
||||
</ul>
|
||||
<button type='button' className={`btn btn-link ${styles.close}`} onClick={props.onClose}><img src={closeIcon} alt='Close' /></button>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className={`col-lg-9 ${styles.mainContent}`}>
|
||||
{activeTab === 0 && <ConcordanceDetailsParagraph tokens={data.tokens} />}
|
||||
{activeTab === 1 && <ConcordanceDetailsTokens tokens={data.tokens} />}
|
||||
</div>
|
||||
<div className={`col-lg-3 ${styles.textInfo}`}>
|
||||
<ConcordanceDetailsText source={data.sourceFile} year={data.year} title={data.title} author={data.author} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let content = <></>;
|
||||
if (isLoading) {
|
||||
content = <Spinner></Spinner>
|
||||
} else if (isError) {
|
||||
content = <p>{t('shared.searchError')}</p>
|
||||
} else if (!isLoading && data && data.items.length === 0) {
|
||||
content = <p>{t('shared.noResults')}</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.details}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ConcordanceDetails;
|
||||
@@ -0,0 +1,72 @@
|
||||
@import '../../variables';
|
||||
|
||||
.details {
|
||||
background-color: #ffffff;
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 1.6rem rgba(0, 0, 0, 0.08);
|
||||
margin: 1.6rem 0;
|
||||
padding: 2.4rem 2.8rem;
|
||||
position: relative;
|
||||
|
||||
.tabs {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 2.2rem;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
padding: 0 2rem;
|
||||
|
||||
button {
|
||||
color: $text-color-secondary;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-bottom: 3px solid $primary-color;
|
||||
|
||||
button {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 2.8rem;
|
||||
top: 2.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
border-right: 2px solid $border-color;
|
||||
padding-right: 3rem;
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
border-right: none;
|
||||
margin-bottom: 3rem;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.textInfo {
|
||||
padding-left: 3rem;
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import ConcordanceTokens from './ConcordanceTokens';
|
||||
|
||||
const ConcordanceDetailsParagraph = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<ConcordanceTokens tokens={props.tokens} />
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default ConcordanceDetailsParagraph;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import sytles from './ConcordanceDetailsText.module.scss';
|
||||
|
||||
const ConcordanceDetailsText = (props) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={sytles.textInfo}>
|
||||
<h5>{t('concordance.detailsTextSource')}</h5>
|
||||
<p>{props.source}</p>
|
||||
</div>
|
||||
<div className={sytles.textInfo}>
|
||||
<h5>{t('concordance.detailsTextYear')}</h5>
|
||||
<p>{props.year}</p>
|
||||
</div>
|
||||
<div className={sytles.textInfo}>
|
||||
<h5>{t('concordance.detailsTextTitle')}</h5>
|
||||
<p>{props.title}</p>
|
||||
</div>
|
||||
<div className={sytles.textInfo}>
|
||||
<h5>{t('concordance.detailsTextAuthor')}</h5>
|
||||
<p>{props.author}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceDetailsText;
|
||||
@@ -0,0 +1,22 @@
|
||||
@import '../../variables';
|
||||
|
||||
.textInfo {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
h5 {
|
||||
color: $primary-color;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ConcordanceDetailsTokensItem from "./ConcordanceDetailsTokensItem";
|
||||
import styles from './ConcordanceDetailsTokens.module.scss';
|
||||
|
||||
const ConcordanceDetailsTokens = (props) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`row ${styles.header}`}>
|
||||
<div className='col-lg-4'>{t('concordance.detailsAnnotationWord')}</div>
|
||||
<div className='col-lg-4'>{t('concordance.detailsAnnotationBasicForm')}</div>
|
||||
<div className='col-lg-4'>{t('concordance.detailsAnnotationPos')}</div>
|
||||
</div>
|
||||
{props.tokens.filter(x => x.type === 'Word').map((x, i) => <ConcordanceDetailsTokensItem key={i} token={x} />)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceDetailsTokens;
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../../variables';
|
||||
|
||||
.header {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import ConcordanceToken from './ConcordanceToken';
|
||||
|
||||
import styles from './ConcordanceDetailsTokensItem.module.scss';
|
||||
|
||||
const ConcordanceDetailsTokensItem = (props) => {
|
||||
const token = props.token;
|
||||
return (
|
||||
<div className={`row ${styles.item}`}>
|
||||
<div className='col-lg-4'><ConcordanceToken content={token.form} isCenterMatch={token.isCenterMatch} isWordInContextMatch={token.isWordInContextMatch} /></div>
|
||||
<div className='col-lg-4 text-secondary'><ConcordanceToken content={token.lemma} isCenterMatch={token.isCenterMatch} isWordInContextMatch={token.isWordInContextMatch} /></div>
|
||||
<div className='col-lg-4 text-secondary'><ConcordanceToken content={token.msdDescription} isCenterMatch={token.isCenterMatch} isWordInContextMatch={token.isWordInContextMatch} /></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceDetailsTokensItem;
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../../variables';
|
||||
|
||||
.item {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import SearchHelp from '../shared/SearchHelp.js';
|
||||
|
||||
const ConcordanceHelp = () => {
|
||||
return (
|
||||
<SearchHelp>
|
||||
<ul>
|
||||
<li>
|
||||
<h4>rdeča jagoda</h4>
|
||||
<p><em>rdeča jagoda, rdeče jagode, rdeči jagodi</em></p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>"rdečimi jagodami"</h4>
|
||||
<p><em>rdečimi jagodami</em></p>
|
||||
</li>
|
||||
</ul>
|
||||
</SearchHelp>
|
||||
)
|
||||
};
|
||||
|
||||
export default ConcordanceHelp;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
import ConcordanceListItem from './ConcordanceListItem';
|
||||
|
||||
const ConcordanceList = (props) => {
|
||||
const [expandedIndex, setExpandedIndex] = useState(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{props.items.map((x, i) => <ConcordanceListItem key={i} index={i} expanded={i === expandedIndex} item={x} toggleDetails={(index) => setExpandedIndex(index)} />)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceList;
|
||||
@@ -0,0 +1,17 @@
|
||||
import ConcordanceDetails from './ConcordanceDetails';
|
||||
import ConcordanceTokens from './ConcordanceTokens';
|
||||
import styles from './ConcordanceListItem.module.scss';
|
||||
|
||||
const ConcordanceListItem = (props) => {
|
||||
return (
|
||||
<>
|
||||
<div className={`row ${styles.item}`} onClick={() => { props.toggleDetails(props.index) }}>
|
||||
<div className='col text-end'><ConcordanceTokens tokens={props.item.leftContext} /> </div>
|
||||
<div className='col'><ConcordanceTokens tokens={[props.item.centerContext]} /> <ConcordanceTokens tokens={props.item.rightContext} /></div>
|
||||
</div>
|
||||
{props.expanded && <ConcordanceDetails paragraphId={props.item.paragraphId} tokenOrder={props.item.centerContext.tokenOrder} onClose={() => { props.toggleDetails(null) }} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceListItem;
|
||||
@@ -0,0 +1,8 @@
|
||||
@import '../../variables';
|
||||
|
||||
.item {
|
||||
border-bottom: 1px solid $border-color;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const ConcordanceTitle = () => {
|
||||
return (
|
||||
<h1>Iskanje po konkordancah</h1>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceTitle;
|
||||
@@ -0,0 +1,14 @@
|
||||
import styles from './ConcordanceToken.module.scss';
|
||||
|
||||
const ConcordanceToken = (props) => {
|
||||
if (props.isCenterMatch === true) {
|
||||
return <span className={styles.center}>{props.content}</span>
|
||||
} else if (props.isWordInContextMatch === true) {
|
||||
return <em className={styles.wordInContext}>{props.content}</em>
|
||||
} else {
|
||||
return <>{props.content}</>
|
||||
}
|
||||
};
|
||||
|
||||
export default ConcordanceToken;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../../variables';
|
||||
|
||||
.center,
|
||||
.wordInContext {
|
||||
color: $primary-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import ConcordanceToken from "./ConcordanceToken";
|
||||
|
||||
const ConcordanceTokens = (props) => {
|
||||
return (
|
||||
<>
|
||||
{props.tokens.map((x,i) => <ConcordanceToken content={x.form} isCenterMatch={x.isCenterMatch} isWordInContextMatch={x.isWordInContextMatch} key={i} />)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConcordanceTokens;
|
||||
@@ -0,0 +1,29 @@
|
||||
import styles from './IndexMenu.module.scss';
|
||||
import logo from '../../assets/logo.png';
|
||||
import langIcon from '../../assets/language.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const IndexTopBar = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const changeLanguageTitle = i18n.language === 'sl' ? 'English' : 'Slovensko';
|
||||
const changeLanguageCode = i18n.language === 'sl' ? 'EN' : 'SL';
|
||||
|
||||
const languageClickHandler = () => {
|
||||
const newLanguage = i18n.language === 'sl' ? 'en' : 'sl';
|
||||
i18n.changeLanguage(newLanguage);
|
||||
localStorage.setItem('language', newLanguage);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.menu}>
|
||||
<div className='container-fluid'>
|
||||
<div className='row align-items-center'>
|
||||
<div className='col'><img src={logo} alt='Logo' /></div>
|
||||
<div className={`col ${styles.links}`}><button type='button' onClick={languageClickHandler}><img src={langIcon} alt={changeLanguageTitle} />{changeLanguageCode}</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndexTopBar;
|
||||
@@ -0,0 +1,25 @@
|
||||
@import '../../variables';
|
||||
|
||||
.menu {
|
||||
background-color: $primary-color;
|
||||
height: 15.4rem;
|
||||
|
||||
:global(.container-fluid),
|
||||
:global(.row) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.links {
|
||||
text-align: right;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #f5f5f5;
|
||||
|
||||
img {
|
||||
margin-right: 0.625rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import useSearch from '../../hooks/use-search';
|
||||
import styles from './IndexStatistics.module.scss';
|
||||
|
||||
const IndexStatistics = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const search = useSearch();
|
||||
const corpusId = search.corpusId;
|
||||
|
||||
const fetchStatistics = () => fetch(`${window.baseAppPath}/concordancer/corpus/${corpusId}/stats`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then((res) => {
|
||||
return res.json();
|
||||
});
|
||||
|
||||
const { isLoading, isSuccess, isError, data, error } = useQuery(['statistics', corpusId], fetchStatistics, { keepPreviousData: true, refetchOnWindowFocus: false });
|
||||
|
||||
return (
|
||||
<div className={styles.stats}>
|
||||
<div className='row'>
|
||||
<div className={`col-md-4 ${styles.counter}`}><h2>{(!isLoading && isSuccess) ? data.texts : "..."}</h2><p>{t('shared.countTexts')}</p></div>
|
||||
<div className={`col-md-4 ${styles.counter}`}><h2>{(!isLoading && isSuccess) ? data.sentences : "..."}</h2><p>{t('shared.countSentences')}</p></div>
|
||||
<div className={`col-md-4 ${styles.counter}`}><h2>{(!isLoading && isSuccess) ? data.words : "..."}</h2><p>{t('shared.countWords')}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndexStatistics;
|
||||
@@ -0,0 +1,35 @@
|
||||
@import '../../variables';
|
||||
|
||||
.stats {
|
||||
background: #ffffff;
|
||||
border-radius: 1.2rem;
|
||||
box-shadow: 0 0.4rem 2rem rgba(0, 0, 0, 0.2);
|
||||
margin-top: 5.1rem;
|
||||
padding: 3.2rem 0 3.1rem 0;
|
||||
|
||||
.counter {
|
||||
border-right: 1px solid #d9d9d9;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $primary-color;
|
||||
font-size: 4.8rem;
|
||||
font-weight: 100;
|
||||
line-height: 5.6rem;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #848c91;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 400;
|
||||
line-height: 2.1rem;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import SearchHelp from '../shared/SearchHelp.js';
|
||||
|
||||
const ListHelp = () => {
|
||||
return (
|
||||
<SearchHelp>
|
||||
<ul>
|
||||
<li>
|
||||
<h4>*pisati</h4>
|
||||
<p><em>pisati, <strong>na</strong>pisati, <strong>za</strong>pisati, <strong>pod</strong>pisati…</em></p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>?pisati</h4>
|
||||
<p><em><strong>v</strong>pisati, <strong>o</strong>pisati, <strong>s</strong>pisati…</em></p>
|
||||
</li>
|
||||
</ul>
|
||||
</SearchHelp>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListHelp;
|
||||
@@ -0,0 +1,13 @@
|
||||
import styles from './ListItem.module.scss';
|
||||
|
||||
const ListItem = (props) => {
|
||||
return (
|
||||
<div className={`row ${styles.item}`}>
|
||||
<div className='col'>{props.item.form}</div>
|
||||
<div className='col'>{props.item.lemma}</div>
|
||||
<div className='col'>{props.item.frequency}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../../variables';
|
||||
|
||||
.item {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import ListItem from './ListItem';
|
||||
|
||||
import styles from './ListItems.module.scss';
|
||||
|
||||
const ListItems = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<div className={`row ${styles.header}`}>
|
||||
<div className='col'><strong>Termin</strong></div>
|
||||
<div className='col'><strong>Osnovna oblika</strong></div>
|
||||
<div className='col'><strong>Število pojavitev</strong></div>
|
||||
</div>
|
||||
{props.items.map((x, i) => <ListItem key={i} item={x} />)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItems;
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../../variables';
|
||||
|
||||
.header {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import closeIcon from '../../assets/close.svg';
|
||||
import exportIcon from '../../assets/export.svg';
|
||||
import styles from './ResultsExport.module.scss';
|
||||
|
||||
const ResultsExport = (props) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
const allResults = props.results;
|
||||
const defaultValue = allResults < 100 ? allResults : 100;
|
||||
const maxValue = allResults > 1000 ? 1000 : allResults;
|
||||
|
||||
const [numFirst, setNumFirst] = useState(defaultValue);
|
||||
const [numRandom, setNumRandom] = useState(defaultValue);
|
||||
const [type, setType] = useState('FirstRows');
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let value = type === 'FirstRows' ? numFirst : numRandom;
|
||||
let isValid = value >= 1 && value <= maxValue;
|
||||
setIsValid(isValid);
|
||||
}, [type, numFirst, numRandom, maxValue]);
|
||||
|
||||
const onNumFirstChanged = (e) => {
|
||||
setNumFirst(e.target.value);
|
||||
};
|
||||
|
||||
const onNumRandomChanged = (e) => {
|
||||
setNumRandom(e.target.value);
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
if (isValid) {
|
||||
let value = type === 'FirstRows' ? numFirst : numRandom;
|
||||
props.onConfirm(type, value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.export}>
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.header}>
|
||||
<button className={styles.close} type="button" onClick={props.onClose}><img src={closeIcon} alt='Close' /></button>
|
||||
<img className={styles.icon} src={exportIcon} alt='Export' />
|
||||
<h1>{t('shared.exportTitle')}</h1>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className='row'>
|
||||
<div className='col'>{t('shared.exportTotal', { total: allResults })}</div>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className='col'><input type='radio' value='FirstRows' checked={type === 'FirstRows'} onChange={e => setType(e.target.value)} /> {t('shared.exportFirst')} <input type='number' value={numFirst} onChange={onNumFirstChanged} min='1' max={maxValue} /> {t('shared.exportRecords')}</div>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className='col'><input type='radio' value='RandomRows' checked={type === 'RandomRows'} onChange={e => setType(e.target.value)} /> {t('shared.exportRandom')} <input type='number' value={numRandom} onChange={onNumRandomChanged} min='1' max={maxValue} /> {t('shared.exportRecords')}</div>
|
||||
</div>
|
||||
{!isValid && <div className='row'><div className='col'>{t('shared.exportMax', { max: maxValue })}</div></div>}
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<button type="button" onClick={confirm}>{t('shared.export')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResultsExport;
|
||||
@@ -0,0 +1,85 @@
|
||||
@import '../../variables';
|
||||
|
||||
.export {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
height: 100%;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
.modal {
|
||||
background-color: #ffffff;
|
||||
border-radius: 0.6rem;
|
||||
box-shadow: 0 0.4rem 4rem rgba(0, 0, 0, 0.35);
|
||||
max-width: 70rem;
|
||||
margin: 15% auto;
|
||||
position: relative;
|
||||
width: auto;
|
||||
|
||||
.header {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 0.6rem 0.6rem 0 0;
|
||||
padding: 2.4rem 2.1rem 2.3rem 2.1rem;
|
||||
|
||||
.close {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: 2.4rem;
|
||||
right: 2.1rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
height: 3.4rem;
|
||||
margin: 4.2rem auto 2.2rem auto;
|
||||
width: 3.4rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: $primary-color;
|
||||
font-size: 3.6rem;
|
||||
font-weight: 300;
|
||||
line-height: 4.2rem;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 2.8rem 9.5rem 0 9.5rem;
|
||||
|
||||
:global(.row) {
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #b6bec4;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 5.9rem 2.1rem 3.9rem 2.1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
background-color: $primary-color;
|
||||
box-shadow: 0 0.4rem 0.4rem rgba(0, 0, 0, 0.12);
|
||||
border: none;
|
||||
border-radius: 0.6rem;
|
||||
color: #ffffff;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 400;
|
||||
line-height: 2.1rem;
|
||||
padding: 1rem 6rem 1rem 6rem;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import exportIcon from '../../assets/export.svg';
|
||||
import styles from './ResultsHeader.module.scss';
|
||||
|
||||
const ResultsHeader = (props) => {
|
||||
return (
|
||||
<div className={`row ${styles.resultsHeader}`}>
|
||||
<div className='col'><h1>{props.query}</h1></div>
|
||||
<div className='col text-end'><button type='button' className='btn btn-link' onClick={props.onExport}><img src={exportIcon} alt='Export' /></button></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResultsHeader;
|
||||
@@ -0,0 +1,10 @@
|
||||
@import '../../variables';
|
||||
|
||||
.resultsHeader {
|
||||
h1 {
|
||||
color: $primary-color;
|
||||
font-size: 3.6rem;
|
||||
font-weight: 400;
|
||||
line-height: 4.2rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import useSearch from '../../hooks/use-search';
|
||||
import styles from './ResultsPager.module.scss';
|
||||
|
||||
const ResultsPager = (props) => {
|
||||
const pageSize = 20;
|
||||
const offset = props.offset;
|
||||
const total = props.total;
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
const currentPage = Math.floor(offset / pageSize) + 1;
|
||||
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const search = useSearch();
|
||||
|
||||
const renderPreviousPage = () => {
|
||||
const className = currentPage <= 1 ? 'disabled' : null;
|
||||
return renderPage(currentPage - 1, '<<', className);
|
||||
};
|
||||
|
||||
const renderFirstPage = () => {
|
||||
const className = currentPage === 1 ? 'active' : null;
|
||||
return renderPage(1, '1', className);
|
||||
};
|
||||
|
||||
const renderCurrentPage = () => {
|
||||
return renderPage(currentPage, currentPage, 'active');
|
||||
};
|
||||
|
||||
const renderLastPage = () => {
|
||||
const className = currentPage === totalPages ? 'active' : null;
|
||||
return renderPage(totalPages, totalPages, className);
|
||||
};
|
||||
|
||||
const renderNextPage = () => {
|
||||
const className = currentPage >= totalPages ? 'disabled' : null;
|
||||
return renderPage(currentPage + 1, '>>', className);
|
||||
};
|
||||
|
||||
const renderPage = (page, title, className) => {
|
||||
const link = search.getPagerLink(page);
|
||||
return (
|
||||
<Link className={styles[className]} to={link}>{title}</Link>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`row align-items-center ${styles.pager}`}>
|
||||
<div className='col'>{t('shared.searchRecords', { start: offset + 1, end: offset + pageSize, total: total })}</div>
|
||||
<div className='col'>
|
||||
<div className={`${styles.pages} float-end`}>
|
||||
{renderPreviousPage()}
|
||||
{currentPage > 1 && renderFirstPage()}
|
||||
{renderCurrentPage()}
|
||||
{currentPage < totalPages && renderLastPage()}
|
||||
{renderNextPage()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default ResultsPager;
|
||||
@@ -0,0 +1,32 @@
|
||||
@import '../../variables';
|
||||
|
||||
.pager {
|
||||
border-bottom: 2px solid $border-color;
|
||||
padding-bottom: 1.1rem;
|
||||
margin-bottom: 2.2rem;
|
||||
|
||||
.pages {
|
||||
display: inline-block;
|
||||
|
||||
a {
|
||||
color: $text-color;
|
||||
display: inline-block;
|
||||
padding: 1rem;
|
||||
text-decoration: none;
|
||||
|
||||
&.active {
|
||||
background-color: #b6bec3;
|
||||
border-radius: 0.4rem;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $text-color-secondary;
|
||||
cursor: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SearchDropdown from "./SearchDropdown";
|
||||
import HistoryItem from "./HistoryItem";
|
||||
import styles from './History.module.scss';
|
||||
import useHistory from "../../hooks/use-history";
|
||||
|
||||
const History = (props) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
const source = props.source;
|
||||
|
||||
return (
|
||||
<SearchDropdown>
|
||||
<div className={styles.history}>
|
||||
<h3>{t('shared.searchHistory')}</h3>
|
||||
<ul className={styles.historyList}>
|
||||
{history.getHistory(props.source).map((x, i) => <HistoryItem key={i} source={source} query={x.query} />)}
|
||||
</ul>
|
||||
</div>
|
||||
</SearchDropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default History;
|
||||
@@ -0,0 +1,18 @@
|
||||
@import '../../variables';
|
||||
|
||||
.history {
|
||||
padding: 0 5rem 4.9rem 3.8rem;
|
||||
|
||||
h3 {
|
||||
color: $primary-color;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 2.8rem;
|
||||
margin-top: 1.9rem;
|
||||
}
|
||||
|
||||
ul.historyList {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import useSearch from '../../hooks/use-search';
|
||||
|
||||
import styles from './HistoryItem.module.scss';
|
||||
|
||||
const HistoryItem = (props) => {
|
||||
const search = useSearch();
|
||||
const source = props.source;
|
||||
|
||||
return (
|
||||
<li className={styles.historyListItem}>
|
||||
<Link to={search.getSearchLink(source, props.query)}>{props.query}</Link>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryItem;
|
||||
@@ -0,0 +1,12 @@
|
||||
@import '../../variables';
|
||||
|
||||
li.historyListItem {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 0.8rem;
|
||||
padding-bottom: 0.8rem;
|
||||
|
||||
a {
|
||||
color: $text-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import styles from './SearchDropdown.module.scss';
|
||||
|
||||
const SearchDropdown = (props) => {
|
||||
return (
|
||||
<div className={styles.dropdown}>
|
||||
<div className={styles.dropdownContent}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchDropdown;
|
||||
@@ -0,0 +1,13 @@
|
||||
.dropdown {
|
||||
position: relative;
|
||||
|
||||
.dropdownContent {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 0 0 0.6rem 0.6rem;
|
||||
box-shadow: 0 0.4rem 1.2rem rgba(0, 0, 0, 0.25);
|
||||
padding: 1.6rem;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SearchDropdown from './../shared/SearchDropdown.js';
|
||||
import styles from './SearchHelp.module.scss';
|
||||
|
||||
const SearchHelp = (props) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SearchDropdown>
|
||||
<div className={styles.help}>
|
||||
<h3>{t('shared.searchHelp')}</h3>
|
||||
{props.children}
|
||||
</div>
|
||||
</SearchDropdown>
|
||||
)
|
||||
};
|
||||
|
||||
export default SearchHelp;
|
||||
@@ -0,0 +1,36 @@
|
||||
@import '../../variables';
|
||||
|
||||
.help {
|
||||
padding: 0 5rem 4.9rem 3.8rem;
|
||||
|
||||
h3 {
|
||||
color: $primary-color;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 2.8rem;
|
||||
margin-top: 1.9rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 2.8rem;
|
||||
padding-bottom: 2.6rem;
|
||||
|
||||
h4 {
|
||||
color: $primary-color;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $text-color;
|
||||
font-size: 1.6rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import styles from './Spinner.module.scss';
|
||||
|
||||
const Spinner = () => {
|
||||
return (
|
||||
<div className={styles.ldsDualRing}></div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
@@ -0,0 +1,29 @@
|
||||
@import '../../variables';
|
||||
|
||||
.ldsDualRing {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.ldsDualRing:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 8px;
|
||||
border-radius: 50%;
|
||||
border: 6px solid $text-color;
|
||||
border-color: $text-color transparent $text-color transparent;
|
||||
animation: ldsDualRing 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes ldsDualRing {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
const useDownload = (url) => {
|
||||
const start = (body) => {
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res) => res.blob())
|
||||
.then(blob => {
|
||||
var url = window.URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = "export.csv";
|
||||
document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox
|
||||
a.click();
|
||||
a.remove(); //afterwards we remove the element again
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
start
|
||||
};
|
||||
};
|
||||
|
||||
export default useDownload;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
|
||||
const useHistory = () => {
|
||||
const { corpusId } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const getStorageKey = (source) => {
|
||||
return `${source}_history`;
|
||||
};
|
||||
|
||||
const getHistory = (source) => {
|
||||
const storageKey = getStorageKey(source);
|
||||
return JSON.parse(localStorage.getItem(storageKey)) ?? [];
|
||||
};
|
||||
|
||||
const saveSearch = (source) => {
|
||||
const query = searchParams.get('query');
|
||||
const storageKey = getStorageKey(source);
|
||||
let searchItem = { corpusId: corpusId, query: query };
|
||||
|
||||
let currentHistory = getHistory(source);
|
||||
|
||||
// Check if the item already exists in the history
|
||||
let searchItemJson = JSON.stringify(searchItem);
|
||||
if (!currentHistory.some(x => JSON.stringify(x) === searchItemJson)) {
|
||||
var newHistory = [searchItem, ...currentHistory.slice(0, 9)];
|
||||
localStorage.setItem(storageKey, JSON.stringify(newHistory));
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getHistory,
|
||||
saveSearch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useHistory;
|
||||
@@ -0,0 +1,156 @@
|
||||
import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
const useSearch = () => {
|
||||
const navigate = useNavigate();
|
||||
const { corpusId } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
const rootUrl = `/${corpusId}`;
|
||||
|
||||
const newSearch = (source, query) => {
|
||||
const link = `/${corpusId}/${source}/search?query=${query}`;
|
||||
navigate(link);
|
||||
};
|
||||
|
||||
const getSearchLink = (source, query) => {
|
||||
return `/${corpusId}/${source}/search?query=${query}`;
|
||||
};
|
||||
|
||||
const getSearchRequest = (includePage = true) => {
|
||||
// Common properties
|
||||
var request = {
|
||||
corpusId: corpusId,
|
||||
};
|
||||
|
||||
if (includePage) {
|
||||
const page = searchParams.get('page') ? searchParams.get('page') : 1;
|
||||
const from = (page - 1) * 20;
|
||||
const size = 20;
|
||||
|
||||
request['from'] = from;
|
||||
request['size'] = size;
|
||||
}
|
||||
|
||||
// Query
|
||||
if (searchParams.get('query')) {
|
||||
request['query'] = searchParams.get('query');
|
||||
|
||||
} else {
|
||||
// Main word
|
||||
let mainWord = {
|
||||
form: searchParams.get('mainWord.form'),
|
||||
lemma: searchParams.get('mainWord.lemma'),
|
||||
formSearchType: searchParams.get('mainWord.formSearchType'),
|
||||
};
|
||||
request['mainWord'] = mainWord;
|
||||
|
||||
// Word in context
|
||||
let wordsInContext = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (searchParams.has('wordInContext[' + i + '].lemma')) {
|
||||
let wordInContext = {
|
||||
form: searchParams.get('wordInContext[' + i + '].form'),
|
||||
lemma: searchParams.get('wordInContext[' + i + '].lemma'),
|
||||
formSearchType: searchParams.get('wordInContext[' + i + '].formSearchType'),
|
||||
distanceType: searchParams.get('wordInContext[' + i + '].distanceType'),
|
||||
leftPosition: searchParams.get('wordInContext[' + i + '].leftPosition'),
|
||||
rightPosition: searchParams.get('wordInContext[' + i + '].rightPosition')
|
||||
};
|
||||
wordsInContext.push(wordInContext);
|
||||
}
|
||||
}
|
||||
if (wordsInContext.length > 0) {
|
||||
request['wordsInContext'] = wordsInContext;
|
||||
}
|
||||
}
|
||||
|
||||
// Filters
|
||||
if (searchParams.has('TextIds') === true) {
|
||||
request['TextIds'] = searchParams.getAll('TextIds');
|
||||
}
|
||||
return request;
|
||||
};
|
||||
|
||||
const getDetailsRequest = (paragraphId, tokenOrder) => {
|
||||
var request = getSearchRequest(false);
|
||||
request['ParagraphId'] = paragraphId;
|
||||
request['TokenOrder'] = tokenOrder;
|
||||
return request;
|
||||
};
|
||||
|
||||
const getQuery = () => {
|
||||
return searchParams.get('query');
|
||||
}
|
||||
|
||||
const getPage = () => {
|
||||
return searchParams.get('page');
|
||||
}
|
||||
|
||||
const getPagerLink = (page) => {
|
||||
|
||||
searchParams.set('page', page);
|
||||
|
||||
return `${location.pathname}?${searchParams.toString()}`;
|
||||
};
|
||||
|
||||
const getFilterLink = (name, key) => {
|
||||
let newParams = new URLSearchParams(searchParams);
|
||||
newParams.append(name, key);
|
||||
return `${location.pathname}?${newParams.toString()}`;
|
||||
};
|
||||
|
||||
const getClearFilterLink = (name, key) => {
|
||||
var newParams = new URLSearchParams();
|
||||
searchParams.forEach((v, k) => {
|
||||
if (k !== name || v !== key) {
|
||||
newParams.append(k, v);
|
||||
}
|
||||
});
|
||||
|
||||
return `${location.pathname}?${newParams.toString()}`;
|
||||
}
|
||||
|
||||
const isFiltered = (name, key) => {
|
||||
var values = searchParams.getAll(name);
|
||||
return values.indexOf(key) !== -1;
|
||||
};
|
||||
|
||||
const getAlternateSearchLink = (search) => {
|
||||
let newParams = new URLSearchParams();
|
||||
newParams.append('mainWord.form', search.mainWord.form);
|
||||
newParams.append('mainWord.lemma', search.mainWord.lemma);
|
||||
newParams.append('mainWord.formSearchType', search.mainWord.formSearchType);
|
||||
|
||||
if (search.wordsInContext) {
|
||||
search.wordsInContext.forEach((w, i) => {
|
||||
newParams.append('wordInContext[' + i + '].form', w.form);
|
||||
newParams.append('wordInContext[' + i + '].lemma', w.lemma);
|
||||
newParams.append('wordInContext[' + i + '].formSearchType', w.formSearchType);
|
||||
newParams.append('wordInContext[' + i + '].distanceType', w.distanceType);
|
||||
newParams.append('wordInContext[' + i + '].leftPosition', 0);
|
||||
newParams.append('wordInContext[' + i + '].rightPosition', i + 1);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
return `${location.pathname}?${newParams.toString()}`;
|
||||
};
|
||||
|
||||
return {
|
||||
corpusId,
|
||||
rootUrl,
|
||||
newSearch,
|
||||
getSearchLink,
|
||||
getSearchRequest,
|
||||
getDetailsRequest,
|
||||
getQuery,
|
||||
getPage,
|
||||
getPagerLink,
|
||||
getFilterLink,
|
||||
getClearFilterLink,
|
||||
isFiltered,
|
||||
getAlternateSearchLink,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSearch;
|
||||
@@ -0,0 +1,99 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: {
|
||||
shared: {
|
||||
'concordance': 'Search',
|
||||
'countSentences': 'sentences',
|
||||
'countTexts': 'texts',
|
||||
'countWords': 'words',
|
||||
'enterSearch': 'Enter search query',
|
||||
'export': 'Export',
|
||||
'exportFirst': 'Export first',
|
||||
'exportMax': 'You can export up to {{max}} records!',
|
||||
'exportRandom': 'Export random',
|
||||
'exportRecords': 'records.',
|
||||
'exportTitle': 'Export data',
|
||||
'exportTotal': 'The number of records is {{total}}.',
|
||||
'list': 'List',
|
||||
'noResults': 'No results were found for your search.',
|
||||
'searchError': 'Server returned error when searching.',
|
||||
'searchHelp': 'Help',
|
||||
'searchHistory': 'Search history',
|
||||
'searchRecords': 'Showing {{start}}-{{end}} from {{total}} records.'
|
||||
},
|
||||
concordance: {
|
||||
'title': 'Search concordances',
|
||||
'detailsAnnotation': 'Corpus annotation',
|
||||
'detailsAnnotationBasicForm': 'Basic form',
|
||||
'detailsAnnotationPos': 'Morphological properties',
|
||||
'detailsAnnotationWord': 'Word',
|
||||
'detailsParagraph': 'Paragraph',
|
||||
'detailsTextAuthor': 'Author',
|
||||
'detailsTextSource': 'Source',
|
||||
'detailsTextTitle': 'Title',
|
||||
'detailsTextYear': 'Year',
|
||||
'aggregationLemmas': 'Basic forms',
|
||||
'aggregationText': 'Source'
|
||||
},
|
||||
list: {
|
||||
'title': 'Search list'
|
||||
}
|
||||
}
|
||||
},
|
||||
sl: {
|
||||
translation: {
|
||||
shared: {
|
||||
'concordance': 'Iskanje',
|
||||
'countSentences': 'stavkov',
|
||||
'countTexts': 'besedil',
|
||||
'countWords': 'besed',
|
||||
'enterSearch': 'Vpišite iskalni niz',
|
||||
'export': 'Izvozi',
|
||||
'exportFirst': 'Izvozi prvih',
|
||||
'exportMax': 'Izvozite lahko največ {{max}} zapisov!',
|
||||
'exportRandom': 'Izvozi naključnih',
|
||||
'exportRecords': 'zapisov.',
|
||||
'exportTitle': 'Izvoz podatkov',
|
||||
'exportTotal': 'Število zapisov je {{total}}.',
|
||||
'list': 'Seznami',
|
||||
'noResults': 'Vaše iskanje ni obrodilo sadov.',
|
||||
'searchError': 'Strežnik je vrnil napako pri iskanju.',
|
||||
'searchHelp': 'Pomoč',
|
||||
'searchHistory': 'Zgodovina iskanj',
|
||||
'searchRecords': 'Prikazujem {{start}}-{{end}} od {{total}} zapisov.'
|
||||
},
|
||||
concordance: {
|
||||
'title': 'Iskanje po konkordancah',
|
||||
'detailsAnnotation': 'Korpusne oznake',
|
||||
'detailsAnnotationBasicForm': 'Osnovna oblika',
|
||||
'detailsAnnotationPos': 'Oblikoslovne značilnosti osnovne oblike',
|
||||
'detailsAnnotationWord': 'Beseda',
|
||||
'detailsParagraph': 'Prikaz odstavka',
|
||||
'detailsTextAuthor': 'avtor',
|
||||
'detailsTextSource': 'vir',
|
||||
'detailsTextTitle': 'naslov',
|
||||
'detailsTextYear': 'leto nastanka',
|
||||
'aggregationLemmas': 'Osnovne oblike',
|
||||
'aggregationText': 'Vir'
|
||||
},
|
||||
list: {
|
||||
'title': 'Iskanje po seznamih'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
lng: 'sl',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import i18n from './i18n';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
|
||||
// Set default language
|
||||
const language = localStorage.getItem("language");
|
||||
if (language) {
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter basename={window.baseAppPath}>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
// Bootstrap variables overrides
|
||||
$grid-gutter-width: 0;
|
||||
|
||||
// Import Bootstrap
|
||||
@import '../node_modules/bootstrap/scss/bootstrap';
|
||||
|
||||
// RSDO
|
||||
|
||||
@import 'variables';
|
||||
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f5f5f5;
|
||||
color: $text-color;
|
||||
font-family: 'Roboto', 'Helvetica';
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
padding-left: 3.2rem;
|
||||
padding-right: 3.2rem;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import styles from './IndexLayout.module.scss';
|
||||
import searchIcon from '../assets/search.svg';
|
||||
import historyIcon from '../assets/history.svg';
|
||||
import helpIcon from '../assets/help.svg';
|
||||
import concordanceActiveIcon from '../assets/concordance-active.svg';
|
||||
import concordanceInactiveIcon from '../assets/concordance-inactive.svg';
|
||||
import listActiveIcon from '../assets/list-active.svg';
|
||||
import listInactiveIcon from '../assets/list-inactive.svg';
|
||||
import IndexStatistics from '../components/index/IndexStatistics';
|
||||
import IndexMenu from '../components/index/IndexMenu';
|
||||
import useSearch from '../hooks/use-search';
|
||||
import History from '../components/shared/History';
|
||||
|
||||
const IndexLayout = (props) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const search = useSearch();
|
||||
|
||||
// State
|
||||
const [query, setQuery] = useState(search.getQuery());
|
||||
const [isHelpVisible, setIsHelpVisible] = useState(false);
|
||||
const [isHistoryVisible, setIsHistoryVisible] = useState(false);
|
||||
|
||||
// Props
|
||||
const { source, help, title } = props;
|
||||
const isConcordance = source === 'concordance';
|
||||
|
||||
const onSubmitHandler = (e) => {
|
||||
e.preventDefault();
|
||||
if (query) {
|
||||
search.newSearch(source, query);
|
||||
}
|
||||
};
|
||||
|
||||
const onHistoryClickHandler = () => {
|
||||
setIsHistoryVisible(isHistoryVisibleOld => !isHistoryVisibleOld);
|
||||
}
|
||||
|
||||
const onHelpClickHandler = () => {
|
||||
setIsHelpVisible(isHelpVisibleOld => !isHelpVisibleOld);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<IndexMenu />
|
||||
<div className={styles.search}>
|
||||
<div className='container-xl'>
|
||||
<div className='row'>
|
||||
<div className='col-xs-12 col-xl-6 offset-xl-3'>
|
||||
<h1>{title}</h1>
|
||||
<ul className={styles.areas}>
|
||||
<li className={isConcordance ? styles.active : null}><img src={isConcordance ? concordanceActiveIcon : concordanceInactiveIcon} alt={t('shared.concordance')} /><Link to={`/${search.corpusId}/concordance`}>{t('shared.concordance')}</Link></li>
|
||||
<li className={!isConcordance ? styles.active : null}><img src={!isConcordance ? listActiveIcon : listInactiveIcon} alt={t('shared.list')} /><Link to={`/${search.corpusId}/list`}>{t('shared.list')}</Link></li>
|
||||
</ul>
|
||||
<form onSubmit={onSubmitHandler}>
|
||||
<div className={`${styles.searchInput} ${isHelpVisible || isHistoryVisible ? styles.dropdownActive : null}`}>
|
||||
<button className={styles.searchButton} type='submit'><img src={searchIcon} alt='Search' /></button>
|
||||
<input type='text' autoFocus placeholder={t('shared.enterSearch')} onChange={(e) => setQuery(e.target.value)} />
|
||||
<button className={styles.historyButton} type='button' onClick={onHistoryClickHandler}><img src={historyIcon} alt='History' /></button>
|
||||
<button className={styles.helpButton} type='button' onClick={onHelpClickHandler}><img src={helpIcon} alt='Help' /></button>
|
||||
</div>
|
||||
</form>
|
||||
{isHistoryVisible && <History source={source} />}
|
||||
{isHelpVisible && help}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='container-xl'>
|
||||
<IndexStatistics />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndexLayout;
|
||||
@@ -0,0 +1,97 @@
|
||||
@import '../variables';
|
||||
|
||||
.search {
|
||||
background-color: $primary-color;
|
||||
padding-bottom: 5.6rem;
|
||||
|
||||
h1 {
|
||||
color: #d9d9d9;
|
||||
font-size: 3.6rem;
|
||||
font-weight: 300;
|
||||
line-height: 4.2rem;
|
||||
margin-top: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ul.areas {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 3.5rem 0 0 0;
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
background-color: #eeeeee;
|
||||
display: inline-flex;
|
||||
height: 4.7rem;
|
||||
padding-left: 1.3rem;
|
||||
width: 18.1rem;
|
||||
|
||||
&:first-child {
|
||||
border-radius: 0.6rem 0 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0.6rem 0 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #848c91;
|
||||
padding-left: 1.4rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #f5f5f5;
|
||||
|
||||
a {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 0 0.6rem 0.6rem 0.6rem;
|
||||
box-shadow: 0 0.4rem 1.4rem rgba(0, 0, 0, 0.16);
|
||||
display: flex;
|
||||
height: 8rem;
|
||||
|
||||
&.dropdownActive {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
margin-left: 1.3rem;
|
||||
}
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #848c91;
|
||||
flex-grow: 1;
|
||||
font-size: 2.4rem;
|
||||
line-height: 2.8rem;
|
||||
margin-left: 4.9rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.historyButton {
|
||||
margin-left: 2.1rem;
|
||||
}
|
||||
|
||||
.helpButton {
|
||||
margin: 0 2.8rem 0 2.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSearch from '../hooks/use-search';
|
||||
|
||||
import History from '../components/shared/History';
|
||||
import logo from '../assets/logo-small.png';
|
||||
import styles from './ResultsLayout.module.scss';
|
||||
import searchIcon from '../assets/search.svg';
|
||||
import historyIcon from '../assets/history.svg';
|
||||
import helpIcon from '../assets/help.svg';
|
||||
import concordanceActiveIcon from '../assets/concordance-active.svg';
|
||||
import concordanceInactiveIcon from '../assets/concordance-inactive.svg';
|
||||
import listActiveIcon from '../assets/list-active.svg';
|
||||
import listInactiveIcon from '../assets/list-inactive.svg';
|
||||
|
||||
const ResultsLayout = (props) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const search = useSearch();
|
||||
|
||||
// State
|
||||
const [query, setQuery] = useState(search.getQuery());
|
||||
const [isHelpVisible, setIsHelpVisible] = useState(false);
|
||||
const [isHistoryVisible, setIsHistoryVisible] = useState(false);
|
||||
|
||||
// Props
|
||||
const { source, help } = props;
|
||||
const isConcordance = source === 'concordance';
|
||||
|
||||
const onSubmitHandler = (e) => {
|
||||
e.preventDefault();
|
||||
if (query) {
|
||||
search.newSearch(source, query);
|
||||
}
|
||||
};
|
||||
|
||||
const onHistoryClickHandler = () => {
|
||||
setIsHistoryVisible(isHistoryVisibleOld => !isHistoryVisibleOld);
|
||||
}
|
||||
|
||||
const onHelpClickHandler = () => {
|
||||
setIsHelpVisible(isHelpVisibleOld => !isHelpVisibleOld);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.menu}>
|
||||
<div className='container-fluid'>
|
||||
<div className='row align-items-center'>
|
||||
<div className='col-sm-4 col-lg-2 col-xl-2'>
|
||||
<Link to={search.rootUrl}><img src={logo} alt='Logo' /></Link>
|
||||
<ul className={styles.areaButtons}>
|
||||
<li className={`${styles.areaButton} ${isConcordance ? styles.active : null}`}><Link to={`/${search.corpusId}/concordance`}><img src={isConcordance ? concordanceActiveIcon : concordanceInactiveIcon} alt='Concordance' /></Link></li>
|
||||
<li className={`${styles.areaButton} ${!isConcordance ? styles.active : null}`}><Link to={`/${search.corpusId}/list`}><img src={!isConcordance ? listActiveIcon : listInactiveIcon} alt='List' /></Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='col-sm-8 col-lg-6 col-xl-4'>
|
||||
<form onSubmit={onSubmitHandler}>
|
||||
<div className={`${styles.searchInput} ${isHelpVisible || isHistoryVisible ? styles.dropdownActive : null}`}>
|
||||
<button className={styles.searchButton} type='submit'><img src={searchIcon} alt='Search' /></button>
|
||||
<input type='text' placeholder='Vpišite iskalni niz' value={query} onChange={(e) => setQuery(e.target.value)} />
|
||||
<button className={styles.historyButton} type='button' onClick={onHistoryClickHandler}><img src={historyIcon} alt='History' /></button>
|
||||
<button className={styles.helpButton} type='button' onClick={onHelpClickHandler}><img src={helpIcon} alt='Help' /></button>
|
||||
</div>
|
||||
</form>
|
||||
{isHistoryVisible && <History source={source} />}
|
||||
{isHelpVisible && help}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.results}>
|
||||
<div className='container-fluid'>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<div className='container-fluid'>
|
||||
<div className='row align-items-center'>
|
||||
<div className='col'>© Amebis, d. o. o., Kamnik</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResultsLayout;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
@import '../variables';
|
||||
|
||||
.menu {
|
||||
background-color: $primary-color;
|
||||
height: 6.4rem;
|
||||
|
||||
:global(.container-fluid),
|
||||
:global(.row) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ul.areaButtons {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
float: right;
|
||||
|
||||
li.areaButton {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
background-color: #ffffff;
|
||||
border-radius: 0.8rem;
|
||||
height: 4.6rem;
|
||||
justify-content: center;
|
||||
margin-right: 0.4rem;
|
||||
width: 4.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 0.6rem 0.6rem 0.6rem 0.6rem;
|
||||
display: flex;
|
||||
height: 4.6rem;
|
||||
|
||||
&.dropdownActive {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
margin-left: 2.5rem;
|
||||
}
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #3a464e;
|
||||
flex-grow: 1;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.9rem;
|
||||
margin-left: 2.6rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.historyButton {
|
||||
margin-left: 1.9rem;
|
||||
}
|
||||
|
||||
.helpButton {
|
||||
margin: 0 1.8rem 0 1.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
padding-top: 4.6rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #d9d9d9;
|
||||
height: 4rem;
|
||||
margin-top: 1.6rem;
|
||||
|
||||
:global(.container-fluid),
|
||||
:global(.row) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const NoCorpus = () => {
|
||||
return (
|
||||
<div>
|
||||
No corpus selected!
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoCorpus;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import IndexLayout from '../../layout/IndexLayout';
|
||||
import ConcordanceHelp from '../../components/concordance/ConcordanceHelp';
|
||||
|
||||
const ConcordanceIndex = () => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IndexLayout source='concordance' title={t('concordance.title')} help={<ConcordanceHelp />} />
|
||||
);
|
||||
}
|
||||
|
||||
export default ConcordanceIndex;
|
||||
@@ -0,0 +1,87 @@
|
||||
import ConcordanceAggregations from '../../components/concordance/ConcordanceAggregations';
|
||||
import ConcordanceList from '../../components/concordance/ConcordanceList';
|
||||
import ConcordanceHelp from '../../components/concordance/ConcordanceHelp';
|
||||
import ResultsHeader from '../../components/results/ResultsHeader';
|
||||
import ResultsPager from '../../components/results/ResultsPager';
|
||||
import ResultsExport from '../../components/results/ResultsExport';
|
||||
import useHistory from '../../hooks/use-history';
|
||||
import useSearch from '../../hooks/use-search';
|
||||
import useDownload from '../../hooks/use-download';
|
||||
import ResultsLayout from '../../layout/ResultsLayout';
|
||||
import Spinner from '../../components/shared/Spinner';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ConcordanceResults = () => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const search = useSearch();
|
||||
const history = useHistory();
|
||||
|
||||
const body = search.getSearchRequest();
|
||||
|
||||
const fetchConcordances = () => fetch(`${window.baseAppPath}/concordancer/corpus/${search.corpusId}/concordances/search`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then((res) => {
|
||||
history.saveSearch('concordance');
|
||||
return res.json();
|
||||
});
|
||||
|
||||
const { isLoading, isSuccess, isError, data, error } = useQuery(['concordances', body], fetchConcordances, { keepPreviousData: true, refetchOnWindowFocus: false });
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const download = useDownload(`${window.baseAppPath}/concordancer/corpus/${search.corpusId}/concordances/export`);
|
||||
|
||||
const onExportClose = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const onExportConfirm = (type, rows) => {
|
||||
setIsVisible(false);
|
||||
body.type = type;
|
||||
body.rows = rows;
|
||||
download.start(body);
|
||||
};
|
||||
|
||||
if (!isLoading && isSuccess && data.items.length > 0) {
|
||||
return (
|
||||
<ResultsLayout source='concordance' help={<ConcordanceHelp />}>
|
||||
<div className='row'>
|
||||
<div className='col-lg-2'>
|
||||
<ConcordanceAggregations aggregations={data.aggregations} lemmasAlternateSearch={data.lemmasAlternateSearch} />
|
||||
</div>
|
||||
<div className='col-lg-10'>
|
||||
<ResultsHeader query={body.query} onExport={() => setIsVisible(prevIsVisible => !prevIsVisible)} />
|
||||
<ResultsPager total={data.total} offset={data.offset} />
|
||||
{isVisible && <ResultsExport onClose={onExportClose} onConfirm={onExportConfirm} results={data.total} />}
|
||||
<ConcordanceList items={data.items} />
|
||||
</div>
|
||||
</div>
|
||||
</ResultsLayout>
|
||||
);
|
||||
} else {
|
||||
let content = <></>;
|
||||
if (isLoading) {
|
||||
content = <Spinner></Spinner>
|
||||
} else if (isError) {
|
||||
content = <p>{t('shared.searchError')}</p>
|
||||
} else if (!isLoading && data && data.items.length === 0) {
|
||||
content = <p>{t('shared.noResults')}</p>
|
||||
}
|
||||
return (
|
||||
<ResultsLayout source='concordance'>
|
||||
<div className='row'>
|
||||
<div className='col-lg-10 offset-lg-2'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</ResultsLayout>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ConcordanceResults;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import IndexLayout from '../../layout/IndexLayout';
|
||||
import ListHelp from '../../components/list/ListHelp';
|
||||
|
||||
const ListIndex = () => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IndexLayout source='list' title={t('list.title')} help={<ListHelp />} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ListIndex;
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useQuery } from 'react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
|
||||
import useSearch from '../../hooks/use-search';
|
||||
import useDownload from '../../hooks/use-download';
|
||||
import useHistory from '../../hooks/use-history';
|
||||
|
||||
import ResultsLayout from '../../layout/ResultsLayout';
|
||||
import ResultsHeader from '../../components/results/ResultsHeader';
|
||||
import ResultsPager from '../../components/results/ResultsPager';
|
||||
import ResultsExport from '../../components/results/ResultsExport';
|
||||
import ListItems from '../../components/list/ListItems';
|
||||
import Spinner from '../../components/shared/Spinner';
|
||||
|
||||
const ListResults = () => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const search = useSearch();
|
||||
const history = useHistory();
|
||||
const body = search.getSearchRequest();
|
||||
|
||||
const fetchList = () => fetch(`${window.baseAppPath}/concordancer/corpus/${search.corpusId}/list/search`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then((res) => {
|
||||
history.saveSearch('list');
|
||||
return res.json();
|
||||
});
|
||||
|
||||
const { isLoading, isSuccess, isError, data, error } = useQuery(['list', body], fetchList, { keepPreviousData: true, refetchOnWindowFocus: false });
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const download = useDownload(`${window.baseAppPath}/concordancer/corpus/${search.corpusId}/list/export`);
|
||||
|
||||
const onExportClose = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const onExportConfirm = (type, rows) => {
|
||||
setIsVisible(false);
|
||||
body.type = type;
|
||||
body.rows = rows;
|
||||
download.start(body);
|
||||
};
|
||||
|
||||
if (!isLoading && isSuccess && data.items.length > 0) {
|
||||
return (
|
||||
<ResultsLayout source='list'>
|
||||
<div className='row'>
|
||||
<div className='col'>
|
||||
<ResultsHeader query={body.query} onExport={() => setIsVisible(prevIsVisible => !prevIsVisible)} />
|
||||
<ResultsPager total={data.total} offset={data.offset} />
|
||||
{isVisible && <ResultsExport onClose={onExportClose} onConfirm={onExportConfirm} results={data.total} />}
|
||||
<ListItems items={data.items} />
|
||||
</div>
|
||||
</div>
|
||||
</ResultsLayout>
|
||||
);
|
||||
} else {
|
||||
let content = <></>;
|
||||
if (isLoading) {
|
||||
content = <Spinner></Spinner>
|
||||
} else if (isError) {
|
||||
content = <p>{t('shared.searchError')}</p>
|
||||
} else if (!isLoading && data && data.items.length === 0) {
|
||||
content = <p>{t('shared.noResults')}</p>
|
||||
}
|
||||
return (
|
||||
<ResultsLayout source='list'>
|
||||
<div className='row'>
|
||||
<div className='col-lg-10 offset-lg-2'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</ResultsLayout>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ListResults;
|
||||
@@ -0,0 +1,32 @@
|
||||
const response = {
|
||||
items: [
|
||||
{ term: 'cemènt', lemma: 'cemènt', frequency: 9023 },
|
||||
{ term: 'aluminátni cemènt', lemma: 'aluminátni cemènt', frequency: 7646 },
|
||||
{ term: 'béli cemènt', lemma: 'béli cemènt', frequency: 3183 },
|
||||
{ term: 'cemènt visôke zgódnje tŕdnosti', lemma: 'cemènt visôke zgódnje tŕdnosti', frequency: 5096 },
|
||||
{ term: 'cemènt z nízko hidratácijsko toplôto', lemma: 'cemènt z nízko hidratácijsko toplôto', frequency: 8468 },
|
||||
{ term: 'ekspanzívni cemènt', lemma: 'ekspanzívni cemènt', frequency: 1525 },
|
||||
{ term: 'hidrávlični cemènt', lemma: 'hidrávlični cemènt', frequency: 9036 },
|
||||
{ term: 'hidrofóbni cemènt', lemma: 'hidrofóbni cemènt', frequency: 7425 },
|
||||
{ term: 'hitrovézni cemènt', lemma: 'hitrovézni cemènt', frequency: 8029 },
|
||||
{ term: 'hitrovezóči cemènt', lemma: 'hitrovezóči cemènt', frequency: 4772 },
|
||||
{ term: 'méšani cemènt', lemma: 'méšani cemènt', frequency: 3233 },
|
||||
{ term: 'méšani pórtlandski cemènt', lemma: 'méšani pórtlandski cemènt', frequency: 9402 },
|
||||
{ term: 'metalúrški cemènt', lemma: 'metalúrški cemènt', frequency: 1249 },
|
||||
{ term: 'navádni pórtlandski cemènt', lemma: 'navádni pórtlandski cemènt', frequency: 8574 },
|
||||
{ term: 'normálni cemènt', lemma: 'normálni cemènt', frequency: 1501 },
|
||||
{ term: 'normálnovezóči cemènt', lemma: 'normálnovezóči cemènt', frequency: 7748 },
|
||||
{ term: 'počásivezóči cemènt', lemma: 'počásivezóči cemènt', frequency: 1992 },
|
||||
{ term: 'pórtland cemènt', lemma: 'pórtland cemènt', frequency: 7025 },
|
||||
{ term: 'pórtlandski cemènt', lemma: 'pórtlandski cemènt', frequency: 7975 },
|
||||
{ term: 'pucolánski cemènt', lemma: 'pucolánski cemènt', frequency: 5747 },
|
||||
{ term: 'sulfátnoodpórni cemènt', lemma: 'sulfátnoodpórni cemènt', frequency: 4074 },
|
||||
{ term: 'súpersulfátni cemènt', lemma: 'súpersulfátni cemènt', frequency: 1389 },
|
||||
{ term: 'vodoodpórni cemènt', lemma: 'vodoodpórni cemènt', frequency: 1986 },
|
||||
{ term: 'žlíndrin cemènt', lemma: 'žlíndrin cemènt', frequency: 6759 },
|
||||
],
|
||||
total: 24,
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
export default response;
|
||||
@@ -0,0 +1,5 @@
|
||||
$primary-color: #006cb7;
|
||||
$border-color: #d9d9d9;
|
||||
|
||||
$text-color: #46535b;
|
||||
$text-color-secondary: #848c90;
|
||||