diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f614dcbe..2f151e2e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -23,7 +23,7 @@ jobs: if_true: false if_false: true - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: submodules: 'recursive' diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml new file mode 100644 index 00000000..5f5d4520 --- /dev/null +++ b/.github/workflows/prettier.yml @@ -0,0 +1,20 @@ +name: Lint Frontend +on: + push: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: 'recursive' + - name: Restore npm dependencies + working-directory: Parlance.ClientApp + run: | + npm ci + - name: Lint + working-directory: Parlance.ClientApp + run: | + npm run lint \ No newline at end of file diff --git a/Parlance.ClientApp/.prettierignore b/Parlance.ClientApp/.prettierignore new file mode 100644 index 00000000..42e57c72 --- /dev/null +++ b/Parlance.ClientApp/.prettierignore @@ -0,0 +1,3 @@ +dist +obj +public/resources/translations diff --git a/Parlance.ClientApp/.prettierrc b/Parlance.ClientApp/.prettierrc new file mode 100644 index 00000000..0156c36e --- /dev/null +++ b/Parlance.ClientApp/.prettierrc @@ -0,0 +1,4 @@ +{ + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/Parlance.ClientApp/README.md b/Parlance.ClientApp/README.md index c56022ee..d566f80d 100644 --- a/Parlance.ClientApp/README.md +++ b/Parlance.ClientApp/README.md @@ -7,102 +7,102 @@ guide [here](https://github.com/facebookincubator/create-react-app/blob/master/p ## Table of Contents -- [Updating to New Releases](#updating-to-new-releases) -- [Sending Feedback](#sending-feedback) -- [Folder Structure](#folder-structure) -- [Available Scripts](#available-scripts) - - [npm start](#npm-start) - - [npm test](#npm-test) - - [npm run build](#npm-run-build) - - [npm run eject](#npm-run-eject) -- [Supported Language Features and Polyfills](#supported-language-features-and-polyfills) -- [Syntax Highlighting in the Editor](#syntax-highlighting-in-the-editor) -- [Displaying Lint Output in the Editor](#displaying-lint-output-in-the-editor) -- [Debugging in the Editor](#debugging-in-the-editor) -- [Formatting Code Automatically](#formatting-code-automatically) -- [Changing the Page ``](#changing-the-page-title) -- [Installing a Dependency](#installing-a-dependency) -- [Importing a Component](#importing-a-component) -- [Code Splitting](#code-splitting) -- [Adding a Stylesheet](#adding-a-stylesheet) -- [Post-Processing CSS](#post-processing-css) -- [Adding a CSS Preprocessor (Sass, Less etc.)](#adding-a-css-preprocessor-sass-less-etc) -- [Adding Images, Fonts, and Files](#adding-images-fonts-and-files) -- [Using the `public` Folder](#using-the-public-folder) - - [Changing the HTML](#changing-the-html) - - [Adding Assets Outside of the Module System](#adding-assets-outside-of-the-module-system) - - [When to Use the `public` Folder](#when-to-use-the-public-folder) -- [Using Global Variables](#using-global-variables) -- [Adding Bootstrap](#adding-bootstrap) - - [Using a Custom Theme](#using-a-custom-theme) -- [Adding Flow](#adding-flow) -- [Adding Custom Environment Variables](#adding-custom-environment-variables) - - [Referencing Environment Variables in the HTML](#referencing-environment-variables-in-the-html) - - [Adding Temporary Environment Variables In Your Shell](#adding-temporary-environment-variables-in-your-shell) - - [Adding Development Environment Variables In `.env`](#adding-development-environment-variables-in-env) -- [Can I Use Decorators?](#can-i-use-decorators) -- [Integrating with an API Backend](#integrating-with-an-api-backend) - - [Node](#node) - - [Ruby on Rails](#ruby-on-rails) -- [Proxying API Requests in Development](#proxying-api-requests-in-development) - - ["Invalid Host Header" Errors After Configuring Proxy](#invalid-host-header-errors-after-configuring-proxy) - - [Configuring the Proxy Manually](#configuring-the-proxy-manually) - - [Configuring a WebSocket Proxy](#configuring-a-websocket-proxy) -- [Using HTTPS in Development](#using-https-in-development) -- [Generating Dynamic `<meta>` Tags on the Server](#generating-dynamic-meta-tags-on-the-server) -- [Pre-Rendering into Static HTML Files](#pre-rendering-into-static-html-files) -- [Injecting Data from the Server into the Page](#injecting-data-from-the-server-into-the-page) -- [Running Tests](#running-tests) - - [Filename Conventions](#filename-conventions) - - [Command Line Interface](#command-line-interface) - - [Version Control Integration](#version-control-integration) - - [Writing Tests](#writing-tests) - - [Testing Components](#testing-components) - - [Using Third Party Assertion Libraries](#using-third-party-assertion-libraries) - - [Initializing Test Environment](#initializing-test-environment) - - [Focusing and Excluding Tests](#focusing-and-excluding-tests) - - [Coverage Reporting](#coverage-reporting) - - [Continuous Integration](#continuous-integration) - - [Disabling jsdom](#disabling-jsdom) - - [Snapshot Testing](#snapshot-testing) - - [Editor Integration](#editor-integration) -- [Developing Components in Isolation](#developing-components-in-isolation) - - [Getting Started with Storybook](#getting-started-with-storybook) - - [Getting Started with Styleguidist](#getting-started-with-styleguidist) -- [Making a Progressive Web App](#making-a-progressive-web-app) - - [Opting Out of Caching](#opting-out-of-caching) - - [Offline-First Considerations](#offline-first-considerations) - - [Progressive Web App Metadata](#progressive-web-app-metadata) -- [Analyzing the Bundle Size](#analyzing-the-bundle-size) -- [Deployment](#deployment) - - [Static Server](#static-server) - - [Other Solutions](#other-solutions) - - [Serving Apps with Client-Side Routing](#serving-apps-with-client-side-routing) - - [Building for Relative Paths](#building-for-relative-paths) - - [Azure](#azure) - - [Firebase](#firebase) - - [GitHub Pages](#github-pages) - - [Heroku](#heroku) - - [Netlify](#netlify) - - [Now](#now) - - [S3 and CloudFront](#s3-and-cloudfront) - - [Surge](#surge) -- [Advanced Configuration](#advanced-configuration) -- [Troubleshooting](#troubleshooting) - - [`npm start` doesn’t detect changes](#npm-start-doesnt-detect-changes) - - [`npm test` hangs on macOS Sierra](#npm-test-hangs-on-macos-sierra) - - [`npm run build` exits too early](#npm-run-build-exits-too-early) - - [`npm run build` fails on Heroku](#npm-run-build-fails-on-heroku) - - [`npm run build` fails to minify](#npm-run-build-fails-to-minify) - - [Moment.js locales are missing](#momentjs-locales-are-missing) -- [Something Missing?](#something-missing) +- [Updating to New Releases](#updating-to-new-releases) +- [Sending Feedback](#sending-feedback) +- [Folder Structure](#folder-structure) +- [Available Scripts](#available-scripts) + - [npm start](#npm-start) + - [npm test](#npm-test) + - [npm run build](#npm-run-build) + - [npm run eject](#npm-run-eject) +- [Supported Language Features and Polyfills](#supported-language-features-and-polyfills) +- [Syntax Highlighting in the Editor](#syntax-highlighting-in-the-editor) +- [Displaying Lint Output in the Editor](#displaying-lint-output-in-the-editor) +- [Debugging in the Editor](#debugging-in-the-editor) +- [Formatting Code Automatically](#formatting-code-automatically) +- [Changing the Page `<title>`](#changing-the-page-title) +- [Installing a Dependency](#installing-a-dependency) +- [Importing a Component](#importing-a-component) +- [Code Splitting](#code-splitting) +- [Adding a Stylesheet](#adding-a-stylesheet) +- [Post-Processing CSS](#post-processing-css) +- [Adding a CSS Preprocessor (Sass, Less etc.)](#adding-a-css-preprocessor-sass-less-etc) +- [Adding Images, Fonts, and Files](#adding-images-fonts-and-files) +- [Using the `public` Folder](#using-the-public-folder) + - [Changing the HTML](#changing-the-html) + - [Adding Assets Outside of the Module System](#adding-assets-outside-of-the-module-system) + - [When to Use the `public` Folder](#when-to-use-the-public-folder) +- [Using Global Variables](#using-global-variables) +- [Adding Bootstrap](#adding-bootstrap) + - [Using a Custom Theme](#using-a-custom-theme) +- [Adding Flow](#adding-flow) +- [Adding Custom Environment Variables](#adding-custom-environment-variables) + - [Referencing Environment Variables in the HTML](#referencing-environment-variables-in-the-html) + - [Adding Temporary Environment Variables In Your Shell](#adding-temporary-environment-variables-in-your-shell) + - [Adding Development Environment Variables In `.env`](#adding-development-environment-variables-in-env) +- [Can I Use Decorators?](#can-i-use-decorators) +- [Integrating with an API Backend](#integrating-with-an-api-backend) + - [Node](#node) + - [Ruby on Rails](#ruby-on-rails) +- [Proxying API Requests in Development](#proxying-api-requests-in-development) + - ["Invalid Host Header" Errors After Configuring Proxy](#invalid-host-header-errors-after-configuring-proxy) + - [Configuring the Proxy Manually](#configuring-the-proxy-manually) + - [Configuring a WebSocket Proxy](#configuring-a-websocket-proxy) +- [Using HTTPS in Development](#using-https-in-development) +- [Generating Dynamic `<meta>` Tags on the Server](#generating-dynamic-meta-tags-on-the-server) +- [Pre-Rendering into Static HTML Files](#pre-rendering-into-static-html-files) +- [Injecting Data from the Server into the Page](#injecting-data-from-the-server-into-the-page) +- [Running Tests](#running-tests) + - [Filename Conventions](#filename-conventions) + - [Command Line Interface](#command-line-interface) + - [Version Control Integration](#version-control-integration) + - [Writing Tests](#writing-tests) + - [Testing Components](#testing-components) + - [Using Third Party Assertion Libraries](#using-third-party-assertion-libraries) + - [Initializing Test Environment](#initializing-test-environment) + - [Focusing and Excluding Tests](#focusing-and-excluding-tests) + - [Coverage Reporting](#coverage-reporting) + - [Continuous Integration](#continuous-integration) + - [Disabling jsdom](#disabling-jsdom) + - [Snapshot Testing](#snapshot-testing) + - [Editor Integration](#editor-integration) +- [Developing Components in Isolation](#developing-components-in-isolation) + - [Getting Started with Storybook](#getting-started-with-storybook) + - [Getting Started with Styleguidist](#getting-started-with-styleguidist) +- [Making a Progressive Web App](#making-a-progressive-web-app) + - [Opting Out of Caching](#opting-out-of-caching) + - [Offline-First Considerations](#offline-first-considerations) + - [Progressive Web App Metadata](#progressive-web-app-metadata) +- [Analyzing the Bundle Size](#analyzing-the-bundle-size) +- [Deployment](#deployment) + - [Static Server](#static-server) + - [Other Solutions](#other-solutions) + - [Serving Apps with Client-Side Routing](#serving-apps-with-client-side-routing) + - [Building for Relative Paths](#building-for-relative-paths) + - [Azure](#azure) + - [Firebase](#firebase) + - [GitHub Pages](#github-pages) + - [Heroku](#heroku) + - [Netlify](#netlify) + - [Now](#now) + - [S3 and CloudFront](#s3-and-cloudfront) + - [Surge](#surge) +- [Advanced Configuration](#advanced-configuration) +- [Troubleshooting](#troubleshooting) + - [`npm start` doesn’t detect changes](#npm-start-doesnt-detect-changes) + - [`npm test` hangs on macOS Sierra](#npm-test-hangs-on-macos-sierra) + - [`npm run build` exits too early](#npm-run-build-exits-too-early) + - [`npm run build` fails on Heroku](#npm-run-build-fails-on-heroku) + - [`npm run build` fails to minify](#npm-run-build-fails-to-minify) + - [Moment.js locales are missing](#momentjs-locales-are-missing) +- [Something Missing?](#something-missing) ## Updating to New Releases Create React App is divided into two packages: -* `create-react-app` is a global command-line utility that you use to create new projects. -* `react-scripts` is a development dependency in the generated projects (including this one). +- `create-react-app` is a global command-line utility that you use to create new projects. +- `react-scripts` is a development dependency in the generated projects (including this one). You almost never need to update `create-react-app` itself: it delegates all the setup to `react-scripts`. @@ -148,8 +148,8 @@ my-app/ For the project to build, **these files must exist with exact filenames**: -* `public/index.html` is the page template; -* `src/index.js` is the JavaScript entry point. +- `public/index.html` is the page template; +- `src/index.js` is the JavaScript entry point. You can delete or rename the other files. @@ -209,12 +209,12 @@ customize it when you are ready for it. This project supports a superset of the latest JavaScript standard.<br> In addition to [ES6](https://github.com/lukehoban/es6features) syntax features, it also supports: -* [Exponentiation Operator](https://github.com/rwaldron/exponentiation-operator) (ES2016). -* [Async/await](https://github.com/tc39/ecmascript-asyncawait) (ES2017). -* [Object Rest/Spread Properties](https://github.com/sebmarkbage/ecmascript-rest-spread) (stage 3 proposal). -* [Dynamic import()](https://github.com/tc39/proposal-dynamic-import) (stage 3 proposal) -* [Class Fields and Static Properties](https://github.com/tc39/proposal-class-public-fields) (part of stage 3 proposal). -* [JSX](https://facebook.github.io/react/docs/introducing-jsx.html) and [Flow](https://flowtype.org/) syntax. +- [Exponentiation Operator](https://github.com/rwaldron/exponentiation-operator) (ES2016). +- [Async/await](https://github.com/tc39/ecmascript-asyncawait) (ES2017). +- [Object Rest/Spread Properties](https://github.com/sebmarkbage/ecmascript-rest-spread) (stage 3 proposal). +- [Dynamic import()](https://github.com/tc39/proposal-dynamic-import) (stage 3 proposal) +- [Class Fields and Static Properties](https://github.com/tc39/proposal-class-public-fields) (part of stage 3 proposal). +- [JSX](https://facebook.github.io/react/docs/introducing-jsx.html) and [Flow](https://flowtype.org/) syntax. Learn more about [different proposal stages](https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-). @@ -224,12 +224,12 @@ of these proposals change in the future. Note that **the project only includes a few ES6 [polyfills](https://en.wikipedia.org/wiki/Polyfill)**: -* [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) - via [`object-assign`](https://github.com/sindresorhus/object-assign). -* [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) - via [`promise`](https://github.com/then/promise). -* [`fetch()`](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) - via [`whatwg-fetch`](https://github.com/github/fetch). +- [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + via [`object-assign`](https://github.com/sindresorhus/object-assign). +- [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) + via [`promise`](https://github.com/then/promise). +- [`fetch()`](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) + via [`whatwg-fetch`](https://github.com/github/fetch). If you use any other ES6+ features that need **runtime support** (such as `Array.from()` or `Symbol`), make sure you are including the appropriate polyfills manually, or that the browsers you are targeting already support them. @@ -287,17 +287,19 @@ Then add the block below to your `launch.json` file and put it inside the `.vsco ```json { - "version": "0.2.0", - "configurations": [{ - "name": "Chrome", - "type": "chrome", - "request": "launch", - "url": "http://localhost:3000", - "webRoot": "${workspaceRoot}/src", - "sourceMapPathOverrides": { - "webpack:///src/*": "${webRoot}/*" - } - }] + "version": "0.2.0", + "configurations": [ + { + "name": "Chrome", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000", + "webRoot": "${workspaceRoot}/src", + "sourceMapPathOverrides": { + "webpack:///src/*": "${webRoot}/*" + } + } + ] } ``` @@ -347,11 +349,11 @@ Alternatively you may use `yarn`: yarn add husky lint-staged prettier ``` -* `husky` makes it easy to use githooks as if they are npm scripts. -* `lint-staged` allows us to run scripts on staged files in git. See - this [blog post about lint-staged to learn more about it](https://medium.com/@okonetchnikov/make-linting-great-again-f3890e1ad6b8) - . -* `prettier` is the JavaScript formatter we will run before commits. +- `husky` makes it easy to use githooks as if they are npm scripts. +- `lint-staged` allows us to run scripts on staged files in git. See + this [blog post about lint-staged to learn more about it](https://medium.com/@okonetchnikov/make-linting-great-again-f3890e1ad6b8) + . +- `prettier` is the JavaScript formatter we will run before commits. Now we can make sure every file is formatted correctly by adding a few lines to the `package.json` in the project root. @@ -432,12 +434,12 @@ For example: ### `Button.js` ```js -import React, { Component } from 'react'; +import React, { Component } from "react"; class Button extends Component { - render() { - // ... - } + render() { + // ... + } } export default Button; // Don’t forget to use export default! @@ -446,13 +448,13 @@ export default Button; // Don’t forget to use export default! ### `DangerButton.js` ```js -import React, { Component } from 'react'; -import Button from './Button'; // Import a component from another file +import React, { Component } from "react"; +import Button from "./Button"; // Import a component from another file class DangerButton extends Component { - render() { - return <Button color="red" />; - } + render() { + return <Button color="red" />; + } } export default DangerButton; @@ -470,9 +472,9 @@ and as many named exports as you like. Learn more about ES6 modules: -* [When to use the curly braces?](http://stackoverflow.com/questions/36795819/react-native-es-6-when-should-i-use-curly-braces-for-import/36796281#36796281) -* [Exploring ES6: Modules](http://exploringjs.com/es6/ch_modules.html) -* [Understanding ES6: Modules](https://leanpub.com/understandinges6/read#leanpub-auto-encapsulating-code-with-modules) +- [When to use the curly braces?](http://stackoverflow.com/questions/36795819/react-native-es-6-when-should-i-use-curly-braces-for-import/36796281#36796281) +- [Exploring ES6: Modules](http://exploringjs.com/es6/ch_modules.html) +- [Understanding ES6: Modules](https://leanpub.com/understandinges6/read#leanpub-auto-encapsulating-code-with-modules) ## Code Splitting @@ -491,7 +493,7 @@ Here is an example: ### `moduleA.js` ```js -const moduleA = 'Hello'; +const moduleA = "Hello"; export { moduleA }; ``` @@ -499,26 +501,26 @@ export { moduleA }; ### `App.js` ```js -import React, { Component } from 'react'; +import React, { Component } from "react"; class App extends Component { - handleClick = () => { - import('./moduleA') - .then(({ moduleA }) => { - // Use moduleA - }) - .catch(err => { - // Handle failure - }); - }; - - render() { - return ( - <div> - <button onClick={this.handleClick}>Load</button> - </div> - ); - } + handleClick = () => { + import("./moduleA") + .then(({ moduleA }) => { + // Use moduleA + }) + .catch(err => { + // Handle failure + }); + }; + + render() { + return ( + <div> + <button onClick={this.handleClick}>Load</button> + </div> + ); + } } export default App; @@ -547,21 +549,21 @@ to **import the CSS from the JavaScript file**: ```css .Button { - padding: 20px; + padding: 20px; } ``` ### `Button.js` ```js -import React, { Component } from 'react'; -import './Button.css'; // Tell Webpack that Button.js uses these styles +import React, { Component } from "react"; +import "./Button.css"; // Tell Webpack that Button.js uses these styles class Button extends Component { - render() { - // You can use them as regular CSS styles - return <div className="Button" />; - } + render() { + // You can use them as regular CSS styles + return <div className="Button" />; + } } ``` @@ -585,9 +587,9 @@ For example, this: ```css .App { - display: flex; - flex-direction: row; - align-items: center; + display: flex; + flex-direction: row; + align-items: center; } ``` @@ -595,16 +597,16 @@ becomes this: ```css .App { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; } ``` @@ -656,7 +658,7 @@ edit `src/App.scss`, and `src/App.css` will be regenerated. To share variables between Sass files, you can use Sass imports. For example, `src/App.scss` and other component style files could include `@import "./shared.scss";` with variable definitions. -To enable importing files without using relative paths, you can add the `--include-path` option to the command +To enable importing files without using relative paths, you can add the `--include-path` option to the command in `package.json`. ``` @@ -667,8 +669,8 @@ in `package.json`. This will allow you to do imports like ```scss -@import 'styles/_colors.scss'; // assuming a styles directory under src/ -@import 'nprogress/nprogress'; // importing a css file from the nprogress node module +@import "styles/_colors.scss"; // assuming a styles directory under src/ +@import "nprogress/nprogress"; // importing a css file from the nprogress node module ``` At this point you might want to remove all CSS files from the source control, and add `src/**/*.css` to @@ -711,13 +713,13 @@ Now running `npm start` and `npm run build` also builds Sass files. `node-sass` has been reported as having the following issues: -- `node-sass --watch` has been reported to have *performance issues* in certain conditions when used in a virtual - machine or with docker. +- `node-sass --watch` has been reported to have _performance issues_ in certain conditions when used in a virtual + machine or with docker. -- Infinite styles compiling [#1939](https://github.com/facebookincubator/create-react-app/issues/1939) +- Infinite styles compiling [#1939](https://github.com/facebookincubator/create-react-app/issues/1939) -- `node-sass` has been reported as having issues with detecting new files in a - directory [#1891](https://github.com/sass/node-sass/issues/1891) +- `node-sass` has been reported as having issues with detecting new files in a + directory [#1891](https://github.com/sass/node-sass/issues/1891) `node-sass-chokidar` is used here as it addresses these issues. @@ -737,14 +739,14 @@ to [#1153](https://github.com/facebookincubator/create-react-app/issues/1153). Here is an example: ```js -import React from 'react'; -import logo from './logo.png'; // Tell Webpack this JS file uses this image +import React from "react"; +import logo from "./logo.png"; // Tell Webpack this JS file uses this image console.log(logo); // /logo.84287d09.png function Header() { - // Import result is the URL of your image - return <img src={logo} alt="Logo" />; + // Import result is the URL of your image + return <img src={logo} alt="Logo" />; } export default Header; @@ -757,7 +759,7 @@ This works in CSS too: ```css .Logo { - background-image: url(./logo.png); + background-image: url(./logo.png); } ``` @@ -791,9 +793,9 @@ For example, see the sections on [adding a stylesheet](#adding-a-stylesheet) and [adding images and fonts](#adding-images-fonts-and-files). This mechanism provides a number of benefits: -* Scripts and stylesheets get minified and bundled together to avoid extra network requests. -* Missing files cause compilation errors instead of 404 errors for your users. -* Result filenames include content hashes so you don’t need to worry about browsers caching their old versions. +- Scripts and stylesheets get minified and bundled together to avoid extra network requests. +- Missing files cause compilation errors instead of 404 errors for your users. +- Result filenames include content hashes so you don’t need to worry about browsers caching their old versions. However there is an **escape hatch** that you can use to add an asset outside of the module system. @@ -804,7 +806,7 @@ called `PUBLIC_URL`. Inside `index.html`, you can use it like this: ```html -<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> +<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> ``` Only files inside the `public` folder will be accessible by `%PUBLIC_URL%` prefix. If you need to use a file from `src` @@ -827,10 +829,10 @@ render() { Keep in mind the downsides of this approach: -* None of the files in `public` folder get post-processed or minified. -* Missing files will not be called at compilation time, and will cause 404 errors for your users. -* Result filenames won’t include content hashes so you’ll need to add query arguments or rename them every time they - change. +- None of the files in `public` folder get post-processed or minified. +- Missing files will not be called at compilation time, and will cause 404 errors for your users. +- Result filenames won’t include content hashes so you’ll need to add query arguments or rename them every time they + change. ### When to Use the `public` Folder @@ -838,12 +840,12 @@ Normally we recommend importing [stylesheets](#adding-a-stylesheet), [images, an from JavaScript. The `public` folder is useful as a workaround for a number of less common cases: -* You need a file with a specific name in the build output, such - as [`manifest.webmanifest`](https://developer.mozilla.org/en-US/docs/Web/Manifest). -* You have thousands of images and need to dynamically reference their paths. -* You want to include a small script like [`pace.js`](http://github.hubspot.com/pace/docs/welcome/) outside of the - bundled code. -* Some library may be incompatible with Webpack and you have no other option but to include it as a `<script>` tag. +- You need a file with a specific name in the build output, such + as [`manifest.webmanifest`](https://developer.mozilla.org/en-US/docs/Web/Manifest). +- You have thousands of images and need to dynamically reference their paths. +- You want to include a small script like [`pace.js`](http://github.hubspot.com/pace/docs/welcome/) outside of the + bundled code. +- Some library may be incompatible with Webpack and you have no other option but to include it as a `<script>` tag. Note that if you add a `<script>` that declares global variables, you also need to read the next section on using them. @@ -881,18 +883,18 @@ Alternatively you may use `yarn`: yarn add reactstrap bootstrap@4 ``` -Import Bootstrap CSS and optionally Bootstrap theme CSS in the beginning of your ```src/index.js``` file: +Import Bootstrap CSS and optionally Bootstrap theme CSS in the beginning of your `src/index.js` file: ```js -import 'bootstrap/dist/css/bootstrap.css'; +import "bootstrap/dist/css/bootstrap.css"; // Put any other imports below so that CSS from your // components takes precedence over default styles. ``` -Import required React Bootstrap components within ```src/App.js``` file or your custom component files: +Import required React Bootstrap components within `src/App.js` file or your custom component files: ```js -import { Navbar, Button } from 'reactstrap'; +import { Navbar, Button } from "reactstrap"; ``` Now you are ready to use the imported React Bootstrap components within your component hierarchy defined in the render @@ -905,9 +907,9 @@ redone using React Bootstrap. Sometimes you might need to tweak the visual styles of Bootstrap (or equivalent package).<br> We suggest the following approach: -* Create a new package that depends on the package you wish to customize, e.g. Bootstrap. -* Add the necessary build steps to tweak the theme, and publish your package on npm. -* Install your own theme npm package as a dependency of your app. +- Create a new package that depends on the package you wish to customize, e.g. Bootstrap. +- Add the necessary build steps to tweak the theme, and publish your package on npm. +- Install your own theme npm package as a dependency of your app. Here is an example of adding a [customized Bootstrap](https://medium.com/@tacomanator/customizing-create-react-app-aa9ffb88165) that follows these @@ -991,10 +993,10 @@ text will show the environment provided when using `npm start`: ```html <div> - <small>You are running this application in <b>development</b> mode.</small> - <form> - <input type="hidden" value="abcdef" /> - </form> + <small>You are running this application in <b>development</b> mode.</small> + <form> + <input type="hidden" value="abcdef" /> + </form> </div> ``` @@ -1005,8 +1007,8 @@ a `.env` file. Both of these ways are described in the next few sections. Having access to the `NODE_ENV` is also useful for performing actions conditionally: ```js -if (process.env.NODE_ENV !== 'production') { - analytics.disable(); +if (process.env.NODE_ENV !== "production") { + analytics.disable(); } ``` @@ -1025,10 +1027,10 @@ You can also access the environment variables starting with `REACT_APP_` in the Note that the caveats from the above section apply: -* Apart from a few built-in variables (`NODE_ENV` and `PUBLIC_URL`), variable names must start with `REACT_APP_` to - work. -* The environment variables are injected at build time. If you need to inject them at - runtime, [follow this approach instead](#generating-dynamic-meta-tags-on-the-server). +- Apart from a few built-in variables (`NODE_ENV` and `PUBLIC_URL`), variable names must start with `REACT_APP_` to + work. +- The environment variables are injected at build time. If you need to inject them at + runtime, [follow this approach instead](#generating-dynamic-meta-tags-on-the-server). ### Adding Temporary Environment Variables In Your Shell @@ -1065,17 +1067,17 @@ REACT_APP_SECRET_CODE=abcdef > Note: this feature is **available with `react-scripts@1.0.0` and higher**. -* `.env`: Default. -* `.env.local`: Local overrides. **This file is loaded for all environments except test.** -* `.env.development`, `.env.test`, `.env.production`: Environment-specific settings. -* `.env.development.local`, `.env.test.local`, `.env.production.local`: Local overrides of environment-specific - settings. +- `.env`: Default. +- `.env.local`: Local overrides. **This file is loaded for all environments except test.** +- `.env.development`, `.env.test`, `.env.production`: Environment-specific settings. +- `.env.development.local`, `.env.test.local`, `.env.production.local`: Local overrides of environment-specific + settings. Files on the left have more priority than files on the right: -* `npm start`: `.env.development.local`, `.env.development`, `.env.local`, `.env` -* `npm run build`: `.env.production.local`, `.env.production`, `.env.local`, `.env` -* `npm test`: `.env.test.local`, `.env.test`, `.env` (note `.env.local` is missing) +- `npm start`: `.env.development.local`, `.env.development`, `.env.local`, `.env` +- `npm run build`: `.env.production.local`, `.env.production`, `.env.local`, `.env` +- `npm test`: `.env.test.local`, `.env.test`, `.env` (note `.env.local` is missing) These variables will act as the defaults if the machine does not explicitly set them.<br> Please refer to the [dotenv documentation](https://github.com/motdotla/dotenv) for more details. @@ -1091,15 +1093,15 @@ Many popular libraries use [decorators](https://medium.com/google-developers/exp their documentation.<br> Create React App doesn’t support decorator syntax at the moment because: -* It is an experimental proposal and is subject to change. -* The current specification version is not officially supported by Babel. -* If the specification changes, we won’t be able to write a codemod because we don’t use them internally at Facebook. +- It is an experimental proposal and is subject to change. +- The current specification version is not officially supported by Babel. +- If the specification changes, we won’t be able to write a codemod because we don’t use them internally at Facebook. However in many cases you can rewrite decorator-based code without decorators just as fine.<br> Please refer to these two threads for reference: -* [#214](https://github.com/facebookincubator/create-react-app/issues/214) -* [#411](https://github.com/facebookincubator/create-react-app/issues/411) +- [#214](https://github.com/facebookincubator/create-react-app/issues/214) +- [#411](https://github.com/facebookincubator/create-react-app/issues/411) Create React App will add decorator support when the specification advances to a stable stage. @@ -1161,10 +1163,10 @@ request without a `text/html` accept header will be redirected to the specified The `proxy` option supports HTTP, HTTPS and WebSocket connections.<br> If the `proxy` option is **not** flexible enough for you, alternatively you can: -* [Configure the proxy yourself](#configuring-the-proxy-manually) -* Enable CORS on your server ([here’s how to do it for Express](http://enable-cors.org/server_expressjs.html)). -* Use [environment variables](#adding-custom-environment-variables) to inject the right server host and port into your - app. +- [Configure the proxy yourself](#configuring-the-proxy-manually) +- Enable CORS on your server ([here’s how to do it for Express](http://enable-cors.org/server_expressjs.html)). +- Use [environment variables](#adding-custom-environment-variables) to inject the right server host and port into your + app. ### "Invalid Host Header" Errors After Configuring Proxy @@ -1333,9 +1335,11 @@ reflect the current URL. To solve this, we recommend to add placeholders into th ```html <!doctype html> <html lang="en"> - <head> - <meta property="og:title" content="__OG_TITLE__"> - <meta property="og:description" content="__OG_DESCRIPTION__"> + <head> + <meta property="og:title" content="__OG_TITLE__" /> + <meta property="og:description" content="__OG_DESCRIPTION__" /> + </head> +</html> ``` Then, on the server, regardless of the backend you use, you can read `index.html` into memory and replace `__OG_TITLE__` @@ -1384,8 +1388,7 @@ as it makes your app vulnerable to XSS attacks.** ## Running Tests -> Note: this feature is available with `react-scripts@0.3.0` and higher.<br> -> [Read the migration guide to learn how to enable it in older projects!](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md#migrating-from-023-to-030) +> Note: this feature is available with `react-scripts@0.3.0` and higher.<br> > [Read the migration guide to learn how to enable it in older projects!](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md#migrating-from-023-to-030) Create React App uses [Jest](https://facebook.github.io/jest/) as its test runner. To prepare for this integration, we did a [major revamp](https://facebook.github.io/jest/blog/2016/09/01/jest-15.html) of Jest so if you heard bad things @@ -1405,9 +1408,9 @@ Create React App. Jest will look for test files with any of the following popular naming conventions: -* Files with `.js` suffix in `__tests__` folders. -* Files with `.test.js` suffix. -* Files with `.spec.js` suffix. +- Files with `.js` suffix in `__tests__` folders. +- Files with `.test.js` suffix. +- Files with `.spec.js` suffix. The `.test.js` / `.spec.js` files (or the `__tests__` folders) can be located at any depth under the `src` top level folder. @@ -1448,11 +1451,11 @@ in `describe()` blocks for logical grouping but this is neither required nor rec Jest provides a built-in `expect()` global function for making assertions. A basic test could look like this: ```js -import sum from './sum'; +import sum from "./sum"; -it('sums numbers', () => { - expect(sum(1, 2)).toEqual(3); - expect(sum(2, 2)).toEqual(4); +it("sums numbers", () => { + expect(sum(1, 2)).toEqual(3); + expect(sum(2, 2)).toEqual(4); }); ``` @@ -1473,13 +1476,13 @@ contain. If you haven’t decided on a testing strategy yet, we recommend that y for your components: ```js -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./App"; -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(<App />, div); +it("renders without crashing", () => { + const div = document.createElement("div"); + ReactDOM.render(<App />, div); }); ``` @@ -1512,8 +1515,8 @@ The adapter will also need to be configured in your [global setup file](#initial #### `src/setupTests.js` ```js -import { configure } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; +import { configure } from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; configure({ adapter: new Adapter() }); ``` @@ -1521,12 +1524,12 @@ configure({ adapter: new Adapter() }); Now you can write a smoke test with it: ```js -import React from 'react'; -import { shallow } from 'enzyme'; -import App from './App'; +import React from "react"; +import { shallow } from "enzyme"; +import App from "./App"; -it('renders without crashing', () => { - shallow(<App />); +it("renders without crashing", () => { + shallow(<App />); }); ``` @@ -1543,15 +1546,15 @@ for spies. Here is an example from Enzyme documentation that asserts specific output, rewritten to use Jest matchers: ```js -import React from 'react'; -import { shallow } from 'enzyme'; -import App from './App'; - -it('renders welcome message', () => { - const wrapper = shallow(<App />); - const welcome = <h2>Welcome to React</h2>; - // expect(wrapper.contains(welcome)).to.equal(true); - expect(wrapper.contains(welcome)).toEqual(true); +import React from "react"; +import { shallow } from "enzyme"; +import App from "./App"; + +it("renders welcome message", () => { + const wrapper = shallow(<App />); + const welcome = <h2>Welcome to React</h2>; + // expect(wrapper.contains(welcome)).to.equal(true); + expect(wrapper.contains(welcome)).toEqual(true); }); ``` @@ -1563,7 +1566,7 @@ Additionally, you might find [jest-enzyme](https://github.com/blainekasten/enzym tests with readable matchers. The above `contains` code can be written simpler with jest-enzyme. ```js -expect(wrapper).toContainReact(welcome) +expect(wrapper).toContainReact(welcome); ``` To enable this, install `jest-enzyme`: @@ -1581,7 +1584,7 @@ yarn add jest-enzyme Import it in [`src/setupTests.js`](#initializing-test-environment) to make its matchers available in every test: ```js -import 'jest-enzyme'; +import "jest-enzyme"; ``` ### Using Third Party Assertion Libraries @@ -1595,8 +1598,8 @@ However, if you are used to other libraries, such as [Chai](http://chaijs.com/) you have existing code using them that you’d like to port over, you can import them normally like this: ```js -import sinon from 'sinon'; -import { expect } from 'chai'; +import sinon from "sinon"; +import { expect } from "chai"; ``` and then use them in your tests like you normally do. @@ -1614,11 +1617,11 @@ For example: ```js const localStorageMock = { - getItem: jest.fn(), - setItem: jest.fn(), - clear: jest.fn() + getItem: jest.fn(), + setItem: jest.fn(), + clear: jest.fn(), }; -global.localStorage = localStorageMock +global.localStorage = localStorageMock; ``` ### Focusing and Excluding Tests @@ -1642,33 +1645,33 @@ in your package.json. Supported overrides: -- [`collectCoverageFrom`](https://facebook.github.io/jest/docs/en/configuration.html#collectcoveragefrom-array) -- [`coverageReporters`](https://facebook.github.io/jest/docs/en/configuration.html#coveragereporters-array-string) -- [`coverageThreshold`](https://facebook.github.io/jest/docs/en/configuration.html#coveragethreshold-object) -- [`snapshotSerializers`](https://facebook.github.io/jest/docs/en/configuration.html#snapshotserializers-array-string) +- [`collectCoverageFrom`](https://facebook.github.io/jest/docs/en/configuration.html#collectcoveragefrom-array) +- [`coverageReporters`](https://facebook.github.io/jest/docs/en/configuration.html#coveragereporters-array-string) +- [`coverageThreshold`](https://facebook.github.io/jest/docs/en/configuration.html#coveragethreshold-object) +- [`snapshotSerializers`](https://facebook.github.io/jest/docs/en/configuration.html#snapshotserializers-array-string) Example package.json: ```json { - "name": "your-package", - "jest": { - "collectCoverageFrom" : [ - "src/**/*.{js,jsx}", - "!<rootDir>/node_modules/", - "!<rootDir>/path/to/dir/" - ], - "coverageThreshold": { - "global": { - "branches": 90, - "functions": 90, - "lines": 90, - "statements": 90 - } - }, - "coverageReporters": ["text"], - "snapshotSerializers": ["my-serializer-module"] - } + "name": "your-package", + "jest": { + "collectCoverageFrom": [ + "src/**/*.{js,jsx}", + "!<rootDir>/node_modules/", + "!<rootDir>/path/to/dir/" + ], + "coverageThreshold": { + "global": { + "branches": 90, + "functions": 90, + "lines": 90, + "statements": 90 + } + }, + "coverageReporters": ["text"], + "snapshotSerializers": ["my-serializer-module"] + } } ``` @@ -1768,17 +1771,17 @@ remove `--env=jsdom`, and your tests will run faster: To help you make up your mind, here is a list of APIs that **need jsdom**: -* Any browser globals like `window` and `document` -* [`ReactDOM.render()`](https://facebook.github.io/react/docs/top-level-api.html#reactdom.render) -* [`TestUtils.renderIntoDocument()`](https://facebook.github.io/react/docs/test-utils.html#renderintodocument) ([a shortcut](https://github.com/facebook/react/blob/34761cf9a252964abfaab6faf74d473ad95d1f21/src/test/ReactTestUtils.js#L83-L91) - for the above) -* [`mount()`](http://airbnb.io/enzyme/docs/api/mount.html) in [Enzyme](http://airbnb.io/enzyme/index.html) +- Any browser globals like `window` and `document` +- [`ReactDOM.render()`](https://facebook.github.io/react/docs/top-level-api.html#reactdom.render) +- [`TestUtils.renderIntoDocument()`](https://facebook.github.io/react/docs/test-utils.html#renderintodocument) ([a shortcut](https://github.com/facebook/react/blob/34761cf9a252964abfaab6faf74d473ad95d1f21/src/test/ReactTestUtils.js#L83-L91) + for the above) +- [`mount()`](http://airbnb.io/enzyme/docs/api/mount.html) in [Enzyme](http://airbnb.io/enzyme/index.html) In contrast, **jsdom is not needed** for the following APIs: -* [`TestUtils.createRenderer()`](https://facebook.github.io/react/docs/test-utils.html#shallow-rendering) (shallow - rendering) -* [`shallow()`](http://airbnb.io/enzyme/docs/api/shallow.html) in [Enzyme](http://airbnb.io/enzyme/index.html) +- [`TestUtils.createRenderer()`](https://facebook.github.io/react/docs/test-utils.html#shallow-rendering) (shallow + rendering) +- [`shallow()`](http://airbnb.io/enzyme/docs/api/shallow.html) in [Enzyme](http://airbnb.io/enzyme/index.html) Finally, jsdom is also not needed for [snapshot testing](http://facebook.github.io/jest/blog/2016/07/27/jest-14.html). @@ -1802,9 +1805,9 @@ inline, starting and stopping the watcher automatically, and offering one-click Usually, in an app, you have a lot of UI components, and each of them has many different states. For an example, a simple button component could have following states: -* In a regular state, with a text label. -* In the disabled mode. -* In a loading state. +- In a regular state, with a text label. +- In the disabled mode. +- In a loading state. Usually, it’s hard to see these states without running a sample app or some examples. @@ -1840,12 +1843,11 @@ After that, follow the instructions on the screen. Learn more about React Storybook: -* -Screencast: [Getting Started with React Storybook](https://egghead.io/lessons/react-getting-started-with-react-storybook) -* [GitHub Repo](https://github.com/storybooks/storybook) -* [Documentation](https://storybook.js.org/basics/introduction/) -* [Snapshot Testing UI](https://github.com/storybooks/storybook/tree/master/addons/storyshots) with Storybook + - addon/storyshot +- Screencast: [Getting Started with React Storybook](https://egghead.io/lessons/react-getting-started-with-react-storybook) +- [GitHub Repo](https://github.com/storybooks/storybook) +- [Documentation](https://storybook.js.org/basics/introduction/) +- [Snapshot Testing UI](https://github.com/storybooks/storybook/tree/master/addons/storyshots) with Storybook + + addon/storyshot ### Getting Started with Styleguidist @@ -1884,8 +1886,8 @@ After that, follow the instructions on the screen. Learn more about React Styleguidist: -* [GitHub Repo](https://github.com/styleguidist/react-styleguidist) -* [Documentation](https://react-styleguidist.js.org/docs/getting-started.html) +- [GitHub Repo](https://github.com/styleguidist/react-styleguidist) +- [Documentation](https://react-styleguidist.js.org/docs/getting-started.html) ## Making a Progressive Web App @@ -1894,12 +1896,12 @@ By default, the production build is a fully functional, offline-first Progressive Web Apps are faster and more reliable than traditional web pages, and provide an engaging mobile experience: -* All static site assets are cached so that your page loads fast on subsequent visits, regardless of network - connectivity (such as 2G or 3G). Updates are downloaded in the background. -* Your app will work regardless of network state, even if offline. This means your users will be able to use your app at - 10,000 feet and on the subway. -* On mobile devices, your app can be added directly to the user's home screen, app icon and all. You can also re-engage - users using web **push notifications**. This eliminates the need for the app store. +- All static site assets are cached so that your page loads fast on subsequent visits, regardless of network + connectivity (such as 2G or 3G). Updates are downloaded in the background. +- Your app will work regardless of network state, even if offline. This means your users will be able to use your app at + 10,000 feet and on the subway. +- On mobile devices, your app can be added directly to the user's home screen, app icon and all. You can also re-engage + users using web **push notifications**. This eliminates the need for the app store. The [`sw-precache-webpack-plugin`](https://github.com/goldhand/sw-precache-webpack-plugin) is integrated into production configuration, @@ -1922,7 +1924,7 @@ you can swap out the call to `registerServiceWorker()` in [`src/index.js`](src/index.js) first by modifying the service worker import: ```javascript -import { unregister } from './registerServiceWorker'; +import { unregister } from "./registerServiceWorker"; ``` and then call `unregister()` instead. @@ -1951,12 +1953,12 @@ it may take up to 24 hours for the cache to be invalidated. frustration when previously cached assets are used and do not include the latest changes you've made locally. -1. If you *need* to test your offline-first service worker locally, build +1. If you _need_ to test your offline-first service worker locally, build the application (using `npm run build`) and run a simple http server from your build directory. After running the build script, `create-react-app` will give instructions for one way to test your production build locally and the [deployment instructions](#deployment) have - instructions for using other methods. *Be sure to always use an - incognito window to avoid complications with your browser cache.* + instructions for using other methods. _Be sure to always use an + incognito window to avoid complications with your browser cache._ 1. If possible, configure your production environment to serve the generated `service-worker.js` [with HTTP caching disabled](http://stackoverflow.com/questions/38843970/service-worker-javascript-update-frequency-every-24-hours) @@ -2075,14 +2077,14 @@ fine integrated into an existing dynamic one. Here’s a programmatic example using [Node](https://nodejs.org/) and [Express](http://expressjs.com/): ```javascript -const express = require('express'); -const path = require('path'); +const express = require("express"); +const path = require("path"); const app = express(); -app.use(express.static(path.join(__dirname, 'build'))); +app.use(express.static(path.join(__dirname, "build"))); -app.get('/', function (req, res) { - res.sendFile(path.join(__dirname, 'build', 'index.html')); +app.get("/", function (req, res) { + res.sendFile(path.join(__dirname, "build", "index.html")); }); app.listen(9000); @@ -2331,15 +2333,15 @@ like `http://user.github.io/todomvc/todos/42`, where `/todos/42` is a frontend r 404 because it knows nothing of `/todos/42`. If you want to add a router to a project hosted on GitHub Pages, here are a couple of solutions: -* You could switch from using HTML5 history API to routing with hashes. If you use React Router, you can switch - to `hashHistory` for this effect, but the URL will be longer and more verbose (for - example, `http://user.github.io/todomvc/#/todos/42?_k=yknaj`) - . [Read more](https://reacttraining.com/react-router/web/api/Router) about different history implementations in React - Router. -* Alternatively, you can use a trick to teach GitHub Pages to handle 404 by redirecting to your `index.html` page with a - special redirect parameter. You would need to add a `404.html` file with the redirection code to the `build` folder - before deploying your project, and you’ll need to add code handling the redirect parameter to `index.html`. You can - find a detailed explanation of this technique [in this guide](https://github.com/rafrex/spa-github-pages). +- You could switch from using HTML5 history API to routing with hashes. If you use React Router, you can switch + to `hashHistory` for this effect, but the URL will be longer and more verbose (for + example, `http://user.github.io/todomvc/#/todos/42?_k=yknaj`) + . [Read more](https://reacttraining.com/react-router/web/api/Router) about different history implementations in React + Router. +- Alternatively, you can use a trick to teach GitHub Pages to handle 404 by redirecting to your `index.html` page with a + special redirect parameter. You would need to add a `404.html` file with the redirection code to the `build` folder + before deploying your project, and you’ll need to add code handling the redirect parameter to `index.html`. You can + find a detailed explanation of this technique [in this guide](https://github.com/rafrex/spa-github-pages). ### [Heroku](https://www.heroku.com/) @@ -2431,7 +2433,7 @@ Now offers a zero-configuration single-command deployment. You can use `now` to > Ready! https://your-project-name-tpspyhtdtk.now.sh (copied to clipboard) ``` - Paste that URL into your browser when the build is complete, and you will see your deployed app. + Paste that URL into your browser when the build is complete, and you will see your deployed app. Details are available in [this article.](https://zeit.co/blog/unlimited-static) @@ -2460,17 +2462,17 @@ This [ensures that every URL falls back to that file](https://surge.sh/help/addi You can adjust various development and production settings by setting environment variables in your shell or with [.env](#adding-development-environment-variables-in-env). -Variable | Development | Production | Usage -:--- | :---: | :---: | :--- -BROWSER | :white_check_mark: | :x: | By default, Create React App will open the default system browser, favoring Chrome on macOS. Specify a [browser](https://github.com/sindresorhus/opn#app) to override this behavior, or set it to `none` to disable it completely. If you need to customize the way the browser is launched, you can specify a node script instead. Any arguments passed to `npm start` will also be passed to this script, and the url where your app is served will be the last argument. Your script's file name must have the `.js` extension. -HOST | :white_check_mark: | :x: | By default, the development web server binds to `localhost`. You may use this variable to specify a different host. -PORT | :white_check_mark: | :x: | By default, the development web server will attempt to listen on port 3000 or prompt you to attempt the next available port. You may use this variable to specify a different port. -HTTPS | :white_check_mark: | :x: | When set to `true`, Create React App will run the development server in `https` mode. -PUBLIC_URL | :x: | :white_check_mark: | Create React App assumes your application is hosted at the serving web server's root or a subpath as specified in [`package.json` (`homepage`)](#building-for-relative-paths). Normally, Create React App ignores the hostname. You may use this variable to force assets to be referenced verbatim to the url you provide (hostname included). This may be particularly useful when using a CDN to host your application. -CI | :large_orange_diamond: | :white_check_mark: | When set to `true`, Create React App treats warnings as failures in the build. It also makes the test runner non-watching. Most CIs set this flag by default. -REACT_EDITOR | :white_check_mark: | :x: | When an app crashes in development, you will see an error overlay with clickable stack trace. When you click on it, Create React App will try to determine the editor you are using based on currently running processes, and open the relevant source file. You can [send a pull request to detect your editor of choice](https://github.com/facebookincubator/create-react-app/issues/2636). Setting this environment variable overrides the automatic detection. If you do it, make sure your systems [PATH](https://en.wikipedia.org/wiki/PATH_(variable)) environment variable points to your editor’s bin folder. -CHOKIDAR_USEPOLLING | :white_check_mark: | :x: | When set to `true`, the watcher runs in polling mode, as necessary inside a VM. Use this option if `npm start` isn't detecting changes. -GENERATE_SOURCEMAP | :x: | :white_check_mark: | When set to `false`, source maps are not generated for a production build. This solves OOM issues on some smaller machines. +| Variable | Development | Production | Usage | +| :------------------ | :--------------------: | :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| BROWSER | :white_check_mark: | :x: | By default, Create React App will open the default system browser, favoring Chrome on macOS. Specify a [browser](https://github.com/sindresorhus/opn#app) to override this behavior, or set it to `none` to disable it completely. If you need to customize the way the browser is launched, you can specify a node script instead. Any arguments passed to `npm start` will also be passed to this script, and the url where your app is served will be the last argument. Your script's file name must have the `.js` extension. | +| HOST | :white_check_mark: | :x: | By default, the development web server binds to `localhost`. You may use this variable to specify a different host. | +| PORT | :white_check_mark: | :x: | By default, the development web server will attempt to listen on port 3000 or prompt you to attempt the next available port. You may use this variable to specify a different port. | +| HTTPS | :white_check_mark: | :x: | When set to `true`, Create React App will run the development server in `https` mode. | +| PUBLIC_URL | :x: | :white_check_mark: | Create React App assumes your application is hosted at the serving web server's root or a subpath as specified in [`package.json` (`homepage`)](#building-for-relative-paths). Normally, Create React App ignores the hostname. You may use this variable to force assets to be referenced verbatim to the url you provide (hostname included). This may be particularly useful when using a CDN to host your application. | +| CI | :large_orange_diamond: | :white_check_mark: | When set to `true`, Create React App treats warnings as failures in the build. It also makes the test runner non-watching. Most CIs set this flag by default. | +| REACT_EDITOR | :white_check_mark: | :x: | When an app crashes in development, you will see an error overlay with clickable stack trace. When you click on it, Create React App will try to determine the editor you are using based on currently running processes, and open the relevant source file. You can [send a pull request to detect your editor of choice](https://github.com/facebookincubator/create-react-app/issues/2636). Setting this environment variable overrides the automatic detection. If you do it, make sure your systems [PATH](<https://en.wikipedia.org/wiki/PATH_(variable)>) environment variable points to your editor’s bin folder. | +| CHOKIDAR_USEPOLLING | :white_check_mark: | :x: | When set to `true`, the watcher runs in polling mode, as necessary inside a VM. Use this option if `npm start` isn't detecting changes. | +| GENERATE_SOURCEMAP | :x: | :white_check_mark: | When set to `false`, source maps are not generated for a production build. This solves OOM issues on some smaller machines. | ## Troubleshooting @@ -2479,21 +2481,21 @@ GENERATE_SOURCEMAP | :x: | :white_check_mark: | When set to `false`, source maps When you save a file while `npm start` is running, the browser should refresh with the updated code.<br> If this doesn’t happen, try one of the following workarounds: -* If your project is in a Dropbox folder, try moving it out. -* If the watcher doesn’t see a file called `index.js` and you’re referencing it by the folder name, - you [need to restart the watcher](https://github.com/facebookincubator/create-react-app/issues/1164) due to a Webpack - bug. -* Some editors like Vim and IntelliJ have a “safe write” feature that currently breaks the watcher. You will need to - disable it. Follow the instructions - in [“Adjusting Your Text Editor”](https://webpack.js.org/guides/development/#adjusting-your-text-editor). -* If your project path contains parentheses, try moving the project to a path without them. This is caused by - a [Webpack watcher bug](https://github.com/webpack/watchpack/issues/42). -* On Linux and macOS, you might need - to [tweak system settings](https://webpack.github.io/docs/troubleshooting.html#not-enough-watchers) to allow more - watchers. -* If the project runs inside a virtual machine such as (a Vagrant provisioned) VirtualBox, create an `.env` file in your - project directory if it doesn’t exist, and add `CHOKIDAR_USEPOLLING=true` to it. This ensures that the next time you - run `npm start`, the watcher uses the polling mode, as necessary inside a VM. +- If your project is in a Dropbox folder, try moving it out. +- If the watcher doesn’t see a file called `index.js` and you’re referencing it by the folder name, + you [need to restart the watcher](https://github.com/facebookincubator/create-react-app/issues/1164) due to a Webpack + bug. +- Some editors like Vim and IntelliJ have a “safe write” feature that currently breaks the watcher. You will need to + disable it. Follow the instructions + in [“Adjusting Your Text Editor”](https://webpack.js.org/guides/development/#adjusting-your-text-editor). +- If your project path contains parentheses, try moving the project to a path without them. This is caused by + a [Webpack watcher bug](https://github.com/webpack/watchpack/issues/42). +- On Linux and macOS, you might need + to [tweak system settings](https://webpack.github.io/docs/troubleshooting.html#not-enough-watchers) to allow more + watchers. +- If the project runs inside a virtual machine such as (a Vagrant provisioned) VirtualBox, create an `.env` file in your + project directory if it doesn’t exist, and add `CHOKIDAR_USEPOLLING=true` to it. This ensures that the next time you + run `npm start`, the watcher uses the polling mode, as necessary inside a VM. If none of these solutions help please leave a comment [in this thread](https://github.com/facebookincubator/create-react-app/issues/659). @@ -2507,9 +2509,9 @@ in [facebookincubator/create-react-app#713](https://github.com/facebookincubator We recommend deleting `node_modules` in your project and running `npm install` (or `yarn` if you use it) first. If it doesn't help, you can try one of the numerous workarounds mentioned in these issues: -* [facebook/jest#1767](https://github.com/facebook/jest/issues/1767) -* [facebook/watchman#358](https://github.com/facebook/watchman/issues/358) -* [ember-cli/ember-cli#6259](https://github.com/ember-cli/ember-cli/issues/6259) +- [facebook/jest#1767](https://github.com/facebook/jest/issues/1767) +- [facebook/watchman#358](https://github.com/facebook/watchman/issues/358) +- [ember-cli/ember-cli#6259](https://github.com/ember-cli/ember-cli/issues/6259) It is reported that installing Watchman 4.7.0 or newer fixes the issue. If you use [Homebrew](http://brew.sh/), you can run these commands to update it: @@ -2525,7 +2527,7 @@ Watchman documentation page. If this still doesn’t help, try running `launchctl unload -F ~/Library/LaunchAgents/com.github.facebook.watchman.plist`. -There are also reports that *uninstalling* Watchman fixes the issue. So if nothing else helps, remove it from your +There are also reports that _uninstalling_ Watchman fixes the issue. So if nothing else helps, remove it from your system and try again. ### `npm run build` exits too early @@ -2556,21 +2558,21 @@ To add a specific Moment.js locale to your bundle, you need to import it explici For example: ```js -import moment from 'moment'; -import 'moment/locale/fr'; +import moment from "moment"; +import "moment/locale/fr"; ``` If import multiple locales this way, you can later switch between them by calling `moment.locale()` with the locale name: ```js -import moment from 'moment'; -import 'moment/locale/fr'; -import 'moment/locale/es'; +import moment from "moment"; +import "moment/locale/fr"; +import "moment/locale/es"; // ... -moment.locale('fr'); +moment.locale("fr"); ``` This will only work for locales that have been explicitly imported before. @@ -2586,10 +2588,10 @@ To resolve this: 1. Open an issue on the dependency's issue tracker and ask that the package be published pre-compiled. -* Note: Create React App can consume both CommonJS and ES modules. For Node.js compatibility, it is recommended that the - main entry point is CommonJS. However, they can optionally provide an ES module entry point with the `module` field - in `package.json`. Note that **even if a library provides an ES Modules version, it should still precompile other ES6 - features to ES5 if it intends to support older browsers**. +- Note: Create React App can consume both CommonJS and ES modules. For Node.js compatibility, it is recommended that the + main entry point is CommonJS. However, they can optionally provide an ES module entry point with the `module` field + in `package.json`. Note that **even if a library provides an ES Modules version, it should still precompile other ES6 + features to ES5 if it intends to support older browsers**. 2. Fork the package and publish a corrected version yourself. diff --git a/Parlance.ClientApp/genlangindex.js b/Parlance.ClientApp/genlangindex.js index bad986d7..b6232fb3 100644 --- a/Parlance.ClientApp/genlangindex.js +++ b/Parlance.ClientApp/genlangindex.js @@ -4,4 +4,7 @@ import fs from "fs/promises"; let availableTranslations = await fs.readdir("public/resources/translations"); availableTranslations = availableTranslations.filter(x => x !== "index.json"); -await fs.writeFile("public/resources/translations/index.json", JSON.stringify(availableTranslations)); +await fs.writeFile( + "public/resources/translations/index.json", + JSON.stringify(availableTranslations), +); diff --git a/Parlance.ClientApp/index.html b/Parlance.ClientApp/index.html index 08cf30aa..43e1ef70 100644 --- a/Parlance.ClientApp/index.html +++ b/Parlance.ClientApp/index.html @@ -1,22 +1,23 @@ -<!DOCTYPE html> +<!doctype html> <html lang="en"> -<head> - <meta charset="utf-8"> - <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport"> - <base href="/"/> - <!-- + <head> + <meta charset="utf-8" /> + <meta + content="width=device-width, initial-scale=1, shrink-to-fit=no" + name="viewport" + /> + <base href="/" /> + <!-- manifest.json provides metadata used when your web app is added to the homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ --> - <link href="/favicon.png" rel="shortcut icon"> - <title>Parlance - - - -
-
- - + + Parlance + + + +
+
+ + diff --git a/Parlance.ClientApp/package-lock.json b/Parlance.ClientApp/package-lock.json index cea864a2..7d3f54e6 100644 --- a/Parlance.ClientApp/package-lock.json +++ b/Parlance.ClientApp/package-lock.json @@ -62,9 +62,11 @@ "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", "eslint": "^8.55.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "prettier": "3.3.2", "typescript": "^5.1.6", "vite": "^5.0.8" } @@ -7430,6 +7432,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-config-react-app": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", @@ -17304,6 +17319,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -26454,6 +26485,13 @@ } } }, + "eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "requires": {} + }, "eslint-config-react-app": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", @@ -33052,6 +33090,12 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" }, + "prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true + }, "pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", diff --git a/Parlance.ClientApp/package.json b/Parlance.ClientApp/package.json index f0a8b706..637ecdcd 100644 --- a/Parlance.ClientApp/package.json +++ b/Parlance.ClientApp/package.json @@ -57,12 +57,13 @@ "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", - "vite": "^5.0.8", "eslint": "^8.55.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", - "typescript": "^5.1.6" + "prettier": "3.3.2", + "typescript": "^5.1.6", + "vite": "^5.0.8" }, "overrides": { "autoprefixer": "10.4.5", @@ -75,7 +76,7 @@ "scripts": { "dev": "node genlangindex.js && vite", "build": "node genlangindex.js && vite build", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "lint": "prettier . --check", "preview": "vite preview" }, "eslintConfig": { diff --git a/Parlance.ClientApp/public/mail/mail.css b/Parlance.ClientApp/public/mail/mail.css index 4b614bb9..a4dfb5f3 100644 --- a/Parlance.ClientApp/public/mail/mail.css +++ b/Parlance.ClientApp/public/mail/mail.css @@ -1,6 +1,7 @@ @font-face { - font-family: 'Contemporary'; + font-family: "Contemporary"; font-style: normal; font-weight: 400; - src: local('Contemporary'), local('Contemporary-Regular'), url('https://vicr123.com/typeface/Contemporary-Regular.ttf'); + src: local("Contemporary"), local("Contemporary-Regular"), + url("https://vicr123.com/typeface/Contemporary-Regular.ttf"); } diff --git a/Parlance.ClientApp/src/App.jsx b/Parlance.ClientApp/src/App.jsx index ed77b701..01c19d51 100644 --- a/Parlance.ClientApp/src/App.jsx +++ b/Parlance.ClientApp/src/App.jsx @@ -1,19 +1,21 @@ -import React, {Component} from 'react'; -import {Route, Routes} from 'react-router-dom'; -import AppRoutes from './AppRoutes'; -import Layout from './components/Layout'; -import './custom.css'; -import Styles from './App.module.css'; -import {useTranslation} from "react-i18next"; +import React, { Component } from "react"; +import { Route, Routes } from "react-router-dom"; +import AppRoutes from "./AppRoutes"; +import Layout from "./components/Layout"; +import "./custom.css"; +import Styles from "./App.module.css"; +import { useTranslation } from "react-i18next"; import i18n from "./helpers/i18n"; -import {ServerInformationProvider} from "./context/ServerInformationContext"; +import { ServerInformationProvider } from "./context/ServerInformationContext"; -function ErrorIndicator({error}) { - const {t} = useTranslation(); +function ErrorIndicator({ error }) { + const { t } = useTranslation(); - return
- {t("Sorry, an error occurred.")} -
+ return ( +
+ {t("Sorry, an error occurred.")} +
+ ); } export default class App extends Component { @@ -24,19 +26,19 @@ export default class App extends Component { this.state = { error: null, - dir: i18n.dir() - } + dir: i18n.dir(), + }; i18n.on("languageChanged", () => { this.setState({ - dir: i18n.dir() + dir: i18n.dir(), }); }); } render() { if (this.state.error) { - return + return ; } return ( @@ -45,8 +47,14 @@ export default class App extends Component { {AppRoutes.map((route, index) => { - const {element, ...rest} = route; - return ; + const { element, ...rest } = route; + return ( + + ); })} @@ -58,7 +66,7 @@ export default class App extends Component { componentDidCatch(error, errorInfo) { console.log(error); this.setState({ - error: error + error: error, }); } } diff --git a/Parlance.ClientApp/src/App.module.css b/Parlance.ClientApp/src/App.module.css index 1754a042..94aba6f0 100644 --- a/Parlance.ClientApp/src/App.module.css +++ b/Parlance.ClientApp/src/App.module.css @@ -6,4 +6,4 @@ width: 100dvw; height: 100dvh; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/App.test.jsx b/Parlance.ClientApp/src/App.test.jsx index 44c5141a..6f65c401 100644 --- a/Parlance.ClientApp/src/App.test.jsx +++ b/Parlance.ClientApp/src/App.test.jsx @@ -1,14 +1,15 @@ -import React from 'react'; -import {createRoot} from 'react-dom/client'; -import {MemoryRouter} from 'react-router-dom'; -import App from './App'; +import React from "react"; +import { createRoot } from "react-dom/client"; +import { MemoryRouter } from "react-router-dom"; +import App from "./App"; -it('renders without crashing', async () => { - const div = document.createElement('div'); +it("renders without crashing", async () => { + const div = document.createElement("div"); const root = createRoot(div); root.render( - - ); + + , + ); await new Promise(resolve => setTimeout(resolve, 1000)); }); diff --git a/Parlance.ClientApp/src/AppRoutes.jsx b/Parlance.ClientApp/src/AppRoutes.jsx index b53c719f..700d9c21 100644 --- a/Parlance.ClientApp/src/AppRoutes.jsx +++ b/Parlance.ClientApp/src/AppRoutes.jsx @@ -1,6 +1,6 @@ -import {Home} from "./pages/Home/index"; +import { Home } from "./pages/Home/index"; import UnknownPage from "./pages/UnknownPage"; -import {lazy} from "react"; +import { lazy } from "react"; import Spinner from "./components/Spinner"; const Administration = lazy(() => import("./pages/Administration")); @@ -8,53 +8,65 @@ const Account = lazy(() => import("./pages/Account")); const Projects = lazy(() => import("./pages/Projects")); const Languages = lazy(() => import("./pages/Languages")); const Glossaries = lazy(() => import("./pages/Glossaries")); -const EmailUnsubscribe = lazy(() => import("./pages/EmailUnsubscribe")) +const EmailUnsubscribe = lazy(() => import("./pages/EmailUnsubscribe")); const AppRoutes = [ { index: true, - element: + element: , }, { path: "/admin/*", - element: - - + element: ( + + + + ), }, { path: "/account/*", - element: - - + element: ( + + + + ), }, { path: "/projects/*", - element: - - + element: ( + + + + ), }, { path: "/languages/*", - element: - - + element: ( + + + + ), }, { path: "/glossaries/*", - element: - - + element: ( + + + + ), }, { path: "/email-unsubscribe", - element: - - + element: ( + + + + ), }, { path: "*", - element: - } + element: , + }, ]; export default AppRoutes; diff --git a/Parlance.ClientApp/src/checks.js b/Parlance.ClientApp/src/checks.js index 24cdee4a..032bf901 100644 --- a/Parlance.ClientApp/src/checks.js +++ b/Parlance.ClientApp/src/checks.js @@ -1,22 +1,28 @@ function checkDuplicate(source, translation) { - if (source === translation) return { - checkSeverity: "warn", - message: "Source is equal to translation" - } + if (source === translation) + return { + checkSeverity: "warn", + message: "Source is equal to translation", + }; } function checkLeadingSpace(source, translation) { - if (translation.trimLeft() !== translation && source.trimLeft() === source) return { - checkSeverity: "warn", - message: "Leading space exists" - }; + if (translation.trimLeft() !== translation && source.trimLeft() === source) + return { + checkSeverity: "warn", + message: "Leading space exists", + }; } function checkTrailingSpace(source, translation) { - if (translation.trimRight() !== translation && source.trimRight() === source) return { - checkSeverity: "warn", - message: "Trailing space exists" - }; + if ( + translation.trimRight() !== translation && + source.trimRight() === source + ) + return { + checkSeverity: "warn", + message: "Trailing space exists", + }; } function checkQtPlaceholders(source, translation) { @@ -25,17 +31,18 @@ function checkQtPlaceholders(source, translation) { if (!translation.includes(`%${num}`)) { return { checkSeverity: "error", - message: `Placeholder %${num} not present in translation` - } + message: `Placeholder %${num} not present in translation`, + }; } }); } function checkQtNumericPlaceholders(source, translation) { - if (source.includes("%n") && !translation.includes("%n")) return { - checkSeverity: "error", - message: "Placeholder %n not present in translation" - }; + if (source.includes("%n") && !translation.includes("%n")) + return { + checkSeverity: "error", + message: "Placeholder %n not present in translation", + }; } function checki18nextPlaceholders(source, translation) { @@ -44,8 +51,8 @@ function checki18nextPlaceholders(source, translation) { if (!translation.includes(`{{${ph}}}`)) { return { checkSeverity: "error", - message: `Placeholder {{${ph}}} not present in translation` - } + message: `Placeholder {{${ph}}} not present in translation`, + }; } }); } @@ -56,8 +63,8 @@ function checki18nextHtmlPlaceholders(source, translation) { if (!translation.includes(`<${ph}>`)) { return { checkSeverity: "error", - message: `Placeholder <${ph}> not present in translation` - } + message: `Placeholder <${ph}> not present in translation`, + }; } }); } @@ -68,8 +75,8 @@ function checkResxPlaceholders(source, translation) { if (!translation.includes(`{${ph}}`)) { return { checkSeverity: "error", - message: `Placeholder {${ph}} not present in translation` - } + message: `Placeholder {${ph}} not present in translation`, + }; } }); } @@ -80,8 +87,8 @@ function checkVueI18nPlaceholders(source, translation) { if (!translation.includes(`{${ph}}`)) { return { checkSeverity: "error", - message: `Placeholder {${ph}} not present in translation` - } + message: `Placeholder {${ph}} not present in translation`, + }; } }); } @@ -89,19 +96,19 @@ function checkVueI18nPlaceholders(source, translation) { function checkJavaPlaceholders(source, translation) { return [..."abcdefghnostx"].flatMap(placeholder => { let ph = `%${placeholder}`; - let sourceMatches = [...source.matchAll(new RegExp(ph, 'g'))]; - let translationMatches = [...translation.matchAll(new RegExp(ph, 'g'))]; - + let sourceMatches = [...source.matchAll(new RegExp(ph, "g"))]; + let translationMatches = [...translation.matchAll(new RegExp(ph, "g"))]; + const difference = translationMatches.length - sourceMatches.length; if (difference < 0) { return Array(-difference).fill({ checkSeverity: "error", - message: `Placeholder ${ph} not present in translation` + message: `Placeholder ${ph} not present in translation`, }); } else if (difference > 0) { return Array(difference).fill({ checkSeverity: "error", - message: `Extraneous placeholder ${ph} in translation` + message: `Extraneous placeholder ${ph} in translation`, }); } }); @@ -113,75 +120,56 @@ function checkGettextPlaceholders(source, translation) { if (!translation.includes(`{${ph}}`)) { return { checkSeverity: "error", - message: `Placeholder {${ph}} not present in translation` - } + message: `Placeholder {${ph}} not present in translation`, + }; } }); } const Checks = { - "common": [ - checkDuplicate, - checkLeadingSpace, - checkTrailingSpace - ], - "qt": [ - checkQtPlaceholders, - checkQtNumericPlaceholders, - "common" - ], - "i18next": [ - checki18nextPlaceholders, - checki18nextHtmlPlaceholders, - "common" - ], - "resx": [ - checkResxPlaceholders, - "common" - ], - "vue-i18n": [ - checkVueI18nPlaceholders, - "common" - ], - "minecraft-fabric": [ - checkJavaPlaceholders, - "common" - ], - "gettext": [ - checkGettextPlaceholders, - "common" - ] -} + common: [checkDuplicate, checkLeadingSpace, checkTrailingSpace], + qt: [checkQtPlaceholders, checkQtNumericPlaceholders, "common"], + i18next: [checki18nextPlaceholders, checki18nextHtmlPlaceholders, "common"], + resx: [checkResxPlaceholders, "common"], + "vue-i18n": [checkVueI18nPlaceholders, "common"], + "minecraft-fabric": [checkJavaPlaceholders, "common"], + gettext: [checkGettextPlaceholders, "common"], +}; function checkTranslation(source, translation, checkSuite) { if (translation === "") return []; let suite = Checks[checkSuite]; if (!suite) return []; - return suite.map(check => { - if (typeof (check) === "string") { - return checkTranslation(source, translation, check); - } else { - try { - return check(source, translation); - } catch (ex) { - console?.log?.(ex); - return { - checkSeverity: "error", - message: `Unable to run the check` + return suite + .map(check => { + if (typeof check === "string") { + return checkTranslation(source, translation, check); + } else { + try { + return check(source, translation); + } catch (ex) { + console?.log?.(ex); + return { + checkSeverity: "error", + message: `Unable to run the check`, + }; } } - } - }).flat().filter(result => result); + }) + .flat() + .filter(result => result); } function mostSevereType(checks) { - let severities = checks.map(check => typeof (check) === "string" ? check : check?.checkSeverity); + let severities = checks.map(check => + typeof check === "string" ? check : check?.checkSeverity, + ); if (severities.includes("error")) return "error"; if (severities.includes("warn")) return "warn"; return null; } -export {Checks}; -export {checkTranslation}; -export {mostSevereType}; \ No newline at end of file +export { Checks }; +export { checkTranslation }; +export { mostSevereType }; diff --git a/Parlance.ClientApp/src/components/BackButton.module.css b/Parlance.ClientApp/src/components/BackButton.module.css index 3ecb753f..db7c65c2 100644 --- a/Parlance.ClientApp/src/components/BackButton.module.css +++ b/Parlance.ClientApp/src/components/BackButton.module.css @@ -25,4 +25,4 @@ .translationViewContainer.backButton { padding-left: 6px; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/BackButton.tsx b/Parlance.ClientApp/src/components/BackButton.tsx index e6d53994..cc8f08e9 100644 --- a/Parlance.ClientApp/src/components/BackButton.tsx +++ b/Parlance.ClientApp/src/components/BackButton.tsx @@ -1,9 +1,9 @@ import Container from "./Container"; -import Styles from "./BackButton.module.css" +import Styles from "./BackButton.module.css"; import Icon from "./Icon"; -import {HorizontalLayout} from "./Layouts"; -import {useTranslation} from "react-i18next"; -import {ReactElement} from "react"; +import { HorizontalLayout } from "./Layouts"; +import { useTranslation } from "react-i18next"; +import { ReactElement } from "react"; interface BackButtonProps { onClick: () => void; @@ -13,27 +13,51 @@ interface BackButtonProps { className?: string; } -export default function BackButton({onClick, inListPage, inTranslationView, text, className}: BackButtonProps): ReactElement { - const {t} = useTranslation(); +export default function BackButton({ + onClick, + inListPage, + inTranslationView, + text, + className, +}: BackButtonProps): ReactElement { + const { t } = useTranslation(); if (!text) text = t("BACK"); - const child = - {text} - ; + const child = ( + + + {text} + + ); if (inListPage) { - return
- {child} -
; + return ( +
+ {child} +
+ ); } else if (inTranslationView) { - return
- {child} -
; + return ( +
+ {child} +
+ ); } else { - return - {child} - + return ( + + {child} + + ); } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/Button.module.css b/Parlance.ClientApp/src/components/Button.module.css index 43773c6e..764ea934 100644 --- a/Parlance.ClientApp/src/components/Button.module.css +++ b/Parlance.ClientApp/src/components/Button.module.css @@ -19,4 +19,4 @@ .button.disabled { color: var(--foreground-disabled-color); -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/Button.tsx b/Parlance.ClientApp/src/components/Button.tsx index b4f8b81d..7172855e 100644 --- a/Parlance.ClientApp/src/components/Button.tsx +++ b/Parlance.ClientApp/src/components/Button.tsx @@ -1,13 +1,21 @@ import Styles from "./Button.module.css"; -import {HTMLAttributes, ReactNode} from "react"; +import { HTMLAttributes, ReactNode } from "react"; interface ButtonProps extends HTMLAttributes { - disabled?: boolean - children: ReactNode + disabled?: boolean; + children: ReactNode; } export default function (props: ButtonProps) { - return
- {props.children} -
-} \ No newline at end of file + return ( +
+ {props.children} +
+ ); +} diff --git a/Parlance.ClientApp/src/components/Container.module.css b/Parlance.ClientApp/src/components/Container.module.css index 179553de..9a62444c 100644 --- a/Parlance.ClientApp/src/components/Container.module.css +++ b/Parlance.ClientApp/src/components/Container.module.css @@ -2,13 +2,12 @@ display: flex; flex-direction: column; align-items: center; - + padding-top: 3px; padding-bottom: 3px; } .containerWrapper { - } .containerInner { @@ -22,4 +21,4 @@ .bottomBorder { border-bottom: 1px solid var(--border-color); -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/Container.tsx b/Parlance.ClientApp/src/components/Container.tsx index 8f377f28..343243e4 100644 --- a/Parlance.ClientApp/src/components/Container.tsx +++ b/Parlance.ClientApp/src/components/Container.tsx @@ -1,5 +1,5 @@ import Styles from "./Container.module.css"; -import {CSSProperties, ReactNode} from "react"; +import { CSSProperties, ReactNode } from "react"; interface ContainerProps { onClick?: () => void; @@ -12,13 +12,19 @@ interface ContainerProps { export default function Container(props: ContainerProps) { let styles = [Styles.container]; // if (props.bottomBorder) styles.push(Styles.bottomBorder); - + let innerStyles = [Styles.containerInner]; if (props.className) innerStyles.push(props.className); - - return
-
- {props.children} + + return ( +
+
+ {props.children} +
-
+ ); } diff --git a/Parlance.ClientApp/src/components/ErrorCover.module.css b/Parlance.ClientApp/src/components/ErrorCover.module.css index 97333209..27342c1e 100644 --- a/Parlance.ClientApp/src/components/ErrorCover.module.css +++ b/Parlance.ClientApp/src/components/ErrorCover.module.css @@ -13,4 +13,4 @@ grid-area: inner; background-color: rgba(0, 0, 0, 0.5); color: white; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/ErrorCover.tsx b/Parlance.ClientApp/src/components/ErrorCover.tsx index 87f9cade..baf2b795 100644 --- a/Parlance.ClientApp/src/components/ErrorCover.tsx +++ b/Parlance.ClientApp/src/components/ErrorCover.tsx @@ -1,20 +1,20 @@ import Styles from "./ErrorCover.module.css"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import SilentInformation from "./SilentInformation"; -import {ReactNode, useEffect, useState} from "react"; +import { ReactNode, useEffect, useState } from "react"; interface CustomError { - title: string - text: string + title: string; + text: string; } interface ErrorCoverProps { - error: any - children: ReactNode + error: any; + children: ReactNode; } -export default function ErrorCover({error, children}: ErrorCoverProps) { - const {t} = useTranslation(); +export default function ErrorCover({ error, children }: ErrorCoverProps) { + const { t } = useTranslation(); const [customError, setCustomError] = useState({}); useEffect(() => { @@ -24,29 +24,33 @@ export default function ErrorCover({error, children}: ErrorCoverProps) { case "ParlanceJsonFileParseError": setCustomError({ title: t("ERROR_PARLANCE_JSON_FILE_PARSE_TITLE"), - text: t("ERROR_PARLANCE_JSON_FILE_PARSE_TEXT") - }) + text: t("ERROR_PARLANCE_JSON_FILE_PARSE_TEXT"), + }); return; case "InvalidBaseFile": setCustomError({ title: t("ERROR_INVALID_BASE_FILE_TITLE"), - text: t("ERROR_INVALID_BASE_FILE_TEXT") - }) + text: t("ERROR_INVALID_BASE_FILE_TEXT"), + }); return; } } })(); - }, [error]) + }, [error]); const title = customError?.title ?? t("ERROR"); const text = customError?.text ?? t("ERROR_PROMPT"); - return
-
- {children} + return ( +
+
{children}
+ {error && ( + + )}
- {error && - - } -
-} \ No newline at end of file + ); +} diff --git a/Parlance.ClientApp/src/components/ErrorText.module.css b/Parlance.ClientApp/src/components/ErrorText.module.css index 6ed365ff..8b814796 100644 --- a/Parlance.ClientApp/src/components/ErrorText.module.css +++ b/Parlance.ClientApp/src/components/ErrorText.module.css @@ -1,3 +1,3 @@ .error { color: var(--foreground-error-color); -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/ErrorText.tsx b/Parlance.ClientApp/src/components/ErrorText.tsx index 7fc1409b..567dda64 100644 --- a/Parlance.ClientApp/src/components/ErrorText.tsx +++ b/Parlance.ClientApp/src/components/ErrorText.tsx @@ -1,9 +1,9 @@ -import Styles from "./ErrorText.module.css" +import Styles from "./ErrorText.module.css"; interface ErrorTextProps { - error?: string + error?: string; } -export default function ErrorText({error}: ErrorTextProps) { - return error && {error} +export default function ErrorText({ error }: ErrorTextProps) { + return error && {error}; } diff --git a/Parlance.ClientApp/src/components/Footer.module.css b/Parlance.ClientApp/src/components/Footer.module.css index d9d9dae2..5437c924 100644 --- a/Parlance.ClientApp/src/components/Footer.module.css +++ b/Parlance.ClientApp/src/components/Footer.module.css @@ -15,7 +15,6 @@ width: 100vw; max-width: var(--content-width); - } .hiddenFooter { @@ -24,4 +23,4 @@ .footerButtonContainer { display: flex; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/Footer.tsx b/Parlance.ClientApp/src/components/Footer.tsx index d1d0f9f6..41102787 100644 --- a/Parlance.ClientApp/src/components/Footer.tsx +++ b/Parlance.ClientApp/src/components/Footer.tsx @@ -1,30 +1,46 @@ import Styles from "./Footer.module.css"; import i18n from "../helpers/i18n"; -import {useMatch} from "react-router-dom"; +import { useMatch } from "react-router-dom"; import Button from "./Button"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import LanguageSelectionModal from "./modals/LanguageSelectionModal"; import Modal from "./Modal"; import Icon from "./Icon"; export default function Footer() { - const {t} = useTranslation(); - const match = useMatch("/projects/:project/:subproject/:language/translate/*"); + const { t } = useTranslation(); + const match = useMatch( + "/projects/:project/:subproject/:language/translate/*", + ); const changeLanguage = () => { - Modal.mount() + Modal.mount(); }; - return
-
-
- -
-
- + return ( +
+
+
+ +
+
+ +
-
-} \ No newline at end of file + ); +} diff --git a/Parlance.ClientApp/src/components/Hero.module.css b/Parlance.ClientApp/src/components/Hero.module.css index f873a7c2..3bc7ce23 100644 --- a/Parlance.ClientApp/src/components/Hero.module.css +++ b/Parlance.ClientApp/src/components/Hero.module.css @@ -12,4 +12,4 @@ .buttonBox { display: flex; flex-direction: row; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/Hero.tsx b/Parlance.ClientApp/src/components/Hero.tsx index 24c80f35..272ed918 100644 --- a/Parlance.ClientApp/src/components/Hero.tsx +++ b/Parlance.ClientApp/src/components/Hero.tsx @@ -1,4 +1,4 @@ -import Styles from "./Hero.module.css" +import Styles from "./Hero.module.css"; import Container from "./Container"; import PageHeading from "./PageHeading"; import React from "react"; @@ -6,23 +6,29 @@ import SmallButton from "./SmallButton"; interface HeroButton { onClick: () => void; - text: string + text: string; } interface HeroProps { - heading: string - subheading?: string - buttons?: HeroButton[] + heading: string; + subheading?: string; + buttons?: HeroButton[]; } -export default function Hero({heading, subheading, buttons}: HeroProps) { - return -
- {heading} - {subheading} -
- {buttons?.map((button, i) => {button.text})} +export default function Hero({ heading, subheading, buttons }: HeroProps) { + return ( + +
+ {heading} + {subheading} +
+ {buttons?.map((button, i) => ( + + {button.text} + + ))} +
-
- -} \ No newline at end of file + + ); +} diff --git a/Parlance.ClientApp/src/components/Icon.module.css b/Parlance.ClientApp/src/components/Icon.module.css index 8275e622..cc00831c 100644 --- a/Parlance.ClientApp/src/components/Icon.module.css +++ b/Parlance.ClientApp/src/components/Icon.module.css @@ -10,4 +10,4 @@ .icon { filter: none; } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/Icon.tsx b/Parlance.ClientApp/src/components/Icon.tsx index b68104b0..31b3320d 100644 --- a/Parlance.ClientApp/src/components/Icon.tsx +++ b/Parlance.ClientApp/src/components/Icon.tsx @@ -1,5 +1,5 @@ import Styles from "./Icon.module.css"; -import {ReactElement} from "react"; +import { ReactElement } from "react"; const commitish = "v1.10"; @@ -9,7 +9,17 @@ interface IconProps { className?: string; } -export default function Icon({icon, flip, className} : IconProps) : ReactElement { +export default function Icon({ + icon, + flip, + className, +}: IconProps): ReactElement { let content = `https://cdn.jsdelivr.net/gh/vicr123/contemporary-icons@${commitish}/actions/16/${icon}.svg`; - return {icon} + return ( + {icon} + ); } diff --git a/Parlance.ClientApp/src/components/KeyboardShortcut.jsx b/Parlance.ClientApp/src/components/KeyboardShortcut.jsx index 3a346d86..31996300 100644 --- a/Parlance.ClientApp/src/components/KeyboardShortcut.jsx +++ b/Parlance.ClientApp/src/components/KeyboardShortcut.jsx @@ -1,12 +1,10 @@ import Styles from "./KeyboardShortcut.module.css"; -function KeyboardShortcutPart({text}) { - return
- {text} -
+function KeyboardShortcutPart({ text }) { + return
{text}
; } -export default function KeyboardShortcut({shortcut}) { +export default function KeyboardShortcut({ shortcut }) { let isMac = navigator.userAgent.toLowerCase().includes("mac"); const resolvedShortcut = shortcut[0].map(key => { if (isMac) { @@ -44,7 +42,11 @@ export default function KeyboardShortcut({shortcut}) { } }); - return
- {resolvedShortcut.map((key, i) => )} -
-} \ No newline at end of file + return ( +
+ {resolvedShortcut.map((key, i) => ( + + ))} +
+ ); +} diff --git a/Parlance.ClientApp/src/components/KeyboardShortcut.module.css b/Parlance.ClientApp/src/components/KeyboardShortcut.module.css index 2bf073c1..418fab68 100644 --- a/Parlance.ClientApp/src/components/KeyboardShortcut.module.css +++ b/Parlance.ClientApp/src/components/KeyboardShortcut.module.css @@ -10,4 +10,4 @@ color: var(--background-color); padding: 2px; border-radius: 2px; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/Layout.module.css b/Parlance.ClientApp/src/components/Layout.module.css index 1a424f2b..ad80beeb 100644 --- a/Parlance.ClientApp/src/components/Layout.module.css +++ b/Parlance.ClientApp/src/components/Layout.module.css @@ -6,10 +6,10 @@ grid-template-columns: 1fr; grid-template-rows: max-content 1fr max-content; gap: 0 0; - grid-template-areas: + grid-template-areas: "header" "content" "footer"; overflow: auto; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/Layout.tsx b/Parlance.ClientApp/src/components/Layout.tsx index 49baabbf..598c35aa 100644 --- a/Parlance.ClientApp/src/components/Layout.tsx +++ b/Parlance.ClientApp/src/components/Layout.tsx @@ -1,15 +1,20 @@ -import React, { ReactNode } from 'react'; -import NavMenu from './NavMenu'; +import React, { ReactNode } from "react"; +import NavMenu from "./NavMenu"; import Styles from "./Layout.module.css"; import Footer from "./Footer"; -export default function Layout({dir, children}: { - dir: "ltr" | "rtl", - children: ReactNode +export default function Layout({ + dir, + children, +}: { + dir: "ltr" | "rtl"; + children: ReactNode; }) { - return
- - {children} -
-
-} \ No newline at end of file + return ( +
+ + {children} +
+
+ ); +} diff --git a/Parlance.ClientApp/src/components/Layouts.module.css b/Parlance.ClientApp/src/components/Layouts.module.css index 7db3f17a..2de5274a 100644 --- a/Parlance.ClientApp/src/components/Layouts.module.css +++ b/Parlance.ClientApp/src/components/Layouts.module.css @@ -6,6 +6,6 @@ .horizontalLayout { display: flex; flex-direction: row; - + align-items: center; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/Layouts.tsx b/Parlance.ClientApp/src/components/Layouts.tsx index 670d19d7..31b12b16 100644 --- a/Parlance.ClientApp/src/components/Layouts.tsx +++ b/Parlance.ClientApp/src/components/Layouts.tsx @@ -1,5 +1,5 @@ -import Styles from "./Layouts.module.css" -import {ReactElement, ReactNode} from "react"; +import Styles from "./Layouts.module.css"; +import { ReactElement, ReactNode } from "react"; interface LayoutProps { children: ReactNode; @@ -12,26 +12,50 @@ interface SpacerProps { height?: number; } -export function VerticalLayout({children, gap = 6, className}: LayoutProps): ReactElement { - return
- {children} -
+export function VerticalLayout({ + children, + gap = 6, + className, +}: LayoutProps): ReactElement { + return ( +
+ {children} +
+ ); } -export function VerticalSpacer({children, height = 20}: SpacerProps): ReactElement { - return
- {children} -
+export function VerticalSpacer({ + children, + height = 20, +}: SpacerProps): ReactElement { + return ( +
+ {children} +
+ ); } -export function HorizontalLayout({children, gap = 6}: LayoutProps): ReactElement { - return
- {children} -
-} \ No newline at end of file +export function HorizontalLayout({ + children, + gap = 6, +}: LayoutProps): ReactElement { + return ( +
+ {children} +
+ ); +} diff --git a/Parlance.ClientApp/src/components/LineEdit.module.css b/Parlance.ClientApp/src/components/LineEdit.module.css index aac53cae..0010ddd0 100644 --- a/Parlance.ClientApp/src/components/LineEdit.module.css +++ b/Parlance.ClientApp/src/components/LineEdit.module.css @@ -1,10 +1,10 @@ .container { display: flex; flex-direction: column; - + background: var(--layer-color); border-radius: var(--border-radius); - + cursor: text; border: 3px solid transparent; transition: border 0.1s ease-out; @@ -17,15 +17,16 @@ .label { font-size: 8pt; position: absolute; - + transition: all 0.2s; } -.input[type=text], .input[type=password] { +.input[type="text"], +.input[type="password"] { border: none; margin-top: 10pt; outline: none; - + transition: all 0.2s; } @@ -36,7 +37,8 @@ cursor: text; } -.input:not(:placeholder-shown) + .label, .input:focus + .label { +.input:not(:placeholder-shown) + .label, +.input:focus + .label { transform: translateY(0) translateX(2%); font-size: 8pt; color: var(--foreground-color); @@ -52,4 +54,4 @@ 100% { border: 3px solid var(--focus-decoration-start); } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/LineEdit.tsx b/Parlance.ClientApp/src/components/LineEdit.tsx index b5e00c27..e601534c 100644 --- a/Parlance.ClientApp/src/components/LineEdit.tsx +++ b/Parlance.ClientApp/src/components/LineEdit.tsx @@ -1,5 +1,5 @@ import Styles from "./LineEdit.module.css"; -import {HTMLProps, useId, useRef} from "react"; +import { HTMLProps, useId, useRef } from "react"; interface LineEditProps extends HTMLProps { password?: boolean; @@ -9,17 +9,30 @@ export default function LineEdit(props: LineEditProps) { const id = useId(); const input = useRef(null); - let inputProps = {...props}; + let inputProps = { ...props }; inputProps.placeholder = ""; inputProps.style = {}; inputProps.password = false; - return
-
input.current?.focus()}> - - -
+ return ( +
+
input.current?.focus()} + > + + +
+
-
-} \ No newline at end of file + ); +} diff --git a/Parlance.ClientApp/src/components/ListPage.module.css b/Parlance.ClientApp/src/components/ListPage.module.css index 4ed38a1d..4a54d76d 100644 --- a/Parlance.ClientApp/src/components/ListPage.module.css +++ b/Parlance.ClientApp/src/components/ListPage.module.css @@ -1,7 +1,7 @@ .parent { display: flex; flex-direction: row; - + flex-grow: 1; justify-content: center; } @@ -11,9 +11,9 @@ grid-template-columns: 1fr 2fr; place-items: stretch; gap: 3px; - + flex-grow: 1; - + max-width: var(--content-width); } @@ -25,11 +25,11 @@ .leftPaneInner { background-color: var(--layer-color); border-radius: var(--border-radius); - + display: flex; flex-direction: column; align-items: flex-end; - + padding: 6px; gap: 3px; } @@ -43,7 +43,7 @@ display: flex; flex-direction: column; justify-content: stretch; - + gap: 3px; } @@ -61,7 +61,8 @@ background-color: var(--hover-color); } -.listItemClickable:active, .selected { +.listItemClickable:active, +.selected { background-color: var(--active-color); } @@ -73,20 +74,20 @@ .widthConstrainer { grid-template-columns: 1fr; } - + .mobileOnly { display: block; } - + .desktopOnly { display: none; } - + .leftPaneInner { align-items: stretch; } - + .listItem { width: initial; } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/ListPage.tsx b/Parlance.ClientApp/src/components/ListPage.tsx index 71d2f6c5..197eebac 100644 --- a/Parlance.ClientApp/src/components/ListPage.tsx +++ b/Parlance.ClientApp/src/components/ListPage.tsx @@ -1,23 +1,25 @@ import Styles from "./ListPage.module.css"; -import {Outlet, Route, Routes, useLocation, useNavigate} from "react-router-dom"; -import {ReactNode} from "react"; +import { + Outlet, + Route, + Routes, + useLocation, + useNavigate, +} from "react-router-dom"; +import { ReactNode } from "react"; import BackButton from "./BackButton"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; interface ListPageItemObject { - name: string, - slug: string, - render: ReactNode - default?: boolean + name: string; + slug: string; + render: ReactNode; + default?: boolean; } type ListPageItem = ListPageItemObject | string; -function ListItem(props: { - name: string - slug: string - default?: boolean -}) { +function ListItem(props: { name: string; slug: string; default?: boolean }) { const navigate = useNavigate(); const location = useLocation(); @@ -25,67 +27,112 @@ function ListItem(props: { navigate(props.slug); }; - let styles = [Styles.listItem, Styles.listItemClickable] + let styles = [Styles.listItem, Styles.listItemClickable]; if (location.pathname.includes(props.slug)) { styles.push(Styles.selected); } - return
- {props.name} -
+ return ( +
+ {props.name} +
+ ); } -function ListPageInner({items, isLeftPane}: { - items: ListPageItem[], - isLeftPane: boolean +function ListPageInner({ + items, + isLeftPane, +}: { + items: ListPageItem[]; + isLeftPane: boolean; }) { const navigate = useNavigate(); - const {t} = useTranslation(); - + const { t } = useTranslation(); + const goBack = () => { - navigate("..") + navigate(".."); }; - - return
-
-
-
- {items.map((item, i) => { - if (typeof (item) === "string") { - return {item.toUpperCase()} - } else { - return - } - })} + + return ( +
+
+
+
+ {items.map((item, i) => { + if (typeof item === "string") { + return ( + + {item.toUpperCase()} + + ); + } else { + return ; + } + })} +
-
-
-
-
- +
+
+
+ +
+
-
-
+ ); } -export default function ListPage({items}: { - items: ListPageItem[] -}) { - return - } path={"/"}> - {(items.filter(item => typeof (item) === "object") as ListPageItemObject[]) - .filter(item => item.default) - .map(item => )} - - }> - {(items.filter(item => typeof (item) === "object") as ListPageItemObject[]).flatMap((item, index) => { - const routes = [] - if (item.default) routes.push() - return routes; - })} - - -} \ No newline at end of file +export default function ListPage({ items }: { items: ListPageItem[] }) { + return ( + + } + path={"/"} + > + {( + items.filter( + item => typeof item === "object", + ) as ListPageItemObject[] + ) + .filter(item => item.default) + .map(item => ( + + ))} + + }> + {( + items.filter( + item => typeof item === "object", + ) as ListPageItemObject[] + ).flatMap((item, index) => { + const routes = [ + , + ]; + if (item.default) + routes.push( + , + ); + return routes; + })} + + + ); +} diff --git a/Parlance.ClientApp/src/components/ListPageBlock.module.css b/Parlance.ClientApp/src/components/ListPageBlock.module.css index b146f364..c80fb12b 100644 --- a/Parlance.ClientApp/src/components/ListPageBlock.module.css +++ b/Parlance.ClientApp/src/components/ListPageBlock.module.css @@ -1,10 +1,10 @@ .listPageBlock { background-color: var(--layer-color); border-radius: var(--border-radius); - + padding: 9px; max-width: 600px; - + display: flex; flex-direction: column; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/ListPageBlock.tsx b/Parlance.ClientApp/src/components/ListPageBlock.tsx index 933ec0bc..75e83567 100644 --- a/Parlance.ClientApp/src/components/ListPageBlock.tsx +++ b/Parlance.ClientApp/src/components/ListPageBlock.tsx @@ -1,12 +1,10 @@ import Styles from "./ListPageBlock.module.css"; -import {ReactElement, ReactNode} from "react"; +import { ReactElement, ReactNode } from "react"; interface ListPageBlockProps { children: ReactNode; } export default function ListPageBlock(props: ListPageBlockProps): ReactElement { - return
- {props.children} -
-} \ No newline at end of file + return
{props.children}
; +} diff --git a/Parlance.ClientApp/src/components/Modal.module.css b/Parlance.ClientApp/src/components/Modal.module.css index 649bb426..514949a9 100644 --- a/Parlance.ClientApp/src/components/Modal.module.css +++ b/Parlance.ClientApp/src/components/Modal.module.css @@ -14,7 +14,6 @@ z-index: 1000; } - .PopoverBackground { justify-content: flex-end; align-items: stretch; @@ -26,7 +25,7 @@ flex-basis: 600px; max-height: 100vh; - + border-radius: var(--border-radius); } @@ -167,8 +166,8 @@ border-left: none; border-right: none; border-bottom: none; - + border-bottom-left-radius: 0; border-bottom-right-radius: 0; } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/Modal.tsx b/Parlance.ClientApp/src/components/Modal.tsx index 1b3157b3..af40823c 100644 --- a/Parlance.ClientApp/src/components/Modal.tsx +++ b/Parlance.ClientApp/src/components/Modal.tsx @@ -1,37 +1,36 @@ -import React, {ReactNode} from 'react'; -import Styles from './Modal.module.css'; -import {createRoot, Root} from "react-dom/client"; -import {WithTranslation, withTranslation} from "react-i18next"; +import React, { ReactNode } from "react"; +import Styles from "./Modal.module.css"; +import { createRoot, Root } from "react-dom/client"; +import { WithTranslation, withTranslation } from "react-i18next"; import i18n from "../helpers/i18n"; import Icon from "./Icon"; -import {TFunctionResult} from "i18next"; +import { TFunctionResult } from "i18next"; let root: Root | null; interface ModalButton { - destructive?: boolean - onClick?: () => void - text: string + destructive?: boolean; + onClick?: () => void; + text: string; } interface ModalExportProps { - popover?: any - buttons?: ModalButton[] - onButtonClick?: () => {} - children?: ReactNode | TFunctionResult - onBackClicked?: () => void - heading?: string - topComponent?: ReactNode + popover?: any; + buttons?: ModalButton[]; + onButtonClick?: () => {}; + children?: ReactNode | TFunctionResult; + onBackClicked?: () => void; + heading?: string; + topComponent?: ReactNode; } -interface ModalProps extends WithTranslation, ModalExportProps { -} +interface ModalProps extends WithTranslation, ModalExportProps {} -interface ModalState { - -} +interface ModalState {} -type ModalComponentExportType = new(ModalProps: any) => React.Component; +type ModalComponentExportType = new ( + ModalProps: any, +) => React.Component; interface ModalExportButtons { CancelButton: ModalButton; @@ -41,7 +40,7 @@ interface ModalExportButtons { interface ModalExports extends ModalComponentExportType, ModalExportButtons { mount: (modal: React.ReactElement) => void; unmount: () => void; - ModalProgressSpinner: typeof ModalProgressSpinner + ModalProgressSpinner: typeof ModalProgressSpinner; } interface ModalProgressSpinnerProps { @@ -50,106 +49,141 @@ interface ModalProgressSpinnerProps { class Modal extends React.Component { render() { - return
-
- {this.renderHeading()} - {this.renderTopComponent()} - {this.renderModalText()} - {this.renderModalList()} -
- {this.props.buttons?.map(button => { - if (typeof button === "object") { - let classes = [Styles.ModalButton]; - if (button.destructive) classes.push(Styles.DestructiveModalButton); - return
{button.text}
- } else { - // deprecated - // @ts-ignore - return
{button}
- } - })} + return ( +
+
+ {this.renderHeading()} + {this.renderTopComponent()} + {this.renderModalText()} + {this.renderModalList()} +
+ {this.props.buttons?.map(button => { + if (typeof button === "object") { + let classes = [Styles.ModalButton]; + if (button.destructive) + classes.push(Styles.DestructiveModalButton); + return ( +
+ {button.text} +
+ ); + } else { + // deprecated + // @ts-ignore + return ( +
+ {button} +
+ ); + } + })} +
-
+ ); } private renderModalList() { // @ts-ignore - let children = React.Children.toArray(this.props.children).filter(child => child.type?.displayName === "ModalList") + let children = React.Children.toArray(this.props.children).filter( + child => child.type?.displayName === "ModalList", + ); - return children.length !== 0 && <>{children} + return children.length !== 0 && <>{children}; } private renderModalText() { // @ts-ignore - let children = React.Children.toArray(this.props.children).filter(child => child.type?.displayName !== "ModalList") - - return children.length !== 0 &&
- {children} -
+ let children = React.Children.toArray(this.props.children).filter( + child => child.type?.displayName !== "ModalList", + ); + + return ( + children.length !== 0 && ( +
{children}
+ ) + ); } private renderHeading() { if (this.props.heading) { - return
- {this.props.popover && -
} - {this.props.heading} -
+ return ( +
+ {this.props.popover && ( +
+ +
+ )} + + {this.props.heading} + +
+ ); } return null; } private renderTopComponent() { if (this.props.topComponent) { - return
- {this.props.topComponent} -
; + return ( +
+ {this.props.topComponent} +
+ ); } return null; } } -function ModalProgressSpinner(props : ModalProgressSpinnerProps) { - return
- {props?.message} -
+function ModalProgressSpinner(props: ModalProgressSpinnerProps) { + return
{props?.message}
; } let setStandardButtons = () => { ExportProperty.CancelButton = { - text: i18n.t('CANCEL'), - onClick: () => ExportProperty.unmount() + text: i18n.t("CANCEL"), + onClick: () => ExportProperty.unmount(), }; ExportProperty.OkButton = { - text: i18n.t('OK'), - onClick: () => ExportProperty.unmount() + text: i18n.t("OK"), + onClick: () => ExportProperty.unmount(), }; -} +}; i18n.on("initialized", setStandardButtons); i18n.on("languageChanged", setStandardButtons); i18n.on("loaded", setStandardButtons); let ExportProperty = withTranslation()(Modal) as ModalExports; -ExportProperty.mount = (modal) => { +ExportProperty.mount = modal => { if (root) ExportProperty.unmount(); - root = createRoot(document.getElementById('modalContainer')!); - root.render( - - {modal} - ); -} + root = createRoot(document.getElementById("modalContainer")!); + root.render({modal}); +}; ExportProperty.unmount = () => { root?.unmount(); root = null; -} +}; ExportProperty.ModalProgressSpinner = ModalProgressSpinner; setStandardButtons(); -export default ExportProperty; \ No newline at end of file +export default ExportProperty; diff --git a/Parlance.ClientApp/src/components/ModalList.module.css b/Parlance.ClientApp/src/components/ModalList.module.css index 7e0b453d..4afea10f 100644 --- a/Parlance.ClientApp/src/components/ModalList.module.css +++ b/Parlance.ClientApp/src/components/ModalList.module.css @@ -32,4 +32,4 @@ .DestructiveListItem:active { background-color: var(--destructive-active-color); -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/ModalList.tsx b/Parlance.ClientApp/src/components/ModalList.tsx index 43f7f3dc..4cacd661 100644 --- a/Parlance.ClientApp/src/components/ModalList.tsx +++ b/Parlance.ClientApp/src/components/ModalList.tsx @@ -1,18 +1,18 @@ -import React from 'react'; -import Styles from './ModalList.module.css'; +import React from "react"; +import Styles from "./ModalList.module.css"; interface ModalListProps { - children?: ModalListItem | ModalListItem[] + children?: ModalListItem | ModalListItem[]; } interface ModalListItem { - type?: "destructive" + type?: "destructive"; onClick: () => void; dir?: "rtl" | "ltr"; - text: string + text: string; } -function ModalList({children}: ModalListProps) { +function ModalList({ children }: ModalListProps) { let items: ModalListItem[] | undefined; if (children instanceof Array) { items = children; @@ -21,17 +21,28 @@ function ModalList({children}: ModalListProps) { } else { items = [children]; } - - return
- {items?.map((item, index) => { - let styles = [Styles.ModalListItem]; - if (item.type === "destructive") styles.push(Styles.DestructiveListItem); - return
{item.text}
; - })} -
+ + return ( +
+ {items?.map((item, index) => { + let styles = [Styles.ModalListItem]; + if (item.type === "destructive") + styles.push(Styles.DestructiveListItem); + return ( +
+ {item.text} +
+ ); + })} +
+ ); } ModalList.displayName = "ModalList"; -export default ModalList; \ No newline at end of file +export default ModalList; diff --git a/Parlance.ClientApp/src/components/NavMenu.module.css b/Parlance.ClientApp/src/components/NavMenu.module.css index 58b4a3e7..648b59d7 100644 --- a/Parlance.ClientApp/src/components/NavMenu.module.css +++ b/Parlance.ClientApp/src/components/NavMenu.module.css @@ -38,4 +38,4 @@ padding-left: 9px; padding-right: 9px; } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/NavMenu.tsx b/Parlance.ClientApp/src/components/NavMenu.tsx index 7cc06c4a..e18310bf 100644 --- a/Parlance.ClientApp/src/components/NavMenu.tsx +++ b/Parlance.ClientApp/src/components/NavMenu.tsx @@ -1,32 +1,32 @@ -import React, {useEffect, useState} from 'react'; -import Styles from './NavMenu.module.css'; +import React, { useEffect, useState } from "react"; +import Styles from "./NavMenu.module.css"; import Button from "./Button"; import Modal from "./Modal"; import LoginUsernameModal from "./modals/account/LoginUsernameModal"; import UserManager from "../helpers/UserManager"; import UserModal from "./modals/account/UserModal"; -import {useTranslation} from "react-i18next"; -import {useNavigate} from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; import ParlanceLogo from "../images/parlance.svg"; export default function NavMenu() { const [currentUser, setCurrentUser] = useState(); - const {t} = useTranslation(); + const { t } = useTranslation(); const navigate = useNavigate(); UserManager.on("currentUserChanged", () => { - setCurrentUser(UserManager.currentUser?.username || t("LOG_IN")) + setCurrentUser(UserManager.currentUser?.username || t("LOG_IN")); }); useEffect(() => { - setCurrentUser(UserManager.currentUser?.username || t("LOG_IN")) + setCurrentUser(UserManager.currentUser?.username || t("LOG_IN")); }, []); const manageAccount = () => { if (UserManager.isLoggedIn) { - Modal.mount() + Modal.mount(); } else { UserManager.clearLoginDetails(); - Modal.mount() + Modal.mount(); } }; const goHome = () => { @@ -36,32 +36,31 @@ export default function NavMenu() { const goProjects = () => { navigate("/projects"); }; - + const goLanguages = () => { navigate("/languages"); }; - + const goGlossaries = () => { navigate("/glossaries"); - } + }; return (
- - - + + diff --git a/Parlance.ClientApp/src/components/PageContainer.jsx b/Parlance.ClientApp/src/components/PageContainer.jsx index 238799ad..e80f0ebd 100644 --- a/Parlance.ClientApp/src/components/PageContainer.jsx +++ b/Parlance.ClientApp/src/components/PageContainer.jsx @@ -1,7 +1,5 @@ -import Styles from "./PageContainer.module.css" +import Styles from "./PageContainer.module.css"; -export default function(props) { - return
- {props.children} -
-} \ No newline at end of file +export default function (props) { + return
{props.children}
; +} diff --git a/Parlance.ClientApp/src/components/PageContainer.module.css b/Parlance.ClientApp/src/components/PageContainer.module.css index 1eca1b00..dd6a5f8a 100644 --- a/Parlance.ClientApp/src/components/PageContainer.module.css +++ b/Parlance.ClientApp/src/components/PageContainer.module.css @@ -3,4 +3,4 @@ grid-area: content; overflow: auto; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/PageHeading.module.css b/Parlance.ClientApp/src/components/PageHeading.module.css index 9a1fcfa3..12e3337a 100644 --- a/Parlance.ClientApp/src/components/PageHeading.module.css +++ b/Parlance.ClientApp/src/components/PageHeading.module.css @@ -20,4 +20,4 @@ font-weight: normal; text-transform: uppercase; margin: 0; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/PageHeading.tsx b/Parlance.ClientApp/src/components/PageHeading.tsx index e71733bb..63b8fcd6 100644 --- a/Parlance.ClientApp/src/components/PageHeading.tsx +++ b/Parlance.ClientApp/src/components/PageHeading.tsx @@ -1,5 +1,5 @@ import Styles from "./PageHeading.module.css"; -import {ReactElement, ReactNode} from "react"; +import { ReactElement, ReactNode } from "react"; interface PageHeadingProps { level?: 1 | 2 | 3 | 4; @@ -10,12 +10,28 @@ interface PageHeadingProps { export default function PageHeading(props: PageHeadingProps): ReactElement { switch (props.level) { case 2: - return

{props.children}

+ return ( +

+ {props.children} +

+ ); case 3: - return

{props.children}

+ return ( +

+ {props.children} +

+ ); case 4: - return

{props.children}

+ return ( +

+ {props.children} +

+ ); default: - return

{props.children}

+ return ( +

+ {props.children} +

+ ); } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/PreloadingBlock.module.css b/Parlance.ClientApp/src/components/PreloadingBlock.module.css index c74d832d..b71a51b9 100644 --- a/Parlance.ClientApp/src/components/PreloadingBlock.module.css +++ b/Parlance.ClientApp/src/components/PreloadingBlock.module.css @@ -26,11 +26,21 @@ div.preloadingBlock { } :global(.ltr) div.preloadingBlock { - background-image: linear-gradient(to right, var(--preloading-block-color-1) 0%, var(--preloading-block-color-2) 10%, var(--preloading-block-color-1) 20%); + background-image: linear-gradient( + to right, + var(--preloading-block-color-1) 0%, + var(--preloading-block-color-2) 10%, + var(--preloading-block-color-1) 20% + ); animation: preloadingBlockKeyframeLtr 3s linear 0s infinite; } :global(.rtl) div.preloadingBlock { - background-image: linear-gradient(to left, var(--preloading-block-color-1) 0%, var(--preloading-block-color-2) 10%, var(--preloading-block-color-1) 20%); + background-image: linear-gradient( + to left, + var(--preloading-block-color-1) 0%, + var(--preloading-block-color-2) 10%, + var(--preloading-block-color-1) 20% + ); animation: preloadingBlockKeyframeRtl 3s linear 0s infinite; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/PreloadingBlock.tsx b/Parlance.ClientApp/src/components/PreloadingBlock.tsx index c905b161..16e12c2d 100644 --- a/Parlance.ClientApp/src/components/PreloadingBlock.tsx +++ b/Parlance.ClientApp/src/components/PreloadingBlock.tsx @@ -1,5 +1,5 @@ import Styles from "./PreloadingBlock.module.css"; -import {CSSProperties, ReactElement, ReactNode} from "react"; +import { CSSProperties, ReactElement, ReactNode } from "react"; interface PreloadingBlockProps { className?: string; @@ -7,11 +7,20 @@ interface PreloadingBlockProps { width?: number; } -export default function PreloadingBlock({className, children, width = 100}: PreloadingBlockProps): ReactElement { +export default function PreloadingBlock({ + className, + children, + width = 100, +}: PreloadingBlockProps): ReactElement { let style: CSSProperties = {}; if (width) style.width = `${width}%`; - - return
- {children} -
-} \ No newline at end of file + + return ( +
+ {children} +
+ ); +} diff --git a/Parlance.ClientApp/src/components/SelectableList.module.css b/Parlance.ClientApp/src/components/SelectableList.module.css index a533d615..63793db9 100644 --- a/Parlance.ClientApp/src/components/SelectableList.module.css +++ b/Parlance.ClientApp/src/components/SelectableList.module.css @@ -1,7 +1,7 @@ .listContainer { display: flex; flex-direction: column; - + gap: 2px; } @@ -40,4 +40,4 @@ .listItemOnState { text-transform: uppercase; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/SelectableList.tsx b/Parlance.ClientApp/src/components/SelectableList.tsx index 6b0f3db9..3655db3b 100644 --- a/Parlance.ClientApp/src/components/SelectableList.tsx +++ b/Parlance.ClientApp/src/components/SelectableList.tsx @@ -1,83 +1,109 @@ -import Styles from "./SelectableList.module.css" -import React, {ReactElement, ReactNode, useEffect, useState} from "react"; +import Styles from "./SelectableList.module.css"; +import React, { ReactElement, ReactNode, useEffect, useState } from "react"; import Fetch from "../helpers/Fetch"; import i18n from "../helpers/i18n"; -import {VerticalLayout} from "./Layouts"; +import { VerticalLayout } from "./Layouts"; import LineEdit from "./LineEdit"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import PreloadingBlock from "./PreloadingBlock"; interface SelectableListItemObject { - onClick?: () => void - contents: ReactNode - containerClass?: string, - on?: boolean - type?: "destructive" + onClick?: () => void; + contents: ReactNode; + containerClass?: string; + on?: boolean; + type?: "destructive"; } type SelectableListItem = SelectableListItemObject | string; interface SelectableListOneItemProps { - children?: React.ReactElement - onClick?: () => void - on?: boolean + children?: React.ReactElement; + onClick?: () => void; + on?: boolean; } interface SelectableListMultiItemProps { - items?: (SelectableListItem | undefined)[] + items?: (SelectableListItem | undefined)[]; } -type SelectableListProps = SelectableListOneItemProps | SelectableListMultiItemProps; +type SelectableListProps = + | SelectableListOneItemProps + | SelectableListMultiItemProps; interface SelectableListLocaleProps { locales: string[]; onLocaleSelected: (locale: string) => void; } -function SelectableListItem({item}: { - item: SelectableListItemObject -}) { - const {t} = useTranslation(); - - return
-
{item.contents}
- {item.on !== undefined &&
- {item.on ? t("On") : t("Off")} -
} -
+function SelectableListItem({ item }: { item: SelectableListItemObject }) { + const { t } = useTranslation(); + + return ( +
+
{item.contents}
+ {item.on !== undefined && ( +
+ {item.on ? t("On") : t("Off")} +
+ )} +
+ ); } -export default function SelectableList(props: SelectableListProps): ReactElement | null { +export default function SelectableList( + props: SelectableListProps, +): ReactElement | null { if ("children" in props && props.children) { - const {children, onClick, on} = props; - return
- -
+ const { children, onClick, on } = props; + return ( +
+ +
+ ); } else if ("items" in props) { - const {items} = props; + const { items } = props; if (!items?.length) return null; - return
- {items.filter(item => item).map((item, index) => { - if (typeof (item) === "string") { - return
{item}
- } else { - return - } - })} -
+ return ( +
+ {items + .filter(item => item) + .map((item, index) => { + if (typeof item === "string") { + return ( +
+ {item} +
+ ); + } else { + return ( + + ); + } + })} +
+ ); } return null; } -SelectableList.Locales = function Locales({locales, onLocaleSelected}: SelectableListLocaleProps): ReactElement { +SelectableList.Locales = function Locales({ + locales, + onLocaleSelected, +}: SelectableListLocaleProps): ReactElement { const [query, setQuery] = useState(""); const [availableLocales, setAvailableLocales] = useState([]); - const {t} = useTranslation(); + const { t } = useTranslation(); useEffect(() => { (async () => { @@ -87,29 +113,43 @@ SelectableList.Locales = function Locales({locales, onLocaleSelected}: Selectabl setAvailableLocales(await Fetch.get("/api/cldr")); } })(); - }, [locales]) + }, [locales]); - const items = availableLocales.map(locale => ({ - contents: i18n.humanReadableLocale(locale), - onClick: () => onLocaleSelected(locale), - locale - })) - .filter(x => query === "" || x.locale.toLowerCase().includes(query.toLowerCase()) || x.contents.toLowerCase().includes(query.toLowerCase())) - .sort((a, b) => new Intl.Collator(i18n.language).compare(a.contents, b.contents)) + const items = availableLocales + .map(locale => ({ + contents: i18n.humanReadableLocale(locale), + onClick: () => onLocaleSelected(locale), + locale, + })) + .filter( + x => + query === "" || + x.locale.toLowerCase().includes(query.toLowerCase()) || + x.contents.toLowerCase().includes(query.toLowerCase()), + ) + .sort((a, b) => + new Intl.Collator(i18n.language).compare(a.contents, b.contents), + ); - return - setQuery((e.target as HTMLInputElement).value)}/> - - -} + return ( + + setQuery((e.target as HTMLInputElement).value)} + /> + + + ); +}; SelectableList.PreloadingText = function (num = 3) { let arr: { - contents: ReactNode + contents: ReactNode; }[] = []; for (let i = 0; i < num; i++) { arr.push({ - contents: Text + contents: Text, }); } return arr; diff --git a/Parlance.ClientApp/src/components/SilentInformation.module.css b/Parlance.ClientApp/src/components/SilentInformation.module.css index 2b23800a..e1b0108d 100644 --- a/Parlance.ClientApp/src/components/SilentInformation.module.css +++ b/Parlance.ClientApp/src/components/SilentInformation.module.css @@ -11,5 +11,4 @@ } .text { - -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/SilentInformation.tsx b/Parlance.ClientApp/src/components/SilentInformation.tsx index 6750be00..491a5b82 100644 --- a/Parlance.ClientApp/src/components/SilentInformation.tsx +++ b/Parlance.ClientApp/src/components/SilentInformation.tsx @@ -1,14 +1,20 @@ import Styles from "./SilentInformation.module.css"; interface SilentInformationProps { - title: string - text: string - className?: string + title: string; + text: string; + className?: string; } -export default function SilentInformation({title, text, className = ""}: SilentInformationProps) { - return
- {title} - {text} -
-} \ No newline at end of file +export default function SilentInformation({ + title, + text, + className = "", +}: SilentInformationProps) { + return ( +
+ {title} + {text} +
+ ); +} diff --git a/Parlance.ClientApp/src/components/SmallButton.tsx b/Parlance.ClientApp/src/components/SmallButton.tsx index 10ab4f21..6a0cee58 100644 --- a/Parlance.ClientApp/src/components/SmallButton.tsx +++ b/Parlance.ClientApp/src/components/SmallButton.tsx @@ -1,17 +1,27 @@ -import Styles from "./SmallButton.module.css" -import {useTabIndex} from "react-tabindex"; -import {ReactElement, ReactNode} from "react"; +import Styles from "./SmallButton.module.css"; +import { useTabIndex } from "react-tabindex"; +import { ReactElement, ReactNode } from "react"; interface SmallButtonProps { - children: ReactNode - onClick?: () => void - tabIndex?: number + children: ReactNode; + onClick?: () => void; + tabIndex?: number; } -export default function SmallButton({children, onClick, tabIndex}: SmallButtonProps): ReactElement { +export default function SmallButton({ + children, + onClick, + tabIndex, +}: SmallButtonProps): ReactElement { tabIndex = useTabIndex(tabIndex); - return
- {children} -
-} \ No newline at end of file + return ( +
+ {children} +
+ ); +} diff --git a/Parlance.ClientApp/src/components/Spinner.module.css b/Parlance.ClientApp/src/components/Spinner.module.css index 1bd12bbb..be249118 100644 --- a/Parlance.ClientApp/src/components/Spinner.module.css +++ b/Parlance.ClientApp/src/components/Spinner.module.css @@ -10,9 +10,9 @@ grid-template-columns: 1fr var(--spinner-bouncer-width); grid-template-rows: 1fr var(--spinner-floor-height); gap: 0px 0px; - grid-template-areas: - "trail bouncer" - "floor floor"; + grid-template-areas: + "trail bouncer" + "floor floor"; animation: ballAnimation 0.5s linear infinite normal; } @@ -63,4 +63,4 @@ align-items: center; justify-content: center; flex-grow: 1; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/Spinner.tsx b/Parlance.ClientApp/src/components/Spinner.tsx index 37274a32..38eb294b 100644 --- a/Parlance.ClientApp/src/components/Spinner.tsx +++ b/Parlance.ClientApp/src/components/Spinner.tsx @@ -1,36 +1,42 @@ import Styles from "./Spinner.module.css"; -import {ReactElement, ReactNode, Suspense} from "react"; +import { ReactElement, ReactNode, Suspense } from "react"; interface SpinnerSuspenseProps { - children: ReactNode + children: ReactNode; } -export default function Spinner() : ReactElement { - return
- - - - - -
+export default function Spinner(): ReactElement { + return ( +
+ + + + + +
+ ); } -Spinner.Container = function SpinnerContainer() : ReactElement { - return
- -
-} +Spinner.Container = function SpinnerContainer(): ReactElement { + return ( +
+ +
+ ); +}; -Spinner.Suspense = function SpinnerSuspense({children} : SpinnerSuspenseProps) : ReactElement { - return }> - {children} - -} \ No newline at end of file +Spinner.Suspense = function SpinnerSuspense({ + children, +}: SpinnerSuspenseProps): ReactElement { + return }>{children}; +}; diff --git a/Parlance.ClientApp/src/components/TranslationProgressIndicator.jsx b/Parlance.ClientApp/src/components/TranslationProgressIndicator.jsx index 946b9dad..e1d2d6ef 100644 --- a/Parlance.ClientApp/src/components/TranslationProgressIndicator.jsx +++ b/Parlance.ClientApp/src/components/TranslationProgressIndicator.jsx @@ -1,130 +1,179 @@ import Styles from "./TranslationProgressIndicator.module.css"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import React from "react"; import PreloadingBlock from "./PreloadingBlock"; -import {calculateDeadline} from "../helpers/Misc"; +import { calculateDeadline } from "../helpers/Misc"; const percent = value => `${value * 100}%`; -function TranslationProgressMetric({value, title, shortTitle, className}) { - if (typeof (value) === "number") { +function TranslationProgressMetric({ value, title, shortTitle, className }) { + if (typeof value === "number") { if (value > 10000) { - value = `${(value / 1000).toFixed(0)}k` + value = `${(value / 1000).toFixed(0)}k`; } else if (value > 1000) { - value = `${(value / 1000).toFixed(1)}k` + value = `${(value / 1000).toFixed(1)}k`; } } - return
- {shortTitle} - {value} - {title} -
+ return ( +
+ {shortTitle} + {value} + {title} +
+ ); } -function TranslationProgressBar({data, className}) { +function TranslationProgressBar({ data, className }) { if (data?.count) { - return
-
-
-
-
+ return ( +
+
+
+
+
+ ); } else { - return
- -
+ return
; } } -export default function TranslationProgressIndicator({title, data, deadline, badges = []}) { - const {t} = useTranslation(); +export default function TranslationProgressIndicator({ + title, + data, + deadline, + badges = [], +}) { + const { t } = useTranslation(); const deadlineData = calculateDeadline(deadline); let metrics = []; if (data?.count == null) { - } else { metrics.push([ - , - , - - ]) + , + , + , + ]); } - if (data?.warnings > 0) metrics.push(); - if (data?.errors > 0) metrics.push(); - + if (data?.warnings > 0) + metrics.push( + , + ); + if (data?.errors > 0) + metrics.push( + , + ); + metrics = metrics.flat().reverse(); let titleStyles = [Styles.title]; if (data?.count == null) titleStyles.push(Styles.newTitle); - - return
-
-
- {title} - {badges.map((text, i) => {text})} + + return ( +
+
+
+ {title} + {badges.map((text, i) => ( + + {text} + + ))} +
+ {deadlineData.valid && ( + + )} +
{metrics}
+
- {deadlineData.valid && } -
- {metrics} -
- -
+ ); } TranslationProgressIndicator.Preloading = function () { - return
- -
- - 20 - TEXT - - - 20 - TEXT - - - 20 - TEXT - - - 20 - TEXT - - - 20 - TEXT - + return ( +
+ +
+ + 20 + TEXT + + + 20 + TEXT + + + 20 + TEXT + + + 20 + TEXT + + + 20 + TEXT + +
+
- -
-} + ); +}; TranslationProgressIndicator.PreloadContents = function (num = 3) { let arr = []; for (let i = 0; i < num; i++) { arr.push({ - contents: + contents: , }); } return arr; diff --git a/Parlance.ClientApp/src/components/TranslationProgressIndicator.module.css b/Parlance.ClientApp/src/components/TranslationProgressIndicator.module.css index 8849073e..6f3d0e9d 100644 --- a/Parlance.ClientApp/src/components/TranslationProgressIndicator.module.css +++ b/Parlance.ClientApp/src/components/TranslationProgressIndicator.module.css @@ -3,14 +3,14 @@ grid-template-columns: max-content 1fr max-content max-content; grid-template-rows: max-content max-content; gap: 6px 18px; - grid-template-areas: - "title . deadline metrics" - "progress progress progress progress"; + grid-template-areas: + "title . deadline metrics" + "progress progress progress progress"; } .title { grid-area: title; - + display: flex; align-items: flex-start; } @@ -34,7 +34,6 @@ } .percentComplete { - } .warnings { @@ -61,7 +60,6 @@ } .metricValue { - } .metricTitle { @@ -105,18 +103,18 @@ @media (max-width: 700px) { .root { - grid-template-areas: + grid-template-areas: "title deadline" "metrics metrics" "progress progress"; grid-template-columns: 1fr; grid-template-rows: max-content max-content max-content; } - + .metrics { justify-content: space-around; } - + .deadline { flex-direction: row; gap: 2px; @@ -138,4 +136,4 @@ gap: 2px; align-items: baseline; } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/WallMessage.jsx b/Parlance.ClientApp/src/components/WallMessage.jsx index 033d485f..56d2558c 100644 --- a/Parlance.ClientApp/src/components/WallMessage.jsx +++ b/Parlance.ClientApp/src/components/WallMessage.jsx @@ -1,13 +1,15 @@ import Styles from "./WallMessage.module.css"; import Container from "./Container"; -import {VerticalLayout} from "./Layouts"; +import { VerticalLayout } from "./Layouts"; import PageHeading from "./PageHeading"; -export default function WallMessage({message, title}) { - return - - {title} - {message} - - -} \ No newline at end of file +export default function WallMessage({ message, title }) { + return ( + + + {title} + {message} + + + ); +} diff --git a/Parlance.ClientApp/src/components/comments/ThreadItem.module.css b/Parlance.ClientApp/src/components/comments/ThreadItem.module.css index 637b3e41..33012069 100644 --- a/Parlance.ClientApp/src/components/comments/ThreadItem.module.css +++ b/Parlance.ClientApp/src/components/comments/ThreadItem.module.css @@ -3,7 +3,7 @@ grid-template-columns: 1fr max-content; grid-template-rows: max-content max-content max-content; gap: 6px 0px; - grid-template-areas: + grid-template-areas: "threadTitle goButton" "lastMessageDate goButton" "lastMessage goButton"; @@ -45,7 +45,7 @@ .threadCreator { font-size: 10pt; - + display: inline-flex; align-items: center; gap: 6px; @@ -58,4 +58,4 @@ .threadCreatorImage { height: 16px; border-radius: 50%; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/comments/ThreadItem.tsx b/Parlance.ClientApp/src/components/comments/ThreadItem.tsx index 2122ff81..41287d9a 100644 --- a/Parlance.ClientApp/src/components/comments/ThreadItem.tsx +++ b/Parlance.ClientApp/src/components/comments/ThreadItem.tsx @@ -1,29 +1,44 @@ -import {Thread} from "../../interfaces/comments"; +import { Thread } from "../../interfaces/comments"; import Styles from "./ThreadItem.module.css"; import Icon from "../Icon"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import moment from "moment"; -export function ThreadItem({item, onCurrentThreadChanged, noPadding}: { - item: Thread, - onCurrentThreadChanged: (thread: Thread) => void, - noPadding?: boolean +export function ThreadItem({ + item, + onCurrentThreadChanged, + noPadding, +}: { + item: Thread; + onCurrentThreadChanged: (thread: Thread) => void; + noPadding?: boolean; }) { - const {t} = useTranslation(); - - return
onCurrentThreadChanged(item)}> - {item.title} - -
- {moment(item.headComment.date).fromNow(false)} -
-
- {item.headComment.text} -  —  - - {t("COMMENT_THREAD_PROFILE_PICTURE_ALT_TEXT", - {item.headComment.author.username} - + const { t } = useTranslation(); + + return ( +
onCurrentThreadChanged(item)} + > + {item.title} + +
+ {moment(item.headComment.date).fromNow(false)} +
+
+ {item.headComment.text} +  —  + + {t("COMMENT_THREAD_PROFILE_PICTURE_ALT_TEXT", + {item.headComment.author.username} + +
-
-} \ No newline at end of file + ); +} diff --git a/Parlance.ClientApp/src/components/modals/ErrorModal.tsx b/Parlance.ClientApp/src/components/modals/ErrorModal.tsx index fd34eac5..5a70d426 100644 --- a/Parlance.ClientApp/src/components/modals/ErrorModal.tsx +++ b/Parlance.ClientApp/src/components/modals/ErrorModal.tsx @@ -1,19 +1,26 @@ -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import Modal from "../Modal"; -import {VerticalLayout} from "../Layouts"; -import {ReactElement, ReactNode, useEffect, useState} from "react"; +import { VerticalLayout } from "../Layouts"; +import { ReactElement, ReactNode, useEffect, useState } from "react"; interface ErrorModalProps { error: any; onContinue?: () => void; - specialRenderings?: {[key: string]: ReactNode}; + specialRenderings?: { [key: string]: ReactNode }; okButtonText?: string; } -export default function ErrorModal({error, onContinue, specialRenderings, okButtonText}: ErrorModalProps): ReactElement { - const {t} = useTranslation(); +export default function ErrorModal({ + error, + onContinue, + specialRenderings, + okButtonText, +}: ErrorModalProps): ReactElement { + const { t } = useTranslation(); const [message, setMessage] = useState(t("ERROR_GENERIC")); - const [specialRendering, setSpecialRendering] = useState(null); + const [specialRendering, setSpecialRendering] = useState( + null, + ); useEffect(() => { (async () => { @@ -24,67 +31,69 @@ export default function ErrorModal({error, onContinue, specialRenderings, okButt let jsonError = json.error; if (specialRenderings && specialRenderings[jsonError]) { - setSpecialRendering(specialRenderings[jsonError]) + setSpecialRendering(specialRenderings[jsonError]); return; } switch (jsonError) { case "UnknownUser": - setMessage(t("ERROR_UNKNOWN_USER")) + setMessage(t("ERROR_UNKNOWN_USER")); return; case "PermissionAlreadyGranted": - setMessage(t("ERROR_PERMISSION_ALREADY_GRANTED")) + setMessage(t("ERROR_PERMISSION_ALREADY_GRANTED")); return; case "UsernameAlreadyExists": - setMessage(t("ERROR_USERNAME_ALREADY_EXISTS")) + setMessage(t("ERROR_USERNAME_ALREADY_EXISTS")); return; case "TwoFactorIsDisabled": - setMessage(t("ERROR_TWO_FACTOR_IS_DISABLED")) + setMessage(t("ERROR_TWO_FACTOR_IS_DISABLED")); return; case "TwoFactorAlreadyEnabled": - setMessage(t("ERROR_TWO_FACTOR_ALREADY_ENABLED")) + setMessage(t("ERROR_TWO_FACTOR_ALREADY_ENABLED")); return; case "TwoFactorAlreadyDisabled": - setMessage(t("ERROR_TWO_FACTOR_ALREADY_DISABLED")) + setMessage(t("ERROR_TWO_FACTOR_ALREADY_DISABLED")); return; case "TwoFactorCodeIncorrect": - setMessage(t("ERROR_TWO_FACTOR_INCORRECT")) + setMessage(t("ERROR_TWO_FACTOR_INCORRECT")); return; case "NonFastForwardableError": - setMessage(t("ERROR_NON_FAST_FORWARDABLE")) + setMessage(t("ERROR_NON_FAST_FORWARDABLE")); return; case "MergeConflict": - setMessage(t("ERROR_MERGE_CONFLICT")) + setMessage(t("ERROR_MERGE_CONFLICT")); return; case "DirtyWorkingTree": - setMessage(t("ERROR_DIRTY_WORKING_TREE")) + setMessage(t("ERROR_DIRTY_WORKING_TREE")); return; case "BadTokenRequestType": - setMessage(t("ERROR_BAD_TOKEN_REQUEST_TYPE")) + setMessage(t("ERROR_BAD_TOKEN_REQUEST_TYPE")); return; } - } catch { - - } + } catch {} })(); - }, [error]) + }, [error]); - if (specialRendering) return <> - specialRendering - ; + if (specialRendering) return <>specialRendering; - return { - if (onContinue) { - onContinue(); - } else { - Modal.unmount(); - } - } - }]}> - - {message} - - -} \ No newline at end of file + return ( + { + if (onContinue) { + onContinue(); + } else { + Modal.unmount(); + } + }, + }, + ]} + > + + {message} + + + ); +} diff --git a/Parlance.ClientApp/src/components/modals/LanguageSelectionModal.tsx b/Parlance.ClientApp/src/components/modals/LanguageSelectionModal.tsx index 69a0aebc..87c6bf12 100644 --- a/Parlance.ClientApp/src/components/modals/LanguageSelectionModal.tsx +++ b/Parlance.ClientApp/src/components/modals/LanguageSelectionModal.tsx @@ -1,6 +1,6 @@ -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import Modal from "../Modal"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import Spinner from "../Spinner"; import Fetch from "../../helpers/Fetch"; import ModalList from "../ModalList"; @@ -10,17 +10,19 @@ import LoadingModal from "./LoadingModal"; export default function LanguageSelectionModal() { const [done, setDone] = useState(false); const [languageIndex, setLanguageIndex] = useState([]); - const {t} = useTranslation(); + const { t } = useTranslation(); useEffect(() => { (async () => { - setLanguageIndex(await Fetch.get("/resources/translations/index.json")); + setLanguageIndex( + await Fetch.get("/resources/translations/index.json"), + ); setDone(true); })(); }, []); const setLang = async (lang: string) => { - Modal.mount() + Modal.mount(); if (lang === "system") { localStorage.removeItem("lang"); await i18n.changeLanguage(); @@ -32,16 +34,25 @@ export default function LanguageSelectionModal() { Modal.unmount(); }; - return - {t("SELECT_LANGUAGE_PROMPT")} - {done ? - {["system", ...languageIndex].map(x => { - return { - text: x === "system" ? t("SELECT_LANGUAGE_BROWSER_SETTINGS") : i18n.humanReadableLocale(x, x), - onClick: () => setLang(x), - dir: i18n.dir(x) - }; - })} - : } - -} \ No newline at end of file + return ( + + {t("SELECT_LANGUAGE_PROMPT")} + {done ? ( + + {["system", ...languageIndex].map(x => { + return { + text: + x === "system" + ? t("SELECT_LANGUAGE_BROWSER_SETTINGS") + : i18n.humanReadableLocale(x, x), + onClick: () => setLang(x), + dir: i18n.dir(x), + }; + })} + + ) : ( + + )} + + ); +} diff --git a/Parlance.ClientApp/src/components/modals/LoadingModal.tsx b/Parlance.ClientApp/src/components/modals/LoadingModal.tsx index ec21ffe4..f06f11c8 100644 --- a/Parlance.ClientApp/src/components/modals/LoadingModal.tsx +++ b/Parlance.ClientApp/src/components/modals/LoadingModal.tsx @@ -2,8 +2,10 @@ import Modal from "../Modal"; import React from "react"; import Spinner from "../Spinner"; -export default function LoadingModal() : React.ReactElement { - return - - -} \ No newline at end of file +export default function LoadingModal(): React.ReactElement { + return ( + + + + ); +} diff --git a/Parlance.ClientApp/src/components/modals/account/CreateAccountModal.tsx b/Parlance.ClientApp/src/components/modals/account/CreateAccountModal.tsx index 485decd6..81994a5f 100644 --- a/Parlance.ClientApp/src/components/modals/account/CreateAccountModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/CreateAccountModal.tsx @@ -1,69 +1,121 @@ -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import Modal from "../../Modal"; import LoginUsernameModal from "./LoginUsernameModal"; -import {VerticalLayout, VerticalSpacer} from "../../Layouts"; +import { VerticalLayout, VerticalSpacer } from "../../Layouts"; import LineEdit from "../../LineEdit"; -import React, {useContext, useState} from "react"; +import React, { useContext, useState } from "react"; import LoadingModal from "../LoadingModal"; import ErrorModal from "../ErrorModal"; import UserManager from "../../../helpers/UserManager"; import Fetch from "../../../helpers/Fetch"; -import {RegisterSecurityKeyAdvertisement} from "./securityKeys/RegisterSecurityKeyAdvertisement"; -import {TokenResponseToken} from "../../../interfaces/users"; -import {ServerInformationContext} from "@/context/ServerInformationContext"; +import { RegisterSecurityKeyAdvertisement } from "./securityKeys/RegisterSecurityKeyAdvertisement"; +import { TokenResponseToken } from "../../../interfaces/users"; +import { ServerInformationContext } from "@/context/ServerInformationContext"; export default function CreateAccountModal() { const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); - const {t} = useTranslation(); + const { t } = useTranslation(); const serverInformation = useContext(ServerInformationContext); - - let modal = Modal.mount() - }, - { - text: t("CREATE_ACCOUNT"), - onClick: async () => { - if (password !== confirmPassword) return; - Modal.mount(); - try { - let result = await Fetch.post("/api/user", { - username: username, - password: password, - emailAddress: email - }); - - await UserManager.setToken(result.token); - - if (window.PublicKeyCredential && !localStorage.getItem("passkey-advertisement-never-ask")) { - Modal.mount() - return; + let modal = ( + Modal.mount(), + }, + { + text: t("CREATE_ACCOUNT"), + onClick: async () => { + if (password !== confirmPassword) return; + + Modal.mount(); + try { + let result = await Fetch.post( + "/api/user", + { + username: username, + password: password, + emailAddress: email, + }, + ); + + await UserManager.setToken(result.token); + + if ( + window.PublicKeyCredential && + !localStorage.getItem( + "passkey-advertisement-never-ask", + ) + ) { + Modal.mount( + , + ); + return; + } + + Modal.unmount(); + } catch (error) { + Modal.mount( + Modal.mount(modal)} + />, + ); + } + }, + }, + ]} + > + + + {t("CREATE_ACCOUNT_PROMPT", { + type: serverInformation.accountName, + })} + + + setUsername((e.target as HTMLInputElement).value) } - - Modal.unmount(); - } catch (error) { - Modal.mount( Modal.mount(modal)} />); - } - } - } - ]}> - - {t("CREATE_ACCOUNT_PROMPT", {type: serverInformation.accountName})} - setUsername((e.target as HTMLInputElement).value)} /> - setEmail((e.target as HTMLInputElement).value)} /> - - - - {t("PASSWORD_SET_SECURITY_PROMPT")} - setPassword((e.target as HTMLInputElement).value)} /> - setConfirmPassword((e.target as HTMLInputElement).value)} /> - - - + /> + + setEmail((e.target as HTMLInputElement).value) + } + /> + + + + {t("PASSWORD_SET_SECURITY_PROMPT")} + + setPassword((e.target as HTMLInputElement).value) + } + /> + + setConfirmPassword((e.target as HTMLInputElement).value) + } + /> + + + ); + return modal; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/modals/account/LoginErrorModal.jsx b/Parlance.ClientApp/src/components/modals/account/LoginErrorModal.jsx index 0e698da5..e677e9ea 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginErrorModal.jsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginErrorModal.jsx @@ -1,22 +1,27 @@ -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import Modal from "../../Modal"; import UserManager from "../../../helpers/UserManager"; import LoginUsernameModal from "./LoginUsernameModal"; import React from "react"; export default function LoginErrorModal() { - const {t} = useTranslation(); - - return { - UserManager.clearLoginDetails(); - Modal.mount() - } - }, - Modal.OkButton - ]}> - {t("LOGIN_ERROR_PROMPT")} - + const { t } = useTranslation(); + + return ( + { + UserManager.clearLoginDetails(); + Modal.mount(); + }, + }, + Modal.OkButton, + ]} + > + {t("LOGIN_ERROR_PROMPT")} + + ); } diff --git a/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.module.css b/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.module.css index e22c9910..3b425133 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.module.css +++ b/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.module.css @@ -1,3 +1,3 @@ .hint { color: var(--foreground-disabled-color); -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.tsx b/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.tsx index 42c80eae..5e69a838 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginOtpModal.tsx @@ -1,59 +1,84 @@ import Modal from "../../Modal"; -import React, {FormEvent, ReactElement} from "react"; +import React, { FormEvent, ReactElement } from "react"; import UserManager from "../../../helpers/UserManager"; import LoginPasswordModal from "./LoginPasswordModal"; -import {TFunction, withTranslation} from "react-i18next"; +import { TFunction, withTranslation } from "react-i18next"; import LineEdit from "../../LineEdit"; -import {VerticalLayout, VerticalSpacer} from "../../Layouts"; +import { VerticalLayout, VerticalSpacer } from "../../Layouts"; import Styles from "./LoginOtpModal.module.css"; interface LoginOtpModalProps { - t: TFunction + t: TFunction; } interface LoginOtpModalState { - otp: string + otp: string; } -class LoginOtpModal extends React.Component { +class LoginOtpModal extends React.Component< + LoginOtpModalProps, + LoginOtpModalState +> { constructor(props: LoginOtpModalProps) { super(props); this.state = { - otp: "" - } + otp: "", + }; } otpTextChanged(e: FormEvent) { this.setState({ - otp: (e.target as HTMLInputElement).value + otp: (e.target as HTMLInputElement).value, }); } render() { - return Modal.mount() - }, - { - text: this.props.t('NEXT'), - onClick: () => { - UserManager.setLoginDetail("otpToken", this.state.otp); - UserManager.attemptLogin(); - } - } - ]}> -
- - {this.props.t('LOG_IN_TWO_FACTOR_AUTHENTICATION_PROMPT_1')} - {this.props.t('LOG_IN_TWO_FACTOR_AUTHENTICATION_PROMPT_2')} - - - -
-
+ return ( + Modal.mount(), + }, + { + text: this.props.t("NEXT"), + onClick: () => { + UserManager.setLoginDetail( + "otpToken", + this.state.otp, + ); + UserManager.attemptLogin(); + }, + }, + ]} + > +
+ + + {this.props.t( + "LOG_IN_TWO_FACTOR_AUTHENTICATION_PROMPT_1", + )} + + + {this.props.t( + "LOG_IN_TWO_FACTOR_AUTHENTICATION_PROMPT_2", + )} + + + + +
+
+ ); } } -export default withTranslation()(LoginOtpModal); \ No newline at end of file +export default withTranslation()(LoginOtpModal); diff --git a/Parlance.ClientApp/src/components/modals/account/LoginPasswordModal.tsx b/Parlance.ClientApp/src/components/modals/account/LoginPasswordModal.tsx index 7904d10b..16131f49 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginPasswordModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginPasswordModal.tsx @@ -1,15 +1,17 @@ import Modal from "../../Modal"; -import React, {useEffect, useState} from "react"; +import React, { useEffect, useState } from "react"; import LoginUsernameModal from "./LoginUsernameModal"; import UserManager from "../../../helpers/UserManager"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import LineEdit from "../../LineEdit"; import ModalList from "../../ModalList"; -import {VerticalSpacer} from "@/components/Layouts"; +import { VerticalSpacer } from "@/components/Layouts"; export default function LoginPasswordModal() { - const [password, setPassword] = useState(UserManager.loginDetail("prePassword")); - const {t} = useTranslation(); + const [password, setPassword] = useState( + UserManager.loginDetail("prePassword"), + ); + const { t } = useTranslation(); useEffect(() => { UserManager.setLoginDetail("prePassword"); @@ -18,46 +20,67 @@ export default function LoginPasswordModal() { const loginTypes = UserManager.loginTypes.map(type => { switch (type) { case "password": - return
- {t('LOG_IN_PASSWORD_PROMPT')} - - setPassword((e.target as HTMLInputElement).value)}/> -
+ return ( +
+ {t("LOG_IN_PASSWORD_PROMPT")} + + + setPassword( + (e.target as HTMLInputElement).value, + ) + } + /> +
+ ); case "fido": //Ensure the browser supports webauthn if (!window.PublicKeyCredential) return null; - return - {[ - { - text: t("LOG_IN_USE_SECURITY_KEY_PROMPT"), - onClick: () => UserManager.attemptFido2Login() - } - ]} - + return ( + + {[ + { + text: t("LOG_IN_USE_SECURITY_KEY_PROMPT"), + onClick: () => UserManager.attemptFido2Login(), + }, + ]} + + ); } }); - return Modal.mount() - }, - { - text: t('FORGOT_PASSWORD'), - onClick: () => UserManager.triggerPasswordReset() - }, - { - text: t('NEXT'), - onClick: () => { - UserManager.setLoginDetail("password", password); - UserManager.setLoginDetail("type", "password"); - UserManager.attemptLogin(); - } - } - ]}> - {loginTypes} - + return ( + Modal.mount(), + }, + { + text: t("FORGOT_PASSWORD"), + onClick: () => UserManager.triggerPasswordReset(), + }, + { + text: t("NEXT"), + onClick: () => { + UserManager.setLoginDetail("password", password); + UserManager.setLoginDetail("type", "password"); + UserManager.attemptLogin(); + }, + }, + ]} + > + {loginTypes} + + ); } - diff --git a/Parlance.ClientApp/src/components/modals/account/LoginPasswordResetModal.jsx b/Parlance.ClientApp/src/components/modals/account/LoginPasswordResetModal.jsx index fabfa801..00e7c41e 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginPasswordResetModal.jsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginPasswordResetModal.jsx @@ -2,55 +2,81 @@ import Modal from "../../Modal"; import React from "react"; import LoginUsernameModal from "./LoginUsernameModal"; import UserManager from "../../../helpers/UserManager"; -import {withTranslation} from "react-i18next"; +import { withTranslation } from "react-i18next"; import LineEdit from "../../LineEdit"; -export default withTranslation()(class LoginPasswordResetModal extends React.Component { - constructor(props) { - super(props); +export default withTranslation()( + class LoginPasswordResetModal extends React.Component { + constructor(props) { + super(props); - this.state = { - password: "", - confirmPassword: "" + this.state = { + password: "", + confirmPassword: "", + }; } - } - passwordTextChanged(e) { - this.setState({ - password: e.target.value - }); - } + passwordTextChanged(e) { + this.setState({ + password: e.target.value, + }); + } + + confirmPasswordTextChanged(e) { + this.setState({ + confirmPassword: e.target.value, + }); + } - confirmPasswordTextChanged(e) { - this.setState({ - confirmPassword: e.target.value - }); - } + render(props) { + return ( + Modal.mount(), + }, + { + text: this.props.t("OK"), + onClick: () => { + if ( + this.state.password !== + this.state.confirmPassword + ) { + return; + } - render(props) { - return Modal.mount() - }, - { - text: this.props.t('OK'), - onClick: () => { - if (this.state.password !== this.state.confirmPassword) { - return; - } - - UserManager.setLoginDetail("newPassword", this.state.password); - UserManager.attemptLogin(); - } - } - ]}> -
- {this.props.t('LOG_IN_PASSWORD_RESET_PROMPT_1')} - {this.props.t('PASSWORD_SET_SECURITY_PROMPT')} - - -
-
- } -}); \ No newline at end of file + UserManager.setLoginDetail( + "newPassword", + this.state.password, + ); + UserManager.attemptLogin(); + }, + }, + ]} + > +
+ {this.props.t("LOG_IN_PASSWORD_RESET_PROMPT_1")} + {this.props.t("PASSWORD_SET_SECURITY_PROMPT")} + + +
+
+ ); + } + }, +); diff --git a/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyFailureModal.jsx b/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyFailureModal.jsx index d53284cc..bc1008cb 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyFailureModal.jsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyFailureModal.jsx @@ -1,23 +1,27 @@ -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import Modal from "../../Modal"; import UserManager from "../../../helpers/UserManager"; import LoginPasswordModal from "./LoginPasswordModal"; export default function LoginSecurityKeyFailureModal() { - const {t} = useTranslation(); + const { t } = useTranslation(); - return { - Modal.mount() - } - }, - { - text: t("SECURITY_KEY_RETRY_LOGIN"), - onClick: () => UserManager.attemptFido2Login() - } - ]}> - {t("SECURITY_KEY_LOGIN_FAILURE")} - -} \ No newline at end of file + return ( + { + Modal.mount(); + }, + }, + { + text: t("SECURITY_KEY_RETRY_LOGIN"), + onClick: () => UserManager.attemptFido2Login(), + }, + ]} + > + {t("SECURITY_KEY_LOGIN_FAILURE")} + + ); +} diff --git a/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyModal.jsx b/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyModal.jsx index 425fdd4d..1178edd8 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyModal.jsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginSecurityKeyModal.jsx @@ -1,12 +1,9 @@ -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import Modal from "../../Modal"; import React from "react"; -export default function LoginSecurityKeyModal({details}) { - const {t} = useTranslation(); +export default function LoginSecurityKeyModal({ details }) { + const { t } = useTranslation(); - return - {t("SECURITY_KEY_LOGIN_PROMPT")} - + return {t("SECURITY_KEY_LOGIN_PROMPT")}; } - diff --git a/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.module.css b/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.module.css index 64a54d1a..a59d95cd 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.module.css +++ b/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.module.css @@ -1,3 +1,3 @@ .password { display: none; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.tsx b/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.tsx index 942938f4..80a5fb1f 100644 --- a/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/LoginUsernameModal.tsx @@ -1,50 +1,73 @@ import Modal from "../../Modal"; -import React, {useContext, useState} from "react"; +import React, { useContext, useState } from "react"; import LoginPasswordModal from "./LoginPasswordModal"; import UserManager from "../../../helpers/UserManager"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import LineEdit from "../../LineEdit"; import CreateAccountModal from "./CreateAccountModal"; import LoadingModal from "../LoadingModal"; import Styles from "./LoginUsernameModal.module.css"; -import {ServerInformationContext} from "@/context/ServerInformationContext"; -import {VerticalSpacer} from "@/components/Layouts"; +import { ServerInformationContext } from "@/context/ServerInformationContext"; +import { VerticalSpacer } from "@/components/Layouts"; export default function LoginUsernameModal() { - const [username, setUsername] = useState(UserManager.loginDetail("username")); + const [username, setUsername] = useState( + UserManager.loginDetail("username"), + ); const [password, setPassword] = useState(""); - const {t} = useTranslation(); + const { t } = useTranslation(); const serverInformation = useContext(ServerInformationContext); - return Modal.mount() - }, - { - text: t('NEXT'), - onClick: async () => { - try { - Modal.mount() - await UserManager.setUsername(username); - if (password) await UserManager.setLoginDetail("prePassword", password); - Modal.mount() - } catch { - Modal.mount() - } - } - } - ]}> -
- {t('LOG_IN_PROMPT', {account: serverInformation.accountName})} - - setUsername((e.target as HTMLInputElement).value)} autoComplete={"off"}/> -
- setPassword((e.target as HTMLInputElement).value)}/> + return ( + Modal.mount(), + }, + { + text: t("NEXT"), + onClick: async () => { + try { + Modal.mount(); + await UserManager.setUsername(username); + if (password) + await UserManager.setLoginDetail( + "prePassword", + password, + ); + Modal.mount(); + } catch { + Modal.mount(); + } + }, + }, + ]} + > +
+ {t("LOG_IN_PROMPT", { account: serverInformation.accountName })} + + + setUsername((e.target as HTMLInputElement).value) + } + autoComplete={"off"} + /> +
+ + setPassword((e.target as HTMLInputElement).value) + } + /> +
-
- ; -} \ No newline at end of file + + ); +} diff --git a/Parlance.ClientApp/src/components/modals/account/PasswordConfirmModal.tsx b/Parlance.ClientApp/src/components/modals/account/PasswordConfirmModal.tsx index 519e56b0..cbafeda0 100644 --- a/Parlance.ClientApp/src/components/modals/account/PasswordConfirmModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/PasswordConfirmModal.tsx @@ -1,29 +1,45 @@ -import {useTranslation} from "react-i18next"; -import {VerticalLayout} from "../../Layouts"; +import { useTranslation } from "react-i18next"; +import { VerticalLayout } from "../../Layouts"; import Modal from "../../Modal"; -import {useState} from "react"; +import { useState } from "react"; import LineEdit from "../../LineEdit"; -export default function({onAccepted, onRejected}: { - onAccepted: (password: string) => void, - onRejected: () => void +export default function ({ + onAccepted, + onRejected, +}: { + onAccepted: (password: string) => void; + onRejected: () => void; }) { const [password, setPassword] = useState(""); - const {t} = useTranslation(); - - return onRejected ? onRejected() : Modal.unmount() - }, - { - text: t("OK"), - onClick: () => onAccepted(password) - } - ]}> - - {t("CONFIRM_PASSWORD_PROMPT")} - setPassword((e.target as HTMLInputElement).value)} /> - - -} \ No newline at end of file + const { t } = useTranslation(); + + return ( + + onRejected ? onRejected() : Modal.unmount(), + }, + { + text: t("OK"), + onClick: () => onAccepted(password), + }, + ]} + > + + {t("CONFIRM_PASSWORD_PROMPT")} + + setPassword((e.target as HTMLInputElement).value) + } + /> + + + ); +} diff --git a/Parlance.ClientApp/src/components/modals/account/UserModal.tsx b/Parlance.ClientApp/src/components/modals/account/UserModal.tsx index a11dc4b6..3f63e272 100644 --- a/Parlance.ClientApp/src/components/modals/account/UserModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/UserModal.tsx @@ -1,49 +1,52 @@ import Modal from "../../Modal"; import UserManager from "../../../helpers/UserManager"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import ModalList from "../../ModalList"; import { NavigateFunction } from "react-router-dom"; -export default function({navigate}: { - navigate: NavigateFunction -}) { - const {t} = useTranslation(); - +export default function ({ navigate }: { navigate: NavigateFunction }) { + const { t } = useTranslation(); + let bottomButtons = []; if (UserManager.currentUserIsSuperuser) { bottomButtons.push({ - text: t('PARLANCE_ADMINISTRATION'), + text: t("PARLANCE_ADMINISTRATION"), onClick: () => { navigate("/admin"); Modal.unmount(); - } - }) + }, + }); } - + bottomButtons.push({ text: t("ACCOUNT_SETTINGS"), onClick: () => { navigate("/account"); Modal.unmount(); - } - }) - - return Modal.unmount() }, - { - text: t('LOG_OUT'), - onClick: () => { - UserManager.logout(); - Modal.unmount(); - } - } - ]}> - {t('USER_MANAGEMENT_PROMPT', {username: UserManager.currentUser!.username})} - - {bottomButtons} - - -} \ No newline at end of file + }); + + return ( + Modal.unmount(), + }, + { + text: t("LOG_OUT"), + onClick: () => { + UserManager.logout(); + Modal.unmount(); + }, + }, + ]} + > + {t("USER_MANAGEMENT_PROMPT", { + username: UserManager.currentUser!.username, + })} + {bottomButtons} + + ); +} diff --git a/Parlance.ClientApp/src/components/modals/account/resets/EmailResetModal.jsx b/Parlance.ClientApp/src/components/modals/account/resets/EmailResetModal.jsx index 5c583c67..e320abe9 100644 --- a/Parlance.ClientApp/src/components/modals/account/resets/EmailResetModal.jsx +++ b/Parlance.ClientApp/src/components/modals/account/resets/EmailResetModal.jsx @@ -2,43 +2,61 @@ import React from "react"; import Modal from "../../../Modal"; import PasswordResetModal from "./PasswordResetModal"; import UserManager from "../../../../helpers/UserManager"; -import {withTranslation} from "react-i18next"; +import { withTranslation } from "react-i18next"; import LineEdit from "../../../../components/LineEdit"; -import {VerticalSpacer} from "../../../../components/Layouts"; +import { VerticalSpacer } from "../../../../components/Layouts"; -export default withTranslation()(class EmailResetModal extends React.Component { - constructor(props) { - super(props); +export default withTranslation()( + class EmailResetModal extends React.Component { + constructor(props) { + super(props); - this.state = { - email: "" + this.state = { + email: "", + }; } - } - emailTextChanged(e) { - this.setState({ - email: e.target.value - }); - } + emailTextChanged(e) { + this.setState({ + email: e.target.value, + }); + } - render(props) { - return Modal.mount() - }, - { - text: this.props.t('RESET_PASSWORD'), - onClick: () => UserManager.performPasswordReset("email", { - email: this.state.email - }) - } - ]}> -
- {this.props.t('PASSWORD_RECOVERY_EMAIL_PROMPT_1')} - - -
-
- } -}) \ No newline at end of file + render(props) { + return ( + + Modal.mount( + , + ), + }, + { + text: this.props.t("RESET_PASSWORD"), + onClick: () => + UserManager.performPasswordReset("email", { + email: this.state.email, + }), + }, + ]} + > +
+ {this.props.t("PASSWORD_RECOVERY_EMAIL_PROMPT_1")} + + +
+
+ ); + } + }, +); diff --git a/Parlance.ClientApp/src/components/modals/account/resets/PasswordResetModal.jsx b/Parlance.ClientApp/src/components/modals/account/resets/PasswordResetModal.jsx index deca9999..57e28d0e 100644 --- a/Parlance.ClientApp/src/components/modals/account/resets/PasswordResetModal.jsx +++ b/Parlance.ClientApp/src/components/modals/account/resets/PasswordResetModal.jsx @@ -3,33 +3,45 @@ import Modal from "../../../Modal"; import ModalList from "../../../ModalList"; import EmailResetModal from "./EmailResetModal"; import LoginPasswordModal from "../LoginPasswordModal"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; export default function PasswordResetModal(props) { - let {t} = useTranslation(); - - return Modal.mount() - } - ]}> -
- {t('PASSWORD_RECOVERY_PROMPT_1')} -
- - {props.resetMethods.map(method => { - switch (method.type) { - case "email": - return { - text: t("PASSWORD_RECOVERY_EMAIL", {email: `${method.user}∙∙∙@${method.domain}∙∙∙`}), - onClick: () => Modal.mount() - } - default: - return null; - } - })} - -
-} \ No newline at end of file + let { t } = useTranslation(); + + return ( + Modal.mount(), + }, + ]} + > +
+ {t("PASSWORD_RECOVERY_PROMPT_1")} +
+ + {props.resetMethods.map(method => { + switch (method.type) { + case "email": + return { + text: t("PASSWORD_RECOVERY_EMAIL", { + email: `${method.user}∙∙∙@${method.domain}∙∙∙`, + }), + onClick: () => + Modal.mount( + , + ), + }; + default: + return null; + } + })} + +
+ ); +} diff --git a/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyAdvertisement.module.css b/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyAdvertisement.module.css index e7c8cf30..f40c3cd4 100644 --- a/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyAdvertisement.module.css +++ b/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyAdvertisement.module.css @@ -1,9 +1,10 @@ .FeatureBox { display: grid; - grid-template-areas: "icon heading" - ". content"; + grid-template-areas: + "icon heading" + ". content"; grid-template-columns: max-content 1fr; - + background: var(--border-color); padding: 9px; border-radius: 9px; @@ -25,4 +26,4 @@ gap: 6px; padding-top: 10px; padding-bottom: 10px; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyAdvertisement.tsx b/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyAdvertisement.tsx index 72ec8f13..c42a0bcd 100644 --- a/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyAdvertisement.tsx +++ b/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyAdvertisement.tsx @@ -1,92 +1,126 @@ -import React, {ReactNode} from "react"; +import React, { ReactNode } from "react"; import Modal from "../../../Modal"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import ModalList from "../../../ModalList"; import RegisterSecurityKeyModal from "./RegisterSecurityKeyModal"; import moment from "moment"; -import Styles from "./RegisterSecurityKeyAdvertisement.module.css" +import Styles from "./RegisterSecurityKeyAdvertisement.module.css"; interface RegisterSecurityKeyAdvertisementProps { - password: string + password: string; } interface FeatureBoxProps { - heading: string - children: ReactNode + heading: string; + children: ReactNode; } function SecurityKeySetupCompleteModal() { - const {t} = useTranslation(); + const { t } = useTranslation(); - return - {t("SECURITY_KEY_ADVERTISEMENT_SETUP_SUCCESS")} -
-
- {t("SECURITY_KEY_ADVERTISEMENT_SETUP_SUCCESS_2")} -
-
- {t("SECURITY_KEY_ADVERTISEMENT_SETUP_SUCCESS_3")} -
+ return ( + + {t("SECURITY_KEY_ADVERTISEMENT_SETUP_SUCCESS")} +
+
+ {t("SECURITY_KEY_ADVERTISEMENT_SETUP_SUCCESS_2")} +
+
+ {t("SECURITY_KEY_ADVERTISEMENT_SETUP_SUCCESS_3")} +
+ ); } -function FeatureBox({heading, children}: FeatureBoxProps) { - return
- {heading} - {children} -
+function FeatureBox({ heading, children }: FeatureBoxProps) { + return ( +
+ {heading} + {children} +
+ ); } -export function RegisterSecurityKeyAdvertisement({password}: RegisterSecurityKeyAdvertisementProps) { - const {t} = useTranslation(); - - return { - localStorage.setItem("passkey-advertisement-never-ask", "true"); - Modal.unmount(); - }, - destructive: true - }]}> - {t("SECURITY_KEY_ADVERTISEMENT")} -
- - {t("SECURITY_KEY_ADVERTISEMENT_FEATURE_1_CONTENT")} - - - {t("SECURITY_KEY_ADVERTISEMENT_FEATURE_2_CONTENT")} - - - {t("SECURITY_KEY_ADVERTISEMENT_FEATURE_3_CONTENT")} - - - {t("SECURITY_KEY_ADVERTISEMENT_FEATURE_4_CONTENT")} - -
- {t("SECURITY_KEY_ADVERTISEMENT_2")} - - {[ - { - text: t("SECURITY_KEY_ADVERTISEMENT_OK"), - onClick: () => { - const onDone = () => { - Modal.mount() - } - Modal.mount() - } - }, +export function RegisterSecurityKeyAdvertisement({ + password, +}: RegisterSecurityKeyAdvertisementProps) { + const { t } = useTranslation(); + + return ( + { + localStorage.setItem( + "passkey-advertisement-never-ask", + "true", + ); Modal.unmount(); - } - } + }, + destructive: true, + }, ]} - -
-} \ No newline at end of file + > + {t("SECURITY_KEY_ADVERTISEMENT")} +
+ + {t("SECURITY_KEY_ADVERTISEMENT_FEATURE_1_CONTENT")} + + + {t("SECURITY_KEY_ADVERTISEMENT_FEATURE_2_CONTENT")} + + + {t("SECURITY_KEY_ADVERTISEMENT_FEATURE_3_CONTENT")} + + + {t("SECURITY_KEY_ADVERTISEMENT_FEATURE_4_CONTENT")} + +
+ {t("SECURITY_KEY_ADVERTISEMENT_2")} + + {[ + { + text: t("SECURITY_KEY_ADVERTISEMENT_OK"), + onClick: () => { + const onDone = () => { + Modal.mount(); + }; + Modal.mount( + , + ); + }, + }, + { + text: t("SECURITY_KEY_ADVERTISEMENT_LATER"), + onClick: () => { + Modal.unmount(); + }, + }, + ]} + + + ); +} diff --git a/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyModal.tsx b/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyModal.tsx index b82772d9..9b822554 100644 --- a/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyModal.tsx +++ b/Parlance.ClientApp/src/components/modals/account/securityKeys/RegisterSecurityKeyModal.tsx @@ -1,46 +1,66 @@ import Modal from "../../../Modal"; -import {VerticalLayout} from "../../../Layouts"; +import { VerticalLayout } from "../../../Layouts"; import LineEdit from "../../../LineEdit"; -import React, {useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import Fetch from "../../../../helpers/Fetch"; import LoadingModal from "../../LoadingModal"; -import {decode, encode} from "../../../../helpers/Base64"; +import { decode, encode } from "../../../../helpers/Base64"; let pendingSecurityCredential = false; interface RegisterSecurityKeyModalProps { - initialName: string - type: string - password: string - onDone: () => void + initialName: string; + type: string; + password: string; + onDone: () => void; } interface RegisterSecurityKeyInnerModalProps { - nickname: string - type: string - password: string - onDone: () => void + nickname: string; + type: string; + password: string; + onDone: () => void; } -function BrowserRegisterSecurityKeyModalFailure({nickname, type, password, onDone}: RegisterSecurityKeyInnerModalProps) { - const {t} = useTranslation(); - - return { - Modal.mount() - } - } - ]}> - {t("SECURITY_KEY_ADD_ERROR_PROMPT")} - +function BrowserRegisterSecurityKeyModalFailure({ + nickname, + type, + password, + onDone, +}: RegisterSecurityKeyInnerModalProps) { + const { t } = useTranslation(); + + return ( + { + Modal.mount( + , + ); + }, + }, + ]} + > + {t("SECURITY_KEY_ADD_ERROR_PROMPT")} + + ); } -function RequestBrowserRegisterSecurityKeyModal({nickname, type, password, onDone}: RegisterSecurityKeyInnerModalProps) { +function RequestBrowserRegisterSecurityKeyModal({ + nickname, + type, + password, + onDone, +}: RegisterSecurityKeyInnerModalProps) { useEffect(() => { (async () => { if (pendingSecurityCredential) return; @@ -49,80 +69,126 @@ function RequestBrowserRegisterSecurityKeyModal({nickname, type, password, onDon pendingSecurityCredential = true; //Request details from server - let credDetails = await Fetch.post("/api/user/keys/prepareregister", { - password: password, - authenticatorAttachmentType: type - }); - - - let credential = await navigator.credentials.create({ + let credDetails = await Fetch.post( + "/api/user/keys/prepareregister", + { + password: password, + authenticatorAttachmentType: type, + }, + ); + + let credential = (await navigator.credentials.create({ publicKey: { - challenge: decode(credDetails.authenticatorOptions.challenge), + challenge: decode( + credDetails.authenticatorOptions.challenge, + ), rp: credDetails.authenticatorOptions.rp, user: { - id: decode(credDetails.authenticatorOptions.user.id), + id: decode( + credDetails.authenticatorOptions.user.id, + ), name: credDetails.authenticatorOptions.user.name, - displayName: credDetails.authenticatorOptions.user.displayName, + displayName: + credDetails.authenticatorOptions.user + .displayName, }, - pubKeyCredParams: credDetails.authenticatorOptions.pubKeyCredParams, - authenticatorSelection: credDetails.authenticatorOptions.authenticatorSelection, + pubKeyCredParams: + credDetails.authenticatorOptions.pubKeyCredParams, + authenticatorSelection: + credDetails.authenticatorOptions + .authenticatorSelection, timeout: credDetails.authenticatorOptions.timeout, - attestation: credDetails.authenticatorOptions.attestation - } - }) as PublicKeyCredential; + attestation: + credDetails.authenticatorOptions.attestation, + }, + })) as PublicKeyCredential; - Modal.mount() + Modal.mount(); let response = await Fetch.post("/api/user/keys/register", { password: password, id: credDetails.id, name: nickname, response: { - authenticatorAttachment: credential.authenticatorAttachment, + authenticatorAttachment: + credential.authenticatorAttachment, id: credential.id, rawId: encode(credential.rawId), response: { - attestationObject: encode((credential.response as AuthenticatorAttestationResponse).attestationObject), - clientDataJSON: encode(credential.response.clientDataJSON) + attestationObject: encode( + ( + credential.response as AuthenticatorAttestationResponse + ).attestationObject, + ), + clientDataJSON: encode( + credential.response.clientDataJSON, + ), }, - type: credential.type - } + type: credential.type, + }, }); - + Modal.unmount(); await onDone(); } catch (e) { console.log(e); - Modal.mount() + Modal.mount( + , + ); } finally { pendingSecurityCredential = false; } })(); }, []); - const {t} = useTranslation(); + const { t } = useTranslation(); - return - {t("SECURITY_KEY_ADD_PROMPT")} - + return {t("SECURITY_KEY_ADD_PROMPT")}; } -export default function RegisterSecurityKeyModal({type, password, onDone, initialName = ""}: RegisterSecurityKeyModalProps) { +export default function RegisterSecurityKeyModal({ + type, + password, + onDone, + initialName = "", +}: RegisterSecurityKeyModalProps) { const [securityKeyName, setSecurityKeyName] = useState(initialName); - const {t} = useTranslation(); - - - return { - Modal.mount() - } - }]}> - -
{t('SECURITY_KEY_ADD_NAME')}
- setSecurityKeyName((e.target as HTMLInputElement).value)}/> -
-
-} \ No newline at end of file + const { t } = useTranslation(); + + return ( + { + Modal.mount( + , + ); + }, + }, + ]} + > + +
{t("SECURITY_KEY_ADD_NAME")}
+ + setSecurityKeyName((e.target as HTMLInputElement).value) + } + /> +
+
+ ); +} diff --git a/Parlance.ClientApp/src/components/modals/glossary/AddToGlossaryModal.module.css b/Parlance.ClientApp/src/components/modals/glossary/AddToGlossaryModal.module.css index 293b1ce2..b98ecb06 100644 --- a/Parlance.ClientApp/src/components/modals/glossary/AddToGlossaryModal.module.css +++ b/Parlance.ClientApp/src/components/modals/glossary/AddToGlossaryModal.module.css @@ -13,4 +13,4 @@ .termBox { flex-grow: 1; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/modals/glossary/AddToGlossaryModal.tsx b/Parlance.ClientApp/src/components/modals/glossary/AddToGlossaryModal.tsx index 2c61ec0b..6fc146e3 100644 --- a/Parlance.ClientApp/src/components/modals/glossary/AddToGlossaryModal.tsx +++ b/Parlance.ClientApp/src/components/modals/glossary/AddToGlossaryModal.tsx @@ -1,12 +1,21 @@ -import {ReactElement, useId, useState} from "react"; +import { ReactElement, useId, useState } from "react"; import Modal from "../../Modal"; -import {Trans, useTranslation} from "react-i18next"; -import {HorizontalLayout, VerticalLayout, VerticalSpacer} from "../../Layouts"; +import { Trans, useTranslation } from "react-i18next"; +import { + HorizontalLayout, + VerticalLayout, + VerticalSpacer, +} from "../../Layouts"; import LineEdit from "../../LineEdit"; -import {Glossary, GlossaryItem, PartOfSpeech, PartOfSpeechTranslationString} from "../../../interfaces/glossary"; +import { + Glossary, + GlossaryItem, + PartOfSpeech, + PartOfSpeechTranslationString, +} from "../../../interfaces/glossary"; import SelectableList from "../../SelectableList"; -import Styles from "./AddToGlossaryModal.module.css" +import Styles from "./AddToGlossaryModal.module.css"; import Icon from "../../Icon"; import PageHeading from "../../PageHeading"; import I18n from "../../../helpers/i18n"; @@ -21,87 +30,155 @@ interface AddToGlossaryModalProps { onGlossaryItemAdded: (item: GlossaryItem) => void; } -export default function AddToGlossaryModal({initialTerm, connectedGlossaries, language, onGlossaryItemAdded}: AddToGlossaryModalProps): ReactElement { +export default function AddToGlossaryModal({ + initialTerm, + connectedGlossaries, + language, + onGlossaryItemAdded, +}: AddToGlossaryModalProps): ReactElement { const [term, setTerm] = useState(initialTerm || ""); const [pos, setPos] = useState(PartOfSpeech.Unknown); const [translation, setTranslation] = useState(""); - const [addGlossary, setAddGlossary] = useState(connectedGlossaries[0]); + const [addGlossary, setAddGlossary] = useState( + connectedGlossaries[0], + ); const [regionAgnostic, setRegionAgnostic] = useState(true); const [error, setError] = useState(); const regionAgnosticCheckboxId = useId(); - const {t} = useTranslation(); - - const selectedLanguage = () => regionAgnostic ? language.substring(0, 2) : language; - + const { t } = useTranslation(); + + const selectedLanguage = () => + regionAgnostic ? language.substring(0, 2) : language; + const addToGlossary = async () => { if (!term) { setError(t("ADD_TO_GLOSSARY_ERROR_NO_TERM")); return; } - + if (!translation) { setError(t("ADD_TO_GLOSSARY_ERROR_NO_TRANSLATION")); return; } - + try { Modal.unmount(); - - await Fetch.post(`/api/glossarymanager/${addGlossary.id}/${selectedLanguage()}`, { - term: term, - translation: translation, - partOfSpeech: pos - }) + + await Fetch.post( + `/api/glossarymanager/${addGlossary.id}/${selectedLanguage()}`, + { + term: term, + translation: translation, + partOfSpeech: pos, + }, + ); onGlossaryItemAdded({ term: term, translation: translation, partOfSpeech: pos, id: "", - lang: selectedLanguage() - }) + lang: selectedLanguage(), + }); } catch (e) { - Modal.mount() + Modal.mount(); } }; - - return - - -
- setTerm((e.target as HTMLInputElement).value)} /> -
- -
- setTranslation((e.target as HTMLInputElement).value)} /> - {I18n.isRegionAgnostic(language) ||
- setRegionAgnostic((e.target as HTMLInputElement).checked)} /> - -
} - {connectedGlossaries.length > 1 && <> - - {t("GLOSSARY")} - ({ - contents:
- - {glossary.name} -
, - onClick: () => setAddGlossary(glossary) - }))} /> - } - -
-
+ + return ( + + + +
+ + setTerm((e.target as HTMLInputElement).value) + } + /> +
+ +
+ + setTranslation((e.target as HTMLInputElement).value) + } + /> + {I18n.isRegionAgnostic(language) || ( +
+ + setRegionAgnostic( + (e.target as HTMLInputElement).checked, + ) + } + /> + +
+ )} + {connectedGlossaries.length > 1 && ( + <> + + {t("GLOSSARY")} + ({ + contents: ( +
+ + {glossary.name} +
+ ), + onClick: () => setAddGlossary(glossary), + }))} + /> + + )} + +
+
+ ); } diff --git a/Parlance.ClientApp/src/components/modals/glossary/SearchGlossaryModal.module.css b/Parlance.ClientApp/src/components/modals/glossary/SearchGlossaryModal.module.css index b364476b..7fbb44b7 100644 --- a/Parlance.ClientApp/src/components/modals/glossary/SearchGlossaryModal.module.css +++ b/Parlance.ClientApp/src/components/modals/glossary/SearchGlossaryModal.module.css @@ -8,7 +8,7 @@ input.searchBox { display: grid; grid-template-columns: 1fr 1fr min-content; grid-gap: 1px; - + background: var(--border-color); border-top: 1px solid var(--border-color); } @@ -25,4 +25,4 @@ input.searchBox { .partOfSpeech { margin-left: 6px; color: var(--foreground-disabled-color); -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/components/modals/glossary/SearchGlossaryModal.tsx b/Parlance.ClientApp/src/components/modals/glossary/SearchGlossaryModal.tsx index 3eca65e3..d5997fc0 100644 --- a/Parlance.ClientApp/src/components/modals/glossary/SearchGlossaryModal.tsx +++ b/Parlance.ClientApp/src/components/modals/glossary/SearchGlossaryModal.tsx @@ -1,64 +1,117 @@ import Modal from "../../Modal"; -import {useTranslation} from "react-i18next"; -import {GlossaryItem, PartOfSpeech, PartOfSpeechTranslationString} from "../../../interfaces/glossary"; +import { useTranslation } from "react-i18next"; +import { + GlossaryItem, + PartOfSpeech, + PartOfSpeechTranslationString, +} from "../../../interfaces/glossary"; -import Styles from "./SearchGlossaryModal.module.css" -import {useState} from "react"; +import Styles from "./SearchGlossaryModal.module.css"; +import { useState } from "react"; import I18n from "../../../helpers/i18n"; import SmallButton from "../../SmallButton"; interface SearchGlossaryModalProps { - language: string - glossaryData: GlossaryItem[] + language: string; + glossaryData: GlossaryItem[]; } interface GlossaryTableProps { - language: string + language: string; glossaryData: GlossaryItem[]; - searchQuery: string + searchQuery: string; } -function GlossaryTable({language, glossaryData, searchQuery}: GlossaryTableProps) { - const {t} = useTranslation(); - - return
- {t("Term")} - {t("Translation")} - - - {glossaryData.filter(x => x.term.toLowerCase().includes(searchQuery.toLowerCase())).sort().map(glossaryItem => { - const onCopy = async () => { - await navigator.clipboard.writeText(glossaryItem.translation); - Modal.unmount(); - } - - return <> -
- {glossaryItem.term} - {glossaryItem.partOfSpeech !== PartOfSpeech.Unknown && {t(PartOfSpeechTranslationString(glossaryItem.partOfSpeech))}} -
- {glossaryItem.translation} -
- {t("Copy")} -
- ; - })} -
+function GlossaryTable({ + language, + glossaryData, + searchQuery, +}: GlossaryTableProps) { + const { t } = useTranslation(); + + return ( +
+ {t("Term")} + {t("Translation")} + + + {glossaryData + .filter(x => + x.term.toLowerCase().includes(searchQuery.toLowerCase()), + ) + .sort() + .map(glossaryItem => { + const onCopy = async () => { + await navigator.clipboard.writeText( + glossaryItem.translation, + ); + Modal.unmount(); + }; + + return ( + <> +
+ {glossaryItem.term} + {glossaryItem.partOfSpeech !== + PartOfSpeech.Unknown && ( + + {t( + PartOfSpeechTranslationString( + glossaryItem.partOfSpeech, + ), + )} + + )} +
+ + {glossaryItem.translation} + +
+ + {t("Copy")} + +
+ + ); + })} +
+ ); } -export default function SearchGlossaryModal({language, glossaryData}: SearchGlossaryModalProps) { +export default function SearchGlossaryModal({ + language, + glossaryData, +}: SearchGlossaryModalProps) { const [searchQuery, setSearchQuery] = useState(""); - const {t} = useTranslation(); - - return Modal.unmount() - } - ]} topComponent={<> - setSearchQuery((e.target as HTMLInputElement).value)}/> - - } /> + const { t } = useTranslation(); + + return ( + Modal.unmount(), + }, + ]} + topComponent={ + <> + + setSearchQuery((e.target as HTMLInputElement).value) + } + /> + + + } + /> + ); } diff --git a/Parlance.ClientApp/src/components/notifications/Events.tsx b/Parlance.ClientApp/src/components/notifications/Events.tsx index 2f784f7f..25bb7d21 100644 --- a/Parlance.ClientApp/src/components/notifications/Events.tsx +++ b/Parlance.ClientApp/src/components/notifications/Events.tsx @@ -1,64 +1,90 @@ -import {AutoSubscription, AutoSubscriptionEventName, Subscription, SubscriptionChannelName} from "@/interfaces/unsubscribe"; -import {useTranslation} from "react-i18next"; +import { + AutoSubscription, + AutoSubscriptionEventName, + Subscription, + SubscriptionChannelName, +} from "@/interfaces/unsubscribe"; +import { useTranslation } from "react-i18next"; -export function UnsubscribeEvent({subscription}: { - subscription: Subscription +export function UnsubscribeEvent({ + subscription, +}: { + subscription: Subscription; }) { - const {t} = useTranslation(); - + const { t } = useTranslation(); + switch (subscription.type) { case "TranslationFreeze": - return {t("Translation Freeze for project {{project}}", { project: subscription.projectName })} + return ( + + {t("Translation Freeze for project {{project}}", { + project: subscription.projectName, + })} + + ); } } -export function AutoSubscribeEvent({autoSubscribeEvent}: { - autoSubscribeEvent: AutoSubscription +export function AutoSubscribeEvent({ + autoSubscribeEvent, +}: { + autoSubscribeEvent: AutoSubscription; }) { - const {t} = useTranslation(); - + const { t } = useTranslation(); + switch (autoSubscribeEvent.type) { case "TranslationSubmit": - return {t("Submit translations")} + return {t("Submit translations")}; } } export enum NotificationChannelType { - Name + Name, } -export function notificationChannelText(channel: SubscriptionChannelName, type: NotificationChannelType) { +export function notificationChannelText( + channel: SubscriptionChannelName, + type: NotificationChannelType, +) { const data: Record = { - "TranslationFreeze": ["SUBSCRIPTION_CHANNEL_NAME_TRANSLATION_FREEZE"] - } + TranslationFreeze: ["SUBSCRIPTION_CHANNEL_NAME_TRANSLATION_FREEZE"], + }; return data[channel][type]; } -export function NotificationChannel({channel, type}: { - channel: SubscriptionChannelName - type: NotificationChannelType +export function NotificationChannel({ + channel, + type, +}: { + channel: SubscriptionChannelName; + type: NotificationChannelType; }) { - const {t} = useTranslation(); - - return t(notificationChannelText(channel, type)) + const { t } = useTranslation(); + + return t(notificationChannelText(channel, type)); } export enum AutoSubscriptionEventType { Name, - PresentTenseAction + PresentTenseAction, } -export function AutoSubscriptionEvent({event, type}: { - event: AutoSubscriptionEventName, - type: AutoSubscriptionEventType +export function AutoSubscriptionEvent({ + event, + type, +}: { + event: AutoSubscriptionEventName; + type: AutoSubscriptionEventType; }) { - - const {t} = useTranslation(); + const { t } = useTranslation(); const data: Record = { - "TranslationSubmit": ["AUTO_SUBSCRIPTION_EVENT_NAME_TRANSLATION_SUBMIT", "AUTO_SUBSCRIPTION_EVENT_PRESENT_TENSE_ACTION_TRANSLATION_SUBMIT"] - } + TranslationSubmit: [ + "AUTO_SUBSCRIPTION_EVENT_NAME_TRANSLATION_SUBMIT", + "AUTO_SUBSCRIPTION_EVENT_PRESENT_TENSE_ACTION_TRANSLATION_SUBMIT", + ], + }; return t(data[event][type]); -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/context/ServerInformationContext.tsx b/Parlance.ClientApp/src/context/ServerInformationContext.tsx index 3fe6bc52..7f6389cf 100644 --- a/Parlance.ClientApp/src/context/ServerInformationContext.tsx +++ b/Parlance.ClientApp/src/context/ServerInformationContext.tsx @@ -1,33 +1,40 @@ -import {ReactNode, createContext, useEffect, useState } from "react"; +import { ReactNode, createContext, useEffect, useState } from "react"; import Fetch from "@/helpers/Fetch"; export interface ServerInformation { - serverName: string - accountName: string + serverName: string; + accountName: string; } export const ServerInformationContext = createContext({ serverName: "", - accountName: "" + accountName: "", }); -export function ServerInformationProvider({children}: { - children: ReactNode +export function ServerInformationProvider({ + children, +}: { + children: ReactNode; }) { // @ts-ignore - const [serverInformation, setServerInformation] = useState(null) + const [serverInformation, setServerInformation] = + useState(null); useEffect(() => { (async () => { - setServerInformation(await Fetch.get("/api/serverinformation")); + setServerInformation( + await Fetch.get("/api/serverinformation"), + ); })(); }, []); - + if (!serverInformation) { return null; } - - return - {children} - + + return ( + + {children} + + ); } diff --git a/Parlance.ClientApp/src/custom.css b/Parlance.ClientApp/src/custom.css index aa1988f9..69ceb3bf 100644 --- a/Parlance.ClientApp/src/custom.css +++ b/Parlance.ClientApp/src/custom.css @@ -1,10 +1,11 @@ -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap"); @font-face { - font-family: 'Contemporary'; + font-family: "Contemporary"; font-style: normal; font-weight: 400; - src: local('Contemporary'), local('Contemporary-Regular'), url('https://vicr123.com/typeface/Contemporary-Regular.ttf'); + src: local("Contemporary"), local("Contemporary-Regular"), + url("https://vicr123.com/typeface/Contemporary-Regular.ttf"); } body { @@ -40,16 +41,16 @@ body { --preloading-block-color-1: rgba(255, 255, 255, 0.3); --preloading-block-color-2: rgba(255, 255, 255, 0.4); - + --layer-color: rgba(255, 255, 255, calc(10 / 255)); - + --border-radius: 4px; - + --focus-decoration-start: rgb(20, 125, 200); --focus-decoration-end: rgb(20, 160, 255); - --fixed-font: 'JetBrains Mono', monospace; - --standard-font: 'Contemporary', sans-serif; + --fixed-font: "JetBrains Mono", monospace; + --standard-font: "Contemporary", sans-serif; background-color: var(--background-color); color: var(--foreground-color); @@ -78,7 +79,7 @@ body { --preloading-block-color-1: rgba(0, 0, 0, 0.3); --preloading-block-color-2: rgba(0, 0, 0, 0.4); - + --layer-color: rgba(0, 0, 0, calc(20 / 255)); } } @@ -91,7 +92,8 @@ code { font-family: var(--fixed-font); } -input[type=text], input[type=password] { +input[type="text"], +input[type="password"] { background-color: transparent; color: var(--foreground-color); border: 1px solid var(--border-color); @@ -109,4 +111,4 @@ hr { * { scrollbar-width: thin; scrollbar-color: var(--active-color) transparent; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/helpers/Base64.ts b/Parlance.ClientApp/src/helpers/Base64.ts index c40aac8e..b74252c0 100644 --- a/Parlance.ClientApp/src/helpers/Base64.ts +++ b/Parlance.ClientApp/src/helpers/Base64.ts @@ -1,9 +1,9 @@ function decode64(str: string) { - return Uint8Array.from(atob(str), c => c.charCodeAt(0)) + return Uint8Array.from(atob(str), c => c.charCodeAt(0)); } function encode64(buf: Uint8Array) { - return btoa(String.fromCharCode(...new Uint8Array(buf))) + return btoa(String.fromCharCode(...new Uint8Array(buf))); } function decode(str: string) { @@ -11,10 +11,9 @@ function decode(str: string) { } function encode(buf: ArrayBuffer) { - return encode64(new Uint8Array(buf)).replaceAll("+", "-").replaceAll("/", "_"); + return encode64(new Uint8Array(buf)) + .replaceAll("+", "-") + .replaceAll("/", "_"); } -export { - decode, - encode -} +export { decode, encode }; diff --git a/Parlance.ClientApp/src/helpers/Fetch.ts b/Parlance.ClientApp/src/helpers/Fetch.ts index 65eb4e47..173c3138 100644 --- a/Parlance.ClientApp/src/helpers/Fetch.ts +++ b/Parlance.ClientApp/src/helpers/Fetch.ts @@ -4,8 +4,8 @@ class Fetch { * Define and create custom headers for Fetch requests */ static headers() { - let headers: {[key: string]: string} = { - "Content-Type": "application/json" + let headers: { [key: string]: string } = { + "Content-Type": "application/json", }; let token = window.localStorage.getItem("token"); @@ -20,33 +20,35 @@ class Fetch { * @param {string} url endpoint for the API call * @param {function} resultCallback Callback to call when the result is ready containing raw fetch data */ - static async performRequest(method: string, url: string, resultCallback = (result: WebFetchResponse) => {}) : Promise> { + static async performRequest( + method: string, + url: string, + resultCallback = (result: WebFetchResponse) => {}, + ): Promise> { let err = null; // Display loading animation for the user let headers = Fetch.headers(); - let result = await fetch(url, { + let result = (await fetch(url, { method: method, - headers: headers - }).catch((error) => { - err = error; - }).finally(() => { - - }) as WebFetchResponse; + headers: headers, + }) + .catch(error => { + err = error; + }) + .finally(() => {})) as WebFetchResponse; if (err) throw err; if (result.status < 200 || result.status > 299) { try { result.jsonBody = await result.json(); - } catch { - - } + } catch {} throw result; } resultCallback(result); if (result.status === 204) return {}; - return await result.json() as T; + return (await result.json()) as T; } /** @@ -56,20 +58,25 @@ class Fetch { * @param {Object} headers Headers to include * @param resultCallback Callback to call after result is available but before the JSON is retrieved */ - static async post(url: string, data: any, headers = {}, resultCallback = (result: WebFetchResponse) => {}): Promise { + static async post( + url: string, + data: any, + headers = {}, + resultCallback = (result: WebFetchResponse) => {}, + ): Promise { let err = null; - let result = await fetch(url, { + let result = (await fetch(url, { method: "POST", headers: { ...headers, - ...Fetch.headers() + ...Fetch.headers(), }, - body: JSON.stringify(data) - }).catch((error) => { - err = error; - }).finally(() => { - - }) as WebFetchResponse; + body: JSON.stringify(data), + }) + .catch(error => { + err = error; + }) + .finally(() => {})) as WebFetchResponse; if (err) throw err; if (result.status < 200 || result.status > 299) throw result; @@ -86,14 +93,15 @@ class Fetch { */ static async patch(url: string, data: any): Promise> { let err = null; - let result = await fetch(`/api${url}`, { + let result = (await fetch(`/api${url}`, { method: "PATCH", headers: Fetch.headers(), - body: JSON.stringify(data) - }).catch((error) => { - err = error; - }).finally(() => { - }) as WebFetchResponse; + body: JSON.stringify(data), + }) + .catch(error => { + err = error; + }) + .finally(() => {})) as WebFetchResponse; if (err) throw err; if (result.status === 204) return {}; @@ -106,7 +114,7 @@ class Fetch { * @param {string} url url to perform API request * @param {function} resultCallback Callback to call when the result is ready containing raw fetch data */ - static get(url: string, resultCallback = () => {}) : Promise { + static get(url: string, resultCallback = () => {}): Promise { return Fetch.performRequest("GET", url, resultCallback) as Promise; } diff --git a/Parlance.ClientApp/src/helpers/Hooks.js b/Parlance.ClientApp/src/helpers/Hooks.js index 5f3d3afd..c5d43cb3 100644 --- a/Parlance.ClientApp/src/helpers/Hooks.js +++ b/Parlance.ClientApp/src/helpers/Hooks.js @@ -1,4 +1,4 @@ -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import UserManager from "./UserManager"; function useForceUpdate() { @@ -17,8 +17,8 @@ function useUserUpdateEffect(callback, deps) { return () => { UserManager.off("currentUserChanged", callback); - } + }; }, deps); } -export {useForceUpdate, useForceUpdateOnUserChange, useUserUpdateEffect} \ No newline at end of file +export { useForceUpdate, useForceUpdateOnUserChange, useUserUpdateEffect }; diff --git a/Parlance.ClientApp/src/helpers/Misc.js b/Parlance.ClientApp/src/helpers/Misc.js index becb43c7..c5e78ec4 100644 --- a/Parlance.ClientApp/src/helpers/Misc.js +++ b/Parlance.ClientApp/src/helpers/Misc.js @@ -8,16 +8,16 @@ function calculateDeadline(deadline) { text: dl.fromNow(true), ms: moment.duration(dl.diff(moment())), date: dl.format("LL"), - valid: true - } + valid: true, + }; } } return { text: "", ms: 999999999999999, - valid: false - } + valid: false, + }; } -export {calculateDeadline} \ No newline at end of file +export { calculateDeadline }; diff --git a/Parlance.ClientApp/src/helpers/UserManager.tsx b/Parlance.ClientApp/src/helpers/UserManager.tsx index 76374e4c..450b882e 100644 --- a/Parlance.ClientApp/src/helpers/UserManager.tsx +++ b/Parlance.ClientApp/src/helpers/UserManager.tsx @@ -18,17 +18,17 @@ import React from "react"; import LoginSecurityKeyModal from "../components/modals/account/LoginSecurityKeyModal"; // @ts-ignore import LoginSecurityKeyFailureModal from "../components/modals/account/LoginSecurityKeyFailureModal"; -import {decode, encode} from "./Base64"; +import { decode, encode } from "./Base64"; // @ts-ignore +import { RegisterSecurityKeyAdvertisement } from "../components/modals/account/securityKeys/RegisterSecurityKeyAdvertisement"; import { - RegisterSecurityKeyAdvertisement -} from "../components/modals/account/securityKeys/RegisterSecurityKeyAdvertisement"; -import { - LoginType, PasswordResetChallenge, PasswordResetType, + LoginType, + PasswordResetChallenge, + PasswordResetType, TokenResponseFido, TokenResponseFidoOptionsCredentials, TokenResponseToken, - User + User, } from "../interfaces/users"; class UserManager extends EventEmitter { @@ -42,7 +42,9 @@ class UserManager extends EventEmitter { this.#currentUser = null; this.updateDetails(); - i18n.on("languageChanged", () => this.emit("currentUserChanged", this.#currentUser)); + i18n.on("languageChanged", () => + this.emit("currentUserChanged", this.#currentUser), + ); } get isLoggedIn() { @@ -76,7 +78,7 @@ class UserManager extends EventEmitter { } catch { //Couldn't get user details, so log out await this.logout(); - Modal.mount() + Modal.mount(); } } else { this.#currentUser = null; @@ -99,29 +101,44 @@ class UserManager extends EventEmitter { async setUsername(username: string) { this.#availableLoginTypes = await Fetch.post("/api/user/tokentypes", { - username: username + username: username, }); this.setLoginDetail("username", username); } - async attemptLogin({fido2Details = null} = {}) { - Modal.mount() + async attemptLogin({ fido2Details = null } = {}) { + Modal.mount(); try { - let response = await Fetch.post(`/api/user/token`, this.#loginSessionDetails); + let response = await Fetch.post( + `/api/user/token`, + this.#loginSessionDetails, + ); await this.setToken(response.token); - - if (!fido2Details && window.PublicKeyCredential && !localStorage.getItem("passkey-advertisement-never-ask")) { - Modal.mount() + + if ( + !fido2Details && + window.PublicKeyCredential && + !localStorage.getItem("passkey-advertisement-never-ask") + ) { + Modal.mount( + , + ); return; } - + Modal.unmount(); } catch (e: FetchResponse) { let json = await e.json(); if (this.#loginSessionDetails.newPassword) { - this.#loginSessionDetails.password = this.#loginSessionDetails.newPassword; + this.#loginSessionDetails.password = + this.#loginSessionDetails.newPassword; delete this.#loginSessionDetails.newPassword; } @@ -129,80 +146,105 @@ class UserManager extends EventEmitter { this.setLoginDetail("keyResponse", null); if (fido2Details) { - Modal.mount() + Modal.mount( + , + ); return; } switch (json.status) { case "DisabledAccount": - Modal.mount( - {i18n.t('ACCOUNT_DISABLED_PROMPT')} - ); + Modal.mount( + + {i18n.t("ACCOUNT_DISABLED_PROMPT")} + , + ); return; case "OtpRequired": - Modal.mount(); + Modal.mount(); return; case "PasswordResetRequired": - Modal.mount(); + Modal.mount(); return; case "PasswordResetRequestRequired": - Modal.mount( this.triggerPasswordReset() - } - ]}> - {i18n.t('RESET_PASSWORD_PROMPT')} - ); + Modal.mount( + this.triggerPasswordReset(), + }, + ]} + > + {i18n.t("RESET_PASSWORD_PROMPT")} + , + ); return; default: - Modal.mount() + Modal.mount(); } } } async triggerPasswordReset() { - Modal.mount(); + Modal.mount(); try { let response = await Fetch.post(`/api/user/reset/methods`, { - username: this.#loginSessionDetails.username + username: this.#loginSessionDetails.username, }); - Modal.mount() + Modal.mount(); } catch (e) { - Modal.mount( Modal.mount() - } - ]}> - {i18n.t('PASSWORD_RECOVERY_ERROR_PROMPT')} - ) + Modal.mount( + Modal.mount(), + }, + ]} + > + {i18n.t("PASSWORD_RECOVERY_ERROR_PROMPT")} + , + ); } } - async performPasswordReset(type: PasswordResetType, challenge: PasswordResetChallenge) { - Modal.mount() + async performPasswordReset( + type: PasswordResetType, + challenge: PasswordResetChallenge, + ) { + Modal.mount(); try { await Fetch.post("/api/user/reset", { username: this.#loginSessionDetails.username, type, - challenge + challenge, }); - Modal.mount( Modal.mount() - } - ]}> - {i18n.t('PASSWORD_RECOVERY_SUCCESS_PROMPT')} - ) + Modal.mount( + Modal.mount(), + }, + ]} + > + {i18n.t("PASSWORD_RECOVERY_SUCCESS_PROMPT")} + , + ); } catch (e) { - Modal.mount( - {i18n.t('PASSWORD_RECOVERY_ERROR_PROMPT_2')} - ) + Modal.mount( + + {i18n.t("PASSWORD_RECOVERY_ERROR_PROMPT_2")} + , + ); } } @@ -217,38 +259,41 @@ class UserManager extends EventEmitter { } async attemptFido2Login() { - Modal.mount() + Modal.mount(); let details: any; try { details = await Fetch.post("/api/user/token", { type: "fido", - username: this.loginDetail("username") + username: this.loginDetail("username"), }); } catch { - Modal.mount(); + Modal.mount(); return; } //Perform webauthn authentication // noinspection ExceptionCaughtLocallyJS try { - let assertion = await navigator.credentials.get({ + let assertion = (await navigator.credentials.get({ publicKey: { challenge: decode(details.options.challenge), - allowCredentials: details.options.allowCredentials.map((x: TokenResponseFidoOptionsCredentials) => ({ - type: x.type, - id: decode(x.id) - })), + allowCredentials: details.options.allowCredentials.map( + (x: TokenResponseFidoOptionsCredentials) => ({ + type: x.type, + id: decode(x.id), + }), + ), userVerification: details.options.userVerification, - extensions: details.options.extensions - } - }) as PublicKeyCredential; + extensions: details.options.extensions, + }, + })) as PublicKeyCredential; console.log(assertion); if (!assertion) throw assertion; - - const response = assertion.response as AuthenticatorAssertionResponse; + + const response = + assertion.response as AuthenticatorAssertionResponse; this.setLoginDetail("type", "fido"); this.setLoginDetail("keyTokenId", details.id); @@ -261,19 +306,19 @@ class UserManager extends EventEmitter { authenticatorData: encode(response.authenticatorData), clientDataJSON: encode(response.clientDataJSON), signature: encode(response.signature), - userHandle: encode(response.userHandle!) - } + userHandle: encode(response.userHandle!), + }, }); await this.attemptLogin({ - fido2Details: details + fido2Details: details, }); } catch (e) { console.log(e); - Modal.mount() + Modal.mount(); } } } let mgr = new UserManager(); -export default mgr; \ No newline at end of file +export default mgr; diff --git a/Parlance.ClientApp/src/helpers/i18n.ts b/Parlance.ClientApp/src/helpers/i18n.ts index 2442f067..4248859d 100644 --- a/Parlance.ClientApp/src/helpers/i18n.ts +++ b/Parlance.ClientApp/src/helpers/i18n.ts @@ -1,11 +1,11 @@ -import i18n from 'i18next'; -import HttpBackend from 'i18next-http-backend'; -import LanguageDetector from 'i18next-browser-languagedetector'; -import {initReactI18next} from "react-i18next"; +import i18n from "i18next"; +import HttpBackend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { initReactI18next } from "react-i18next"; import Pseudo from "i18next-pseudo"; import Fetch from "./Fetch"; import moment from "moment/moment"; -import 'moment/min/locales' +import "moment/min/locales"; type i18next = typeof i18n; @@ -16,7 +16,7 @@ interface ParlanceI18nExport extends i18next { pluralPatterns: (locale: string) => Promise; isRegionAgnostic: (locale: string) => boolean; dir: (lng?: string) => "ltr" | "rtl"; - t: typeof i18n.t + t: typeof i18n.t; } interface PluralPattern { @@ -25,18 +25,21 @@ interface PluralPattern { index: number; } -type PluralCategoryDictionary = {[key: string]: any}; +type PluralCategoryDictionary = { [key: string]: any }; let exportMember = i18n as ParlanceI18nExport; let instance = i18n.use(HttpBackend); -const pluralPatternsCache: PluralCategoryDictionary | Promise = {}; +const pluralPatternsCache: PluralCategoryDictionary | Promise = + {}; if (import.meta.env.REACT_APP_USE_PSEUDOTRANSLATION) { - instance.use(new Pseudo({ - enabled: true - })) + instance.use( + new Pseudo({ + enabled: true, + }), + ); } else { instance.use(LanguageDetector); } @@ -44,18 +47,18 @@ instance.use(initReactI18next).init({ fallbackLng: "en", debug: true, interpolation: { - escapeValue: false + escapeValue: false, }, backend: { - loadPath: "/resources/translations/{{lng}}/{{ns}}.json" + loadPath: "/resources/translations/{{lng}}/{{ns}}.json", }, detection: { - order: ['querystring', 'localStorage', 'navigator'], - lookupLocalStorage: "lang" + order: ["querystring", "localStorage", "navigator"], + lookupLocalStorage: "lang", }, - postProcess: ['pseudo'], - returnEmptyString: false -}) + postProcess: ["pseudo"], + returnEmptyString: false, +}); instance.on("languageChanged", language => { moment.locale(language); @@ -64,7 +67,10 @@ instance.on("initialized", options => { moment.locale(i18n.resolvedLanguage); }); -exportMember.humanReadableLocale = (locale, selectedLanguage = i18n.language) => { +exportMember.humanReadableLocale = ( + locale, + selectedLanguage = i18n.language, +) => { try { let parts = locale.split("-"); @@ -73,38 +79,48 @@ exportMember.humanReadableLocale = (locale, selectedLanguage = i18n.language) => let script = parts.shift(); let country = parts.shift(); - if (language) readableParts.push((new Intl.DisplayNames([selectedLanguage], {type: "language"})).of(language)); + if (language) + readableParts.push( + new Intl.DisplayNames([selectedLanguage], { + type: "language", + }).of(language), + ); if (script) { //Ensure this is actually a script try { - readableParts.push(`(${(new Intl.DisplayNames([selectedLanguage], {type: "script"})).of(script)})`); + readableParts.push( + `(${new Intl.DisplayNames([selectedLanguage], { type: "script" }).of(script)})`, + ); } catch { //Probably a country then country = script; } } - if (country) readableParts.push(`(${(new Intl.DisplayNames([selectedLanguage], {type: "region"})).of(country)})`); + if (country) + readableParts.push( + `(${new Intl.DisplayNames([selectedLanguage], { type: "region" }).of(country)})`, + ); return readableParts.join(" "); } catch { return locale; } -} +}; exportMember.number = (locale, number) => { - return (new Intl.NumberFormat(locale)).format(number); -} + return new Intl.NumberFormat(locale).format(number); +}; exportMember.list = (locale, items) => { - return (new Intl.ListFormat(locale, { + return new Intl.ListFormat(locale, { style: "narrow", - type: "conjunction" - })).format(items); -} + type: "conjunction", + }).format(items); +}; -exportMember.pluralPatterns = async (locale) => { +exportMember.pluralPatterns = async locale => { let promise: Promise | PluralCategoryDictionary; if (pluralPatternsCache[locale]) { promise = pluralPatternsCache[locale]; @@ -112,11 +128,11 @@ exportMember.pluralPatterns = async (locale) => { return pluralPatternsCache[locale]; } } else { - promise = Fetch.get(`/api/cldr/${locale}/plurals`) + promise = Fetch.get(`/api/cldr/${locale}/plurals`); } pluralPatternsCache[locale] = promise; - let data: PluralPattern[] = await promise as PluralPattern[]; + let data: PluralPattern[] = (await promise) as PluralPattern[]; let categories: PluralCategoryDictionary = {}; for (let category of data) { categories[category.category] = category.examples; @@ -124,17 +140,22 @@ exportMember.pluralPatterns = async (locale) => { pluralPatternsCache[locale] = categories; return categories; -} +}; exportMember.isRegionAgnostic = locale => { return locale.length === 2; }; const i18ndir = exportMember.dir.bind(i18n); -exportMember.dir = (lng) => { - if (!lng) lng = i18n.resolvedLanguage || (i18n.languages && i18n.languages.length > 0 ? i18n.languages[0] : i18n.language); +exportMember.dir = lng => { + if (!lng) + lng = + i18n.resolvedLanguage || + (i18n.languages && i18n.languages.length > 0 + ? i18n.languages[0] + : i18n.language); if (!lng) return "ltr"; return i18ndir(lng); -} +}; -export default exportMember; \ No newline at end of file +export default exportMember; diff --git a/Parlance.ClientApp/src/index.jsx b/Parlance.ClientApp/src/index.jsx index 9d4430a5..736e4f3c 100644 --- a/Parlance.ClientApp/src/index.jsx +++ b/Parlance.ClientApp/src/index.jsx @@ -1,25 +1,26 @@ -import React from 'react'; -import {createRoot} from 'react-dom/client'; -import {BrowserRouter} from 'react-router-dom'; -import App from './App'; -import * as serviceWorkerRegistration from './serviceWorkerRegistration'; -import reportWebVitals from './reportWebVitals'; +import React from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App"; +import * as serviceWorkerRegistration from "./serviceWorkerRegistration"; +import reportWebVitals from "./reportWebVitals"; if (!Array.prototype.findLast) { Array.prototype.findLast = function (delegate) { const hits = this.filter(delegate); return hits[hits.length - 1]; - } + }; } -const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href'); -const rootElement = document.getElementById('root'); +const baseUrl = document.getElementsByTagName("base")[0].getAttribute("href"); +const rootElement = document.getElementById("root"); const root = createRoot(rootElement); root.render( - - ); + + , +); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/Parlance.ClientApp/src/interfaces/comments.ts b/Parlance.ClientApp/src/interfaces/comments.ts index 38ba3863..3a74826f 100644 --- a/Parlance.ClientApp/src/interfaces/comments.ts +++ b/Parlance.ClientApp/src/interfaces/comments.ts @@ -1,27 +1,27 @@ export interface Thread { - id: string - title: string - isClosed: boolean - isFlagged: boolean - headComment: HeadComment - project: string - subproject: string - language: string - key: string - sourceTranslation?: string + id: string; + title: string; + isClosed: boolean; + isFlagged: boolean; + headComment: HeadComment; + project: string; + subproject: string; + language: string; + key: string; + sourceTranslation?: string; } interface HeadComment { - text: string - date: number - author: Author + text: string; + date: number; + author: Author; } export interface Comment extends HeadComment { - event: string | null + event: string | null; } interface Author { - username: string - picture: string + username: string; + picture: string; } diff --git a/Parlance.ClientApp/src/interfaces/fetch.ts b/Parlance.ClientApp/src/interfaces/fetch.ts index 5fe075a6..652bc03b 100644 --- a/Parlance.ClientApp/src/interfaces/fetch.ts +++ b/Parlance.ClientApp/src/interfaces/fetch.ts @@ -2,4 +2,4 @@ interface WebFetchResponse extends Response { jsonBody: any; } -type FetchResponse = T | {}; \ No newline at end of file +type FetchResponse = T | {}; diff --git a/Parlance.ClientApp/src/interfaces/glossary.ts b/Parlance.ClientApp/src/interfaces/glossary.ts index 369f56c2..c350689a 100644 --- a/Parlance.ClientApp/src/interfaces/glossary.ts +++ b/Parlance.ClientApp/src/interfaces/glossary.ts @@ -2,22 +2,22 @@ export enum PartOfSpeech { Unknown = 0, Noun, Verb, - Adjective + Adjective, } export interface Glossary { - id: string, - name: string, - createdDate: string, - usedByProjects: number + id: string; + name: string; + createdDate: string; + usedByProjects: number; } export interface GlossaryItem { - id: string - term: string - translation: string - partOfSpeech: PartOfSpeech - lang: string + id: string; + term: string; + translation: string; + partOfSpeech: PartOfSpeech; + lang: string; } export function PartOfSpeechTranslationString(pos: PartOfSpeech) { @@ -31,4 +31,4 @@ export function PartOfSpeechTranslationString(pos: PartOfSpeech) { case PartOfSpeech.Unknown: return "PART_OF_SPEECH_UNKNOWN"; } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/interfaces/projects.ts b/Parlance.ClientApp/src/interfaces/projects.ts index d77c05ce..d6cbbd2c 100644 --- a/Parlance.ClientApp/src/interfaces/projects.ts +++ b/Parlance.ClientApp/src/interfaces/projects.ts @@ -1,20 +1,20 @@ -import {Thread} from "./comments"; +import { Thread } from "./comments"; export interface SubprojectLocaleMeta { - completionData: CompletionData - projectName: string, - subprojectName: string, - language: string, - canEdit: boolean, - openThreads: Thread[] + completionData: CompletionData; + projectName: string; + subprojectName: string; + language: string; + canEdit: boolean; + openThreads: Thread[]; } export interface CompletionData { - count: number, - complete: number, - warnings: number, - errors: number, - cumulativeWarnings: number, - passedChecks: number, - needsAttention: number -} \ No newline at end of file + count: number; + complete: number; + warnings: number; + errors: number; + cumulativeWarnings: number; + passedChecks: number; + needsAttention: number; +} diff --git a/Parlance.ClientApp/src/interfaces/unsubscribe.ts b/Parlance.ClientApp/src/interfaces/unsubscribe.ts index ff519462..c4b6f121 100644 --- a/Parlance.ClientApp/src/interfaces/unsubscribe.ts +++ b/Parlance.ClientApp/src/interfaces/unsubscribe.ts @@ -21,6 +21,6 @@ interface TranslationFreezeSubscription extends BaseSubscription { export type Subscription = TranslationFreezeSubscription; export interface UnsubscribeInformation { - emailNotificationsOn: boolean, - subscription: Subscription -} \ No newline at end of file + emailNotificationsOn: boolean; + subscription: Subscription; +} diff --git a/Parlance.ClientApp/src/interfaces/users.ts b/Parlance.ClientApp/src/interfaces/users.ts index 9bc9b63e..41068405 100644 --- a/Parlance.ClientApp/src/interfaces/users.ts +++ b/Parlance.ClientApp/src/interfaces/users.ts @@ -1,37 +1,37 @@ export interface TokenResponseToken { - token: string + token: string; } export interface TokenResponseFido { - options: TokenResponseFidoOptions - id: number + options: TokenResponseFidoOptions; + id: number; } interface TokenResponseFidoOptions { - challenge: string - timeout: number - rpId: string - allowCredentials: TokenResponseFidoOptionsCredentials[] - userVerification: "discouraged" | "preferred" | "required" - extensions: Record - status: string - errorMessage: string + challenge: string; + timeout: number; + rpId: string; + allowCredentials: TokenResponseFidoOptionsCredentials[]; + userVerification: "discouraged" | "preferred" | "required"; + extensions: Record; + status: string; + errorMessage: string; } export interface TokenResponseFidoOptionsCredentials { - type: "public-key" - id: string + type: "public-key"; + id: string; } export type LoginType = "password" | "fido"; export interface User { - id: string - username: string - email: string - emailVerified: boolean - superuser: boolean - languagePermissions: string[] + id: string; + username: string; + email: string; + emailVerified: boolean; + superuser: boolean; + languagePermissions: string[]; } export type PasswordResetType = "email"; @@ -39,28 +39,28 @@ export type PasswordResetType = "email"; export type PasswordResetChallenge = PasswordResetChallengeEmail; interface PasswordResetChallengeEmail { - email: string + email: string; } interface OtpBackupCode { - used: boolean - code: string + used: boolean; + code: string; } -export type OtpState = OtpStateEnabled | OtpStateDisabled +export type OtpState = OtpStateEnabled | OtpStateDisabled; export interface OtpStateEnabled { - enabled: true - backupCodes: OtpBackupCode[] + enabled: true; + backupCodes: OtpBackupCode[]; } export interface OtpStateDisabled { - enabled: false - key: string + enabled: false; + key: string; } export interface SecurityKey { id: string; - name: string - application: string -} \ No newline at end of file + name: string; + application: string; +} diff --git a/Parlance.ClientApp/src/pages/Account/AccountSettings.jsx b/Parlance.ClientApp/src/pages/Account/AccountSettings.jsx index c41aa725..eec533b0 100644 --- a/Parlance.ClientApp/src/pages/Account/AccountSettings.jsx +++ b/Parlance.ClientApp/src/pages/Account/AccountSettings.jsx @@ -1,12 +1,12 @@ -import React, {useReducer} from "react"; -import {useTranslation} from "react-i18next"; +import React, { useReducer } from "react"; +import { useTranslation } from "react-i18next"; import UserManager from "../../helpers/UserManager"; import Container from "../../components/Container"; import PageHeading from "../../components/PageHeading"; import Styles from "./AccountSettings.module.css"; import SmallButton from "../../components/SmallButton"; import SelectableList from "../../components/SelectableList"; -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import Modal from "../../components/Modal"; import LoadingModal from "../../components/modals/LoadingModal"; import Fetch from "../../helpers/Fetch"; @@ -14,7 +14,7 @@ import Fetch from "../../helpers/Fetch"; export default function AccountSettings() { const [ignored, forceUpdate] = useReducer(x => x + 1, 0); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); UserManager.on("currentUserChanged", forceUpdate); @@ -23,87 +23,125 @@ export default function AccountSettings() { } const resendVerificationEmail = async () => { - Modal.mount() + Modal.mount(); try { await Fetch.post("/api/user/verification/resend", {}); - Modal.mount( - {t("VERIFICATION_EMAIL_RESEND_PROMPT")} - ) + Modal.mount( + + {t("VERIFICATION_EMAIL_RESEND_PROMPT")} + , + ); } catch { Modal.unmount(); } - } + }; - let verifyEmailPrompt = UserManager.currentUser.emailVerified ? null :
- {t("EMAIL_VERIFY_TITLE")} -

{t("EMAIL_VERIFY_PROMPT")}

-
- {t("EMAIL_VERIFY_RESEND")} - navigate("verify")}>{t("EMAIL_VERIFY_ENTER_CODE")} + let verifyEmailPrompt = UserManager.currentUser.emailVerified ? null : ( +
+ {t("EMAIL_VERIFY_TITLE")} +

{t("EMAIL_VERIFY_PROMPT")}

+
+ + {t("EMAIL_VERIFY_RESEND")} + + navigate("verify")}> + {t("EMAIL_VERIFY_ENTER_CODE")} + +
-
+ ); - return
- -
- {UserManager.currentUser.username} - {UserManager.currentUser.email} + return ( +
+ +
+ + {UserManager.currentUser.username} + + + {UserManager.currentUser.email} + - - {verifyEmailPrompt} -
-
- - navigate("username") - }, - { - contents: t("ACCOUNT_SETTINGS_CHANGE_PROFILE_PICTURE"), - onClick: () => window.open("https://en.gravatar.com/gravatars/new/", "_blank") - }, - { - contents: t("ACCOUNT_SETTINGS_CHANGE_EMAIL_ADDRESS"), - onClick: () => navigate("email") - },]} - /> - - - navigate("password") - }, - { - contents: t("ACCOUNT_SETTINGS_TWO_FACTOR"), - onClick: () => navigate("otp") - }, - { - contents: t("ACCOUNT_SETTINGS_MANAGE_SECURITY_KEYS"), - onClick: () => navigate("keys") - }, - ]} /> - - - navigate("notifications") - }, - ]} /> - - - navigate("attribution") - } - ]}/> - -
-} \ No newline at end of file + + {verifyEmailPrompt} +
+
+ + navigate("username"), + }, + { + contents: t( + "ACCOUNT_SETTINGS_CHANGE_PROFILE_PICTURE", + ), + onClick: () => + window.open( + "https://en.gravatar.com/gravatars/new/", + "_blank", + ), + }, + { + contents: t( + "ACCOUNT_SETTINGS_CHANGE_EMAIL_ADDRESS", + ), + onClick: () => navigate("email"), + }, + ]} + /> + + + navigate("password"), + }, + { + contents: t("ACCOUNT_SETTINGS_TWO_FACTOR"), + onClick: () => navigate("otp"), + }, + { + contents: t( + "ACCOUNT_SETTINGS_MANAGE_SECURITY_KEYS", + ), + onClick: () => navigate("keys"), + }, + ]} + /> + + + navigate("notifications"), + }, + ]} + /> + + + navigate("attribution"), + }, + ]} + /> + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Account/AccountSettings.module.css b/Parlance.ClientApp/src/pages/Account/AccountSettings.module.css index 7140ed90..527adb0e 100644 --- a/Parlance.ClientApp/src/pages/Account/AccountSettings.module.css +++ b/Parlance.ClientApp/src/pages/Account/AccountSettings.module.css @@ -3,7 +3,7 @@ grid-template-columns: max-content 1fr; grid-template-rows: 1fr 1fr; - grid-template-areas: + grid-template-areas: "accountImage accountUsername" "accountImage accountEmail" ". verifyEmail"; @@ -49,5 +49,4 @@ } .slideRightToLeft { - -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Account/Attribution.jsx b/Parlance.ClientApp/src/pages/Account/Attribution.jsx index aa77a478..6c157447 100644 --- a/Parlance.ClientApp/src/pages/Account/Attribution.jsx +++ b/Parlance.ClientApp/src/pages/Account/Attribution.jsx @@ -1,12 +1,12 @@ import BackButton from "../../components/BackButton"; -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import Container from "../../components/Container"; -import {VerticalLayout, VerticalSpacer} from "../../components/Layouts"; +import { VerticalLayout, VerticalSpacer } from "../../components/Layouts"; import PageHeading from "../../components/PageHeading"; import LineEdit from "../../components/LineEdit"; import SelectableList from "../../components/SelectableList"; -import {useTranslation} from "react-i18next"; -import {useEffect, useId, useState} from "react"; +import { useTranslation } from "react-i18next"; +import { useEffect, useId, useState } from "react"; import LoadingModal from "../../components/modals/LoadingModal"; import Modal from "../../components/Modal"; import ErrorModal from "../../components/modals/ErrorModal"; @@ -15,61 +15,77 @@ import Fetch from "../../helpers/Fetch"; export default function Attribution() { const [haveConsent, setHaveConsent] = useState(false); const [preferredName, setPreferredName] = useState(""); - const {t} = useTranslation(); + const { t } = useTranslation(); const navigate = useNavigate(); const checkboxId = useId(); useEffect(() => { (async () => { - Modal.mount(); + Modal.mount(); try { - const consentDetails = await Fetch.get("/api/user/attribution/consent"); + const consentDetails = await Fetch.get( + "/api/user/attribution/consent", + ); setHaveConsent(consentDetails.consentProvided); setPreferredName(consentDetails.preferredUserName); Modal.unmount(); } catch (err) { - Modal.mount() + Modal.mount(); navigate(".."); } })(); }, []); const applySettings = async () => { - Modal.mount(); + Modal.mount(); try { await Fetch.post("/api/user/attribution/consent", { consentProvided: haveConsent, - preferredName: preferredName + preferredName: preferredName, }); Modal.unmount(); navigate(".."); } catch (err) { - Modal.mount() + Modal.mount(); } - } + }; - return
- navigate("..")}/> - - - {t("ATTRIBUTION")} - {t("ATTRIBUTION_PROMPT_1")} - {t("ATTRIBUTION_PROMPT_2")} -
- setHaveConsent(e.target.checked)}/> - -
- {haveConsent && <> -
- {t("ATTRIBUTION_PROMPT_3")} - setPreferredName(e.target.value)}/> - } - - {t("APPLY_ATTRIBUTION_SETTINGS")} -
-
-
-} \ No newline at end of file + return ( +
+ navigate("..")} /> + + + {t("ATTRIBUTION")} + {t("ATTRIBUTION_PROMPT_1")} + {t("ATTRIBUTION_PROMPT_2")} +
+ setHaveConsent(e.target.checked)} + /> + +
+ {haveConsent && ( + <> +
+ {t("ATTRIBUTION_PROMPT_3")} + setPreferredName(e.target.value)} + /> + + )} + + + {t("APPLY_ATTRIBUTION_SETTINGS")} + +
+
+
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Account/EmailChange.jsx b/Parlance.ClientApp/src/pages/Account/EmailChange.jsx index 415affbb..f6749c85 100644 --- a/Parlance.ClientApp/src/pages/Account/EmailChange.jsx +++ b/Parlance.ClientApp/src/pages/Account/EmailChange.jsx @@ -1,22 +1,22 @@ import Container from "../../components/Container"; import PageHeading from "../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import SelectableList from "../../components/SelectableList"; -import {VerticalLayout, VerticalSpacer} from "../../components/Layouts"; -import {useState} from "react"; +import { VerticalLayout, VerticalSpacer } from "../../components/Layouts"; +import { useState } from "react"; import PasswordConfirmModal from "../../components/modals/account/PasswordConfirmModal"; import Modal from "../../components/Modal"; import Fetch from "../../helpers/Fetch"; import LoadingModal from "../../components/modals/LoadingModal"; -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import UserManager from "../../helpers/UserManager"; import BackButton from "../../components/BackButton"; import LineEdit from "../../components/LineEdit"; -export default function(props) { +export default function (props) { const [newEmail, setNewEmail] = useState(""); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const performEmailChange = () => { if (newEmail === "") return; @@ -27,7 +27,7 @@ export default function(props) { try { await Fetch.post("/api/user/email", { newEmail: newEmail, - password: password + password: password, }); await UserManager.updateDetails(); navigate(".."); @@ -39,25 +39,40 @@ export default function(props) { return; } - Modal.mount( - {t("CHANGE_EMAIL_ERROR_2")} - ) + Modal.mount( + + {t("CHANGE_EMAIL_ERROR_2")} + , + ); } - } + }; - Modal.mount() - } + Modal.mount(); + }; - return
- navigate("..")} /> - - - {t("ACCOUNT_SETTINGS_CHANGE_EMAIL_ADDRESS")} -

{t("CHANGE_EMAIL_PROMPT_1")}

- setNewEmail(e.target.value)} /> - - {t("ACCOUNT_SETTINGS_CHANGE_EMAIL_ADDRESS")} -
-
-
-} \ No newline at end of file + return ( +
+ navigate("..")} /> + + + + {t("ACCOUNT_SETTINGS_CHANGE_EMAIL_ADDRESS")} + +

{t("CHANGE_EMAIL_PROMPT_1")}

+ setNewEmail(e.target.value)} + /> + + + {t("ACCOUNT_SETTINGS_CHANGE_EMAIL_ADDRESS")} + +
+
+
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Account/Notifications/AutomaticSubscriptions.tsx b/Parlance.ClientApp/src/pages/Account/Notifications/AutomaticSubscriptions.tsx index 996fe31b..e9bd1c2b 100644 --- a/Parlance.ClientApp/src/pages/Account/Notifications/AutomaticSubscriptions.tsx +++ b/Parlance.ClientApp/src/pages/Account/Notifications/AutomaticSubscriptions.tsx @@ -1,17 +1,20 @@ -import {Trans, useTranslation} from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import ListPageBlock from "@/components/ListPageBlock"; -import {VerticalLayout} from "@/components/Layouts"; +import { VerticalLayout } from "@/components/Layouts"; import PageHeading from "@/components/PageHeading"; -import React, {useEffect, useReducer, useState} from "react"; +import React, { useEffect, useReducer, useState } from "react"; import SelectableList from "@/components/SelectableList"; import Fetch from "@/helpers/Fetch"; import { AutoSubscriptionEvent, AutoSubscriptionEventType, notificationChannelText, - NotificationChannelType + NotificationChannelType, } from "@/components/notifications/Events"; -import {AutoSubscriptionEventName, SubscriptionChannelName} from "@/interfaces/unsubscribe"; +import { + AutoSubscriptionEventName, + SubscriptionChannelName, +} from "@/interfaces/unsubscribe"; import Modal from "@/components/Modal"; import ErrorModal from "@/components/modals/ErrorModal"; import Spinner from "@/components/Spinner"; @@ -23,101 +26,150 @@ interface AutoSubscriptionState { } interface SubscriptionsInitAction { - action: "init" - data: AutoSubscriptionState[] + action: "init"; + data: AutoSubscriptionState[]; } interface SubscriptionsSetAction { - action: "set" - channel: string - event: string - subscribed: boolean + action: "set"; + channel: string; + event: string; + subscribed: boolean; } type SubscriptionsActions = SubscriptionsInitAction | SubscriptionsSetAction; export function AutomaticSubscriptions() { const [ready, setReady] = useState(false); - const [subscriptions, dispatchSubscriptions] = useReducer((state: AutoSubscriptionState[], action: SubscriptionsActions) => { - switch (action.action) { - case "init": - return action.data; - case "set": - return state.map(item => { - if (item.channel == action.channel && item.event == action.event) { - item.subscribed = action.subscribed; - } - return item; - }); - } - }, []) - const {t} = useTranslation(); + const [subscriptions, dispatchSubscriptions] = useReducer( + (state: AutoSubscriptionState[], action: SubscriptionsActions) => { + switch (action.action) { + case "init": + return action.data; + case "set": + return state.map(item => { + if ( + item.channel == action.channel && + item.event == action.event + ) { + item.subscribed = action.subscribed; + } + return item; + }); + } + }, + [], + ); + const { t } = useTranslation(); const updateAutoSubscriptionsState = async () => { - const response = await Fetch.get("/api/notifications/autosubscriptions"); + const response = await Fetch.get( + "/api/notifications/autosubscriptions", + ); dispatchSubscriptions({ action: "init", - data: response + data: response, }); setReady(true); - } + }; useEffect(() => { void updateAutoSubscriptionsState(); }, []); - - const setAutoSubscription = async (channel: SubscriptionChannelName, event: AutoSubscriptionEventName, subscribed: boolean) => { + + const setAutoSubscription = async ( + channel: SubscriptionChannelName, + event: AutoSubscriptionEventName, + subscribed: boolean, + ) => { dispatchSubscriptions({ action: "set", event: event, channel: channel, - subscribed: subscribed + subscribed: subscribed, }); try { await Fetch.post("/api/notifications/autosubscriptions", { channel: channel, event: event, - subscribed: subscribed + subscribed: subscribed, }); } catch (e) { - Modal.mount( window.location.reload()} okButtonText={t("RELOAD")} />) + Modal.mount( + window.location.reload()} + okButtonText={t("RELOAD")} + />, + ); } - } - + }; + if (!ready) { - return + return ; } - - const groups = Object.groupBy(subscriptions, subscription => subscription.channel); - return <> - - - {t("AUTO_SUBSCRIPTION_SETTINGS_TITLE")} - {t("AUTO_SUBSCRIPTION_SETTINGS_DESCRIPTION")} - - - {Object.keys(groups).map(groupString => { - const group = groupString as SubscriptionChannelName; - const autoSubscriptions = groups[group]; - - return + const groups = Object.groupBy( + subscriptions, + subscription => subscription.channel, + ); + + return ( + <> + - ({ - contents: - When  - - , - onClick: () => setAutoSubscription(subscription.channel, subscription.event, !subscription.subscribed), - on: subscription.subscribed - })) - ]} /> + + {t("AUTO_SUBSCRIPTION_SETTINGS_TITLE")} + + {t("AUTO_SUBSCRIPTION_SETTINGS_DESCRIPTION")} - ; - })} - -} \ No newline at end of file + + {Object.keys(groups).map(groupString => { + const group = groupString as SubscriptionChannelName; + const autoSubscriptions = groups[group]; + + return ( + + + ({ + contents: ( + + When  + + + ), + onClick: () => + setAutoSubscription( + subscription.channel, + subscription.event, + !subscription.subscribed, + ), + on: subscription.subscribed, + })), + ]} + /> + + + ); + })} + + ); +} diff --git a/Parlance.ClientApp/src/pages/Account/Notifications/Channel.tsx b/Parlance.ClientApp/src/pages/Account/Notifications/Channel.tsx index b6f5fe5a..7649384b 100644 --- a/Parlance.ClientApp/src/pages/Account/Notifications/Channel.tsx +++ b/Parlance.ClientApp/src/pages/Account/Notifications/Channel.tsx @@ -1,16 +1,18 @@ -import {SubscriptionChannelName} from "@/interfaces/unsubscribe"; +import { SubscriptionChannelName } from "@/interfaces/unsubscribe"; import ListPageBlock from "@/components/ListPageBlock"; -import {VerticalLayout} from "@/components/Layouts"; +import { VerticalLayout } from "@/components/Layouts"; import PageHeading from "@/components/PageHeading"; -import React, {Dispatch} from "react"; -import {useTranslation} from "react-i18next"; -import {notificationChannelText, NotificationChannelType} from "@/components/notifications/Events"; +import React, { Dispatch } from "react"; +import { useTranslation } from "react-i18next"; +import { + notificationChannelText, + NotificationChannelType, +} from "@/components/notifications/Events"; import SelectableList from "@/components/SelectableList"; import Fetch from "@/helpers/Fetch"; import Modal from "@/components/Modal"; import ErrorModal from "@/components/modals/ErrorModal"; - export interface ChannelSubscriptionState { channel: SubscriptionChannelName; subscriptionData: string; @@ -18,64 +20,96 @@ export interface ChannelSubscriptionState { } interface ChannelSubscriptionsInitAction { - action: "init" - data: ChannelSubscriptionState[] + action: "init"; + data: ChannelSubscriptionState[]; } interface ChannelSubscriptionsSetAction { - action: "set" - channel: string - subscriptionData: string - enabled: boolean + action: "set"; + channel: string; + subscriptionData: string; + enabled: boolean; } -export type ChannelSubscriptionsActions = ChannelSubscriptionsInitAction | ChannelSubscriptionsSetAction; +export type ChannelSubscriptionsActions = + | ChannelSubscriptionsInitAction + | ChannelSubscriptionsSetAction; -export function Channel({channel, channels, dispatchChannels}: { - channel: SubscriptionChannelName, - channels: ChannelSubscriptionState[], - dispatchChannels: Dispatch +export function Channel({ + channel, + channels, + dispatchChannels, +}: { + channel: SubscriptionChannelName; + channels: ChannelSubscriptionState[]; + dispatchChannels: Dispatch; }) { - const {t} = useTranslation(); - - const updateChannel = async (subscriptionData: string, enabled: boolean) => { + const { t } = useTranslation(); + + const updateChannel = async ( + subscriptionData: string, + enabled: boolean, + ) => { dispatchChannels({ action: "set", channel: channel, subscriptionData: subscriptionData, - enabled: enabled + enabled: enabled, }); try { await Fetch.post("/api/notifications/channels", { channel: channel, subscriptionData: subscriptionData, - enabled: enabled + enabled: enabled, }); } catch (e) { - Modal.mount( window.location.reload()} okButtonText={t("RELOAD")} />) + Modal.mount( + window.location.reload()} + okButtonText={t("RELOAD")} + />, + ); } - } - - return
- - - {t(notificationChannelText(channel, NotificationChannelType.Name))} - x.channel == channel).map(x => { - return { - contents: StateContents(x), - on: x.enabled, - onClick: () => updateChannel(x.subscriptionData, !x.enabled) - } - })} /> - - -
+ }; + + return ( +
+ + + + {t( + notificationChannelText( + channel, + NotificationChannelType.Name, + ), + )} + + x.channel == channel) + .map(x => { + return { + contents: StateContents(x), + on: x.enabled, + onClick: () => + updateChannel( + x.subscriptionData, + !x.enabled, + ), + }; + })} + /> + + +
+ ); } function StateContents(subscription: ChannelSubscriptionState) { const data = JSON.parse(subscription.subscriptionData); - + switch (subscription.channel) { case "TranslationFreeze": return data.Project; diff --git a/Parlance.ClientApp/src/pages/Account/Notifications/General.tsx b/Parlance.ClientApp/src/pages/Account/Notifications/General.tsx index 2d509e85..a9f6d3e4 100644 --- a/Parlance.ClientApp/src/pages/Account/Notifications/General.tsx +++ b/Parlance.ClientApp/src/pages/Account/Notifications/General.tsx @@ -1,8 +1,8 @@ -import {VerticalLayout} from "@/components/Layouts"; +import { VerticalLayout } from "@/components/Layouts"; import PageHeading from "@/components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import ListPageBlock from "@/components/ListPageBlock"; -import React, {useEffect, useState} from "react"; +import React, { useEffect, useState } from "react"; import SelectableList from "@/components/SelectableList"; import Fetch from "@/helpers/Fetch"; import Modal from "@/components/Modal"; @@ -12,43 +12,64 @@ import Spinner from "@/components/Spinner"; export function GeneralNotificationSettings() { const [emailNotificationsOff, setEmailNotificationsOff] = useState(false); const [ready, setReady] = useState(false); - const {t} = useTranslation(); - + const { t } = useTranslation(); + const updateEmailNotificationsState = async () => { - const response = await Fetch.get<{ unsubscribed: boolean }>("/api/notifications/unsubscription"); + const response = await Fetch.get<{ unsubscribed: boolean }>( + "/api/notifications/unsubscription", + ); setEmailNotificationsOff(response.unsubscribed); setReady(true); - } + }; useEffect(() => { void updateEmailNotificationsState(); }, []); - + const toggleEmailNotifications = async () => { const newState = !emailNotificationsOff; setEmailNotificationsOff(newState); - + try { await Fetch.post("/api/notifications/unsubscription", { - unsubscribed: newState + unsubscribed: newState, }); } catch (e) { - Modal.mount( window.location.reload()} okButtonText={t("RELOAD")} />) + Modal.mount( + window.location.reload()} + okButtonText={t("RELOAD")} + />, + ); } - } - + }; + if (!ready) { - return + return ; } - - return
- - - {t("GENERAL")} - {t("If you don't want to receive any email notifications from Parlance, you can turn off email notifications altogether.")} - {t("EMAIL_UNSUBSCRIBE_COMPLETELY_DESCRIPTION_2")} - {t("Email Notifications")} - - -
-} \ No newline at end of file + + return ( +
+ + + {t("GENERAL")} + + {t( + "If you don't want to receive any email notifications from Parlance, you can turn off email notifications altogether.", + )} + + + {t("EMAIL_UNSUBSCRIBE_COMPLETELY_DESCRIPTION_2")} + + + {t("Email Notifications")} + + + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Account/Notifications/index.tsx b/Parlance.ClientApp/src/pages/Account/Notifications/index.tsx index 807c8d9e..09d50059 100644 --- a/Parlance.ClientApp/src/pages/Account/Notifications/index.tsx +++ b/Parlance.ClientApp/src/pages/Account/Notifications/index.tsx @@ -1,85 +1,122 @@ import BackButton from "@/components/BackButton"; import Container from "@/components/Container"; -import {useNavigate} from "react-router-dom"; -import {useTranslation} from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import ListPage from "@/components/ListPage"; -import {GeneralNotificationSettings} from "@/pages/Account/Notifications/General"; -import {AutomaticSubscriptions} from "@/pages/Account/Notifications/AutomaticSubscriptions"; -import {useEffect, useReducer, useState} from "react"; +import { GeneralNotificationSettings } from "@/pages/Account/Notifications/General"; +import { AutomaticSubscriptions } from "@/pages/Account/Notifications/AutomaticSubscriptions"; +import { useEffect, useReducer, useState } from "react"; import Spinner from "@/components/Spinner"; -import {SubscriptionChannelName} from "@/interfaces/unsubscribe"; +import { SubscriptionChannelName } from "@/interfaces/unsubscribe"; import Fetch from "@/helpers/Fetch"; -import {notificationChannelText, NotificationChannelType} from "@/components/notifications/Events"; -import {Channel, ChannelSubscriptionsActions, ChannelSubscriptionState} from "@/pages/Account/Notifications/Channel"; +import { + notificationChannelText, + NotificationChannelType, +} from "@/components/notifications/Events"; +import { + Channel, + ChannelSubscriptionsActions, + ChannelSubscriptionState, +} from "@/pages/Account/Notifications/Channel"; export function NotificationsSettings() { const [ready, setReady] = useState(false); - const {t} = useTranslation(); + const { t } = useTranslation(); const navigate = useNavigate(); - const [channels, dispatchChannels] = useReducer((state: ChannelSubscriptionState[], action: ChannelSubscriptionsActions) => { - switch (action.action) { - case "init": - return action.data; - case "set": - return state.map(item => { - if (item.channel == action.channel && item.subscriptionData == action.subscriptionData) { - item.enabled = action.enabled; - } - return item; - }); - } - }, []) + const [channels, dispatchChannels] = useReducer( + ( + state: ChannelSubscriptionState[], + action: ChannelSubscriptionsActions, + ) => { + switch (action.action) { + case "init": + return action.data; + case "set": + return state.map(item => { + if ( + item.channel == action.channel && + item.subscriptionData == action.subscriptionData + ) { + item.enabled = action.enabled; + } + return item; + }); + } + }, + [], + ); const updateChannelSubscriptionState = async () => { - const response = await Fetch.get("/api/notifications/channels"); + const response = await Fetch.get( + "/api/notifications/channels", + ); dispatchChannels({ action: "init", - data: response + data: response, }); setReady(true); - } + }; useEffect(() => { void updateChannelSubscriptionState(); }, []); if (!ready) { - return
- navigate("..")}/> - -
+ return ( +
+ navigate("..")} /> + +
+ ); } - const groups = Object.groupBy(channels, subscription => subscription.channel); + const groups = Object.groupBy( + channels, + subscription => subscription.channel, + ); - return
- navigate("..")}/> - - , - default: true - }, - { - slug: "automatic-subscriptions", - name: t("AUTO_SUBSCRIPTION_SETTINGS_TITLE"), - render: - }, - t("NOTIFICATIONS_CHANNELS"), - ...Object.keys(groups).map(groupString => { - const group = groupString as SubscriptionChannelName; - - return { - slug: group, - name: t(notificationChannelText(group, NotificationChannelType.Name)), - render: - }; - }) - ]} /> -
-} + return ( +
+ navigate("..")} /> + , + default: true, + }, + { + slug: "automatic-subscriptions", + name: t("AUTO_SUBSCRIPTION_SETTINGS_TITLE"), + render: , + }, + t("NOTIFICATIONS_CHANNELS"), + ...Object.keys(groups).map(groupString => { + const group = groupString as SubscriptionChannelName; + + return { + slug: group, + name: t( + notificationChannelText( + group, + NotificationChannelType.Name, + ), + ), + render: ( + + ), + }; + }), + ]} + /> +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Account/Otp.module.css b/Parlance.ClientApp/src/pages/Account/Otp.module.css index 2a06723d..3cf8b80b 100644 --- a/Parlance.ClientApp/src/pages/Account/Otp.module.css +++ b/Parlance.ClientApp/src/pages/Account/Otp.module.css @@ -24,7 +24,7 @@ grid-template-columns: 1fr 1fr; grid-template-rows: max-content max-content max-content max-content max-content; gap: 9px 9px; - + justify-items: center; } @@ -52,4 +52,4 @@ .printPage .backupCode { font-size: 15pt; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Account/Otp.tsx b/Parlance.ClientApp/src/pages/Account/Otp.tsx index 7d43f7d4..b16efcf3 100644 --- a/Parlance.ClientApp/src/pages/Account/Otp.tsx +++ b/Parlance.ClientApp/src/pages/Account/Otp.tsx @@ -1,12 +1,18 @@ import BackButton from "../../components/BackButton"; import Container from "../../components/Container"; -import {VerticalLayout, VerticalSpacer} from "../../components/Layouts"; +import { VerticalLayout, VerticalSpacer } from "../../components/Layouts"; import PageHeading from "../../components/PageHeading"; import LineEdit from "../../components/LineEdit"; import SelectableList from "../../components/SelectableList"; -import {useNavigate} from "react-router-dom"; -import {useTranslation} from "react-i18next"; -import React, {useEffect, useRef, useState, useContext, ForwardedRef} from "react"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import React, { + useEffect, + useRef, + useState, + useContext, + ForwardedRef, +} from "react"; import Modal from "../../components/Modal"; import PasswordConfirmModal from "../../components/modals/account/PasswordConfirmModal"; import Fetch from "../../helpers/Fetch"; @@ -14,218 +20,339 @@ import Styles from "./Otp.module.css"; import QRCode from "react-qr-code"; import LoadingModal from "../../components/modals/LoadingModal"; import ErrorModal from "../../components/modals/ErrorModal"; -import {useReactToPrint} from "react-to-print"; +import { useReactToPrint } from "react-to-print"; import i18n from "../../helpers/i18n"; -import {OtpState, OtpStateDisabled, OtpStateEnabled} from "@/interfaces/users" -import {ServerInformationContext} from "@/context/ServerInformationContext"; +import { + OtpState, + OtpStateDisabled, + OtpStateEnabled, +} from "@/interfaces/users"; +import { ServerInformationContext } from "@/context/ServerInformationContext"; -function OtpBlock({otpState}: { - otpState: OtpStateEnabled -}) { - return
- {otpState.backupCodes.map(code => {code.code.match(/.{1,4}/g)!.join(" ")})} -
+function OtpBlock({ otpState }: { otpState: OtpStateEnabled }) { + return ( +
+ {otpState.backupCodes.map(code => ( + + {code.code.match(/.{1,4}/g)!.join(" ")} + + ))} +
+ ); } -const PrintableOtpCodes = React.forwardRef( - function PrintableOtpCodes({otpState}: {otpState: OtpStateEnabled}, ref: ForwardedRef) { - const {t} = useTranslation(); - const serverInformation = useContext(ServerInformationContext); - - return
+const PrintableOtpCodes = React.forwardRef(function PrintableOtpCodes( + { otpState }: { otpState: OtpStateEnabled }, + ref: ForwardedRef, +) { + const { t } = useTranslation(); + const serverInformation = useContext(ServerInformationContext); + + return ( +
Parlance - {t("BACKUP_CODES_PRINT_TITLE")} + + {t("BACKUP_CODES_PRINT_TITLE")} + -
+
{t("BACKUP_CODES_PRINT_PROMPT_1")} {t("BACKUP_CODES_PRINT_PROMPT_2")} {t("BACKUP_CODES_PRINT_PROMPT_3")} - {t("BACKUP_CODES_PRINT_PROMPT_4", { - date: (new Intl.DateTimeFormat(i18n.language, { - dateStyle: "full" - }).format(new Date())) - })} - + + {t("BACKUP_CODES_PRINT_PROMPT_4", { + date: new Intl.DateTimeFormat(i18n.language, { + dateStyle: "full", + }).format(new Date()), + })} + + - + - {t("BACKUP_CODES_PRINT_PROMPT_5")} - {t("BACKUP_CODES_PRINT_PROMPT_6", {account: serverInformation.accountName})} + + {t("BACKUP_CODES_PRINT_PROMPT_5")} + + + {t("BACKUP_CODES_PRINT_PROMPT_6", { + account: serverInformation.accountName, + })} +
- } -) + ); +}); -function OtpDisabledContent({otpState, onReload, password}: { - otpState: OtpStateDisabled, - onReload: () => void, - password: string +function OtpDisabledContent({ + otpState, + onReload, + password, +}: { + otpState: OtpStateDisabled; + onReload: () => void; + password: string; }) { const [otpCode, setOtpCode] = useState(""); - const {t} = useTranslation(); + const { t } = useTranslation(); const performEnable = async () => { - Modal.mount() + Modal.mount(); try { await Fetch.post("/api/user/otp/enable", { password: password, - otpCode: otpCode + otpCode: otpCode, }); onReload(); } catch (error) { - Modal.mount() + Modal.mount(); } - } + }; - return <> - - - {t("ACCOUNT_SETTINGS_TWO_FACTOR")} - {t("ACCOUNT_SETTINGS_TWO_FACTOR_WELCOME_PROMPT")} - {t("ACCOUNT_SETTINGS_TWO_FACTOR_PREAMBLE")} - - - -
- 1 - {t("ACCOUNT_SETTINGS_TWO_FACTOR_STEP_ONE")} -
-
- -
- 2 -
- - {t("ACCOUNT_SETTINGS_TWO_FACTOR_STEP_TWO_1")} - - {t("ACCOUNT_SETTINGS_TWO_FACTOR_STEP_TWO_2")} - {otpState.key.match(/.{1,4}/g)!.join(" ")} - + return ( + <> + + + + {t("ACCOUNT_SETTINGS_TWO_FACTOR")} + + + {t("ACCOUNT_SETTINGS_TWO_FACTOR_WELCOME_PROMPT")} + + {t("ACCOUNT_SETTINGS_TWO_FACTOR_PREAMBLE")} + + + +
+ 1 + {t("ACCOUNT_SETTINGS_TWO_FACTOR_STEP_ONE")}
-
- - -
- 3 - {t("ACCOUNT_SETTINGS_TWO_FACTOR_STEP_THREE")} -
-
- - - {t("ACCOUNT_SETTINGS_TWO_FACTOR_COMPLETE_SETUP")} - {t("ACCOUNT_SETTINGS_TWO_FACTOR_COMPLETE_SETUP_PROMPT")} - setOtpCode((e.target as HTMLInputElement).value)}/> - {t("ENABLE_TWO_FACTOR_AUTHENTICATION")} - - - + + +
+ 2 +
+ + + {t("ACCOUNT_SETTINGS_TWO_FACTOR_STEP_TWO_1")} + + + + {t("ACCOUNT_SETTINGS_TWO_FACTOR_STEP_TWO_2")} + + + {otpState.key.match(/.{1,4}/g)!.join(" ")} + + +
+
+
+ +
+ 3 + {t("ACCOUNT_SETTINGS_TWO_FACTOR_STEP_THREE")} +
+
+ + + + {t("ACCOUNT_SETTINGS_TWO_FACTOR_COMPLETE_SETUP")} + + + {t("ACCOUNT_SETTINGS_TWO_FACTOR_COMPLETE_SETUP_PROMPT")} + + + setOtpCode((e.target as HTMLInputElement).value) + } + /> + + {t("ENABLE_TWO_FACTOR_AUTHENTICATION")} + + + + + ); } -function OtpEnabledContent({otpState, onReload, password}: { - otpState: OtpStateEnabled, - onReload: () => void, - password: string +function OtpEnabledContent({ + otpState, + onReload, + password, +}: { + otpState: OtpStateEnabled; + onReload: () => void; + password: string; }) { const navigate = useNavigate(); const printRef = useRef(null); - const {t} = useTranslation(); + const { t } = useTranslation(); const handlePrint = useReactToPrint({ - content: () => printRef.current! + content: () => printRef.current!, }); - return <> - - - {t("ACCOUNT_SETTINGS_TWO_FACTOR")} - {t("ACCOUNT_SETTINGS_TWO_FACTOR_ENABLED_PROMPT_1")} - {t("ACCOUNT_SETTINGS_TWO_FACTOR_ENABLED_PROMPT_2")} - - {t("ACCOUNT_SETTINGS_TWO_FACTOR_ENABLED_PROMPT_3")} - - - - - - {t("ACTIONS")} - { - handlePrint() - } - }, - { - contents: t("REGENERATE_BACKUP_CODES"), - onClick: () => { - Modal.mount( { - Modal.mount() - try { - await Fetch.post("/api/user/otp/regenerate", { - password: password - }); - onReload(); - } catch (error) { - Modal.mount() - } - }, - destructive: true - } - ]}> - {t("REGENERATE_BACKUP_CODES_PROMPT")} - ) - } - }, - { - contents: t("ACCOUNT_SETTINGS_TWO_FACTOR_DISABLE"), - onClick: () => { - Modal.mount( { - Modal.mount() - try { - await Fetch.post("/api/user/otp/disable", { - password: password - }); - navigate(".."); - Modal.unmount(); - } catch (error) { - Modal.mount() - } - }, - destructive: true - } - ]}> - {t("ACCOUNT_SETTINGS_TWO_FACTOR_DISABLE_PROMPT")} - ) - }, - type: "destructive" - } - ]}/> - - - + return ( + <> + + + + {t("ACCOUNT_SETTINGS_TWO_FACTOR")} + + + {t("ACCOUNT_SETTINGS_TWO_FACTOR_ENABLED_PROMPT_1")} + + + {t("ACCOUNT_SETTINGS_TWO_FACTOR_ENABLED_PROMPT_2")} + + + + {t("ACCOUNT_SETTINGS_TWO_FACTOR_ENABLED_PROMPT_3")} + + + + + + + {t("ACTIONS")} + { + handlePrint(); + }, + }, + { + contents: t("REGENERATE_BACKUP_CODES"), + onClick: () => { + Modal.mount( + { + Modal.mount( + , + ); + try { + await Fetch.post( + "/api/user/otp/regenerate", + { + password: + password, + }, + ); + onReload(); + } catch (error) { + Modal.mount( + , + ); + } + }, + destructive: true, + }, + ]} + > + {t( + "REGENERATE_BACKUP_CODES_PROMPT", + )} + , + ); + }, + }, + { + contents: t( + "ACCOUNT_SETTINGS_TWO_FACTOR_DISABLE", + ), + onClick: () => { + Modal.mount( + { + Modal.mount( + , + ); + try { + await Fetch.post( + "/api/user/otp/disable", + { + password: + password, + }, + ); + navigate(".."); + Modal.unmount(); + } catch (error) { + Modal.mount( + , + ); + } + }, + destructive: true, + }, + ]} + > + {t( + "ACCOUNT_SETTINGS_TWO_FACTOR_DISABLE_PROMPT", + )} + , + ); + }, + type: "destructive", + }, + ]} + /> + + + + ); } export default function Otp() { const [password, setPassword] = useState(""); const [otpState, setOtpState] = useState(null); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const requestPassword = () => { const accept = (password: string) => { @@ -235,29 +362,38 @@ export default function Otp() { const reject = () => { navigate(".."); Modal.unmount(); - } + }; - Modal.mount() - } + Modal.mount( + , + ); + }; const updateState = async () => { if (!password) return; - Modal.mount(); + Modal.mount(); try { - setOtpState(await Fetch.post("/api/user/otp", { - password: password - })); + setOtpState( + await Fetch.post("/api/user/otp", { + password: password, + }), + ); Modal.unmount(); } catch (error) { const responseError = error as WebFetchResponse; if (responseError.status === 403) { requestPassword(); } else { - Modal.mount( { - navigate(".."); - Modal.unmount(); - }}/>) + Modal.mount( + { + navigate(".."); + Modal.unmount(); + }} + />, + ); } } }; @@ -273,19 +409,37 @@ export default function Otp() { let content; if (otpState === null) { - content = - - {t("ACCOUNT_SETTINGS_TWO_FACTOR")} - - + content = ( + + + + {t("ACCOUNT_SETTINGS_TWO_FACTOR")} + + + + ); } else if (otpState.enabled) { - content = + content = ( + + ); } else { - content = + content = ( + + ); } - return
- navigate("..")}/> - {content} -
-} \ No newline at end of file + return ( +
+ navigate("..")} /> + {content} +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Account/PasswordChange.jsx b/Parlance.ClientApp/src/pages/Account/PasswordChange.jsx index 6fce2279..7308f6cf 100644 --- a/Parlance.ClientApp/src/pages/Account/PasswordChange.jsx +++ b/Parlance.ClientApp/src/pages/Account/PasswordChange.jsx @@ -1,31 +1,36 @@ import Container from "../../components/Container"; import PageHeading from "../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import SelectableList from "../../components/SelectableList"; -import {VerticalLayout, VerticalSpacer} from "../../components/Layouts"; -import {useState} from "react"; +import { VerticalLayout, VerticalSpacer } from "../../components/Layouts"; +import { useState } from "react"; import PasswordConfirmModal from "../../components/modals/account/PasswordConfirmModal"; import Modal from "../../components/Modal"; import Fetch from "../../helpers/Fetch"; import LoadingModal from "../../components/modals/LoadingModal"; -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import UserManager from "../../helpers/UserManager"; import BackButton from "../../components/BackButton"; import LineEdit from "../../components/LineEdit"; -export default function(props) { +export default function (props) { const [newPassword, setNewPassword] = useState(""); const [newPasswordConfirm, setNewPasswordConfirm] = useState(""); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const performPasswordChange = () => { if (newPassword === "") return; - + if (newPassword !== newPasswordConfirm) { - Modal.mount( - {t("PASSWORD_CHANGE_NO_MATCH")} - ) + Modal.mount( + + {t("PASSWORD_CHANGE_NO_MATCH")} + , + ); return; } @@ -35,7 +40,7 @@ export default function(props) { try { await Fetch.post("/api/user/password", { newPassword: newPassword, - password: password + password: password, }); await UserManager.updateDetails(); navigate(".."); @@ -47,28 +52,49 @@ export default function(props) { return; } - Modal.mount( - {t("PASSWORD_CHANGE_ERROR_2")} - ) + Modal.mount( + + {t("PASSWORD_CHANGE_ERROR_2")} + , + ); } - } + }; - Modal.mount() - } + Modal.mount(); + }; - return
- navigate("..")} /> - - - {t("ACCOUNT_SETTINGS_CHANGE_PASSWORD")} -

{t("PASSWORD_CHANGE_PROMPT_1")}

-

{t("PASSWORD_SET_SECURITY_PROMPT")}

- setNewPassword(e.target.value)} /> - - setNewPasswordConfirm(e.target.value)} /> - - {t("ACCOUNT_SETTINGS_CHANGE_PASSWORD")} -
-
-
-} \ No newline at end of file + return ( +
+ navigate("..")} /> + + + + {t("ACCOUNT_SETTINGS_CHANGE_PASSWORD")} + +

{t("PASSWORD_CHANGE_PROMPT_1")}

+

{t("PASSWORD_SET_SECURITY_PROMPT")}

+ setNewPassword(e.target.value)} + /> + + setNewPasswordConfirm(e.target.value)} + /> + + + {t("ACCOUNT_SETTINGS_CHANGE_PASSWORD")} + +
+
+
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Account/SecurityKeys.module.css b/Parlance.ClientApp/src/pages/Account/SecurityKeys.module.css index b00704da..20e5940f 100644 --- a/Parlance.ClientApp/src/pages/Account/SecurityKeys.module.css +++ b/Parlance.ClientApp/src/pages/Account/SecurityKeys.module.css @@ -4,10 +4,9 @@ } .SecurityKeyName { - } .SecurityKeyApplication { color: var(--foreground-disabled-color); font-size: 10pt; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Account/SecurityKeys.tsx b/Parlance.ClientApp/src/pages/Account/SecurityKeys.tsx index 1c02c5b3..9eac85f7 100644 --- a/Parlance.ClientApp/src/pages/Account/SecurityKeys.tsx +++ b/Parlance.ClientApp/src/pages/Account/SecurityKeys.tsx @@ -1,134 +1,212 @@ import BackButton from "../../components/BackButton"; -import {useNavigate} from "react-router-dom"; -import React, {useEffect, useState, useContext, ReactNode} from "react"; -import {useTranslation} from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import React, { useEffect, useState, useContext, ReactNode } from "react"; +import { useTranslation } from "react-i18next"; import Modal from "../../components/Modal"; import PasswordConfirmModal from "../../components/modals/account/PasswordConfirmModal"; import LoadingModal from "../../components/modals/LoadingModal"; import Fetch from "../../helpers/Fetch"; import ErrorModal from "../../components/modals/ErrorModal"; import Container from "../../components/Container"; -import {VerticalLayout, VerticalSpacer} from "../../components/Layouts"; +import { VerticalLayout, VerticalSpacer } from "../../components/Layouts"; import PageHeading from "../../components/PageHeading"; import SelectableList from "../../components/SelectableList"; import RegisterSecurityKeyModal from "../../components/modals/account/securityKeys/RegisterSecurityKeyModal"; import Styles from "./SecurityKeys.module.css"; -import {ServerInformationContext} from "@/context/ServerInformationContext.js"; -import {SecurityKey} from "../../interfaces/users"; - -function KeyList({keys, title, after, onManageKey}: { - keys: SecurityKey[] - after: ReactNode - title: string - onManageKey: (key: SecurityKey) => void +import { ServerInformationContext } from "@/context/ServerInformationContext.js"; +import { SecurityKey } from "../../interfaces/users"; + +function KeyList({ + keys, + title, + after, + onManageKey, +}: { + keys: SecurityKey[]; + after: ReactNode; + title: string; + onManageKey: (key: SecurityKey) => void; }) { if (keys.length === 0) return null; - return - ({ - contents: key.application === "Parlance" ? key.name :
- {key.name} - {key.application} -
, - onClick: () => onManageKey(key) - })), - ]}/> - {after} -
+ return ( + + ({ + contents: + key.application === "Parlance" ? ( + key.name + ) : ( +
+ + {key.name} + + + {key.application} + +
+ ), + onClick: () => onManageKey(key), + })), + ]} + /> + {after} +
+ ); } -function SecurityKeysUi({password, onUpdateKeys, keys}: { - password: string - onUpdateKeys: () => void - keys: SecurityKey[] +function SecurityKeysUi({ + password, + onUpdateKeys, + keys, +}: { + password: string; + onUpdateKeys: () => void; + keys: SecurityKey[]; }) { - const {t} = useTranslation(); + const { t } = useTranslation(); const serverInformation = useContext(ServerInformationContext); const manageKey = (key: SecurityKey) => { - Modal.mount( { - Modal.mount() - try { - await Fetch.post(`/api/user/keys/${key.id}/delete`, { - password: password - }); - - await onUpdateKeys(); - } catch (err) { - Modal.mount() - } - } - } - ]}> - {t("SECURITY_KEY_DEREGISTER_PROMPT", { - key: key.name, - application: key.application - })} - ) - } + Modal.mount( + { + Modal.mount(); + try { + await Fetch.post( + `/api/user/keys/${key.id}/delete`, + { + password: password, + }, + ); + + await onUpdateKeys(); + } catch (err) { + Modal.mount(); + } + }, + }, + ]} + > + {t("SECURITY_KEY_DEREGISTER_PROMPT", { + key: key.name, + application: key.application, + })} + , + ); + }; const registerKey = (type: string) => { //Ensure the browser supports webauthn if (!window.PublicKeyCredential) { - Modal.mount( - {t("SECURITY_KEY_UNSUPPORTED_BROWSER_PROMPT")} -
    -
  • Google Chrome
  • -
  • Firefox
  • -
  • Microsoft Edge
  • -
  • Safari
  • -
-
) + Modal.mount( + + {t("SECURITY_KEY_UNSUPPORTED_BROWSER_PROMPT")} +
    +
  • Google Chrome
  • +
  • Firefox
  • +
  • Microsoft Edge
  • +
  • Safari
  • +
+
, + ); return; } - Modal.mount() + Modal.mount( + , + ); }; - return <> - - - {t("ACCOUNT_SETTINGS_MANAGE_SECURITY_KEYS")} - {t("ACCOUNT_SETTINGS_MANAGE_SECURITY_KEYS_PROMPT")} - - - {keys.filter(key => key.application === "Parlance").length > 0 && - { key.application === "Parlance")} - title={t("SECURITY_KEY_REGISTERED_SECURITY_KEYS")} - after={t("SECURITY_KEY_REGISTERED_SECURITY_KEYS_PROMPT")} onManageKey={manageKey}/>} - } - {keys.filter(key => key.application !== "Parlance").length > 0 && - { key.application !== "Parlance")} - title={t("SECURITY_KEY_OTHER_SECURITY_KEYS")} - after={t("translation:SECURITY_KEY_OTHER_SECURITY_KEYS_PROMPT", {account: serverInformation.accountName})} - onManageKey={manageKey}/>} - } - - - + + + + {t("ACCOUNT_SETTINGS_MANAGE_SECURITY_KEYS")} + + + {t("ACCOUNT_SETTINGS_MANAGE_SECURITY_KEYS_PROMPT")} + + + + {keys.filter(key => key.application === "Parlance").length > 0 && ( + { - contents: t("SECURITY_KEY_REGISTER_SECURITY_KEY"), - onClick: () => registerKey("") + key.application === "Parlance", + )} + title={t("SECURITY_KEY_REGISTERED_SECURITY_KEYS")} + after={t( + "SECURITY_KEY_REGISTERED_SECURITY_KEYS_PROMPT", + )} + onManageKey={manageKey} + /> } - ]}/> - - - + + )} + {keys.filter(key => key.application !== "Parlance").length > 0 && ( + + { + key.application !== "Parlance", + )} + title={t("SECURITY_KEY_OTHER_SECURITY_KEYS")} + after={t( + "translation:SECURITY_KEY_OTHER_SECURITY_KEYS_PROMPT", + { account: serverInformation.accountName }, + )} + onManageKey={manageKey} + /> + } + + )} + + + registerKey(""), + }, + ]} + /> + + + + ); } export default function SecurityKeys() { const [password, setPassword] = useState(""); const [securityKeyState, setSecurityKeyState] = useState(null); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const requestPassword = () => { const accept = (password: string) => { @@ -138,29 +216,38 @@ export default function SecurityKeys() { const reject = () => { navigate(".."); Modal.unmount(); - } + }; - Modal.mount() - } + Modal.mount( + , + ); + }; const updateState = async () => { if (!password) return; - Modal.mount(); + Modal.mount(); try { - setSecurityKeyState(await Fetch.post("/api/user/keys", { - password: password - })); + setSecurityKeyState( + await Fetch.post("/api/user/keys", { + password: password, + }), + ); Modal.unmount(); } catch (error) { const responseError = error as WebFetchResponse; if (responseError.status === 403) { requestPassword(); } else { - Modal.mount( { - navigate(".."); - Modal.unmount(); - }}/>) + Modal.mount( + { + navigate(".."); + Modal.unmount(); + }} + />, + ); } } }; @@ -176,17 +263,29 @@ export default function SecurityKeys() { let content; if (securityKeyState === null) { - content = - - {t("ACCOUNT_SETTINGS_MANAGE_SECURITY_KEYS")} - - + content = ( + + + + {t("ACCOUNT_SETTINGS_MANAGE_SECURITY_KEYS")} + + + + ); } else { - content = + content = ( + + ); } - return
- navigate("..")}/> - {content} -
-} \ No newline at end of file + return ( +
+ navigate("..")} /> + {content} +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Account/UsernameChange.jsx b/Parlance.ClientApp/src/pages/Account/UsernameChange.jsx index 105a9b15..7e6b29ef 100644 --- a/Parlance.ClientApp/src/pages/Account/UsernameChange.jsx +++ b/Parlance.ClientApp/src/pages/Account/UsernameChange.jsx @@ -1,33 +1,33 @@ import Container from "../../components/Container"; import PageHeading from "../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import SelectableList from "../../components/SelectableList"; -import {VerticalLayout, VerticalSpacer} from "../../components/Layouts"; -import {useState} from "react"; +import { VerticalLayout, VerticalSpacer } from "../../components/Layouts"; +import { useState } from "react"; import PasswordConfirmModal from "../../components/modals/account/PasswordConfirmModal"; import Modal from "../../components/Modal"; import Fetch from "../../helpers/Fetch"; import LoadingModal from "../../components/modals/LoadingModal"; -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import UserManager from "../../helpers/UserManager"; import BackButton from "../../components/BackButton"; import LineEdit from "../../components/LineEdit"; -export default function(props) { +export default function (props) { const [newUsername, setNewUsername] = useState(""); const navigate = useNavigate(); - const {t} = useTranslation(); - + const { t } = useTranslation(); + const performUsernameChange = () => { if (newUsername === "") return; - + const accept = async password => { //Perform the username change Modal.mount(); try { await Fetch.post("/api/user/username", { newUsername: newUsername, - password: password + password: password, }); await UserManager.updateDetails(); navigate(".."); @@ -38,26 +38,41 @@ export default function(props) { performUsernameChange(); return; } - - Modal.mount( - {t("CHANGE_USERNAME_ERROR_2")} - ) + + Modal.mount( + + {t("CHANGE_USERNAME_ERROR_2")} + , + ); } - } - - Modal.mount() - } - - return
- navigate("..")} /> - - - {t("ACCOUNT_SETTINGS_CHANGE_USERNAME")} -

{t("CHANGE_USERNAME_PROMPT_1")}

- setNewUsername(e.target.value)} /> - - {t("ACCOUNT_SETTINGS_CHANGE_USERNAME")} -
-
-
-} \ No newline at end of file + }; + + Modal.mount(); + }; + + return ( +
+ navigate("..")} /> + + + + {t("ACCOUNT_SETTINGS_CHANGE_USERNAME")} + +

{t("CHANGE_USERNAME_PROMPT_1")}

+ setNewUsername(e.target.value)} + /> + + + {t("ACCOUNT_SETTINGS_CHANGE_USERNAME")} + +
+
+
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Account/VerifyEmail.jsx b/Parlance.ClientApp/src/pages/Account/VerifyEmail.jsx index 1f4669a5..ef40fed8 100644 --- a/Parlance.ClientApp/src/pages/Account/VerifyEmail.jsx +++ b/Parlance.ClientApp/src/pages/Account/VerifyEmail.jsx @@ -1,32 +1,38 @@ import Container from "../../components/Container"; import PageHeading from "../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import SelectableList from "../../components/SelectableList"; -import {VerticalLayout, VerticalSpacer} from "../../components/Layouts"; -import {useState} from "react"; +import { VerticalLayout, VerticalSpacer } from "../../components/Layouts"; +import { useState } from "react"; import Modal from "../../components/Modal"; import Fetch from "../../helpers/Fetch"; import LoadingModal from "../../components/modals/LoadingModal"; -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import UserManager from "../../helpers/UserManager"; import BackButton from "../../components/BackButton"; import LineEdit from "../../components/LineEdit"; -export default function(props) { +export default function (props) { const [verificationCode, setVerificationCode] = useState(""); const navigate = useNavigate(); - const {t} = useTranslation(); - + const { t } = useTranslation(); + if (UserManager.currentUser.emailVerified) { - return
- navigate("..")} /> - - {t("VERIFY_EMAIL_SUCCESS")} -

{t("VERIFY_EMAIL_SUCCESS_PROMPT")}

-
-
+ return ( +
+ navigate("..")} /> + + + {t("VERIFY_EMAIL_SUCCESS")} + +

{t("VERIFY_EMAIL_SUCCESS_PROMPT")}

+
+
+ ); } const performVerification = async () => { @@ -36,43 +42,63 @@ export default function(props) { Modal.mount(); try { await Fetch.post("/api/user/verification", { - verificationCode: verificationCode + verificationCode: verificationCode, }); await UserManager.updateDetails(); - - Modal.mount( { - navigate(".."); - Modal.unmount(); - } - } - ]}> - {t("VERIFY_EMAIL_SUCCESS_PROMPT")} - ) + + Modal.mount( + { + navigate(".."); + Modal.unmount(); + }, + }, + ]} + > + {t("VERIFY_EMAIL_SUCCESS_PROMPT")} + , + ); } catch (ex) { - Modal.mount( - - {t("VERIFY_EMAIL_FAILED_2")} - {t("VERIFY_EMAIL_FAILED_3")} - - ) + Modal.mount( + + + {t("VERIFY_EMAIL_FAILED_2")} + {t("VERIFY_EMAIL_FAILED_3")} + + , + ); } - } + }; - return
- navigate("..")} /> - - - {t("VERIFY_EMAIL")} -

{t("VERIFY_EMAIL_PROMPT")}

- setVerificationCode(e.target.value)} /> - - {t("VERIFY_EMAIL")} -
-
-
-} \ No newline at end of file + return ( +
+ navigate("..")} /> + + + {t("VERIFY_EMAIL")} +

{t("VERIFY_EMAIL_PROMPT")}

+ setVerificationCode(e.target.value)} + /> + + + {t("VERIFY_EMAIL")} + +
+
+
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Account/index.css b/Parlance.ClientApp/src/pages/Account/index.css index cffe5e54..3131beb6 100644 --- a/Parlance.ClientApp/src/pages/Account/index.css +++ b/Parlance.ClientApp/src/pages/Account/index.css @@ -13,14 +13,14 @@ min-height: 700px; background-color: var(--background-color); z-index: 10; - + opacity: 0; transform: translateY(10%); - + transition: all 0.25s; } .account-settings-lift-enter-active { opacity: 1; transform: translateY(0%); -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Account/index.jsx b/Parlance.ClientApp/src/pages/Account/index.jsx index 45d378b2..34bde4f3 100644 --- a/Parlance.ClientApp/src/pages/Account/index.jsx +++ b/Parlance.ClientApp/src/pages/Account/index.jsx @@ -1,8 +1,8 @@ -import React, {useReducer, useContext} from "react"; -import {CSSTransition, TransitionGroup} from "react-transition-group"; +import React, { useReducer, useContext } from "react"; +import { CSSTransition, TransitionGroup } from "react-transition-group"; import AccountSettings from "./AccountSettings"; -import {Route, Routes, useLocation} from "react-router-dom"; -import {useTranslation} from "react-i18next"; +import { Route, Routes, useLocation } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import UserManager from "../../helpers/UserManager"; import Container from "../../components/Container"; import PageHeading from "../../components/PageHeading"; @@ -15,49 +15,78 @@ import "./index.css"; import Otp from "./Otp"; import SecurityKeys from "./SecurityKeys"; import Attribution from "./Attribution"; -import {NotificationsSettings} from "@/pages/Account/Notifications/index"; -import {ServerInformationContext} from "@/context/ServerInformationContext"; +import { NotificationsSettings } from "@/pages/Account/Notifications/index"; +import { ServerInformationContext } from "@/context/ServerInformationContext"; import Hero from "@/components/Hero"; export default function Account() { const [ignored, forceUpdate] = useReducer(x => x + 1, 0); const location = useLocation(); - const {t} = useTranslation(); + const { t } = useTranslation(); const serverInformation = useContext(ServerInformationContext); UserManager.on("currentUserChanged", forceUpdate); if (!UserManager.isLoggedIn) { - return
- - - {t("NOT_LOGGED_IN")} -

{t("ACCOUNT_SETTINGS_LOGIN_PROMPT")}

-
-
+ return ( +
+ + + {t("NOT_LOGGED_IN")} +

{t("ACCOUNT_SETTINGS_LOGIN_PROMPT")}

+
+
+ ); } - return
- - - - - } path={"/"}/> - } path={"/username"}/> - } path={"/email"}/> - } path={"/password"}/> - } path={"/verify"}/> - } path={"/keys"}/> - } path={"/otp"}/> - } path={"/attribution"}/> - } path={"/notifications/*"}/> - - - -
-} \ No newline at end of file + return ( +
+ + + + + } path={"/"} /> + } + path={"/username"} + /> + } path={"/email"} /> + } + path={"/password"} + /> + } path={"/verify"} /> + } path={"/keys"} /> + } path={"/otp"} /> + } + path={"/attribution"} + /> + } + path={"/notifications/*"} + /> + + + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Administration/Glossaries/GlossaryListing.module.css b/Parlance.ClientApp/src/pages/Administration/Glossaries/GlossaryListing.module.css index a9cd08a7..42854988 100644 --- a/Parlance.ClientApp/src/pages/Administration/Glossaries/GlossaryListing.module.css +++ b/Parlance.ClientApp/src/pages/Administration/Glossaries/GlossaryListing.module.css @@ -6,4 +6,4 @@ .glossaryItemSubtext { font-size: 8pt; color: var(--foreground-disabled-color); -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Administration/Glossaries/GlossaryListing.tsx b/Parlance.ClientApp/src/pages/Administration/Glossaries/GlossaryListing.tsx index 4b751f7f..235d12d0 100644 --- a/Parlance.ClientApp/src/pages/Administration/Glossaries/GlossaryListing.tsx +++ b/Parlance.ClientApp/src/pages/Administration/Glossaries/GlossaryListing.tsx @@ -1,11 +1,11 @@ import ListPageBlock from "@/components/ListPageBlock"; -import {VerticalLayout} from "@/components/Layouts"; +import { VerticalLayout } from "@/components/Layouts"; import PageHeading from "@/components/PageHeading"; import SelectableList from "@/components/SelectableList"; -import {useTranslation} from "react-i18next"; -import {ReactElement, useEffect, useState} from "react"; +import { useTranslation } from "react-i18next"; +import { ReactElement, useEffect, useState } from "react"; import Fetch from "@/helpers/Fetch"; -import {Glossary} from "@/interfaces/glossary"; +import { Glossary } from "@/interfaces/glossary"; import Modal from "@/components/Modal"; import LineEdit from "@/components/LineEdit"; import LoadingModal from "@/components/modals/LoadingModal"; @@ -13,113 +13,152 @@ import LoadingModal from "@/components/modals/LoadingModal"; import Styles from "./GlossaryListing.module.css"; interface AddGlossaryModalProps { - onDone: () => {} + onDone: () => {}; } interface RemoveGlossaryModalProps { - onDone: () => {} - glossary: Glossary + onDone: () => {}; + glossary: Glossary; } -function AddGlossaryModal({onDone} : AddGlossaryModalProps) : ReactElement { +function AddGlossaryModal({ onDone }: AddGlossaryModalProps): ReactElement { const [glossaryName, setGlossaryName] = useState(""); - const {t} = useTranslation(); - + const { t } = useTranslation(); + const addGlossary = async () => { try { - Modal.mount() + Modal.mount(); await Fetch.post("/api/glossarymanager", { - name: glossaryName - }) + name: glossaryName, + }); Modal.unmount(); onDone(); } catch (err) { - Modal.mount( - {t("ADD_GLOSSARY_ERROR")} - ) - } - } - - return + {t("ADD_GLOSSARY_ERROR")} + , + ); } - ]}> - {t('GLOSSARY_ADD_PROMPT')} - setGlossaryName((e.target as HTMLInputElement).value)}/> - + }; + + return ( + + {t("GLOSSARY_ADD_PROMPT")} + + setGlossaryName((e.target as HTMLInputElement).value) + } + /> + + ); } -function RemoveGlossaryModal({onDone, glossary} : RemoveGlossaryModalProps) : ReactElement { - const {t} = useTranslation(); +function RemoveGlossaryModal({ + onDone, + glossary, +}: RemoveGlossaryModalProps): ReactElement { + const { t } = useTranslation(); const addGlossary = async () => { try { - Modal.mount() - await Fetch.delete(`/api/glossarymanager/${glossary.id}`) + Modal.mount(); + await Fetch.delete(`/api/glossarymanager/${glossary.id}`); Modal.unmount(); onDone(); } catch (err) { - Modal.mount( - {t("GLOSSARY_REMOVE_ERROR")} - ) + Modal.mount( + + {t("GLOSSARY_REMOVE_ERROR")} + , + ); } - } + }; - return - {t('GLOSSARY_REMOVE_PROMPT', { - glossary: glossary.name - })} - + return ( + + {t("GLOSSARY_REMOVE_PROMPT", { + glossary: glossary.name, + })} + + ); } -export default function GlossaryListing() : ReactElement { +export default function GlossaryListing(): ReactElement { const [glossaries, setGlossaries] = useState([]); - const {t} = useTranslation(); - + const { t } = useTranslation(); + const updateGlossaries = async () => { let glossaries = await Fetch.get("/api/glossarymanager"); setGlossaries(glossaries); - } - + }; + const addGlossary = () => { - Modal.mount() - } - + Modal.mount(); + }; + const removeGlossary = (glossary: Glossary) => { - Modal.mount(); - } - + Modal.mount( + , + ); + }; + useEffect(() => { updateGlossaries(); }, []); - - return
- - - {t("GLOSSARIES")} - {t("GLOSSARY_LISTING_PROMPT")} - ({ - contents:
- {glossary.name} - {t("ADD_GLOSSARY_PROJECTS_CONNECTED", { - count: glossary.usedByProjects - })} -
, - onClick: () => removeGlossary(glossary) - }))} /> - {t("ADD_GLOSSARY")} - {t("ADD_GLOSSARY_CONNECT_PROJECT_PROMPT")} -
-
-
-} \ No newline at end of file + + return ( +
+ + + {t("GLOSSARIES")} + {t("GLOSSARY_LISTING_PROMPT")} + ({ + contents: ( +
+ {glossary.name} + + {t("ADD_GLOSSARY_PROJECTS_CONNECTED", { + count: glossary.usedByProjects, + })} + +
+ ), + onClick: () => removeGlossary(glossary), + }))} + /> + + {t("ADD_GLOSSARY")} + + {t("ADD_GLOSSARY_CONNECT_PROJECT_PROMPT")} +
+
+
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Administration/Glossaries/index.tsx b/Parlance.ClientApp/src/pages/Administration/Glossaries/index.tsx index dea672cb..7ace04b8 100644 --- a/Parlance.ClientApp/src/pages/Administration/Glossaries/index.tsx +++ b/Parlance.ClientApp/src/pages/Administration/Glossaries/index.tsx @@ -1,10 +1,12 @@ -import {Route, Routes} from "react-router-dom"; +import { Route, Routes } from "react-router-dom"; import GlossaryListing from "./GlossaryListing"; export default function Glossaries() { - return - {/*} path={"/add"} />*/} - } path={"/"} /> - {/*} path={"/:project"} />*/} - + return ( + + {/*} path={"/add"} />*/} + } path={"/"} /> + {/*} path={"/:project"} />*/} + + ); } diff --git a/Parlance.ClientApp/src/pages/Administration/Locales/LocaleSelection.jsx b/Parlance.ClientApp/src/pages/Administration/Locales/LocaleSelection.jsx index 95d5de26..1f6ab05a 100644 --- a/Parlance.ClientApp/src/pages/Administration/Locales/LocaleSelection.jsx +++ b/Parlance.ClientApp/src/pages/Administration/Locales/LocaleSelection.jsx @@ -1,25 +1,27 @@ import ListPageBlock from "../../../components/ListPageBlock"; -import {VerticalLayout} from "../../../components/Layouts"; +import { VerticalLayout } from "../../../components/Layouts"; import PageHeading from "../../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import SelectableList from "../../../components/SelectableList"; -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; export default function LocaleSelection(props) { const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const localeSelected = locale => { navigate(locale); }; - return
- - - {t("LOCALES")} - {t("LOCALES_PROMPT")} - - - -
-} \ No newline at end of file + return ( +
+ + + {t("LOCALES")} + {t("LOCALES_PROMPT")} + + + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Administration/Locales/LocaleSettings.jsx b/Parlance.ClientApp/src/pages/Administration/Locales/LocaleSettings.jsx index 8fc21065..0ceb9c28 100644 --- a/Parlance.ClientApp/src/pages/Administration/Locales/LocaleSettings.jsx +++ b/Parlance.ClientApp/src/pages/Administration/Locales/LocaleSettings.jsx @@ -1,13 +1,13 @@ -import {useNavigate, useParams} from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import BackButton from "../../../components/BackButton"; import ListPageBlock from "../../../components/ListPageBlock"; -import {VerticalLayout} from "../../../components/Layouts"; +import { VerticalLayout } from "../../../components/Layouts"; import PageHeading from "../../../components/PageHeading"; import LineEdit from "../../../components/LineEdit"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import SelectableList from "../../../components/SelectableList"; import i18n from "../../../helpers/i18n"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import Fetch from "../../../helpers/Fetch"; import LoadingModal from "../../../components/modals/LoadingModal"; import Modal from "../../../components/Modal"; @@ -17,39 +17,60 @@ import ErrorModal from "../../../components/modals/ErrorModal"; export default function LocaleSettings(props) { const [users, setUsers] = useState([]); const [addingUser, setAddingUser] = useState(""); - const {locale} = useParams(); + const { locale } = useParams(); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const updateUsers = async () => { let users = await Fetch.get(`/api/permissions/language/${locale}`); - setUsers(users.map(x => ({ - contents: x, - onClick: () => { - Modal.mount( - {t("GENERIC_PROMPT")} - - {[ - { - text: t("LANGUAGE_PERMISSION_REVOKE_BUTTON", {lang: i18n.humanReadableLocale(locale)}), - type: "destructive", - onClick: async () => { - Modal.mount(); - try { - await Fetch.delete(`/api/permissions/language/${locale}/${encodeURIComponent(x)}`, {}); - await updateUsers(); + setUsers( + users.map(x => ({ + contents: x, + onClick: () => { + Modal.mount( + + {t("GENERIC_PROMPT")} + + {[ + { + text: t( + "LANGUAGE_PERMISSION_REVOKE_BUTTON", + { + lang: i18n.humanReadableLocale( + locale, + ), + }, + ), + type: "destructive", + onClick: async () => { + Modal.mount(); + try { + await Fetch.delete( + `/api/permissions/language/${locale}/${encodeURIComponent(x)}`, + {}, + ); + await updateUsers(); - Modal.unmount(); - } catch (error) { - Modal.mount() - } - } - } - ]} - - ) - } - }))); + Modal.unmount(); + } catch (error) { + Modal.mount( + , + ); + } + }, + }, + ]} + + , + ); + }, + })), + ); }; useEffect(() => { @@ -59,36 +80,60 @@ export default function LocaleSettings(props) { const addUser = async () => { if (addingUser === "") return; - Modal.mount(); + Modal.mount(); try { - await Fetch.post(`/api/permissions/language/${locale}/${encodeURIComponent(addingUser)}`, {}); + await Fetch.post( + `/api/permissions/language/${locale}/${encodeURIComponent(addingUser)}`, + {}, + ); await updateUsers(); setAddingUser(""); Modal.unmount(); } catch (error) { - Modal.mount() + Modal.mount(); } - } + }; - return <> - navigate("..")}/> - - - {i18n.humanReadableLocale(locale)} - {t("LANGUAGE_PERMISSIONS_PROMPT", {lang: i18n.humanReadableLocale(locale)})} - - - - - - {t("LANGUAGE_PERMISSIONS_ADD_NEW")} - {t("translation:LANGUAGE_PERMISSIONS_ADD_NEW_PROMPT", {lang: i18n.humanReadableLocale(locale)})} - setAddingUser(e.target.value)}/> - {t("LANGUAGE_PERMISSIONS_ADD_NEW")} - - - -} \ No newline at end of file + return ( + <> + navigate("..")} /> + + + + {i18n.humanReadableLocale(locale)} + + + {t("LANGUAGE_PERMISSIONS_PROMPT", { + lang: i18n.humanReadableLocale(locale), + })} + + + + + + + + {t("LANGUAGE_PERMISSIONS_ADD_NEW")} + + + {t("translation:LANGUAGE_PERMISSIONS_ADD_NEW_PROMPT", { + lang: i18n.humanReadableLocale(locale), + })} + + setAddingUser(e.target.value)} + /> + + {t("LANGUAGE_PERMISSIONS_ADD_NEW")} + + + + + ); +} diff --git a/Parlance.ClientApp/src/pages/Administration/Locales/index.jsx b/Parlance.ClientApp/src/pages/Administration/Locales/index.jsx index fd4431c1..3fa09fce 100644 --- a/Parlance.ClientApp/src/pages/Administration/Locales/index.jsx +++ b/Parlance.ClientApp/src/pages/Administration/Locales/index.jsx @@ -1,10 +1,12 @@ -import {Route, Routes} from "react-router-dom"; +import { Route, Routes } from "react-router-dom"; import LocaleSelection from "./LocaleSelection"; import LocaleSettings from "./LocaleSettings"; export default function Locales(props) { - return - } path={"/"} /> - } path={"/:locale"} /> - -} \ No newline at end of file + return ( + + } path={"/"} /> + } path={"/:locale"} /> + + ); +} diff --git a/Parlance.ClientApp/src/pages/Administration/Projects/AddProject.jsx b/Parlance.ClientApp/src/pages/Administration/Projects/AddProject.jsx index 17f55660..639946da 100644 --- a/Parlance.ClientApp/src/pages/Administration/Projects/AddProject.jsx +++ b/Parlance.ClientApp/src/pages/Administration/Projects/AddProject.jsx @@ -1,10 +1,10 @@ import ListPageBlock from "../../../components/ListPageBlock"; import PageHeading from "../../../components/PageHeading"; -import {Trans, useTranslation} from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import BackButton from "../../../components/BackButton"; -import {useNavigate} from "react-router-dom"; -import {VerticalLayout} from "../../../components/Layouts"; -import {useState} from "react"; +import { useNavigate } from "react-router-dom"; +import { VerticalLayout } from "../../../components/Layouts"; +import { useState } from "react"; import SelectableList from "../../../components/SelectableList"; import LineEdit from "../../../components/LineEdit"; import Fetch from "../../../helpers/Fetch"; @@ -16,16 +16,16 @@ export default function (props) { const [cloneUrl, setCloneUrl] = useState(""); const [branch, setBranch] = useState("main"); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const addProject = async () => { - Modal.mount(); + Modal.mount(); try { await Fetch.post("/api/projects", { cloneUrl: cloneUrl, name: projectName, - branch: branch + branch: branch, }); Modal.unmount(); navigate(".."); @@ -34,29 +34,50 @@ export default function (props) { if (ex.status === 400) { message = (await ex.json()).extraData; } - Modal.mount( - {message} - ) + Modal.mount( + + {message} + , + ); } }; - return <> - navigate("..")}/> - - - {t("ADD_PROJECT")} - {t("ADD_PROJECT_PROMPT_1")} - setProjectName(e.target.value)} - placeholder={t("PROJECT_NAME")}/> - setCloneUrl(e.target.value)} - placeholder={t("GIT_CLONE_URL")}/> - setBranch(e.target.value)} placeholder={t("BRANCH")}/> - - Ensure that the project contains a .parlance.json file in the root. - - {t("ADD_PROJECT")} - - - -} \ No newline at end of file + return ( + <> + navigate("..")} /> + + + {t("ADD_PROJECT")} + {t("ADD_PROJECT_PROMPT_1")} + setProjectName(e.target.value)} + placeholder={t("PROJECT_NAME")} + /> + setCloneUrl(e.target.value)} + placeholder={t("GIT_CLONE_URL")} + /> + setBranch(e.target.value)} + placeholder={t("BRANCH")} + /> + + + Ensure that the project contains a{" "} + .parlance.json file in the root. + + + + {t("ADD_PROJECT")} + + + + + ); +} diff --git a/Parlance.ClientApp/src/pages/Administration/Projects/Project.jsx b/Parlance.ClientApp/src/pages/Administration/Projects/Project.jsx index bc73ba26..0587fb3e 100644 --- a/Parlance.ClientApp/src/pages/Administration/Projects/Project.jsx +++ b/Parlance.ClientApp/src/pages/Administration/Projects/Project.jsx @@ -1,15 +1,15 @@ import BackButton from "../../../components/BackButton"; import ListPageBlock from "../../../components/ListPageBlock"; -import {VerticalLayout, VerticalSpacer} from "../../../components/Layouts"; +import { VerticalLayout, VerticalSpacer } from "../../../components/Layouts"; import PageHeading from "../../../components/PageHeading"; import SelectableList from "../../../components/SelectableList"; -import {useNavigate, useParams} from "react-router-dom"; -import {useTranslation} from "react-i18next"; +import { useNavigate, useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import Modal from "../../../components/Modal"; import ErrorModal from "../../../components/modals/ErrorModal"; import Fetch from "../../../helpers/Fetch"; import LoadingModal from "../../../components/modals/LoadingModal"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import LineEdit from "../../../components/LineEdit"; import ModalList from "../../../components/ModalList"; @@ -17,115 +17,162 @@ export default function Project(props) { const [projectInfo, setProjectInfo] = useState({}); const [maintainers, setMaintainers] = useState([]); const [addingUser, setAddingUser] = useState(""); - const {project} = useParams(); + const { project } = useParams(); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const updateProjectInfo = async () => { const [projectInfo, maintainers] = await Promise.all([ await Fetch.get(`/api/projects/${project}`), - await Fetch.get(`/api/projects/${project}/maintainers`) + await Fetch.get(`/api/projects/${project}/maintainers`), ]); setProjectInfo(projectInfo); - setMaintainers(maintainers.map(x => ({ - contents: x, - onClick: () => { - Modal.mount( - {t("GENERIC_PROMPT")} - - {[ - { - text: t("PROJECT_MAINTAINER_REMOVE"), - type: "destructive", - onClick: async () => { - Modal.mount(); - try { - await Fetch.delete(`/api/projects/${project}/maintainers/${encodeURIComponent(x)}`, {}); - await updateProjectInfo(); + setMaintainers( + maintainers.map(x => ({ + contents: x, + onClick: () => { + Modal.mount( + + {t("GENERIC_PROMPT")} + + {[ + { + text: t("PROJECT_MAINTAINER_REMOVE"), + type: "destructive", + onClick: async () => { + Modal.mount(); + try { + await Fetch.delete( + `/api/projects/${project}/maintainers/${encodeURIComponent(x)}`, + {}, + ); + await updateProjectInfo(); - Modal.unmount(); - } catch (error) { - Modal.mount() - } - } - } - ]} - - ) - } - }))) - } + Modal.unmount(); + } catch (error) { + Modal.mount( + , + ); + } + }, + }, + ]} + + , + ); + }, + })), + ); + }; useEffect(() => { updateProjectInfo(); - }, []) + }, []); const deleteProject = () => { - Modal.mount( { - Modal.mount() - try { - await Fetch.delete(`/api/projects/${project}`); - navigate(".."); - Modal.unmount(); - } catch (error) { - Modal.mount() - } - }, - type: "destructive" - } - ]}> - - {t("PROJECT_DELETE_CONFIRM_PROMPT", {project: projectInfo.name})} - - ) - } + Modal.mount( + { + Modal.mount(); + try { + await Fetch.delete(`/api/projects/${project}`); + navigate(".."); + Modal.unmount(); + } catch (error) { + Modal.mount(); + } + }, + type: "destructive", + }, + ]} + > + + + {t("PROJECT_DELETE_CONFIRM_PROMPT", { + project: projectInfo.name, + })} + + + , + ); + }; const addMaintainer = async () => { if (addingUser === "") return; - Modal.mount(); + Modal.mount(); try { await Fetch.post(`/api/projects/${project}/maintainers`, { - name: addingUser + name: addingUser, }); await updateProjectInfo(); setAddingUser(""); Modal.unmount(); } catch (error) { - Modal.mount() + Modal.mount(); } - } + }; - return <> - navigate("..")}/> - - - {t("PROJECT_MAINTAINERS")} - {t("PROJECT_MAINTAINERS_PROMPT")} - {maintainers.length > 0 && <> - - - } - {t("PROJECT_MAINTAINERS_ADD_PROMPT")} - setAddingUser(e.target.value)}/> - {t("PROJECT_MAINTAINERS_ADD")} - - - - - {t("PROJECT_DELETE")} - {t("PROJECT_DELETE_PROMPT", {project: projectInfo.name})} - {t("PROJECT_DELETE")} - - - -} \ No newline at end of file + return ( + <> + navigate("..")} /> + + + + {t("PROJECT_MAINTAINERS")} + + {t("PROJECT_MAINTAINERS_PROMPT")} + {maintainers.length > 0 && ( + <> + + + + )} + {t("PROJECT_MAINTAINERS_ADD_PROMPT")} + setAddingUser(e.target.value)} + /> + + {t("PROJECT_MAINTAINERS_ADD")} + + + + + + {t("PROJECT_DELETE")} + + {t("PROJECT_DELETE_PROMPT", { + project: projectInfo.name, + })} + + + {t("PROJECT_DELETE")} + + + + + ); +} diff --git a/Parlance.ClientApp/src/pages/Administration/Projects/ProjectListing.jsx b/Parlance.ClientApp/src/pages/Administration/Projects/ProjectListing.jsx index e36dbb8e..910d42a6 100644 --- a/Parlance.ClientApp/src/pages/Administration/Projects/ProjectListing.jsx +++ b/Parlance.ClientApp/src/pages/Administration/Projects/ProjectListing.jsx @@ -1,45 +1,51 @@ import PageHeading from "../../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import ListPageBlock from "../../../components/ListPageBlock"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import SelectableList from "../../../components/SelectableList"; import Fetch from "../../../helpers/Fetch"; -import {VerticalLayout} from "../../../components/Layouts"; -import {useNavigate} from "react-router-dom"; +import { VerticalLayout } from "../../../components/Layouts"; +import { useNavigate } from "react-router-dom"; -export default function(props) { +export default function (props) { const [projects, setProjects] = useState([]); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const updateProjects = async () => { let projects = await Fetch.get("/api/projects"); - setProjects(projects.map(project => ({ - contents: project.name, - onClick: () => navigate(project.systemName) - }))); + setProjects( + projects.map(project => ({ + contents: project.name, + onClick: () => navigate(project.systemName), + })), + ); }; useEffect(() => { updateProjects(); }, []); - + const addProject = () => { navigate("add"); - } + }; - return <> - - - {t("PROJECTS")} - {t("PROJECT_LISTING_PROMPT")} - - - - - - {t("ADD_PROJECT")} - - - -} \ No newline at end of file + return ( + <> + + + {t("PROJECTS")} + {t("PROJECT_LISTING_PROMPT")} + + + + + + + {t("ADD_PROJECT")} + + + + + ); +} diff --git a/Parlance.ClientApp/src/pages/Administration/Projects/index.jsx b/Parlance.ClientApp/src/pages/Administration/Projects/index.jsx index 661b2883..1a30fc08 100644 --- a/Parlance.ClientApp/src/pages/Administration/Projects/index.jsx +++ b/Parlance.ClientApp/src/pages/Administration/Projects/index.jsx @@ -1,12 +1,14 @@ import ProjectListing from "./ProjectListing"; -import {Route, Routes} from "react-router-dom"; +import { Route, Routes } from "react-router-dom"; import AddProject from "./AddProject"; import Project from "./Project"; -export default function(props) { - return - } path={"/add"} /> - } path={"/"} /> - } path={"/:project"} /> - -} \ No newline at end of file +export default function (props) { + return ( + + } path={"/add"} /> + } path={"/"} /> + } path={"/:project"} /> + + ); +} diff --git a/Parlance.ClientApp/src/pages/Administration/SSH.jsx b/Parlance.ClientApp/src/pages/Administration/SSH.jsx index cd8e0779..4456b322 100644 --- a/Parlance.ClientApp/src/pages/Administration/SSH.jsx +++ b/Parlance.ClientApp/src/pages/Administration/SSH.jsx @@ -1,94 +1,112 @@ -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import Fetch from "../../helpers/Fetch"; import ListPageBlock from "../../components/ListPageBlock"; import PageHeading from "../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import SelectableList from "../../components/SelectableList"; import Modal from "../../components/Modal"; import LoadingModal from "../../components/modals/LoadingModal"; -import {VerticalLayout, VerticalSpacer} from "../../components/Layouts"; +import { VerticalLayout, VerticalSpacer } from "../../components/Layouts"; export default function (props) { const [haveSshKey, setHaveSshKey] = useState(null); const [sshKey, setSshKey] = useState(); - const {t} = useTranslation(); - + const { t } = useTranslation(); + const updateSshKey = async () => { try { - let keyDetails = await Fetch.get("/api/ssh"); - setSshKey(keyDetails.publicKey); + let keyDetails = await Fetch.get("/api/ssh"); + setSshKey(keyDetails.publicKey); setHaveSshKey(true); } catch { setHaveSshKey(false); } }; - + useEffect(() => { updateSshKey(); }, []); - + if (haveSshKey === null) { - return
+ return
; } - + const generateKey = async () => { Modal.mount(); await Fetch.post("/api/ssh", {}); await updateSshKey(); Modal.unmount(); }; - + const copyKey = async () => { await navigator.clipboard.writeText(sshKey); }; - + const deleteKey = async () => { - Modal.mount( { - Modal.mount(); - await Fetch.delete("/api/ssh", {}); - await updateSshKey(); - Modal.unmount(); - }, - destructive: true - } - ]}> - - {t("SERVER_SSH_KEY_DELETE_PROMPT_1")} - {t("SERVER_SSH_KEY_DELETE_PROMPT_2")} - - ) + Modal.mount( + { + Modal.mount(); + await Fetch.delete("/api/ssh", {}); + await updateSshKey(); + Modal.unmount(); + }, + destructive: true, + }, + ]} + > + + {t("SERVER_SSH_KEY_DELETE_PROMPT_1")} + {t("SERVER_SSH_KEY_DELETE_PROMPT_2")} + + , + ); }; - - let sshKeySection = haveSshKey ? + + let sshKeySection = haveSshKey ? ( <>

{t("SERVER_SSH_KEY_VALID_PROMPT")}

- {sshKey} + + {sshKey} + - - : <> + + + ) : ( + <>

{t("SERVER_SSH_KEY_INVALID_PROMPT")}

- {t("SERVER_SSH_KEY_GENERATE")} - ; - - return
- - {t("SERVER_SSH_KEY")} - {sshKeySection} - -
-} \ No newline at end of file + + {t("SERVER_SSH_KEY_GENERATE")} + + + ); + + return ( +
+ + {t("SERVER_SSH_KEY")} + {sshKeySection} + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Administration/Superusers.jsx b/Parlance.ClientApp/src/pages/Administration/Superusers.jsx index cd164dc4..abff05dd 100644 --- a/Parlance.ClientApp/src/pages/Administration/Superusers.jsx +++ b/Parlance.ClientApp/src/pages/Administration/Superusers.jsx @@ -1,109 +1,141 @@ import Container from "../../components/Container"; import PageHeading from "../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import ListPageBlock from "../../components/ListPageBlock"; import SelectableList from "../../components/SelectableList"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import Fetch from "../../helpers/Fetch"; import Modal from "../../components/Modal"; import LoadingModal from "../../components/modals/LoadingModal"; import UserManager from "../../helpers/UserManager"; import LineEdit from "../../components/LineEdit"; -import {VerticalLayout} from "../../components/Layouts"; +import { VerticalLayout } from "../../components/Layouts"; -export default function(props) { +export default function (props) { const [superusers, setSuperusers] = useState([]); const [promotingUser, setPromotingUser] = useState(""); - const {t} = useTranslation(); - + const { t } = useTranslation(); + const updateSuperusers = async () => { let superusers = await Fetch.get("/api/superusers"); - setSuperusers(superusers.map(x => ({ - contents: x, - onClick: () => { - if (x === UserManager.currentUser.username) return; - - Modal.mount( ({ + contents: x, + onClick: () => { + if (x === UserManager.currentUser.username) return; + + Modal.mount( + { + Modal.mount(); + try { + await Fetch.delete( + `/api/superusers/${encodeURIComponent(x)}`, + ); + await updateSuperusers(); + Modal.unmount(); + } catch (e) { + Modal.unmount(); + } + }, + }, + ]} + > +

{t("DEMOTE_SUPERUSER_PROMPT_1", { user: x })}

+

{t("DEMOTE_SUPERUSER_PROMPT_2", { user: x })}

+
, + ); + }, + })), + ); + }; + + useEffect(() => { + updateSuperusers(); + }, []); + + const promote = () => { + if (promotingUser === "") { + Modal.mount( + + {t("PROMOTE_NO_USER_PROMPT")} + , + ); + return; + } + + Modal.mount( + { - Modal.mount(); + Modal.mount(); try { - await Fetch.delete(`/api/superusers/${encodeURIComponent(x)}`); + await Fetch.post("/api/superusers", { + username: promotingUser, + }); await updateSuperusers(); + + setPromotingUser(""); Modal.unmount(); } catch (e) { Modal.unmount(); } - } - } - ]}> -

{t("DEMOTE_SUPERUSER_PROMPT_1", {user: x})}

-

{t("DEMOTE_SUPERUSER_PROMPT_2", {user: x})}

-
) - } - }))) + }, + }, + ]} + > + + + {t("PROMOTE_PROMPT_1", { user: promotingUser })} + + + {t("PROMOTE_PROMPT_2", { user: promotingUser })} + + +
, + ); }; - - useEffect(() => { - updateSuperusers(); - }, []); - - const promote = () => { - if (promotingUser === "") { - Modal.mount( - {t("PROMOTE_NO_USER_PROMPT")} - ) - return; - } - - Modal.mount( { - Modal.mount(); - try { - await Fetch.post("/api/superusers", { - username: promotingUser - }); - await updateSuperusers(); - - setPromotingUser(""); - Modal.unmount(); - } catch (e) { - Modal.unmount(); - } - } - } - ]}> - - {t("PROMOTE_PROMPT_1", {user: promotingUser})} - {t("PROMOTE_PROMPT_2", {user: promotingUser})} - - ) - } - - return <> - - - {t("SUPERUSERS")} - {t("SUPERUSER_PROMPT_1")} - - - - - - {t("PROMOTE_TO_SUPERUSER")} - {t("SUPERUSER_PROMOTE_PROMPT_1")} - setPromotingUser(e.target.value)} /> - {t("PROMOTE_TO_SUPERUSER")} - - - -} \ No newline at end of file + + return ( + <> + + + {t("SUPERUSERS")} + {t("SUPERUSER_PROMPT_1")} + + + + + + + {t("PROMOTE_TO_SUPERUSER")} + + {t("SUPERUSER_PROMOTE_PROMPT_1")} + setPromotingUser(e.target.value)} + /> + + {t("PROMOTE_TO_SUPERUSER")} + + + + + ); +} diff --git a/Parlance.ClientApp/src/pages/Administration/index.jsx b/Parlance.ClientApp/src/pages/Administration/index.jsx index b6548035..a9694b63 100644 --- a/Parlance.ClientApp/src/pages/Administration/index.jsx +++ b/Parlance.ClientApp/src/pages/Administration/index.jsx @@ -1,6 +1,6 @@ import React from "react"; import ListPage from "../../components/ListPage"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import Container from "../../components/Container"; import PageHeading from "../../components/PageHeading"; import Superusers from "./Superusers"; @@ -9,52 +9,61 @@ import Projects from "./Projects"; import Locales from "./Locales"; import Glossaries from "./Glossaries"; -export default function(props) { - const {t} = useTranslation(); - +export default function (props) { + const { t } = useTranslation(); + const items = [ t("PROJECTS"), { name: t("PROJECTS"), slug: "projects", - render: + render: , }, { name: t("GLOSSARIES"), slug: "glossaries", - render: + render: , }, t("SSH"), { name: t("SSH_KEYS"), slug: "ssh-keys", - render: + render: , }, t("USERS_AND_PERMISSIONS"), { name: t("SUPERUSERS"), slug: "superusers", - render: + render: , }, { name: t("LOCALES"), slug: "locales", - render: - } + render: , + }, ]; - - return
- -
- {t("PARLANCE_ADMINISTRATION")} - {t("PARLANCE_ADMINISTRATION_DESCRIPTION")} -
-
- -
-} \ No newline at end of file + + return ( +
+ +
+ {t("PARLANCE_ADMINISTRATION")} + + {t("PARLANCE_ADMINISTRATION_DESCRIPTION")} + +
+
+ +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/EmailUnsubscribe/index.tsx b/Parlance.ClientApp/src/pages/EmailUnsubscribe/index.tsx index 2d8fd441..4d16ddf6 100644 --- a/Parlance.ClientApp/src/pages/EmailUnsubscribe/index.tsx +++ b/Parlance.ClientApp/src/pages/EmailUnsubscribe/index.tsx @@ -1,46 +1,60 @@ import Container from "@/components/Container"; import PageHeading from "@/components/PageHeading"; import SelectableList from "@/components/SelectableList"; -import React, {useEffect, useMemo, useState} from "react"; -import {Trans, useTranslation} from "react-i18next"; -import {VerticalSpacer} from "@/components/Layouts"; -import {Subscription, UnsubscribeInformation} from "@/interfaces/unsubscribe"; +import React, { useEffect, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { VerticalSpacer } from "@/components/Layouts"; +import { Subscription, UnsubscribeInformation } from "@/interfaces/unsubscribe"; import Fetch from "@/helpers/Fetch"; import Spinner from "@/components/Spinner"; import Modal from "@/components/Modal"; import LoadingModal from "@/components/modals/LoadingModal"; -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import ModalList from "@/components/ModalList"; -import {AutoSubscribeEvent, UnsubscribeEvent} from "@/components/notifications/Events"; +import { + AutoSubscribeEvent, + UnsubscribeEvent, +} from "@/components/notifications/Events"; -type UnsubscribeOptions = "UnsubscribeOnly" | "UnsubscribeTerminateAutoSubscription" | "UnsubscribeTotally" | "UnverifyEmail"; +type UnsubscribeOptions = + | "UnsubscribeOnly" + | "UnsubscribeTerminateAutoSubscription" + | "UnsubscribeTotally" + | "UnverifyEmail"; export default function EmailUnsubscribe() { - const {t} = useTranslation(); + const { t } = useTranslation(); const token = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("subscription")!; }, []); - const [subscription, setSubscription] = useState(); + const [subscription, setSubscription] = useState< + Subscription | undefined + >(); const [emailNotificationsOn, setEmailNotificationsOn] = useState(false); const navigate = useNavigate(); const updateSubscriptionState = async () => { - const response = await Fetch.post("/api/emailunsubscribe", { - token: token - }); + const response = await Fetch.post( + "/api/emailunsubscribe", + { + token: token, + }, + ); setSubscription(response.subscription); setEmailNotificationsOn(response.emailNotificationsOn); - } - + }; + useEffect(() => { void updateSubscriptionState(); }, []); - + if (!subscription) { - return
- -
+ return ( +
+ +
+ ); } const unsubscribe = async (type: UnsubscribeOptions) => { @@ -49,90 +63,140 @@ export default function EmailUnsubscribe() { try { await Fetch.post("/api/emailunsubscribe/unsubscribe", { token: token, - unsubscribeOption: type + unsubscribeOption: type, }); - Modal.mount( { - Modal.unmount(); - navigate("/account/notifications") - } - } - ]}> - {t("UNSUBSCRIBE_SUCCESS")} - ) + Modal.mount( + { + Modal.unmount(); + navigate("/account/notifications"); + }, + }, + ]} + > + {t("UNSUBSCRIBE_SUCCESS")} + , + ); } catch (ex) { - Modal.mount( - {t("UNSUBSCRIBE_ERROR_BODY")} - ) + Modal.mount( + + {t("UNSUBSCRIBE_ERROR_BODY")} + , + ); } - } - + }; + const unsubscribeTotally = () => { - Modal.mount( - {t("EMAIL_UNSUBSCRIBE_COMPLETELY_DESCRIPTION_2")} - - {[ - { - text: t("EMAIL_UNSUBSCRIBE_COMPLETELY"), - type: "destructive", - onClick: () => unsubscribe("UnsubscribeTotally") - } - ]} - - ) - } + Modal.mount( + + {t("EMAIL_UNSUBSCRIBE_COMPLETELY_DESCRIPTION_2")} + + {[ + { + text: t("EMAIL_UNSUBSCRIBE_COMPLETELY"), + type: "destructive", + onClick: () => unsubscribe("UnsubscribeTotally"), + }, + ]} + + , + ); + }; const unverifyEmail = () => { - Modal.mount( - {t("UNSUBSCRIBE_UNVERIFY_CONFIRM_BODY")} - - {[ - { - text: t("EMAIL_UNVERIFY_BUTTON"), - type: "destructive", - onClick: () => unsubscribe("UnverifyEmail") - } - ]} - - ) - } - - return
- - {t("UNSUBSCRIBE")} -

- - Unsubscribe from ? - -

- unsubscribe("UnsubscribeOnly") - }, - subscription.autoSubscription && { - contents: - Unsubscribe me and stop automatically subscribing me when I - , - onClick: () => unsubscribe("UnsubscribeTerminateAutoSubscription") - } - ]}/> -
- {emailNotificationsOn && <> - - {t("EMAIL_UNSUBSCRIBE_COMPLETELY_HEADER")} -

{t("EMAIL_UNSUBSCRIBE_COMPLETELY_DESCRIPTION_1")}

-

{t("EMAIL_UNSUBSCRIBE_COMPLETELY_DESCRIPTION_2")}

- {t("EMAIL_UNSUBSCRIBE_COMPLETELY")} -
+ Modal.mount( + + {t("UNSUBSCRIBE_UNVERIFY_CONFIRM_BODY")} + + {[ + { + text: t("EMAIL_UNVERIFY_BUTTON"), + type: "destructive", + onClick: () => unsubscribe("UnverifyEmail"), + }, + ]} + + , + ); + }; + + return ( +
- {t("EMAIL_UNSUBSCRIBE_UNVERIFY_HEADER")} -

{t("EMAIL_UNSUBSCRIBE_UNVERIFY_DESCRIPTION")}

- {t("EMAIL_UNVERIFY_BUTTON")} + {t("UNSUBSCRIBE")} +

+ + Unsubscribe from{" "} + ? + +

+ unsubscribe("UnsubscribeOnly"), + }, + subscription.autoSubscription && { + contents: ( + + Unsubscribe me and stop automatically + subscribing me when I{" "} + + + ), + onClick: () => + unsubscribe( + "UnsubscribeTerminateAutoSubscription", + ), + }, + ]} + />
- } -
+ {emailNotificationsOn && ( + <> + + + {t("EMAIL_UNSUBSCRIBE_COMPLETELY_HEADER")} + +

{t("EMAIL_UNSUBSCRIBE_COMPLETELY_DESCRIPTION_1")}

+

{t("EMAIL_UNSUBSCRIBE_COMPLETELY_DESCRIPTION_2")}

+ + {t("EMAIL_UNSUBSCRIBE_COMPLETELY")} + +
+ + + {t("EMAIL_UNSUBSCRIBE_UNVERIFY_HEADER")} + +

{t("EMAIL_UNSUBSCRIBE_UNVERIFY_DESCRIPTION")}

+ + {t("EMAIL_UNVERIFY_BUTTON")} + +
+ + )} +
+ ); } diff --git a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryLanguageSelector.module.css b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryLanguageSelector.module.css index ef6f3c19..f0ee8ede 100644 --- a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryLanguageSelector.module.css +++ b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryLanguageSelector.module.css @@ -1,7 +1,7 @@ .language { padding: 6px; cursor: pointer; - + border-radius: var(--border-radius); } @@ -16,6 +16,6 @@ .languageListInner { background: var(--layer-color); border-radius: var(--border-radius); - + padding: 3px; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryLanguageSelector.tsx b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryLanguageSelector.tsx index c2def557..e2e4e1eb 100644 --- a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryLanguageSelector.tsx +++ b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryLanguageSelector.tsx @@ -1,55 +1,92 @@ import BackButton from "../../../components/BackButton"; -import {useNavigate, useParams} from "react-router-dom"; -import {useTranslation} from "react-i18next"; -import {VerticalLayout} from "../../../components/Layouts"; +import { useNavigate, useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { VerticalLayout } from "../../../components/Layouts"; import UserManager from "../../../helpers/UserManager"; -import {GlossaryItem} from "../../../interfaces/glossary"; +import { GlossaryItem } from "../../../interfaces/glossary"; import I18n from "../../../helpers/i18n"; -import Styles from "./GlossaryLanguageSelector.module.css" +import Styles from "./GlossaryLanguageSelector.module.css"; import PageHeading from "../../../components/PageHeading"; interface GlossaryLanguageSelectorProps { className: string; - glossaryData: GlossaryItem[] + glossaryData: GlossaryItem[]; } interface LanguagePickerProps { - lang: string + lang: string; } -function LanguagePicker({lang}: LanguagePickerProps) { - const {glossary, language} = useParams(); +function LanguagePicker({ lang }: LanguagePickerProps) { + const { glossary, language } = useParams(); const navigate = useNavigate(); - + const navigateToLanguage = () => { navigate(language ? `../${glossary}/${lang}` : lang); - } - - return
- {I18n.humanReadableLocale(lang)} -
+ }; + + return ( +
+ {I18n.humanReadableLocale(lang)} +
+ ); } -export default function GlossaryLanguageSelector({className, glossaryData}: GlossaryLanguageSelectorProps) { - const {t} = useTranslation(); +export default function GlossaryLanguageSelector({ + className, + glossaryData, +}: GlossaryLanguageSelectorProps) { + const { t } = useTranslation(); const navigate = useNavigate(); - let showLanguages = [...UserManager.currentUser?.languagePermissions ?? [], ...glossaryData.map(item => item.lang)] - showLanguages = showLanguages.filter((x, i) => showLanguages.indexOf(x) === i); - const myLanguages = UserManager.currentUser?.languagePermissions && showLanguages.filter(lang => UserManager.currentUser!.languagePermissions.includes(lang)); - const otherLanguages = UserManager.currentUser?.languagePermissions ? showLanguages.filter(lang => !UserManager.currentUser!.languagePermissions.includes(lang)) : showLanguages; - - - return - navigate("../")} inTranslationView={true}/> - {myLanguages && myLanguages?.length !== 0 &&
- {t("MY_LANGUAGES")} - {myLanguages?.map(lang => )} -
} - {otherLanguages && otherLanguages?.length !== 0 &&
- {t("OTHER_LANGUAGES")} - {otherLanguages?.map(lang => )} -
} -
-} \ No newline at end of file + let showLanguages = [ + ...(UserManager.currentUser?.languagePermissions ?? []), + ...glossaryData.map(item => item.lang), + ]; + showLanguages = showLanguages.filter( + (x, i) => showLanguages.indexOf(x) === i, + ); + const myLanguages = + UserManager.currentUser?.languagePermissions && + showLanguages.filter(lang => + UserManager.currentUser!.languagePermissions.includes(lang), + ); + const otherLanguages = UserManager.currentUser?.languagePermissions + ? showLanguages.filter( + lang => + !UserManager.currentUser!.languagePermissions.includes(lang), + ) + : showLanguages; + + return ( + + navigate("../")} + inTranslationView={true} + /> + {myLanguages && myLanguages?.length !== 0 && ( +
+ + {t("MY_LANGUAGES")} + + {myLanguages?.map(lang => )} +
+ )} + {otherLanguages && otherLanguages?.length !== 0 && ( +
+ + {t("OTHER_LANGUAGES")} + + {otherLanguages?.map(lang => ( + + ))} +
+ )} +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryTable.module.css b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryTable.module.css index 1eefe99d..bee0ab03 100644 --- a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryTable.module.css +++ b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryTable.module.css @@ -1,14 +1,13 @@ .errorView { flex-grow: 1; align-self: center; - + display: flex; align-items: center; justify-items: center; } .errorViewInner { - } .glossaryTable { @@ -21,12 +20,13 @@ padding: 9px; display: grid; - grid-template-areas: "term pos" - "translation translation" - "buttons buttons"; + grid-template-areas: + "term pos" + "translation translation" + "buttons buttons"; grid-template-columns: max-content 1fr; gap: 6px; - + background-color: var(--layer-color); border-radius: var(--border-radius); } @@ -49,7 +49,7 @@ .buttons { grid-area: buttons; justify-self: end; - + display: flex; gap: 6px; } @@ -59,7 +59,7 @@ justify-content: center; padding: 6px; gap: 6px; - + cursor: pointer; } @@ -72,7 +72,7 @@ input.searchBox { .searchContainer { display: flex; flex-direction: column; - + background-color: var(--layer-color); border-radius: var(--border-radius); } @@ -85,4 +85,4 @@ input.searchBox { .backButton { display: block; } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryTable.tsx b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryTable.tsx index 8849f6d4..54f95c0c 100644 --- a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryTable.tsx +++ b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/GlossaryTable.tsx @@ -1,25 +1,29 @@ -import {useNavigate, useParams} from "react-router-dom"; -import {VerticalLayout} from "../../../components/Layouts"; +import { useNavigate, useParams } from "react-router-dom"; +import { VerticalLayout } from "../../../components/Layouts"; import PageHeading from "../../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import SilentInformation from "../../../components/SilentInformation"; import Styles from "./GlossaryTable.module.css"; import GlossaryLookup from "../../Projects/Subprojects/Languages/Translation/TranslationEditor/GlossaryLookup"; -import {Glossary, GlossaryItem, PartOfSpeechTranslationString} from "../../../interfaces/glossary"; +import { + Glossary, + GlossaryItem, + PartOfSpeechTranslationString, +} from "../../../interfaces/glossary"; import SmallButton from "../../../components/SmallButton"; import Icon from "../../../components/Icon"; import Modal from "../../../components/Modal"; import AddToGlossaryModal from "../../../components/modals/glossary/AddToGlossaryModal"; import Fetch from "../../../helpers/Fetch"; import ErrorModal from "../../../components/modals/ErrorModal"; -import React, {useState} from "react"; +import React, { useState } from "react"; import BackButton from "../../../components/BackButton"; import i18n from "@/helpers/i18n"; interface GlossaryTableProps { - className: string - glossaryData: GlossaryItem[] + className: string; + glossaryData: GlossaryItem[]; onGlossaryItemAdded: (item: GlossaryItem) => void; onGlossaryItemDeleted: (item: GlossaryItem) => void; glossaryObject: Glossary; @@ -27,68 +31,135 @@ interface GlossaryTableProps { } interface NoGlossaryViewProps { - className: string + className: string; } -function NoGlossaryView({className}: NoGlossaryViewProps) { - const {t} = useTranslation(); - - return
-
- - - +function NoGlossaryView({ className }: NoGlossaryViewProps) { + const { t } = useTranslation(); + + return ( +
+
+ + + +
-
; + ); } -export default function GlossaryTable({className, glossaryData, onGlossaryItemAdded, onGlossaryItemDeleted, glossaryObject, canTranslate}: GlossaryTableProps) { - const {t} = useTranslation(); +export default function GlossaryTable({ + className, + glossaryData, + onGlossaryItemAdded, + onGlossaryItemDeleted, + glossaryObject, + canTranslate, +}: GlossaryTableProps) { + const { t } = useTranslation(); const navigate = useNavigate(); - const {glossary, language} = useParams(); + const { glossary, language } = useParams(); const [searchQuery, setSearchQuery] = useState(""); - + if (!language) { - return + return ; } - + const glossaryItems = glossaryData.filter(item => item.lang == language); - + const onAdd = () => { - Modal.mount(); - } - + Modal.mount( + , + ); + }; + const onDelete = async (item: GlossaryItem) => { onGlossaryItemDeleted(item); - + try { - await Fetch.delete(`/api/GlossaryManager/${glossary}/${language}/${item.id}`); + await Fetch.delete( + `/api/GlossaryManager/${glossary}/${language}/${item.id}`, + ); } catch (e) { - Modal.mount( window.location.reload()} okButtonText={t("RELOAD")} />) + Modal.mount( + window.location.reload()} + okButtonText={t("RELOAD")} + />, + ); } - } - - const visibleGlossaryItems = searchQuery ? glossaryItems.filter(x => x.term.toLowerCase().includes(searchQuery.toLowerCase())) : glossaryItems - - return
- navigate(`../${glossary}`)} inTranslationView={true} /> -
- setSearchQuery((e.target as HTMLInputElement).value)}/> -
-
- {visibleGlossaryItems.map(match =>
- {match.term} - {match.translation && {t(PartOfSpeechTranslationString(match.partOfSpeech))}} - {match.translation || "?"} - - {canTranslate && onDelete(match)}>{t("DELETE")}} - -
)} - {canTranslate &&
- - {t("ADD_TO_GLOSSARY")} -
} + }; + + const visibleGlossaryItems = searchQuery + ? glossaryItems.filter(x => + x.term.toLowerCase().includes(searchQuery.toLowerCase()), + ) + : glossaryItems; + + return ( +
+ navigate(`../${glossary}`)} + inTranslationView={true} + /> +
+ + setSearchQuery((e.target as HTMLInputElement).value) + } + /> +
+
+ {visibleGlossaryItems.map(match => ( +
+ {match.term} + {match.translation && ( + + {t( + PartOfSpeechTranslationString( + match.partOfSpeech, + ), + )} + + )} + + {match.translation || "?"} + + + {canTranslate && ( + onDelete(match)}> + {t("DELETE")} + + )} + +
+ ))} + {canTranslate && ( +
+ + {t("ADD_TO_GLOSSARY")} +
+ )} +
-
-} \ No newline at end of file + ); +} diff --git a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/index.module.css b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/index.module.css index 9b026677..58c8f609 100644 --- a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/index.module.css +++ b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/index.module.css @@ -3,7 +3,7 @@ grid-template-areas: "language table"; grid-template-columns: 400px 1fr; overflow-y: hidden; - + padding: 3px; gap: 3px; } @@ -15,7 +15,7 @@ .table { grid-area: table; - + display: flex; flex-direction: column; align-items: stretch; @@ -29,28 +29,28 @@ grid-template-areas: "language"; grid-template-columns: 1fr; } - + .languageEditor.root { grid-template-areas: "table"; } - + :global(.ltr) .language { border: none; } - + :global(.rtl) .language { border: none; } - + .table { display: none; } - + .languageEditor .table { display: flex; } - + .languageEditor .language { display: none; } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/index.tsx b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/index.tsx index 5c0638c2..db0a4360 100644 --- a/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/index.tsx +++ b/Parlance.ClientApp/src/pages/Glossaries/GlossaryEditor/index.tsx @@ -1,53 +1,73 @@ import BackButton from "components/BackButton"; -import {useNavigate, useParams} from "react-router-dom"; -import {useTranslation} from "react-i18next"; +import { useNavigate, useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; -import Styles from "./index.module.css" +import Styles from "./index.module.css"; import GlossaryLanguageSelector from "./GlossaryLanguageSelector"; import GlossaryTable from "./GlossaryTable"; -import {useEffect, useMemo, useState} from "react"; -import {Glossary, GlossaryItem} from "../../../interfaces/glossary"; +import { useEffect, useMemo, useState } from "react"; +import { Glossary, GlossaryItem } from "../../../interfaces/glossary"; import Spinner from "../../../components/Spinner"; import Fetch from "../../../helpers/Fetch"; import UserManager from "../../../helpers/UserManager"; export default function GlossaryEditor() { const navigate = useNavigate(); - const {t} = useTranslation(); - const {glossary, language} = useParams(); + const { t } = useTranslation(); + const { glossary, language } = useParams(); const [glossaryData, setGlossaryData] = useState([]); const [glossaryObject, setGlossaryObject] = useState(); const [done, setDone] = useState(false); - const canTranslate = useMemo(() => !!(UserManager.isLoggedIn && (UserManager.currentUserIsSuperuser || UserManager.currentUser?.languagePermissions?.some(x => x === language))), [ - language - ]); - + const canTranslate = useMemo( + () => + !!( + UserManager.isLoggedIn && + (UserManager.currentUserIsSuperuser || + UserManager.currentUser?.languagePermissions?.some( + x => x === language, + )) + ), + [language], + ); + const updateGlossary = async () => { setGlossaryData(await Fetch.get(`/api/GlossaryManager/${glossary}`)); const glossaries = await Fetch.get(`/api/GlossaryManager`); setGlossaryObject(glossaries.find(x => x.id == glossary)); setDone(true); - } - + }; + useEffect(() => { updateGlossary(); - }, []) - + }, []); + if (!done) { - return + return ; } - + const onGlossaryItemAdded = (item: GlossaryItem) => { setGlossaryData([...glossaryData, item]); - } - + }; + const onGlossaryItemDeleted = (item: GlossaryItem) => { setGlossaryData([...glossaryData.filter(x => x.id !== item.id)]); - } - - return
- - -
-} \ No newline at end of file + }; + + return ( +
+ + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Glossaries/ServerGlossaryListing.tsx b/Parlance.ClientApp/src/pages/Glossaries/ServerGlossaryListing.tsx index 765ed46e..2043fc00 100644 --- a/Parlance.ClientApp/src/pages/Glossaries/ServerGlossaryListing.tsx +++ b/Parlance.ClientApp/src/pages/Glossaries/ServerGlossaryListing.tsx @@ -1,26 +1,27 @@ -import {VerticalSpacer} from "../../components/Layouts"; +import { VerticalSpacer } from "../../components/Layouts"; import Container from "../../components/Container"; import PageHeading from "../../components/PageHeading"; import SelectableList from "../../components/SelectableList"; import i18n from "../../helpers/i18n"; -import React, {useEffect, useState} from "react"; -import {useNavigate} from "react-router-dom"; -import {useTranslation} from "react-i18next"; +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import Hero from "../../components/Hero"; import ErrorCover from "../../components/ErrorCover"; import Fetch from "../../helpers/Fetch"; -import {Glossary} from "../../interfaces/glossary"; +import { Glossary } from "../../interfaces/glossary"; export default function ServerGlossaryListing() { const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const [glossaries, setGlossaries] = useState([]); const [done, setDone] = useState(false); const [error, setError] = useState(); const updateGlossaries = async () => { try { - let glossaryData = await Fetch.get(`/api/glossarymanager`); + let glossaryData = + await Fetch.get(`/api/glossarymanager`); setGlossaries(glossaryData); setDone(true); } catch (err) { @@ -31,21 +32,29 @@ export default function ServerGlossaryListing() { useEffect(() => { updateGlossaries(); }, []); - + const glossaryClicked = (glossary: Glossary) => { navigate(glossary.id); - } - - return
- - - - {t("GLOSSARIES")} - ({ - contents: g.name, - onClick: () => glossaryClicked(g) - })) : SelectableList.PreloadingText()}/> - - -
-} \ No newline at end of file + }; + + return ( +
+ + + + {t("GLOSSARIES")} + ({ + contents: g.name, + onClick: () => glossaryClicked(g), + })) + : SelectableList.PreloadingText() + } + /> + + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Glossaries/index.tsx b/Parlance.ClientApp/src/pages/Glossaries/index.tsx index cdb11949..8fb24481 100644 --- a/Parlance.ClientApp/src/pages/Glossaries/index.tsx +++ b/Parlance.ClientApp/src/pages/Glossaries/index.tsx @@ -1,11 +1,13 @@ -import {Route, Routes} from "react-router-dom"; +import { Route, Routes } from "react-router-dom"; import ServerGlossaryListing from "./ServerGlossaryListing"; import GlossaryEditor from "./GlossaryEditor"; -export default function() { - return - } path={"/"} /> - } path={"/:glossary"} /> - } path={"/:glossary/:language"} /> - -} \ No newline at end of file +export default function () { + return ( + + } path={"/"} /> + } path={"/:glossary"} /> + } path={"/:glossary/:language"} /> + + ); +} diff --git a/Parlance.ClientApp/src/pages/Home/index.module.css b/Parlance.ClientApp/src/pages/Home/index.module.css index eb1c44a6..13abeb18 100644 --- a/Parlance.ClientApp/src/pages/Home/index.module.css +++ b/Parlance.ClientApp/src/pages/Home/index.module.css @@ -53,4 +53,4 @@ .promoSub { font-size: 20pt; } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Home/index.tsx b/Parlance.ClientApp/src/pages/Home/index.tsx index 6d396c6e..971cb7e8 100644 --- a/Parlance.ClientApp/src/pages/Home/index.tsx +++ b/Parlance.ClientApp/src/pages/Home/index.tsx @@ -1,40 +1,46 @@ -import React, {ReactNode, useContext } from 'react'; -import {useTranslation} from "react-i18next"; -import {useNavigate} from "react-router-dom"; +import React, { ReactNode, useContext } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; import SmallButton from "../../components/SmallButton"; import Styles from "./index.module.css"; -import {ServerInformationContext} from "../../context/ServerInformationContext"; +import { ServerInformationContext } from "../../context/ServerInformationContext"; -function Promo({children}: { - children: ReactNode -}) { - return
-
- {children} +function Promo({ children }: { children: ReactNode }) { + return ( +
+
{children}
-
+ ); } export function Home() { const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const serverInformation = useContext(ServerInformationContext); return (
-
{t("HOME_HERO_TITLE", {serverName: serverInformation.serverName})}
-
{t("HOME_HERO_SUBTITLE", {serverName: serverInformation.serverName})}
+
+ {t("HOME_HERO_TITLE", { + serverName: serverInformation.serverName, + })} +
+
+ {t("HOME_HERO_SUBTITLE", { + serverName: serverInformation.serverName, + })} +
- navigate("projects")}>{t("PROJECTS")} + navigate("projects")}> + {t("PROJECTS")} +
); } -Home.displayName = Home.name +Home.displayName = Home.name; diff --git a/Parlance.ClientApp/src/pages/Languages/ServerLanguageListing.jsx b/Parlance.ClientApp/src/pages/Languages/ServerLanguageListing.jsx index 2a36ca7a..5b2294f4 100644 --- a/Parlance.ClientApp/src/pages/Languages/ServerLanguageListing.jsx +++ b/Parlance.ClientApp/src/pages/Languages/ServerLanguageListing.jsx @@ -1,12 +1,12 @@ -import {useNavigate} from "react-router-dom"; -import React, {useEffect, useReducer, useState} from "react"; +import { useNavigate } from "react-router-dom"; +import React, { useEffect, useReducer, useState } from "react"; import Hero from "../../components/Hero"; import BackButton from "../../components/BackButton"; -import {VerticalSpacer} from "../../components/Layouts"; +import { VerticalSpacer } from "../../components/Layouts"; import ErrorCover from "../../components/ErrorCover"; import PageHeading from "../../components/PageHeading"; import SelectableList from "../../components/SelectableList"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import UserManager from "../../helpers/UserManager"; import Fetch from "../../helpers/Fetch"; import i18n from "../../helpers/i18n"; @@ -20,7 +20,7 @@ export default function ServerLanguageListing() { const [done, setDone] = useState(false); const [error, setError] = useState(); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); UserManager.on("currentUserChanged", forceUpdate); @@ -39,55 +39,112 @@ export default function ServerLanguageListing() { }, []); const seenLanguages = []; - const showLanguages = [...languages, ...(UserManager.currentUser?.languagePermissions?.map(language => ({ - language, - completionData: {} - })) || [])] + const showLanguages = [ + ...languages, + ...(UserManager.currentUser?.languagePermissions?.map(language => ({ + language, + completionData: {}, + })) || []), + ] .filter(x => { if (seenLanguages.includes(x.language)) return false; seenLanguages.push(x.language); return true; }) - .sort((a, b) => i18n.humanReadableLocale(a.language) > i18n.humanReadableLocale(b.language)); + .sort( + (a, b) => + i18n.humanReadableLocale(a.language) > + i18n.humanReadableLocale(b.language), + ); const translationClicked = language => { if (languages.some(l => l.language === language)) { navigate(language); } else { - Modal.mount( - {t("START_NEW_TRANSLATION_GUIDE")} - ) + Modal.mount( + + {t("START_NEW_TRANSLATION_GUIDE")} + , + ); } - } + }; - const myLanguages = UserManager.currentUser?.languagePermissions && showLanguages.filter(lang => UserManager.currentUser.languagePermissions.includes(lang.language)); - const otherLanguages = UserManager.currentUser?.languagePermissions ? showLanguages.filter(lang => !UserManager.currentUser.languagePermissions.includes(lang.language)) : showLanguages; + const myLanguages = + UserManager.currentUser?.languagePermissions && + showLanguages.filter(lang => + UserManager.currentUser.languagePermissions.includes(lang.language), + ); + const otherLanguages = UserManager.currentUser?.languagePermissions + ? showLanguages.filter( + lang => + !UserManager.currentUser.languagePermissions.includes( + lang.language, + ), + ) + : showLanguages; - return
- - - {myLanguages && - <> - - {t("MY_LANGUAGES")} - ({ - contents: , - onClick: () => translationClicked(p.language) - })) : TranslationProgressIndicator.PreloadContents()}/> - - - } - - {myLanguages ? t("OTHER_LANGUAGES") : t("AVAILABLE_LANGUAGES")} - ({ - contents: , - onClick: () => translationClicked(p.language) - })) : TranslationProgressIndicator.PreloadContents()}/> - - -
-} \ No newline at end of file + return ( +
+ + + {myLanguages && ( + <> + + + {t("MY_LANGUAGES")} + + ({ + contents: ( + + ), + onClick: () => + translationClicked( + p.language, + ), + })) + : TranslationProgressIndicator.PreloadContents() + } + /> + + + )} + + + {myLanguages + ? t("OTHER_LANGUAGES") + : t("AVAILABLE_LANGUAGES")} + + ({ + contents: ( + + ), + onClick: () => + translationClicked(p.language), + })) + : TranslationProgressIndicator.PreloadContents() + } + /> + + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Languages/ServerLanguageProjectListing.jsx b/Parlance.ClientApp/src/pages/Languages/ServerLanguageProjectListing.jsx index ce19822f..c1b56d6d 100644 --- a/Parlance.ClientApp/src/pages/Languages/ServerLanguageProjectListing.jsx +++ b/Parlance.ClientApp/src/pages/Languages/ServerLanguageProjectListing.jsx @@ -1,5 +1,5 @@ -import {useNavigate, useParams} from "react-router-dom"; -import React, {useEffect, useReducer, useState} from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import React, { useEffect, useReducer, useState } from "react"; import Fetch from "../../helpers/Fetch"; import Container from "../../components/Container"; import PageHeading from "../../components/PageHeading"; @@ -8,10 +8,10 @@ import i18n from "../../helpers/i18n"; import TranslationProgressIndicator from "../../components/TranslationProgressIndicator"; import UserManager from "../../helpers/UserManager"; import Modal from "../../components/Modal"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import ErrorModal from "../../components/modals/ErrorModal"; import LoadingModal from "../../components/modals/LoadingModal"; -import {VerticalSpacer} from "../../components/Layouts"; +import { VerticalSpacer } from "../../components/Layouts"; import BackButton from "../../components/BackButton"; import ErrorCover from "../../components/ErrorCover"; import Hero from "../../components/Hero"; @@ -20,18 +20,20 @@ import LoginUsernameModal from "../../components/modals/account/LoginUsernameMod export default function ServerLanguageProjectListing() { const [ignored, forceUpdate] = useReducer(x => x + 1, 0); - const {language} = useParams(); + const { language } = useParams(); const [subprojectData, setSubprojectData] = useState([]); const [done, setDone] = useState(false); const [error, setError] = useState(); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); UserManager.on("currentUserChanged", forceUpdate); const updateProjects = async () => { try { - let subprojectData = await Fetch.get(`/api/projects/languages/${language}`); + let subprojectData = await Fetch.get( + `/api/projects/languages/${language}`, + ); setSubprojectData(subprojectData); setDone(true); } catch (err) { @@ -43,72 +45,133 @@ export default function ServerLanguageProjectListing() { updateProjects(); }, []); - const translationClicked = (project, subproject) => { if (subproject.completionData) { - navigate(`../../projects/${project.systemName}/${subproject.systemName}/${subproject.realLocale}`); + navigate( + `../../projects/${project.systemName}/${subproject.systemName}/${subproject.realLocale}`, + ); } else { if (!UserManager.isLoggedIn) { - Modal.mount( { - Modal.mount() - } - }, - Modal.OkButton - ]}> - {t("START_NEW_TRANSLATION_LOGIN_REQUIRED")} - ) + Modal.mount( + { + Modal.mount(); + }, + }, + Modal.OkButton, + ]} + > + {t("START_NEW_TRANSLATION_LOGIN_REQUIRED")} + , + ); return; } - Modal.mount( { - Modal.mount(); + Modal.mount( + { + Modal.mount(); - try { - await Fetch.post(`/api/projects/${project.systemName}/${subproject.systemName}/${subproject.realLocale}`, {}); - navigate(`../../projects/${project.systemName}/${subproject.systemName}/${subproject.realLocale}`); - Modal.unmount(); - } catch (error) { - Modal.mount() - } - } - } - ]}> - {t("START_NEW_TRANSLATION_PROMPT", {lang: i18n.humanReadableLocale(subproject.realLocale)})} - ) + try { + await Fetch.post( + `/api/projects/${project.systemName}/${subproject.systemName}/${subproject.realLocale}`, + {}, + ); + navigate( + `../../projects/${project.systemName}/${subproject.systemName}/${subproject.realLocale}`, + ); + Modal.unmount(); + } catch (error) { + Modal.mount(); + } + }, + }, + ]} + > + {t("START_NEW_TRANSLATION_PROMPT", { + lang: i18n.humanReadableLocale(subproject.realLocale), + })} + , + ); } - } - - return
- - navigate("../")}/> - - {done ? subprojectData.map(project => - <> - - - {project.name} - ({ - contents: , - onClick: () => translationClicked(project, sp) - }))}/> - - - ) : [1, 2, 3].map(() => - TEXT - - ) - } - -
-} \ No newline at end of file + }; + + return ( +
+ + navigate("../")} + /> + + {done + ? subprojectData.map(project => ( + <> + + + + {project.name} + + ({ + contents: ( + + ), + onClick: () => + translationClicked( + project, + sp, + ), + }), + )} + /> + + + + )) + : [1, 2, 3].map(() => ( + + + + TEXT + + + + + ))} + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Languages/index.jsx b/Parlance.ClientApp/src/pages/Languages/index.jsx index 24524c77..55c8ba3d 100644 --- a/Parlance.ClientApp/src/pages/Languages/index.jsx +++ b/Parlance.ClientApp/src/pages/Languages/index.jsx @@ -1,10 +1,15 @@ import ServerLanguageListing from "./ServerLanguageListing"; -import {Route, Routes} from "react-router-dom"; +import { Route, Routes } from "react-router-dom"; import ServerLanguageProjectListing from "./ServerLanguageProjectListing"; -export default function(props) { - return - } path={"/"} /> - } path={"/:language"} /> - -} \ No newline at end of file +export default function (props) { + return ( + + } path={"/"} /> + } + path={"/:language"} + /> + + ); +} diff --git a/Parlance.ClientApp/src/pages/Projects/ProjectListing.jsx b/Parlance.ClientApp/src/pages/Projects/ProjectListing.jsx index 9b41157b..043658c8 100644 --- a/Parlance.ClientApp/src/pages/Projects/ProjectListing.jsx +++ b/Parlance.ClientApp/src/pages/Projects/ProjectListing.jsx @@ -1,21 +1,21 @@ import Container from "../../components/Container"; import PageHeading from "../../components/PageHeading"; -import React, {useEffect, useState} from "react"; +import React, { useEffect, useState } from "react"; import Fetch from "../../helpers/Fetch"; import SelectableList from "../../components/SelectableList"; -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import TranslationProgressIndicator from "../../components/TranslationProgressIndicator"; -import {useTranslation} from "react-i18next"; -import {VerticalSpacer} from "../../components/Layouts"; +import { useTranslation } from "react-i18next"; +import { VerticalSpacer } from "../../components/Layouts"; import ErrorCover from "../../components/ErrorCover"; -import {calculateDeadline} from "../../helpers/Misc"; +import { calculateDeadline } from "../../helpers/Misc"; export default function ProjectListing() { const [projects, setProjects] = useState([]); const [done, setDone] = useState(false); const [error, setError] = useState(false); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const updateProjects = async () => { try { @@ -31,17 +31,37 @@ export default function ProjectListing() { updateProjects(); }, []); - return
- - - {t("AVAILABLE_PROJECTS")} - calculateDeadline(a.deadline).ms - calculateDeadline(b.deadline).ms).map(p => ({ - contents: , - onClick: () => navigate(p.systemName) - })) : TranslationProgressIndicator.PreloadContents()}/> - - -
-} \ No newline at end of file + return ( +
+ + + + {t("AVAILABLE_PROJECTS")} + + + calculateDeadline(a.deadline).ms - + calculateDeadline(b.deadline).ms, + ) + .map(p => ({ + contents: ( + + ), + onClick: () => navigate(p.systemName), + })) + : TranslationProgressIndicator.PreloadContents() + } + /> + + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Glossaries/index.module.css b/Parlance.ClientApp/src/pages/Projects/Subprojects/Glossaries/index.module.css index bcdfb460..9209ea67 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Glossaries/index.module.css +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Glossaries/index.module.css @@ -60,19 +60,19 @@ .glossaryItemIcon { visibility: visible; } - + .mobileSwitcher { display: block; } - + .glossaryManagerRoot { display: grid; grid-template-columns: 100% 100%; - + transition: transform 0.25s ease-in-out; } - + .mobileSwitch .glossaryManagerRoot { transform: translateX(calc(-100% - 3px)); } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Glossaries/index.tsx b/Parlance.ClientApp/src/pages/Projects/Subprojects/Glossaries/index.tsx index e300d475..f11fec64 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Glossaries/index.tsx +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Glossaries/index.tsx @@ -1,13 +1,19 @@ -import {VerticalLayout, VerticalSpacer} from "../../../../components/Layouts"; -import React, {ReactElement, ReactNode, useEffect, useMemo, useState} from "react"; +import { VerticalLayout, VerticalSpacer } from "../../../../components/Layouts"; +import React, { + ReactElement, + ReactNode, + useEffect, + useMemo, + useState, +} from "react"; import BackButton from "../../../../components/BackButton"; -import {useTranslation} from "react-i18next"; -import {useNavigate, useParams} from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams } from "react-router-dom"; import PageHeading from "../../../../components/PageHeading"; import Container from "../../../../components/Container"; -import Styles from "./index.module.css" -import {Glossary} from "../../../../interfaces/glossary"; +import Styles from "./index.module.css"; +import { Glossary } from "../../../../interfaces/glossary"; import Fetch from "../../../../helpers/Fetch"; import SelectableList from "../../../../components/SelectableList"; import Modal from "../../../../components/Modal"; @@ -22,117 +28,195 @@ interface GlossaryListProps { onLeft: boolean; } -function GlossaryList({glossaries, title, shiftGlossary, onLeft}: GlossaryListProps): ReactElement { - const {t} = useTranslation(); - +function GlossaryList({ + glossaries, + title, + shiftGlossary, + onLeft, +}: GlossaryListProps): ReactElement { + const { t } = useTranslation(); + let list: ReactNode; if (glossaries === null) { - list = + list = ; } else if (glossaries.length === 0) { - list = t("NO_GLOSSARIES") + list = t("NO_GLOSSARIES"); } else { - list = ({ - contents:
- {x.name} - -
, - onClick: () => shiftGlossary(x), - containerClass: Styles.glossaryItemContainer - }))} />; + list = ( + ({ + contents: ( +
+ {x.name} + +
+ ), + onClick: () => shiftGlossary(x), + containerClass: Styles.glossaryItemContainer, + }))} + /> + ); } - - return
- {title} - {list} -
+ + return ( +
+ + {title} + + {list} +
+ ); } -function GlossaryManager() : ReactElement { - const {t} = useTranslation(); - const {project} = useParams(); - const [connectedGlossaries, setConnectedGlossaries] = useState(null); - const [disconnectedGlossaries, setDisconnectedGlossaries] = useState(null); +function GlossaryManager(): ReactElement { + const { t } = useTranslation(); + const { project } = useParams(); + const [connectedGlossaries, setConnectedGlossaries] = useState< + Glossary[] | null + >(null); + const [disconnectedGlossaries, setDisconnectedGlossaries] = useState< + Glossary[] | null + >(null); const [mobileSwitch, setMobileSwitch] = useState(false); - + useEffect(() => { - (async() => { + (async () => { let [glossaries, connectedGlossaries] = await Promise.all([ Fetch.get("/api/glossarymanager"), - Fetch.get(`/api/projects/${project}/glossary`) + Fetch.get(`/api/projects/${project}/glossary`), ]); setConnectedGlossaries(connectedGlossaries); - setDisconnectedGlossaries(glossaries.filter(x => !connectedGlossaries.some(y => x.id === y.id))); + setDisconnectedGlossaries( + glossaries.filter( + x => !connectedGlossaries.some(y => x.id === y.id), + ), + ); })(); }, []); - + const connectGlossary = async (glossary: Glossary) => { - setConnectedGlossaries([...connectedGlossaries as Glossary[], glossary]); - - let disconnected = [...disconnectedGlossaries as Glossary[]]; - disconnected.splice(disconnectedGlossaries!.findIndex(x => x.id == glossary.id), 1); - setDisconnectedGlossaries(disconnected) - + setConnectedGlossaries([ + ...(connectedGlossaries as Glossary[]), + glossary, + ]); + + let disconnected = [...(disconnectedGlossaries as Glossary[])]; + disconnected.splice( + disconnectedGlossaries!.findIndex(x => x.id == glossary.id), + 1, + ); + setDisconnectedGlossaries(disconnected); + try { await Fetch.post(`/api/projects/${project}/glossary`, { - glossaryId: glossary.id - }) + glossaryId: glossary.id, + }); } catch (e) { - Modal.mount( window.location.reload()} okButtonText={t("RELOAD")} />) + Modal.mount( + window.location.reload()} + okButtonText={t("RELOAD")} + />, + ); } - } - + }; + const disconnectGlossary = async (glossary: Glossary) => { - setDisconnectedGlossaries([...disconnectedGlossaries as Glossary[], glossary]); - - let connected = [...connectedGlossaries as Glossary[]]; - connected.splice(connectedGlossaries!.findIndex(x => x.id == glossary.id), 1); + setDisconnectedGlossaries([ + ...(disconnectedGlossaries as Glossary[]), + glossary, + ]); + + let connected = [...(connectedGlossaries as Glossary[])]; + connected.splice( + connectedGlossaries!.findIndex(x => x.id == glossary.id), + 1, + ); setConnectedGlossaries(connected); - + try { - await Fetch.delete(`/api/projects/${project}/glossary/${glossary.id}`) + await Fetch.delete( + `/api/projects/${project}/glossary/${glossary.id}`, + ); } catch (e) { - Modal.mount( window.location.reload()} okButtonText={t("RELOAD")} />) + Modal.mount( + window.location.reload()} + okButtonText={t("RELOAD")} + />, + ); } - } - - return <> -
-
- - -
- setMobileSwitch(true)}> - {t("MANAGE_GLOSSARIES_MOBILE_SHIFT_1")} - -
-
- - -
- setMobileSwitch(false)}> - {t("MANAGE_GLOSSARIES_MOBILE_SHIFT_2")} - -
-
+ }; + + return ( + <> +
+
+ + +
+ setMobileSwitch(true)} + > + {t("MANAGE_GLOSSARIES_MOBILE_SHIFT_1")} + +
+
+ + +
+ setMobileSwitch(false)} + > + {t("MANAGE_GLOSSARIES_MOBILE_SHIFT_2")} + +
+
+
-
- + + ); } -export default function Glossaries() : ReactElement { - const {t} = useTranslation(); +export default function Glossaries(): ReactElement { + const { t } = useTranslation(); const navigate = useNavigate(); - - return
- navigate("..")}/> - - - {t("MANAGE_GLOSSARIES")} - {t("MANAGE_GLOSSARIES_PROMPT_1")} -
- {t("MANAGE_GLOSSARIES_PROMPT_2")} -
-
- -
-} \ No newline at end of file + + return ( +
+ navigate("..")} + /> + + + + {t("MANAGE_GLOSSARIES")} + + {t("MANAGE_GLOSSARIES_PROMPT_1")} +
+ {t("MANAGE_GLOSSARIES_PROMPT_2")} +
+
+ +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/LanguageListing.jsx b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/LanguageListing.jsx index c97d949d..6aee4a75 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/LanguageListing.jsx +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/LanguageListing.jsx @@ -1,5 +1,5 @@ -import {useNavigate, useParams} from "react-router-dom"; -import React, {useEffect, useReducer, useState} from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import React, { useEffect, useReducer, useState } from "react"; import Fetch from "../../../../helpers/Fetch"; import Container from "../../../../components/Container"; import PageHeading from "../../../../components/PageHeading"; @@ -8,29 +8,31 @@ import i18n from "../../../../helpers/i18n"; import TranslationProgressIndicator from "../../../../components/TranslationProgressIndicator"; import UserManager from "../../../../helpers/UserManager"; import Modal from "../../../../components/Modal"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import ErrorModal from "../../../../components/modals/ErrorModal"; import LoadingModal from "../../../../components/modals/LoadingModal"; -import {VerticalSpacer} from "../../../../components/Layouts"; +import { VerticalSpacer } from "../../../../components/Layouts"; import BackButton from "../../../../components/BackButton"; import ErrorCover from "../../../../components/ErrorCover"; import Hero from "../../../../components/Hero"; export default function LanguageListing() { const [ignored, forceUpdate] = useReducer(x => x + 1, 0); - const {project, subproject} = useParams(); + const { project, subproject } = useParams(); const [subprojectData, setSubprojectData] = useState({}); const [languages, setLanguages] = useState([]); const [done, setDone] = useState(false); const [error, setError] = useState(); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); UserManager.on("currentUserChanged", forceUpdate); const updateProjects = async () => { try { - let subprojectData = await Fetch.get(`/api/projects/${project}/${subproject}`); + let subprojectData = await Fetch.get( + `/api/projects/${project}/${subproject}`, + ); setSubprojectData(subprojectData); setLanguages(subprojectData.availableLanguages); setDone(true); @@ -44,72 +46,159 @@ export default function LanguageListing() { }, []); const seenLanguages = []; - const showLanguages = [...languages, ...(UserManager.currentUser?.languagePermissions?.map(language => ({ - language, - completionData: {} - })) || [])] + const showLanguages = [ + ...languages, + ...(UserManager.currentUser?.languagePermissions?.map(language => ({ + language, + completionData: {}, + })) || []), + ] .filter(x => { if (seenLanguages.includes(x.language)) return false; seenLanguages.push(x.language); return true; }) - .sort((a, b) => i18n.humanReadableLocale(a.language) > i18n.humanReadableLocale(b.language)); + .sort( + (a, b) => + i18n.humanReadableLocale(a.language) > + i18n.humanReadableLocale(b.language), + ); const translationClicked = language => { if (languages.some(l => l.language === language)) { navigate(language); } else { - Modal.mount( { - Modal.mount(); + Modal.mount( + { + Modal.mount(); - try { - await Fetch.post(`/api/projects/${project}/${subproject}/${language}`, {}); - navigate(language); - Modal.unmount(); - } catch (error) { - Modal.mount() - } - } - } - ]}> - {t("START_NEW_TRANSLATION_PROMPT", {lang: i18n.humanReadableLocale(language)})} - ) + try { + await Fetch.post( + `/api/projects/${project}/${subproject}/${language}`, + {}, + ); + navigate(language); + Modal.unmount(); + } catch (error) { + Modal.mount(); + } + }, + }, + ]} + > + {t("START_NEW_TRANSLATION_PROMPT", { + lang: i18n.humanReadableLocale(language), + })} + , + ); } - } + }; - const myLanguages = UserManager.currentUser?.languagePermissions && showLanguages.filter(lang => UserManager.currentUser.languagePermissions.includes(lang.language)); - const otherLanguages = UserManager.currentUser?.languagePermissions ? showLanguages.filter(lang => !UserManager.currentUser.languagePermissions.includes(lang.language)) : showLanguages; + const myLanguages = + UserManager.currentUser?.languagePermissions && + showLanguages.filter(lang => + UserManager.currentUser.languagePermissions.includes(lang.language), + ); + const otherLanguages = UserManager.currentUser?.languagePermissions + ? showLanguages.filter( + lang => + !UserManager.currentUser.languagePermissions.includes( + lang.language, + ), + ) + : showLanguages; - return
- - navigate("../..")}/> - - {myLanguages && - <> - - {t("MY_LANGUAGES")} - ({ - contents: , - onClick: () => translationClicked(p.language) - })) : TranslationProgressIndicator.PreloadContents()}/> - - - } - - {myLanguages ? t("OTHER_LANGUAGES") : t("AVAILABLE_LANGUAGES")} - ({ - contents: , - onClick: () => translationClicked(p.language) - })) : TranslationProgressIndicator.PreloadContents()}/> - - -
-} \ No newline at end of file + return ( +
+ + navigate("../..")} + /> + + {myLanguages && ( + <> + + + {t("MY_LANGUAGES")} + + ({ + contents: ( + + ), + onClick: () => + translationClicked( + p.language, + ), + })) + : TranslationProgressIndicator.PreloadContents() + } + /> + + + )} + + + {myLanguages + ? t("OTHER_LANGUAGES") + : t("AVAILABLE_LANGUAGES")} + + ({ + contents: ( + + ), + onClick: () => + translationClicked(p.language), + })) + : TranslationProgressIndicator.PreloadContents() + } + /> + + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/CommentsDashboard.tsx b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/CommentsDashboard.tsx index 35c360f3..1eb6e761 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/CommentsDashboard.tsx +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/CommentsDashboard.tsx @@ -1,52 +1,93 @@ import ListPageBlock from "../../../../../components/ListPageBlock"; -import {VerticalLayout} from "../../../../../components/Layouts"; +import { VerticalLayout } from "../../../../../components/Layouts"; import PageHeading from "../../../../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import SelectableList from "../../../../../components/SelectableList"; -import {useEffect, useState} from "react"; -import {Thread} from "../../../../../interfaces/comments"; -import {useParams} from "react-router-dom"; +import { useEffect, useState } from "react"; +import { Thread } from "../../../../../interfaces/comments"; +import { useParams } from "react-router-dom"; import Fetch from "../../../../../helpers/Fetch"; -import {ThreadItem} from "../../../../../components/comments/ThreadItem"; +import { ThreadItem } from "../../../../../components/comments/ThreadItem"; import PreloadingBlock from "../../../../../components/PreloadingBlock"; import SilentInformation from "../../../../../components/SilentInformation"; import Modal from "../../../../../components/Modal"; -import {CommentsThreadModal} from "./TranslationEditor/Comments/CommentsModal"; +import { CommentsThreadModal } from "./TranslationEditor/Comments/CommentsModal"; export function CommentsDashboard() { - const {project, subproject, language} = useParams(); - const {t} = useTranslation(); + const { project, subproject, language } = useParams(); + const { t } = useTranslation(); const [comments, setComments] = useState(); - + const updateComments = async () => { setComments(null); - setComments(await Fetch.get(`/api/comments/${project}/${subproject}/${language}`)); - } - + setComments( + await Fetch.get( + `/api/comments/${project}/${subproject}/${language}`, + ), + ); + }; + useEffect(() => { void updateComments(); }, []); - + const openThread = (thread: Thread) => { - Modal.mount() - } - - return
- - - {t("COMMENTS")} - {comments?.length === 0 ? -
- -
: <> - {comments ?
{t("COMMENT_OPEN_THREADS", {count: comments.length})}
: {t("COMMENT_OPEN_THREADS", {count: 0})}} - ({ - contents: {}} />, - onClick: () => openThread(thread) - })) : SelectableList.PreloadingText(3)}/> - - } -
-
-
+ Modal.mount( + , + ); + }; + + return ( +
+ + + {t("COMMENTS")} + {comments?.length === 0 ? ( +
+ +
+ ) : ( + <> + {comments ? ( +
+ {t("COMMENT_OPEN_THREADS", { + count: comments.length, + })} +
+ ) : ( + + {t("COMMENT_OPEN_THREADS", { count: 0 })} + + )} + ({ + contents: ( + {}} + /> + ), + onClick: () => openThread(thread), + })) + : SelectableList.PreloadingText(3) + } + /> + + )} +
+
+
+ ); } diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/Dashboard.tsx b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/Dashboard.tsx index aab38476..2c2eb423 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/Dashboard.tsx +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/Dashboard.tsx @@ -1,6 +1,6 @@ -import {useTranslation} from "react-i18next"; -import {useNavigate, useParams} from "react-router-dom"; -import React, {useEffect, useState} from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams } from "react-router-dom"; +import React, { useEffect, useState } from "react"; import Fetch from "../../../../../helpers/Fetch"; import i18n from "../../../../../helpers/i18n"; import Overview from "./Overview"; @@ -9,54 +9,67 @@ import Hero from "../../../../../components/Hero"; import BackButton from "../../../../../components/BackButton"; import Spinner from "../../../../../components/Spinner"; import GlossariesDashboard from "./GlossariesDashboard"; -import {SubprojectLocaleMeta} from "../../../../../interfaces/projects"; -import {CommentsDashboard} from "./CommentsDashboard"; +import { SubprojectLocaleMeta } from "../../../../../interfaces/projects"; +import { CommentsDashboard } from "./CommentsDashboard"; export default function Dashboard() { - const {project, subproject, language} = useParams(); + const { project, subproject, language } = useParams(); const [data, setData] = useState(); const navigate = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); const updateData = async () => { - setData(await Fetch.get(`/api/projects/${project}/${subproject}/${language}`)); - } + setData( + await Fetch.get( + `/api/projects/${project}/${subproject}/${language}`, + ), + ); + }; useEffect(() => { updateData(); }, []); //TODO - if (!data) return + if (!data) return ; const items = [ t("DASHBOARD"), { name: t("OVERVIEW"), slug: "overview", - render: , - default: true + render: , + default: true, }, { name: t("GLOSSARIES"), slug: "glossaries", - render: + render: , }, { name: t("COMMENTS"), slug: "comments", - render: - } + render: , + }, ]; - return
- navigate("../..")}/> - navigate("translate") - } - ]}/> - -
+ return ( +
+ navigate("../..")} + /> + navigate("translate"), + }, + ]} + /> + +
+ ); } diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/GlossariesDashboard.tsx b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/GlossariesDashboard.tsx index 93e6af5d..43a94a34 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/GlossariesDashboard.tsx +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/GlossariesDashboard.tsx @@ -1,44 +1,54 @@ import Spinner from "../../../../../components/Spinner"; -import {Glossary} from "../../../../../interfaces/glossary"; -import React, {useEffect, useState} from "react"; +import { Glossary } from "../../../../../interfaces/glossary"; +import React, { useEffect, useState } from "react"; import Fetch from "../../../../../helpers/Fetch"; -import {useNavigate, useParams} from "react-router-dom"; -import {VerticalLayout} from "../../../../../components/Layouts"; +import { useNavigate, useParams } from "react-router-dom"; +import { VerticalLayout } from "../../../../../components/Layouts"; import PageHeading from "../../../../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import ListPageBlock from "../../../../../components/ListPageBlock"; import SelectableList from "../../../../../components/SelectableList"; export default function GlossariesDashboard() { - const {t} = useTranslation(); - const {project, language} = useParams(); + const { t } = useTranslation(); + const { project, language } = useParams(); const [glossaries, setGlossaries] = useState([]); const [done, setDone] = useState(false); const navigate = useNavigate(); - + const loadGlossaries = async () => { - setGlossaries(await Fetch.get(`/api/projects/${project}/glossary`)); + setGlossaries( + await Fetch.get(`/api/projects/${project}/glossary`), + ); setDone(true); }; - + useEffect(() => { loadGlossaries(); }, []); - + const glossaryClicked = (glossary: Glossary) => { - navigate(`/glossaries/${glossary.id}/${language}`) - } - - return
- - - {t("GLOSSARIES")} - {t("GLOSSARIES_DASHBOARD_DESCRIPTION")} - ({ - contents: g.name, - onClick: () => glossaryClicked(g) - })) : SelectableList.PreloadingText()}/> - - -
-} \ No newline at end of file + navigate(`/glossaries/${glossary.id}/${language}`); + }; + + return ( +
+ + + {t("GLOSSARIES")} + {t("GLOSSARIES_DASHBOARD_DESCRIPTION")} + ({ + contents: g.name, + onClick: () => glossaryClicked(g), + })) + : SelectableList.PreloadingText() + } + /> + + +
+ ); +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/Overview.tsx b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/Overview.tsx index 8f8733b0..02c10782 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/Overview.tsx +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/Overview.tsx @@ -1,38 +1,45 @@ -import {SubprojectLocaleMeta} from "../../../../../interfaces/projects"; -import {VerticalLayout} from "../../../../../components/Layouts"; +import { SubprojectLocaleMeta } from "../../../../../interfaces/projects"; +import { VerticalLayout } from "../../../../../components/Layouts"; import ListPageBlock from "../../../../../components/ListPageBlock"; import PageHeading from "../../../../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import SelectableList from "../../../../../components/SelectableList"; -import {useNavigate} from "react-router-dom"; +import { useNavigate } from "react-router-dom"; -export default function Overview({data}: { - data: SubprojectLocaleMeta -}) { - const {t} = useTranslation(); +export default function Overview({ data }: { data: SubprojectLocaleMeta }) { + const { t } = useTranslation(); const navigate = useNavigate(); - + const goToComments = () => { navigate("../comments"); - } - - return <> - - - {t("OVERVIEW_STATS")} -
- {data.completionData.complete}/{data.completionData.count} strings translated -
-
{data.completionData.warnings} warnings
-
{data.completionData.errors} errors
-
-
- - - {t("COMMENTS")} -
{t("COMMENT_OPEN_THREADS", { count: data.openThreads.length })}
- {t("OVERVIEW_GO_TO_COMMENTS")} -
-
- -} \ No newline at end of file + }; + + return ( + <> + + + {t("OVERVIEW_STATS")} +
+ {data.completionData.complete}/ + {data.completionData.count} strings translated +
+
{data.completionData.warnings} warnings
+
{data.completionData.errors} errors
+
+
+ + + {t("COMMENTS")} +
+ {t("COMMENT_OPEN_THREADS", { + count: data.openThreads.length, + })} +
+ + {t("OVERVIEW_GO_TO_COMMENTS")} + +
+
+ + ); +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/AssistantArea/index.jsx b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/AssistantArea/index.jsx index 84135c87..dfffa29d 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/AssistantArea/index.jsx +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/AssistantArea/index.jsx @@ -1,16 +1,16 @@ import Styles from "./index.module.css"; -import {useParams} from "react-router-dom"; -import {VerticalLayout} from "../../../../../../../components/Layouts"; +import { useParams } from "react-router-dom"; +import { VerticalLayout } from "../../../../../../../components/Layouts"; import PageHeading from "../../../../../../../components/PageHeading"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import useTranslationEntries from "../EntryUtils"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import SmallButton from "../../../../../../../components/SmallButton"; import PreloadingBlock from "../../../../../../../components/PreloadingBlock"; -import {Box} from "../Box"; +import { Box } from "../Box"; -function SuggestedTranslation({suggestion, index, translationDirection}) { - const {t} = useTranslation(); +function SuggestedTranslation({ suggestion, index, translationDirection }) { + const { t } = useTranslation(); let type; switch (suggestion?.type) { @@ -18,29 +18,47 @@ function SuggestedTranslation({suggestion, index, translationDirection}) { break; } - return
-
-
{suggestion?.source || - text}
-
{suggestion?.translation || - text}
-
-
-
- {t("Copy to Translation")} + return ( +
+
+
+ {suggestion?.source || ( + text + )} +
+
+ {suggestion?.translation || ( + text + )} +
+
+
+
+ {t("Copy to Translation")} +
-
+ ); } -export default function AssistantArea({entries, searchParams, translationDirection}) { - const {project, subproject, language, key} = useParams(); +export default function AssistantArea({ + entries, + searchParams, + translationDirection, +}) { + const { project, subproject, language, key } = useParams(); const [suggested, setSuggested] = useState([]); const [loading, setLoading] = useState(false); - const {entry} = useTranslationEntries(entries, searchParams); - const {t} = useTranslation(); + const { entry } = useTranslationEntries(entries, searchParams); + const { t } = useTranslation(); useEffect(() => { setSuggested([]); @@ -56,40 +74,58 @@ export default function AssistantArea({entries, searchParams, translationDirecti }, 500); return () => clearTimeout(timeout); - }, [entry]) + }, [entry]); - let suggestions = loading ? <> - - - - - - : (suggested.length === 0 ?
- {t("SUGGESTIONS_NO_SUGGESTIONS")} -
: suggested.map((result, i) => )) + let suggestions = loading ? ( + <> + + + + + + + ) : suggested.length === 0 ? ( +
{t("SUGGESTIONS_NO_SUGGESTIONS")}
+ ) : ( + suggested.map((result, i) => ( + + )) + ); - return
-
- - -
- {t("ASSISTANT")} - {t("ASSISTANT_DESCRIPTION")} -
-
-
- - - {t("ASSISTANT_SUGGESTED_TRANSLATIONS")} -
- {suggestions} -
-
-
- {/**/} - {/* {t("ASSISTANT_RESOURCES")}*/} - {/**/} + return ( +
+
+ + +
+ + {t("ASSISTANT")} + + {t("ASSISTANT_DESCRIPTION")} +
+
+
+ + + + {t("ASSISTANT_SUGGESTED_TRANSLATIONS")} + +
+ {suggestions} +
+
+
+ {/**/} + {/* {t("ASSISTANT_RESOURCES")}*/} + {/**/} +
-
-} \ No newline at end of file + ); +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/AssistantArea/index.module.css b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/AssistantArea/index.module.css index 327f2d2c..9be926cc 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/AssistantArea/index.module.css +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/AssistantArea/index.module.css @@ -5,7 +5,7 @@ .assistantAreaInner { position: sticky; top: 0; - + display: flex; flex-direction: column; gap: 3px; @@ -24,7 +24,7 @@ grid-template-columns: 1fr; grid-template-rows: 0px 1fr 1fr 0px; gap: 0px 0px; - grid-template-areas: + grid-template-areas: "border" "meta" "translation" @@ -88,9 +88,8 @@ gap: 6px; } - @media (max-width: 1024px) { .assistantArea { display: none; } -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Box.module.css b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Box.module.css index 004c03b3..cb78f2c5 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Box.module.css +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Box.module.css @@ -3,4 +3,4 @@ flex-direction: column; border-radius: var(--border-radius); background-color: var(--layer-color); -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Box.tsx b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Box.tsx index 893d9fd2..03c6baa2 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Box.tsx +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Box.tsx @@ -1,8 +1,10 @@ -import {HTMLAttributes, ReactNode } from "react"; -import Styles from "./Box.module.css" +import { HTMLAttributes, ReactNode } from "react"; +import Styles from "./Box.module.css"; export function Box(props: HTMLAttributes) { - return
- {props.children} -
+ return ( +
+ {props.children} +
+ ); } diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/CommentsModal.module.css b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/CommentsModal.module.css index 3c84799c..acc92fa0 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/CommentsModal.module.css +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/CommentsModal.module.css @@ -8,4 +8,4 @@ .noThreads { padding: 6px; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/CommentsModal.tsx b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/CommentsModal.tsx index 8010e1ee..640fd452 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/CommentsModal.tsx +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/CommentsModal.tsx @@ -1,28 +1,34 @@ -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import Modal from "../../../../../../../components/Modal"; -import {useState} from "react"; -import {VerticalLayout} from "../../../../../../../components/Layouts"; +import { useState } from "react"; +import { VerticalLayout } from "../../../../../../../components/Layouts"; import PageHeading from "../../../../../../../components/PageHeading"; import Styles from "./CommentsModal.module.css"; import Icon from "../../../../../../../components/Icon"; import ThreadView from "./ThreadView"; import ThreadReplyArea from "./ThreadReplyArea"; -import {Thread} from "../../../../../../../interfaces/comments"; -import {ThreadItem} from "../../../../../../../components/comments/ThreadItem"; +import { Thread } from "../../../../../../../interfaces/comments"; +import { ThreadItem } from "../../../../../../../components/comments/ThreadItem"; import UserManager from "../../../../../../../helpers/UserManager"; - -export function CommentsModal({project, subproject, language, tkey, threads, onUpdateThreads}: { - project: string, - subproject: string, - language: string, - tkey: string, - threads: Thread[], - onUpdateThreads: () => void, +export function CommentsModal({ + project, + subproject, + language, + tkey, + threads, + onUpdateThreads, +}: { + project: string; + subproject: string; + language: string; + tkey: string; + threads: Thread[]; + onUpdateThreads: () => void; }) { const [currentThread, setCurrentThread] = useState(); - const {t} = useTranslation(); + const { t } = useTranslation(); const goBack = () => { if (currentThread) { @@ -32,38 +38,84 @@ export function CommentsModal({project, subproject, language, tkey, threads, onU } }; - return - {currentThread ? : - <> - -
- {t("THREADS")} -
- {threads.length ? threads.map((x, i) => ) :
- {t("THREADS_NO_THREADS")} -
} -
- {UserManager.currentUser && <> - + return ( + + {currentThread ? ( + + ) : ( + <> +
- {t("THREADS_NEW_THREAD")} + {t("THREADS")}
- + {threads.length ? ( + threads.map((x, i) => ( + + )) + ) : ( +
+ {t("THREADS_NO_THREADS")} +
+ )}
- } - } -
+ {UserManager.currentUser && ( + <> + +
+ + {t("THREADS_NEW_THREAD")} + +
+ +
+ + )} + + )} +
+ ); } -export function CommentsThreadModal({thread, onUpdateThreads, showHeader}: { - thread: Thread - onUpdateThreads: () => void, - showHeader?: boolean +export function CommentsThreadModal({ + thread, + onUpdateThreads, + showHeader, +}: { + thread: Thread; + onUpdateThreads: () => void; + showHeader?: boolean; }) { - return Modal.unmount()}> - {}} onReloadThreads={onUpdateThreads} showHeader={showHeader} /> - -} \ No newline at end of file + return ( + Modal.unmount()} + > + {}} + onReloadThreads={onUpdateThreads} + showHeader={showHeader} + /> + + ); +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/ThreadReplyArea.module.css b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/ThreadReplyArea.module.css index eaa35d8d..c9ee009f 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/ThreadReplyArea.module.css +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/ThreadReplyArea.module.css @@ -58,4 +58,4 @@ input.titleBox { .loggedOutMessage { padding: 9px; -} \ No newline at end of file +} diff --git a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/ThreadReplyArea.tsx b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/ThreadReplyArea.tsx index aa1b9efd..d88c79e5 100644 --- a/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/ThreadReplyArea.tsx +++ b/Parlance.ClientApp/src/pages/Projects/Subprojects/Languages/Translation/TranslationEditor/Comments/ThreadReplyArea.tsx @@ -1,39 +1,40 @@ -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import Styles from "./ThreadReplyArea.module.css"; import Button from "../../../../../../../components/Button"; import UserManager from "../../../../../../../helpers/UserManager"; import Fetch from "../../../../../../../helpers/Fetch"; -import {useState} from "react"; -import {Comment, Thread} from "../../../../../../../interfaces/comments"; +import { useState } from "react"; +import { Comment, Thread } from "../../../../../../../interfaces/comments"; interface CloseResponse { - comments: Comment[], - thread: Thread + comments: Comment[]; + thread: Thread; } export default function ThreadReplyArea({ - project, - subproject, - language, - tkey, - thread, - onReloadThreads, - onCurrentThreadChanged, onThreadDataChanged - }: { - project?: string, - subproject?: string, - language?: string, - tkey?: string, - thread?: Thread, - onReloadThreads: () => void, - onCurrentThreadChanged: (thread: Thread | null) => void, - onThreadDataChanged?: (threadData: Comment[]) => void + project, + subproject, + language, + tkey, + thread, + onReloadThreads, + onCurrentThreadChanged, + onThreadDataChanged, +}: { + project?: string; + subproject?: string; + language?: string; + tkey?: string; + thread?: Thread; + onReloadThreads: () => void; + onCurrentThreadChanged: (thread: Thread | null) => void; + onThreadDataChanged?: (threadData: Comment[]) => void; }) { const [title, setTitle] = useState(""); const [body, setBody] = useState(""); const [error, setError] = useState(""); - const {t} = useTranslation(); + const { t } = useTranslation(); const threadId = thread?.id; @@ -41,22 +42,27 @@ export default function ThreadReplyArea({ setError(""); if (threadId) { try { - onThreadDataChanged!(await Fetch.post(`/api/comments/${thread.id}`, { - body: body - })); + onThreadDataChanged!( + await Fetch.post(`/api/comments/${thread.id}`, { + body: body, + }), + ); } catch { - setError(t("COMMENT_SEND_FAILURE_PROMPT")) + setError(t("COMMENT_SEND_FAILURE_PROMPT")); return; } } else { let thread: Thread | null; try { - thread = await Fetch.post(`/api/comments/${project}/${subproject}/${language}/${tkey}`, { - title: title, - body: body - }); + thread = await Fetch.post( + `/api/comments/${project}/${subproject}/${language}/${tkey}`, + { + title: title, + body: body, + }, + ); } catch { - setError(t("THREAD_CREATE_FAILURE_PROMPT")) + setError(t("THREAD_CREATE_FAILURE_PROMPT")); return; } onReloadThreads(); @@ -69,20 +75,27 @@ export default function ThreadReplyArea({ const toggleClosed = async () => { if (!thread) return; - + let comments, newThread; if (thread.isClosed) { try { - ({comments, thread: newThread} = await Fetch.delete(`/api/comments/${thread.id}/close`)); + ({ comments, thread: newThread } = + await Fetch.delete( + `/api/comments/${thread.id}/close`, + )); } catch { - setError(t("THREAD_REOPEN_FAILURE_PROMPT")) + setError(t("THREAD_REOPEN_FAILURE_PROMPT")); return; } } else { try { - ({comments, thread: newThread} = await Fetch.post(`/api/comments/${thread.id}/close`, null)); + ({ comments, thread: newThread } = + await Fetch.post( + `/api/comments/${thread.id}/close`, + null, + )); } catch { - setError(t("THREAD_CLOSE_FAILURE_PROMPT")) + setError(t("THREAD_CLOSE_FAILURE_PROMPT")); return; } } @@ -90,31 +103,61 @@ export default function ThreadReplyArea({ onThreadDataChanged!(comments); onReloadThreads(); onCurrentThreadChanged(newThread); - } + }; - return
- {UserManager.currentUser ? <> - {!threadId && setTitle(e.target.value)}/>} -