Compare commits
83 Commits
feature/co
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e9e9611f80 | ||
0c054687ba | |||
3b147e50a1 | |||
![]() |
e94514ed02 | ||
![]() |
0032e51858 | ||
8d6a9ab74c | |||
07c4ae8cbd | |||
a782930d3d | |||
4c1877caa5 | |||
e9324f6c14 | |||
![]() |
9fcee44e0c | ||
![]() |
9939b1c824 | ||
00deb05850 | |||
b8849945b9 | |||
08f5ee272b | |||
35aa957b35 | |||
472e8c2ea5 | |||
f17d5f3079 | |||
![]() |
4f9b33e5b9 | ||
![]() |
753223c7a9 | ||
![]() |
28cc9b1a71 | ||
![]() |
47472868e1 | ||
![]() |
f965eaddbc | ||
![]() |
2075426ff7 | ||
![]() |
209507833e | ||
eff20ae459 | |||
dcfe260f0e | |||
1af206c360 | |||
![]() |
932d23befa | ||
![]() |
e52026cf86 | ||
033a1f31ad | |||
9c79f449c5 | |||
24652d0908 | |||
944baced6c | |||
![]() |
01a873477b | ||
![]() |
d1b17592c3 | ||
1475d2db27 | |||
1ba9dc28b5 | |||
1817946fe6 | |||
de5b6ad60c | |||
![]() |
31706ca4e8 | ||
![]() |
969749c0c0 | ||
![]() |
5667f30a92 | ||
![]() |
d77c1c6388 | ||
![]() |
bd6fbbebfd | ||
![]() |
2ffce71e74 | ||
35900bec5d | |||
![]() |
c63c19e1c7 | ||
![]() |
b5443aa033 | ||
![]() |
18d02945f3 | ||
558a1fd5a7 | |||
![]() |
bee5499816 | ||
8a2a7f0c53 | |||
15b9ed1f20 | |||
cad2174bc5 | |||
0c0a79d81d | |||
df4b4d5c32 | |||
92dddab82a | |||
93cefd9ce3 | |||
373d55a543 | |||
![]() |
8a075e926e | ||
![]() |
705ae6471f | ||
![]() |
64a6e6ab20 | ||
![]() |
3cac7c7d04 | ||
![]() |
9154b1fb96 | ||
![]() |
b390ae7a2e | ||
![]() |
3c739247d2 | ||
904730dfd9 | |||
dcda165488 | |||
49d15e538b | |||
![]() |
da389cc337 | ||
3f9a76815e | |||
c99e9217f1 | |||
4479300027 | |||
e97d3abe96 | |||
![]() |
eff1a2a04f | ||
81faa7121e | |||
bbe6bcca51 | |||
c161606e08 | |||
5550b7c241 | |||
![]() |
779b091bd5 | ||
![]() |
a26faeeb02 | ||
50a42955d6 |
27
Dockerfile
27
Dockerfile
@ -17,22 +17,25 @@ COPY . .
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Bundle static assets with nginx
|
# Bundle static assets with nginx
|
||||||
FROM nginx:1.21.6 as production
|
FROM node:16-alpine as production
|
||||||
# Copy built assets from builder
|
# Copy built assets from builder
|
||||||
COPY --from=builder /app/build /usr/share/nginx/html
|
WORKDIR /app
|
||||||
# Add nginx.config
|
COPY --from=builder /app/build .
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
# Expose ports
|
# Expose ports
|
||||||
EXPOSE 80
|
EXPOSE 3000
|
||||||
|
|
||||||
COPY entrypoint.sh .
|
|
||||||
COPY .env.production .
|
COPY .env.production .
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
|
ENV USER_NAME=node_user USER_UID=2000 GROUP_NAME=node_group GROUP_UID=2000
|
||||||
|
|
||||||
# Execute script
|
RUN npm i -g serve \
|
||||||
RUN ["chmod", "+x", "./entrypoint.sh"]
|
&& deluser --remove-home node \
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
&& addgroup --g ${GROUP_UID} -S ${GROUP_NAME} \
|
||||||
|
&& adduser -D -S -s /sbin/nologin -u ${USER_UID} -G ${GROUP_NAME} ${USER_NAME}\
|
||||||
|
&& chown -R ${USER_NAME}:${GROUP_NAME} "/app/"
|
||||||
|
|
||||||
# Start serving
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
USER "${USER_NAME}"
|
||||||
|
CMD serve -s .
|
259
package-lock.json
generated
259
package-lock.json
generated
@ -8,10 +8,15 @@
|
|||||||
"name": "freeland",
|
"name": "freeland",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.2.0",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^6.2.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.6.6",
|
"@headlessui/react": "^1.6.6",
|
||||||
"@reduxjs/toolkit": "^1.8.3",
|
"@reduxjs/toolkit": "^1.8.3",
|
||||||
"@types/node": "^16.11.47",
|
"@types/node": "^16.11.47",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
|
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@uiw/react-md-editor": "^3.18.1",
|
"@uiw/react-md-editor": "^3.18.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
@ -25,10 +30,12 @@
|
|||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hotkeys": "^2.0.0",
|
"react-hotkeys": "^2.0.0",
|
||||||
"react-i18next": "^11.18.3",
|
"react-i18next": "^11.18.3",
|
||||||
"react-loading-skeleton": "^3.1.0",
|
"react-loading-skeleton": "^3.1.0",
|
||||||
|
"react-lottie": "^1.2.3",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
@ -63,6 +70,7 @@
|
|||||||
"@testing-library/react": "^13.3.0",
|
"@testing-library/react": "^13.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
|
"@types/react-lottie": "^1.2.6",
|
||||||
"@types/react-syntax-highlighter": "^15.5.5",
|
"@types/react-syntax-highlighter": "^15.5.5",
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.8",
|
||||||
"babel-plugin-named-exports-order": "^0.0.2",
|
"babel-plugin-named-exports-order": "^0.0.2",
|
||||||
@ -2382,6 +2390,63 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-rBevIsj2nclStJ7AxTdfsa3ovHb1H+qApwrxcTVo+NNdeJiB9V75hsKfrkG5AwNcRUNxrPPiScGYCNmLMoh8pg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cf2mAAeMWFMzpLC7Y9H1I4o3wEU+XovVJhTiNG8ZNgSQj53yl7OCJaS80K4YjrABWZzbAHVaoHE1dVJ27AAYXw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-fm1y4NyZ2qKYNmYhdMz9VAWRw1Et7PMHNunSw3W0SVAwKwv6o0qiJworLH3Y9SnmhHzAymXJwCX1op22FFvGiA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-UjCILHIQ4I8cN46EiQn0CZL/h8AwCGgR//1c4R96Q5viSRwuKVo0NdQEc4bm+69ZwC0dUvjbDqAHF1RR5FA3XA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/react-fontawesome": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||||
|
"react": ">=16.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@gar/promisify": {
|
"node_modules/@gar/promisify": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
||||||
@ -11479,6 +11544,14 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-copy-to-clipboard": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-otTJsJpofYAeaIeOwV5xBUGpo6exXG2HX7X4nseToCB2VgPEBxGBHCm/FecZ676doNR7HCSTVtmohxfG2b3/yQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-dom": {
|
"node_modules/@types/react-dom": {
|
||||||
"version": "18.0.6",
|
"version": "18.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz",
|
||||||
@ -11487,6 +11560,15 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-lottie": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-lottie/-/react-lottie-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-fvGJHD7SeUdVESHo7f7erRnXkTWaa/6Mo5TB+R0/ieSftKoFspA4sMlF2qMH6BljXI7ehFJbBtrD5bzDxPCkGg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-syntax-highlighter": {
|
"node_modules/@types/react-syntax-highlighter": {
|
||||||
"version": "15.5.5",
|
"version": "15.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.5.tgz",
|
||||||
@ -13529,6 +13611,27 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/babel-runtime": {
|
||||||
|
"version": "6.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||||
|
"integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
|
||||||
|
"dependencies": {
|
||||||
|
"core-js": "^2.4.0",
|
||||||
|
"regenerator-runtime": "^0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/babel-runtime/node_modules/core-js": {
|
||||||
|
"version": "2.6.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
|
||||||
|
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
|
||||||
|
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
|
||||||
|
"hasInstallScript": true
|
||||||
|
},
|
||||||
|
"node_modules/babel-runtime/node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||||
|
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||||
|
},
|
||||||
"node_modules/bail": {
|
"node_modules/bail": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
|
||||||
@ -15021,6 +15124,14 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/copy-to-clipboard": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==",
|
||||||
|
"dependencies": {
|
||||||
|
"toggle-selection": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/core-js": {
|
"node_modules/core-js": {
|
||||||
"version": "3.24.1",
|
"version": "3.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
|
||||||
@ -25567,6 +25678,11 @@
|
|||||||
"loose-envify": "cli.js"
|
"loose-envify": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lottie-web": {
|
||||||
|
"version": "5.9.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.9.6.tgz",
|
||||||
|
"integrity": "sha512-JFs7KsHwflugH5qIXBpB4905yC1Sub2MZWtl/elvO/QC6qj1ApqbUZJyjzJseJUtVpgiDaXQLjBlIJGS7UUUXA=="
|
||||||
|
},
|
||||||
"node_modules/loud-rejection": {
|
"node_modules/loud-rejection": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
|
||||||
@ -30025,6 +30141,18 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-copy-to-clipboard": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
|
||||||
|
"dependencies": {
|
||||||
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^15.3.0 || 16 || 17 || 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dev-utils": {
|
"node_modules/react-dev-utils": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
||||||
@ -30292,6 +30420,21 @@
|
|||||||
"react": ">=16.8.0"
|
"react": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-lottie": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-lottie/-/react-lottie-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-qLCERxUr8M+4mm1LU0Ruxw5Y5Fn/OmYkGfnA+JDM/dZb3oKwVAJCjwnjkj9TMHtzR2U6sMEUD3ZZ1RaHagM7kA==",
|
||||||
|
"dependencies": {
|
||||||
|
"babel-runtime": "^6.26.0",
|
||||||
|
"lottie-web": "^5.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"npm": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^0.14.7 || ^15.0.0 || ^16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-markdown": {
|
"node_modules/react-markdown": {
|
||||||
"version": "8.0.3",
|
"version": "8.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.3.tgz",
|
||||||
@ -37574,6 +37717,11 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toggle-selection": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
|
||||||
|
},
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
@ -41454,6 +41602,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@fortawesome/fontawesome-common-types": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-rBevIsj2nclStJ7AxTdfsa3ovHb1H+qApwrxcTVo+NNdeJiB9V75hsKfrkG5AwNcRUNxrPPiScGYCNmLMoh8pg=="
|
||||||
|
},
|
||||||
|
"@fortawesome/fontawesome-svg-core": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cf2mAAeMWFMzpLC7Y9H1I4o3wEU+XovVJhTiNG8ZNgSQj53yl7OCJaS80K4YjrABWZzbAHVaoHE1dVJ27AAYXw==",
|
||||||
|
"requires": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@fortawesome/free-brands-svg-icons": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-fm1y4NyZ2qKYNmYhdMz9VAWRw1Et7PMHNunSw3W0SVAwKwv6o0qiJworLH3Y9SnmhHzAymXJwCX1op22FFvGiA==",
|
||||||
|
"requires": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@fortawesome/free-solid-svg-icons": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-UjCILHIQ4I8cN46EiQn0CZL/h8AwCGgR//1c4R96Q5viSRwuKVo0NdQEc4bm+69ZwC0dUvjbDqAHF1RR5FA3XA==",
|
||||||
|
"requires": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@fortawesome/react-fontawesome": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@gar/promisify": {
|
"@gar/promisify": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
||||||
@ -48448,6 +48633,14 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-copy-to-clipboard": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-otTJsJpofYAeaIeOwV5xBUGpo6exXG2HX7X4nseToCB2VgPEBxGBHCm/FecZ676doNR7HCSTVtmohxfG2b3/yQ==",
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-dom": {
|
"@types/react-dom": {
|
||||||
"version": "18.0.6",
|
"version": "18.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz",
|
||||||
@ -48456,6 +48649,15 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-lottie": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-lottie/-/react-lottie-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-fvGJHD7SeUdVESHo7f7erRnXkTWaa/6Mo5TB+R0/ieSftKoFspA4sMlF2qMH6BljXI7ehFJbBtrD5bzDxPCkGg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-syntax-highlighter": {
|
"@types/react-syntax-highlighter": {
|
||||||
"version": "15.5.5",
|
"version": "15.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.5.tgz",
|
||||||
@ -50064,6 +50266,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"babel-runtime": {
|
||||||
|
"version": "6.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||||
|
"integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
|
||||||
|
"requires": {
|
||||||
|
"core-js": "^2.4.0",
|
||||||
|
"regenerator-runtime": "^0.11.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"core-js": {
|
||||||
|
"version": "2.6.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
|
||||||
|
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="
|
||||||
|
},
|
||||||
|
"regenerator-runtime": {
|
||||||
|
"version": "0.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||||
|
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"bail": {
|
"bail": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
|
||||||
@ -51233,6 +51456,14 @@
|
|||||||
"integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==",
|
"integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"copy-to-clipboard": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==",
|
||||||
|
"requires": {
|
||||||
|
"toggle-selection": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"core-js": {
|
"core-js": {
|
||||||
"version": "3.24.1",
|
"version": "3.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
|
||||||
@ -59181,6 +59412,11 @@
|
|||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lottie-web": {
|
||||||
|
"version": "5.9.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.9.6.tgz",
|
||||||
|
"integrity": "sha512-JFs7KsHwflugH5qIXBpB4905yC1Sub2MZWtl/elvO/QC6qj1ApqbUZJyjzJseJUtVpgiDaXQLjBlIJGS7UUUXA=="
|
||||||
|
},
|
||||||
"loud-rejection": {
|
"loud-rejection": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
|
||||||
@ -62246,6 +62482,15 @@
|
|||||||
"whatwg-fetch": "^3.6.2"
|
"whatwg-fetch": "^3.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-copy-to-clipboard": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
|
||||||
|
"requires": {
|
||||||
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-dev-utils": {
|
"react-dev-utils": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
||||||
@ -62445,6 +62690,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.1.0.tgz",
|
||||||
"integrity": "sha512-j1U1CWWs68nBPOg7tkQqnlFcAMFF6oEK6MgqAo15f8A5p7mjH6xyKn2gHbkcimpwfO0VQXqxAswnSYVr8lWzjw=="
|
"integrity": "sha512-j1U1CWWs68nBPOg7tkQqnlFcAMFF6oEK6MgqAo15f8A5p7mjH6xyKn2gHbkcimpwfO0VQXqxAswnSYVr8lWzjw=="
|
||||||
},
|
},
|
||||||
|
"react-lottie": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-lottie/-/react-lottie-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-qLCERxUr8M+4mm1LU0Ruxw5Y5Fn/OmYkGfnA+JDM/dZb3oKwVAJCjwnjkj9TMHtzR2U6sMEUD3ZZ1RaHagM7kA==",
|
||||||
|
"requires": {
|
||||||
|
"babel-runtime": "^6.26.0",
|
||||||
|
"lottie-web": "^5.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-markdown": {
|
"react-markdown": {
|
||||||
"version": "8.0.3",
|
"version": "8.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.3.tgz",
|
||||||
@ -67797,6 +68051,11 @@
|
|||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"toggle-selection": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
|
||||||
|
},
|
||||||
"toidentifier": {
|
"toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
@ -3,10 +3,15 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.2.0",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^6.2.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.6.6",
|
"@headlessui/react": "^1.6.6",
|
||||||
"@reduxjs/toolkit": "^1.8.3",
|
"@reduxjs/toolkit": "^1.8.3",
|
||||||
"@types/node": "^16.11.47",
|
"@types/node": "^16.11.47",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
|
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@uiw/react-md-editor": "^3.18.1",
|
"@uiw/react-md-editor": "^3.18.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
@ -20,10 +25,12 @@
|
|||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hotkeys": "^2.0.0",
|
"react-hotkeys": "^2.0.0",
|
||||||
"react-i18next": "^11.18.3",
|
"react-i18next": "^11.18.3",
|
||||||
"react-loading-skeleton": "^3.1.0",
|
"react-loading-skeleton": "^3.1.0",
|
||||||
|
"react-lottie": "^1.2.3",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
@ -103,6 +110,7 @@
|
|||||||
"@testing-library/react": "^13.3.0",
|
"@testing-library/react": "^13.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
|
"@types/react-lottie": "^1.2.6",
|
||||||
"@types/react-syntax-highlighter": "^15.5.5",
|
"@types/react-syntax-highlighter": "^15.5.5",
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.8",
|
||||||
"babel-plugin-named-exports-order": "^0.0.2",
|
"babel-plugin-named-exports-order": "^0.0.2",
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
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`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>Freeland</title>
|
<title>Scipaper</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"serv": {
|
"serv": {
|
||||||
"goHome": "Home",
|
"goHome": "Home page",
|
||||||
"noSuchPath": "We don't have this page"
|
"noSuchPath": "We don't have such a page"
|
||||||
},
|
},
|
||||||
"sidemenu": {
|
"sidemenu": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
@ -19,45 +19,130 @@
|
|||||||
"hellousr": "Hello, {{username}}",
|
"hellousr": "Hello, {{username}}",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"selectLanguage": "Select language",
|
"selectLanguage": "Select a language",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"account": {
|
"account": {
|
||||||
"info": "Personal information",
|
"info": "Personal Information",
|
||||||
"mail": "Mail",
|
"mail": "Mail",
|
||||||
"connect": "Add account",
|
"connect": "Add Account",
|
||||||
"connectedAccounts_one": "Connected account",
|
"connectedAccounts_one": "Linked Account",
|
||||||
"connectedAccounts_other": "Connected accounts",
|
"connectedAccounts_other": "Linked Accounts",
|
||||||
"settings": "Account settings"
|
"settings": "Account Settings"
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"password": {
|
"password": {
|
||||||
"caption": "Password",
|
"caption": "Password",
|
||||||
"twoFactor": "Two factor authentication (2FA)",
|
"twoFactor": "Two-factor authentication (2FA)",
|
||||||
"description": "Keep your account secure by enabling 2FA via SMS or using a temporary one-time passcode (TOTP) from an authenticator app."
|
"description": "Protect your account by enabling 2FA via SMS or using a temporary one-time password (OTP) from the authentication app."
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"caption": "Device activity"
|
"caption": "Device activity"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"label": "Search for something.."
|
"label": "We will find something.."
|
||||||
},
|
},
|
||||||
"subscriptions": {
|
"subscriptions": {
|
||||||
"subscribed": "Service have been connected"
|
"subscribed": "The service is attached to the account"
|
||||||
},
|
},
|
||||||
"viewHistory": "View history",
|
"viewHistory": "View history",
|
||||||
"logOutEverywhere": "log out from all devices",
|
"logOutEverywhere": "Log out from all devices",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"logOut": "Log out",
|
"logOut": "Exit",
|
||||||
"failures": {
|
"failures": {
|
||||||
"subscriptions": {
|
"subscriptions": {
|
||||||
"failure": "Failed to connect service",
|
"failure": "Failed to attach the service to your account",
|
||||||
"exists": "Service have already been connected",
|
"exists": "The service was already attached to your account earlier",
|
||||||
"confirmation": "Invalid confirmation information provided"
|
"confirmation": "Invalid password"
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"fork": "Failed to authenticate in service"
|
"fork": "Failed to perform authorization in the service"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"articlePage": {
|
||||||
|
"abstract": "Abstract",
|
||||||
|
"keywords": "Keywords",
|
||||||
|
"interactionButtons":{
|
||||||
|
"abstract": "Abstract",
|
||||||
|
"readFile": "Read File",
|
||||||
|
"download" : "Download",
|
||||||
|
"share" : "Share",
|
||||||
|
"cite" : "Cite",
|
||||||
|
"copied": "Copied"
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"createNew": "Create New",
|
||||||
|
"about": {
|
||||||
|
"navTitle": "About",
|
||||||
|
"aboutProject": "About Scipaper",
|
||||||
|
"contacts": "Contacts",
|
||||||
|
"help": "Help"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"navTitle": "My library",
|
||||||
|
"publications": "Publications",
|
||||||
|
"favorites": "Favorites",
|
||||||
|
"collections": "Collections",
|
||||||
|
"recentViewed": "History"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"signIn": "Sign In",
|
||||||
|
"signUp": "Sign Up"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"accountSettings": "Account Settings",
|
||||||
|
"about": "About Scipaper",
|
||||||
|
"help": "Help",
|
||||||
|
"contactUs": "Contacts",
|
||||||
|
"allRightsReserved": "All rights reserved",
|
||||||
|
"termsOfUse": "Terms of Use",
|
||||||
|
"privacyPolicy": "Privacy Policy",
|
||||||
|
"coockiesPolicy": "coockies Usage Policy",
|
||||||
|
"supportedBy": "Created"
|
||||||
|
},
|
||||||
|
"mainPage": {
|
||||||
|
"title": "Scientific Library with Free Access",
|
||||||
|
"search": "Search",
|
||||||
|
"article_one": "Articles",
|
||||||
|
"article_few": "Articles",
|
||||||
|
"article_many": "Articles",
|
||||||
|
"advancedSearch": "Advanced search",
|
||||||
|
"featuredArticles": {
|
||||||
|
"title": "Featured articles",
|
||||||
|
"descriptionPart1": "Select the category of science you are interested in",
|
||||||
|
"descriptionPart2": "Scientific category",
|
||||||
|
"categories": {
|
||||||
|
"Medical": "Medical",
|
||||||
|
"TechnicsAndTechlonogies": "Technics and Technology",
|
||||||
|
"Fundamental": "Fundamental",
|
||||||
|
"Humanitarian": "Humanitarian",
|
||||||
|
"Agricultural": "Agricultural",
|
||||||
|
"Social": "Social"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"featuredAuthors": "Featured authors",
|
||||||
|
"more": "See More ",
|
||||||
|
"showAll": "Show all"
|
||||||
|
},
|
||||||
|
"searchResults": {
|
||||||
|
"title": "Search results",
|
||||||
|
"totalResults":"Total results",
|
||||||
|
"nothingFound": "Nothing found"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"authors":"Authors",
|
||||||
|
"publicationsType": "Publications Type",
|
||||||
|
"content":"Content",
|
||||||
|
"publisher":"Publisher",
|
||||||
|
"publicationTopic":"Publication Topic",
|
||||||
|
"appliedFitlers":"Applied Fitlers",
|
||||||
|
"clearAll":"Clear All",
|
||||||
|
"enterAuthorsName":"Enter Author Name",
|
||||||
|
"showAll":"Show All"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -59,5 +59,87 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"fork": "Не удалось выполнить авторизацию в сервисе"
|
"fork": "Не удалось выполнить авторизацию в сервисе"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"articlePage": {
|
||||||
|
"abstract": "Введение",
|
||||||
|
"keywords": "Ключевые слова",
|
||||||
|
"interactionButtons":{
|
||||||
|
"abstract": "Развернуть",
|
||||||
|
"readFile": "Читать",
|
||||||
|
"download" : "Скачать",
|
||||||
|
"share" : "Поделиться",
|
||||||
|
"cite" : "Цитировать",
|
||||||
|
"copied": "Скопировано"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"createNew": "Создать статью",
|
||||||
|
"about": {
|
||||||
|
"navTitle": "О проекте",
|
||||||
|
"aboutProject": "О Scipaper",
|
||||||
|
"contacts": "Контакты",
|
||||||
|
"help": "Помощь"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"navTitle": "Моя библиотека",
|
||||||
|
"publications": "Публикации",
|
||||||
|
"favorites": "Избранное",
|
||||||
|
"collections": "Коллекции",
|
||||||
|
"recentViewed": "История"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"signIn": "Вход",
|
||||||
|
"signUp": "Регистрация"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"accountSettings": "Настройки аккаунта",
|
||||||
|
"about": "О scipaper",
|
||||||
|
"help": "Помощь",
|
||||||
|
"contactUs": "Контакты",
|
||||||
|
"allRightsReserved": "Все права защищены",
|
||||||
|
"termsOfUse": "Правила использования",
|
||||||
|
"privacyPolicy": "Политика конфиденциальности",
|
||||||
|
"coockiesPolicy": "Политика использования coockies",
|
||||||
|
"supportedBy": "Создано"
|
||||||
|
},
|
||||||
|
"mainPage": {
|
||||||
|
"title": "Библиотека научных статей с бесплатным доступом",
|
||||||
|
"search": "Поиск",
|
||||||
|
"article_one": "статьи",
|
||||||
|
"article_few": "статей",
|
||||||
|
"article_many": "статей",
|
||||||
|
"advancedSearch": "Расширенный поиск",
|
||||||
|
"featuredArticles": {
|
||||||
|
"title": "Популярные статьи",
|
||||||
|
"descriptionPart1": "Выберете интересующую вас ",
|
||||||
|
"descriptionPart2": "научную категорию",
|
||||||
|
"categories": {
|
||||||
|
"Medical": "Медицина",
|
||||||
|
"TechnicsAndTechlonogies": "Техника и технологии",
|
||||||
|
"Fundamental": "Естественые",
|
||||||
|
"Humanitarian": "Гуманитарные",
|
||||||
|
"Agricultural": "Аuрокультурa",
|
||||||
|
"Social": "Социальные"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"featuredAuthors": "Популярные авторы",
|
||||||
|
"more": "Больше",
|
||||||
|
"showAll": "Показать все"
|
||||||
|
},
|
||||||
|
"searchResults": {
|
||||||
|
"title": "Результаты поиска",
|
||||||
|
"totalResults":"Всего найдено",
|
||||||
|
"nothingFound": "Ничего не найдено"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"authors":"Авторы",
|
||||||
|
"publicationsType": "Публикации",
|
||||||
|
"publisher":"Издатель",
|
||||||
|
"publicationTopic":"Тема публикации",
|
||||||
|
"appliedFitlers":"Фильтры",
|
||||||
|
"clearAll":"Очистить всё",
|
||||||
|
"enterAuthorsName":"Введите имя автора",
|
||||||
|
"showAll":"Показать все"
|
||||||
}
|
}
|
||||||
}
|
}
|
24
src/article/controller/articleViewModel.ts
Normal file
24
src/article/controller/articleViewModel.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { ArticleStore } from "../domain/articleStore";
|
||||||
|
import { getArticleUseCase } from "../useCases/getArticleUseCase";
|
||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
|
function useArticleViewModel(store: ArticleStore) {
|
||||||
|
const _getArticle = useCallback(
|
||||||
|
(id: string) => getArticleUseCase(store.getArticle, store.setArticle, id),
|
||||||
|
[store.getArticle, store.setArticle]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (store.article != undefined) {
|
||||||
|
_getArticle(store.article.id);
|
||||||
|
}
|
||||||
|
}, [store.article?.id]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
article: store.article,
|
||||||
|
shouldShowLoading: typeof store.article === "undefined" || store.isLoading,
|
||||||
|
hasError: store.hasError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useArticleViewModel };
|
36
src/article/data/articleAPIService.ts
Normal file
36
src/article/data/articleAPIService.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { Article } from "../domain/articleEntity";
|
||||||
|
import { create } from "../domain/articleModel";
|
||||||
|
import { FetchArticleByIdDTO } from "./dto/fetch_article_by_id_dto";
|
||||||
|
import Failure from "core/failure";
|
||||||
|
import { integratorApiClient } from "core/httpClient";
|
||||||
|
|
||||||
|
const articleEndpoint = "/papers/"
|
||||||
|
|
||||||
|
async function getArticle(id: string): Promise<Article> {
|
||||||
|
try {
|
||||||
|
const response = await integratorApiClient.get<FetchArticleByIdDTO>(
|
||||||
|
// `https://run.mocky.io/v3/62cd4581-d864-4d46-b1d6-02b45b5d1994/${id}`
|
||||||
|
// `https://jsonplaceholder.typicode.com/posts/${id}`
|
||||||
|
// `https://run.mocky.io/v3/066be3d8-0568-439a-8b20-062deed49a97`
|
||||||
|
articleEndpoint + id
|
||||||
|
);
|
||||||
|
const dto = response.data;
|
||||||
|
return create({
|
||||||
|
id: dto.id,
|
||||||
|
topic: [dto.topic],
|
||||||
|
title: dto.title,
|
||||||
|
authors: dto.authors,
|
||||||
|
tags: dto.tags,
|
||||||
|
summary: dto.summary,
|
||||||
|
content: dto.content,
|
||||||
|
});
|
||||||
|
} catch (reason) {
|
||||||
|
if (axios.isAxiosError(reason)) {
|
||||||
|
throw Failure.fromReason(reason, "failures.services.load");
|
||||||
|
}
|
||||||
|
throw reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getArticle };
|
4
src/article/data/articleActionTypes.ts
Normal file
4
src/article/data/articleActionTypes.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const SET_ARTICLE = "SET_ARTICLE";
|
||||||
|
export const GET_ARTICLE = "GET_ARTICLE";
|
||||||
|
export const GET_ARTICLE_SUCCESS = "GET_ARTICLE.success";
|
||||||
|
export const GET_ARTICLE_FAILURE = "GET_ARTICLE.failure";
|
23
src/article/data/articleActions.ts
Normal file
23
src/article/data/articleActions.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { Article } from "../domain/articleEntity";
|
||||||
|
import { getArticle as getArticleAPI } from "./articleAPIService";
|
||||||
|
import * as actionTypes from "./articleActionTypes";
|
||||||
|
import { dispatchStatus } from "../../store/index";
|
||||||
|
|
||||||
|
const setArticleAction = (article: Article) => (dispatch: any) =>
|
||||||
|
dispatch({ type: actionTypes.SET_ARTICLE, article });
|
||||||
|
|
||||||
|
const getArticleAction = (id: string) => (dispatch: any) => {
|
||||||
|
dispatch({ type: actionTypes.GET_ARTICLE });
|
||||||
|
|
||||||
|
return getArticleAPI(id)
|
||||||
|
.then((article) => {
|
||||||
|
dispatchStatus(actionTypes.GET_ARTICLE, ".success", article)(dispatch);
|
||||||
|
return article;
|
||||||
|
})
|
||||||
|
.catch((reason) => {
|
||||||
|
dispatchStatus(actionTypes.GET_ARTICLE, ".failure", reason)(dispatch);
|
||||||
|
return reason;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { setArticleAction, getArticleAction };
|
39
src/article/data/articleCommonStateStore.ts
Normal file
39
src/article/data/articleCommonStateStore.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { ArticleStore } from "../domain/articleStore";
|
||||||
|
import type { Article } from "../domain/articleEntity";
|
||||||
|
import { getArticle as getArticleAPI } from "./articleAPIService";
|
||||||
|
|
||||||
|
const useArticleCommonStore = (): ArticleStore => {
|
||||||
|
const [isLoading, setLoading] = useState<boolean>(false);
|
||||||
|
const [hasError, setError] = useState<boolean>(false);
|
||||||
|
const [article, setArticleState] = useState<Article | undefined>();
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const getArticle = useCallback(
|
||||||
|
async (id: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const article = await getArticleAPI(id);
|
||||||
|
setArticleState(article);
|
||||||
|
setLoading(false);
|
||||||
|
return article;
|
||||||
|
} catch (error) {
|
||||||
|
setError(true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
article: article,
|
||||||
|
isLoading,
|
||||||
|
hasError,
|
||||||
|
setArticle: setArticleState,
|
||||||
|
getArticle,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useArticleCommonStore };
|
32
src/article/data/articleReducer.ts
Normal file
32
src/article/data/articleReducer.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { AnyAction } from "@reduxjs/toolkit";
|
||||||
|
import type { ArticleStore } from "../domain/articleStore";
|
||||||
|
import * as actionTypes from "./articleActionTypes";
|
||||||
|
|
||||||
|
type ArticleStoreState = Omit<ArticleStore, "getArticle" | "setArticle">;
|
||||||
|
|
||||||
|
const INITIAL_STATE: ArticleStoreState = {
|
||||||
|
article: undefined,
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const articleReducer = (
|
||||||
|
state: ArticleStoreState = INITIAL_STATE,
|
||||||
|
action: AnyAction
|
||||||
|
): ArticleStoreState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case actionTypes.SET_ARTICLE:
|
||||||
|
return { ...state, article: action.article };
|
||||||
|
case actionTypes.GET_ARTICLE:
|
||||||
|
return { ...state, isLoading: true };
|
||||||
|
case actionTypes.GET_ARTICLE_SUCCESS:
|
||||||
|
return { ...state, isLoading: false, article: action.payload };
|
||||||
|
case actionTypes.GET_ARTICLE_FAILURE:
|
||||||
|
return { ...state, hasError: true, isLoading: false };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { articleReducer };
|
||||||
|
export type { ArticleStoreState };
|
35
src/article/data/articleStoreImplementation.ts
Normal file
35
src/article/data/articleStoreImplementation.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { ArticleStore } from "../domain/articleStore";
|
||||||
|
import type { Article } from "../domain/articleEntity";
|
||||||
|
import type { ArticleStoreState } from "../data/articleReducer";
|
||||||
|
import { getArticleAction, setArticleAction } from "./articleActions";
|
||||||
|
import { RootState, useAppSelector } from "store";
|
||||||
|
|
||||||
|
const articleSelector = (state: RootState): ArticleStoreState => state.article;
|
||||||
|
|
||||||
|
const useArticleStore = (): ArticleStore => {
|
||||||
|
const { isLoading, article, hasError } = useAppSelector(articleSelector);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const setArticle = useCallback(
|
||||||
|
(article: Article) => setArticleAction(article)(dispatch),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getArticle = useCallback(
|
||||||
|
(id: string) => getArticleAction(id)(dispatch),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
article: article,
|
||||||
|
isLoading,
|
||||||
|
hasError,
|
||||||
|
setArticle,
|
||||||
|
getArticle,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useArticleStore };
|
9
src/article/data/dto/fetch_article_by_id_dto.ts
Normal file
9
src/article/data/dto/fetch_article_by_id_dto.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface FetchArticleByIdDTO {
|
||||||
|
id: string;
|
||||||
|
topic: string;
|
||||||
|
title: string;
|
||||||
|
authors: string[];
|
||||||
|
tags: string[];
|
||||||
|
summary: string;
|
||||||
|
content: string;
|
||||||
|
}
|
11
src/article/domain/ArticleEntity.ts → src/article/domain/articleEntity.ts
Executable file → Normal file
11
src/article/domain/ArticleEntity.ts → src/article/domain/articleEntity.ts
Executable file → Normal file
@ -1,10 +1,9 @@
|
|||||||
export interface Article {
|
export interface Article {
|
||||||
id?: string;
|
id: string;
|
||||||
title?: string;
|
title: string;
|
||||||
authors?: string[];
|
content: string;
|
||||||
topic?: string[];
|
topic?: string[];
|
||||||
summary?: string;
|
authors?: string[];
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
content?: string;
|
summary?: string;
|
||||||
publisher?: string;
|
|
||||||
}
|
}
|
14
src/article/domain/articleModel.ts
Normal file
14
src/article/domain/articleModel.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { CreateArticleParams } from "article/useCases/params/create_article_params";
|
||||||
|
import { Article } from "./articleEntity";
|
||||||
|
|
||||||
|
const create = (props: CreateArticleParams): Article => ({
|
||||||
|
id: props.id,
|
||||||
|
topic: props.topic,
|
||||||
|
title: props.title,
|
||||||
|
authors: props.authors,
|
||||||
|
tags: props.tags,
|
||||||
|
summary: props.summary,
|
||||||
|
content: props.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { create };
|
13
src/article/domain/articleStore.ts
Normal file
13
src/article/domain/articleStore.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Article } from './articleEntity';
|
||||||
|
interface ArticleStore {
|
||||||
|
// State
|
||||||
|
article: Article | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
hasError: boolean;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setArticle(article?: Article): void;
|
||||||
|
getArticle(identifier: string): Promise<Article | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ArticleStore };
|
16
src/article/useCases/getArticleUseCase.ts
Normal file
16
src/article/useCases/getArticleUseCase.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Article } from "article/domain/articleEntity";
|
||||||
|
import type { ArticleStore } from "../domain/articleStore";
|
||||||
|
|
||||||
|
const getArticleUseCase = async (
|
||||||
|
getArticle: ArticleStore["getArticle"],
|
||||||
|
setArticle: ArticleStore["setArticle"],
|
||||||
|
id: Article["id"]
|
||||||
|
): Promise<Article | null> => {
|
||||||
|
const article = await getArticle(id);
|
||||||
|
if (article) {
|
||||||
|
await setArticle(article);
|
||||||
|
}
|
||||||
|
return article;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getArticleUseCase };
|
9
src/article/useCases/params/create_article_params.ts
Normal file
9
src/article/useCases/params/create_article_params.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface CreateArticleParams {
|
||||||
|
id: string;
|
||||||
|
topic: string[];
|
||||||
|
title: string;
|
||||||
|
authors: string[];
|
||||||
|
tags: string[];
|
||||||
|
summary: string;
|
||||||
|
content: string;
|
||||||
|
}
|
1
src/assets/lotties/notFoundAnimation.json
Normal file
1
src/assets/lotties/notFoundAnimation.json
Normal file
File diff suppressed because one or more lines are too long
@ -12,14 +12,11 @@ import {
|
|||||||
} from "components/icons";
|
} from "components/icons";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
|
import Link from "components/typography/Link";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ShareButton } from "./ArticleShareButton";
|
||||||
|
|
||||||
const interactionButtonsStore = [
|
const interactionButtonsStore = [
|
||||||
{
|
|
||||||
icon: <SVGFiletext />,
|
|
||||||
title: "Read file",
|
|
||||||
buttonEmphasis: "high",
|
|
||||||
iconClassName: "h-6 fill-white stroke-white",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: <SVGDownload />,
|
icon: <SVGDownload />,
|
||||||
title: "Download",
|
title: "Download",
|
||||||
@ -41,21 +38,27 @@ const interactionButtonsStore = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
type ArticleButtonProps = {
|
type ArticleButtonProps = {
|
||||||
isAbstractOpen: boolean;
|
isAbstractOpen?: boolean;
|
||||||
openAbstract: () => void;
|
openAbstract?: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
emphasis?: "high" | "low";
|
emphasis?: "high" | "low";
|
||||||
|
articleID?: string;
|
||||||
} & Omit<React.ComponentPropsWithoutRef<"button">, "">;
|
} & Omit<React.ComponentPropsWithoutRef<"button">, "">;
|
||||||
|
|
||||||
export function ArticleInteractionButtons({
|
export function ArticleInteractionButtons({
|
||||||
isAbstractOpen = false,
|
isAbstractOpen = false,
|
||||||
children,
|
children,
|
||||||
openAbstract = () => { },
|
openAbstract = () => {},
|
||||||
className,
|
className,
|
||||||
|
articleID,
|
||||||
emphasis = "high", //to change displaying of component
|
emphasis = "high", //to change displaying of component
|
||||||
...props
|
...props
|
||||||
}: ArticleButtonProps) {
|
}: ArticleButtonProps) {
|
||||||
|
const [t, i18next] = useTranslation("translation", {
|
||||||
|
keyPrefix: "articlePage.interactionButtons",
|
||||||
|
});
|
||||||
|
/* ----------------------------- Abstract Button ---------------------------- */
|
||||||
const abstractButton = (
|
const abstractButton = (
|
||||||
<Button
|
<Button
|
||||||
emphasis="medium"
|
emphasis="medium"
|
||||||
@ -63,32 +66,66 @@ export function ArticleInteractionButtons({
|
|||||||
onClick={openAbstract}
|
onClick={openAbstract}
|
||||||
>
|
>
|
||||||
<Typography fontWeightVariant="bold" className="pr-2">
|
<Typography fontWeightVariant="bold" className="pr-2">
|
||||||
Abstract
|
{t("abstract")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button.Icon>
|
<Button.Icon>
|
||||||
{!isAbstractOpen ? <SVGArrowDown className="w-4 fill-blue-700 stroke-blue-700" /> : <SVGArrowUp className="w-4 fill-blue-700 stroke-blue-700" />}
|
{!isAbstractOpen ? (
|
||||||
|
<SVGArrowDown className="w-4 fill-blue-700 stroke-blue-700" />
|
||||||
|
) : (
|
||||||
|
<SVGArrowUp className="w-4 fill-blue-700 stroke-blue-700" />
|
||||||
|
)}
|
||||||
</Button.Icon>
|
</Button.Icon>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
const fileInteractionButtons = interactionButtonsStore.map((button) => {
|
/* ---------------------------- Read file button ---------------------------- */
|
||||||
return (
|
const readFileButton = (
|
||||||
<Button
|
<Link
|
||||||
emphasis={button.buttonEmphasis === "high" ? "high" : "low"}
|
to={`/article/content/${articleID}`}
|
||||||
className="h-max px-2 mr-2"
|
className="rounded active:outline focus:outline focus:outline-blue-400/10 outline-8 active:outline-blue-400/10"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-blue-600
|
||||||
|
hover:bg-blue-500
|
||||||
|
active:bg-blue-700
|
||||||
|
focus:shadow-lg shadow-blue-500
|
||||||
|
p-2 rounded
|
||||||
|
flex flex-row space-x-2
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<Button.Icon>
|
<SVGFiletext className="w-6 fill-white stroke-white" />
|
||||||
{React.cloneElement(button.icon, { className: button.iconClassName })}
|
{emphasis === "high" && <Typography fontWeightVariant="semibold" className="text-white">{t("readFile")}</Typography>}
|
||||||
</Button.Icon>
|
</div>
|
||||||
{emphasis === "high" ? <Typography>{button.title}</Typography> : null}
|
</Link>
|
||||||
</Button>
|
);
|
||||||
);
|
/* ----------------------------- Download button ---------------------------- */
|
||||||
});
|
const downLoadButton = (
|
||||||
|
<Button emphasis="low" className="px-2 space-x-2">
|
||||||
|
<Button.Icon>
|
||||||
|
<SVGDownload className="w-6 fill-gray-900 stroke-gray-900" />
|
||||||
|
</Button.Icon>
|
||||||
|
{emphasis === "high" && <Typography fontWeightVariant="semibold" className="text-gray-900">{t("download")}</Typography>}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ------------------------------- Cite button ------------------------------ */
|
||||||
|
|
||||||
|
const citeButton = (
|
||||||
|
<Button emphasis="low" className="px-2 space-x-2">
|
||||||
|
<Button.Icon>
|
||||||
|
<SVGCite className="w-6 fill-gray-900 stroke-gray-900" />
|
||||||
|
</Button.Icon>
|
||||||
|
{emphasis === "high" && <Typography fontWeightVariant="semibold" className="text-gray-900">{t("cite")}</Typography>}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row space-x-2">
|
||||||
{emphasis === "low" && !children ? abstractButton : null}
|
{emphasis != "high" && abstractButton}
|
||||||
{children ? children : fileInteractionButtons}
|
{readFileButton}
|
||||||
|
{downLoadButton}
|
||||||
|
{citeButton}
|
||||||
|
<ShareButton emphasis={emphasis} linktoCopy={`/article/content/${articleID}`} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
import { BASE_URL } from "core/httpClient";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button } from "../../../Button/Button";
|
||||||
|
import { SVGCopy, SVGShare, SVGXmark } from "../../../icons";
|
||||||
|
import Typography from "../../../typography/Typography";
|
||||||
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
|
import { Popover } from "@headlessui/react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
emphasis?: "high" | "low";
|
||||||
|
linktoCopy?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ShareButton({ emphasis, linktoCopy }: Props) {
|
||||||
|
const [t, i18next] = useTranslation("translation", {
|
||||||
|
keyPrefix: "articlePage.interactionButtons",
|
||||||
|
});
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const copyValue =
|
||||||
|
BASE_URL != undefined && linktoCopy != undefined
|
||||||
|
? BASE_URL + linktoCopy
|
||||||
|
: t("searchResults.nothingFound");
|
||||||
|
|
||||||
|
function onCopy() {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = (event: any) => event.target.select();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover className="relative">
|
||||||
|
<Popover.Button>
|
||||||
|
<Button emphasis="low" className="px-2 space-x-2">
|
||||||
|
<Button.Icon>
|
||||||
|
<SVGShare className="w-6 fill-gray-900 stroke-gray-900" />
|
||||||
|
</Button.Icon>
|
||||||
|
{emphasis === "high" && <Typography fontWeightVariant="semibold" className="text-gray-900">{t("share")}</Typography>}
|
||||||
|
</Button>
|
||||||
|
</Popover.Button>
|
||||||
|
|
||||||
|
<Popover.Panel className="absolute z-10 -right-3/4 mt-3 pl-2 bg-white border-2 border-gray-300 shadow-lg rounded">
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<input
|
||||||
|
className="focus:outline-none active:outline-none p-1 mr-1"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={copyValue}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
/>
|
||||||
|
<div className="relative">
|
||||||
|
<CopyToClipboard text={copyValue} onCopy={onCopy}>
|
||||||
|
<Button
|
||||||
|
emphasis="low"
|
||||||
|
className="items-center border-l-2 rounded-none "
|
||||||
|
>
|
||||||
|
<Button.Icon>
|
||||||
|
<SVGCopy className="w-5 fill-gray-900 stroke-gray-900" />
|
||||||
|
</Button.Icon>
|
||||||
|
</Button>
|
||||||
|
</CopyToClipboard>
|
||||||
|
|
||||||
|
{copied && (
|
||||||
|
<div className="absolute z-11 -bottom-full bg-gray-900 text-white px-2 py-1 rounded before:content[''] before:absolute before:bg-gray-900 before:rotate-45 before:w-3 before:h-3 before:-top-1 before:left-4 before:block">
|
||||||
|
{t("copied")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Popover.Button>
|
||||||
|
<Button
|
||||||
|
emphasis="low"
|
||||||
|
className="items-center border-l-2 rounded-none "
|
||||||
|
>
|
||||||
|
<Button.Icon>
|
||||||
|
<SVGXmark className="w-5 fill-gray-900 stroke-gray-900" />
|
||||||
|
</Button.Icon>
|
||||||
|
</Button>
|
||||||
|
</Popover.Button>
|
||||||
|
</div>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Article } from "components/Article/Article";
|
import { Article } from "components/Article/Article";
|
||||||
import { Article as ArticleTypes } from "article/domain/ArticleEntity";
|
import { Article as ArticleTypes } from "article/domain/articleEntity";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import { useSearchStoreImplementation } from "searchResults/data/searchStoreImplementation";
|
import { useSearchStoreImplementation } from "searchResults/data/searchStoreImplementation";
|
||||||
@ -37,7 +37,7 @@ export const ArticleSearchResult = ({ searchItem }: Props) => {
|
|||||||
<Article.SubscriptionsButtons />
|
<Article.SubscriptionsButtons />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Article.Title linkTo={searchItem.id} className="text-2xl">
|
<Article.Title linkTo={`/article/info/${searchItem.id}`} className="text-2xl">
|
||||||
{searchItem.title}
|
{searchItem.title}
|
||||||
</Article.Title>
|
</Article.Title>
|
||||||
<Article.Authors emphasis="low" className="flex flex-wrap flex-row">
|
<Article.Authors emphasis="low" className="flex flex-wrap flex-row">
|
||||||
@ -47,10 +47,11 @@ export const ArticleSearchResult = ({ searchItem }: Props) => {
|
|||||||
{searchItem.tags}
|
{searchItem.tags}
|
||||||
</Article.Keywords>
|
</Article.Keywords>
|
||||||
<Article.InteractionButtons
|
<Article.InteractionButtons
|
||||||
className="py-2 "
|
className="py-2"
|
||||||
emphasis="low"
|
emphasis="low"
|
||||||
openAbstract={open}
|
openAbstract={open}
|
||||||
isAbstractOpen={openAbstract}
|
isAbstractOpen={openAbstract}
|
||||||
|
articleID={searchItem.id}
|
||||||
/>
|
/>
|
||||||
<Article.Description
|
<Article.Description
|
||||||
emphasis="low"
|
emphasis="low"
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { StyleType } from "core/_variants";
|
import { StyleType } from "core/_variants";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { ReactComponent as DeleteIcon } from "../assets/svg/xmark.svg";
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Component props */
|
/* Component props */
|
||||||
@ -15,7 +16,8 @@ type Props = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
iconed?: boolean;
|
iconed?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: (element: any) => void;
|
||||||
|
closeOption?: Boolean;
|
||||||
} & Omit<React.MouseEventHandler<HTMLSpanElement>, "">;
|
} & Omit<React.MouseEventHandler<HTMLSpanElement>, "">;
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
@ -27,17 +29,18 @@ function Badge({
|
|||||||
children,
|
children,
|
||||||
onClick,
|
onClick,
|
||||||
emphasis = "low",
|
emphasis = "low",
|
||||||
|
closeOption = false,
|
||||||
...props
|
...props
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<span
|
<span
|
||||||
onClick={onClick}
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"border p-2 rounded-sm text-xs text-center",
|
"border-none p-2 rounded-md text-xs text-center",
|
||||||
{
|
{
|
||||||
"cursor-pointer": onClick,
|
"cursor-pointer": onClick,
|
||||||
"border-transparent": emphasis == "low",
|
"border-transparent": emphasis == "low",
|
||||||
"bg-blue-400 text-white": emphasis == "high",
|
"bg-blue-400 text-white rounded-xs": emphasis == "high",
|
||||||
"border-gray-400 background-gray-200": emphasis == "medium",
|
"border-gray-400 background-gray-200": emphasis == "medium",
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
@ -45,7 +48,18 @@ function Badge({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
{closeOption && (
|
||||||
|
<button onClick={onClick} className="h-4 w-5">
|
||||||
|
<div>
|
||||||
|
<DeleteIcon
|
||||||
|
className="relative top-1 left-1 h-4 w-5 fill-white hover:fill-white stroke-white
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default Badge;
|
export default Badge;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { joinClassnames } from "core/helpers";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Footer } from "./parts/Footer";
|
import { Footer } from "./parts/Footer";
|
||||||
import Header from "./parts/Header";
|
import Header from "./parts/Header";
|
||||||
@ -10,10 +11,10 @@ type Props = {
|
|||||||
|
|
||||||
function BaseLayout({ header, footer, children, className }: Props) {
|
function BaseLayout({ header, footer, children, className }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={joinClassnames(className, 'flex min-h-screen flex-col justify-between')}>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main>{children}</main>
|
<main className="flex-1">{children}</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
198
src/components/Burger.tsx
Normal file
198
src/components/Burger.tsx
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
import React, { Fragment } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Disclosure } from "@headlessui/react";
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Components */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
import ContextMenuAction from "./drop-down-menu/ContextMenuAction";
|
||||||
|
import ContextMenu from "./drop-down-menu/ContextMenu";
|
||||||
|
import { Button } from "./Button/Button";
|
||||||
|
import Link from "./typography/Link";
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Icons */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
import { ReactComponent as SVGFavoriteOutlined } from "assets/svg/favorite-outlined.svg";
|
||||||
|
import { ReactComponent as SVGHamburger } from "assets/svg/hamburger.svg";
|
||||||
|
import { ReactComponent as SVGFolder } from "assets/svg/folder.svg";
|
||||||
|
import { ReactComponent as SVGFile } from "assets/svg/file.svg";
|
||||||
|
import { ReactComponent as SVGEye } from "assets/svg/eye.svg";
|
||||||
|
import { ReactComponent as SVGArrowUp } from "assets/svg/arrow-up.svg";
|
||||||
|
import { ReactComponent as SVGCaretDown } from "assets/svg/caret-down.svg";
|
||||||
|
|
||||||
|
type Props = React.ComponentPropsWithoutRef<"div">;
|
||||||
|
|
||||||
|
const Burger = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<Menu as="div" className="relative inline-block text-left z-30">
|
||||||
|
<div>
|
||||||
|
<Menu.Button as={Button} emphasis="low">
|
||||||
|
<Button.Icon>
|
||||||
|
<SVGHamburger className="h-6 w-6" />
|
||||||
|
</Button.Icon>
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items
|
||||||
|
className="origin-top-right absolute right-0 mt-5 w-44 rounded-md
|
||||||
|
shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="py-1">
|
||||||
|
<Disclosure>
|
||||||
|
<Disclosure.Button className="uppercase text-base px-2 py-1">
|
||||||
|
<Link className="text-[#096DD9]">create new</Link>
|
||||||
|
</Disclosure.Button>
|
||||||
|
</Disclosure>
|
||||||
|
<hr />
|
||||||
|
<Disclosure>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button
|
||||||
|
className="uppercase px-2 flex justify-between w-full items-center
|
||||||
|
py-1
|
||||||
|
hover:bg-gray-100
|
||||||
|
text-base
|
||||||
|
"
|
||||||
|
>
|
||||||
|
my library
|
||||||
|
<SVGArrowUp
|
||||||
|
className={`${
|
||||||
|
open ? "rotate-180 transform" : "rotate-360"
|
||||||
|
} h-5 w-5 `}
|
||||||
|
/>
|
||||||
|
</Disclosure.Button>
|
||||||
|
|
||||||
|
<Link to="#" className="w-full">
|
||||||
|
<Disclosure.Panel
|
||||||
|
className="px-2 flex items-center font-normal gap-1 py-1 w-full ease-in-out duration-300
|
||||||
|
rounded
|
||||||
|
hover:bg-gray-200
|
||||||
|
text-base
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<SVGFile className="stroke-black w-4 h-4" />
|
||||||
|
My Publications
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link to="#" className="w-full">
|
||||||
|
<Disclosure.Panel
|
||||||
|
className="px-2 flex items-center font-normal gap-1 py-1 w-full ease-in-out duration-300
|
||||||
|
rounded
|
||||||
|
hover:bg-gray-200
|
||||||
|
text-base
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<SVGFavoriteOutlined className="stroke-black w-4 h-4" />
|
||||||
|
My Favorites
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link to="#" className="w-full">
|
||||||
|
<Disclosure.Panel
|
||||||
|
className="px-2 flex items-center font-normal gap-1 py-1 w-full ease-in-out duration-300
|
||||||
|
rounded
|
||||||
|
hover:bg-gray-200
|
||||||
|
text-base
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<SVGFolder className="stroke-black fill-black w-4 h-4" />
|
||||||
|
My Collections
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link to="#" className="w-full">
|
||||||
|
<Disclosure.Panel
|
||||||
|
className="px-2 flex items-center font-normal gap-1 py-1 w-full ease-in-out duration-300
|
||||||
|
rounded
|
||||||
|
hover:bg-gray-200
|
||||||
|
text-base
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<SVGEye className="stroke-black w-4 h-4" />
|
||||||
|
Recent Viewed
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
<hr />
|
||||||
|
{/* Third list - start */}
|
||||||
|
<Disclosure>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button
|
||||||
|
className="uppercase px-2 flex justify-between w-full items-center
|
||||||
|
py-1
|
||||||
|
hover:bg-gray-100
|
||||||
|
text-base
|
||||||
|
"
|
||||||
|
>
|
||||||
|
About
|
||||||
|
<SVGArrowUp
|
||||||
|
className={`${
|
||||||
|
open ? "rotate-180 transform" : "rotate-360"
|
||||||
|
} h-5 w-5 `}
|
||||||
|
/>
|
||||||
|
</Disclosure.Button>
|
||||||
|
|
||||||
|
<Link to="#" className="w-full">
|
||||||
|
<Disclosure.Panel
|
||||||
|
className="px-2 flex items-center font-normal gap-1 py-1 w-full ease-in-out duration-300
|
||||||
|
rounded
|
||||||
|
hover:bg-gray-200
|
||||||
|
text-base
|
||||||
|
"
|
||||||
|
>
|
||||||
|
About Freeland
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link to="#" className="w-full">
|
||||||
|
<Disclosure.Panel
|
||||||
|
className="px-2 flex items-center font-normal gap-1 py-1 w-full ease-in-out duration-300
|
||||||
|
rounded
|
||||||
|
hover:bg-gray-200
|
||||||
|
text-base
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Contact Us
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link to="#" className="w-full">
|
||||||
|
<Disclosure.Panel
|
||||||
|
className="px-2 flex items-center font-normal gap-1 py-1 w-full ease-in-out duration-300
|
||||||
|
rounded
|
||||||
|
hover:bg-gray-200
|
||||||
|
text-base
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Help
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
{/* Third list - end */}
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Burger;
|
@ -3,7 +3,7 @@ import { SVGMedicine } from "../icons";
|
|||||||
import Typography from "components/typography/Typography";
|
import Typography from "components/typography/Typography";
|
||||||
import { Button } from "components/Button/Button";
|
import { Button } from "components/Button/Button";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { JsxElement } from "typescript";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
count?: number;
|
count?: number;
|
||||||
@ -13,6 +13,7 @@ type Props = {
|
|||||||
} & Omit<React.ComponentPropsWithoutRef<"div">, "">;
|
} & Omit<React.ComponentPropsWithoutRef<"div">, "">;
|
||||||
|
|
||||||
function CategoryCard({ count, title, iconChild, className, ...props }: Props) {
|
function CategoryCard({ count, title, iconChild, className, ...props }: Props) {
|
||||||
|
const [t, i18next] = useTranslation();
|
||||||
const iconChildStyle =
|
const iconChildStyle =
|
||||||
"h-7 fill-gray-500 stroke-gray-500 group-focus:fill-blue-600 group-active:fill-blue-600 group-focus:stroke-blue-600 group-active:stroke-blue-600";
|
"h-7 fill-gray-500 stroke-gray-500 group-focus:fill-blue-600 group-active:fill-blue-600 group-focus:stroke-blue-600 group-active:stroke-blue-600";
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ function CategoryCard({ count, title, iconChild, className, ...props }: Props) {
|
|||||||
fontWeightVariant="bold"
|
fontWeightVariant="bold"
|
||||||
className="text-sm leading-6 min-w-max group-active:text-blue-600 group-focus:text-blue-600"
|
className="text-sm leading-6 min-w-max group-active:text-blue-600 group-focus:text-blue-600"
|
||||||
>
|
>
|
||||||
{title}
|
{t(title)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-max ">
|
<div className="max-w-max ">
|
||||||
@ -42,7 +43,8 @@ function CategoryCard({ count, title, iconChild, className, ...props }: Props) {
|
|||||||
fontWeightVariant="normal"
|
fontWeightVariant="normal"
|
||||||
className="text-xs text-gray-500 group-active:text-blue-600 group-focus:text-blue-600"
|
className="text-xs text-gray-500 group-active:text-blue-600 group-focus:text-blue-600"
|
||||||
>
|
>
|
||||||
{count} Items
|
{count}{" "}
|
||||||
|
{t("mainPage.article_many", { count: count }).toString()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ReactComponent as Checkmark } from "assets/svg/check.svg";
|
import { ReactComponent as Checkmark } from "../assets/svg/arrow-down.svg";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
/**
|
/**
|
||||||
* Control the state of checkbox
|
* Control the state of checkbox
|
||||||
*/
|
*/
|
||||||
isChecked: boolean;
|
isChecked?: boolean;
|
||||||
/**
|
/**
|
||||||
* An Element next to the checkbox
|
* An Element next to the checkbox
|
||||||
*/
|
*/
|
||||||
@ -22,7 +22,7 @@ const Checkbox = ({ children, className, isChecked, ...props }: Props) => {
|
|||||||
)}
|
)}
|
||||||
htmlFor={props.id}
|
htmlFor={props.id}
|
||||||
>
|
>
|
||||||
<div className="w-6 h-6 relative">
|
<div className="w-5 h-5 relative">
|
||||||
<input
|
<input
|
||||||
className="peer appearance-none transition-colors bg-transparent border-2 border-gray-300 w-6 h-6
|
className="peer appearance-none transition-colors bg-transparent border-2 border-gray-300 w-6 h-6
|
||||||
rounded checked:bg-blue-500 checked:border-blue-500
|
rounded checked:bg-blue-500 checked:border-blue-500
|
||||||
@ -33,12 +33,8 @@ const Checkbox = ({ children, className, isChecked, ...props }: Props) => {
|
|||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<div //
|
<div className="h-2 w-2 absolute top-0.5 left-0.5">
|
||||||
className=" w-4 h-3 leading-[0] absolute top-1.5 left-1 opacity-0
|
<Checkmark className="h-5 w-5 fill-white hover:fill-white stroke-white" />
|
||||||
pointer-events-none focus:pointer-events-auto flex items-center justify-center
|
|
||||||
fill-white peer-disabled:fill-gray-500 "
|
|
||||||
>
|
|
||||||
<Checkmark className="fill-inherit" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
|
55
src/components/Disclosure.tsx
Normal file
55
src/components/Disclosure.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Imports */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
import React from "react";
|
||||||
|
import { Disclosure as Disclose } from "@headlessui/react";
|
||||||
|
import { ReactComponent as SelectIcon } from "../assets/svg/arrow-down.svg";
|
||||||
|
import classNames from "classnames";
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Component props */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
type Props = {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
caption?: string;
|
||||||
|
};
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* styles */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
const ButtonStyle = `
|
||||||
|
flex w-full
|
||||||
|
justify-between
|
||||||
|
items-center
|
||||||
|
py-2 text-left
|
||||||
|
text-base
|
||||||
|
font-medium
|
||||||
|
text-black-400
|
||||||
|
`;
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Component implementation */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
export default function Disclosure({ children, className, caption }: Props) {
|
||||||
|
return (
|
||||||
|
<div className={classNames("top-16 ", className)}>
|
||||||
|
<div className="mx-auto w-full bg-white py-2">
|
||||||
|
<Disclose as="div">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclose.Button className={`${ButtonStyle}`}>
|
||||||
|
<span>{caption}</span>
|
||||||
|
<SelectIcon
|
||||||
|
className={`${
|
||||||
|
open ? "rotate-180 transform" : ""
|
||||||
|
} h-4 w-5 fill-black hover:fill-black stroke-black`}
|
||||||
|
/>
|
||||||
|
</Disclose.Button>
|
||||||
|
<Disclose.Panel className="py-2 text-sm text-gray-500 ">
|
||||||
|
{children}
|
||||||
|
</Disclose.Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclose>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
69
src/components/Filters/AppiledFilters.tsx
Normal file
69
src/components/Filters/AppiledFilters.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Imports */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
import React from "react";
|
||||||
|
import Badge from "components/Badge";
|
||||||
|
import { IProduct } from "./IProdutct";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Component props */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
type Props = {
|
||||||
|
hints?: IProduct[];
|
||||||
|
delFilter: (element: IProduct) => void;
|
||||||
|
clearAll: () => void;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Component implementation */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
export default function AppiledFilters({
|
||||||
|
hints,
|
||||||
|
delFilter,
|
||||||
|
clearAll,
|
||||||
|
className,
|
||||||
|
}: Props) {
|
||||||
|
|
||||||
|
const [t, i18next] = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"w-full py-3 h-36 max-h-36 overflow-hidden ",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center justify-between space-x-3 ">
|
||||||
|
<div className="font-medium">{t("filters.appliedFitlers")}</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={clearAll}
|
||||||
|
className="font-normal text-sm text-gray-400"
|
||||||
|
>
|
||||||
|
{t("filters.clearAll")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="list-none mt-2 h-24 max-h-32 overflow-auto">
|
||||||
|
{hints &&
|
||||||
|
hints.length > 0 &&
|
||||||
|
hints.map((item: IProduct) => (
|
||||||
|
<li className="mt-1" key={item.id}>
|
||||||
|
<Badge
|
||||||
|
className="py-1 cursor-default"
|
||||||
|
emphasis="high"
|
||||||
|
closeOption={true}
|
||||||
|
onClick={() => delFilter(item)}
|
||||||
|
>
|
||||||
|
{item.brand}
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
112
src/components/Filters/Filter.tsx
Normal file
112
src/components/Filters/Filter.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Disclosure from "components/Disclosure";
|
||||||
|
import AppiledFilters from "./AppiledFilters";
|
||||||
|
import SearchFilterBar from "./SearchFilterBar";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useDebounce } from "./functions/debounce";
|
||||||
|
import { IProduct } from "./IProdutct";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Fiter({ className }: Props) {
|
||||||
|
const [checkedId, setCheckedId] = React.useState<Array<number>>([]);
|
||||||
|
const [activeFilter, setActiveFilter] = React.useState<Array<IProduct>>([]);
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Test request to demonstrate the basic functionality of the filter */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const [query, setQuery] = React.useState("");
|
||||||
|
const [hints, setHints] = React.useState<Array<IProduct>>([]);
|
||||||
|
|
||||||
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setQuery(e.target.value);
|
||||||
|
};
|
||||||
|
const debounced = useDebounce(query);
|
||||||
|
|
||||||
|
const [t, i18next] = useTranslation();
|
||||||
|
|
||||||
|
async function fetchProducts() {
|
||||||
|
const response = await axios.get(
|
||||||
|
`https://dummyjson.com/products/search?q=${debounced}`
|
||||||
|
);
|
||||||
|
setHints(response.data.products);
|
||||||
|
}
|
||||||
|
React.useEffect(() => {
|
||||||
|
fetchProducts();
|
||||||
|
}, [debounced, query]);
|
||||||
|
|
||||||
|
const isChecked = (item: number) => (checkedId.includes(item) ? true : false);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selectedId = parseInt(e.target.value);
|
||||||
|
|
||||||
|
/* ------------- Check if "checkedId" contains "selectedIds" ------------- */
|
||||||
|
/* -------------- If true, this checkbox is already checked -------------- */
|
||||||
|
|
||||||
|
if (checkedId.includes(selectedId)) {
|
||||||
|
setCheckedId(checkedId.filter((id) => id !== selectedId));
|
||||||
|
setActiveFilter(activeFilter.filter((item) => item.id !== selectedId));
|
||||||
|
} else {
|
||||||
|
const newId = [...checkedId];
|
||||||
|
newId.push(selectedId);
|
||||||
|
setCheckedId(newId);
|
||||||
|
|
||||||
|
/* --------------- Adding checked filters to AppliedFilters component -------------- */
|
||||||
|
|
||||||
|
hints.forEach((hint) => {
|
||||||
|
if (hint.id === selectedId)
|
||||||
|
activeFilter.push({ id: selectedId, brand: hint.brand });
|
||||||
|
setActiveFilter(activeFilter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------------- Delete filter from AppliedFilters component ------------- */
|
||||||
|
|
||||||
|
const DelFilter = (e: IProduct) => {
|
||||||
|
setActiveFilter(activeFilter.filter((item) => item.id !== e.id));
|
||||||
|
setCheckedId(checkedId.filter((id) => id !== e.id));
|
||||||
|
};
|
||||||
|
const clearAll = () => {
|
||||||
|
setActiveFilter([]);
|
||||||
|
setCheckedId([]);
|
||||||
|
setQuery("");
|
||||||
|
};
|
||||||
|
//console.log(activeFilter)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="pl-2">
|
||||||
|
<div className="max-w-[268px] mx-auto divide-y divide-solid">
|
||||||
|
<AppiledFilters
|
||||||
|
hints={activeFilter}
|
||||||
|
delFilter={DelFilter}
|
||||||
|
clearAll={clearAll}
|
||||||
|
></AppiledFilters>
|
||||||
|
<Disclosure caption={t("filters.authors")}>
|
||||||
|
<SearchFilterBar
|
||||||
|
hints={hints}
|
||||||
|
isChecked={isChecked}
|
||||||
|
handleChange={handleChange}
|
||||||
|
onChange={onChange}
|
||||||
|
query={query}
|
||||||
|
placeHolder={t("filters.enterAuthorsName")}
|
||||||
|
/>
|
||||||
|
</Disclosure>
|
||||||
|
<Disclosure caption={t("filters.publicationsType")}>
|
||||||
|
<p>{t("filters.content")}</p>
|
||||||
|
</Disclosure>
|
||||||
|
<Disclosure caption={t("filters.publisher")}>
|
||||||
|
<p>{t("filters.content")}</p>
|
||||||
|
</Disclosure>
|
||||||
|
<Disclosure caption={t("filters.publicationTopic")}>
|
||||||
|
<p>{t("filters.content")}</p>
|
||||||
|
</Disclosure>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
23
src/components/Filters/IProdutct.ts
Normal file
23
src/components/Filters/IProdutct.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export interface IProduct {
|
||||||
|
id?: number;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
price?: number;
|
||||||
|
discountPercentage?: number;
|
||||||
|
rating?: number;
|
||||||
|
stock?: number;
|
||||||
|
brand?: string;
|
||||||
|
category?: string;
|
||||||
|
thumbnail?: string;
|
||||||
|
images?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface serverResponse<T> {
|
||||||
|
products: T[];
|
||||||
|
total: number;
|
||||||
|
skip: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
120
src/components/Filters/SearchFilterBar.tsx
Normal file
120
src/components/Filters/SearchFilterBar.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { useState, useCallback, Fragment } from "react";
|
||||||
|
import { Combobox, Transition } from "@headlessui/react";
|
||||||
|
import { IProduct } from "./IProdutct";
|
||||||
|
import Checkbox from "components/Checkbox";
|
||||||
|
import { ReactComponent as SearchIcon } from "assets/svg/search.svg";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
hints: IProduct[];
|
||||||
|
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
isChecked: (item: number) => boolean;
|
||||||
|
className?: string;
|
||||||
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
placeHolder: string;
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SearchFilterBar({
|
||||||
|
hints,
|
||||||
|
isChecked,
|
||||||
|
handleChange,
|
||||||
|
className,
|
||||||
|
onChange,
|
||||||
|
placeHolder,
|
||||||
|
query,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const [checkList, setCheckList] = useState(hints);
|
||||||
|
const [showFilters, SetShowFilters] = useState(false);
|
||||||
|
const [t, i18next] = useTranslation()
|
||||||
|
|
||||||
|
const ShowAllFilters = useCallback(() => {
|
||||||
|
SetShowFilters((prev) => !prev);
|
||||||
|
}, [SetShowFilters]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Combobox value={checkList} onChange={setCheckList}>
|
||||||
|
{({ open }) => (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"flex flex-row w-full items-center border border-solid rounded border-gray-200",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="basis-6 ">
|
||||||
|
<Combobox.Button>
|
||||||
|
<div className="pl-1 pt-1.5">
|
||||||
|
<SearchIcon className="h-5 w-6 fill-gray-400 hover:fill-gray-400 stroke-gray-400" />
|
||||||
|
</div>
|
||||||
|
</Combobox.Button>
|
||||||
|
</div>
|
||||||
|
<div className="grow">
|
||||||
|
<Combobox.Input
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={placeHolder}
|
||||||
|
className="w-full text-sm py-1 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Combobox.Options static={true}>
|
||||||
|
{open && (
|
||||||
|
<ul
|
||||||
|
className={classNames([
|
||||||
|
"w-[276px] relative h-40 top-0 right-2",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"max-h-60 h-60 overflow-auto": showFilters === true,
|
||||||
|
"max-h-40 h-40 overflow-hidden":
|
||||||
|
showFilters === false || hints.length <= 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
<li className="">
|
||||||
|
{hints.length >= 6 && query !== "" && (
|
||||||
|
<button
|
||||||
|
onClick={ShowAllFilters}
|
||||||
|
className="text-right text-blue-500 pl-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
{t("filters.showAll")}({hints.length})
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
{hints.length > 0 ? (
|
||||||
|
hints.map((item) => (
|
||||||
|
<li key={item.id} className="mt-1 pl-2">
|
||||||
|
<Checkbox
|
||||||
|
className="z-10 mt-1 w-full max-h-56"
|
||||||
|
value={item.id}
|
||||||
|
isChecked={item.id ? isChecked(item.id) : false}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<p>{item.brand}</p>
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-blue-300 pl-2">Ничего не найдено</p>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
11
src/components/Filters/functions/debounce.ts
Normal file
11
src/components/Filters/functions/debounce.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function useDebounce(value: string, delay: number = 300) {
|
||||||
|
const [debounced, setDebouced] = React.useState(value);
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handler = setTimeout(() => setDebouced(value), delay);
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debounced;
|
||||||
|
}
|
89
src/components/LocalizationButton.tsx
Normal file
89
src/components/LocalizationButton.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import React, { Fragment, useState } from "react";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Button } from "./Button/Button";
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faLanguage } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
type Props = React.ComponentPropsWithoutRef<"div">;
|
||||||
|
|
||||||
|
const LocalizationButton = (props: Props) => {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
const changeLanguage = (lng: string) => {
|
||||||
|
i18n.changeLanguage(lng);
|
||||||
|
setLanguage(lng);
|
||||||
|
};
|
||||||
|
const [language, setLanguage] = useState("en");
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
|
<div>
|
||||||
|
<Menu.Button
|
||||||
|
as={Button}
|
||||||
|
emphasis="low"
|
||||||
|
className="flex items-center gap-2 border"
|
||||||
|
>
|
||||||
|
<div className=" w-4">
|
||||||
|
<FontAwesomeIcon icon={faLanguage} />
|
||||||
|
</div>
|
||||||
|
{/* {language} */}
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items
|
||||||
|
className="origin-top-right absolute right-0 mt-5 w-12 rounded-md
|
||||||
|
shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="py-1">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("en");
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
|
||||||
|
"block px-4 py-2 text-sm"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
En
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("ru");
|
||||||
|
}}
|
||||||
|
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
|
||||||
|
"block px-4 py-2 text-sm"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Ru
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocalizationButton;
|
39
src/components/LogoScipaper.tsx
Executable file
39
src/components/LogoScipaper.tsx
Executable file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -31,6 +31,7 @@ import "swiper/css/pagination";
|
|||||||
import "swiper/css/navigation";
|
import "swiper/css/navigation";
|
||||||
// import "./styles.css";
|
// import "./styles.css";
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Article mock data */
|
/* Article mock data */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
@ -97,9 +98,10 @@ const FeaturedArticlesCards = () => {
|
|||||||
const navigationPrevRef = useRef(null);
|
const navigationPrevRef = useRef(null);
|
||||||
const navigationNextRef = useRef(null);
|
const navigationNextRef = useRef(null);
|
||||||
const paginationRef = useRef(null);
|
const paginationRef = useRef(null);
|
||||||
|
const [t, i18next] = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="slider-wrapper articles">
|
<div className="slider-wrapper articles px-8">
|
||||||
<div className="flex justify-end gap-2 my-2">
|
<div className="flex justify-end gap-2 my-2">
|
||||||
<div
|
<div
|
||||||
className="prev inline-flex justify-center items-center
|
className="prev inline-flex justify-center items-center
|
||||||
@ -166,11 +168,12 @@ const FeaturedArticlesCards = () => {
|
|||||||
</Card.CardContent>
|
</Card.CardContent>
|
||||||
|
|
||||||
<Card.CardAction href={Articale.Link}>
|
<Card.CardAction href={Articale.Link}>
|
||||||
<Link to="*">
|
<Typography
|
||||||
<Typography className="text-blue-500 font-bold">
|
className="text-blue-500 font-bold"
|
||||||
Read More
|
fontWeightVariant="bold"
|
||||||
</Typography>
|
>
|
||||||
</Link>
|
{t("mainPage.more")}
|
||||||
|
</Typography>
|
||||||
<SVGCaretRight className="fill-blue-500 w-4 h-4" />
|
<SVGCaretRight className="fill-blue-500 w-4 h-4" />
|
||||||
</Card.CardAction>
|
</Card.CardAction>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -9,22 +9,25 @@ import {
|
|||||||
SVGTechnicsAndTechology,
|
SVGTechnicsAndTechology,
|
||||||
SVGFundamental,
|
SVGFundamental,
|
||||||
} from "components/icons";
|
} from "components/icons";
|
||||||
|
import { useTranslation} from "react-i18next";
|
||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
{ id: 1, title: "Medical", count: 5617813, icon: <SVGMedicine /> },
|
{ id: 1, title: "mainPage.featuredArticles.categories.Medical", count: 5617813, icon: <SVGMedicine /> },
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "Technics and Technology",
|
title: "mainPage.featuredArticles.categories.TechnicsAndTechlonogies",
|
||||||
count: 5617813,
|
count: 5617813,
|
||||||
icon: <SVGTechnicsAndTechology />,
|
icon: <SVGTechnicsAndTechology />,
|
||||||
},
|
},
|
||||||
{ id: 3, title: "Fundamental", count: 5617813, icon: <SVGFundamental /> },
|
{ id: 3, title: "mainPage.featuredArticles.categories.Fundamental", count: 5617813, icon: <SVGFundamental /> },
|
||||||
{ id: 4, title: "Humanitarian", count: 5617813, icon: <SVGHumanitarian /> },
|
{ id: 4, title: "mainPage.featuredArticles.categories.Humanitarian", count: 5617813, icon: <SVGHumanitarian /> },
|
||||||
{ id: 5, title: "Agricultural", count: 5617813, icon: <SVGAgricultural /> },
|
{ id: 5, title: "mainPage.featuredArticles.categories.Agricultural", count: 5617813, icon: <SVGAgricultural /> },
|
||||||
{ id: 6, title: "Social", count: 5617813, icon: <SVGSocials /> },
|
{ id: 6, title: "mainPage.featuredArticles.categories.Social", count: 5617813, icon: <SVGSocials /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function FeaturedArticlesCategories() {
|
export function FeaturedArticlesCategories() {
|
||||||
|
const [t, i18next] = useTranslation();
|
||||||
|
|
||||||
const categoryCards = useMemo(
|
const categoryCards = useMemo(
|
||||||
() =>
|
() =>
|
||||||
categories.map((category) => (
|
categories.map((category) => (
|
||||||
@ -45,11 +48,11 @@ export function FeaturedArticlesCategories() {
|
|||||||
fontWeightVariant="semibold"
|
fontWeightVariant="semibold"
|
||||||
className="text-3xl mb-2"
|
className="text-3xl mb-2"
|
||||||
>
|
>
|
||||||
Featured articles
|
{t("mainPage.featuredArticles.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography htmlTag="h2" className="text-base text-gray-500">
|
<Typography htmlTag="h2" className="text-base text-gray-500">
|
||||||
Select the category of science <br className="visible sm:hidden" />
|
{t("mainPage.featuredArticles.descriptionPart1")}<br className="visible sm:hidden" />
|
||||||
you are interested in
|
{t("mainPage.featuredArticles.descriptionPart2")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-8 px-10 flex md:flex justify-start md:justify-center md:flex-wrap overflow-x-scroll md:overflow-hidden snap-x scroll-smooth overscroll-x-contain">
|
<div className="py-8 px-10 flex md:flex justify-start md:justify-center md:flex-wrap overflow-x-scroll md:overflow-hidden snap-x scroll-smooth overscroll-x-contain">
|
||||||
|
@ -17,12 +17,9 @@ import "swiper/css/navigation";
|
|||||||
// import "./styles.css";
|
// import "./styles.css";
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
/* Icons */
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
import { ReactComponent as SVGCaretRight } from "assets/svg/caret-right.svg";
|
import { ReactComponent as SVGCaretRight } from "assets/svg/caret-right.svg";
|
||||||
import Link from "../../typography/Link";
|
import Link from "../../typography/Link";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Variables */
|
/* Variables */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
@ -73,13 +70,17 @@ if (authors.length == 2) {
|
|||||||
* Featured authors component to display ...
|
* Featured authors component to display ...
|
||||||
*/
|
*/
|
||||||
export default function FeaturedAuthorsCards(): JSX.Element {
|
export default function FeaturedAuthorsCards(): JSX.Element {
|
||||||
|
const [t, i18next] = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* The Title of Featured Authors section */}
|
{/* The Title of Featured Authors section */}
|
||||||
<Heading className="text-center my-8">Featured Authors</Heading>
|
<Heading className="text-center my-8 text-3xl font-semibold">
|
||||||
|
{t("mainPage.featuredAuthors")}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
{/* Featured Authors section */}
|
{/* Featured Authors section */}
|
||||||
<div className="slider-wrapper Authors">
|
<div className="slider-wrapper Authors px-8">
|
||||||
<Swiper
|
<Swiper
|
||||||
slidesPerView={1.25}
|
slidesPerView={1.25}
|
||||||
slidesPerGroup={1}
|
slidesPerGroup={1}
|
||||||
@ -134,7 +135,7 @@ export default function FeaturedAuthorsCards(): JSX.Element {
|
|||||||
|
|
||||||
<Card.CardAction href={card.Link}>
|
<Card.CardAction href={card.Link}>
|
||||||
<Link className="text-blue-500 font-bold" to="*">
|
<Link className="text-blue-500 font-bold" to="*">
|
||||||
See More
|
{t("mainPage.more")}
|
||||||
</Link>
|
</Link>
|
||||||
<SVGCaretRight className="fill-blue-500 w-4 h-4" />
|
<SVGCaretRight className="fill-blue-500 w-4 h-4" />
|
||||||
</Card.CardAction>
|
</Card.CardAction>
|
||||||
@ -188,7 +189,7 @@ export default function FeaturedAuthorsCards(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button emphasis="high" className="font-bold m-auto my-8">
|
<Button emphasis="high" className="font-bold m-auto my-8">
|
||||||
Show All
|
{t("mainPage.showAll")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,24 +2,36 @@
|
|||||||
/* Imports */
|
/* Imports */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { t as nextT } from "i18next";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { SearchBar } from "../../search/SearchBar";
|
import { SearchBar } from "../../search/SearchBar";
|
||||||
|
import { formatNumber } from "core/helpers";
|
||||||
|
|
||||||
export default function MainSection() {
|
export default function MainSection() {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
const amountArticles = 4202020;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-main bg-center bg-cover bg-origin-border bg-no-repeat min-h-[100vh] py-32 px-2 sm:px-6 md:px-6 lg:px-0 items-center flex justify-center ">
|
<section className="bg-main bg-center bg-cover bg-origin-border bg-no-repeat min-h-[100vh] py-32 px-2 sm:px-6 md:px-6 lg:px-0 items-center flex justify-center ">
|
||||||
<div className="flex-col">
|
<div className="flex-col">
|
||||||
<div className="m-auto text-center font-bold text-4xl ">
|
<div className="m-auto text-center font-bold text-4xl ">
|
||||||
Scientific Library with Free Access
|
{t("mainPage.title")}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center justify-center space-x-3 pt-2">
|
<div className="flex flex-row items-center justify-center space-x-3 pt-2">
|
||||||
<div className=" text-2xl text-gray-400">Search</div>
|
<div className=" text-2xl text-gray-400">{t("mainPage.search")}</div>
|
||||||
<div className=" text-3xl text-blue-500">320 455</div>
|
<div className=" text-3xl text-blue-500">
|
||||||
<div className=" text-2xl text-gray-400">Items</div>
|
{formatNumber(amountArticles)}
|
||||||
|
</div>
|
||||||
|
<div className=" text-2xl text-gray-400">
|
||||||
|
{nextT("mainPage.article_many", {
|
||||||
|
count: amountArticles,
|
||||||
|
}).toString()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-xl m-auto pt-16 ">
|
<div className="max-w-xl m-auto pt-16 ">
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
<div className="mt-7 pr-1 text-right font-semibold text-sm">
|
<div className="mt-7 pr-1 text-right font-semibold text-sm">
|
||||||
Advanced Search
|
{t("mainPage.advancedSearch")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* MarkDown */
|
/* MarkDown */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
@ -15,9 +13,10 @@ import Heading from "./typography/Heading";
|
|||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Code */
|
/* Code */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||||
import Link from "./typography/Link";
|
import Link from "./typography/Link";
|
||||||
|
import style from "react-syntax-highlighter/dist/esm/styles/hljs/a11y-dark";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
markdown: string;
|
markdown: string;
|
||||||
@ -29,19 +28,125 @@ const Markdown = ({ markdown }: Props) => {
|
|||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
children={markdown}
|
children={markdown}
|
||||||
components={{
|
components={{
|
||||||
h1: Heading,
|
ul: ({ node, ...props }) => (
|
||||||
h2: Typography,
|
<ul
|
||||||
a: (props) => {
|
style={{
|
||||||
return (
|
listStyleType: "disc",
|
||||||
<Link
|
}}
|
||||||
href={props.href}
|
className="mx-8"
|
||||||
className="text-sky-600 font-bold text-base"
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
>
|
),
|
||||||
{props.children}
|
|
||||||
</Link>
|
ol: ({ node, ...props }) => (
|
||||||
);
|
<ol className="list-decimal mx-8" {...props} />
|
||||||
},
|
),
|
||||||
|
|
||||||
|
h1: ({ node, ...props }) => (
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginInlineEnd: 0,
|
||||||
|
marginInlineStart: 0,
|
||||||
|
marginBlockStart: 11,
|
||||||
|
marginBlockEnd: 11,
|
||||||
|
|
||||||
|
fontSize: 32,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
|
||||||
|
h2: ({ node, ...props }) => (
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
|
||||||
|
marginInlineEnd: 0,
|
||||||
|
marginInlineStart: 0,
|
||||||
|
|
||||||
|
marginBlockStart: 13.28,
|
||||||
|
marginBlockEnd: 13.28,
|
||||||
|
|
||||||
|
fontSize: 24,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
|
||||||
|
h3: ({ node, ...props }) => (
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
|
||||||
|
marginInlineEnd: 0,
|
||||||
|
marginInlineStart: 0,
|
||||||
|
|
||||||
|
marginBlockStart: 16,
|
||||||
|
marginBlockEnd: 16,
|
||||||
|
|
||||||
|
fontSize: 18.72,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
|
||||||
|
h4: ({ node, ...props }) => (
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
|
||||||
|
marginInlineEnd: 0,
|
||||||
|
marginInlineStart: 0,
|
||||||
|
|
||||||
|
marginBlockStart: 21.28,
|
||||||
|
marginBlockEnd: 21.28,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
|
||||||
|
h5: ({ node, ...props }) => (
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
|
||||||
|
marginInlineEnd: 0,
|
||||||
|
marginInlineStart: 0,
|
||||||
|
|
||||||
|
marginBlockStart: 26.72,
|
||||||
|
marginBlockEnd: 26.72,
|
||||||
|
|
||||||
|
fontSize: 13.28,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
|
||||||
|
h6: ({ node, ...props }) => (
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
|
||||||
|
marginInlineEnd: 0,
|
||||||
|
marginInlineStart: 0,
|
||||||
|
|
||||||
|
marginBlockStart: 37.28,
|
||||||
|
marginBlockEnd: 37.28,
|
||||||
|
|
||||||
|
fontSize: 10.72,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
p: Typography,
|
||||||
|
|
||||||
|
a: ({ node, ...props }) => (
|
||||||
|
<Link
|
||||||
|
className=" inline-flex text-sm font-bold text-blue-500"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
|
||||||
code({ node, inline, className, children, ...props }) {
|
code({ node, inline, className, children, ...props }) {
|
||||||
const match = /language-(\w+)/.exec(className || "");
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
@ -4,10 +4,12 @@ import { useSearchStoreImplementation } from "searchResults/data/searchStoreImpl
|
|||||||
import { useSearchViewModel } from "../searchResults/controller/searchResultsViewModel";
|
import { useSearchViewModel } from "../searchResults/controller/searchResultsViewModel";
|
||||||
import { ArticleSearchResult } from "./ArticleSearchResult";
|
import { ArticleSearchResult } from "./ArticleSearchResult";
|
||||||
import { Loader } from "./Loader/Loader";
|
import { Loader } from "./Loader/Loader";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const SearchResultSection = () => {
|
export const SearchResultSection = () => {
|
||||||
const store = useSearchStoreImplementation();
|
const store = useSearchStoreImplementation();
|
||||||
const { searchResults, isLoading } = useSearchViewModel(store);
|
const { searchResults, isLoading } = useSearchViewModel(store);
|
||||||
|
const [t, i18next] = useTranslation()
|
||||||
|
|
||||||
function getResults() {
|
function getResults() {
|
||||||
if (searchResults === undefined || searchResults?.data.length === 0) {
|
if (searchResults === undefined || searchResults?.data.length === 0) {
|
||||||
@ -16,7 +18,7 @@ export const SearchResultSection = () => {
|
|||||||
fontWeightVariant="semibold"
|
fontWeightVariant="semibold"
|
||||||
className="text-xl w-full text-center items-center py-3"
|
className="text-xl w-full text-center items-center py-3"
|
||||||
>
|
>
|
||||||
Nothing found.
|
{t("searchResults.nothingFound")}.
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -31,14 +33,14 @@ export const SearchResultSection = () => {
|
|||||||
<div className="p-4 md:px-6 md:py-8">
|
<div className="p-4 md:px-6 md:py-8">
|
||||||
<div className="pb-2">
|
<div className="pb-2">
|
||||||
<Typography fontWeightVariant="semibold" className="text-3xl">
|
<Typography fontWeightVariant="semibold" className="text-3xl">
|
||||||
Search Results
|
{t("searchResults.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography className="text-gray-300 text-sm">
|
<Typography className="text-gray-300 text-sm">
|
||||||
Total results: {searchResults?.meta.total}
|
{t("searchResults.totalResults")}: {searchResults?.meta.total}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<hr className="w-full border-gray-100" />
|
<hr className="w-full border-gray-100" />
|
||||||
<div className="divide divide-y divide-gray-100">{ getResults()}</div>
|
<div className="divide divide-y divide-gray-100">{getResults()}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
86
src/components/fetchAnArticle/AnArticle.tsx
Normal file
86
src/components/fetchAnArticle/AnArticle.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { useArticleViewModel } from "article/controller/articleViewModel";
|
||||||
|
import { useArticleStore } from "article/data/articleStoreImplementation";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import * as ArticlePart from "../../components/Article/Article";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import AskeletonArticle from "./AskeletonArticle";
|
||||||
|
import Container from "components/Container";
|
||||||
|
import NotFound from "./NotFound";
|
||||||
|
import { SVGSearch } from "components/icons";
|
||||||
|
import BaseLayout from "components/BaseLayout";
|
||||||
|
import Typography from "components/typography/Typography";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const AnArticle = () => {
|
||||||
|
const store = useArticleStore();
|
||||||
|
const { article, hasError, shouldShowLoading } = useArticleViewModel(store);
|
||||||
|
const { i18n, t } = useTranslation();
|
||||||
|
|
||||||
|
const { id } = useParams();
|
||||||
|
const newId = `${id}`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
store.getArticle(newId);
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
return <NotFound />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<BaseLayout>
|
||||||
|
<Container variant="straight">
|
||||||
|
{shouldShowLoading ? (
|
||||||
|
<AskeletonArticle />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArticlePart.Article.Breadcumbs className="py-4">
|
||||||
|
{article?.topic}
|
||||||
|
</ArticlePart.Article.Breadcumbs>
|
||||||
|
<div className="flex flex-col gap-4 pb-4">
|
||||||
|
<ArticlePart.Article.Title className="text-3xl ">
|
||||||
|
{article?.title}
|
||||||
|
</ArticlePart.Article.Title>
|
||||||
|
<ArticlePart.Article.Authors>
|
||||||
|
{article?.authors !== undefined ? article?.authors : "Unknown"}
|
||||||
|
</ArticlePart.Article.Authors>
|
||||||
|
<hr></hr>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ArticlePart.Article.InteractionButtons
|
||||||
|
emphasis="high"
|
||||||
|
articleID={article?.id}
|
||||||
|
/>
|
||||||
|
{article?.tags && (
|
||||||
|
<div className="keywords my-10 flex flex-col gap-2">
|
||||||
|
<Typography className="text-2xl" fontWeightVariant="semibold">
|
||||||
|
{t("articlePage.keywords")}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ArticlePart.Article.Keywords className="transition ease-in-out delay-50">
|
||||||
|
{article?.tags}
|
||||||
|
</ArticlePart.Article.Keywords>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="abstract my-10 flex flex-col gap-2">
|
||||||
|
<Typography className="text-2xl" fontWeightVariant="semibold">
|
||||||
|
{t("articlePage.abstract")}
|
||||||
|
</Typography>
|
||||||
|
<ArticlePart.Article.Description>
|
||||||
|
{article?.summary !== undefined ? (
|
||||||
|
article?.summary
|
||||||
|
) : (
|
||||||
|
<div className=" bg-gray-100 w-full stroke-gray-800 text-xl flex justify-center py-1">
|
||||||
|
<SVGSearch className="w-14" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ArticlePart.Article.Description>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// \n
|
||||||
|
export default AnArticle;
|
64
src/components/fetchAnArticle/AnArticleBody.tsx
Normal file
64
src/components/fetchAnArticle/AnArticleBody.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { useArticleViewModel } from "article/controller/articleViewModel";
|
||||||
|
import { useArticleStore } from "article/data/articleStoreImplementation";
|
||||||
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
|
import Skeleton from "react-loading-skeleton";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Components */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
import * as ArticlePart from "../../components/Article/Article";
|
||||||
|
import BaseLayout from "components/BaseLayout";
|
||||||
|
import Container from "components/Container";
|
||||||
|
import NotFound from "./NotFound";
|
||||||
|
import Markdown from "components/Markdown";
|
||||||
|
|
||||||
|
const AnArticleBody = () => {
|
||||||
|
const store = useArticleStore();
|
||||||
|
const { article, hasError, shouldShowLoading } = useArticleViewModel(store);
|
||||||
|
const { id } = useParams();
|
||||||
|
const newId = `${id}`;
|
||||||
|
useEffect(() => {
|
||||||
|
store.getArticle(newId);
|
||||||
|
}, [id]);
|
||||||
|
if (hasError) <NotFound />;
|
||||||
|
return (
|
||||||
|
<BaseLayout>
|
||||||
|
<Container variant="straight">
|
||||||
|
{shouldShowLoading ? (
|
||||||
|
<>
|
||||||
|
<Skeleton count={1} className="my-4" />
|
||||||
|
<div className="gap-4 py-12 px-20">
|
||||||
|
<Skeleton
|
||||||
|
count={1}
|
||||||
|
containerClassName=" text-3xl"
|
||||||
|
className="mb-6"
|
||||||
|
/>
|
||||||
|
<Skeleton count={15} containerClassName="" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArticlePart.Article.Breadcumbs className="py-4">
|
||||||
|
{article?.topic}
|
||||||
|
</ArticlePart.Article.Breadcumbs>
|
||||||
|
<div className="gap-4 py-12 px-20">
|
||||||
|
<ArticlePart.Article.Title className="text-3xl">
|
||||||
|
{article?.title}
|
||||||
|
</ArticlePart.Article.Title>
|
||||||
|
|
||||||
|
<div className="py-6">
|
||||||
|
<Markdown
|
||||||
|
markdown={article?.content ?? ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnArticleBody;
|
59
src/components/fetchAnArticle/AskeletonArticle.tsx
Normal file
59
src/components/fetchAnArticle/AskeletonArticle.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import Skeleton from "react-loading-skeleton";
|
||||||
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
|
const AskeletonArticle = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Skeleton count={1} className="my-4" />
|
||||||
|
<div className="flex flex-col gap-4 pb-4">
|
||||||
|
<Skeleton count={1} containerClassName="title w-3/4 text-2xl" />
|
||||||
|
<Skeleton count={1} containerClassName="authors w-1/4" />
|
||||||
|
<hr></hr>
|
||||||
|
</div>
|
||||||
|
<div className="interActions-buttons flex">
|
||||||
|
<Skeleton count={1} containerClassName="w-1/12 mr-2 text-xl" />
|
||||||
|
<Skeleton count={1} containerClassName="w-1/12 mr-2 text-xl" />
|
||||||
|
<Skeleton count={1} containerClassName="w-1/12 mr-2 text-xl" />
|
||||||
|
<Skeleton count={1} containerClassName="w-1/12 mr-2 text-xl" />
|
||||||
|
</div>
|
||||||
|
<div className=" my-10 flex flex-col gap-2">
|
||||||
|
<Skeleton
|
||||||
|
count={1}
|
||||||
|
className="keywords-title text-2xl"
|
||||||
|
containerClassName="w-1/4"
|
||||||
|
/>
|
||||||
|
<div className="flex">
|
||||||
|
<Skeleton
|
||||||
|
count={1}
|
||||||
|
className="border-2 border-[#ebebeb]"
|
||||||
|
containerClassName="w-1/6 mr-2"
|
||||||
|
baseColor="transparent"
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
count={1}
|
||||||
|
className="border-2 border-[#ebebeb]"
|
||||||
|
containerClassName="w-1/6 mr-2"
|
||||||
|
baseColor="transparent"
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
count={1}
|
||||||
|
className="border-2 border-[#ebebeb]"
|
||||||
|
containerClassName="w-1/6 mr-2"
|
||||||
|
baseColor="transparent"
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
count={1}
|
||||||
|
className="border-2 border-[#ebebeb]"
|
||||||
|
containerClassName="w-1/6 mr-2"
|
||||||
|
baseColor="transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="my-10 flex flex-col gap-2">
|
||||||
|
<Skeleton count={1} width={200} className="text-2xl" />
|
||||||
|
<Skeleton count={25} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AskeletonArticle;
|
40
src/components/fetchAnArticle/NotFound.tsx
Normal file
40
src/components/fetchAnArticle/NotFound.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Container from "components/Container";
|
||||||
|
import { Button } from "components/Button/Button";
|
||||||
|
import Link from "components/typography/Link";
|
||||||
|
|
||||||
|
import Lottie from "react-lottie";
|
||||||
|
import animationData from "../../assets/lotties/notFoundAnimation.json";
|
||||||
|
|
||||||
|
const NotFound = () => {
|
||||||
|
const defaultOptions = {
|
||||||
|
loop: true,
|
||||||
|
autoplay: true,
|
||||||
|
animationData: animationData,
|
||||||
|
rendererSettings: {
|
||||||
|
preserveAspectRatio: "xMidYMid slice",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
variant="straight"
|
||||||
|
className="flex flex-col items-center justify-center
|
||||||
|
font-serif h-screen my-auto
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Lottie options={defaultOptions} height={200} width={200} />
|
||||||
|
<span className="font-bold text-5xl ">404</span>
|
||||||
|
<h3 className="font-bold text-2xl text-center">Page does not exist</h3>
|
||||||
|
<h4 className="text-center">
|
||||||
|
Maybe you got a broken link, or maybe you made a misprint in the address
|
||||||
|
bar
|
||||||
|
</h4>
|
||||||
|
<Button className="my-4">
|
||||||
|
<Link to="/">Go to home</Link>
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
@ -6,29 +6,29 @@ import Typography from "components/typography/Typography";
|
|||||||
import { SVGFacebook, SVGInstagram, SVGCircle } from "components/icons";
|
import { SVGFacebook, SVGInstagram, SVGCircle } from "components/icons";
|
||||||
import { RouterLink } from "components/typography/RouterLink";
|
import { RouterLink } from "components/typography/RouterLink";
|
||||||
import Link from "components/typography/Link";
|
import Link from "components/typography/Link";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Define consts */
|
/* Define consts */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
const mainLinks = [
|
const mainLinks = [
|
||||||
{ label: "account settings", url: "/account/settings", disabled: false },
|
{ label: "footer.accountSettings", url: "/account/settings", disabled: false },
|
||||||
{ label: "about freeland", url: "/about", disabled: false },
|
{ label: "footer.about", url: "/about", disabled: false },
|
||||||
{ label: "help", url: "/help", disabled: false },
|
{ label: "footer.help", url: "/help", disabled: false },
|
||||||
{ label: "contact us", url: "/contact-us", disabled: false },
|
{ label: "footer.contactUs", url: "/contact-us", disabled: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
const secondaryLinks = [
|
const secondaryLinks = [
|
||||||
{ index: 1, label: "Terms of Use", url: "/terms-of-use", disabled: false },
|
{ index: 1, label: "footer.termsOfUse", url: "/terms-of-use", disabled: false },
|
||||||
{
|
{
|
||||||
index: 2,
|
index: 2,
|
||||||
label: "Privacy Policy",
|
label: "footer.privacyPolicy",
|
||||||
url: "/privacy-policy",
|
url: "/privacy-policy",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 3,
|
index: 3,
|
||||||
label: "Cookies Policy",
|
label: "footer.coockiesPolicy",
|
||||||
url: "/cookies-policy",
|
url: "/cookies-policy",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
@ -48,6 +48,8 @@ const circleDivider = (
|
|||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
|
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
/* -------------------------- Part with main links -------------------------- */
|
/* -------------------------- Part with main links -------------------------- */
|
||||||
const mainLinksPart = useMemo(
|
const mainLinksPart = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -59,7 +61,7 @@ export function Footer() {
|
|||||||
to={link.url}
|
to={link.url}
|
||||||
>
|
>
|
||||||
<Typography className="" fontWeightVariant="semibold" htmlTag="p">
|
<Typography className="" fontWeightVariant="semibold" htmlTag="p">
|
||||||
{link.label.toUpperCase()}
|
{t(link.label).toUpperCase()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
)),
|
)),
|
||||||
@ -72,7 +74,7 @@ export function Footer() {
|
|||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
{link.index != 1 && circleDivider}
|
{link.index != 1 && circleDivider}
|
||||||
<Link key={link.url} disabled={link.disabled} to={link.url}>
|
<Link key={link.url} disabled={link.disabled} to={link.url}>
|
||||||
{link.label}
|
{t(link.label) }
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)),
|
)),
|
||||||
@ -88,7 +90,7 @@ export function Footer() {
|
|||||||
<div className="sm:col-span-1">
|
<div className="sm:col-span-1">
|
||||||
<Link to="*">
|
<Link to="*">
|
||||||
<Typography className="text-2xl" fontWeightVariant="semibold">
|
<Typography className="text-2xl" fontWeightVariant="semibold">
|
||||||
Freeland
|
Scipaper
|
||||||
</Typography>
|
</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -108,13 +110,13 @@ export function Footer() {
|
|||||||
<section className="w-full flex flex-col md:flex-row text-gray-500 text-xs justify-between">
|
<section className="w-full flex flex-col md:flex-row text-gray-500 text-xs justify-between">
|
||||||
<div className="flex flex-col md:flex-row justify-center items-center">
|
<div className="flex flex-col md:flex-row justify-center items-center">
|
||||||
<Typography>
|
<Typography>
|
||||||
@ Copyright 2022 Freeland - All rights reserved
|
@ Copyright 2022 Freeland - {t("footer.allRightsReserved")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className="hidden md:flex">{circleDivider}</div>
|
<div className="hidden md:flex">{circleDivider}</div>
|
||||||
<div className="flex flex-row items-center">{secondaryLinksPart}</div>
|
<div className="flex flex-row items-center">{secondaryLinksPart}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-center md:justify-end">
|
<div className="flex flex-row justify-center md:justify-end">
|
||||||
Supported by
|
{t("footer.supportedBy")}
|
||||||
<Typography className="ml-1 lg:ml-2" fontWeightVariant="bold">
|
<Typography className="ml-1 lg:ml-2" fontWeightVariant="bold">
|
||||||
Comfortel
|
Comfortel
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useState } from "react";
|
import { useState, useTransition } from "react";
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Components */
|
/* Components */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
import ContextMenuAction from "../drop-down-menu/ContextMenuAction";
|
import ContextMenuAction from "../drop-down-menu/ContextMenuAction";
|
||||||
import ContextMenu from "../drop-down-menu/ContextMenu";
|
import ContextMenu from "../drop-down-menu/ContextMenu";
|
||||||
import Logofreeland from "../Logofreeland";
|
|
||||||
import { Button } from "../Button/Button";
|
import { Button } from "../Button/Button";
|
||||||
import Avatar from "../Avatar";
|
import Avatar from "../Avatar";
|
||||||
import Navbar from "../Navbar";
|
import Navbar from "../Navbar";
|
||||||
@ -23,11 +22,18 @@ import {
|
|||||||
SVGFile,
|
SVGFile,
|
||||||
SVGEye,
|
SVGEye,
|
||||||
} from "components/icons";
|
} from "components/icons";
|
||||||
|
import i18n from "localization/i18n";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Link from "components/typography/Link";
|
||||||
|
import LocalizationButton from "components/LocalizationButton";
|
||||||
|
import LogoScipaper from "components/LogoScipaper";
|
||||||
|
import Burger from "components/Burger";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const [authenticated, setAuthenticated] = useState(false);
|
const [authenticated, setAuthenticated] = useState(false);
|
||||||
const onClick = () => setAuthenticated(true);
|
const onClick = () => setAuthenticated(true);
|
||||||
const [notification, setNotification] = useState(false);
|
const [notification, setNotification] = useState(false);
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Implement Header Component */
|
/* Implement Header Component */
|
||||||
@ -46,14 +52,14 @@ const Header = () => {
|
|||||||
{/* Logo and Menu */}
|
{/* Logo and Menu */}
|
||||||
<div className="flex gap-8 xl:gap-x-16">
|
<div className="flex gap-8 xl:gap-x-16">
|
||||||
{/* Logo - start - className="w-7 sm:w-10 " /> */}
|
{/* Logo - start - className="w-7 sm:w-10 " /> */}
|
||||||
<a className="Logo flex items-center gap-1 sm:gap-2 " href="/">
|
<Link className="Logo flex items-center gap-1 sm:gap-2 " to="/">
|
||||||
<Logo
|
<Logo
|
||||||
className={classNames(authenticated ? "w-10" : "w-7 sm:w-10")}
|
className={classNames(authenticated ? "w-10" : "w-7 sm:w-10")}
|
||||||
/>
|
/>
|
||||||
<Logofreeland
|
<LogoScipaper
|
||||||
className={classNames(authenticated ? "w-28" : "w-20 sm:w-28")}
|
className={classNames(authenticated ? "w-28" : "w-20 sm:w-28")}
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
{/* Logo - end - */}
|
{/* Logo - end - */}
|
||||||
|
|
||||||
{/* Menu( Create new - My library - About ) Start */}
|
{/* Menu( Create new - My library - About ) Start */}
|
||||||
@ -66,36 +72,36 @@ const Header = () => {
|
|||||||
className="text-blue-500 px-4 font-bold uppercase"
|
className="text-blue-500 px-4 font-bold uppercase"
|
||||||
to="/create-new"
|
to="/create-new"
|
||||||
>
|
>
|
||||||
Create new
|
{t("navbar.createNew")}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
{/* Link Create now - end - */}
|
{/* Link Create now - end - */}
|
||||||
|
|
||||||
{/* Dropdown Menu My library - start - */}
|
{/* Dropdown Menu My library - start - */}
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
emphasis="high"
|
emphasis="high"
|
||||||
button="My library"
|
button={t("navbar.library.navTitle")}
|
||||||
className="border-none uppercase"
|
className="border-none uppercase"
|
||||||
>
|
>
|
||||||
<ContextMenuAction
|
<ContextMenuAction
|
||||||
caption="My Publications"
|
caption={t("navbar.library.publications")}
|
||||||
action={() => console.log("My publications")}
|
action={() => console.log("My publications")}
|
||||||
icon={<SVGFile className="stroke-black " />}
|
icon={<SVGFile className="stroke-black " />}
|
||||||
></ContextMenuAction>
|
></ContextMenuAction>
|
||||||
|
|
||||||
<ContextMenuAction
|
<ContextMenuAction
|
||||||
caption="My Favorites"
|
caption={t("navbar.library.favorites")}
|
||||||
action={() => console.log("My Favorites")}
|
action={() => console.log("My Favorites")}
|
||||||
icon={<SVGFavoriteOutlined className="stroke-black" />}
|
icon={<SVGFavoriteOutlined className="stroke-black" />}
|
||||||
></ContextMenuAction>
|
></ContextMenuAction>
|
||||||
|
|
||||||
<ContextMenuAction
|
<ContextMenuAction
|
||||||
caption="My Collections"
|
caption={t("navbar.library.collections")}
|
||||||
action={() => console.log("My Collections")}
|
action={() => console.log("My Collections")}
|
||||||
icon={<SVGFolder className="stroke-black fill-black" />}
|
icon={<SVGFolder className="stroke-black fill-black" />}
|
||||||
></ContextMenuAction>
|
></ContextMenuAction>
|
||||||
|
|
||||||
<ContextMenuAction
|
<ContextMenuAction
|
||||||
caption="Recent Viewed"
|
caption={t("navbar.library.recentViewed")}
|
||||||
action={() => console.log("Recent Viewed")}
|
action={() => console.log("Recent Viewed")}
|
||||||
icon={<SVGEye className="stroke-black " />}
|
icon={<SVGEye className="stroke-black " />}
|
||||||
></ContextMenuAction>
|
></ContextMenuAction>
|
||||||
@ -105,21 +111,21 @@ const Header = () => {
|
|||||||
{/* Dropdown Menu About - start - */}
|
{/* Dropdown Menu About - start - */}
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
emphasis="high"
|
emphasis="high"
|
||||||
button="About"
|
button={t("navbar.about.navTitle")}
|
||||||
className="border-none uppercase"
|
className="border-none uppercase"
|
||||||
>
|
>
|
||||||
<ContextMenuAction
|
<ContextMenuAction
|
||||||
caption="About Freeland"
|
caption={t("navbar.about.aboutProject")}
|
||||||
action={() => console.log("About Freeland")}
|
action={() => console.log("About Freeland")}
|
||||||
></ContextMenuAction>
|
></ContextMenuAction>
|
||||||
|
|
||||||
<ContextMenuAction
|
<ContextMenuAction
|
||||||
caption="Contact Us"
|
caption={t("navbar.about.contacts")}
|
||||||
action={() => console.log("Contact Us")}
|
action={() => console.log("Contact Us")}
|
||||||
></ContextMenuAction>
|
></ContextMenuAction>
|
||||||
|
|
||||||
<ContextMenuAction
|
<ContextMenuAction
|
||||||
caption="Help"
|
caption={t("navbar.about.help")}
|
||||||
action={() => console.log("Help")}
|
action={() => console.log("Help")}
|
||||||
></ContextMenuAction>
|
></ContextMenuAction>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
@ -132,19 +138,22 @@ const Header = () => {
|
|||||||
<div className="flex items-center font-bold text-sm gap-1 md:gap-2 ">
|
<div className="flex items-center font-bold text-sm gap-1 md:gap-2 ">
|
||||||
{!authenticated
|
{!authenticated
|
||||||
? [
|
? [
|
||||||
<Button
|
<>
|
||||||
emphasis="low"
|
<LocalizationButton className="hidden md:flex" />
|
||||||
onClick={onClick}
|
<Button
|
||||||
className="text-xs sm:px-4 sm:text-sm "
|
emphasis="low"
|
||||||
>
|
onClick={onClick}
|
||||||
Sign in
|
className="text-xs sm:px-4 sm:text-sm "
|
||||||
</Button>,
|
>
|
||||||
|
{t("navbar.auth.signIn")}
|
||||||
|
</Button>
|
||||||
|
</>,
|
||||||
<Button
|
<Button
|
||||||
emphasis="medium"
|
emphasis="medium"
|
||||||
className="hidden md:flex"
|
className="hidden md:flex"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
Sign up
|
{t("navbar.auth.signUp")}
|
||||||
</Button>,
|
</Button>,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
@ -165,7 +174,7 @@ const Header = () => {
|
|||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
{/* Burger component will be shown for the small screens */}
|
{/* Burger component will be shown for the small screens */}
|
||||||
<Navbar className="block lg:hidden" />
|
<Burger className="block lg:hidden" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,7 @@ import SearchInput from "./SearchInput";
|
|||||||
import { Button } from "components/Button/Button";
|
import { Button } from "components/Button/Button";
|
||||||
import { SVGSearch } from "components/icons";
|
import { SVGSearch } from "components/icons";
|
||||||
import Link from "components/typography/Link";
|
import Link from "components/typography/Link";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -15,6 +16,8 @@ type Props = {
|
|||||||
|
|
||||||
export function SearchBar({ className }: Props) {
|
export function SearchBar({ className }: Props) {
|
||||||
const [onSelected, setOnSelected] = useState(""); // Selected item from response list
|
const [onSelected, setOnSelected] = useState(""); // Selected item from response list
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
|
||||||
const searchResolver = (item: any) => {
|
const searchResolver = (item: any) => {
|
||||||
setOnSelected(item.caption);
|
setOnSelected(item.caption);
|
||||||
@ -31,6 +34,7 @@ export function SearchBar({ className }: Props) {
|
|||||||
onSelected={searchResolver}
|
onSelected={searchResolver}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
inGroup={true}
|
inGroup={true}
|
||||||
|
placeHolder={t("mainPage.search") + "..."}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,10 +51,11 @@ export default function Link({
|
|||||||
: style
|
: style
|
||||||
}
|
}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
|
className={classNames("flex items-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
return <div>{link}</div>;
|
return link;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,17 @@ export const handleScrollTo = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function capitalization (str: string) {
|
export function capitalization(str: string): string {
|
||||||
return str.substring(0,1).toUpperCase() + str.substring(1);
|
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatNumber(num: number): string {
|
||||||
|
return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinClassnames(first?: string, second?: string): string {
|
||||||
|
return [
|
||||||
|
first ?? '',
|
||||||
|
second ?? '',
|
||||||
|
].join('');
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
const BASE_URL = process.env.REACT_APP_INTEGRATOR_URL;
|
export const BASE_URL = process.env.REACT_APP_INTEGRATOR_URL;
|
||||||
export const GRAPHQL_URL = process.env.REACT_APP_GRAPHQL_URL ?? "";
|
export const GRAPHQL_URL = process.env.REACT_APP_GRAPHQL_URL ?? "";
|
||||||
|
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
|
@ -16,6 +16,9 @@ import AccountSettings from "pages/Information/AccountSettings";
|
|||||||
import { store } from "store/store";
|
import { store } from "store/store";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { SearchResultsPage } from "pages/SearchResultsPage";
|
import { SearchResultsPage } from "pages/SearchResultsPage";
|
||||||
|
import AnArticle from "components/fetchAnArticle/AnArticle";
|
||||||
|
import NotFound from "components/fetchAnArticle/NotFound";
|
||||||
|
import AnArticleBody from "components/fetchAnArticle/AnArticleBody";
|
||||||
|
|
||||||
const rootElement = document.getElementById("root");
|
const rootElement = document.getElementById("root");
|
||||||
if (!rootElement) throw new Error("Failed to find the root element");
|
if (!rootElement) throw new Error("Failed to find the root element");
|
||||||
@ -32,10 +35,15 @@ root.render(
|
|||||||
<Route path="/terms-of-use" element={<TermsOfUse />} />
|
<Route path="/terms-of-use" element={<TermsOfUse />} />
|
||||||
<Route path="/privacy-policy" element={<PrivacyPolicy />} />
|
<Route path="/privacy-policy" element={<PrivacyPolicy />} />
|
||||||
<Route path="/cookies-policy" element={<CookiesPolicy />} />
|
<Route path="/cookies-policy" element={<CookiesPolicy />} />
|
||||||
|
<Route path="/article">
|
||||||
|
<Route path="info/:id" element={<AnArticle />} />
|
||||||
|
<Route path="content/:id" element={<AnArticleBody />} />
|
||||||
|
</Route>
|
||||||
<Route path="/account">
|
<Route path="/account">
|
||||||
<Route path="settings" element={<AccountSettings />} />
|
<Route path="settings" element={<AccountSettings />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/search-results" element={<SearchResultsPage />}></Route>
|
<Route path="/search-results" element={<SearchResultsPage />} />
|
||||||
|
<Route path="/*" element={<NotFound />}></Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
@ -18,7 +18,7 @@ export const languages: Record<Langs, Lang> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
export const popularLangKeys = ["ru", "en"];
|
export const popularLangKeys = ["ru", "en"];
|
||||||
const fallbackLng: Langs = "en";
|
const fallbackLng: Langs = "ru";
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
.use(Backend)
|
.use(Backend)
|
||||||
|
@ -20,7 +20,6 @@ export default function MainPage({ className }: Props) {
|
|||||||
<FeaturedArticlesCategories></FeaturedArticlesCategories>
|
<FeaturedArticlesCategories></FeaturedArticlesCategories>
|
||||||
<FeaturedArticlesCards></FeaturedArticlesCards>
|
<FeaturedArticlesCards></FeaturedArticlesCards>
|
||||||
<FeaturedAuthorsCards></FeaturedAuthorsCards>
|
<FeaturedAuthorsCards></FeaturedAuthorsCards>
|
||||||
<BottomBarAcceptCookies></BottomBarAcceptCookies>
|
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -3,9 +3,7 @@ import BaseLayout from "components/BaseLayout";
|
|||||||
import { SearchSection } from "components/SearchSection";
|
import { SearchSection } from "components/SearchSection";
|
||||||
import { ColumnLayout } from "components/layouts/ThreeColumn/ColumnLayout";
|
import { ColumnLayout } from "components/layouts/ThreeColumn/ColumnLayout";
|
||||||
import { SearchResultSection } from "components/SearchResultsSection";
|
import { SearchResultSection } from "components/SearchResultsSection";
|
||||||
import { useSearchStoreImplementation } from "searchResults/data/searchStoreImplementation";
|
import Fiter from "components/Filters/Filter";
|
||||||
import { useSearchViewModel } from "searchResults/controller/searchResultsViewModel";
|
|
||||||
import { Loader } from "components/Loader/Loader";
|
|
||||||
|
|
||||||
export const SearchResultsPage = () => {
|
export const SearchResultsPage = () => {
|
||||||
return (
|
return (
|
||||||
@ -13,13 +11,13 @@ export const SearchResultsPage = () => {
|
|||||||
<SearchSection />
|
<SearchSection />
|
||||||
<ColumnLayout>
|
<ColumnLayout>
|
||||||
<ColumnLayout.Left>
|
<ColumnLayout.Left>
|
||||||
<div className="h-98 bg-blue-200 w-[300px]">left bar</div>
|
<div className="h-98 w-[300px] border rounded my-5"><Fiter /></div>
|
||||||
</ColumnLayout.Left>
|
</ColumnLayout.Left>
|
||||||
<ColumnLayout.Main>
|
<ColumnLayout.Main>
|
||||||
<SearchResultSection />
|
<SearchResultSection />
|
||||||
</ColumnLayout.Main>
|
</ColumnLayout.Main>
|
||||||
<ColumnLayout.Right>
|
<ColumnLayout.Right>
|
||||||
<div className="h-98 bg-blue-200 w-[300px]">right bar</div>
|
<div className="w-[300px]"></div>
|
||||||
</ColumnLayout.Right>
|
</ColumnLayout.Right>
|
||||||
</ColumnLayout>
|
</ColumnLayout>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
|
import { Article } from "article/domain/articleEntity";
|
||||||
|
|
||||||
export type SearchResultsDTO = {
|
export type SearchResultsDTO = {
|
||||||
data: ArticleDTO[];
|
data: ArticleDTO[];
|
||||||
meta: SearchResultsMeta;
|
meta: SearchResultsMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ArticleDTO = {
|
export type ArticleDTO = {
|
||||||
id?: string;
|
id: string;
|
||||||
title?: string;
|
title: string;
|
||||||
authors?: string[];
|
authors: string[];
|
||||||
topic?: string[];
|
topic: string[];
|
||||||
summary?: string;
|
summary: string;
|
||||||
tags?: string[];
|
tags: string[];
|
||||||
content?: string;
|
content: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SearchResultsMeta = {
|
export type SearchResultsMeta = {
|
||||||
total: string;
|
total: string;
|
||||||
}
|
}
|
@ -10,11 +10,23 @@ const searchEndpoint = "/papers/search";
|
|||||||
async function search(request: string): Promise<SearchResults> {
|
async function search(request: string): Promise<SearchResults> {
|
||||||
try {
|
try {
|
||||||
const response = await integratorApiClient.get<SearchResultsDTO>(
|
const response = await integratorApiClient.get<SearchResultsDTO>(
|
||||||
searchEndpoint + `?query=` + request + `&limit=10&offset=0`
|
searchEndpoint + `?query=` + request
|
||||||
// "https://run.mocky.io/v3/ea705665-2479-4039-8b81-412e011fc145"
|
// "https://run.mocky.io/v3/ea705665-2479-4039-8b81-412e011fc145"
|
||||||
);
|
);
|
||||||
const dto = response.data;
|
const dto = response.data;
|
||||||
return create({ data: dto.data, meta: dto.meta });
|
return create({
|
||||||
|
data: dto.data.map((e) => {
|
||||||
|
return {
|
||||||
|
authors: e.authors,
|
||||||
|
content: e.content,
|
||||||
|
id: e.id,
|
||||||
|
summary: e.summary,
|
||||||
|
tags: e.tags,
|
||||||
|
title: e.title,
|
||||||
|
topic: e.topic,
|
||||||
|
}
|
||||||
|
}), meta: dto.meta
|
||||||
|
});
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
if (axios.isAxiosError(reason)) {
|
if (axios.isAxiosError(reason)) {
|
||||||
throw Failure.fromReason(reason, "failures.services.load");
|
throw Failure.fromReason(reason, "failures.services.load");
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Article } from "article/domain/ArticleEntity";
|
import { Article } from "article/domain/articleEntity";
|
||||||
|
|
||||||
export interface SearchResults {
|
export interface SearchResults {
|
||||||
data: Article[];
|
data: Article[];
|
||||||
meta:SearchResultsMeta ;
|
meta: SearchResultsMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchResultsMeta {
|
export interface SearchResultsMeta {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import { articleReducer } from "article/data/articleReducer";
|
||||||
import thunk from "redux-thunk";
|
import thunk from "redux-thunk";
|
||||||
import { searchResultReducer } from "searchResults/data/searchReducer";
|
import { searchResultReducer } from "searchResults/data/searchReducer";
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
searchResults: searchResultReducer,
|
searchResults: searchResultReducer,
|
||||||
|
article: articleReducer,
|
||||||
},
|
},
|
||||||
middleware: [thunk],
|
middleware: [thunk],
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user