├── .eslintignore ├── public ├── config.js ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json ├── index.html ├── logo.svg └── default-themes │ ├── dark.css │ └── light.css ├── src ├── Components │ ├── Nav │ │ ├── UpdateAvailable.scss │ │ ├── NavButton.jsx │ │ ├── UpdateAvailable.jsx │ │ ├── Nav.scss │ │ ├── NavButton.scss │ │ └── MobileTopNav.scss │ ├── Flag │ │ ├── flags.png │ │ ├── Flag.scss │ │ └── Flag.jsx │ ├── Congratulation │ │ ├── Congratulation.scss │ │ └── Congratulation.jsx │ ├── VersionTable │ │ └── VersionTable.scss │ ├── sections.scss │ ├── Timer │ │ ├── CountdownTimer.scss │ │ └── CountdownTimer.jsx │ ├── Layout │ │ ├── CleanLayout │ │ │ ├── CleanLayout.scss │ │ │ └── CleanLayout.jsx │ │ ├── UnauthenticatedLayout │ │ │ ├── UnauthenticatedLayout.scss │ │ │ └── UnauthenticatedLayout.jsx │ │ └── AuthenticatedLayout │ │ │ └── AuthenticatedLayout.scss │ ├── Counter │ │ ├── Counter.jsx │ │ └── Counter.scss │ ├── ConfirmDialog │ │ ├── ConfirmDialog.scss │ │ └── ConfirmDialog.jsx │ ├── Form │ │ ├── ArrayTextInput │ │ │ └── ArrayTextInput.scss │ │ ├── Switch │ │ │ ├── Switch.jsx │ │ │ └── Switch.scss │ │ ├── Textarea │ │ │ ├── Textarea.jsx │ │ │ └── Textarea.scss │ │ └── TextInput │ │ │ └── TextInput.scss │ ├── Indicators │ │ ├── LoadingIndicator │ │ │ ├── LoadingIndicator.jsx │ │ │ └── LoadingIndicator.scss │ │ ├── InviteCodeValidIndicator │ │ │ ├── InviteCodeValidIndicator.scss │ │ │ └── InviteCodeValidIndicator.jsx │ │ ├── PageIndicator │ │ │ ├── PageIndicator.scss │ │ │ └── PageIndicator.jsx │ │ ├── ServerValidIndicator │ │ │ └── ServerValidIndicator.scss │ │ └── PasswordComplexityIndicator │ │ │ ├── PasswordComplexityIndicator.scss │ │ │ └── PasswordComplexityIndicator.jsx │ ├── Tooltip │ │ ├── Tooltip.jsx │ │ └── Tooltip.scss │ ├── Snackbar │ │ ├── Snackbar.jsx │ │ └── Snackbar.scss │ ├── Details │ │ ├── Details.jsx │ │ └── Details.scss │ ├── SlideShow │ │ ├── SlideShow.scss │ │ └── SlideShow.jsx │ ├── ContributorCard │ │ ├── ContributorCard.jsx │ │ └── ContributorCard.scss │ ├── Charts │ │ └── ProgressBar │ │ │ ├── ProgressBar.scss │ │ │ └── ProgressBar.jsx │ ├── CookieConsentBanner │ │ ├── CookieConsentBanner.scss │ │ └── CookieConsentBanner.jsx │ ├── ThemeSelector │ │ └── ThemeSelector.jsx │ ├── LanguageSelector │ │ └── LanguageSelector.jsx │ ├── DirectionBox │ │ ├── DirectionBox.scss │ │ └── DirectionBox.jsx │ ├── InviteCode │ │ ├── InviteCode.scss │ │ └── InviteCode.jsx │ ├── Modal │ │ ├── Modal.scss │ │ └── Modal.jsx │ ├── PackageOverview │ │ └── PackageOverview.scss │ ├── Button │ │ └── Button.jsx │ ├── Table │ │ └── Table.scss │ ├── SelectionBox │ │ ├── SelectionBox.scss │ │ └── SelectionBox.jsx │ └── StatsTable │ │ └── StatsTable.jsx ├── Forms │ ├── ImportPreviewForm │ │ ├── ImportPreviewForm.scss │ │ ├── GroupPreview │ │ │ └── GroupPreview.scss │ │ ├── ImportPreviewForm.jsx │ │ └── PackagePreview │ │ │ └── PackagePreview.scss │ ├── InviteCodeForm │ │ └── InviteCodeForm.scss │ ├── GroupForm │ │ └── GroupForm.scss │ ├── PackageForm │ │ └── PackageForm.scss │ └── VocabForm │ │ └── VocabForm.scss ├── constants.scss ├── images │ ├── icons │ │ ├── check.png │ │ ├── addBtn-blue.png │ │ ├── addBtn-grey.png │ │ ├── addBtn-grey-30px.png │ │ └── hamburger.svg │ ├── logo │ │ ├── vocascan-logo.png │ │ ├── vocascan-round-mac.png │ │ ├── vocascan-round-win.ico │ │ ├── vocascan-round-linux.png │ │ ├── vocascan-github-cover.png │ │ ├── transparent-round.svg │ │ ├── transparent-rect.svg │ │ ├── color-round.svg │ │ └── color-rect.svg │ └── preview │ │ ├── dashboard.png │ │ ├── query-card-back.png │ │ ├── add-vocab-screen.png │ │ ├── query-card-front.png │ │ └── language-selection.png ├── screens │ ├── Learn │ │ ├── Learn.scss │ │ ├── End │ │ │ ├── End.scss │ │ │ └── End.jsx │ │ ├── Learn.jsx │ │ ├── Dashboard │ │ │ ├── Dashboard.scss │ │ │ └── Dashboard.jsx │ │ ├── Query │ │ │ └── Query.scss │ │ └── Direction │ │ │ ├── Direction.scss │ │ │ └── Direction.jsx │ ├── AddVocab │ │ ├── AddVocab.scss │ │ └── AddVocab.jsx │ ├── Guide │ │ ├── Pages │ │ │ ├── VocabDescription │ │ │ │ ├── addVocab.png │ │ │ │ ├── VocabDescription.jsx │ │ │ │ └── VocabDescription.scss │ │ │ ├── GroupDescription │ │ │ │ ├── addGroup1.png │ │ │ │ ├── addGroup2.png │ │ │ │ ├── GroupDescription.jsx │ │ │ │ └── GroupDescription.scss │ │ │ ├── PackageDescription │ │ │ │ ├── addPackage1.png │ │ │ │ ├── addPackage2.png │ │ │ │ ├── PackageDescription.jsx │ │ │ │ └── PackageDescription.scss │ │ │ ├── End │ │ │ │ ├── End.scss │ │ │ │ └── End.jsx │ │ │ └── Start │ │ │ │ ├── Start.scss │ │ │ │ └── Start.jsx │ │ └── Guide.scss │ ├── Library │ │ ├── Library.scss │ │ ├── Library.jsx │ │ ├── AllVocabs │ │ │ └── AllVocabs.scss │ │ ├── AllPackages │ │ │ └── AllPackages.scss │ │ └── AllGroups │ │ │ └── AllGroups.scss │ ├── Custom │ │ ├── Custom.scss │ │ └── Custom.jsx │ ├── Progress │ │ ├── Progress.scss │ │ └── Progress.jsx │ ├── SelectionScreen │ │ └── SelectionScreen.scss │ ├── Auth │ │ ├── EmailVerify │ │ │ └── EmailVerify.scss │ │ ├── Login │ │ │ └── Login.scss │ │ └── Register │ │ │ └── Register.scss │ ├── About │ │ ├── About.scss │ │ └── About.jsx │ ├── Settings │ │ ├── Settings.scss │ │ └── Settings.jsx │ ├── Admin │ │ └── Admin.scss │ └── Profile │ │ └── Profile.scss ├── redux │ ├── Actions │ │ ├── language.js │ │ ├── table.js │ │ ├── setting.js │ │ ├── form.js │ │ ├── index.js │ │ ├── query.js │ │ └── login.js │ ├── Store │ │ └── index.js │ └── Reducers │ │ ├── index.js │ │ ├── table.js │ │ ├── language.js │ │ ├── setting.js │ │ ├── form.js │ │ └── query.js ├── setupTests.js ├── modules │ ├── about.js │ ├── update.js │ ├── clipboard.js │ └── fileOperations.js ├── App.test.js ├── hooks │ ├── useSnack.js │ ├── useLanguage.js │ ├── useTheme.js │ ├── useHumanizer.js │ ├── useFeature.js │ ├── useDebounce.js │ ├── useLinkCreator.js │ └── useScrollBlock.js ├── reportWebVitals.js ├── i18n │ ├── locales │ │ └── locales.json │ └── I18nProvider.js ├── index.scss ├── config.js ├── index.js ├── context │ └── SnackbarContext.jsx ├── utils │ └── index.js ├── logo.svg └── colors.scss ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── config.yml │ └── bug_report.md ├── pull_request_template.md └── workflows │ ├── pullrequest.yml │ └── translations.yml ├── docker ├── nginx.conf ├── docker-compose.yml └── generate-config.js ├── .eslintrc.json ├── Dockerfile.ci ├── .stylelintrc.json ├── SECURITY.md ├── localazy.json ├── Dockerfile ├── .prettierrc ├── .gitignore └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | -------------------------------------------------------------------------------- /public/config.js: -------------------------------------------------------------------------------- 1 | window.VOCASCAN_CONFIG = JSON.parse("{}"); 2 | -------------------------------------------------------------------------------- /src/Components/Nav/UpdateAvailable.scss: -------------------------------------------------------------------------------- 1 | .update-button { 2 | padding: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/Forms/ImportPreviewForm/ImportPreviewForm.scss: -------------------------------------------------------------------------------- 1 | .import-preview { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /src/constants.scss: -------------------------------------------------------------------------------- 1 | $bp-sm: 640px; 2 | $bp-md: 768px; 3 | $bp-lg: 1024px; 4 | $bp-xl: 1280px; 5 | $bp-2xl: 1536px; 6 | -------------------------------------------------------------------------------- /src/images/icons/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/icons/check.png -------------------------------------------------------------------------------- /src/screens/Learn/Learn.scss: -------------------------------------------------------------------------------- 1 | .learn-wrapper { 2 | grid-area: main; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /src/Components/Flag/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/Components/Flag/flags.png -------------------------------------------------------------------------------- /src/images/icons/addBtn-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/icons/addBtn-blue.png -------------------------------------------------------------------------------- /src/images/icons/addBtn-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/icons/addBtn-grey.png -------------------------------------------------------------------------------- /src/images/logo/vocascan-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/logo/vocascan-logo.png -------------------------------------------------------------------------------- /src/images/preview/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/preview/dashboard.png -------------------------------------------------------------------------------- /src/Components/Congratulation/Congratulation.scss: -------------------------------------------------------------------------------- 1 | .congratulation { 2 | font-size: 30px; 3 | text-transform: uppercase; 4 | } 5 | -------------------------------------------------------------------------------- /src/images/icons/addBtn-grey-30px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/icons/addBtn-grey-30px.png -------------------------------------------------------------------------------- /src/images/logo/vocascan-round-mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/logo/vocascan-round-mac.png -------------------------------------------------------------------------------- /src/images/logo/vocascan-round-win.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/logo/vocascan-round-win.ico -------------------------------------------------------------------------------- /src/images/preview/query-card-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/preview/query-card-back.png -------------------------------------------------------------------------------- /src/images/logo/vocascan-round-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/logo/vocascan-round-linux.png -------------------------------------------------------------------------------- /src/images/preview/add-vocab-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/preview/add-vocab-screen.png -------------------------------------------------------------------------------- /src/images/preview/query-card-front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/preview/query-card-front.png -------------------------------------------------------------------------------- /src/screens/AddVocab/AddVocab.scss: -------------------------------------------------------------------------------- 1 | .add-vocab-form { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | -------------------------------------------------------------------------------- /src/images/logo/vocascan-github-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/logo/vocascan-github-cover.png -------------------------------------------------------------------------------- /src/images/preview/language-selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/images/preview/language-selection.png -------------------------------------------------------------------------------- /src/Components/VersionTable/VersionTable.scss: -------------------------------------------------------------------------------- 1 | .version-table { 2 | .table-cell { 3 | text-align: start; 4 | white-space: pre-wrap; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/screens/Guide/Pages/VocabDescription/addVocab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/screens/Guide/Pages/VocabDescription/addVocab.png -------------------------------------------------------------------------------- /src/screens/Guide/Pages/GroupDescription/addGroup1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/screens/Guide/Pages/GroupDescription/addGroup1.png -------------------------------------------------------------------------------- /src/screens/Guide/Pages/GroupDescription/addGroup2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/screens/Guide/Pages/GroupDescription/addGroup2.png -------------------------------------------------------------------------------- /src/Components/sections.scss: -------------------------------------------------------------------------------- 1 | .routed-section { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | height: 100vh; 6 | background: #f1f3fa; 7 | } 8 | -------------------------------------------------------------------------------- /src/screens/Guide/Pages/PackageDescription/addPackage1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/screens/Guide/Pages/PackageDescription/addPackage1.png -------------------------------------------------------------------------------- /src/screens/Guide/Pages/PackageDescription/addPackage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocascan/vocascan-frontend/HEAD/src/screens/Guide/Pages/PackageDescription/addPackage2.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an feature for vocascan 4 | 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | location / { 5 | root /usr/share/nginx/html/; 6 | include /etc/nginx/mime.types; 7 | try_files $uri $uri/ /index.html; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Components/Timer/CountdownTimer.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | 3 | .countdown-timer-expired { 4 | color: $color-red; 5 | } 6 | 7 | .countdown-timer-running { 8 | color: $color-green; 9 | } 10 | -------------------------------------------------------------------------------- /src/images/icons/hamburger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/screens/Guide/Guide.scss: -------------------------------------------------------------------------------- 1 | @import "../../constants"; 2 | 3 | .skip-button { 4 | position: absolute; 5 | top: 20px; 6 | right: 20px; 7 | 8 | @media screen and (min-width: $bp-md) { 9 | right: 60px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "react-app/jest", "plugin:prettier/recommended"], 3 | "rules": { 4 | "prettier/prettier": "warn" 5 | }, 6 | "overrides": [ 7 | { 8 | "files": "*.jsx" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/redux/Actions/language.js: -------------------------------------------------------------------------------- 1 | import { SET_LANGUAGES } from "./index.js"; 2 | 3 | export const setLanguages = ({ languages }) => { 4 | return { 5 | type: SET_LANGUAGES, 6 | payload: { 7 | languages, 8 | }, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/Components/Layout/CleanLayout/CleanLayout.scss: -------------------------------------------------------------------------------- 1 | @import "../../../constants"; 2 | 3 | .clean-container { 4 | display: flex; 5 | width: 100vw; 6 | height: 100vh; 7 | 8 | @media (min-width: $bp-md) { 9 | width: 100%; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Components/Counter/Counter.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./Counter.scss"; 4 | 5 | const Counter = ({ count, color }) => { 6 | return {count}; 7 | }; 8 | 9 | export default Counter; 10 | -------------------------------------------------------------------------------- /src/redux/Actions/table.js: -------------------------------------------------------------------------------- 1 | import { SET_TABLE_PAGE_SIZE } from "./index.js"; 2 | 3 | export const setTablePageSize = ({ pageSize }) => { 4 | return { 5 | type: SET_TABLE_PAGE_SIZE, 6 | payload: { 7 | pageSize, 8 | }, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | frontend: 5 | image: vocascan/frontend:latest 6 | restart: always 7 | tty: true 8 | environment: 9 | VOCASCAN_BASE_URL: "https://mydomain.com" 10 | ports: 11 | - "80:80" 12 | -------------------------------------------------------------------------------- /src/Components/Layout/CleanLayout/CleanLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./CleanLayout.scss"; 4 | 5 | const CleanLayout = ({ children }) => { 6 | return
{children}
; 7 | }; 8 | 9 | export default CleanLayout; 10 | -------------------------------------------------------------------------------- /src/modules/about.js: -------------------------------------------------------------------------------- 1 | let getDesktopVersions = () => Promise.resolve(); 2 | 3 | if (window.VOCASCAN_CONFIG.ENV === "electron") { 4 | getDesktopVersions = () => { 5 | return window.electron.invoke("getVersions"); 6 | }; 7 | } 8 | 9 | export { getDesktopVersions }; 10 | -------------------------------------------------------------------------------- /Dockerfile.ci: -------------------------------------------------------------------------------- 1 | FROM nginx:1-alpine 2 | 3 | RUN apk add nodejs 4 | 5 | COPY ./build /usr/share/nginx/html 6 | COPY docker/nginx.conf /etc/nginx/conf.d/default.conf 7 | COPY docker/generate-config.js / 8 | 9 | EXPOSE 80 10 | 11 | CMD node generate-config.js && nginx -g "daemon off;" 12 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-sass-guidelines", "stylelint-config-recess-order"], 3 | "rules": { 4 | "order/properties-alphabetical-order": null, 5 | "max-nesting-depth": 4, 6 | "string-quotes": "double", 7 | "selector-max-compound-selectors": null 8 | } 9 | } -------------------------------------------------------------------------------- /src/screens/Library/Library.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .library-wrapper { 5 | padding: 50px 12px; 6 | 7 | @media screen and (min-width: $bp-md) { 8 | padding: 50px; 9 | } 10 | 11 | .title { 12 | margin-bottom: 20px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | 3 | import App from "./App.jsx"; 4 | 5 | test("renders learn react link", () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/screens/Guide/Pages/End/End.scss: -------------------------------------------------------------------------------- 1 | .end { 2 | display: flex; 3 | justify-content: center; 4 | width: 100%; 5 | height: 100%; 6 | 7 | .end-heading { 8 | margin: auto; 9 | text-transform: uppercase; 10 | 11 | h1 { 12 | margin-bottom: 10px; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/screens/Guide/Pages/Start/Start.scss: -------------------------------------------------------------------------------- 1 | .start { 2 | display: flex; 3 | justify-content: center; 4 | width: 100%; 5 | height: 100%; 6 | 7 | .start-heading { 8 | margin: auto; 9 | text-transform: uppercase; 10 | 11 | h1 { 12 | margin-bottom: 10px; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | There is no released version so far 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.0.0 | :x: | 10 | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Go to issues tab and create a new bug report 15 | -------------------------------------------------------------------------------- /src/hooks/useSnack.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | 3 | import SnackbarContext from "../context/SnackbarContext.jsx"; 4 | 5 | const useSnack = () => { 6 | const { showSnack } = useContext(SnackbarContext); 7 | 8 | return { 9 | showSnack, 10 | }; 11 | }; 12 | 13 | export default useSnack; 14 | -------------------------------------------------------------------------------- /src/screens/Custom/Custom.scss: -------------------------------------------------------------------------------- 1 | @import "../../constants"; 2 | 3 | .custom { 4 | display: flex; 5 | grid-area: main; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100%; 9 | height: 100vh; 10 | text-align: center; 11 | 12 | @media screen and (min-width: $bp-md) { 13 | height: 100%; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/screens/Progress/Progress.scss: -------------------------------------------------------------------------------- 1 | @import "../../constants"; 2 | 3 | .progress { 4 | display: flex; 5 | grid-area: main; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100%; 9 | height: 100vh; 10 | text-align: center; 11 | 12 | @media screen and (min-width: $bp-md) { 13 | height: 100%; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/update.js: -------------------------------------------------------------------------------- 1 | let updateNotifier = null; 2 | let startUpdate = null; 3 | const available = window.VOCASCAN_CONFIG.ENV === "electron"; 4 | 5 | if (available) { 6 | updateNotifier = window.electron; 7 | 8 | startUpdate = () => { 9 | window.electron.send("start-update"); 10 | }; 11 | } 12 | 13 | export { updateNotifier, startUpdate, available }; 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: GitHub Discussions 4 | url: https://github.com/vocascan/vocascan-frontend/discussions 5 | about: Please ask and answer questions here. 6 | - name: Discord community server 7 | url: https://discord.gg/Q3Qp72sKaQ 8 | about: If you have discord, you can also ask questions here. 9 | -------------------------------------------------------------------------------- /src/Components/ConfirmDialog/ConfirmDialog.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | 3 | .submit-dialog { 4 | display: flex; 5 | flex-direction: column; 6 | width: 80%; 7 | max-width: 400px; 8 | 9 | .description { 10 | padding: 20px 0; 11 | } 12 | 13 | .actions { 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Components/Form/ArrayTextInput/ArrayTextInput.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | 3 | .array-input-wrapper { 4 | display: flex; 5 | align-items: center; 6 | height: 53px; 7 | margin: 5px 0; 8 | } 9 | 10 | .add-input-wrapper { 11 | display: flex; 12 | justify-content: center; 13 | margin-top: 10px; 14 | 15 | .add-text { 16 | margin-left: 5px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/Store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import storeSynchronize from "redux-localstore"; 3 | 4 | import allReducers from "../Reducers/index.js"; 5 | 6 | const store = createStore(allReducers, window.__REDUX_DEVTOOLS_EXTENSION__?.()); 7 | 8 | export default store; 9 | 10 | storeSynchronize(store, { 11 | whitelist: ["login", "setting", "form", "table", "language"], 12 | }); 13 | -------------------------------------------------------------------------------- /src/redux/Actions/setting.js: -------------------------------------------------------------------------------- 1 | import { SET_LANGUAGE, SET_THEME } from "./index.js"; 2 | 3 | export const setLanguage = ({ language }) => { 4 | return { 5 | type: SET_LANGUAGE, 6 | payload: { 7 | language, 8 | }, 9 | }; 10 | }; 11 | 12 | export const setTheme = ({ theme }) => { 13 | return { 14 | type: SET_THEME, 15 | payload: { 16 | theme, 17 | }, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/Components/Indicators/LoadingIndicator/LoadingIndicator.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./LoadingIndicator.scss"; 4 | 5 | const LoadingIndicator = ({ size, position }) => { 6 | return ( 7 |
8 |
9 |
10 | ); 11 | }; 12 | 13 | export default LoadingIndicator; 14 | -------------------------------------------------------------------------------- /src/Components/Indicators/InviteCodeValidIndicator/InviteCodeValidIndicator.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | 3 | .invite-code-valid-indicator { 4 | margin-top: -5px; 5 | font-size: 11px; 6 | 7 | .valid { 8 | height: 29px; 9 | color: $color-green-dark; 10 | text-align: right; 11 | } 12 | 13 | .error { 14 | height: 29px; 15 | color: $color-red-dark; 16 | text-align: right; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useLanguage.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | const useLanguage = (code) => { 5 | const languages = useSelector((state) => state.language.languages); 6 | 7 | const language = useMemo( 8 | () => languages?.find((language) => language.code === code), 9 | [code, languages] 10 | ); 11 | 12 | return language; 13 | }; 14 | 15 | export default useLanguage; 16 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/Components/Indicators/PageIndicator/PageIndicator.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | 3 | .indicator { 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: space-between; 7 | width: auto; 8 | 9 | .indicator-dot { 10 | width: 10px; 11 | height: 10px; 12 | margin: 0 3px; 13 | background: $color-grey; 14 | border-radius: 100%; 15 | 16 | &.active { 17 | background: $color-primary; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/screens/Custom/Custom.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import "./Custom.scss"; 5 | 6 | const Custom = () => { 7 | const { t } = useTranslation(); 8 | 9 | return ( 10 |
11 |
12 |

{t("screens.custom.title")}

13 |

{t("global.comingSoon")}

14 |
15 |
16 | ); 17 | }; 18 | 19 | export default Custom; 20 | -------------------------------------------------------------------------------- /localazy.json: -------------------------------------------------------------------------------- 1 | { 2 | "upload": { 3 | "type": "json", 4 | "files": "src/i18n/locales/en/default.json", 5 | "deprecate": "file", 6 | "features": ["array", "filter_untranslated", "plural_postfix_us"] 7 | }, 8 | "download": { 9 | "metadataFileJson": "src/i18n/locales/locales.json", 10 | "files": { 11 | "conditions": ["equals: ${file}, default.json"], 12 | "output": "src/i18n/locales/${lang}/default.json" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Components/Indicators/PageIndicator/PageIndicator.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./PageIndicator.scss"; 4 | 5 | const PageIndicator = ({ max, pageNumber }) => { 6 | const dots = [...Array(max)].map((_, i) => ( 7 |
11 | )); 12 | 13 | return
{dots}
; 14 | }; 15 | 16 | export default PageIndicator; 17 | -------------------------------------------------------------------------------- /src/screens/Progress/Progress.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import "./Progress.scss"; 5 | 6 | const Progress = () => { 7 | const { t } = useTranslation(); 8 | 9 | return ( 10 |
11 |
12 |

{t("screens.progress.title")}

13 |

{t("global.comingSoon")}

14 |
15 |
16 | ); 17 | }; 18 | 19 | export default Progress; 20 | -------------------------------------------------------------------------------- /src/modules/clipboard.js: -------------------------------------------------------------------------------- 1 | let copyToClip = () => Promise.reject(); 2 | 3 | if (window.VOCASCAN_CONFIG.ENV === "electron") { 4 | copyToClip = ({ text }) => { 5 | return window.electron.invoke("copy-to-clip", { text }); 6 | }; 7 | } 8 | 9 | if (window.VOCASCAN_CONFIG.ENV === "web") { 10 | if (navigator.clipboard) { 11 | copyToClip = ({ text }) => { 12 | return navigator.clipboard.writeText(text); 13 | }; 14 | } 15 | } 16 | 17 | export { copyToClip }; 18 | -------------------------------------------------------------------------------- /src/screens/AddVocab/AddVocab.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import VocabForm from "../../Forms/VocabForm/VocabForm.jsx"; 5 | 6 | import "./AddVocab.scss"; 7 | 8 | const AddVocab = () => { 9 | const { t } = useTranslation(); 10 | 11 | return ( 12 |
13 | 14 |
15 | ); 16 | }; 17 | 18 | export default AddVocab; 19 | -------------------------------------------------------------------------------- /src/Components/Flag/Flag.scss: -------------------------------------------------------------------------------- 1 | .flag { 2 | display: flex; 3 | justify-content: center; 4 | border-radius: 10px; 5 | transition: all 0.2s ease-in-out; 6 | 7 | &:hover { 8 | cursor: pointer; 9 | transition: all 0.2s ease-in-out; 10 | transform: scale(1.1); 11 | } 12 | 13 | span { 14 | display: inline-block; 15 | 16 | // box shadow color 17 | color: #eee; 18 | background-image: url("flags.png"); 19 | background-repeat: no-repeat; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | COPY package-lock.json . 7 | 8 | RUN npm install 9 | 10 | COPY . . 11 | 12 | RUN npm run build 13 | 14 | FROM nginx:1-alpine as production 15 | 16 | RUN apk add nodejs 17 | 18 | COPY --from=builder /app/build /usr/share/nginx/html 19 | COPY docker/nginx.conf /etc/nginx/conf.d/default.conf 20 | COPY docker/generate-config.js / 21 | 22 | EXPOSE 80 23 | 24 | CMD node generate-config.js && nginx -g "daemon off;" 25 | -------------------------------------------------------------------------------- /src/Forms/InviteCodeForm/InviteCodeForm.scss: -------------------------------------------------------------------------------- 1 | .invite-code-form { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | width: 100%; 7 | height: 100%; 8 | 9 | .form-wrapper { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | width: 70%; 14 | height: 50%; 15 | 16 | .select-wrapper { 17 | width: 100%; 18 | height: auto; 19 | } 20 | 21 | .submit-btn { 22 | margin-top: 20px; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Components/Tooltip/Tooltip.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactTooltip from "react-tooltip"; 3 | 4 | import "./Tooltip.scss"; 5 | 6 | const Tooltip = ({ 7 | place = "bottom", 8 | effect = "solid", 9 | type = "default", 10 | id = null, 11 | children, 12 | }) => { 13 | return ( 14 | 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | export default Tooltip; 26 | -------------------------------------------------------------------------------- /src/Components/Indicators/ServerValidIndicator/ServerValidIndicator.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | 3 | .server-valid-indicator { 4 | margin-top: -5px; 5 | font-size: 11px; 6 | 7 | .error { 8 | height: 29px; 9 | color: $color-red-dark; 10 | text-align: right; 11 | } 12 | 13 | .success { 14 | display: flex; 15 | align-items: center; 16 | justify-content: flex-end; 17 | height: 29px; 18 | color: $color-green-dark; 19 | text-align: right; 20 | 21 | svg { 22 | margin-left: 5px; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Components/Counter/Counter.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | 3 | .counter { 4 | display: inline-block; 5 | min-width: 7px; 6 | height: 18px; 7 | padding: 0 6px; 8 | font-size: 12px; 9 | line-height: 18px; 10 | color: #fff; 11 | text-align: center; 12 | vertical-align: middle; 13 | border-radius: 9px; 14 | 15 | &.primary { 16 | background-color: $color-primary; 17 | } 18 | 19 | &.primary-dark { 20 | background-color: $color-primary-dark; 21 | } 22 | 23 | &.background-inverse { 24 | background-color: $color-background-inverse; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/screens/SelectionScreen/SelectionScreen.scss: -------------------------------------------------------------------------------- 1 | .select-srn-wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | margin: auto; 5 | 6 | .select-srn-header-wrapper { 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | 11 | margin: 20px auto; 12 | } 13 | 14 | .boxes { 15 | display: flex; 16 | justify-content: space-around; 17 | margin: auto; 18 | } 19 | 20 | .select-srn-title, 21 | .select-srn-heading { 22 | text-transform: uppercase; 23 | } 24 | } 25 | 26 | .swiper { 27 | margin: 64px 0 32px; 28 | } 29 | -------------------------------------------------------------------------------- /src/screens/Learn/End/End.scss: -------------------------------------------------------------------------------- 1 | @import "../../../constants"; 2 | 3 | .end-screen { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: space-around; 7 | width: 100%; 8 | max-width: 600px; 9 | height: 100vh; 10 | margin: 0 auto; 11 | 12 | @media screen and (min-width: $bp-md) { 13 | height: 100%; 14 | } 15 | 16 | .end-screen-inner { 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: space-around; 20 | height: 50%; 21 | } 22 | 23 | .percentage-text { 24 | margin: 15px 0; 25 | font-size: 16px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/screens/Guide/Pages/Start/Start.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import "./Start.scss"; 5 | 6 | const Start = ({ setCanContinue }) => { 7 | const { t } = useTranslation(); 8 | 9 | useEffect(() => { 10 | setCanContinue(true); 11 | }, [setCanContinue]); 12 | 13 | return ( 14 |
15 |
16 |

Vocascan

17 |

{t("screens.guide.start.slogan")}

18 |
19 |
20 | ); 21 | }; 22 | 23 | export default Start; 24 | -------------------------------------------------------------------------------- /src/redux/Reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import formReducer from "./form.js"; 4 | import languageReducer from "./language.js"; 5 | import loginReducer from "./login.js"; 6 | import queryReducer from "./query.js"; 7 | import settingReducer from "./setting.js"; 8 | import tableReducer from "./table.js"; 9 | 10 | const allReducers = combineReducers({ 11 | login: loginReducer, 12 | setting: settingReducer, 13 | form: formReducer, 14 | table: tableReducer, 15 | query: queryReducer, 16 | language: languageReducer, 17 | }); 18 | 19 | export default allReducers; 20 | -------------------------------------------------------------------------------- /src/redux/Reducers/table.js: -------------------------------------------------------------------------------- 1 | import { defineState } from "redux-localstore"; 2 | 3 | import { SET_TABLE_PAGE_SIZE } from "../Actions/index.js"; 4 | 5 | const defaultState = { 6 | pageSize: 10, 7 | }; 8 | 9 | const initialState = defineState(defaultState)("table"); 10 | 11 | const tableReducer = (state = initialState, action) => { 12 | switch (action.type) { 13 | case SET_TABLE_PAGE_SIZE: 14 | return { 15 | ...state, 16 | pageSize: action.payload.pageSize, 17 | }; 18 | 19 | default: 20 | return state; 21 | } 22 | }; 23 | 24 | export default tableReducer; 25 | -------------------------------------------------------------------------------- /src/redux/Reducers/language.js: -------------------------------------------------------------------------------- 1 | import { defineState } from "redux-localstore"; 2 | 3 | import { SET_LANGUAGES } from "../Actions/index.js"; 4 | 5 | const defaultState = { 6 | languages: [], 7 | }; 8 | 9 | const initialState = defineState(defaultState)("language"); 10 | 11 | const languageReducer = (state = initialState, action) => { 12 | switch (action.type) { 13 | case SET_LANGUAGES: 14 | return { 15 | ...state, 16 | languages: action.payload.languages, 17 | }; 18 | 19 | default: 20 | return state; 21 | } 22 | }; 23 | 24 | export default languageReducer; 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "endOfLine": "auto", 4 | "trailingComma": "es5", 5 | "printWidth": 80, 6 | "singleQuote": false, 7 | "semi": true, 8 | "useTabs": false, 9 | "importOrder": [ 10 | "^./config.js$", 11 | "^(react|uniqid|i18next|redux|axios|clsx|semver|@testing-library)(.*)$", 12 | "^@material-ui/core(.*)$", 13 | "^@material-ui/icons(.*)$", 14 | "^(.*).jsx$", 15 | "^(.*).js$", 16 | "^(.*).(png|jpg|svg|json)$", 17 | "^(.*).(scss)$", 18 | ".*" 19 | ], 20 | "importOrderSeparation": true, 21 | "experimentalBabelParserPluginsList": ["jsx"] 22 | } 23 | -------------------------------------------------------------------------------- /src/Components/Snackbar/Snackbar.jsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import React, { useContext } from "react"; 3 | 4 | import SnackbarContext from "../../context/SnackbarContext.jsx"; 5 | 6 | import "./Snackbar.scss"; 7 | 8 | const Snackbar = () => { 9 | const { snack } = useContext(SnackbarContext); 10 | 11 | const classes = clsx( 12 | "snackbar", 13 | `snackbar-${snack.variant}`, 14 | snack.show && "show" 15 | ); 16 | 17 | return ( 18 |
19 | {snack.text} 20 |
21 | ); 22 | }; 23 | 24 | export default Snackbar; 25 | -------------------------------------------------------------------------------- /src/i18n/locales/locales.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectUrl": "https://localazy.com/p/vocascan", 3 | "baseLocale": "en", 4 | "languages": [ 5 | { 6 | "language": "en", 7 | "region": "", 8 | "script": "", 9 | "isRtl": false, 10 | "name": "English", 11 | "localizedName": "English" 12 | }, 13 | { 14 | "language": "de", 15 | "region": "", 16 | "script": "", 17 | "isRtl": false, 18 | "name": "German", 19 | "localizedName": "Deutsch" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /src/screens/Guide/Pages/End/End.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import "./End.scss"; 5 | 6 | const End = ({ setCanContinue }) => { 7 | const { t } = useTranslation(); 8 | 9 | useEffect(() => { 10 | setCanContinue(true); 11 | }, [setCanContinue]); 12 | 13 | return ( 14 |
15 |
16 |

{t("screens.guide.end.heading")}

17 |

{t("screens.guide.end.description")}

18 |
19 |
20 | ); 21 | }; 22 | 23 | export default End; 24 | -------------------------------------------------------------------------------- /src/Components/Layout/UnauthenticatedLayout/UnauthenticatedLayout.scss: -------------------------------------------------------------------------------- 1 | .unauthenticated-container { 2 | position: relative; 3 | display: flex; 4 | justify-content: center; 5 | width: 100%; 6 | height: auto; 7 | min-height: 100vh; 8 | background-image: linear-gradient(to top right, #60a5fa, #f9a8d4); 9 | 10 | .language-selector-wrapper { 11 | position: absolute; 12 | top: 10px; 13 | right: 10px; 14 | } 15 | 16 | .select-language-modal-inner { 17 | align-self: center; 18 | width: 250px; 19 | margin: 20px 0 30px; 20 | } 21 | 22 | .select-language-modal-close { 23 | align-self: center; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useTheme.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | import { defaultTheme } from "../utils/constants"; 5 | 6 | const themeStyleComponent = document.createElement("link"); 7 | themeStyleComponent.rel = "stylesheet"; 8 | document.head.appendChild(themeStyleComponent); 9 | 10 | const useTheme = () => { 11 | const theme = useSelector((state) => state.setting.theme); 12 | useEffect(() => { 13 | const key = theme in window.VOCASCAN_CONFIG.themes ? theme : defaultTheme; 14 | themeStyleComponent.href = window.VOCASCAN_CONFIG.themes[key]; 15 | }, [theme]); 16 | }; 17 | 18 | export default useTheme; 19 | -------------------------------------------------------------------------------- /src/redux/Actions/form.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_VOCAB_ACTIVE, 3 | SET_VOCAB_ACTIVATE, 4 | SET_GROUP_ACTIVATE, 5 | } from "./index.js"; 6 | 7 | export const setVocabActive = ({ active }) => { 8 | return { 9 | type: SET_VOCAB_ACTIVE, 10 | payload: { 11 | active, 12 | }, 13 | }; 14 | }; 15 | 16 | export const setVocabActivate = ({ activate }) => { 17 | return { 18 | type: SET_VOCAB_ACTIVATE, 19 | payload: { 20 | activate, 21 | }, 22 | }; 23 | }; 24 | 25 | export const setGroupActive = ({ active }) => { 26 | return { 27 | type: SET_GROUP_ACTIVATE, 28 | payload: { 29 | active, 30 | }, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/Components/Layout/AuthenticatedLayout/AuthenticatedLayout.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | @import "../../../constants"; 3 | 4 | .root { 5 | width: 90%; 6 | height: 100vh; 7 | padding: 0; 8 | margin: 0 auto; 9 | 10 | @media (min-width: $bp-md) { 11 | display: grid; 12 | grid-template-areas: 13 | "topnav topnav" 14 | "sidenav main"; 15 | grid-template-rows: 50px auto; 16 | grid-template-columns: 65px auto; 17 | width: 100%; 18 | margin: 0; 19 | overflow-x: hidden; 20 | } 21 | 22 | .server-error-modal-inner { 23 | padding: 30px 0; 24 | } 25 | 26 | .content { 27 | overflow-y: auto; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Components/Nav/NavButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | import "./NavButton.scss"; 5 | 6 | const NavButton = ({ name, link, icon = null, exact = false }) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default NavButton; 24 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Vocascan", 3 | "name": "Vocascan Web", 4 | "description": "A highly configurable vocabulary trainer", 5 | "categories": ["education"], 6 | "icons": [ 7 | { 8 | "src": "favicon.ico", 9 | "sizes": "64x64 32x32 24x24 16x16", 10 | "type": "image/x-icon" 11 | }, 12 | { 13 | "src": "logo192.png", 14 | "type": "image/png", 15 | "sizes": "192x192" 16 | }, 17 | { 18 | "src": "logo512.png", 19 | "type": "image/png", 20 | "sizes": "512x512" 21 | } 22 | ], 23 | "start_url": ".", 24 | "display": "standalone", 25 | "theme_color": "#2a2e36", 26 | "background_color": "#2a2e36" 27 | } 28 | -------------------------------------------------------------------------------- /src/Components/Details/Details.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Collapsible from "react-collapsible"; 3 | 4 | import Counter from "../Counter/Counter.jsx"; 5 | 6 | import "./Details.scss"; 7 | 8 | const Details = ({ summary, count, children, ...props }) => { 9 | return ( 10 | 13 |
{summary}
14 | {count && } 15 |
16 | } 17 | transitionTime={200} 18 | easing="ease-in-out" 19 | {...props} 20 | > 21 | {children} 22 | 23 | ); 24 | }; 25 | 26 | export default Details; 27 | -------------------------------------------------------------------------------- /src/Components/Indicators/PasswordComplexityIndicator/PasswordComplexityIndicator.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | 3 | .password-complexity-indicator { 4 | width: 100%; 5 | height: 6px; 6 | margin-top: 12px; 7 | background-color: #eee; 8 | border-radius: 5px; 9 | 10 | .bar { 11 | width: 0%; 12 | height: 6px; 13 | border-radius: 3px; 14 | transition: all 0.25s ease-in; 15 | 16 | &.complexity-0, 17 | &.complexity-1, 18 | &.complexity-2 { 19 | background-color: $color-red; 20 | } 21 | 22 | &.complexity-3 { 23 | background-color: $color-yellow; 24 | } 25 | 26 | &.complexity-4, 27 | &.complexity-5 { 28 | background-color: $color-green; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Components/Indicators/LoadingIndicator/LoadingIndicator.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | 3 | .loading-indicator { 4 | display: flex; 5 | width: 100%; 6 | 7 | .circle { 8 | width: 20px; 9 | height: 20px; 10 | border: 2px solid $color-primary-light; 11 | border-top: 2px solid $color-primary-dark; 12 | border-radius: 50%; 13 | animation: spin 1.5s ease-in-out infinite; 14 | 15 | &.large { 16 | width: 120px; 17 | height: 120px; 18 | } 19 | } 20 | 21 | &.center { 22 | align-items: center; 23 | justify-content: center; 24 | } 25 | } 26 | 27 | @keyframes spin { 28 | 0% { 29 | transform: rotate(0deg); 30 | } 31 | 32 | 100% { 33 | transform: rotate(360deg); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vocascan Web 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Components/SlideShow/SlideShow.scss: -------------------------------------------------------------------------------- 1 | .slideshow { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | width: 100%; 6 | height: 100%; 7 | 8 | .slideshow-content { 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | width: 100%; 13 | height: 90%; 14 | overflow: auto; 15 | 16 | } 17 | 18 | .slideshow-bar { 19 | display: flex; 20 | flex-direction: row; 21 | justify-content: space-between; 22 | height: 10%; 23 | max-height: 50px; 24 | 25 | .bar-property { 26 | &.invisible { 27 | pointer-events: none; 28 | visibility: hidden; 29 | } 30 | 31 | &.indicator { 32 | margin: auto 0; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/screens/Auth/EmailVerify/EmailVerify.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | @import "../../../constants"; 3 | 4 | .email-verify { 5 | width: 80%; 6 | margin: 0 auto; 7 | 8 | @media screen and (min-width: $bp-md) { 9 | max-width: 250px; 10 | } 11 | 12 | h2 { 13 | margin-bottom: 10px; 14 | } 15 | 16 | p { 17 | margin-bottom: 10px; 18 | } 19 | 20 | .check-mark-icon { 21 | font-size: 75px; 22 | color: $color-green; 23 | } 24 | 25 | .login-footer button { 26 | margin-bottom: 10px; 27 | } 28 | 29 | .dont-received { 30 | margin-top: 10px; 31 | 32 | span { 33 | color: $color-primary; 34 | cursor: pointer; 35 | } 36 | 37 | span:hover { 38 | text-decoration: underline; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/redux/Reducers/setting.js: -------------------------------------------------------------------------------- 1 | import { defineState } from "redux-localstore"; 2 | 3 | import { defaultTheme } from "../../utils/constants.js"; 4 | import { SET_LANGUAGE, SET_THEME } from "../Actions/index.js"; 5 | 6 | const defaultState = { 7 | language: "en", 8 | theme: defaultTheme, 9 | }; 10 | 11 | const initialState = defineState(defaultState)("setting"); 12 | 13 | const loginReducer = (state = initialState, action) => { 14 | switch (action.type) { 15 | case SET_LANGUAGE: 16 | return { 17 | ...state, 18 | language: action.payload.language, 19 | }; 20 | 21 | case SET_THEME: 22 | return { 23 | ...state, 24 | theme: action.payload.theme, 25 | }; 26 | 27 | default: 28 | return state; 29 | } 30 | }; 31 | 32 | export default loginReducer; 33 | -------------------------------------------------------------------------------- /src/Forms/ImportPreviewForm/GroupPreview/GroupPreview.scss: -------------------------------------------------------------------------------- 1 | @import "../../../constants"; 2 | 3 | .group-preview { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-between; 8 | width: 100%; 9 | height: 100%; 10 | overflow-y: auto; 11 | 12 | .update-fields { 13 | width: 100%; 14 | 15 | .table-wrapper { 16 | overflow-x: scroll; 17 | @media screen and (min-width: $bp-md) { 18 | overflow-x: hidden; 19 | } 20 | } 21 | 22 | .customizables { 23 | margin-bottom: 20px; 24 | 25 | .update-fields { 26 | height: 80%; 27 | overflow-y: auto; 28 | } 29 | } 30 | 31 | .submit-btn { 32 | display: flex; 33 | justify-content: center; 34 | margin-top: 40px; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/screens/Learn/Learn.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch, useRouteMatch } from "react-router"; 3 | 4 | import Dashboard from "./Dashboard/Dashboard.jsx"; 5 | import Direction from "./Direction/Direction.jsx"; 6 | import End from "./End/End.jsx"; 7 | import Query from "./Query/Query.jsx"; 8 | 9 | import "./Learn.scss"; 10 | 11 | const Learn = () => { 12 | const { path } = useRouteMatch(); 13 | 14 | return ( 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default Learn; 27 | -------------------------------------------------------------------------------- /src/modules/fileOperations.js: -------------------------------------------------------------------------------- 1 | const parseFile = async (event) => { 2 | return new Promise((resolve, reject) => { 3 | const reader = new FileReader(); 4 | reader.onerror = reject; 5 | reader.onload = (event) => { 6 | resolve(JSON.parse(event.target.result)); 7 | }; 8 | reader.readAsText(event.target.files[0]); 9 | }); 10 | }; 11 | 12 | const saveFile = ({ input, name, type }) => { 13 | //create temp a tag to download file 14 | const element = document.createElement("a"); 15 | const file = new Blob([JSON.stringify(input)], { 16 | type, 17 | }); 18 | element.href = URL.createObjectURL(file); 19 | element.href = URL.createObjectURL(file); 20 | element.download = `${name}.json`; 21 | document.body.appendChild(element); // Required for this to work in FireFox 22 | element.click(); 23 | }; 24 | 25 | export { parseFile, saveFile }; 26 | -------------------------------------------------------------------------------- /src/Components/ContributorCard/ContributorCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import PersonIcon from "@material-ui/icons/Person"; 4 | 5 | import "./ContributorCard.scss"; 6 | 7 | const Card = ({ name, url, imageUrl, description }) => { 8 | return ( 9 | 10 |
11 |
12 |
{name}
13 |
{description}
14 |
15 | {imageUrl ? ( 16 | {name} 17 | ) : ( 18 |
19 | 20 |
21 | )} 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Card; 28 | -------------------------------------------------------------------------------- /src/Forms/GroupForm/GroupForm.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .group-form { 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: space-between; 8 | width: 100%; 9 | max-width: 700px; 10 | height: 75%; 11 | 12 | label { 13 | text-align: left; 14 | } 15 | 16 | h3 { 17 | margin: 0; 18 | font-size: 15px; 19 | text-align: left; 20 | } 21 | 22 | .dropdown { 23 | z-index: 4; 24 | display: flex; 25 | flex-direction: column; 26 | 27 | @media screen and (min-width: $bp-md) { 28 | flex-direction: row; 29 | justify-content: space-between; 30 | height: 50px; 31 | margin: 0 -10px; 32 | } 33 | 34 | .select-wrapper { 35 | width: 100%; 36 | 37 | @media screen and (min-width: $bp-md) { 38 | margin: 0 10px; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Forms/PackageForm/PackageForm.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .language-package-form { 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: space-between; 8 | width: 100%; 9 | max-width: 700px; 10 | height: 75%; 11 | 12 | label { 13 | text-align: left; 14 | } 15 | 16 | h3 { 17 | margin: 0; 18 | font-size: 15px; 19 | text-align: left; 20 | } 21 | 22 | .dropdown { 23 | z-index: 4; 24 | display: flex; 25 | flex-direction: column; 26 | 27 | @media screen and (min-width: $bp-md) { 28 | flex-direction: row; 29 | justify-content: space-between; 30 | height: 50px; 31 | margin: 0 -10px; 32 | } 33 | 34 | .select-wrapper { 35 | width: 100%; 36 | 37 | @media screen and (min-width: $bp-md) { 38 | margin: 0 10px; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/screens/About/About.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .about { 5 | display: flex; 6 | grid-area: main; 7 | justify-content: center; 8 | width: 100%; 9 | margin: 0 auto; 10 | 11 | .wrapper { 12 | display: flex; 13 | flex-direction: column; 14 | width: 100%; 15 | padding: 0; 16 | margin: 80px 0; 17 | 18 | @media screen and (min-width: $bp-md) { 19 | width: 90%; 20 | } 21 | 22 | h1 { 23 | margin-bottom: 30px; 24 | } 25 | 26 | .dependency-card { 27 | padding: 10px; 28 | margin: 6px; 29 | color: #fff; 30 | text-decoration: none; 31 | background-color: $color-background-inverse; 32 | border-radius: 8px; 33 | transition: all 0.125s ease-in-out; 34 | 35 | &:hover { 36 | background-color: #{$color-background-inverse + "99"}; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/screens/Settings/Settings.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .settings-wrapper { 5 | display: flex; 6 | flex-direction: column; 7 | grid-area: main; 8 | justify-content: center; 9 | width: 100%; 10 | 11 | margin: 80px auto; 12 | 13 | @media screen and (min-width: $bp-md) { 14 | max-width: 900px; 15 | padding: 50px; 16 | margin: 0 auto; 17 | } 18 | 19 | .heading { 20 | margin-bottom: 20px; 21 | } 22 | 23 | .settings-guide { 24 | width: 100%; 25 | width: 50%; 26 | margin: 40px auto; 27 | 28 | @media screen and (min-width: $bp-md) { 29 | margin: 40px auto; 30 | } 31 | 32 | h3 { 33 | margin-bottom: 5px; 34 | } 35 | } 36 | 37 | .table-wrapper { 38 | width: 100%; 39 | overflow-x: scroll; 40 | 41 | @media screen and (min-width: $bp-md) { 42 | overflow-x: hidden; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Forms/ImportPreviewForm/ImportPreviewForm.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import GroupPreview from "./GroupPreview/GroupPreview.jsx"; 4 | import PackagePreview from "./PackagePreview/PackagePreview.jsx"; 5 | 6 | import "./ImportPreviewForm.scss"; 7 | 8 | const ImportPreviewForm = ({ 9 | defaultPackage = null, 10 | onSubmitCallback = null, 11 | importedData, 12 | }) => { 13 | return ( 14 |
15 | {importedData.type === "vocascan/package" ? ( 16 | 20 | ) : ( 21 | 26 | )} 27 |
28 | ); 29 | }; 30 | 31 | export default ImportPreviewForm; 32 | -------------------------------------------------------------------------------- /src/Components/Congratulation/Congratulation.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { scaleValue } from "../../utils/index.js"; 5 | 6 | import "./Congratulation.scss"; 7 | 8 | const Congratulation = ({ percentage }) => { 9 | const { t } = useTranslation(); 10 | const [congratulation, setCongratulations] = useState(); 11 | 12 | const translations = t("screens.endScreen.congratulations", { 13 | returnObjects: true, 14 | }); 15 | 16 | useEffect(() => { 17 | //get size of congratulation array and calculate the message with the given percentage 18 | setCongratulations( 19 | translations[ 20 | scaleValue(percentage, [0, 100], [1, translations.length - 1]) 21 | ] 22 | ); 23 | }, [percentage, t, translations]); 24 | return

{congratulation}

; 25 | }; 26 | 27 | export default Congratulation; 28 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @import "./colors"; 2 | 3 | * { 4 | padding: 0; 5 | margin: 0; 6 | font-family: "Roboto", sans-serif; 7 | font-weight: 300; 8 | text-align: center; 9 | } 10 | 11 | body { 12 | color: $color-main-text; 13 | background-color: $color-background; 14 | } 15 | 16 | .text { 17 | &-success { 18 | color: $color-green; 19 | } 20 | 21 | &-error { 22 | color: $color-red; 23 | } 24 | 25 | &-light { 26 | font-weight: lighter !important; 27 | } 28 | 29 | &-normal { 30 | font-weight: normal !important; 31 | } 32 | 33 | &-bold { 34 | font-weight: bold !important; 35 | } 36 | 37 | &-wrap { 38 | white-space: pre-wrap; 39 | } 40 | 41 | &-left { 42 | text-align: left; 43 | } 44 | } 45 | 46 | .w { 47 | &-25 { 48 | width: 25%; 49 | } 50 | 51 | &-50 { 52 | width: 50%; 53 | } 54 | 55 | &-75 { 56 | width: 75%; 57 | } 58 | 59 | &-100 { 60 | width: 100%; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/screens/Learn/Dashboard/Dashboard.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | @import "../../../constants"; 3 | 4 | .dashboard { 5 | display: flex; 6 | grid-area: main; 7 | justify-content: center; 8 | width: 100%; 9 | height: 100vh; 10 | padding: 0; 11 | 12 | @media screen and (min-width: $bp-md) { 13 | max-width: 850px; 14 | height: 100%; 15 | padding: 50px; 16 | margin: auto; 17 | } 18 | 19 | &.empty { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | width: 100%; 24 | height: 100vh; 25 | padding: 0; 26 | 27 | @media screen and (min-width: $bp-md) { 28 | height: 100%; 29 | } 30 | 31 | h1 { 32 | font-size: 25px; 33 | text-transform: uppercase; 34 | } 35 | } 36 | 37 | .dashboard-inner { 38 | width: 100%; 39 | margin-top: 80px; 40 | 41 | @media screen and (min-width: $bp-md) { 42 | margin-top: 0; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Components/Snackbar/Snackbar.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | 3 | .snackbar { 4 | position: absolute; 5 | right: 15px; 6 | bottom: 15px; 7 | z-index: 10000; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | width: 300px; 12 | padding: 10px 20px; 13 | visibility: hidden; 14 | border-radius: 10px; 15 | opacity: 0; 16 | transition: all 0.4s ease-in; 17 | 18 | &.snackbar-success { 19 | color: $color-green-dark; 20 | background-color: $color-green-light; 21 | } 22 | 23 | &.snackbar-error { 24 | color: $color-red-dark; 25 | background-color: $color-red-light; 26 | } 27 | 28 | &.snackbar-info { 29 | color: $color-primary-dark; 30 | background-color: $color-primary-light; 31 | } 32 | 33 | .snackbar-text { 34 | font-weight: 400; 35 | } 36 | 37 | &.show { 38 | visibility: visible; 39 | user-select: none; 40 | opacity: 1; 41 | transition: all 0.4s ease-in; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Components/Charts/ProgressBar/ProgressBar.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | 3 | .progress-bar { 4 | position: relative; 5 | width: 100%; 6 | max-width: 500px; 7 | height: 20px; 8 | margin: 50px 0; 9 | background-color: $color-background-muted; 10 | 11 | .wrapper { 12 | display: flex; 13 | width: 0%; 14 | max-width: 500px; 15 | height: 100%; 16 | background-color: $color-green; 17 | transition: width 1s ease-in-out; 18 | 19 | .percentage { 20 | position: absolute; 21 | left: 50%; 22 | align-self: center; 23 | font-size: 13px; 24 | font-weight: bold; 25 | line-height: 13px; 26 | color: $color-main-text; 27 | transform: translateX(-50%); 28 | } 29 | } 30 | 31 | .bottom-text { 32 | display: flex; 33 | justify-content: flex-end; 34 | padding: 5px 0; 35 | font-size: 13px; 36 | line-height: 13px; 37 | color: $color-main-text; 38 | letter-spacing: 1px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/screens/Library/Library.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { Route, Switch, useRouteMatch } from "react-router"; 4 | 5 | import AllGroups from "./AllGroups/AllGroups.jsx"; 6 | import AllPackages from "./AllPackages/AllPackages.jsx"; 7 | import AllVocabs from "./AllVocabs/AllVocabs.jsx"; 8 | 9 | import "./Library.scss"; 10 | 11 | const Library = () => { 12 | const { t } = useTranslation(); 13 | const { path } = useRouteMatch(); 14 | 15 | return ( 16 |
17 |

{t("global.library")}

18 | 19 | 20 | 21 | 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default Library; 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: kind/bug 6 | assignees: noctera 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Please complete the following information:** 27 | 28 | 29 | 30 | ``` 31 | Version: 32 | Commit: 33 | Date: 34 | Electron: 35 | Chrome: 36 | Node.js: 37 | V8: 38 | OS: 39 | ``` 40 | 41 | **Additional context** 42 | Add any other context about the problem here. 43 | -------------------------------------------------------------------------------- /src/screens/Learn/Query/Query.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | @import "../../../constants"; 3 | 4 | .query-wrapper { 5 | display: grid; 6 | grid-template-areas: 7 | "progress" 8 | "content"; 9 | grid-template-rows: 15% 85%; 10 | width: 100%; 11 | height: 100vh; 12 | 13 | @media screen and (min-width: $bp-md) { 14 | grid-template-areas: 15 | "progress" 16 | "content" 17 | "footer"; 18 | grid-template-rows: 15% 70% 15%; 19 | height: 100%; 20 | } 21 | 22 | .progress { 23 | display: flex; 24 | grid-area: progress; 25 | align-self: center; 26 | justify-content: center; 27 | justify-self: center; 28 | width: 100%; 29 | max-width: 600px; 30 | margin-top: 80px; 31 | 32 | @media screen and (min-width: $bp-md) { 33 | min-width: 450px; 34 | } 35 | } 36 | 37 | .content { 38 | display: flex; 39 | grid-area: content; 40 | align-items: center; 41 | justify-content: center; 42 | width: 100%; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import deepmerge from "deepmerge"; 2 | import isElectron from "is-electron"; 3 | 4 | const envVars = Object.entries(process.env).reduce((acc, [key, value]) => { 5 | if (key.startsWith("REACT_APP_VOCASCAN_")) { 6 | acc[key.replace(/^REACT_APP_VOCASCAN_/, "")] = value; 7 | } 8 | return acc; 9 | }, {}); 10 | 11 | window.VOCASCAN_CONFIG = deepmerge(window.VOCASCAN_CONFIG, { 12 | ...envVars, 13 | ENV: undefined, 14 | BASE_URL: "", 15 | SHOW_PLANS: undefined, 16 | THEME_SELECT: "auto", 17 | themes: { 18 | dark: "default-themes/dark.css", 19 | light: "default-themes/light.css", 20 | }, 21 | }); 22 | 23 | // detect env automatically if not set 24 | if (window.VOCASCAN_CONFIG.ENV === undefined) { 25 | window.VOCASCAN_CONFIG.ENV = isElectron() ? "electron" : "web"; 26 | } 27 | 28 | // if SHOW_PLANS is not configured but a BASE_URL is configured -> dont show the plans 29 | if (window.VOCASCAN_CONFIG.SHOW_PLANS === undefined) { 30 | window.VOCASCAN_CONFIG.SHOW_PLANS = !window.VOCASCAN_CONFIG.BASE_URL; 31 | } 32 | -------------------------------------------------------------------------------- /src/Components/Nav/UpdateAvailable.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | import GetAppIcon from "@material-ui/icons/GetApp"; 4 | 5 | import { updateNotifier, startUpdate } from "../../modules/update.js"; 6 | 7 | import "./UpdateAvailable.scss"; 8 | 9 | const UpdateAvailable = () => { 10 | const [show, setShow] = useState(false); 11 | 12 | useEffect(() => { 13 | const handleUpdateNotification = (_event, isDarwin, info) => { 14 | console.log(isDarwin, info); 15 | setShow(true); 16 | }; 17 | 18 | updateNotifier.on("update-available", handleUpdateNotification); 19 | 20 | return () => { 21 | updateNotifier.removeListener( 22 | "update-available", 23 | handleUpdateNotification 24 | ); 25 | }; 26 | }); 27 | 28 | const handleUpdate = () => { 29 | startUpdate(); 30 | }; 31 | 32 | if (!show) { 33 | return null; 34 | } 35 | 36 | return ; 37 | }; 38 | 39 | export default UpdateAvailable; 40 | -------------------------------------------------------------------------------- /src/screens/Learn/Direction/Direction.scss: -------------------------------------------------------------------------------- 1 | @import "../../../constants"; 2 | 3 | .direction { 4 | display: grid; 5 | grid-template-areas: 6 | "title" 7 | "boxes"; 8 | grid-template-rows: 0.5fr 4fr; 9 | width: 100%; 10 | height: 100%; 11 | margin: 80px 0; 12 | 13 | @media screen and (min-width: $bp-md) { 14 | grid-template-areas: 15 | "title" 16 | "boxes" 17 | "footer"; 18 | grid-template-rows: 1fr 3fr 1fr; 19 | margin: 0; 20 | } 21 | 22 | .box-title { 23 | display: flex; 24 | grid-area: title; 25 | align-self: center; 26 | justify-self: center; 27 | font-size: 18px; 28 | 29 | @media screen and (min-width: $bp-md) { 30 | margin: 50px 0; 31 | font-size: 24px; 32 | } 33 | } 34 | 35 | .box-wrapper { 36 | display: flex; 37 | flex-direction: column; 38 | grid-area: boxes; 39 | justify-content: space-around; 40 | margin: auto; 41 | 42 | @media screen and (min-width: $bp-md) { 43 | flex-direction: row; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Components/CookieConsentBanner/CookieConsentBanner.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | 3 | /* stylelint-disable selector-class-pattern */ 4 | .CookieConsent { 5 | position: absolute; 6 | right: 0; 7 | left: 0; 8 | z-index: 999; 9 | display: flex; 10 | flex-wrap: wrap; 11 | justify-content: space-between; 12 | width: 40%; 13 | padding: 15px; 14 | margin-right: auto; 15 | margin-left: auto; 16 | color: $color-main-text-inverse; 17 | background: $color-background-inverse; 18 | 19 | div { 20 | margin: auto 0; 21 | } 22 | 23 | button { 24 | flex: 0 0 auto; 25 | padding: 5px 10px; 26 | margin-left: 15px; 27 | color: $color-main-text-inverse; 28 | cursor: pointer; 29 | background: $color-primary; 30 | border: 0; 31 | border-radius: 0; 32 | box-shadow: none; 33 | } 34 | 35 | .cookie-consent-link { 36 | color: $color-primary; 37 | text-decoration: none; 38 | 39 | &:hover { 40 | text-decoration: underline; 41 | } 42 | } 43 | } 44 | /* stylelint-enable selector-class-pattern */ 45 | -------------------------------------------------------------------------------- /src/Components/Timer/CountdownTimer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Countdown from "react-countdown"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | import "./CountdownTimer.scss"; 6 | 7 | const Expired = () => { 8 | const { t } = useTranslation(); 9 | return ( 10 | 11 | {t("components.inviteCode.expired")} 12 | 13 | ); 14 | }; 15 | 16 | const renderer = ({ days, hours, minutes, seconds, completed }) => { 17 | if (completed) { 18 | // Render a completed state 19 | return ; 20 | } else { 21 | // Render a countdown 22 | return ( 23 | 24 | {("00" + days).substr(-2)}:{("00" + hours).substr(-2)}: 25 | {("00" + minutes).substr(-2)}:{("00" + seconds).substr(-2)} 26 | 27 | ); 28 | } 29 | }; 30 | 31 | const CountdownTimer = ({ callQueuedTime }) => { 32 | return ; 33 | }; 34 | 35 | export default CountdownTimer; 36 | -------------------------------------------------------------------------------- /src/redux/Actions/index.js: -------------------------------------------------------------------------------- 1 | export const REGISTER = "REGISTER"; 2 | export const SIGN_IN = "SIGN_IN"; 3 | export const SIGN_OUT = "SIGN_OUT"; 4 | export const SET_SERVER_URL = "SET_SERVER_URL"; 5 | export const SET_SELF_HOSTED = "SET_SELF_HOSTED"; 6 | export const SET_SERVER_INFO = "SET_SERVER_INFO"; 7 | export const OPEN_GUIDE = "OPEN_GUIDE"; 8 | export const CLOSE_GUIDE = "CLOSE_GUIDE"; 9 | 10 | export const SET_LANGUAGE = "SET_LANGUAGE"; 11 | export const SET_LANGUAGES = "SET_LANGUAGES"; 12 | export const SET_THEME = "SET_THEME"; 13 | 14 | export const SET_VOCAB_ACTIVE = "SET_VOCAB_ACTIVE"; 15 | export const SET_VOCAB_ACTIVATE = "SET_VOCAB_ACTIVATE"; 16 | export const SET_GROUP_ACTIVATE = "SET_GROUP_ACTIVATE"; 17 | 18 | export const SET_TABLE_PAGE_SIZE = "SET_TABLE_PAGE_SIZE"; 19 | 20 | export const SET_LEARNED_PACKAGE = "SET_LEARNED_PACKAGE"; 21 | 22 | export const SET_QUERY_CORRECT = "SET_QUERY_CORRECT"; 23 | export const SET_QUERY_WRONG = "SET_QUERY_WRONG"; 24 | export const SET_ACTUAL_PROGRESS = "SET_ACTUAL_PROGRESS"; 25 | export const CLEAR_QUERY = "CLEAR_QUERY"; 26 | -------------------------------------------------------------------------------- /src/redux/Actions/query.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_LEARNED_PACKAGE, 3 | SET_QUERY_CORRECT, 4 | SET_QUERY_WRONG, 5 | CLEAR_QUERY, 6 | SET_ACTUAL_PROGRESS, 7 | } from "./index.js"; 8 | 9 | export const setLearnedPackage = ({ 10 | foreignWordLanguage, 11 | translatedWordLanguage, 12 | languagePackageId, 13 | vocabsToday, 14 | staged, 15 | }) => { 16 | return { 17 | type: SET_LEARNED_PACKAGE, 18 | payload: { 19 | foreignWordLanguage, 20 | translatedWordLanguage, 21 | languagePackageId, 22 | vocabsToday, 23 | staged, 24 | }, 25 | }; 26 | }; 27 | 28 | export const setQueryCorrect = () => { 29 | return { 30 | type: SET_QUERY_CORRECT, 31 | payload: {}, 32 | }; 33 | }; 34 | 35 | export const setQueryWrong = () => { 36 | return { 37 | type: SET_QUERY_WRONG, 38 | payload: {}, 39 | }; 40 | }; 41 | 42 | export const setActualProgress = () => { 43 | return { 44 | type: SET_ACTUAL_PROGRESS, 45 | payload: {}, 46 | }; 47 | }; 48 | 49 | export const clearQuery = () => { 50 | return { 51 | type: CLEAR_QUERY, 52 | payload: {}, 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/images/logo/transparent-round.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Forms/ImportPreviewForm/PackagePreview/PackagePreview.scss: -------------------------------------------------------------------------------- 1 | @import "../../../constants"; 2 | 3 | .package-preview { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-between; 8 | width: 100%; 9 | height: 100%; 10 | overflow-y: auto; 11 | 12 | .update-fields { 13 | width: 100%; 14 | height: 80%; 15 | overflow-y: auto; 16 | 17 | .table-wrapper { 18 | width: 100%; 19 | overflow-x: scroll; 20 | @media screen and (min-width: $bp-md) { 21 | overflow-x: hidden; 22 | } 23 | } 24 | 25 | .dropdown { 26 | z-index: 4; 27 | display: flex; 28 | justify-content: space-between; 29 | height: 50px; 30 | 31 | .select-wrapper { 32 | display: flex; 33 | width: 49%; 34 | } 35 | } 36 | 37 | .group-detail { 38 | width: 100%; 39 | margin: 0 auto; 40 | 41 | @media screen and (min-width: $bp-md) { 42 | width: 90%; 43 | } 44 | } 45 | } 46 | 47 | .submit-btn { 48 | display: flex; 49 | justify-content: center; 50 | margin-top: 40px; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Components/Nav/Nav.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .nav { 5 | z-index: 100; 6 | display: none; 7 | flex-direction: column; 8 | grid-area: sidenav; 9 | justify-content: space-between; 10 | width: 65px; 11 | height: calc(100vh - 50px); 12 | background: $color-background-inverse; 13 | transition: width 0.2s ease-in; 14 | 15 | @media screen and (min-width: $bp-md) { 16 | display: flex; 17 | } 18 | 19 | &:hover { 20 | width: 200px; 21 | transition: width 0.2s ease-in; 22 | 23 | .nav-legal { 24 | opacity: 100; 25 | transition: all 0.6s ease-in; 26 | } 27 | } 28 | 29 | .button-list { 30 | grid-row: 2; 31 | grid-column: 1; 32 | margin-top: 40px; 33 | } 34 | 35 | .nav-legal { 36 | margin: 10px auto; 37 | font-size: 15px; 38 | color: $color-white; 39 | opacity: 0; 40 | 41 | .nav-legal-wrapper { 42 | display: flex; 43 | align-items: center; 44 | margin: 5px 0; 45 | 46 | a { 47 | margin-left: 5px; 48 | color: $color-white; 49 | text-decoration: none; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/screens/Library/AllVocabs/AllVocabs.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | @import "../../../constants"; 3 | 4 | .all-vocabs-wrapper { 5 | .header-wrapper { 6 | position: relative; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | margin-bottom: 20px; 11 | 12 | .back { 13 | position: absolute; 14 | left: 0; 15 | align-self: center; 16 | padding: 5px; 17 | border-radius: 100%; 18 | 19 | &:hover { 20 | color: $color-primary-dark; 21 | cursor: pointer; 22 | } 23 | } 24 | 25 | .add { 26 | position: absolute; 27 | right: 0; 28 | align-self: center; 29 | padding: 5px; 30 | border-radius: 100%; 31 | 32 | &:hover { 33 | color: $color-primary-dark; 34 | cursor: pointer; 35 | } 36 | } 37 | } 38 | 39 | .table-wrapper { 40 | overflow: scroll; 41 | 42 | @media screen and (min-width: $bp-md) { 43 | overflow: hidden; 44 | } 45 | } 46 | 47 | .filters { 48 | display: flex; 49 | justify-content: flex-end; 50 | 51 | .search { 52 | width: 300px; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Components/ThemeSelector/ThemeSelector.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | 5 | import Select from "../Form/Select/Select.jsx"; 6 | 7 | import { setTheme } from "../../redux/Actions/setting.js"; 8 | 9 | const ThemeSelector = () => { 10 | const { t, i18n } = useTranslation(); 11 | 12 | const theme = useSelector((state) => state.setting.theme); 13 | 14 | const dispatch = useDispatch(); 15 | 16 | const mapTheme = (key) => ({ 17 | label: i18n.exists(`themes.${key}`) ? t(`themes.${key}`) : key, 18 | value: key, 19 | }); 20 | 21 | return ( 22 |
23 | code === language)].map(mapLanguages)[0] 26 | } 27 | options={languages.map(mapLanguages)} 28 | onChange={({ value }) => { 29 | dispatch(setLanguage({ language: value })); 30 | }} 31 | menuPortalTarget={document.body} 32 | styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }) }} 33 | /> 34 |
35 | ); 36 | }; 37 | 38 | export default LanguageSelector; 39 | -------------------------------------------------------------------------------- /src/hooks/useFeature.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { gte } from "semver"; 4 | 5 | export const FEATURES = { 6 | IMPORT_EXPORT: { minVersion: "1.1.0" }, 7 | }; 8 | 9 | const useFeature = (feature) => { 10 | const serverInfo = useSelector((state) => state.login.serverInfo); 11 | const isSupported = useMemo(() => { 12 | return Object.entries(feature).every(([key, value]) => { 13 | if (key === "minVersion") { 14 | // in case that serverVersion is not yet defined 15 | if (!serverInfo?.version) { 16 | return false; 17 | } 18 | 19 | // compare semver versions 20 | if (gte(serverInfo?.version, value)) { 21 | return true; 22 | } 23 | 24 | // make the feature always in development versions available to allow testing 25 | if (process.env.NODE_ENV === "development") { 26 | return true; 27 | } 28 | 29 | // feature not supported 30 | return false; 31 | } else if (key === "env") { 32 | return window.VOCASCAN_CONFIG.ENV === value; 33 | } 34 | 35 | return false; 36 | }); 37 | }, [feature, serverInfo?.version]); 38 | 39 | return { 40 | version: serverInfo?.version, 41 | isSupported, 42 | }; 43 | }; 44 | 45 | export default useFeature; 46 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | | Status | Type | 5 | | :---: | :---: | 6 | | :white_check_mark: Ready / :x: Hold | Feature/Bug/Tooling/Refactor/Hotfix | 7 | 8 | ## Description 9 | 10 | 11 | 12 | ## Motivation and Context 13 | 14 | 15 | 16 | 17 | ## Screenshots / GIFs (if appropriate): 18 | 19 | 20 | ## Checklist 21 | 22 | 23 | 24 | 25 | - [ ] I have read the **CONTRIBUTING** document. 26 | - [ ] I have considered the accessibility of my changes (i.e. did I add proper content descriptions to images, or run my changes with talkback enabled?) 27 | - [ ] I have documented my code if needed 28 | 29 | ## Resolves 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Components/Details/Details.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | 3 | /* stylelint-disable selector-class-pattern */ 4 | 5 | .Collapsible { 6 | margin-bottom: 30px; 7 | 8 | .Collapsible__trigger { 9 | position: relative; 10 | display: block; 11 | padding: 5px 0 5px 20px; 12 | cursor: pointer; 13 | user-select: none; 14 | border-bottom: 1px solid $color-background-inverse; 15 | 16 | &::before { 17 | position: absolute; 18 | bottom: 11px; 19 | left: 0; 20 | width: 5px; 21 | height: 5px; 22 | content: " "; 23 | border-color: $color-background-inverse; 24 | border-style: solid; 25 | border-width: 0 2px 2px 0; 26 | transition: all 0.125s ease-in-out; 27 | transform: rotate(-45deg); 28 | } 29 | 30 | &.is-open { 31 | &::before { 32 | transform: rotate(45deg); 33 | } 34 | } 35 | 36 | .trigger-wrapper { 37 | display: flex; 38 | justify-content: space-between; 39 | 40 | .summary { 41 | font-size: 14px; 42 | font-weight: 400; 43 | text-transform: uppercase; 44 | letter-spacing: 2px; 45 | } 46 | } 47 | } 48 | 49 | .Collapsible__contentInner { 50 | display: flex; 51 | flex-wrap: wrap; 52 | margin-top: 10px; 53 | } 54 | } 55 | 56 | /* stylelint-enable selector-class-pattern */ 57 | -------------------------------------------------------------------------------- /src/i18n/I18nProvider.js: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import React, { useEffect } from "react"; 3 | import { initReactI18next, I18nextProvider } from "react-i18next"; 4 | import { useSelector } from "react-redux"; 5 | 6 | import de from "./locales/de/default.json"; 7 | import en from "./locales/en/default.json"; 8 | 9 | // import pl from "./locales/pl/default.json"; 10 | // import ru from "./locales/ru/default.json"; 11 | 12 | i18n.use(initReactI18next).init({ 13 | debug: false, 14 | fallbackLng: ["en"], 15 | interpolation: { escapeValue: false }, 16 | lng: "en", 17 | load: "all", 18 | defaultNS: "default", 19 | react: { useSuspense: true }, 20 | resources: { 21 | de: { default: de }, 22 | en: { default: en }, 23 | // pl: { default: pl }, 24 | // ru: { default: ru }, 25 | }, 26 | }); 27 | 28 | function I18nProvider({ children }) { 29 | const language = useSelector((state) => state.setting.language); 30 | 31 | useEffect(() => { 32 | i18n.changeLanguage(language); 33 | }, [language]); 34 | 35 | return {children}; 36 | } 37 | 38 | export default I18nProvider; 39 | 40 | export const languages = [ 41 | { code: "en", name: "English" }, 42 | { code: "de", name: "Deutsch (German)" }, 43 | // { code: "pl", name: "Polski (Polish)" }, 44 | // { code: "ru", name: "Pусский (Russian)" }, 45 | ]; 46 | -------------------------------------------------------------------------------- /src/screens/Learn/Direction/Direction.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { useSelector } from "react-redux"; 4 | 5 | import DirectionBox from "../../../Components/DirectionBox/DirectionBox.jsx"; 6 | 7 | import "./Direction.scss"; 8 | 9 | const Direction = () => { 10 | const { t } = useTranslation(); 11 | 12 | const foreignWordLanguage = useSelector( 13 | (state) => state.query.foreignWordLanguage 14 | ); 15 | const translatedWordLanguage = useSelector( 16 | (state) => state.query.translatedWordLanguage 17 | ); 18 | return ( 19 |
20 |

{t("screens.direction.title")}

21 |
22 | 26 | 31 | 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Direction; 42 | -------------------------------------------------------------------------------- /src/screens/Guide/Pages/VocabDescription/VocabDescription.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../constants"; 2 | 3 | .vocab-description { 4 | display: flex; 5 | flex-direction: column; 6 | width: 100%; 7 | height: 100%; 8 | 9 | @media screen and (min-width: $bp-md) { 10 | flex-direction: row; 11 | } 12 | 13 | .images { 14 | display: flex; 15 | flex-direction: column; 16 | width: 100%; 17 | margin-top: 30px; 18 | 19 | @media screen and (min-width: $bp-md) { 20 | width: 30%; 21 | margin: auto 0; 22 | } 23 | } 24 | 25 | .description { 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: space-between; 29 | 30 | @media screen and (min-width: $bp-md) { 31 | margin: 10% 0; 32 | } 33 | 34 | .heading { 35 | font-size: 25px; 36 | font-weight: bold; 37 | } 38 | 39 | ul { 40 | padding: 30px 0; 41 | @media screen and (min-width: $bp-md) { 42 | padding: 0 5%; 43 | } 44 | 45 | li { 46 | margin: 0 30px; 47 | font-size: 20px; 48 | 49 | text-align: justify; 50 | text-justify: inter-word; 51 | 52 | @media screen and (min-width: $bp-md) { 53 | margin: 10px 100px; 54 | text-align: left; 55 | } 56 | } 57 | } 58 | 59 | .end-text { 60 | font-size: 20px; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Components/ConfirmDialog/ConfirmDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import Button from "../Button/Button.jsx"; 5 | import Modal from "../Modal/Modal.jsx"; 6 | 7 | import "./ConfirmDialog.scss"; 8 | 9 | const ConfirmDialog = ({ 10 | title = null, 11 | description = null, 12 | onSubmit = null, 13 | submitText, 14 | cancelText, 15 | onClose = null, 16 | show = false, 17 | canSubmit = true, 18 | showAbortButton = true, 19 | children, 20 | }) => { 21 | const [t] = useTranslation(); 22 | 23 | return ( 24 | 25 |
26 |
27 | {description} 28 | {children} 29 |
30 |
31 | {showAbortButton && ( 32 | 35 | )} 36 | 44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default ConfirmDialog; 51 | -------------------------------------------------------------------------------- /src/Components/DirectionBox/DirectionBox.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .direction-box { 5 | display: flex; 6 | justify-content: center; 7 | width: 100%; 8 | min-height: 250px; 9 | margin: 20px 0; 10 | color: $color-main-text-inverse; 11 | background: $color-background-inverse; 12 | border-radius: 10px; 13 | 14 | @media screen and (min-width: $bp-md) { 15 | min-width: 350px; 16 | max-width: 600px; 17 | max-height: 600px; 18 | margin: 0 20px; 19 | } 20 | 21 | &:hover { 22 | cursor: pointer; 23 | box-shadow: 0 0 0 10px $color-primary; 24 | transition: all 0.2s ease-in-out; 25 | transition: all 0.2s ease-in-out; 26 | } 27 | 28 | .direction-box-inner { 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | justify-content: space-around; 33 | width: 90%; 34 | 35 | .flags { 36 | display: flex; 37 | align-items: center; 38 | justify-content: space-between; 39 | margin-top: 50px; 40 | 41 | * { 42 | margin: 0 5px; 43 | } 44 | 45 | .direction-arrow { 46 | font-size: 32px; 47 | 48 | &.invert { 49 | transform: rotate(180deg); 50 | } 51 | } 52 | } 53 | 54 | .languages { 55 | margin-top: 50px; 56 | font-size: 13px; 57 | text-transform: uppercase; 58 | letter-spacing: 2px; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Components/ContributorCard/ContributorCard.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | 3 | .card { 4 | position: relative; 5 | display: inline-block; 6 | width: 130px; 7 | height: 170px; 8 | margin: 8px; 9 | overflow: hidden; 10 | border-radius: 8px; 11 | 12 | img { 13 | width: 130px; 14 | } 15 | 16 | .backdrop { 17 | position: absolute; 18 | top: 130px; 19 | bottom: 0; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | justify-content: center; 24 | width: 100%; 25 | color: #fff; 26 | background-color: $color-background-inverse; 27 | transition: border-radius 0.125s ease-in-out, top 0.125s ease-in-out; 28 | 29 | .name { 30 | font-size: 14px; 31 | } 32 | 33 | .description { 34 | display: none; 35 | margin-top: 5px; 36 | font-size: 12px; 37 | color: #bbb; 38 | white-space: pre-line; 39 | } 40 | } 41 | 42 | .card-image { 43 | width: 128px; 44 | } 45 | 46 | .card-image-bp { 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | height: 65%; 51 | 52 | .card-image-bp-icon { 53 | font-size: 100px; 54 | color: $color-dark; 55 | } 56 | } 57 | 58 | &:hover { 59 | .backdrop { 60 | top: 0; 61 | 62 | background: $color-background-inverse; 63 | 64 | .description { 65 | display: block; 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * See: https://dev.to/gabe_ragland/debouncing-with-react-hooks-jci 3 | */ 4 | import { useState, useEffect } from "react"; 5 | 6 | const useDebounce = (value, delay) => { 7 | const [debouncedValue, setDebouncedValue] = useState(value); 8 | 9 | useEffect( 10 | () => { 11 | // Set debouncedValue to value (passed in) after the specified delay 12 | const handler = setTimeout(() => { 13 | setDebouncedValue(value); 14 | }, delay); 15 | 16 | // Return a cleanup function that will be called every time ... 17 | // ... useEffect is re-called. useEffect will only be re-called ... 18 | // ... if value changes (see the inputs array below). 19 | // This is how we prevent debouncedValue from changing if value is ... 20 | // ... changed within the delay period. Timeout gets cleared and restarted. 21 | // To put it in context, if the user is typing within our app's ... 22 | // ... search box, we don't want the debouncedValue to update until ... 23 | // ... they've stopped typing for more than 500ms. 24 | return () => { 25 | clearTimeout(handler); 26 | }; 27 | }, 28 | // Only re-call effect if value changes 29 | // You could also add the "delay" var to inputs array if you ... 30 | // ... need to be able to change that dynamically. 31 | [value, delay] 32 | ); 33 | 34 | return debouncedValue; 35 | }; 36 | 37 | export default useDebounce; 38 | -------------------------------------------------------------------------------- /src/screens/Settings/Settings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { useDispatch } from "react-redux"; 4 | 5 | import Button from "../../Components/Button/Button.jsx"; 6 | import LanguageSelector from "../../Components/LanguageSelector/LanguageSelector.jsx"; 7 | import ThemeSelector from "../../Components/ThemeSelector/ThemeSelector.jsx"; 8 | 9 | import VersionTable from "../../Components/VersionTable/VersionTable.js"; 10 | import { openGuide } from "../../redux/Actions/login.js"; 11 | 12 | import "./Settings.scss"; 13 | 14 | const Settings = () => { 15 | const { t } = useTranslation(); 16 | 17 | const dispatch = useDispatch(); 18 | 19 | const reopenGuide = useCallback(() => { 20 | dispatch(openGuide()); 21 | }, [dispatch]); 22 | 23 | return ( 24 |
25 |

{t("screens.settings.title")}

26 | 27 | 28 | 29 | 30 | 31 |
32 |

{t("screens.settings.guide.title")}

33 | 36 |
37 | 38 |

{t("screens.settings.versions")}

39 |
40 | 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default Settings; 47 | -------------------------------------------------------------------------------- /src/Forms/VocabForm/VocabForm.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .vocab-form { 5 | grid-area: main; 6 | width: 100%; 7 | height: 75%; 8 | margin: 80px 0; 9 | overflow-y: auto; 10 | 11 | @media screen and (min-width: $bp-md) { 12 | max-width: 700px; 13 | padding: 50px; 14 | margin: 0; 15 | } 16 | 17 | .heading { 18 | display: none; 19 | grid-row: 2; 20 | grid-column: 2; 21 | 22 | @media screen and (min-width: $bp-md) { 23 | display: inline; 24 | } 25 | } 26 | 27 | .dropdowns { 28 | z-index: 4; 29 | display: flex; 30 | flex-direction: column; 31 | 32 | @media screen and (min-width: $bp-md) { 33 | flex-direction: row; 34 | justify-content: space-between; 35 | margin: 20px -10px; 36 | } 37 | 38 | .select-wrapper { 39 | width: 100%; 40 | 41 | @media screen and (min-width: $bp-md) { 42 | margin: 0 10px; 43 | } 44 | } 45 | } 46 | 47 | .input-fields { 48 | display: flex; 49 | flex-direction: column; 50 | grid-row: 4; 51 | grid-column: 2; 52 | 53 | .submit-btn { 54 | grid-row: 5; 55 | grid-column: 2; 56 | width: 150px; 57 | height: 35px; 58 | margin: 0 auto; 59 | background: $color-primary; 60 | box-shadow: -1px 3px 5px $color-alternative; 61 | } 62 | } 63 | 64 | .form-submit { 65 | display: flex; 66 | justify-content: center; 67 | margin-top: 40px; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/redux/Reducers/query.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_LEARNED_PACKAGE, 3 | SET_QUERY_CORRECT, 4 | SET_QUERY_WRONG, 5 | CLEAR_QUERY, 6 | SET_ACTUAL_PROGRESS, 7 | } from "../Actions/index.js"; 8 | 9 | const initialState = { 10 | foreignWordLanguage: "", 11 | translatedWordLanguage: "", 12 | languagePackageId: "", 13 | vocabsToday: 0, 14 | staged: false, 15 | correct: 0, 16 | wrong: 0, 17 | actualProgress: 0, 18 | }; 19 | 20 | const queryReducer = (state = initialState, action) => { 21 | switch (action.type) { 22 | case SET_LEARNED_PACKAGE: 23 | return { 24 | ...state, 25 | foreignWordLanguage: action.payload.foreignWordLanguage, 26 | translatedWordLanguage: action.payload.translatedWordLanguage, 27 | languagePackageId: action.payload.languagePackageId, 28 | vocabsToday: action.payload.vocabsToday, 29 | staged: action.payload.staged, 30 | }; 31 | 32 | case SET_QUERY_CORRECT: 33 | return { 34 | ...state, 35 | correct: state.correct + 1, 36 | }; 37 | 38 | case SET_QUERY_WRONG: 39 | return { 40 | ...state, 41 | wrong: state.wrong + 1, 42 | }; 43 | case SET_ACTUAL_PROGRESS: 44 | return { 45 | ...state, 46 | actualProgress: state.actualProgress + 1, 47 | }; 48 | 49 | case CLEAR_QUERY: 50 | return { 51 | ...initialState, 52 | }; 53 | 54 | default: 55 | return state; 56 | } 57 | }; 58 | 59 | export default queryReducer; 60 | -------------------------------------------------------------------------------- /src/Components/InviteCode/InviteCode.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | 3 | .invite-code { 4 | position: relative; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: space-around; 8 | width: 100%; 9 | height: 100%; 10 | min-height: 130px; 11 | max-height: 100px; 12 | background: $color-background-inverse; 13 | border-radius: 15px; 14 | 15 | &.expired { 16 | opacity: 0.9; 17 | } 18 | 19 | .delete-btn { 20 | position: absolute; 21 | top: 10px; 22 | right: 10px; 23 | color: $color-red; 24 | cursor: pointer; 25 | opacity: 0; 26 | transition: 100ms ease-in-out opacity; 27 | } 28 | 29 | &:hover > .delete-btn { 30 | opacity: 1; 31 | } 32 | 33 | .delete-btn-hide { 34 | display: none; 35 | } 36 | 37 | hr { 38 | width: 60%; 39 | margin: 0 auto; 40 | border: 1px solid #bbb; 41 | } 42 | 43 | .heading { 44 | p { 45 | width: fit-content; 46 | margin-right: auto; 47 | margin-left: auto; 48 | color: $color-main-text-inverse; 49 | cursor: pointer; 50 | transition: 250ms ease-out transform; 51 | 52 | &:hover { 53 | transform: scale(1.2); 54 | } 55 | } 56 | } 57 | 58 | .information { 59 | p { 60 | margin-top: 5px; 61 | font-size: 15px; 62 | color: #bbb; 63 | } 64 | 65 | .uses-valid { 66 | color: $color-green; 67 | } 68 | 69 | .uses-invalid { 70 | color: $color-red; 71 | } 72 | 73 | .expiration-code-valid { 74 | color: $color-green; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/redux/Actions/login.js: -------------------------------------------------------------------------------- 1 | import { 2 | REGISTER, 3 | SIGN_IN, 4 | SIGN_OUT, 5 | SET_SELF_HOSTED, 6 | SET_SERVER_URL, 7 | OPEN_GUIDE, 8 | CLOSE_GUIDE, 9 | SET_SERVER_INFO, 10 | } from "./index.js"; 11 | 12 | export const register = ({ username, email, token, isAdmin }) => { 13 | return { 14 | type: REGISTER, 15 | payload: { 16 | username, 17 | email, 18 | token, 19 | isAdmin, 20 | }, 21 | }; 22 | }; 23 | 24 | export const signIn = ({ username, email, token, isAdmin }) => { 25 | return { 26 | type: SIGN_IN, 27 | payload: { 28 | username, 29 | email, 30 | token, 31 | isAdmin, 32 | }, 33 | }; 34 | }; 35 | 36 | export const setServerUrl = ({ serverAddress }) => { 37 | return { 38 | type: SET_SERVER_URL, 39 | payload: { 40 | serverAddress, 41 | }, 42 | }; 43 | }; 44 | 45 | export const setSelfHosted = ({ selfHosted }) => { 46 | return { 47 | type: SET_SELF_HOSTED, 48 | payload: { 49 | selfHosted, 50 | }, 51 | }; 52 | }; 53 | 54 | export const setServerInfo = ({ serverInfo }) => { 55 | return { 56 | type: SET_SERVER_INFO, 57 | payload: { 58 | serverInfo, 59 | }, 60 | }; 61 | }; 62 | 63 | export const signOut = () => { 64 | return { 65 | type: SIGN_OUT, 66 | payload: {}, 67 | }; 68 | }; 69 | 70 | export const openGuide = () => { 71 | return { 72 | type: OPEN_GUIDE, 73 | payload: {}, 74 | }; 75 | }; 76 | 77 | export const closeGuide = () => { 78 | return { 79 | type: CLOSE_GUIDE, 80 | payload: {}, 81 | }; 82 | }; 83 | -------------------------------------------------------------------------------- /src/images/logo/color-round.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Components/Layout/UnauthenticatedLayout/UnauthenticatedLayout.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { useSelector } from "react-redux"; 4 | 5 | import Button from "../../Button/Button.jsx"; 6 | import Flag from "../../Flag/Flag.jsx"; 7 | import LanguageSelector from "../../LanguageSelector/LanguageSelector.jsx"; 8 | import Modal from "../../Modal/Modal.jsx"; 9 | 10 | import "./UnauthenticatedLayout.scss"; 11 | 12 | const UnauthenticatedLayout = ({ children }) => { 13 | const { t } = useTranslation(); 14 | const language = useSelector((state) => state.setting.language); 15 | const [showLanguageModal, setShowLanguageModal] = useState(false); 16 | 17 | return ( 18 |
19 |
setShowLanguageModal(true)} 21 | className="language-selector-wrapper" 22 | > 23 | 24 |
25 | {children} 26 | setShowLanguageModal(false)} 31 | > 32 |
33 | 34 |
35 | 36 |
37 | 40 |
41 |
42 |
43 | ); 44 | }; 45 | 46 | export default UnauthenticatedLayout; 47 | -------------------------------------------------------------------------------- /src/screens/Guide/Pages/GroupDescription/GroupDescription.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../constants"; 2 | 3 | .group-description { 4 | display: flex; 5 | flex-direction: column; 6 | width: 100%; 7 | height: 100%; 8 | 9 | @media screen and (min-width: $bp-md) { 10 | flex-direction: row; 11 | } 12 | 13 | .images { 14 | display: flex; 15 | flex-direction: row; 16 | width: 100%; 17 | 18 | @media screen and (min-width: $bp-md) { 19 | flex-direction: column; 20 | width: 30%; 21 | margin: 5% 0; 22 | } 23 | 24 | img { 25 | width: 50%; 26 | padding: 5px 0; 27 | object-fit: cover; 28 | 29 | @media screen and (min-width: $bp-md) { 30 | width: auto; 31 | height: 50%; 32 | } 33 | } 34 | } 35 | 36 | .description { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: space-between; 40 | 41 | @media screen and (min-width: $bp-md) { 42 | margin: 10% 0; 43 | } 44 | 45 | .heading { 46 | font-size: 25px; 47 | font-weight: bold; 48 | } 49 | 50 | ul { 51 | padding: 30px 0; 52 | @media screen and (min-width: $bp-md) { 53 | padding: 0 5%; 54 | } 55 | 56 | li { 57 | margin: 0 30px; 58 | font-size: 20px; 59 | 60 | text-align: justify; 61 | text-justify: inter-word; 62 | 63 | @media screen and (min-width: $bp-md) { 64 | margin: 10px 100px; 65 | text-align: left; 66 | } 67 | } 68 | } 69 | 70 | .end-text { 71 | margin-bottom: 30px; 72 | font-size: 20px; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/screens/Guide/Pages/PackageDescription/PackageDescription.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../constants"; 2 | 3 | .package-description { 4 | display: flex; 5 | flex-direction: column; 6 | width: 100%; 7 | height: 100%; 8 | 9 | @media screen and (min-width: $bp-md) { 10 | flex-direction: row; 11 | } 12 | 13 | .images { 14 | display: flex; 15 | flex-direction: row; 16 | width: 100%; 17 | 18 | @media screen and (min-width: $bp-md) { 19 | flex-direction: column; 20 | width: 30%; 21 | margin: 5% 0; 22 | } 23 | 24 | img { 25 | width: 50%; 26 | padding: 5px 0; 27 | object-fit: cover; 28 | 29 | @media screen and (min-width: $bp-md) { 30 | width: auto; 31 | height: 50%; 32 | } 33 | } 34 | } 35 | 36 | .description { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: space-between; 40 | 41 | @media screen and (min-width: $bp-md) { 42 | margin: 10% 0; 43 | } 44 | 45 | .heading { 46 | font-size: 25px; 47 | font-weight: bold; 48 | } 49 | 50 | ul { 51 | padding: 30px 0; 52 | @media screen and (min-width: $bp-md) { 53 | padding: 0 5%; 54 | } 55 | 56 | li { 57 | margin: 0 30px; 58 | font-size: 20px; 59 | 60 | text-align: justify; 61 | text-justify: inter-word; 62 | 63 | @media screen and (min-width: $bp-md) { 64 | margin: 10px 100px; 65 | text-align: left; 66 | } 67 | } 68 | } 69 | 70 | .end-text { 71 | margin-bottom: 30px; 72 | font-size: 20px; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Components/Flag/Flag.jsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState } from "react"; 2 | import { useEffect } from "react"; 3 | 4 | import { 5 | languageCountryMap, 6 | spriteSheetPositions, 7 | } from "./language-country-map.js"; 8 | 9 | import "./Flag.scss"; 10 | 11 | const sizeMap = { 12 | small: 0.13, 13 | medium: 0.3, 14 | large: 0.6, 15 | }; 16 | 17 | const Flag = ({ languageCode, border = false, size = "small", scale }) => { 18 | const [computedScale, setComputedScale] = useState(0); 19 | 20 | useEffect(() => { 21 | if (scale) { 22 | setComputedScale(scale); 23 | } else if (size && sizeMap[size]) { 24 | setComputedScale(sizeMap[size]); 25 | } else { 26 | setComputedScale(sizeMap.small); 27 | } 28 | }, [scale, size]); 29 | 30 | const style = useMemo(() => { 31 | let code = "unknown"; 32 | 33 | if (Object.keys(spriteSheetPositions).includes(languageCode)) { 34 | code = languageCode; 35 | } else if (languageCountryMap[languageCode]) { 36 | code = languageCountryMap[languageCode]; 37 | } 38 | 39 | return { 40 | width: 200 * computedScale, 41 | height: 150 * computedScale, 42 | backgroundSize: `${3300 * computedScale}px ${3060 * computedScale}px`, 43 | backgroundPosition: spriteSheetPositions[code] 44 | .map((x) => `${x * computedScale}px`) 45 | .join(" "), 46 | borderRadius: `${30 * computedScale}px`, 47 | boxShadow: border ? `0 0 0 ${10 * computedScale}px` : "none", 48 | }; 49 | }, [border, computedScale, languageCode]); 50 | 51 | return ( 52 |
53 | 54 |
55 | ); 56 | }; 57 | 58 | export default Flag; 59 | -------------------------------------------------------------------------------- /src/Components/Nav/NavButton.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .nav-button-wrapper { 5 | text-decoration: none; 6 | outline: none; 7 | } 8 | 9 | .nav-button { 10 | position: relative; 11 | display: flex; 12 | flex-wrap: nowrap; 13 | align-items: center; 14 | justify-content: center; 15 | width: 100%; 16 | height: 60px; 17 | color: $color-main-text-inverse; 18 | text-transform: uppercase; 19 | background-color: $color-background; 20 | border: unset; 21 | transition: all 150ms linear; 22 | 23 | @media screen and (min-width: $bp-md) { 24 | background-color: $color-background-inverse; 25 | } 26 | 27 | .button-name { 28 | position: absolute; 29 | left: 60px; 30 | color: $color-main-text; 31 | 32 | @media screen and (min-width: $bp-md) { 33 | color: $color-main-text-inverse; 34 | visibility: hidden; 35 | opacity: 0; 36 | } 37 | } 38 | 39 | .button-icon { 40 | position: absolute; 41 | left: 20px; 42 | color: $color-main-text; 43 | transition: all 150ms linear; 44 | 45 | @media screen and (min-width: $bp-md) { 46 | color: $color-main-text-inverse; 47 | } 48 | } 49 | 50 | &:hover { 51 | cursor: pointer; 52 | background-color: $color-primary; 53 | } 54 | 55 | &:focus { 56 | outline: none; 57 | } 58 | } 59 | 60 | .nav-button-active { 61 | .nav-button { 62 | background-color: $color-primary; 63 | 64 | .button-icon { 65 | color: #fff; 66 | } 67 | 68 | .button-name { 69 | color: #fff; 70 | } 71 | } 72 | } 73 | 74 | .nav:hover .button-name { 75 | visibility: visible; 76 | opacity: 1; 77 | transition: all 150ms linear; 78 | transition-delay: 150ms; 79 | } 80 | -------------------------------------------------------------------------------- /src/hooks/useLinkCreator.js: -------------------------------------------------------------------------------- 1 | import { CancelToken } from "axios"; 2 | import { useEffect, useState } from "react"; 3 | import { useSelector } from "react-redux"; 4 | 5 | import { checkUrlAvailable } from "../utils/api.js"; 6 | import { vocascanServer } from "../utils/constants.js"; 7 | 8 | const useLinkCreator = ({ path, electronFix = false }) => { 9 | const selfHosted = useSelector((state) => state.login.selfHosted); 10 | const serverAddress = useSelector((state) => state.login.serverAddress); 11 | const appLanguage = useSelector((state) => state.setting.language); 12 | 13 | const [isValid, setIsValid] = useState(false); 14 | const [url, setUrl] = useState(""); 15 | 16 | const { BASE_URL: baseURL, ENV: env } = window.VOCASCAN_CONFIG; 17 | 18 | // initialize url with the needed url for the servers 19 | useEffect(() => { 20 | if (baseURL || selfHosted) { 21 | if (electronFix && env === "electron") { 22 | setUrl(vocascanServer + `${path}?lang=` + appLanguage); 23 | } else { 24 | setUrl((serverAddress || baseURL) + `${path}?lang=` + appLanguage); 25 | } 26 | } else { 27 | setUrl(vocascanServer + `${path}?lang=` + appLanguage); 28 | } 29 | }, [appLanguage, baseURL, electronFix, env, path, selfHosted, serverAddress]); 30 | 31 | // check if url is available 32 | useEffect(() => { 33 | const cancelToken = CancelToken.source(); 34 | 35 | checkUrlAvailable(url, cancelToken.token) 36 | .then((response) => { 37 | setIsValid(true); 38 | }) 39 | .catch((error) => { 40 | setIsValid(false); 41 | }); 42 | 43 | return () => cancelToken.cancel(); 44 | }, [url]); 45 | 46 | return { isValid, url }; 47 | }; 48 | 49 | export default useLinkCreator; 50 | -------------------------------------------------------------------------------- /src/Components/Modal/Modal.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .modal { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | z-index: 100; 9 | display: flex; 10 | justify-content: center; 11 | width: 100%; 12 | height: 100vh; 13 | background: hsl(0, 0%, 0%, 0.4); 14 | 15 | .inner { 16 | position: relative; 17 | z-index: 11; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: space-between; 22 | width: 60vw; 23 | height: 60vh; 24 | padding: 40px 24px; 25 | margin: auto; 26 | overflow-y: auto; 27 | background-color: $color-background; 28 | border-radius: 5px; 29 | 30 | @media screen and (min-width: $bp-md) { 31 | padding: 40px 60px; 32 | } 33 | 34 | &.small { 35 | width: 90%; 36 | @media screen and (min-width: $bp-md) { 37 | width: 40vw; 38 | max-width: 500px; 39 | height: auto; 40 | } 41 | } 42 | 43 | &.large { 44 | width: 90%; 45 | height: 90%; 46 | 47 | @media screen and (min-width: $bp-md) { 48 | width: 80vw; 49 | max-width: 1200px; 50 | height: 80vh; 51 | } 52 | } 53 | 54 | &.maxed { 55 | width: 100%; 56 | height: 100vh; 57 | } 58 | 59 | .close { 60 | position: absolute; 61 | top: 20px; 62 | right: 20px; 63 | padding: 10px; 64 | color: $color-primary; 65 | border-radius: 100%; 66 | 67 | &:hover { 68 | color: $color-primary-dark; 69 | cursor: pointer; 70 | } 71 | } 72 | 73 | .heading { 74 | margin: 20px 0 0; 75 | font-size: 25px; 76 | text-transform: uppercase; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/screens/Library/AllGroups/AllGroups.scss: -------------------------------------------------------------------------------- 1 | @import "../../../colors"; 2 | @import "../../../constants"; 3 | 4 | .all-groups-wrapper { 5 | .action-col-btn { 6 | margin-right: 8px; 7 | } 8 | 9 | .header-wrapper { 10 | position: relative; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | margin-bottom: 20px; 15 | 16 | .back { 17 | position: absolute; 18 | left: 0; 19 | align-self: center; 20 | padding: 5px; 21 | border-radius: 100%; 22 | 23 | &:hover { 24 | color: $color-primary-dark; 25 | cursor: pointer; 26 | } 27 | } 28 | 29 | .add { 30 | position: absolute; 31 | right: 0; 32 | align-self: center; 33 | padding: 5px; 34 | border-radius: 100%; 35 | 36 | &:hover { 37 | color: $color-primary-dark; 38 | cursor: pointer; 39 | } 40 | } 41 | 42 | .import { 43 | position: absolute; 44 | right: 40px; 45 | align-self: center; 46 | padding: 5px; 47 | border-radius: 100%; 48 | } 49 | } 50 | 51 | .table-wrapper { 52 | overflow: scroll; 53 | 54 | @media screen and (min-width: $bp-md) { 55 | overflow: hidden; 56 | } 57 | } 58 | 59 | .group-description-cell { 60 | overflow: hidden; 61 | text-align: left; 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | 65 | @media screen and (min-width: $bp-md) { 66 | max-width: 300px; 67 | } 68 | 69 | @media screen and (min-width: $bp-lg) { 70 | max-width: 450px; 71 | } 72 | 73 | @media screen and (min-width: $bp-xl) { 74 | max-width: 600px; 75 | } 76 | 77 | @media screen and (min-width: $bp-2xl) { 78 | max-width: 1000px; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Components/PackageOverview/PackageOverview.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .package-overview-wrapper { 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100%; 9 | height: 128px; 10 | margin: 0 0 40px; 11 | overflow-x: hidden; 12 | background: $color-background-inverse; 13 | border-radius: 15px; 14 | 15 | .package-overview { 16 | display: grid; 17 | grid-template-areas: 18 | "heading today learnbtn" 19 | "unresolved unactivated activatebtn"; 20 | grid-template-rows: 50% 50%; 21 | grid-template-columns: 46% 27% 27%; 22 | width: 90%; 23 | height: 80%; 24 | 25 | .package-inner { 26 | width: 100%; 27 | margin: auto; 28 | font-size: 15px; 29 | color: $color-main-text-inverse; 30 | text-align: left; 31 | 32 | &.package-heading { 33 | grid-area: heading; 34 | overflow: hidden; 35 | font-size: 25px; 36 | text-align: left; 37 | text-overflow: ellipsis; 38 | white-space: nowrap; 39 | } 40 | 41 | &.package-unresolved { 42 | grid-area: unresolved; 43 | } 44 | 45 | &.package-today { 46 | grid-area: today; 47 | } 48 | 49 | &.package-unactivated { 50 | grid-area: unactivated; 51 | } 52 | } 53 | 54 | .package-btn-wrapper { 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | width: 100%; 59 | height: 100%; 60 | @media screen and (min-width: $bp-md) { 61 | justify-content: flex-end; 62 | } 63 | 64 | .package-btn-inner { 65 | width: 100%; 66 | 67 | @media screen and (min-width: $bp-md) { 68 | width: 70%; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/hooks/useScrollBlock.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/reecelucas/2f510e6b8504008deaaa52732202d2da 2 | import { useRef } from "react"; 3 | 4 | const safeDocument = document; 5 | 6 | /** 7 | * Usage: 8 | * const [blockScroll, allowScroll] = useScrollBlock(); 9 | */ 10 | export const useScrollBlock = () => { 11 | const scrollBlocked = useRef(false); 12 | const html = safeDocument.documentElement; 13 | const { body } = safeDocument; 14 | 15 | const blockScroll = () => { 16 | if (!body || !body.style || scrollBlocked.current) return; 17 | if (document === undefined) return; 18 | 19 | const scrollBarWidth = window.innerWidth - html.clientWidth; 20 | const bodyPaddingRight = 21 | Number(window.getComputedStyle(body).getPropertyValue("padding-right")) || 22 | 0; 23 | 24 | /** 25 | * 1. Fixes a bug in iOS and desktop Safari whereby setting 26 | * `overflow: hidden` on the html/body does not prevent scrolling. 27 | * 2. Fixes a bug in desktop Safari where `overflowY` does not prevent 28 | * scroll if an `overflow-x` style is also applied to the body. 29 | */ 30 | html.style.position = "relative"; /* [1] */ 31 | html.style.overflow = "hidden"; /* [2] */ 32 | body.style.position = "relative"; /* [1] */ 33 | body.style.overflow = "hidden"; /* [2] */ 34 | body.style.paddingRight = `${bodyPaddingRight + scrollBarWidth}px`; 35 | 36 | scrollBlocked.current = true; 37 | }; 38 | 39 | const allowScroll = () => { 40 | if (!body || !body.style || !scrollBlocked.current) return; 41 | 42 | html.style.position = ""; 43 | html.style.overflow = ""; 44 | body.style.position = ""; 45 | body.style.overflow = ""; 46 | body.style.paddingRight = ""; 47 | 48 | scrollBlocked.current = false; 49 | }; 50 | 51 | return [blockScroll, allowScroll]; 52 | }; 53 | -------------------------------------------------------------------------------- /src/Components/SlideShow/SlideShow.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import Button from "../../Components/Button/Button.jsx"; 5 | import Indicator from "../../Components/Indicators/PageIndicator/PageIndicator.jsx"; 6 | 7 | import "./SlideShow.scss"; 8 | 9 | const SlideShow = ({ pages, onEnd, canContinue }) => { 10 | const { t } = useTranslation(); 11 | 12 | const [index, setIndex] = useState(0); 13 | 14 | useEffect(() => { 15 | //close first startup when index is higher than pages length. WARNING: index starts at 0, length at 1, so === is used 16 | if (index === pages.length) { 17 | onEnd(); 18 | } 19 | }, [index, onEnd, pages.length]); 20 | 21 | return ( 22 |
23 |
{pages[index]}
24 |
25 |
26 | 34 |
35 |
36 | 37 |
38 |
39 | 49 |
50 |
51 |
52 | ); 53 | }; 54 | 55 | export default SlideShow; 56 | -------------------------------------------------------------------------------- /src/images/logo/color-rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/default-themes/dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-background-inverse: #333340; 3 | --color-background-muted: #333340; 4 | --color-background: #282833; 5 | --color-alternative: #333340; 6 | --color-form: #333340; 7 | --color-box-shadow: #000; 8 | --color-box-shadow-light: #000; 9 | --color-main-text-light: #fff; 10 | 11 | --color-main-text: #fff; 12 | --color-main-text-90: #fff; 13 | --color-main-text-80: #fff; 14 | --color-main-text-70: #fff; 15 | --color-main-text-60: #fff; 16 | --color-main-text-50: #fff; 17 | --color-main-text-40: #fff; 18 | --color-main-text-30: #fff; 19 | --color-main-text-20: #fff; 20 | --color-main-text-10: #fff; 21 | 22 | --color-main-text-inverse: #fff; 23 | 24 | --color-primary-light: #03dac5; 25 | 26 | --color-primary: #bb86fc; 27 | --color-primary-90: #bb86fc; 28 | --color-primary-80: #bb86fc; 29 | --color-primary-70: #bb86fc; 30 | --color-primary-60: #bb86fc; 31 | --color-primary-50: #bb86fc; 32 | --color-primary-40: #bb86fc; 33 | --color-primary-30: #bb86fc; 34 | --color-primary-20: #bb86fc; 35 | --color-primary-10: #bb86fc; 36 | 37 | --color-primary-dark: #bb86fc; 38 | --color-primary-dark-90: #bb86fc; 39 | --color-primary-dark-80: #bb86fc; 40 | --color-primary-dark-70: #bb86fc; 41 | --color-primary-dark-60: #bb86fc; 42 | --color-primary-dark-50: #bb86fc; 43 | --color-primary-dark-40: #bb86fc; 44 | --color-primary-dark-30: #bb86fc; 45 | --color-primary-dark-20: #bb86fc; 46 | --color-primary-dark-10: #bb86fc; 47 | 48 | --color-white: #fff; 49 | --color-dark: #000; 50 | --color-grey: #cbcbcb; 51 | --color-red-light: #f9c5cc; 52 | --color-red: #e53935; 53 | --color-red-dark: #e53935; 54 | --color-yellow: #ffeb3b; 55 | --color-yellow-dark: #c8b900; 56 | --color-green-light: #c8f1e5; 57 | --color-green: #24aa65; 58 | --color-green-dark: #24aa65; 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | 85 | # Webpack 86 | .webpack/ 87 | 88 | # vscode 89 | .vscode 90 | 91 | dist/ 92 | 93 | # react 94 | build/ 95 | 96 | # electron-builder 97 | builds/ 98 | dev-app-update.yml 99 | 100 | # localazy 101 | localazy.keys.json 102 | 103 | # jetbrains 104 | .idea/ 105 | -------------------------------------------------------------------------------- /public/default-themes/light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-background-inverse: #2a2e36; 3 | --color-background-muted: #eceeee; 4 | --color-background: #fff; 5 | --color-alternative: #1c1c1a; 6 | --color-form: #252421; 7 | --color-box-shadow: #212121; 8 | --color-box-shadow-light: #b4bed6; 9 | --color-main-text-light: #383838; 10 | 11 | --color-main-text: #212121; 12 | --color-main-text-90: #363746; 13 | --color-main-text-80: #4d4d5b; 14 | --color-main-text-70: #63636f; 15 | --color-main-text-60: #797984; 16 | --color-main-text-50: #8f8f98; 17 | --color-main-text-40: #a6a6ad; 18 | --color-main-text-30: #bbbbc1; 19 | --color-main-text-20: #d2d2d6; 20 | --color-main-text-10: #e8e8ea; 21 | 22 | --color-main-text-inverse: #fff; 23 | 24 | --color-primary-light: #bec2f3; 25 | 26 | --color-primary: #727cf5; 27 | --color-primary-90: #7f88f5; 28 | --color-primary-80: #8e96f7; 29 | --color-primary-70: #9ca3f7; 30 | --color-primary-60: #aab0f9; 31 | --color-primary-50: #b8bdf9; 32 | --color-primary-40: #c6cafb; 33 | --color-primary-30: #d4d7fb; 34 | --color-primary-20: #e2e4fd; 35 | --color-primary-10: #f0f1fd; 36 | 37 | --color-primary-dark: #4c51ec; 38 | --color-primary-dark-90: #6469ee; 39 | --color-primary-dark-80: #767af0; 40 | --color-primary-dark-70: #868af2; 41 | --color-primary-dark-60: #989bf4; 42 | --color-primary-dark-50: #a9abf5; 43 | --color-primary-dark-40: #babcf7; 44 | --color-primary-dark-30: #cbccf9; 45 | --color-primary-dark-20: #dcddfb; 46 | --color-primary-dark-10: #edeefd; 47 | 48 | --color-white: #fff; 49 | --color-dark: #000; 50 | --color-grey: #cbcbcb; 51 | --color-red-light: #f9c5cc; 52 | --color-red: #ff586e; 53 | --color-red-dark: #fd334e; 54 | --color-yellow: #ffeb3b; 55 | --color-yellow-dark: #c8b900; 56 | --color-green-light: #c8f1e5; 57 | --color-green: #0acf97; 58 | --color-green-dark: #06b483; 59 | } 60 | -------------------------------------------------------------------------------- /src/screens/Admin/Admin.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .admin { 5 | grid-area: main; 6 | width: 100%; 7 | 8 | .invite-codes { 9 | display: flex; 10 | justify-content: center; 11 | margin: 80px 0; 12 | @media screen and (min-width: $bp-md) { 13 | padding: 50px; 14 | } 15 | 16 | .invite-codes-wrapper { 17 | width: 100%; 18 | max-width: 400px; 19 | 20 | @media screen and (min-width: $bp-md) { 21 | max-width: 600px; 22 | } 23 | 24 | @media screen and (min-width: $bp-lg) { 25 | max-width: 1700px; 26 | } 27 | 28 | .invite-code-controls { 29 | display: flex; 30 | justify-content: space-between; 31 | margin-bottom: 30px; 32 | 33 | .add-btn { 34 | align-self: center; 35 | padding: 5px; 36 | cursor: pointer; 37 | border-radius: 100%; 38 | } 39 | } 40 | 41 | .invite-code-container { 42 | display: grid; 43 | grid-template-columns: repeat(6, minmax(0, 1fr)); 44 | gap: 10px; 45 | width: 100%; 46 | 47 | @media (max-width: 1600px) { 48 | grid-template-columns: repeat(5, minmax(0, 1fr)); 49 | } 50 | 51 | @media (max-width: 1400px) { 52 | grid-template-columns: repeat(4, minmax(0, 1fr)); 53 | } 54 | 55 | @media (max-width: 1200px) { 56 | grid-template-columns: repeat(3, minmax(0, 1fr)); 57 | } 58 | 59 | @media (max-width: 1000px) { 60 | grid-template-columns: repeat(2, minmax(0, 1fr)); 61 | } 62 | 63 | @media (max-width: 800px) { 64 | grid-template-columns: repeat(1, minmax(0, 1fr)); 65 | } 66 | } 67 | } 68 | } 69 | 70 | .center-wrapper { 71 | display: flex; 72 | align-items: center; 73 | justify-content: center; 74 | width: 100%; 75 | height: 100%; 76 | text-align: center; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/screens/Profile/Profile.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .profile-screen { 5 | width: 100%; 6 | margin: 80px auto; 7 | 8 | @media screen and (min-width: $bp-md) { 9 | max-width: 600px; 10 | padding: 50px; 11 | margin: 0 auto; 12 | } 13 | 14 | .profile-avatar-wrapper { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | width: 150px; 19 | height: 150px; 20 | margin: 0 auto; 21 | border: 1px solid $color-alternative; 22 | border-radius: 100%; 23 | 24 | .profile-avatar { 25 | width: 70px; 26 | height: 70px; 27 | } 28 | } 29 | 30 | .profile-username { 31 | margin: 20px 0; 32 | } 33 | 34 | .table-wrapper { 35 | width: 100%; 36 | overflow-x: scroll; 37 | 38 | @media screen and (min-width: $bp-md) { 39 | overflow-x: hidden; 40 | } 41 | } 42 | 43 | .account-settings-header { 44 | margin: 15px 0; 45 | font-size: 15px; 46 | font-weight: bold; 47 | text-align: left; 48 | text-transform: uppercase; 49 | } 50 | 51 | .account-settings { 52 | display: flex; 53 | flex-direction: column; 54 | border: 1px solid $color-red; 55 | border-radius: 15px; 56 | 57 | .account-settings-fields { 58 | display: flex; 59 | padding: 15px; 60 | 61 | &.border-bottom { 62 | border-bottom: 1px solid $color-background-muted; 63 | } 64 | 65 | .description-wrapper { 66 | display: flex; 67 | flex-direction: column; 68 | justify-content: space-around; 69 | width: 65%; 70 | height: 60%; 71 | margin: auto 0; 72 | margin-left: 10px; 73 | 74 | h3 { 75 | font-size: 15px; 76 | text-align: left; 77 | } 78 | 79 | p { 80 | font-size: 11px; 81 | text-align: left; 82 | } 83 | } 84 | } 85 | 86 | .button-wrapper { 87 | width: 35%; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /.github/workflows/pullrequest.yml: -------------------------------------------------------------------------------- 1 | name: check pull requests 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | check: 7 | name: 🔍 Check pull request 8 | runs-on: ubuntu-latest 9 | if: ${{ !contains(github.event.pull_request.labels.*.name, 'reviewed/skip-check') }} 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: check translations 15 | uses: dorny/paths-filter@v2 16 | id: changes 17 | with: 18 | list-files: json 19 | filters: | 20 | translations: 21 | - "src/i18n/locales/!(en)/*.json" 22 | - "src/i18n/locales/locales.json" 23 | 24 | - name: format files output 25 | if: steps.changes.outputs.translations == 'true' 26 | id: files-output 27 | run: | 28 | files=$(node -e 'console.log("- " + JSON.parse(`${{ steps.changes.outputs.translations_files }}`).join("\n- "))') 29 | files="${files//$'\n'/'%0A'}" 30 | echo "::set-output name=files::${files}" 31 | 32 | - name: comment on pull request 33 | if: steps.changes.outputs.translations == 'true' 34 | uses: unsplash/comment-on-pr@master 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.VOCASCAN_BOT_TOKEN }} 37 | with: 38 | msg: | 39 | Thank you for your pull request. 40 | Please remove your translations. Change only the `en/default.json` if necessary. 41 | If this pull request is merged into the [`experimental`](https://github.com/vocascan/vocascan-frontend/tree/experimental) branch, it can be translated via [localazy](https://localazy.com/p/vocascan). 42 | 43 | Please remove following files from pull request changes. 44 | ``` 45 | ${{ steps.files-output.outputs.files }} 46 | ``` 47 | 48 | - name: check if failed 49 | if: steps.changes.outputs.translations == 'true' 50 | run: | 51 | echo "only change en.json" 52 | exit 1 53 | -------------------------------------------------------------------------------- /src/Components/Button/Button.jsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import React, { useCallback, useEffect, useRef, useState } from "react"; 3 | 4 | import "./Button.scss"; 5 | 6 | import LoadingIndicator from "../Indicators/LoadingIndicator/LoadingIndicator"; 7 | 8 | const Button = ({ 9 | uppercase = false, 10 | variant = "default", 11 | disabled = false, 12 | appearance = "primary", 13 | block = false, 14 | onClick = () => null, 15 | children, 16 | loading = false, 17 | promiseButton = false, 18 | promiseButtonMinTime = 500, 19 | className = "", 20 | ...props 21 | }) => { 22 | const classes = clsx( 23 | "btn", 24 | `btn-${appearance}`, 25 | `btn-${variant}`, 26 | uppercase && "btn-uppercase", 27 | block && "btn-block", 28 | disabled && "btn-disabled", 29 | className 30 | ); 31 | 32 | const [promiseLoading, setPromiseLoading] = useState(false); 33 | const timeoutId = useRef(null); 34 | 35 | useEffect(() => { 36 | return () => clearTimeout(timeoutId.current); 37 | }, []); 38 | 39 | const handleClick = useCallback( 40 | async (event) => { 41 | const timeStart = Date.now(); 42 | setPromiseLoading(true); 43 | 44 | await Promise.resolve(onClick(event)); 45 | 46 | const timeLeft = -Date.now() + timeStart + promiseButtonMinTime; // calculate time left, if negative time already passed 47 | 48 | if (timeLeft <= 0) { 49 | setPromiseLoading(false); 50 | } else { 51 | timeoutId.current = setTimeout(() => { 52 | setPromiseLoading(false); 53 | timeoutId.current = null; 54 | }, timeLeft); 55 | } 56 | }, 57 | [onClick, promiseButtonMinTime] 58 | ); 59 | 60 | return ( 61 | 70 | ); 71 | }; 72 | 73 | export default Button; 74 | -------------------------------------------------------------------------------- /src/Components/Table/Table.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | @import "../../constants"; 3 | 4 | .table { 5 | width: 100%; 6 | padding-top: 40px; 7 | margin: 20px 0; 8 | border-spacing: 0; 9 | 10 | @media screen and (min-width: $bp-md) { 11 | padding-top: 0; 12 | } 13 | 14 | .table-striped { 15 | tr:nth-child(even) { 16 | background-color: $color-background-muted; 17 | } 18 | } 19 | 20 | .th { 21 | padding: 5px; 22 | text-align: left; 23 | border-bottom: 1px solid $color-main-text; 24 | 25 | .header-text { 26 | display: flex; 27 | align-items: center; 28 | justify-content: space-between; 29 | font-weight: bold; 30 | line-height: 24px; 31 | user-select: none; 32 | } 33 | 34 | .sort-placeholder { 35 | width: 24px; 36 | } 37 | } 38 | 39 | .td { 40 | height: 30px; 41 | padding: 5px; 42 | text-align: left; 43 | 44 | a { 45 | font-weight: bold; 46 | color: $color-primary; 47 | text-decoration: none; 48 | 49 | &:hover { 50 | color: $color-primary-dark; 51 | } 52 | } 53 | 54 | .action-col { 55 | display: flex; 56 | justify-content: flex-end; 57 | } 58 | } 59 | } 60 | 61 | .pagination { 62 | position: absolute; 63 | right: 0; 64 | left: 0; 65 | display: flex; 66 | align-items: center; 67 | justify-content: space-between; 68 | width: 90%; 69 | margin-right: auto; 70 | margin-left: auto; 71 | @media screen and (min-width: $bp-md) { 72 | position: relative; 73 | display: flex; 74 | align-items: center; 75 | justify-content: space-between; 76 | width: 100%; 77 | margin: 10px 0; 78 | } 79 | 80 | .pagination-options { 81 | display: flex; 82 | align-items: center; 83 | justify-content: space-between; 84 | } 85 | 86 | .pagination-button { 87 | padding: 0; 88 | margin-right: 5px; 89 | } 90 | 91 | .pagination-text { 92 | padding: 0; 93 | margin-right: 5px; 94 | font-size: 13px; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Components/Form/Switch/Switch.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import InfoIcon from "@material-ui/icons/Info"; 4 | 5 | import Tooltip from "../../Tooltip/Tooltip.jsx"; 6 | 7 | import "./Switch.scss"; 8 | 9 | const Switch = ({ 10 | disabled = false, 11 | onChange = () => null, 12 | optionRight = "insert a label", 13 | infoRight = null, 14 | switcher = false, 15 | optionLeft = null, 16 | infoLeft = null, 17 | checked = false, 18 | label = null, 19 | appearance = "default", 20 | }) => { 21 | return ( 22 |
23 | {label && } 24 |
29 | {optionLeft && ( 30 |
31 | 32 | {infoLeft && ( 33 | 39 | )} 40 |
41 | )} 42 | 51 | {switcher && optionRight && ( 52 |
53 | {infoRight && ( 54 | 59 | )} 60 | 61 |
62 | )} 63 |
64 | 65 |
66 | ); 67 | }; 68 | 69 | export default Switch; 70 | -------------------------------------------------------------------------------- /src/Components/SelectionBox/SelectionBox.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | 3 | .select-box { 4 | display: flex; 5 | display: flex; 6 | flex-direction: column; 7 | flex-direction: column; 8 | justify-content: space-between; 9 | justify-content: space-between; 10 | min-width: 300px; 11 | max-width: 400px; 12 | height: 100%; 13 | margin: 0 20px; 14 | background: $color-background; 15 | border-radius: 10px; 16 | box-shadow: -1px 3px 5px $color-box-shadow-light; 17 | 18 | .select-box-header { 19 | display: flex; 20 | flex-direction: column; 21 | 22 | .select-box-header-heading { 23 | display: flex; 24 | justify-content: center; 25 | padding: 20px; 26 | background: $color-background-inverse; 27 | border-top-left-radius: 10px; 28 | border-top-right-radius: 10px; 29 | 30 | &.important { 31 | background: $color-red; 32 | } 33 | 34 | .select-box-header-heading-text { 35 | margin: auto; 36 | font-size: 20px; 37 | color: $color-white; 38 | } 39 | } 40 | 41 | .select-box-header-logo { 42 | display: flex; 43 | justify-content: center; 44 | width: 100%; 45 | padding: 20px 0; 46 | 47 | .select-box-header-logo-img { 48 | width: 40%; 49 | margin: auto; 50 | } 51 | } 52 | } 53 | 54 | .select-box-description { 55 | width: 100%; 56 | min-height: 200px; 57 | padding: 20px 0; 58 | 59 | .select-box-description-ul { 60 | width: 100%; 61 | height: 100%; 62 | padding: 0 0 0 10%; 63 | 64 | .description-list-item { 65 | display: flex; 66 | margin: 5px 0; 67 | 68 | .select-box-header-logo-img { 69 | width: 5%; 70 | margin: 0 10px 0 0; 71 | object-fit: scale-down; 72 | } 73 | 74 | .description-list-item-item { 75 | width: 80%; 76 | text-align: left; 77 | } 78 | } 79 | } 80 | } 81 | 82 | .select-box-footer { 83 | display: flex; 84 | align-items: stretch; 85 | justify-content: center; 86 | padding: 20px; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.github/workflows/translations.yml: -------------------------------------------------------------------------------- 1 | name: Translations 2 | on: 3 | push: 4 | branches: 5 | - "experimental" 6 | schedule: 7 | - cron: "0 12 * * 0" 8 | workflow_dispatch: 9 | 10 | jobs: 11 | update-translations: 12 | name: 📥 Update translations 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | ref: experimental 19 | token: ${{ secrets.VOCASCAN_BOT_TOKEN }} 20 | 21 | - name: Download 22 | uses: localazy/download@v1 23 | with: 24 | read_key: ${{ secrets.LOCALAZY_READ_KEY }} 25 | write_key: ${{ secrets.LOCALAZY_WRITE_KEY }} 26 | 27 | - name: Push 28 | uses: stefanzweifel/git-auto-commit-action@v4 29 | with: 30 | commit_message: "[skip ci] updated translations via Localazy" 31 | commit_user_name: VocascanBot 32 | commit_user_email: vocascan@gmail.com 33 | commit_author: VocascanBot 34 | file_pattern: src/i18n/locales/**/default.json 35 | branch: experimental 36 | 37 | - name: Install dependency 38 | run: | 39 | mv package.json package.json.bak 40 | npm install --no-package-lock --no-save semver 41 | mv package.json.bak package.json 42 | 43 | - name: Extract version 44 | uses: actions/github-script@v5 45 | id: prepare 46 | with: 47 | script: | 48 | const Semver = require('semver'); 49 | const package = require('./package.json'); 50 | const semver = Semver.parse(package.version); 51 | let version = 0; 52 | if (semver) { 53 | version = 100000000 * semver.major + 10000 * semver.minor + semver.patch; 54 | } 55 | core.setOutput("version", version.toString(10)); 56 | 57 | - name: Upload 58 | uses: localazy/upload@v1 59 | with: 60 | read_key: ${{ secrets.LOCALAZY_READ_KEY }} 61 | write_key: ${{ secrets.LOCALAZY_WRITE_KEY }} 62 | app_version: ${{ steps.prepare.outputs.version }} 63 | -------------------------------------------------------------------------------- /src/Components/Form/Textarea/Textarea.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from "react"; 2 | 3 | import "./Textarea.scss"; 4 | 5 | const Textarea = ({ 6 | type = "textarea", 7 | placeholder = null, 8 | onChange = () => null, 9 | error = false, 10 | errorText = null, 11 | required = false, 12 | value = "", 13 | autoFocus = false, 14 | rows = 5, 15 | maxLength, 16 | ...props 17 | }) => { 18 | const [flow, setFlow] = useState(false); 19 | const [focused, setFocused] = useState(false); 20 | const [indicator, setIndicator] = useState(false); 21 | 22 | const handleFocus = useCallback(() => { 23 | setFocused(true); 24 | setFlow(true); 25 | }, []); 26 | 27 | const onBlur = useCallback(() => { 28 | setFocused(false); 29 | setFlow(!!value); 30 | }, [value]); 31 | 32 | useEffect(() => { 33 | setFlow(!!value || autoFocus); 34 | setFocused(autoFocus); 35 | // only trigger once the component renders 36 | // eslint-disable-next-line react-hooks/exhaustive-deps 37 | }, []); 38 | 39 | useEffect(() => { 40 | setFlow(!!value || focused); 41 | }, [value, focused]); 42 | 43 | useEffect(() => { 44 | setIndicator(maxLength - value.length); 45 | }, [maxLength, value]); 46 | 47 | return ( 48 |
49 | {`${placeholder}${required ? " *" : ""}`} 52 |