From c7a1a3afb45e8c4a3766560ff979a83a31086e85 Mon Sep 17 00:00:00 2001
From: louis
Date: Sun, 23 Oct 2022 19:56:36 +0200
Subject: [PATCH 01/31] =?UTF-8?q?=F0=9F=92=84=20Use=20MUI=20in=20LoginView?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.eslintrc.json | 2 +-
package.json | 8 +
src/App.tsx | 2 -
src/declaration.d.ts | 1 +
src/index.tsx | 4 +
src/views/LoginView.test.tsx | 83 ++++++++
src/views/LoginView.tsx | 131 ++++++------
yarn.lock | 387 ++++++++++++++++++++++++++++++++++-
8 files changed, 543 insertions(+), 75 deletions(-)
create mode 100644 src/views/LoginView.test.tsx
diff --git a/.eslintrc.json b/.eslintrc.json
index ee0a01c4..b7e65979 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -27,7 +27,7 @@
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "error",
"no-console": "error",
- "react/jsx-no-bind": "warn",
+ "react/jsx-no-bind": "off",
"react/jsx-sort-props": [
"warn",
{
diff --git a/package.json b/package.json
index 6e7979b6..790b47d3 100644
--- a/package.json
+++ b/package.json
@@ -7,10 +7,15 @@
"postinstall": "semantic-ui-css-patch"
},
"dependencies": {
+ "@emotion/react": "^11.10.4",
+ "@emotion/styled": "^11.10.4",
+ "@fontsource/roboto": "^4.5.8",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-brands-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/react-fontawesome": "^0.2.0",
+ "@mui/icons-material": "^5.10.9",
+ "@mui/material": "^5.10.10",
"@semantic-ui-react/css-patch": "^1.1.2",
"@tanstack/react-query": "^4.10.3",
"@tanstack/react-query-devtools": "^4.11.0",
@@ -39,6 +44,8 @@
"@babel/preset-typescript": "^7.18.6",
"@testing-library/jest-dom": "^5.16.5",
"@types/jest": "^29.2.1",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^14.4.3",
"@types/jwt-decode": "^3.1.0",
"@types/leaflet": "^1.9.0",
"@types/leaflet-draw": "^1.0.5",
@@ -75,6 +82,7 @@
"jest-environment-jsdom": "^29.3.1",
"jest-fetch-mock": "^3.0.3",
"jest-transform-stub": "^2.0.0",
+ "jwt-encode": "^1.0.1",
"postcss": "^8.4.16",
"postcss-loader": "^7.0.1",
"postcss-remove-google-fonts": "^1.2.0",
diff --git a/src/App.tsx b/src/App.tsx
index 4cc84b73..ce28f495 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -10,8 +10,6 @@ import TickerView from './views/TickerView'
import UsersView from './views/UsersView'
import ProtectedRoute from './components/ProtectedRoute'
import NotFoundView from './views/NotFoundView'
-import 'semantic-ui-css/semantic.min.css'
-import './index.css'
import '../leaflet.config.js'
import { FeatureProvider } from './components/useFeature'
diff --git a/src/declaration.d.ts b/src/declaration.d.ts
index 6e9fb0ca..e3a26839 100644
--- a/src/declaration.d.ts
+++ b/src/declaration.d.ts
@@ -1,2 +1,3 @@
declare module '*.png'
declare module 'jwt-decode'
+declare module 'jwt-encode'
diff --git a/src/index.tsx b/src/index.tsx
index 93a00949..aad746d0 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,5 +1,9 @@
import React, { createRoot } from 'react-dom/client'
import App from './App'
+import '@fontsource/roboto/300.css'
+import '@fontsource/roboto/400.css'
+import '@fontsource/roboto/500.css'
+import '@fontsource/roboto/700.css'
const container = document.getElementById('root')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
diff --git a/src/views/LoginView.test.tsx b/src/views/LoginView.test.tsx
new file mode 100644
index 00000000..ac10265f
--- /dev/null
+++ b/src/views/LoginView.test.tsx
@@ -0,0 +1,83 @@
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { MemoryRouter } from 'react-router'
+import { AuthProvider } from '../components/useAuth'
+import LoginView from './LoginView'
+import * as api from '../api/Auth'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import sign from 'jwt-encode'
+
+function setup() {
+ const client = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ })
+ return render(
+
+
+
+
+
+
+
+ )
+}
+
+describe('LoginView', function () {
+ test('login successful', async function () {
+ const jwt = sign(
+ { id: 1, email: 'louis@systemli.org', roles: ['admin', 'user'] },
+ 'secret'
+ )
+ jest
+ .spyOn(api, 'login')
+ .mockResolvedValue({ code: 200, token: jwt, expire: new Date() })
+ setup()
+
+ const email = screen
+ .getByTestId('email')
+ .querySelector('input') as HTMLInputElement
+ const password = screen
+ .getByTestId('password')
+ .querySelector('input') as HTMLInputElement
+ const submit = screen.getByTestId('submit') as HTMLElement
+
+ expect(email).toBeInTheDocument()
+ expect(password).toBeInTheDocument()
+ expect(submit).toBeInTheDocument()
+
+ await userEvent.type(email, 'louis@systemli.org')
+ await userEvent.type(password, 'password')
+ await userEvent.click(submit)
+
+ expect(api.login).toHaveBeenCalledWith('louis@systemli.org', 'password')
+ })
+
+ test('login failed', async function () {
+ jest.spyOn(api, 'login').mockRejectedValue(new Error('Login failed'))
+ setup()
+
+ const email = screen
+ .getByTestId('email')
+ .querySelector('input') as HTMLInputElement
+ const password = screen
+ .getByTestId('password')
+ .querySelector('input') as HTMLInputElement
+ const submit = screen.getByTestId('submit') as HTMLElement
+
+ expect(email).toBeInTheDocument()
+ expect(password).toBeInTheDocument()
+ expect(submit).toBeInTheDocument()
+
+ await userEvent.type(email, 'louis@systemli.org')
+ await userEvent.type(password, 'password')
+ await userEvent.click(submit)
+
+ expect(api.login).toHaveBeenCalledWith('louis@systemli.org', 'password')
+ expect(await screen.findByText('Login failed')).toBeInTheDocument()
+ })
+})
diff --git a/src/views/LoginView.tsx b/src/views/LoginView.tsx
index 22103616..23e3e58f 100644
--- a/src/views/LoginView.tsx
+++ b/src/views/LoginView.tsx
@@ -1,17 +1,18 @@
-import React, { FC, FormEvent, useCallback, useEffect } from 'react'
-import { SubmitHandler, useForm } from 'react-hook-form'
-import { useNavigate } from 'react-router'
import {
+ Alert,
+ Box,
Button,
Container,
- Form,
Grid,
- Header,
- Icon,
- InputOnChangeData,
- Message,
-} from 'semantic-ui-react'
+ Paper,
+ TextField,
+ Typography,
+} from '@mui/material'
+import React, { FC, useEffect } from 'react'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { useNavigate } from 'react-router'
import useAuth from '../components/useAuth'
+import logo from '../assets/logo.png'
interface FormValues {
email: string
@@ -19,70 +20,76 @@ interface FormValues {
}
const LoginView: FC = () => {
- const { register, handleSubmit, setValue } = useForm()
+ const { getValues, handleSubmit, register, reset } = useForm()
const { login, error, user } = useAuth()
const navigate = useNavigate()
- const onChange = useCallback(
- (e: FormEvent, { name, value }: InputOnChangeData) => {
- setValue(name, value)
- },
- [setValue]
- )
-
- const onSubmit: SubmitHandler = data => {
- login(data.email, data.password)
+ const onSubmit: SubmitHandler = ({ email, password }) => {
+ login(email, password)
}
useEffect(() => {
if (user) navigate('/')
-
- register('email')
- register('password')
})
+ useEffect(() => {
+ if (error) {
+ reset({ email: getValues('email'), password: '' })
+ }
+ }, [error, getValues, reset])
+
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ Ticker Login
+
+
+
+
+
+
+
-
+
)
}
diff --git a/yarn.lock b/yarn.lock
index 35303c31..6a59c20b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15,7 +15,7 @@
"@jridgewell/gen-mapping" "^0.1.0"
"@jridgewell/trace-mapping" "^0.3.9"
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.8.3":
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.8.3":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
@@ -194,7 +194,7 @@
dependencies:
"@babel/types" "^7.18.9"
-"@babel/helper-module-imports@^7.18.6":
+"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
@@ -586,7 +586,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.0"
-"@babel/plugin-syntax-jsx@^7.18.6", "@babel/plugin-syntax-jsx@^7.7.2":
+"@babel/plugin-syntax-jsx@^7.17.12", "@babel/plugin-syntax-jsx@^7.18.6", "@babel/plugin-syntax-jsx@^7.7.2":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0"
integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==
@@ -1096,6 +1096,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.18.3", "@babel/runtime@^7.19.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
+ version "7.19.4"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78"
+ integrity sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/runtime@^7.9.2":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
@@ -1154,6 +1161,114 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
+"@emotion/babel-plugin@^11.10.0":
+ version "11.10.2"
+ resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz#879db80ba622b3f6076917a1e6f648b1c7d008c7"
+ integrity sha512-xNQ57njWTFVfPAc3cjfuaPdsgLp5QOSuRsj9MA6ndEhH/AzuZM86qIQzt6rq+aGBwj3n5/TkLmU5lhAfdRmogA==
+ dependencies:
+ "@babel/helper-module-imports" "^7.16.7"
+ "@babel/plugin-syntax-jsx" "^7.17.12"
+ "@babel/runtime" "^7.18.3"
+ "@emotion/hash" "^0.9.0"
+ "@emotion/memoize" "^0.8.0"
+ "@emotion/serialize" "^1.1.0"
+ babel-plugin-macros "^3.1.0"
+ convert-source-map "^1.5.0"
+ escape-string-regexp "^4.0.0"
+ find-root "^1.1.0"
+ source-map "^0.5.7"
+ stylis "4.0.13"
+
+"@emotion/cache@^11.10.0", "@emotion/cache@^11.10.3":
+ version "11.10.3"
+ resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.3.tgz#c4f67904fad10c945fea5165c3a5a0583c164b87"
+ integrity sha512-Psmp/7ovAa8appWh3g51goxu/z3iVms7JXOreq136D8Bbn6dYraPnmL6mdM8GThEx9vwSn92Fz+mGSjBzN8UPQ==
+ dependencies:
+ "@emotion/memoize" "^0.8.0"
+ "@emotion/sheet" "^1.2.0"
+ "@emotion/utils" "^1.2.0"
+ "@emotion/weak-memoize" "^0.3.0"
+ stylis "4.0.13"
+
+"@emotion/hash@^0.9.0":
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.0.tgz#c5153d50401ee3c027a57a177bc269b16d889cb7"
+ integrity sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==
+
+"@emotion/is-prop-valid@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz#7f2d35c97891669f7e276eb71c83376a5dc44c83"
+ integrity sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==
+ dependencies:
+ "@emotion/memoize" "^0.8.0"
+
+"@emotion/memoize@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f"
+ integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==
+
+"@emotion/react@^11.10.4":
+ version "11.10.4"
+ resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.4.tgz#9dc6bccbda5d70ff68fdb204746c0e8b13a79199"
+ integrity sha512-j0AkMpr6BL8gldJZ6XQsQ8DnS9TxEQu1R+OGmDZiWjBAJtCcbt0tS3I/YffoqHXxH6MjgI7KdMbYKw3MEiU9eA==
+ dependencies:
+ "@babel/runtime" "^7.18.3"
+ "@emotion/babel-plugin" "^11.10.0"
+ "@emotion/cache" "^11.10.0"
+ "@emotion/serialize" "^1.1.0"
+ "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0"
+ "@emotion/utils" "^1.2.0"
+ "@emotion/weak-memoize" "^0.3.0"
+ hoist-non-react-statics "^3.3.1"
+
+"@emotion/serialize@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.0.tgz#b1f97b1011b09346a40e9796c37a3397b4ea8ea8"
+ integrity sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==
+ dependencies:
+ "@emotion/hash" "^0.9.0"
+ "@emotion/memoize" "^0.8.0"
+ "@emotion/unitless" "^0.8.0"
+ "@emotion/utils" "^1.2.0"
+ csstype "^3.0.2"
+
+"@emotion/sheet@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.0.tgz#771b1987855839e214fc1741bde43089397f7be5"
+ integrity sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==
+
+"@emotion/styled@^11.10.4":
+ version "11.10.4"
+ resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.4.tgz#e93f84a4d54003c2acbde178c3f97b421fce1cd4"
+ integrity sha512-pRl4R8Ez3UXvOPfc2bzIoV8u9P97UedgHS4FPX594ntwEuAMA114wlaHvOK24HB48uqfXiGlYIZYCxVJ1R1ttQ==
+ dependencies:
+ "@babel/runtime" "^7.18.3"
+ "@emotion/babel-plugin" "^11.10.0"
+ "@emotion/is-prop-valid" "^1.2.0"
+ "@emotion/serialize" "^1.1.0"
+ "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0"
+ "@emotion/utils" "^1.2.0"
+
+"@emotion/unitless@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db"
+ integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==
+
+"@emotion/use-insertion-effect-with-fallbacks@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df"
+ integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==
+
+"@emotion/utils@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561"
+ integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==
+
+"@emotion/weak-memoize@^0.3.0":
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb"
+ integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==
+
"@eslint/eslintrc@^1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e"
@@ -1184,6 +1299,11 @@
"@babel/runtime" "^7.10.4"
react-is "^16.6.3"
+"@fontsource/roboto@^4.5.8":
+ version "4.5.8"
+ resolved "https://registry.yarnpkg.com/@fontsource/roboto/-/roboto-4.5.8.tgz#56347764786079838faf43f0eeda22dd7328437f"
+ integrity sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA==
+
"@fortawesome/fontawesome-common-types@6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz#76467a94aa888aeb22aafa43eb6ff889df3a5a7f"
@@ -1526,6 +1646,99 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
+"@mui/base@5.0.0-alpha.102":
+ version "5.0.0-alpha.102"
+ resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.102.tgz#53b07d0b73d3afe1d2a3feb3b43c8188bb821796"
+ integrity sha512-5e/qAIP+DlkrZxIt/cwnDw/A3ii22WkoEoWKHyu4+oeGs3/1Flh7qLaN4h5EAIBB9TvTEZEUzvmsTInmIj6ghg==
+ dependencies:
+ "@babel/runtime" "^7.19.0"
+ "@emotion/is-prop-valid" "^1.2.0"
+ "@mui/types" "^7.2.0"
+ "@mui/utils" "^5.10.9"
+ "@popperjs/core" "^2.11.6"
+ clsx "^1.2.1"
+ prop-types "^15.8.1"
+ react-is "^18.2.0"
+
+"@mui/core-downloads-tracker@^5.10.10":
+ version "5.10.10"
+ resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.10.tgz#a3e5d2f6e5146e9a85d48824c386a31be1746ba3"
+ integrity sha512-aDuE2PNEh+hAndxEWlZgq7uiFPZKJtnkPDX7v6kSCrMXA32ZaQ6rZi5olmC7DUHt/BaOSxb7N/im/ss0XBkDhA==
+
+"@mui/icons-material@^5.10.9":
+ version "5.10.9"
+ resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.10.9.tgz#f9522c49797caf30146acc576e37ecb4f95bbc38"
+ integrity sha512-sqClXdEM39WKQJOQ0ZCPTptaZgqwibhj2EFV9N0v7BU1PO8y4OcX/a2wIQHn4fNuDjIZktJIBrmU23h7aqlGgg==
+ dependencies:
+ "@babel/runtime" "^7.19.0"
+
+"@mui/material@^5.10.10":
+ version "5.10.10"
+ resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.10.10.tgz#df780d933c0aa9d4a5272f32c9cc24bf1ea72cff"
+ integrity sha512-ioLvqY7VvcePz9dnEIRhpiVvtJmAFmvG6rtLXXzVdMmAVbSaelr5Io07mPz/mCyqE+Uv8/4EuJV276DWO7etzA==
+ dependencies:
+ "@babel/runtime" "^7.19.0"
+ "@mui/base" "5.0.0-alpha.102"
+ "@mui/core-downloads-tracker" "^5.10.10"
+ "@mui/system" "^5.10.10"
+ "@mui/types" "^7.2.0"
+ "@mui/utils" "^5.10.9"
+ "@types/react-transition-group" "^4.4.5"
+ clsx "^1.2.1"
+ csstype "^3.1.1"
+ prop-types "^15.8.1"
+ react-is "^18.2.0"
+ react-transition-group "^4.4.5"
+
+"@mui/private-theming@^5.10.9":
+ version "5.10.9"
+ resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.10.9.tgz#c427bfa736455703975cdb108dbde6a174ba7971"
+ integrity sha512-BN7/CnsVPVyBaQpDTij4uV2xGYHHHhOgpdxeYLlIu+TqnsVM7wUeF+37kXvHovxM6xmL5qoaVUD98gDC0IZnHg==
+ dependencies:
+ "@babel/runtime" "^7.19.0"
+ "@mui/utils" "^5.10.9"
+ prop-types "^15.8.1"
+
+"@mui/styled-engine@^5.10.8":
+ version "5.10.8"
+ resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.10.8.tgz#2db411e4278f06f70ccb6b5cd56ace67109513f6"
+ integrity sha512-w+y8WI18EJV6zM/q41ug19cE70JTeO6sWFsQ7tgePQFpy6ToCVPh0YLrtqxUZXSoMStW5FMw0t9fHTFAqPbngw==
+ dependencies:
+ "@babel/runtime" "^7.19.0"
+ "@emotion/cache" "^11.10.3"
+ csstype "^3.1.1"
+ prop-types "^15.8.1"
+
+"@mui/system@^5.10.10":
+ version "5.10.10"
+ resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.10.10.tgz#fbc34f29a3b62268c3d2b2be92819a35fc52de90"
+ integrity sha512-TXwtKN0adKpBrZmO+eilQWoPf2veh050HLYrN78Kps9OhlvO70v/2Kya0+mORFhu9yhpAwjHXO8JII/R4a5ZLA==
+ dependencies:
+ "@babel/runtime" "^7.19.0"
+ "@mui/private-theming" "^5.10.9"
+ "@mui/styled-engine" "^5.10.8"
+ "@mui/types" "^7.2.0"
+ "@mui/utils" "^5.10.9"
+ clsx "^1.2.1"
+ csstype "^3.1.1"
+ prop-types "^15.8.1"
+
+"@mui/types@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.0.tgz#91380c2d42420f51f404120f7a9270eadd6f5c23"
+ integrity sha512-lGXtFKe5lp3UxTBGqKI1l7G8sE2xBik8qCfrLHD5olwP/YU0/ReWoWT7Lp1//ri32dK39oPMrJN8TgbkCSbsNA==
+
+"@mui/utils@^5.10.9":
+ version "5.10.9"
+ resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.10.9.tgz#9dc455f9230f43eeb81d96a9a4bdb3855bb9ea39"
+ integrity sha512-2tdHWrq3+WCy+G6TIIaFx3cg7PorXZ71P375ExuX61od1NOAJP1mK90VxQ8N4aqnj2vmO3AQDkV4oV2Ktvt4bA==
+ dependencies:
+ "@babel/runtime" "^7.19.0"
+ "@types/prop-types" "^15.7.5"
+ "@types/react-is" "^16.7.1 || ^17.0.0"
+ prop-types "^15.8.1"
+ react-is "^18.2.0"
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -1547,6 +1760,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@popperjs/core@^2.11.6":
+ version "2.11.6"
+ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
+ integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
+
"@popperjs/core@^2.6.0":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
@@ -1631,6 +1849,20 @@
"@tanstack/query-core" "4.10.3"
use-sync-external-store "^1.2.0"
+"@testing-library/dom@^8.5.0":
+ version "8.19.0"
+ resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.19.0.tgz#bd3f83c217ebac16694329e413d9ad5fdcfd785f"
+ integrity sha512-6YWYPPpxG3e/xOo6HIWwB/58HukkwIVTOaZ0VwdMVjhRUX/01E4FtQbck9GazOOj7MXHc5RBzMrU86iBJHbI+A==
+ dependencies:
+ "@babel/code-frame" "^7.10.4"
+ "@babel/runtime" "^7.12.5"
+ "@types/aria-query" "^4.2.0"
+ aria-query "^5.0.0"
+ chalk "^4.1.0"
+ dom-accessibility-api "^0.5.9"
+ lz-string "^1.4.4"
+ pretty-format "^27.0.2"
+
"@testing-library/jest-dom@^5.16.5":
version "5.16.5"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz#3912846af19a29b2dbf32a6ae9c31ef52580074e"
@@ -1646,6 +1878,20 @@
lodash "^4.17.15"
redent "^3.0.0"
+"@testing-library/react@^13.4.0":
+ version "13.4.0"
+ resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.4.0.tgz#6a31e3bf5951615593ad984e96b9e5e2d9380966"
+ integrity sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+ "@testing-library/dom" "^8.5.0"
+ "@types/react-dom" "^18.0.0"
+
+"@testing-library/user-event@^14.4.3":
+ version "14.4.3"
+ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591"
+ integrity sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==
+
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@@ -1676,6 +1922,11 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
+"@types/aria-query@^4.2.0":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc"
+ integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==
+
"@types/babel__core@^7.1.14":
version "7.1.19"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460"
@@ -1939,7 +2190,7 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e"
integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==
-"@types/prop-types@*", "@types/prop-types@^15.0.0":
+"@types/prop-types@*", "@types/prop-types@^15.0.0", "@types/prop-types@^15.7.5":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
@@ -1954,13 +2205,20 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
-"@types/react-dom@^18.0.6":
+"@types/react-dom@^18.0.0", "@types/react-dom@^18.0.6":
version "18.0.6"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.6.tgz#36652900024842b74607a17786b6662dd1e103a1"
integrity sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==
dependencies:
"@types/react" "*"
+"@types/react-is@^16.7.1 || ^17.0.0":
+ version "17.0.3"
+ resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
+ integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==
+ dependencies:
+ "@types/react" "*"
+
"@types/react-router-dom@^5.3.3":
version "5.3.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
@@ -1978,6 +2236,13 @@
"@types/history" "^4.7.11"
"@types/react" "*"
+"@types/react-transition-group@^4.4.5":
+ version "4.4.5"
+ resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
+ integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==
+ dependencies:
+ "@types/react" "*"
+
"@types/react-twitter-auth@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@types/react-twitter-auth/-/react-twitter-auth-0.0.4.tgz#bb1836bbbbbdfb8623c78cf3e6ce44b9930e7e45"
@@ -3030,6 +3295,11 @@ clsx@^1.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
+clsx@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
+ integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -3153,13 +3423,25 @@ content-type@~1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
-convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
+convert-source-map@^1.4.0, convert-source-map@^1.6.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
dependencies:
safe-buffer "~5.1.1"
+convert-source-map@^1.5.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
+ integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
+
+convert-source-map@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
+ integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
+ dependencies:
+ safe-buffer "~5.1.1"
+
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
@@ -3368,6 +3650,11 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
+csstype@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
+ integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
+
damerau-levenshtein@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -3548,7 +3835,7 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
-dom-accessibility-api@^0.5.6:
+dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9:
version "0.5.14"
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56"
integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==
@@ -3560,6 +3847,14 @@ dom-converter@^0.2.0:
dependencies:
utila "~0.4"
+dom-helpers@^5.0.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
+ integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
+ dependencies:
+ "@babel/runtime" "^7.8.7"
+ csstype "^3.0.2"
+
dom-serializer@^1.0.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
@@ -4272,6 +4567,11 @@ find-cache-dir@^3.3.2:
make-dir "^3.0.2"
pkg-dir "^4.1.0"
+find-root@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
+ integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
+
find-up@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
@@ -4601,6 +4901,13 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+hoist-non-react-statics@^3.3.1:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
+ integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+ dependencies:
+ react-is "^16.7.0"
+
hpack.js@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
@@ -5652,6 +5959,13 @@ jwt-decode@*, jwt-decode@^3.1.2:
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==
+jwt-encode@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/jwt-encode/-/jwt-encode-1.0.1.tgz#6602a0475f757c7d5d559a7e923f3d3d5ecd938c"
+ integrity sha512-QrGiNhynbAYyFdbC1GbjborzenSFs5Ga+2nE0uBokGXsH11xrgd1AX55HR7P+wGQyyZOn6LUO5iKlh74dlhBkA==
+ dependencies:
+ ts.cryptojs256 "^1.0.1"
+
keyboard-key@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/keyboard-key/-/keyboard-key-1.1.0.tgz#6f2e8e37fa11475bb1f1d65d5174f1b35653f5b7"
@@ -5837,6 +6151,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
+lz-string@^1.4.4:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
+ integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==
+
make-dir@^3.0.0, make-dir@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@@ -6930,7 +7249,25 @@ pretty-error@^4.0.0:
lodash "^4.17.20"
renderkid "^3.0.0"
-pretty-format@^29.0.0, pretty-format@^29.2.1:
+pretty-format@^27.0.2:
+ version "27.5.1"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
+ integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
+ dependencies:
+ ansi-regex "^5.0.1"
+ ansi-styles "^5.0.0"
+ react-is "^17.0.1"
+
+pretty-format@^29.0.0:
+ version "29.1.2"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.1.2.tgz#b1f6b75be7d699be1a051f5da36e8ae9e76a8e6a"
+ integrity sha512-CGJ6VVGXVRP2o2Dorl4mAwwvDWT25luIsYhkyVQW32E4nL+TgW939J7LlKT/npq5Cpq6j3s+sy+13yk7xYpBmg==
+ dependencies:
+ "@jest/schemas" "^29.0.0"
+ ansi-styles "^5.0.0"
+ react-is "^18.0.0"
+
+pretty-format@^29.2.1:
version "29.2.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.2.1.tgz#86e7748fe8bbc96a6a4e04fa99172630907a9611"
integrity sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==
@@ -7110,16 +7447,21 @@ react-image-lightbox@^5.1.4:
prop-types "^15.7.2"
react-modal "^3.11.1"
-react-is@^16.13.1, react-is@^16.6.3:
+react-is@^16.13.1, react-is@^16.6.3, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
-"react-is@^16.8.6 || ^17.0.0 || ^18.0.0":
+"react-is@^16.8.6 || ^17.0.0 || ^18.0.0", react-is@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
+react-is@^17.0.1:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+ integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
react-is@^18.0.0:
version "18.1.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67"
@@ -7204,6 +7546,16 @@ react-router@6.4.1, react-router@^6.4.1:
dependencies:
"@remix-run/router" "1.0.1"
+react-transition-group@^4.4.5:
+ version "4.4.5"
+ resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
+ integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ dom-helpers "^5.0.1"
+ loose-envify "^1.4.0"
+ prop-types "^15.6.2"
+
react-twitter-auth@0.0.13:
version "0.0.13"
resolved "https://registry.yarnpkg.com/react-twitter-auth/-/react-twitter-auth-0.0.13.tgz#73af3b43066d93682adfcbf7ecce007c7a0da1dc"
@@ -7764,6 +8116,11 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+source-map@^0.5.7:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+ integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
+
space-separated-tokens@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz#43193cec4fb858a2ce934b7f98b7f2c18107098b"
@@ -7941,6 +8298,11 @@ stylehacks@^5.1.0:
browserslist "^4.16.6"
postcss-selector-parser "^6.0.4"
+stylis@4.0.13:
+ version "4.0.13"
+ resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
+ integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
+
superjson@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.10.0.tgz#279362e6c1789b0b6bdfa280e82ee43d0e0fa514"
@@ -8134,6 +8496,11 @@ ts-node@^10.9.1:
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
+ts.cryptojs256@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ts.cryptojs256/-/ts.cryptojs256-1.0.1.tgz#56a3d3a3bc022ad358b3e62cd8dae3224d9384f1"
+ integrity sha512-9XtEgRVOZBCdpPcCEhfvv9I2AVXdvfI81I/KpFM0wEfbq5JVHlXH7bfIjGQmYrIHGiBEHKsIaStQ87k926J7dA==
+
tsconfig-paths@^3.14.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"
From 564abeb0d76b7117032dd157f9f7b28c0b2a73da Mon Sep 17 00:00:00 2001
From: louis
Date: Tue, 25 Oct 2022 00:15:05 +0200
Subject: [PATCH 02/31] =?UTF-8?q?=F0=9F=92=84=20Add=20ThemeProvider=20with?=
=?UTF-8?q?=20several=20overrides?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/App.tsx | 71 ++++++------
src/index.css | 14 ---
src/theme/GlobalStyles.tsx | 54 +++++++++
src/theme/ThemeProvider.tsx | 218 ++++++++++++++++++++++++++++++++++++
src/theme/customShadows.ts | 53 +++++++++
src/theme/declaration.d.ts | 15 +++
src/theme/palette.ts | 110 ++++++++++++++++++
src/theme/shadows.ts | 38 +++++++
src/theme/typography.ts | 107 ++++++++++++++++++
src/views/LoginView.tsx | 6 +-
10 files changed, 639 insertions(+), 47 deletions(-)
delete mode 100644 src/index.css
create mode 100644 src/theme/GlobalStyles.tsx
create mode 100644 src/theme/ThemeProvider.tsx
create mode 100644 src/theme/customShadows.ts
create mode 100644 src/theme/declaration.d.ts
create mode 100644 src/theme/palette.ts
create mode 100644 src/theme/shadows.ts
create mode 100644 src/theme/typography.ts
diff --git a/src/App.tsx b/src/App.tsx
index ce28f495..b9dd7079 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -10,8 +10,9 @@ import TickerView from './views/TickerView'
import UsersView from './views/UsersView'
import ProtectedRoute from './components/ProtectedRoute'
import NotFoundView from './views/NotFoundView'
-import '../leaflet.config.js'
import { FeatureProvider } from './components/useFeature'
+import ThemeProvider from './theme/ThemeProvider'
+import '../leaflet.config.js'
const App: FC = () => {
const queryClient = new QueryClient({
@@ -19,37 +20,43 @@ const App: FC = () => {
})
return (
-
-
-
-
-
- } role="user" />}
- path="/"
- />
- } role="user" />}
- path="/ticker/:tickerId"
- />
- } role="admin" />}
- path="/users"
- />
- } role="admin" />
- }
- path="/settings"
- />
- } path="/login" />
- } path="*" />
-
-
-
-
-
-
+
+
+
+
+
+
+ } role="user" />}
+ path="/"
+ />
+ } role="user" />
+ }
+ path="/ticker/:tickerId"
+ />
+ } role="admin" />
+ }
+ path="/users"
+ />
+ } role="admin" />
+ }
+ path="/settings"
+ />
+ } path="/login" />
+ } path="*" />
+
+
+
+
+
+
+
)
}
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index 957354ec..00000000
--- a/src/index.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.ui.modal > .close {
- top: 0.5rem;
- right: 0.5rem;
- color: #000;
-}
-
-.app {
- margin-top: 50px;
-}
-
-.leaflet-container {
- height: 400px;
- z-index: 1;
-}
diff --git a/src/theme/GlobalStyles.tsx b/src/theme/GlobalStyles.tsx
new file mode 100644
index 00000000..b327d61a
--- /dev/null
+++ b/src/theme/GlobalStyles.tsx
@@ -0,0 +1,54 @@
+import React, { FC } from 'react'
+import { GlobalStyles as MUIGlobalStyles } from '@mui/material'
+
+const GlobalStyles: FC = () => {
+ return (
+
+ )
+}
+
+export default GlobalStyles
diff --git a/src/theme/ThemeProvider.tsx b/src/theme/ThemeProvider.tsx
new file mode 100644
index 00000000..5e885753
--- /dev/null
+++ b/src/theme/ThemeProvider.tsx
@@ -0,0 +1,218 @@
+import React, { FC, ReactNode } from 'react'
+import {
+ ThemeProvider as MUIThemeProvider,
+ createTheme,
+ StyledEngineProvider,
+ CssBaseline,
+ alpha,
+} from '@mui/material'
+import GlobalStyles from './GlobalStyles'
+import palette from './palette'
+import shadows from './shadows'
+import typography from './typography'
+import customShadows from './customShadows'
+
+const theme = createTheme({
+ palette: palette,
+ shadows: shadows,
+ typography: typography,
+ customShadows: customShadows,
+})
+theme.components = {
+ MuiAutocomplete: {
+ styleOverrides: {
+ paper: {
+ boxShadow: customShadows.z20,
+ },
+ },
+ },
+ MuiBackdrop: {
+ styleOverrides: {
+ root: {
+ backgroundColor: alpha(palette.grey[800], 0.8),
+ },
+ invisible: {
+ background: 'transparent',
+ },
+ },
+ },
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ '&:hover': {
+ boxShadow: 'none',
+ },
+ },
+ sizeLarge: {
+ height: 48,
+ },
+ containedInherit: {
+ color: palette.grey[800],
+ boxShadow: customShadows.z8,
+ '&:hover': {
+ backgroundColor: palette.grey[400],
+ },
+ },
+ containedPrimary: {
+ boxShadow: customShadows.primary,
+ },
+ containedSecondary: {
+ boxShadow: customShadows.secondary,
+ },
+ outlinedInherit: {
+ border: `1px solid ${alpha(palette.grey[500], 0.32)}`,
+ '&:hover': {
+ backgroundColor: palette.action.hover,
+ },
+ },
+ textInherit: {
+ '&:hover': {
+ backgroundColor: palette.action.hover,
+ },
+ },
+ },
+ },
+ MuiCard: {
+ styleOverrides: {
+ root: {
+ boxShadow: theme.customShadows.card,
+ borderRadius: Number(theme.shape.borderRadius) * 2,
+ position: 'relative',
+ zIndex: 0, // Fix Safari overflow: hidden with border radius
+ },
+ },
+ },
+ MuiCardHeader: {
+ defaultProps: {
+ titleTypographyProps: { variant: 'h6' },
+ subheaderTypographyProps: { variant: 'body2' },
+ },
+ styleOverrides: {
+ root: {
+ padding: theme.spacing(3, 3, 0),
+ },
+ },
+ },
+ MuiCardContent: {
+ styleOverrides: {
+ root: {
+ padding: theme.spacing(3),
+ },
+ },
+ },
+ MuiInputBase: {
+ styleOverrides: {
+ root: {
+ '&.Mui-disabled': {
+ '& svg': { color: palette.text.disabled },
+ },
+ },
+ input: {
+ '&::placeholder': {
+ opacity: 1,
+ color: palette.text.disabled,
+ },
+ },
+ },
+ },
+ MuiInput: {
+ styleOverrides: {
+ underline: {
+ '&:before': {
+ borderBottomColor: alpha(palette.grey[500], 0.56),
+ },
+ },
+ },
+ },
+ MuiFilledInput: {
+ styleOverrides: {
+ root: {
+ backgroundColor: alpha(palette.grey[500], 0.12),
+ '&:hover': {
+ backgroundColor: alpha(palette.grey[500], 0.16),
+ },
+ '&.Mui-focused': {
+ backgroundColor: palette.action.focus,
+ },
+ '&.Mui-disabled': {
+ backgroundColor: palette.action.disabledBackground,
+ },
+ },
+ underline: {
+ '&:before': {
+ borderBottomColor: alpha(palette.grey[500], 0.56),
+ },
+ },
+ },
+ },
+ MuiOutlinedInput: {
+ styleOverrides: {
+ root: {
+ '& .MuiOutlinedInput-notchedOutline': {
+ borderColor: alpha(palette.grey[500], 0.32),
+ },
+ '&.Mui-disabled': {
+ '& .MuiOutlinedInput-notchedOutline': {
+ borderColor: palette.action.disabledBackground,
+ },
+ },
+ },
+ },
+ },
+ MuiPaper: {
+ defaultProps: {
+ elevation: 0,
+ },
+ styleOverrides: {
+ root: {
+ backgroundImage: 'none',
+ },
+ },
+ },
+ MuiTableCell: {
+ styleOverrides: {
+ head: {
+ color: palette.text.secondary,
+ backgroundColor: palette.background.neutral,
+ },
+ },
+ },
+ MuiTooltip: {
+ styleOverrides: {
+ tooltip: {
+ backgroundColor: palette.grey[800],
+ },
+ arrow: {
+ color: palette.grey[800],
+ },
+ },
+ },
+ MuiTypography: {
+ styleOverrides: {
+ paragraph: {
+ marginBottom: theme.spacing(2),
+ },
+ gutterBottom: {
+ marginBottom: theme.spacing(1),
+ },
+ },
+ },
+}
+
+interface Props {
+ children: ReactNode
+}
+
+const ThemeProvider: FC = ({ children }) => {
+ return (
+
+
+
+
+ {children}
+
+
+ )
+}
+
+export default ThemeProvider
diff --git a/src/theme/customShadows.ts b/src/theme/customShadows.ts
new file mode 100644
index 00000000..6106f3a8
--- /dev/null
+++ b/src/theme/customShadows.ts
@@ -0,0 +1,53 @@
+import { alpha } from '@mui/material/styles'
+import palette from './palette'
+
+const color = palette.grey[500]
+const transparent = alpha(color, 0.16)
+
+export interface CustomShadows {
+ z1: string
+ z4: string
+ z8: string
+ z12: string
+ z16: string
+ z20: string
+ z24: string
+ primary: string
+ info: string
+ secondary: string
+ success: string
+ warning: string
+ error: string
+ card: string
+ dialog: string
+ dropdown: string
+}
+
+const customShadows: CustomShadows = {
+ z1: `0 1px 2px 0 ${transparent}`,
+ z4: `0 4px 8px 0 ${transparent}`,
+ z8: `0 8px 16px 0 ${transparent}`,
+ z12: `0 12px 24px -4px ${transparent}`,
+ z16: `0 16px 32px -4px ${transparent}`,
+ z20: `0 20px 40px -4px ${transparent}`,
+ z24: `0 24px 48px 0 ${transparent}`,
+ //
+ primary: `0 8px 16px 0 ${alpha(palette.primary.main, 0.24)}`,
+ info: `0 8px 16px 0 ${alpha(palette.info.main, 0.24)}`,
+ secondary: `0 8px 16px 0 ${alpha(palette.secondary.main, 0.24)}`,
+ success: `0 8px 16px 0 ${alpha(palette.success.main, 0.24)}`,
+ warning: `0 8px 16px 0 ${alpha(palette.warning.main, 0.24)}`,
+ error: `0 8px 16px 0 ${alpha(palette.error.main, 0.24)}`,
+ //
+ card: `0 0 2px 0 ${alpha(color, 0.2)}, 0 12px 24px -4px ${alpha(
+ color,
+ 0.12
+ )}`,
+ dialog: `-40px 40px 80px -8px ${alpha(color, 0.24)}`,
+ dropdown: `0 0 2px 0 ${alpha(color, 0.24)}, -20px 20px 40px -4px ${alpha(
+ color,
+ 0.24
+ )}`,
+}
+
+export default customShadows
diff --git a/src/theme/declaration.d.ts b/src/theme/declaration.d.ts
new file mode 100644
index 00000000..0153bfd9
--- /dev/null
+++ b/src/theme/declaration.d.ts
@@ -0,0 +1,15 @@
+import { CustomShadows } from './customShadows'
+
+declare module '@mui/material/styles' {
+ interface Theme {
+ customShadows: CustomShadows
+ }
+
+ interface ThemeOptions {
+ customShadows: CustomShadows
+ }
+
+ interface TypeBackground {
+ neutral: string
+ }
+}
diff --git a/src/theme/palette.ts b/src/theme/palette.ts
new file mode 100644
index 00000000..a7e79256
--- /dev/null
+++ b/src/theme/palette.ts
@@ -0,0 +1,110 @@
+import { Color } from '@mui/material'
+import { alpha, PaletteColor } from '@mui/material/styles'
+
+const grey: Color = {
+ 50: '',
+ 100: '#F9FAFB',
+ 200: '#F4F6F8',
+ 300: '#DFE3E8',
+ 400: '#C4CDD5',
+ 500: '#919EAB',
+ 600: '#637381',
+ 700: '#454F5B',
+ 800: '#212B36',
+ 900: '#161C24',
+ A100: '',
+ A200: '',
+ A400: '',
+ A700: '',
+}
+
+const primary: PaletteColor = {
+ //lighter: '#D1E9FC',
+ light: '#76B0F1',
+ main: '#2065D1',
+ dark: '#103996',
+ //darker: '#061B64',
+ contrastText: '#fff',
+}
+
+const secondary: PaletteColor = {
+ //lighter: '#D6E4FF',
+ light: '#84A9FF',
+ main: '#3366FF',
+ dark: '#1939B7',
+ //darker: '#091A7A',
+ contrastText: '#fff',
+}
+
+const info: PaletteColor = {
+ //lighter: '#D0F2FF',
+ light: '#74CAFF',
+ main: '#1890FF',
+ dark: '#0C53B7',
+ //darker: '#04297A',
+ contrastText: '#fff',
+}
+
+const success: PaletteColor = {
+ //lighter: '#E9FCD4',
+ light: '#AAF27F',
+ main: '#54D62C',
+ dark: '#229A16',
+ //darker: '#08660D',
+ contrastText: grey[800],
+}
+
+const warning: PaletteColor = {
+ //lighter: '#FFF7CD',
+ light: '#FFE16A',
+ main: '#FFC107',
+ dark: '#B78103',
+ //darker: '#7A4F01',
+ contrastText: grey[800],
+}
+
+const error: PaletteColor = {
+ //lighter: '#FFE7D9',
+ light: '#FFA48D',
+ main: '#FF4842',
+ dark: '#B72136',
+ //darker: '#7A0C2E',
+ contrastText: '#fff',
+}
+
+const palette = {
+ common: { black: '#000', white: '#fff' },
+ primary: primary,
+ secondary: secondary,
+ info: info,
+ success: success,
+ warning: warning,
+ error: error,
+ grey: grey,
+ divider: alpha(grey[500], 0.24),
+ text: {
+ primary: grey[800],
+ secondary: grey[600],
+ disabled: grey[500],
+ },
+ background: {
+ paper: '#fff',
+ default: grey[100],
+ neutral: grey[200],
+ },
+ action: {
+ activatedOpacity: 0,
+ active: grey[600],
+ disabled: alpha(grey[500], 0.8),
+ disabledBackground: alpha(grey[500], 0.24),
+ disabledOpacity: 0.48,
+ focus: alpha(grey[500], 0.24),
+ focusOpacity: 0,
+ hover: alpha(grey[500], 0.08),
+ hoverOpacity: 0.08,
+ selected: alpha(grey[500], 0.16),
+ selectedOpacity: 0,
+ },
+}
+
+export default palette
diff --git a/src/theme/shadows.ts b/src/theme/shadows.ts
new file mode 100644
index 00000000..b4bf7633
--- /dev/null
+++ b/src/theme/shadows.ts
@@ -0,0 +1,38 @@
+import { alpha, Shadows } from '@mui/material/styles'
+import palette from './palette'
+
+const color = palette.grey[500]
+
+const transparent1 = alpha(color, 0.2)
+const transparent2 = alpha(color, 0.14)
+const transparent3 = alpha(color, 0.12)
+
+const shadows: Shadows = [
+ 'none',
+ `0px 2px 1px -1px ${transparent1},0px 1px 1px 0px ${transparent2},0px 1px 3px 0px ${transparent3}`,
+ `0px 3px 1px -2px ${transparent1},0px 2px 2px 0px ${transparent2},0px 1px 5px 0px ${transparent3}`,
+ `0px 3px 3px -2px ${transparent1},0px 3px 4px 0px ${transparent2},0px 1px 8px 0px ${transparent3}`,
+ `0px 2px 4px -1px ${transparent1},0px 4px 5px 0px ${transparent2},0px 1px 10px 0px ${transparent3}`,
+ `0px 3px 5px -1px ${transparent1},0px 5px 8px 0px ${transparent2},0px 1px 14px 0px ${transparent3}`,
+ `0px 3px 5px -1px ${transparent1},0px 6px 10px 0px ${transparent2},0px 1px 18px 0px ${transparent3}`,
+ `0px 4px 5px -2px ${transparent1},0px 7px 10px 1px ${transparent2},0px 2px 16px 1px ${transparent3}`,
+ `0px 5px 5px -3px ${transparent1},0px 8px 10px 1px ${transparent2},0px 3px 14px 2px ${transparent3}`,
+ `0px 5px 6px -3px ${transparent1},0px 9px 12px 1px ${transparent2},0px 3px 16px 2px ${transparent3}`,
+ `0px 6px 6px -3px ${transparent1},0px 10px 14px 1px ${transparent2},0px 4px 18px 3px ${transparent3}`,
+ `0px 6px 7px -4px ${transparent1},0px 11px 15px 1px ${transparent2},0px 4px 20px 3px ${transparent3}`,
+ `0px 7px 8px -4px ${transparent1},0px 12px 17px 2px ${transparent2},0px 5px 22px 4px ${transparent3}`,
+ `0px 7px 8px -4px ${transparent1},0px 13px 19px 2px ${transparent2},0px 5px 24px 4px ${transparent3}`,
+ `0px 7px 9px -4px ${transparent1},0px 14px 21px 2px ${transparent2},0px 5px 26px 4px ${transparent3}`,
+ `0px 8px 9px -5px ${transparent1},0px 15px 22px 2px ${transparent2},0px 6px 28px 5px ${transparent3}`,
+ `0px 8px 10px -5px ${transparent1},0px 16px 24px 2px ${transparent2},0px 6px 30px 5px ${transparent3}`,
+ `0px 8px 11px -5px ${transparent1},0px 17px 26px 2px ${transparent2},0px 6px 32px 5px ${transparent3}`,
+ `0px 9px 11px -5px ${transparent1},0px 18px 28px 2px ${transparent2},0px 7px 34px 6px ${transparent3}`,
+ `0px 9px 12px -6px ${transparent1},0px 19px 29px 2px ${transparent2},0px 7px 36px 6px ${transparent3}`,
+ `0px 10px 13px -6px ${transparent1},0px 20px 31px 3px ${transparent2},0px 8px 38px 7px ${transparent3}`,
+ `0px 10px 13px -6px ${transparent1},0px 21px 33px 3px ${transparent2},0px 8px 40px 7px ${transparent3}`,
+ `0px 10px 14px -6px ${transparent1},0px 22px 35px 3px ${transparent2},0px 8px 42px 7px ${transparent3}`,
+ `0px 11px 14px -7px ${transparent1},0px 23px 36px 3px ${transparent2},0px 9px 44px 8px ${transparent3}`,
+ `0px 11px 15px -7px ${transparent1},0px 24px 38px 3px ${transparent2},0px 9px 46px 8px ${transparent3}`,
+]
+
+export default shadows
diff --git a/src/theme/typography.ts b/src/theme/typography.ts
new file mode 100644
index 00000000..175ca43a
--- /dev/null
+++ b/src/theme/typography.ts
@@ -0,0 +1,107 @@
+import { Typography } from '@mui/material/styles/createTypography'
+
+export function remToPx(value: number) {
+ return Math.round(value * 16)
+}
+
+export function pxToRem(value: number) {
+ return `${value / 16}rem`
+}
+
+export function responsiveFontSizes({ sm, md, lg }) {
+ return {
+ '@media (min-width:600px)': {
+ fontSize: pxToRem(sm),
+ },
+ '@media (min-width:900px)': {
+ fontSize: pxToRem(md),
+ },
+ '@media (min-width:1200px)': {
+ fontSize: pxToRem(lg),
+ },
+ }
+}
+
+// ----------------------------------------------------------------------
+
+const FONT_PRIMARY = 'Roboto, sans-serif' // Google Font
+// const FONT_SECONDARY = 'CircularStd, sans-serif'; // Local Font
+
+const typography: Typography = {
+ fontFamily: FONT_PRIMARY,
+ fontWeightRegular: 400,
+ fontWeightMedium: 600,
+ fontWeightBold: 700,
+ h1: {
+ fontWeight: 800,
+ lineHeight: 80 / 64,
+ fontSize: pxToRem(40),
+ ...responsiveFontSizes({ sm: 52, md: 58, lg: 64 }),
+ },
+ h2: {
+ fontWeight: 800,
+ lineHeight: 64 / 48,
+ fontSize: pxToRem(32),
+ ...responsiveFontSizes({ sm: 40, md: 44, lg: 48 }),
+ },
+ h3: {
+ fontWeight: 700,
+ lineHeight: 1.5,
+ fontSize: pxToRem(24),
+ ...responsiveFontSizes({ sm: 26, md: 30, lg: 32 }),
+ },
+ h4: {
+ fontWeight: 700,
+ lineHeight: 1.5,
+ fontSize: pxToRem(20),
+ ...responsiveFontSizes({ sm: 20, md: 24, lg: 24 }),
+ },
+ h5: {
+ fontWeight: 700,
+ lineHeight: 1.5,
+ fontSize: pxToRem(18),
+ ...responsiveFontSizes({ sm: 19, md: 20, lg: 20 }),
+ },
+ h6: {
+ fontWeight: 700,
+ lineHeight: 28 / 18,
+ fontSize: pxToRem(17),
+ ...responsiveFontSizes({ sm: 18, md: 18, lg: 18 }),
+ },
+ subtitle1: {
+ fontWeight: 600,
+ lineHeight: 1.5,
+ fontSize: pxToRem(16),
+ },
+ subtitle2: {
+ fontWeight: 600,
+ lineHeight: 22 / 14,
+ fontSize: pxToRem(14),
+ },
+ body1: {
+ lineHeight: 1.5,
+ fontSize: pxToRem(16),
+ },
+ body2: {
+ lineHeight: 22 / 14,
+ fontSize: pxToRem(14),
+ },
+ caption: {
+ lineHeight: 1.5,
+ fontSize: pxToRem(12),
+ },
+ overline: {
+ fontWeight: 700,
+ lineHeight: 1.5,
+ fontSize: pxToRem(12),
+ textTransform: 'uppercase',
+ },
+ button: {
+ fontWeight: 700,
+ lineHeight: 24 / 14,
+ fontSize: pxToRem(14),
+ textTransform: 'capitalize',
+ },
+}
+
+export default typography
diff --git a/src/views/LoginView.tsx b/src/views/LoginView.tsx
index 23e3e58f..29de2bcc 100644
--- a/src/views/LoginView.tsx
+++ b/src/views/LoginView.tsx
@@ -43,7 +43,11 @@ const LoginView: FC = () => {
-
+
Ticker Login
From 6c896913db4fd2934d8919d2aba481972ba903aa Mon Sep 17 00:00:00 2001
From: louis
Date: Tue, 25 Oct 2022 16:33:38 +0200
Subject: [PATCH 03/31] =?UTF-8?q?=F0=9F=92=84=20Rework=20the=20Layout=20an?=
=?UTF-8?q?d=20Navigation?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/navigation/Clock.tsx | 25 --------
src/components/navigation/Nav.tsx | 25 ++++++++
src/components/navigation/NavItem.tsx | 33 ++++++++++
src/components/navigation/Navigation.tsx | 74 ----------------------
src/components/navigation/UserDropdown.tsx | 51 +++++++++++++++
src/views/Layout.tsx | 53 ++++++++++++++--
6 files changed, 156 insertions(+), 105 deletions(-)
delete mode 100644 src/components/navigation/Clock.tsx
create mode 100644 src/components/navigation/Nav.tsx
create mode 100644 src/components/navigation/NavItem.tsx
delete mode 100644 src/components/navigation/Navigation.tsx
create mode 100644 src/components/navigation/UserDropdown.tsx
diff --git a/src/components/navigation/Clock.tsx b/src/components/navigation/Clock.tsx
deleted file mode 100644
index b572eceb..00000000
--- a/src/components/navigation/Clock.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React, { FC, useEffect, useState } from 'react'
-import Moment from 'react-moment'
-
-interface Props {
- format?: string
-}
-
-const Clock: FC = props => {
- const [date, setDate] = useState(new Date())
-
- useEffect(() => {
- const interval = setInterval(() => {
- setDate(new Date())
- }, 1000)
- return () => clearInterval(interval)
- }, [])
-
- return (
-
- {date}
-
- )
-}
-
-export default Clock
diff --git a/src/components/navigation/Nav.tsx b/src/components/navigation/Nav.tsx
new file mode 100644
index 00000000..c7ea8e45
--- /dev/null
+++ b/src/components/navigation/Nav.tsx
@@ -0,0 +1,25 @@
+import React, { FC } from 'react'
+import { colors, Container } from '@mui/material'
+
+interface Props {
+ children: React.ReactNode
+}
+
+const Nav: FC = ({ children }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Nav
diff --git a/src/components/navigation/NavItem.tsx b/src/components/navigation/NavItem.tsx
new file mode 100644
index 00000000..85ffde9c
--- /dev/null
+++ b/src/components/navigation/NavItem.tsx
@@ -0,0 +1,33 @@
+import { Box, Button, colors } from '@mui/material'
+import React, { FC } from 'react'
+import { Link } from 'react-router-dom'
+
+interface Props {
+ active: boolean
+ icon: React.ReactNode
+ title: string
+ to: string
+}
+
+const NavItem: FC = ({ active, icon, title, to }) => {
+ return (
+
+
+
+ )
+}
+
+export default NavItem
diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx
deleted file mode 100644
index 9f86ec87..00000000
--- a/src/components/navigation/Navigation.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React, { FC, useCallback } from 'react'
-import { Container, Dropdown, Image, Menu } from 'semantic-ui-react'
-import Clock from './Clock'
-import logo from '../../assets/logo.png'
-import { useNavigate, useLocation } from 'react-router-dom'
-import useAuth from '../useAuth'
-
-const Navigation: FC = () => {
- const { user, logout } = useAuth()
- const navigate = useNavigate()
- const location = useLocation()
-
- const userItem = (
-
-
- {
- logout()
- }, [logout])}
- >
- Logout
-
-
-
- )
-
- const usersItem = (
- navigate('/users'), [navigate])}
- >
- Users
-
- )
-
- const settingsItem = (
- navigate('/settings'), [navigate])}
- >
- Settings
-
- )
-
- return (
-
- )
-}
-
-export default Navigation
diff --git a/src/components/navigation/UserDropdown.tsx b/src/components/navigation/UserDropdown.tsx
new file mode 100644
index 00000000..ff9c0338
--- /dev/null
+++ b/src/components/navigation/UserDropdown.tsx
@@ -0,0 +1,51 @@
+import React, { FC, useCallback, useState } from 'react'
+import { AccountCircle } from '@mui/icons-material'
+import { IconButton, Menu, MenuItem } from '@mui/material'
+import useAuth from '../useAuth'
+
+const UserDropdown: FC = () => {
+ const [anchorEl, setAnchorEl] = useState(null)
+ const { user, logout } = useAuth()
+
+ const handleMenu = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget)
+ }
+
+ const handleClose = () => {
+ setAnchorEl(null)
+ }
+
+ const handleLogout = useCallback(() => {
+ logout()
+ }, [logout])
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
+
+export default UserDropdown
diff --git a/src/views/Layout.tsx b/src/views/Layout.tsx
index 814a3b26..4b42f5c8 100644
--- a/src/views/Layout.tsx
+++ b/src/views/Layout.tsx
@@ -1,17 +1,58 @@
import React, { FC } from 'react'
-import { Container } from 'semantic-ui-react'
-import Navigation from '../components/navigation/Navigation'
+import { Box, Container } from '@mui/material'
+import Nav from '../components/navigation/Nav'
+import NavItem from '../components/navigation/NavItem'
+import UserDropdown from '../components/navigation/UserDropdown'
+import useAuth from '../components/useAuth'
+import { useLocation } from 'react-router-dom'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import {
+ faGaugeHigh,
+ faGears,
+ faUsers,
+} from '@fortawesome/free-solid-svg-icons'
interface Props {
children: React.ReactNode
}
const Layout: FC = ({ children }) => {
+ const { user } = useAuth()
+ const location = useLocation()
+
return (
-
-
- {children}
-
+ <>
+
+ {children}
+ >
)
}
From 5de496a37824f4536445da1311abe4fe78966df1 Mon Sep 17 00:00:00 2001
From: louis
Date: Wed, 26 Oct 2022 22:51:38 +0200
Subject: [PATCH 04/31] =?UTF-8?q?=F0=9F=92=84=20Improve=20NavItem=20for=20?=
=?UTF-8?q?smaller=20Screens?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/navigation/NavItem.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/components/navigation/NavItem.tsx b/src/components/navigation/NavItem.tsx
index 85ffde9c..f38b6a3d 100644
--- a/src/components/navigation/NavItem.tsx
+++ b/src/components/navigation/NavItem.tsx
@@ -16,15 +16,15 @@ const NavItem: FC = ({ active, icon, title, to }) => {
size="large"
sx={{
mx: 1,
- px: 4,
+ px: { xs: 0, md: 4 },
backgroundColor: active ? colors.blue[50] : colors.grey[100],
borderRadius: 4,
color: colors.common['black'],
fontSize: '1rem',
}}
>
- {icon}
- {title}
+ {icon}
+ {title}
)
From cdd91d5a705e696919030b470d853f9978a07edc Mon Sep 17 00:00:00 2001
From: louis
Date: Wed, 26 Oct 2022 22:51:56 +0200
Subject: [PATCH 05/31] =?UTF-8?q?=F0=9F=92=84=20Add=20Loader=20Component?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/Loader.tsx | 15 +++++++++++++++
1 file changed, 15 insertions(+)
create mode 100644 src/components/Loader.tsx
diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx
new file mode 100644
index 00000000..c4b09310
--- /dev/null
+++ b/src/components/Loader.tsx
@@ -0,0 +1,15 @@
+import { CircularProgress, Stack, Typography } from '@mui/material'
+import React, { FC } from 'react'
+
+const Loader: FC = () => {
+ return (
+
+
+
+ Loading
+
+
+ )
+}
+
+export default Loader
From 1a144c028ce1021780989412130953e6383a749e Mon Sep 17 00:00:00 2001
From: louis
Date: Wed, 26 Oct 2022 23:29:23 +0200
Subject: [PATCH 06/31] =?UTF-8?q?=F0=9F=92=84=20Use=20MUI=20for=20UsersVie?=
=?UTF-8?q?w?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/ticker/TickersDropdown.tsx | 103 +++++++-----
src/components/user/UserForm.tsx | 187 ++++++++++++----------
src/components/user/UserList.tsx | 71 ++++----
src/components/user/UserListItem.tsx | 125 +++++++++++----
src/components/user/UserListItems.tsx | 6 +-
src/components/user/UserModalDelete.tsx | 70 +++++---
src/components/user/UserModalForm.tsx | 86 +++++-----
src/views/UsersView.test.tsx | 56 +++++++
src/views/UsersView.tsx | 51 ++++--
9 files changed, 485 insertions(+), 270 deletions(-)
create mode 100644 src/views/UsersView.test.tsx
diff --git a/src/components/ticker/TickersDropdown.tsx b/src/components/ticker/TickersDropdown.tsx
index 1966594f..95a7bc4c 100644
--- a/src/components/ticker/TickersDropdown.tsx
+++ b/src/components/ticker/TickersDropdown.tsx
@@ -1,59 +1,84 @@
-import React, {
- FC,
- SyntheticEvent,
- useCallback,
- useEffect,
- useState,
-} from 'react'
-import { Dropdown, DropdownItemProps, DropdownProps } from 'semantic-ui-react'
-import { useTickerApi } from '../../api/Ticker'
+import React, { FC, useEffect, useState } from 'react'
+import {
+ Box,
+ Chip,
+ FormControl,
+ InputLabel,
+ MenuItem,
+ OutlinedInput,
+ Select,
+ SelectChangeEvent,
+ SxProps,
+} from '@mui/material'
+import { Ticker, useTickerApi } from '../../api/Ticker'
import useAuth from '../useAuth'
interface Props {
name: string
- defaultValue?: Array
- onChange: (event: SyntheticEvent, input: DropdownProps) => void
+ defaultValue: Array
+ onChange: (tickers: number[]) => void
+ sx?: SxProps
}
-const TickersDropdown: FC = props => {
- const [options, setOptions] = useState([])
+const TickersDropdown: FC = ({ name, defaultValue, onChange, sx }) => {
+ const [options, setOptions] = useState>([])
+ const [tickers, setTickers] = useState>(defaultValue)
const { token } = useAuth()
const { getTickers } = useTickerApi(token)
- const onChange = useCallback(
- (event: SyntheticEvent, input: DropdownProps) => {
- props.onChange(event, input)
- },
- [props]
- )
+ const handleChange = (event: SelectChangeEvent) => {
+ if (typeof event.target.value !== 'string') {
+ setTickers(event.target.value)
+ onChange(event.target.value)
+ }
+ }
useEffect(() => {
getTickers()
.then(response => response.data.tickers)
.then(tickers => {
- const availableOptions: DropdownItemProps[] = []
- tickers.map(ticker => {
- availableOptions.push({
- key: ticker.id,
- text: ticker.title,
- value: ticker.id,
- })
- })
- setOptions(availableOptions)
+ setOptions(tickers)
})
- }, [getTickers])
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const renderValue = (selected: number[]) => {
+ const selectedTickers = options.filter(ticker => {
+ return selected.includes(ticker.id)
+ })
+
+ return (
+
+ {selectedTickers.map(ticker => (
+
+ ))}
+
+ )
+ }
return (
-
+
+ Tickers
+ }
+ label="Tickers"
+ multiple
+ name={name}
+ onChange={handleChange}
+ renderValue={renderValue}
+ value={tickers}
+ >
+ {options.map(ticker => (
+
+ ))}
+
+
)
}
diff --git a/src/components/user/UserForm.tsx b/src/components/user/UserForm.tsx
index c630f15b..c3d2cc29 100644
--- a/src/components/user/UserForm.tsx
+++ b/src/components/user/UserForm.tsx
@@ -1,25 +1,21 @@
-import React, {
- ChangeEvent,
- FC,
- FormEvent,
- SyntheticEvent,
- useCallback,
- useEffect,
-} from 'react'
-import {
- CheckboxProps,
- DropdownProps,
- Form,
- Header,
- InputOnChangeData,
-} from 'semantic-ui-react'
+import React, { FC, useEffect } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { User, useUserApi } from '../../api/User'
import { useQueryClient } from '@tanstack/react-query'
-import TickersDropdown from '../ticker/TickersDropdown'
import useAuth from '../useAuth'
+import {
+ FormControlLabel,
+ Checkbox,
+ FormGroup,
+ TextField,
+ Typography,
+ Grid,
+ Divider,
+} from '@mui/material'
+import TickersDropdown from '../ticker/TickersDropdown'
interface Props {
+ id: string
user?: User
callback: () => void
}
@@ -32,35 +28,25 @@ interface FormValues {
tickers: Array
}
-const UserForm: FC = props => {
- const user = props.user
+const UserForm: FC = ({ id, user, callback }) => {
const { token } = useAuth()
const { postUser, putUser } = useUserApi(token)
- const { handleSubmit, register, setValue } = useForm({
+ const {
+ formState: { errors },
+ handleSubmit,
+ register,
+ setValue,
+ watch,
+ } = useForm({
defaultValues: {
email: user?.email,
is_super_admin: user?.is_super_admin,
+ tickers: user?.tickers,
},
})
const queryClient = useQueryClient()
-
- const onChange = useCallback(
- (
- e: ChangeEvent | FormEvent | SyntheticEvent,
- {
- name,
- value,
- checked,
- }: InputOnChangeData | CheckboxProps | DropdownProps
- ) => {
- if (checked !== undefined) {
- setValue(name, checked)
- } else {
- setValue(name, value)
- }
- },
- [setValue]
- )
+ const isSuperAdminChecked = watch('is_super_admin')
+ const password = watch('password', '')
const onSubmit: SubmitHandler = data => {
const formData = {
@@ -73,67 +59,98 @@ const UserForm: FC = props => {
if (user) {
putUser(formData, user).finally(() => {
queryClient.invalidateQueries(['users'])
- props.callback()
+ callback()
})
} else {
postUser(formData).finally(() => {
queryClient.invalidateQueries(['users'])
- props.callback()
+ callback()
})
}
}
useEffect(() => {
- register('email')
- register('is_super_admin')
- register('password')
- register('password_validate')
register('tickers')
})
return (
-
-
-
-
-
-
-
-
- {!user?.is_super_admin ? (
- <>
-
-
- >
- ) : null}
-
+
)
}
diff --git a/src/components/user/UserList.tsx b/src/components/user/UserList.tsx
index 756c2529..7bca5acf 100644
--- a/src/components/user/UserList.tsx
+++ b/src/components/user/UserList.tsx
@@ -1,11 +1,17 @@
import React, { FC } from 'react'
-import { Button, Dimmer, Loader, Table } from 'semantic-ui-react'
import { useQuery } from '@tanstack/react-query'
import UserListItems from './UserListItems'
-import UserModalForm from './UserModalForm'
import useAuth from '../useAuth'
import { useUserApi } from '../../api/User'
import ErrorView from '../../views/ErrorView'
+import {
+ Table,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+} from '@mui/material'
+import Loader from '../Loader'
const UserList: FC = () => {
const { token } = useAuth()
@@ -13,11 +19,7 @@ const UserList: FC = () => {
const { isLoading, error, data } = useQuery(['users'], getUsers)
if (isLoading) {
- return (
-
- Loading
-
- )
+ return
}
if (error || data === undefined || data.status === 'error') {
@@ -27,41 +29,32 @@ const UserList: FC = () => {
const users = data.data.users
return (
-
+
-
-
- ID
- Admin
- Email
- Creation Time
-
-
-
+
+
+
+ ID
+
+
+ Admin
+
+
+ E-Mail
+
+
+ Creation Time
+
+
+
+
-
-
-
-
-
-
-
-
- }
- />
-
-
-
-
+
)
}
diff --git a/src/components/user/UserListItem.tsx b/src/components/user/UserListItem.tsx
index e2a621ac..d7cf4799 100644
--- a/src/components/user/UserListItem.tsx
+++ b/src/components/user/UserListItem.tsx
@@ -1,6 +1,22 @@
-import React, { FC } from 'react'
+import React, { FC, useState } from 'react'
+import {
+ faCheck,
+ faPencil,
+ faTrash,
+ faXmark,
+} from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { MoreVert } from '@mui/icons-material'
+import {
+ colors,
+ IconButton,
+ MenuItem,
+ Popover,
+ TableCell,
+ TableRow,
+ Typography,
+} from '@mui/material'
import Moment from 'react-moment'
-import { Button, Icon, Label, Table } from 'semantic-ui-react'
import { User } from '../../api/User'
import UserModalDelete from './UserModalDelete'
import UserModalForm from './UserModalForm'
@@ -9,35 +25,88 @@ interface Props {
user: User
}
-const UserListItem: FC = props => {
- const user = props.user
+const UserListItem: FC = ({ user }) => {
+ const [formModalOpen, setFormModalOpen] = useState(false)
+ const [deleteModalOpen, setDeleteModalOpen] = useState(false)
+ const [anchorEl, setAnchorEl] = useState(null)
+
+ const handleMenu = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget)
+ }
+
+ const handleClose = () => {
+ setAnchorEl(null)
+ }
return (
-
- {user.id}
-
-
-
- {user.email}
-
+
+
+ {user.id}
+
+
+ {user.is_super_admin ? (
+
+ ) : (
+
+ )}
+
+ {user.email}
+
-
-
-
- }
- user={user}
- />
- }
- user={user}
- />
-
-
-
+
+
+
+
+
+
+
+
+
+ setFormModalOpen(false)}
+ open={formModalOpen}
+ user={user}
+ />
+ setDeleteModalOpen(false)}
+ open={deleteModalOpen}
+ user={user}
+ />
+
+
)
}
diff --git a/src/components/user/UserListItems.tsx b/src/components/user/UserListItems.tsx
index e49cff05..0912132a 100644
--- a/src/components/user/UserListItems.tsx
+++ b/src/components/user/UserListItems.tsx
@@ -1,5 +1,5 @@
import React, { FC } from 'react'
-import { Table } from 'semantic-ui-react'
+import { TableBody } from '@mui/material'
import { User } from '../../api/User'
import UserListItem from './UserListItem'
@@ -9,11 +9,11 @@ interface Props {
const UserListItems: FC = ({ users }: Props) => {
return (
-
+
{users.map(user => (
))}
-
+
)
}
diff --git a/src/components/user/UserModalDelete.tsx b/src/components/user/UserModalDelete.tsx
index fa43cc2e..bd939b70 100644
--- a/src/components/user/UserModalDelete.tsx
+++ b/src/components/user/UserModalDelete.tsx
@@ -1,46 +1,66 @@
-import React, { FC, useCallback, useState } from 'react'
+import React, { FC, useCallback } from 'react'
import { useQueryClient } from '@tanstack/react-query'
-import { Confirm } from 'semantic-ui-react'
import { User, useUserApi } from '../../api/User'
import useAuth from '../useAuth'
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Stack,
+} from '@mui/material'
+import { Close } from '@mui/icons-material'
interface Props {
+ onClose: () => void
+ open: boolean
user: User
- trigger: React.ReactNode
}
-const UserModalDelete: FC = props => {
+const UserModalDelete: FC = ({ onClose, open, user }) => {
const { token } = useAuth()
const { deleteUser } = useUserApi(token)
- const [open, setOpen] = useState(false)
const queryClient = useQueryClient()
- const user = props.user
- const handleCancel = useCallback(() => {
- setOpen(false)
- }, [])
+ const handleClose = () => {
+ onClose()
+ }
- const handleConfirm = useCallback(() => {
+ const handleDelete = useCallback(() => {
deleteUser(user).finally(() => {
queryClient.invalidateQueries(['users'])
- setOpen(false)
+ onClose()
})
- }, [deleteUser, user, queryClient])
-
- const handleOpen = useCallback(() => {
- setOpen(true)
- }, [])
+ }, [deleteUser, user, queryClient, onClose])
return (
-
+
)
}
diff --git a/src/components/user/UserModalForm.tsx b/src/components/user/UserModalForm.tsx
index 5f88a4dd..edc2e72e 100644
--- a/src/components/user/UserModalForm.tsx
+++ b/src/components/user/UserModalForm.tsx
@@ -1,51 +1,59 @@
-import React, { FC, useCallback, useState } from 'react'
-import { Button, Header, Modal } from 'semantic-ui-react'
+import React, { FC } from 'react'
+import { Close } from '@mui/icons-material'
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Stack,
+} from '@mui/material'
import { User } from '../../api/User'
import UserForm from './UserForm'
interface Props {
+ onClose: () => void
+ open: boolean
user?: User
- trigger: React.ReactNode
}
-const UserModalForm: FC = props => {
- const [open, setOpen] = useState(false)
-
- const handleClose = useCallback(() => {
- setOpen(false)
- }, [])
-
- const handleOpen = useCallback(() => {
- setOpen(true)
- }, [])
+const UserModalForm: FC = ({ open, onClose, user }) => {
+ const handleClose = () => {
+ onClose()
+ }
return (
-
- {props.user ? 'Edit User' : 'Create User'}
-
-
-
-
-
-
-
-
-
-
-
+
)
}
diff --git a/src/views/UsersView.test.tsx b/src/views/UsersView.test.tsx
new file mode 100644
index 00000000..d74d8868
--- /dev/null
+++ b/src/views/UsersView.test.tsx
@@ -0,0 +1,56 @@
+import React from 'react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { render, screen } from '@testing-library/react'
+import { MemoryRouter } from 'react-router'
+import { AuthProvider } from '../components/useAuth'
+import UsersView from './UsersView'
+import sign from 'jwt-encode'
+import fetchMock from 'jest-fetch-mock'
+
+describe('UsersView', function () {
+ const jwt = sign(
+ { id: 1, email: 'louis@systemli.org', roles: ['admin', 'user'] },
+ 'secret'
+ )
+
+ function setup() {
+ const client = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ })
+ return render(
+
+
+
+
+
+
+
+ )
+ }
+
+ test('renders list', async function () {
+ jest.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValue(jwt)
+ fetchMock.mockResponseOnce(
+ JSON.stringify({
+ data: {
+ users: [
+ {
+ id: 1,
+ creation_date: new Date(),
+ email: 'admin@systemli.org',
+ is_super_admin: true,
+ },
+ ],
+ },
+ })
+ )
+ setup()
+
+ expect(screen.getByText(/loading/i)).toBeInTheDocument()
+ expect(await screen.findByText('admin@systemli.org')).toBeInTheDocument()
+ })
+})
diff --git a/src/views/UsersView.tsx b/src/views/UsersView.tsx
index 6baab93c..5315663d 100644
--- a/src/views/UsersView.tsx
+++ b/src/views/UsersView.tsx
@@ -1,22 +1,49 @@
-import React, { FC } from 'react'
-import { Grid, Header } from 'semantic-ui-react'
+import { faPlus } from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { Button, Card, Grid, Stack, Typography } from '@mui/material'
+import React, { FC, useState } from 'react'
import UserList from '../components/user/UserList'
+import UserModalForm from '../components/user/UserModalForm'
import Layout from './Layout'
const UsersView: FC = () => {
+ const [formModalOpen, setFormModalOpen] = useState(false)
+
return (
-
-
-
-
-
-
-
-
+
+
+
+
+ Users
+
+
+ {
+ setFormModalOpen(false)
+ }}
+ open={formModalOpen}
+ />
+
+
+
+
-
-
+
+
)
From 640cba6e72823a3bd4d9ec71347a7a12fdc5280c Mon Sep 17 00:00:00 2001
From: louis
Date: Thu, 27 Oct 2022 18:14:23 +0200
Subject: [PATCH 07/31] =?UTF-8?q?=F0=9F=90=9B=20Fix=20types=20in=20typogra?=
=?UTF-8?q?phy.ts?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/theme/typography.ts | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/src/theme/typography.ts b/src/theme/typography.ts
index 175ca43a..8aa25ea5 100644
--- a/src/theme/typography.ts
+++ b/src/theme/typography.ts
@@ -1,4 +1,4 @@
-import { Typography } from '@mui/material/styles/createTypography'
+import { TypographyOptions } from '@mui/material/styles/createTypography'
export function remToPx(value: number) {
return Math.round(value * 16)
@@ -8,7 +8,15 @@ export function pxToRem(value: number) {
return `${value / 16}rem`
}
-export function responsiveFontSizes({ sm, md, lg }) {
+export function responsiveFontSizes({
+ sm,
+ md,
+ lg,
+}: {
+ sm: number
+ md: number
+ lg: number
+}) {
return {
'@media (min-width:600px)': {
fontSize: pxToRem(sm),
@@ -27,7 +35,7 @@ export function responsiveFontSizes({ sm, md, lg }) {
const FONT_PRIMARY = 'Roboto, sans-serif' // Google Font
// const FONT_SECONDARY = 'CircularStd, sans-serif'; // Local Font
-const typography: Typography = {
+const typography: TypographyOptions = {
fontFamily: FONT_PRIMARY,
fontWeightRegular: 400,
fontWeightMedium: 600,
From 49ac683e195e676f17ad8144257652f87b3d3eac Mon Sep 17 00:00:00 2001
From: louis
Date: Thu, 27 Oct 2022 19:14:22 +0200
Subject: [PATCH 08/31] =?UTF-8?q?=E2=9C=85=20Add=20Tests=20for=20UsersView?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/user/UserListItem.tsx | 4 +-
src/views/UsersView.test.tsx | 105 ++++++++++++++++++++++++++-
2 files changed, 107 insertions(+), 2 deletions(-)
diff --git a/src/components/user/UserListItem.tsx b/src/components/user/UserListItem.tsx
index d7cf4799..3c002589 100644
--- a/src/components/user/UserListItem.tsx
+++ b/src/components/user/UserListItem.tsx
@@ -55,7 +55,7 @@ const UserListItem: FC = ({ user }) => {
-
+
= ({ user }) => {
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
+ setFormModalOpen(false)}
+ open={formModalOpen}
+ ticker={ticker}
+ />
+ setDeleteModalOpen(false)}
+ open={deleteModalOpen}
+ ticker={ticker}
+ />
+
+
)
}
diff --git a/src/components/ticker/TickerListItems.tsx b/src/components/ticker/TickerListItems.tsx
index f06785f1..0f000c56 100644
--- a/src/components/ticker/TickerListItems.tsx
+++ b/src/components/ticker/TickerListItems.tsx
@@ -1,5 +1,5 @@
+import { TableBody } from '@mui/material'
import React, { FC } from 'react'
-import { Table } from 'semantic-ui-react'
import { Ticker } from '../../api/Ticker'
import TickerListItem from './TickerListItem'
@@ -9,11 +9,11 @@ interface Props {
const TickerListItems: FC = ({ tickers }: Props) => {
return (
-
+
{tickers.map(ticker => (
))}
-
+
)
}
diff --git a/src/components/ticker/TickerModalDelete.tsx b/src/components/ticker/TickerModalDelete.tsx
index d03ab64f..0d41984d 100644
--- a/src/components/ticker/TickerModalDelete.tsx
+++ b/src/components/ticker/TickerModalDelete.tsx
@@ -1,45 +1,65 @@
-import React, { FC, useCallback, useState } from 'react'
+import React, { FC, useCallback } from 'react'
import { useQueryClient } from '@tanstack/react-query'
-import { Confirm } from 'semantic-ui-react'
import { Ticker, useTickerApi } from '../../api/Ticker'
import useAuth from '../useAuth'
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Stack,
+} from '@mui/material'
+import { Close } from '@mui/icons-material'
interface Props {
+ onClose: () => void
+ open: boolean
ticker: Ticker
- trigger: React.ReactNode
}
-const TickerModalDelete: FC = props => {
- const [open, setOpen] = useState(false)
+const TickerModalDelete: FC = ({ open, onClose, ticker }) => {
const { token } = useAuth()
const { deleteTicker } = useTickerApi(token)
const queryClient = useQueryClient()
- const ticker = props.ticker
- const handleCancel = useCallback(() => {
- setOpen(false)
- }, [])
+ const handleClose = () => {
+ onClose()
+ }
- const handleConfirm = useCallback(() => {
+ const handleDelete = useCallback(() => {
deleteTicker(ticker).finally(() => {
queryClient.invalidateQueries(['tickers'])
})
}, [deleteTicker, ticker, queryClient])
- const handleOpen = useCallback(() => {
- setOpen(true)
- }, [])
-
return (
-
+
)
}
diff --git a/src/components/ticker/TickerModalForm.tsx b/src/components/ticker/TickerModalForm.tsx
index 7d438bb0..0892fa18 100644
--- a/src/components/ticker/TickerModalForm.tsx
+++ b/src/components/ticker/TickerModalForm.tsx
@@ -1,52 +1,59 @@
-import React, { FC, useCallback, useState } from 'react'
-import { Button, Header, Modal } from 'semantic-ui-react'
+import { Close } from '@mui/icons-material'
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Stack,
+} from '@mui/material'
+import React, { FC } from 'react'
import { Ticker } from '../../api/Ticker'
import TickerForm from './TickerForm'
interface Props {
+ onClose: () => void
+ open: boolean
ticker?: Ticker
- trigger: React.ReactNode
}
-const TickerModalForm: FC = props => {
- const [open, setOpen] = useState(false)
-
- const handleClose = useCallback(() => {
- setOpen(false)
- }, [])
-
- const handleOpen = useCallback(() => {
- setOpen(true)
- }, [])
+const TickerModalForm: FC = ({ onClose, open, ticker }) => {
+ const handleClose = () => {
+ onClose()
+ }
return (
-
- {' '}
-
- {props.ticker ? `Edit ${props.ticker.title}` : 'Create Ticker'}
-
-
-
-
-
-
-
-
-
-
-
-
+
)
}
diff --git a/src/views/HomeView.tsx b/src/views/HomeView.tsx
index e0c0f8be..568957fe 100644
--- a/src/views/HomeView.tsx
+++ b/src/views/HomeView.tsx
@@ -1,93 +1,55 @@
-import React, { FC } from 'react'
-import {
- Button,
- Dimmer,
- Grid,
- Header,
- Loader,
- Message,
-} from 'semantic-ui-react'
+import React, { FC, useState } from 'react'
import TickerList from '../components/ticker/TickerList'
import useAuth from '../components/useAuth'
-import { useTickerApi } from '../api/Ticker'
-import { useQuery } from '@tanstack/react-query'
-import TickerModalForm from '../components/ticker/TickerModalForm'
import Layout from './Layout'
-import ErrorView from './ErrorView'
-import { Navigate } from 'react-router'
+import { Button, Card, Grid, Stack, Typography } from '@mui/material'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faPlus } from '@fortawesome/free-solid-svg-icons'
+import TickerModalForm from '../components/ticker/TickerModalForm'
const HomeView: FC = () => {
- const { token, user } = useAuth()
- const { getTickers } = useTickerApi(token)
- const { isLoading, error, data } = useQuery(['tickers'], getTickers)
-
- if (isLoading) {
- return (
-
- Loading
-
- )
- }
-
- if (error || data === undefined || data.status === 'error') {
- return (
-
-
- Unable to fetch tickers from server.
-
-
- )
- }
-
- const tickers = data.data.tickers
-
- if (tickers.length === 0 && user?.roles.includes('admin')) {
- return (
-
-
-
-
-
- Welcome!
- You need to create a your first ticker.
-
-
- }
- />
-
-
-
-
-
-
- )
- }
-
- if (tickers.length === 1 && !user?.roles.includes('admin')) {
- return
- }
+ const { user } = useAuth()
+ const [formModalOpen, setFormModalOpen] = useState(false)
return (
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Tickers
+
+ {user?.roles.includes('admin') ? (
+ <>
+ {
+ setFormModalOpen(true)
+ }}
+ startIcon={}
+ variant="contained"
+ >
+ New Ticker
+
+ {
+ setFormModalOpen(false)
+ }}
+ open={formModalOpen}
+ />
+ >
+ ) : null}
+
+
+
+
+
+
+
)
From ac8618c2b1c8df92390e2ef8ad9747e6e92f92cc Mon Sep 17 00:00:00 2001
From: louis
Date: Sat, 29 Oct 2022 19:20:16 +0200
Subject: [PATCH 13/31] =?UTF-8?q?=E2=9C=85=20Add=20Tests=20for=20HomeView?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
jest.config.ts | 4 +-
src/__mocks__/react-leaflet.tsx | 13 ++
src/components/ticker/TickerList.tsx | 58 ++++----
src/views/HomeView.test.tsx | 201 +++++++++++++++++++++++++++
4 files changed, 245 insertions(+), 31 deletions(-)
create mode 100644 src/__mocks__/react-leaflet.tsx
create mode 100644 src/views/HomeView.test.tsx
diff --git a/jest.config.ts b/jest.config.ts
index 4fed3752..a14cd0eb 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -5,7 +5,7 @@ const config: Config.InitialOptions = {
testEnvironment: 'jsdom',
preset: 'ts-jest',
moduleNameMapper: {
- 'react-markdown': '/src/__mocks_/react-markdown.js',
+ 'react-leaflet': '/src/__mocks__/react-leaflet.tsx',
'^.+.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub',
},
@@ -15,7 +15,7 @@ const config: Config.InitialOptions = {
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub',
},
- transformIgnorePatterns: ['/node_modules/(?!react-markdown/)'],
+ transformIgnorePatterns: ['/node_modules/(?!react-leaflet)'],
setupFilesAfterEnv: ['./jest-setup.ts'],
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
}
diff --git a/src/__mocks__/react-leaflet.tsx b/src/__mocks__/react-leaflet.tsx
new file mode 100644
index 00000000..c002b669
--- /dev/null
+++ b/src/__mocks__/react-leaflet.tsx
@@ -0,0 +1,13 @@
+import React from 'react'
+
+export function MapContainer({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
+
+export function Marker({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
+
+export function TileLayer({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/src/components/ticker/TickerList.tsx b/src/components/ticker/TickerList.tsx
index 57333108..0800ed62 100644
--- a/src/components/ticker/TickerList.tsx
+++ b/src/components/ticker/TickerList.tsx
@@ -36,35 +36,35 @@ const TickerList: FC = () => {
const tickers = data.data.tickers
- if (tickers.length === 0 && user?.roles.includes('admin')) {
- return (
-
-
-
- Welcome!
-
-
- There are no tickers yet. To start with a ticker, create one.
-
-
-
- )
- }
-
- if (tickers.length === 0 && !user?.roles.includes('admin')) {
- return (
-
-
-
- Oh no! Something unexpected happened
-
-
- Currently there are no tickers for you. Contact your administrator
- if that should be different.
-
-
-
- )
+ if (tickers.length === 0) {
+ if (user?.roles.includes('admin')) {
+ return (
+
+
+
+ Welcome!
+
+
+ There are no tickers yet. To start with a ticker, create one.
+
+
+
+ )
+ } else {
+ return (
+
+
+
+ Oh no! Something unexpected happened
+
+
+ Currently there are no tickers for you. Contact your administrator
+ if that should be different.
+
+
+
+ )
+ }
}
if (tickers.length === 1 && !user?.roles.includes('admin')) {
diff --git a/src/views/HomeView.test.tsx b/src/views/HomeView.test.tsx
new file mode 100644
index 00000000..807c8922
--- /dev/null
+++ b/src/views/HomeView.test.tsx
@@ -0,0 +1,201 @@
+import React from 'react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { render, screen } from '@testing-library/react'
+import sign from 'jwt-encode'
+import { MemoryRouter } from 'react-router'
+import { AuthProvider } from '../components/useAuth'
+import HomeView from './HomeView'
+import userEvent from '@testing-library/user-event'
+
+describe('HomeView', function () {
+ beforeEach(() => {
+ fetchMock.resetMocks()
+ })
+
+ function jwt(role: string): string {
+ return sign(
+ {
+ id: 1,
+ email: 'louis@systemli.org',
+ roles: role === 'admin' ? ['admin', 'user'] : ['user'],
+ exp: new Date().getTime() / 1000 + 600,
+ },
+ 'secret'
+ )
+ }
+
+ const emptyTickerResponse = JSON.stringify({
+ data: { tickers: [] },
+ status: 'success',
+ })
+
+ const singleTickerResponse = JSON.stringify({
+ data: {
+ tickers: [
+ {
+ id: 1,
+ creation_date: new Date(),
+ domain: 'localhost',
+ title: 'title',
+ description: 'description',
+ active: true,
+ },
+ ],
+ },
+ status: 'success',
+ })
+
+ function setup() {
+ const client = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ })
+ return render(
+
+
+
+
+
+
+
+ )
+ }
+
+ test('render empty list for admins', async function () {
+ jest
+ .spyOn(window.localStorage.__proto__, 'getItem')
+ .mockReturnValue(jwt('admin'))
+ fetchMock.mockIf(/^http:\/\/localhost:8080\/.*$/, (request: Request) => {
+ if (request.url.endsWith('/admin/tickers')) {
+ return Promise.resolve(emptyTickerResponse)
+ }
+
+ return Promise.resolve(
+ JSON.stringify({
+ data: {},
+ status: 'error',
+ error: 'error message',
+ })
+ )
+ })
+ setup()
+
+ expect(screen.getByText(/loading/i)).toBeInTheDocument()
+ expect(
+ await screen.findByText(
+ 'There are no tickers yet. To start with a ticker, create one.'
+ )
+ ).toBeInTheDocument()
+ })
+
+ test('render empty list for user', async function () {
+ jest
+ .spyOn(window.localStorage.__proto__, 'getItem')
+ .mockReturnValue(jwt('user'))
+ fetchMock.mockIf(/^http:\/\/localhost:8080\/.*$/, (request: Request) => {
+ if (request.url.endsWith('/admin/tickers')) {
+ return Promise.resolve(emptyTickerResponse)
+ }
+
+ return Promise.resolve(
+ JSON.stringify({
+ data: {},
+ status: 'error',
+ error: 'error message',
+ })
+ )
+ })
+ setup()
+
+ expect(screen.getByText(/loading/i)).toBeInTheDocument()
+ expect(
+ await screen.findByText(
+ 'Currently there are no tickers for you. Contact your administrator if that should be different.'
+ )
+ ).toBeInTheDocument()
+ })
+
+ test('render ticker view for user with 1 ticker', async function () {
+ jest
+ .spyOn(window.localStorage.__proto__, 'getItem')
+ .mockReturnValue(jwt('user'))
+ fetchMock.mockIf(/^http:\/\/localhost:8080\/.*$/, (request: Request) => {
+ if (request.url.endsWith('/admin/tickers')) {
+ return Promise.resolve(singleTickerResponse)
+ }
+
+ return Promise.resolve(
+ JSON.stringify({
+ data: {},
+ status: 'error',
+ error: 'error message',
+ })
+ )
+ })
+ setup()
+
+ expect(screen.getByText(/loading/i)).toBeInTheDocument()
+ })
+
+ test('renders list with entries', async function () {
+ jest
+ .spyOn(window.localStorage.__proto__, 'getItem')
+ .mockReturnValue(jwt('admin'))
+ fetchMock.mockIf(/^http:\/\/localhost:8080\/.*$/, (request: Request) => {
+ if (request.url.endsWith('/admin/tickers')) {
+ return Promise.resolve(singleTickerResponse)
+ }
+
+ return Promise.resolve(
+ JSON.stringify({
+ data: {},
+ status: 'error',
+ error: 'error message',
+ })
+ )
+ })
+ setup()
+
+ expect(screen.getByText(/loading/i)).toBeInTheDocument()
+ expect(await screen.findByText('localhost')).toBeInTheDocument()
+ })
+
+ test('open create ticker form', async function () {
+ jest
+ .spyOn(window.localStorage.__proto__, 'getItem')
+ .mockReturnValue(jwt('admin'))
+ fetchMock.mockIf(/^http:\/\/localhost:8080\/.*$/, (request: Request) => {
+ if (request.url.endsWith('/admin/tickers')) {
+ return Promise.resolve(emptyTickerResponse)
+ }
+
+ return Promise.resolve(
+ JSON.stringify({
+ data: {},
+ status: 'error',
+ error: 'error message',
+ })
+ )
+ })
+ setup()
+
+ expect(screen.getByText(/loading/i)).toBeInTheDocument()
+
+ const button = screen.getByRole('button', { name: 'New Ticker' })
+ expect(button).toBeInTheDocument()
+
+ await userEvent.click(button)
+
+ const dialogTitle = screen.getByText(/create ticker/i)
+ expect(dialogTitle).toBeInTheDocument()
+
+ const closeButton = screen.getByRole('button', { name: /close/i })
+ expect(closeButton).toBeInTheDocument()
+
+ await userEvent.click(closeButton)
+ expect(dialogTitle).not.toBeVisible()
+ })
+})
From 89af64859c2b04acf92d6c5ce9996aea11ec296a Mon Sep 17 00:00:00 2001
From: louis
Date: Tue, 17 Jan 2023 17:52:29 -0800
Subject: [PATCH 14/31] =?UTF-8?q?=F0=9F=92=84=20Use=20MUI=20for=20Message?=
=?UTF-8?q?=20Form=20&=20Ticker=20Card?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/common/NamedListItem.tsx | 20 +++
src/components/message/AttachmentPreview.tsx | 35 ++++
src/components/message/AttachmentsPreview.tsx | 33 ++++
.../message/MessageAttachmentsButton.tsx | 54 ------
.../message/MessageAttachmentsPreview.tsx | 49 ------
src/components/message/MessageForm.tsx | 156 +++++++++---------
src/components/message/MessageFormCounter.tsx | 37 +++--
src/components/message/MessageMapModal.tsx | 109 ++++++------
src/components/message/UploadButton.tsx | 54 ++++++
src/components/ticker/MastodonCard.tsx | 115 ++++++-------
.../ticker/SocialConnectionChip.tsx | 25 +++
src/components/ticker/Ticker.tsx | 142 ++++++++--------
src/components/ticker/TickerCard.tsx | 99 +++++++----
13 files changed, 515 insertions(+), 413 deletions(-)
create mode 100644 src/components/common/NamedListItem.tsx
create mode 100644 src/components/message/AttachmentPreview.tsx
create mode 100644 src/components/message/AttachmentsPreview.tsx
delete mode 100644 src/components/message/MessageAttachmentsButton.tsx
delete mode 100644 src/components/message/MessageAttachmentsPreview.tsx
create mode 100644 src/components/message/UploadButton.tsx
create mode 100644 src/components/ticker/SocialConnectionChip.tsx
diff --git a/src/components/common/NamedListItem.tsx b/src/components/common/NamedListItem.tsx
new file mode 100644
index 00000000..8938403f
--- /dev/null
+++ b/src/components/common/NamedListItem.tsx
@@ -0,0 +1,20 @@
+import React, { FC } from 'react'
+import { Box, Typography } from '@mui/material'
+
+interface Props {
+ title: string
+ children: React.ReactNode
+}
+
+const NamedListItem: FC = ({ title, children }) => {
+ return (
+
+
+ {title}
+
+ {children}
+
+ )
+}
+
+export default NamedListItem
diff --git a/src/components/message/AttachmentPreview.tsx b/src/components/message/AttachmentPreview.tsx
new file mode 100644
index 00000000..64bd6c94
--- /dev/null
+++ b/src/components/message/AttachmentPreview.tsx
@@ -0,0 +1,35 @@
+import React, { FC } from 'react'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { IconButton, ImageListItem } from '@mui/material'
+import { Upload } from '../../api/Upload'
+import { faXmarkSquare } from '@fortawesome/free-solid-svg-icons'
+
+interface Props {
+ onDelete: (upload: Upload) => void
+ upload: Upload
+}
+
+const AttachmentPreview: FC = ({ onDelete, upload }) => {
+ const handleDelete = () => {
+ onDelete(upload)
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default AttachmentPreview
diff --git a/src/components/message/AttachmentsPreview.tsx b/src/components/message/AttachmentsPreview.tsx
new file mode 100644
index 00000000..21bf698b
--- /dev/null
+++ b/src/components/message/AttachmentsPreview.tsx
@@ -0,0 +1,33 @@
+import { ImageList } from '@mui/material'
+import React, { FC } from 'react'
+import { Upload } from '../../api/Upload'
+import AttachmentPreview from './AttachmentPreview'
+
+interface Props {
+ attachments: Upload[]
+ onDelete: (upload: Upload) => void
+}
+
+const AttachmentsPreview: FC = ({ attachments, onDelete }) => {
+ const images = attachments.map((upload, key) => {
+ return (
+ onDelete(upload)}
+ upload={upload}
+ />
+ )
+ })
+
+ if (images.length === 0) {
+ return null
+ }
+
+ return (
+
+ {images}
+
+ )
+}
+
+export default AttachmentsPreview
diff --git a/src/components/message/MessageAttachmentsButton.tsx b/src/components/message/MessageAttachmentsButton.tsx
deleted file mode 100644
index 3618eb56..00000000
--- a/src/components/message/MessageAttachmentsButton.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React, { createRef, FC, useCallback } from 'react'
-import { Button } from 'semantic-ui-react'
-import { Ticker } from '../../api/Ticker'
-import { useUploadApi, Upload } from '../../api/Upload'
-import useAuth from '../useAuth'
-
-interface Props {
- ticker: Ticker
- onUpload: (uploads: Upload[]) => void
-}
-
-const MessageAttachmentsButton: FC = props => {
- const ref = createRef()
- const { token } = useAuth()
- const { postUpload } = useUploadApi(token)
-
- const refClick = useCallback(() => {
- ref.current?.click()
- }, [ref])
-
- const onUpload = useCallback(
- (e: React.FormEvent) => {
- e.preventDefault()
- // @ts-ignore
- const files = e.target.files as Array
- const formData = new FormData()
- for (let i = 0; i < files.length; i++) {
- // @ts-ignore
- formData.append('files', files[i])
- }
- formData.append('ticker', props.ticker.id.toString())
-
- postUpload(formData).then(response => {
- props.onUpload(response.data.uploads)
- })
- },
- [postUpload, props]
- )
-
- return (
-
-
-
-
- )
-}
-
-export default MessageAttachmentsButton
diff --git a/src/components/message/MessageAttachmentsPreview.tsx b/src/components/message/MessageAttachmentsPreview.tsx
deleted file mode 100644
index 15341765..00000000
--- a/src/components/message/MessageAttachmentsPreview.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, { FC, useCallback } from 'react'
-import { Button, ButtonProps, Image } from 'semantic-ui-react'
-import { Upload } from '../../api/Upload'
-
-interface Props {
- attachments: Upload[]
- onDelete: (upload: Upload) => void
-}
-
-const MessageAttachmentsPreview: FC = props => {
- const onClick = useCallback(
- (e: React.MouseEvent, data: ButtonProps) => {
- const upload = data.upload as Upload
- props.onDelete(upload)
- },
- [props]
- )
-
- const images = props.attachments.map((upload, key) => {
- return (
-
-
-
-
- )
- })
-
- if (images.length === 0) {
- return null
- }
-
- return {images}
-}
-
-export default MessageAttachmentsPreview
diff --git a/src/components/message/MessageForm.tsx b/src/components/message/MessageForm.tsx
index 216f0a9c..50ce6591 100644
--- a/src/components/message/MessageForm.tsx
+++ b/src/components/message/MessageForm.tsx
@@ -1,28 +1,29 @@
-import React, {
- ChangeEvent,
- FC,
- FormEvent,
- useCallback,
- useEffect,
- useState,
-} from 'react'
+import React, { FC, useCallback, useEffect, useState } from 'react'
import { useMessageApi } from '../../api/Message'
-import {
- Button,
- Form,
- Message as Error,
- TextAreaProps,
-} from 'semantic-ui-react'
import { Ticker } from '../../api/Ticker'
import { SubmitHandler, useForm } from 'react-hook-form'
import { useQueryClient } from '@tanstack/react-query'
import MessageFormCounter from './MessageFormCounter'
import useAuth from '../useAuth'
import { Upload } from '../../api/Upload'
-import MessageAttachmentsButton from './MessageAttachmentsButton'
-import MessageAttachmentsPreview from './MessageAttachmentsPreview'
+import UploadButton from './UploadButton'
+import AttachmentsPreview from './AttachmentsPreview'
import MessageMapModal from './MessageMapModal'
import { FeatureCollection, Geometry } from 'geojson'
+import {
+ Box,
+ Button,
+ FormGroup,
+ IconButton,
+ Stack,
+ TextField,
+} from '@mui/material'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import {
+ faMapLocationDot,
+ faPaperPlane,
+} from '@fortawesome/free-solid-svg-icons'
+import palette from '../../theme/palette'
interface Props {
ticker: Ticker
@@ -36,23 +37,23 @@ export const MESSAGE_LIMIT = 280
const MessageForm: FC = ({ ticker }) => {
const {
- formState: { isSubmitSuccessful },
+ formState: { isSubmitSuccessful, errors },
handleSubmit,
reset,
- setValue,
+ register,
watch,
- } = useForm()
+ } = useForm({ mode: 'onSubmit' })
const { token } = useAuth()
const { postMessage } = useMessageApi(token)
const queryClient = useQueryClient()
- const watchMessage = watch('message', '')
+ const [isSubmitting, setIsSubmitting] = useState(false)
const [attachments, setAttachments] = useState([])
+ const [mapDialogOpen, setMapDialogOpen] = useState(false)
const emptyMap: FeatureCollection = {
type: 'FeatureCollection',
features: [],
}
const [map, setMap] = useState>(emptyMap)
- const [errorMessage, setErrorMessage] = useState('')
const onUpload = useCallback(
(uploads: Upload[]) => {
@@ -81,23 +82,8 @@ const MessageForm: FC = ({ ticker }) => {
[]
)
- const onChange = useCallback(
- (e: ChangeEvent | FormEvent, { name, value }: TextAreaProps) => {
- setValue(name, value)
- if (watchMessage?.length > MESSAGE_LIMIT) {
- setErrorMessage(
- `The message is too long. You must remove ${
- watchMessage?.length - MESSAGE_LIMIT
- } characters.`
- )
- } else if (errorMessage !== '') {
- setErrorMessage('')
- }
- },
- [errorMessage, setValue, watchMessage?.length]
- )
-
const onSubmit: SubmitHandler = data => {
+ setIsSubmitting(true)
const uploads = attachments.map(upload => {
return upload.id
})
@@ -106,6 +92,7 @@ const MessageForm: FC = ({ ticker }) => {
() => {
queryClient.invalidateQueries(['messages', ticker.id])
setAttachments([])
+ setIsSubmitting(false)
}
)
}
@@ -116,47 +103,66 @@ const MessageForm: FC = ({ ticker }) => {
})
}, [isSubmitSuccessful, reset])
+ const message = watch('message')
+
return (
-
-
+
+
+
+
+
+ }
+ sx={{ mr: 1 }}
+ type="submit"
+ variant="outlined"
+ >
+ Send
+
+
+ setMapDialogOpen(true)}>
+
+
+ setMapDialogOpen(false)}
+ open={mapDialogOpen}
+ ticker={ticker}
+ />
+
+
+
+
+
-
-
-
-
-
-
- }
- />
-
-
+
+
)
}
diff --git a/src/components/message/MessageFormCounter.tsx b/src/components/message/MessageFormCounter.tsx
index ba79fe97..f13c719c 100644
--- a/src/components/message/MessageFormCounter.tsx
+++ b/src/components/message/MessageFormCounter.tsx
@@ -1,5 +1,6 @@
+import { ChipPropsColorOverrides, Chip } from '@mui/material'
+import { OverridableStringUnion } from '@mui/types'
import React, { FC, useEffect, useState } from 'react'
-import { Label, SemanticCOLORS } from 'semantic-ui-react'
import { MESSAGE_LIMIT } from './MessageForm'
interface Props {
@@ -7,26 +8,38 @@ interface Props {
}
const MessageFormCounter: FC = ({ letterCount }) => {
- const [color, setColor] = useState('green')
+ const [color, setColor] =
+ useState<
+ OverridableStringUnion<
+ | 'default'
+ | 'primary'
+ | 'secondary'
+ | 'error'
+ | 'info'
+ | 'success'
+ | 'warning',
+ ChipPropsColorOverrides
+ >
+ >('default')
- //TODO: Calculate length for Twitter (cutting links to 20 characters)
useEffect(() => {
if (letterCount > MESSAGE_LIMIT) {
- setColor('red')
- } else if (letterCount >= 260) {
- setColor('orange')
- } else if (letterCount >= 220) {
- setColor('yellow')
+ setColor('error')
+ } else if (letterCount >= 240) {
+ setColor('warning')
+ } else if (letterCount === 0) {
+ setColor('default')
} else {
- setColor('green')
+ setColor('success')
}
}, [letterCount])
return (
-
)
}
diff --git a/src/components/message/MessageMapModal.tsx b/src/components/message/MessageMapModal.tsx
index d7b22956..e9ade0a5 100644
--- a/src/components/message/MessageMapModal.tsx
+++ b/src/components/message/MessageMapModal.tsx
@@ -1,62 +1,77 @@
-import React, { FC, useCallback, useState } from 'react'
+import React, { FC, useState } from 'react'
import L, { FeatureGroup as FG, latLng } from 'leaflet'
import { FeatureGroup, GeoJSON, MapContainer, TileLayer } from 'react-leaflet'
import { EditControl } from 'react-leaflet-draw'
-import { Button, Modal } from 'semantic-ui-react'
import { FeatureCollection, Geometry } from 'geojson'
import { Ticker } from '../../api/Ticker'
import { leafletOnDataAddFitToBounds } from '../../lib/leafletFitBoundsHelper'
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Stack,
+} from '@mui/material'
+import { Close } from '@mui/icons-material'
interface Props {
- callback: (features: FeatureCollection) => void
+ open: boolean
+ onClose: () => void
+ onChange: (features: FeatureCollection) => void
map: FeatureCollection
ticker: Ticker
- trigger: React.ReactElement
}
-const MessageMapModal: FC = props => {
- const [open, setOpen] = useState(false)
+const MessageMapModal: FC = ({
+ open,
+ onChange,
+ onClose,
+ map,
+ ticker,
+}) => {
const [featureGroup, setFeatureGroup] = useState(new L.FeatureGroup())
- const position = latLng(props.ticker.location.lat, props.ticker.location.lon)
+ const position = latLng(ticker.location.lat, ticker.location.lon)
const zoom = 7
- const onClose = useCallback(() => {
- setOpen(false)
- }, [])
+ const handleClose = () => {
+ onClose()
+ }
- const onOpen = useCallback(() => {
- setOpen(true)
- }, [])
-
- const onFeatureGroupUpdate = useCallback((ref: FG) => {
+ const onFeatureGroupUpdate = (ref: FG) => {
if (ref !== null) {
setFeatureGroup(ref)
}
- }, [])
+ }
- const onSubmit = useCallback(() => {
- const geoJSON = new L.GeoJSON(props.map)
+ const handleChange = () => {
+ const geoJSON = new L.GeoJSON(map)
geoJSON.eachLayer(layer => featureGroup.addLayer(layer))
// @ts-ignore
- props.callback(featureGroup.toGeoJSON())
- setOpen(false)
- }, [featureGroup, props])
+ onChange(featureGroup.toGeoJSON())
+ onClose()
+ }
return (
-
- Add Map
-
+
-
-
-
-
-
-
-
-
+
+
+
+ Save
+
+
+ Close
+
+
+
)
}
diff --git a/src/components/message/UploadButton.tsx b/src/components/message/UploadButton.tsx
new file mode 100644
index 00000000..7d996f0d
--- /dev/null
+++ b/src/components/message/UploadButton.tsx
@@ -0,0 +1,54 @@
+import React, { createRef, FC, useCallback } from 'react'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { IconButton } from '@mui/material'
+import { Ticker } from '../../api/Ticker'
+import { useUploadApi, Upload } from '../../api/Upload'
+import useAuth from '../useAuth'
+import { faImages } from '@fortawesome/free-solid-svg-icons'
+import palette from '../../theme/palette'
+
+interface Props {
+ ticker: Ticker
+ onUpload: (uploads: Upload[]) => void
+}
+
+const UploadButton: FC = ({ onUpload, ticker }) => {
+ const ref = createRef()
+ const { token } = useAuth()
+ const { postUpload } = useUploadApi(token)
+
+ const refClick = useCallback(() => {
+ ref.current?.click()
+ }, [ref])
+
+ const handleUpload = (e: React.FormEvent) => {
+ e.preventDefault()
+ // @ts-ignore
+ const files = e.target.files as Array
+ const formData = new FormData()
+ for (let i = 0; i < files.length; i++) {
+ // @ts-ignore
+ formData.append('files', files[i])
+ }
+ formData.append('ticker', ticker.id.toString())
+
+ postUpload(formData).then(response => {
+ onUpload(response.data.uploads)
+ })
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default UploadButton
diff --git a/src/components/ticker/MastodonCard.tsx b/src/components/ticker/MastodonCard.tsx
index 81245236..a6348606 100644
--- a/src/components/ticker/MastodonCard.tsx
+++ b/src/components/ticker/MastodonCard.tsx
@@ -1,11 +1,19 @@
+import React, { FC, useCallback } from 'react'
import { faMastodon } from '@fortawesome/free-brands-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useQueryClient } from '@tanstack/react-query'
-import React, { FC, useCallback } from 'react'
-import { Button, Card, Container, Icon, Image } from 'semantic-ui-react'
import { Ticker, useTickerApi } from '../../api/Ticker'
import useAuth from '../useAuth'
import MastodonModalForm from './MastodonModalForm'
+import {
+ Box,
+ Button,
+ Card,
+ CardActions,
+ CardContent,
+ Stack,
+ Typography,
+} from '@mui/material'
interface Props {
ticker: Ticker
@@ -41,75 +49,46 @@ const MastodonCard: FC = ({ ticker }) => {
)
return mastodon.connected ? (
-
-
-
- {mastodon.image_url != '' && (
-
- )}
-
-
- {mastodon.screen_name}
-
- {profileLink}
-
-
-
- {mastodon.active ? (
-
- ) : (
-
- )}
-
-
-
-
-
+
+
+
+ Mastodon
+
+ }
+ variant="outlined"
+ >
+ Configure
+
+
+
) : (
-
-
-
+
+
+
+
+ Mastodon
+
+ }
+ variant="outlined"
+ >
+ Configure
+
+
+
You are currently not connected to Mastodon. New messages will not be
published to your account and old messages can not be deleted anymore.
-
-
-
-
-
- }
- size="tiny"
- />
- }
- />
-
-
-
+
+
+
+
)
}
diff --git a/src/components/ticker/SocialConnectionChip.tsx b/src/components/ticker/SocialConnectionChip.tsx
new file mode 100644
index 00000000..57263e23
--- /dev/null
+++ b/src/components/ticker/SocialConnectionChip.tsx
@@ -0,0 +1,25 @@
+import React, { FC } from 'react'
+import { faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { Chip } from '@mui/material'
+
+interface Props {
+ active: boolean
+ label: string
+}
+
+const SocialConnectionChip: FC = ({ active, label }) => {
+ return (
+ <>
+ }
+ label={label}
+ size="small"
+ sx={{ mr: 1 }}
+ variant="outlined"
+ />
+ >
+ )
+}
+
+export default SocialConnectionChip
diff --git a/src/components/ticker/Ticker.tsx b/src/components/ticker/Ticker.tsx
index 2e790d73..4a86ef1a 100644
--- a/src/components/ticker/Ticker.tsx
+++ b/src/components/ticker/Ticker.tsx
@@ -1,92 +1,82 @@
-import React, { FC } from 'react'
-import { Button, Grid, Header } from 'semantic-ui-react'
+import React, { FC, useState } from 'react'
import { Ticker as Model } from '../../api/Ticker'
import MessageForm from '../message/MessageForm'
import TickerCard from './TickerCard'
import MessageList from '../message/MessageList'
-import useAuth from '../useAuth'
-import TickerUsersCard from './TickerUserCard'
-import TickerResetModal from './TickerResetModal'
-import TwitterCard from './TwitterCard'
-import TelegramCard from './TelegramCard'
-import useFeature from '../useFeature'
-import MastodonCard from './MastodonCard'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faGear } from '@fortawesome/free-solid-svg-icons'
import {
- faMastodon,
- faTelegram,
- faTwitter,
-} from '@fortawesome/free-brands-svg-icons'
-import { faGear, faRadiation, faUsers } from '@fortawesome/free-solid-svg-icons'
+ Button,
+ Card,
+ CardContent,
+ Grid,
+ Stack,
+ Typography,
+} from '@mui/material'
+import TickerModalForm from './TickerModalForm'
interface Props {
ticker: Model
}
-const Ticker: FC = props => {
- const { user } = useAuth()
- const ticker = props.ticker
- const { telegram_enabled, twitter_enabled } = useFeature()
+const Ticker: FC = ({ ticker }) => {
+ const [formModalOpen, setFormModalOpen] = useState(false)
return (
-
-
-
-
-
-
-
-
-
-
-
-
- {twitter_enabled && (
- <>
-
-
- >
- )}
-
- {telegram_enabled && (
- <>
-
-
- >
- )}
- {user?.roles.includes('admin') && (
-
-
-
-
-
- }
- />
-
- )}
-
-
+
+
+
+
+ Ticker
+
+ {
+ setFormModalOpen(true)
+ }}
+ startIcon={}
+ variant="contained"
+ >
+ Configure
+
+ {
+ setFormModalOpen(false)
+ }}
+ open={formModalOpen}
+ ticker={ticker}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/components/ticker/TickerCard.tsx b/src/components/ticker/TickerCard.tsx
index 74d74bbf..c948e93d 100644
--- a/src/components/ticker/TickerCard.tsx
+++ b/src/components/ticker/TickerCard.tsx
@@ -1,44 +1,77 @@
import React, { FC } from 'react'
-import { Button, Card, Icon, Label } from 'semantic-ui-react'
-import ReactMarkdown from 'react-markdown'
+import { Box, Card, CardContent, Typography } from '@mui/material'
import { Ticker } from '../../api/Ticker'
+import NamedListItem from '../common/NamedListItem'
+import SocialConnectionChip from './SocialConnectionChip'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import {
+ faCheck,
+ faHeading,
+ faLink,
+ faXmark,
+} from '@fortawesome/free-solid-svg-icons'
interface Props {
ticker: Ticker
}
-const TickerCard: FC = props => {
+const TickerCard: FC = ({ ticker }) => {
return (
-
-
-
-
- {props.ticker.title}
-
-
-
-
- {props.ticker.domain}
-
-
-
- {props.ticker.description}
-
-
-
-
-
+
+
+
+
+
+ {ticker.title}
+
+
+
+
+
+ {ticker.active ? 'Active' : 'Inactive'}
+
+
+
+
+
+
+ {ticker.domain}
+
+
+
+
+
+
+
+
+
+
+
)
}
From 7fe86bcbc80ad64fa197b68850e841b9d76fbc9c Mon Sep 17 00:00:00 2001
From: louis
Date: Tue, 17 Jan 2023 20:48:20 -0800
Subject: [PATCH 15/31] =?UTF-8?q?=F0=9F=92=84=20Use=20MUI=20for=20MessageL?=
=?UTF-8?q?ist?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/message/Message.tsx | 151 +++---------------
src/components/message/MessageAttachments.tsx | 63 ++++++++
src/components/message/MessageFooter.tsx | 46 ++++++
src/components/message/MessageList.tsx | 22 +--
src/components/message/MessageModalDelete.tsx | 80 ++++++----
src/components/ticker/Ticker.tsx | 6 +-
6 files changed, 192 insertions(+), 176 deletions(-)
create mode 100644 src/components/message/MessageAttachments.tsx
create mode 100644 src/components/message/MessageFooter.tsx
diff --git a/src/components/message/Message.tsx b/src/components/message/Message.tsx
index e841dc19..7e9a5493 100644
--- a/src/components/message/Message.tsx
+++ b/src/components/message/Message.tsx
@@ -1,19 +1,13 @@
-import React, { FC, useCallback, useState } from 'react'
-import { Card, Icon, Image } from 'semantic-ui-react'
-import Moment from 'react-moment'
+import React, { FC, useState } from 'react'
import { Message as MessageType } from '../../api/Message'
-import Lightbox from 'react-image-lightbox'
-import 'react-image-lightbox/style.css'
import { replaceMagic } from '../../lib/replaceLinksHelper'
import MessageModalDelete from './MessageModalDelete'
import MessageMap from './MessageMap'
import { Ticker } from '../../api/Ticker'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import {
- faMastodon,
- faTelegram,
- faTwitter,
-} from '@fortawesome/free-brands-svg-icons'
+import { Card, CardContent, IconButton, useTheme } from '@mui/material'
+import MessageAttachements from './MessageAttachments'
+import MessageFooter from './MessageFooter'
+import { Close } from '@mui/icons-material'
interface Props {
message: MessageType
@@ -21,133 +15,34 @@ interface Props {
}
const Message: FC = ({ message, ticker }) => {
- const [imageLightboxOpen, setImageLightboxOpen] = useState(false)
- const [imageIndex, setImageIndex] = useState(0)
-
- const openImageLightbox = useCallback(() => setImageLightboxOpen(true), [])
- const closeImageLightbox = useCallback(() => setImageLightboxOpen(false), [])
-
- const twitterIcon = useCallback(() => {
- if (message.twitter_url) {
- return (
-
-
-
-
-
- )
- }
-
- return null
- }, [message.twitter_url])
-
- const telegramIcon = useCallback(() => {
- if (message.telegram_url) {
- return (
-
-
-
-
-
- )
- }
-
- return null
- }, [message.telegram_url])
-
- const mastodonIcon = useCallback(() => {
- if (message.mastodon_url) {
- return (
-
-
-
-
-
- )
- }
-
- return null
- }, [message.mastodon_url])
-
- const renderAttachments = () => {
- const attachments = message.attachments
-
- if (attachments === null || attachments.length === 0) {
- return null
- }
-
- const images = attachments.map((image, key) => (
- {
- openImageLightbox()
- setImageIndex(key)
- }}
- rounded
- src={image.url}
- style={{ width: 200, height: 200, objectFit: 'cover' }}
- />
- ))
- const urls = attachments.map(image => image.url)
-
- return (
-
- {imageLightboxOpen && (
-
- setImageIndex((imageIndex + 1) % urls.length)
- }
- onMovePrevRequest={() =>
- setImageIndex((imageIndex + urls.length - 1) % urls.length)
- }
- prevSrc={urls[(imageIndex + urls.length - 1) % urls.length]}
- />
- )}
- {images}
-
- )
- }
+ const theme = useTheme()
+ const [deleteModalOpen, setDeleteModalOpen] = useState(false)
return (
-
-
+
+
+ {
+ setDeleteModalOpen(true)
+ }}
+ sx={{ position: 'absolute', right: theme.spacing(3) }}
+ >
+
+
- }
+ onClose={() => setDeleteModalOpen(false)}
+ open={deleteModalOpen}
/>
-
- {renderAttachments()}
-
-
- {twitterIcon()}
- {telegramIcon()}
- {mastodonIcon()}
- {message.creation_date}
-
+
+
+
+
)
}
diff --git a/src/components/message/MessageAttachments.tsx b/src/components/message/MessageAttachments.tsx
new file mode 100644
index 00000000..d788440b
--- /dev/null
+++ b/src/components/message/MessageAttachments.tsx
@@ -0,0 +1,63 @@
+import { ImageList, ImageListItem } from '@mui/material'
+import React, { FC, useCallback, useState } from 'react'
+import Lightbox from 'react-image-lightbox'
+import { Message } from '../../api/Message'
+import 'react-image-lightbox/style.css'
+
+interface Props {
+ message: Message
+}
+
+const MessageAttachements: FC = ({ message }) => {
+ const [imageLightboxOpen, setImageLightboxOpen] = useState(false)
+ const [imageIndex, setImageIndex] = useState(0)
+ const attachments = message.attachments
+
+ const openImageLightbox = useCallback(() => setImageLightboxOpen(true), [])
+ const closeImageLightbox = useCallback(() => setImageLightboxOpen(false), [])
+
+ if (attachments === null || attachments.length === 0) {
+ return null
+ }
+
+ const images = attachments.map((image, key) => (
+ {
+ openImageLightbox()
+ setImageIndex(key)
+ }}
+ sx={{ position: 'relative' }}
+ >
+
+
+ ))
+ const urls = attachments.map(image => image.url)
+
+ return (
+ <>
+ {imageLightboxOpen && (
+
+ setImageIndex((imageIndex + 1) % urls.length)
+ }
+ onMovePrevRequest={() =>
+ setImageIndex((imageIndex + urls.length - 1) % urls.length)
+ }
+ prevSrc={urls[(imageIndex + urls.length - 1) % urls.length]}
+ />
+ )}
+ {images}
+ >
+ )
+}
+
+export default MessageAttachements
diff --git a/src/components/message/MessageFooter.tsx b/src/components/message/MessageFooter.tsx
new file mode 100644
index 00000000..73b8f0fa
--- /dev/null
+++ b/src/components/message/MessageFooter.tsx
@@ -0,0 +1,46 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core'
+import {
+ faMastodon,
+ faTelegram,
+ faTwitter,
+} from '@fortawesome/free-brands-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { Box, Stack, Typography } from '@mui/material'
+import React, { FC } from 'react'
+import Moment from 'react-moment'
+import { Message } from '../../api/Message'
+
+interface Props {
+ message: Message
+}
+const MessageFooter: FC = ({ message }) => {
+ return (
+
+
+
+ {message.creation_date}
+
+
+
+
+
+
+
+
+ )
+}
+
+interface IconProps {
+ url?: string
+ icon: IconProp
+}
+
+const Icon: FC = ({ url, icon }) => {
+ return url ? (
+
+
+
+ ) : null
+}
+
+export default MessageFooter
diff --git a/src/components/message/MessageList.tsx b/src/components/message/MessageList.tsx
index bb257861..159c9ea8 100644
--- a/src/components/message/MessageList.tsx
+++ b/src/components/message/MessageList.tsx
@@ -1,10 +1,11 @@
import React, { FC } from 'react'
-import { Dimmer, Feed, Loader } from 'semantic-ui-react'
+import { useQuery } from '@tanstack/react-query'
import { Ticker } from '../../api/Ticker'
import { useMessageApi } from '../../api/Message'
import Message from './Message'
-import { useQuery } from '@tanstack/react-query'
import useAuth from '../useAuth'
+import ErrorView from '../../views/ErrorView'
+import Loader from '../Loader'
interface Props {
ticker: Ticker
@@ -18,24 +19,23 @@ const MessageList: FC = ({ ticker }) => {
)
if (isLoading) {
- return (
-
- Loading
-
- )
+ return
}
if (error || data === undefined) {
- //TODO: Generic Error View
- return Error occured
+ return (
+
+ Unable to fetch messages from server.
+
+ )
}
return (
-
+ <>
{data.data.messages.map(message => (
))}
-
+ >
)
}
diff --git a/src/components/message/MessageModalDelete.tsx b/src/components/message/MessageModalDelete.tsx
index 8e9e2539..48861231 100644
--- a/src/components/message/MessageModalDelete.tsx
+++ b/src/components/message/MessageModalDelete.tsx
@@ -1,49 +1,65 @@
-import React, { FC, useCallback, useState } from 'react'
+import React, { FC, useCallback } from 'react'
import { useQueryClient } from '@tanstack/react-query'
-import { Confirm } from 'semantic-ui-react'
import { Message, useMessageApi } from '../../api/Message'
import useAuth from '../useAuth'
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Stack,
+} from '@mui/material'
+import { Close } from '@mui/icons-material'
interface Props {
+ onClose: () => void
+ open: boolean
message: Message
- trigger: React.ReactNode
}
-
-const MessageModalDelete: FC = props => {
+const MessageModalDelete: FC = ({ message, onClose, open }) => {
const { token } = useAuth()
const { deleteMessage } = useMessageApi(token)
- const [open, setOpen] = useState(false)
const queryClient = useQueryClient()
- const message = props.message
-
- const handleCancel = useCallback(() => {
- setOpen(false)
- }, [])
- const handleConfirm = useCallback(() => {
- deleteMessage(message)
- .then(() => {
- queryClient.invalidateQueries(['messages', message.ticker])
- })
- .finally(() => {
- setOpen(false)
- })
- }, [deleteMessage, message, queryClient])
+ const handleClose = () => {
+ onClose()
+ }
- const handleOpen = useCallback(() => {
- setOpen(true)
- }, [])
+ const handleDelete = useCallback(() => {
+ deleteMessage(message).then(() => {
+ queryClient.invalidateQueries(['messages', message.ticker])
+ onClose()
+ })
+ }, [deleteMessage, message, onClose, queryClient])
return (
-
+
)
}
diff --git a/src/components/ticker/Ticker.tsx b/src/components/ticker/Ticker.tsx
index 4a86ef1a..506830d2 100644
--- a/src/components/ticker/Ticker.tsx
+++ b/src/components/ticker/Ticker.tsx
@@ -70,11 +70,7 @@ const Ticker: FC = ({ ticker }) => {
-
-
-
-
-
+
From 1ce724349c9341b7719da54e6fbae696691ed3e2 Mon Sep 17 00:00:00 2001
From: louis
Date: Wed, 18 Jan 2023 09:46:10 -0800
Subject: [PATCH 16/31] =?UTF-8?q?=F0=9F=92=84=20Use=20MUI=20for=20Ticker?=
=?UTF-8?q?=20DangerZone?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/ticker/Ticker.tsx | 5 ++
.../ticker/TickerDangerZoneCard.tsx | 42 +++++++++
src/components/ticker/TickerResetModal.tsx | 86 ++++++++++---------
3 files changed, 93 insertions(+), 40 deletions(-)
create mode 100644 src/components/ticker/TickerDangerZoneCard.tsx
diff --git a/src/components/ticker/Ticker.tsx b/src/components/ticker/Ticker.tsx
index 506830d2..a63bb83c 100644
--- a/src/components/ticker/Ticker.tsx
+++ b/src/components/ticker/Ticker.tsx
@@ -6,6 +6,7 @@ import MessageList from '../message/MessageList'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faGear } from '@fortawesome/free-solid-svg-icons'
import {
+ Box,
Button,
Card,
CardContent,
@@ -14,6 +15,7 @@ import {
Typography,
} from '@mui/material'
import TickerModalForm from './TickerModalForm'
+import TickerDangerZoneCard from './TickerDangerZoneCard'
interface Props {
ticker: Model
@@ -60,6 +62,9 @@ const Ticker: FC = ({ ticker }) => {
xs={12}
>
+
+
+
diff --git a/src/components/ticker/TickerDangerZoneCard.tsx b/src/components/ticker/TickerDangerZoneCard.tsx
new file mode 100644
index 00000000..ea3a4efb
--- /dev/null
+++ b/src/components/ticker/TickerDangerZoneCard.tsx
@@ -0,0 +1,42 @@
+import React, { FC, useState } from 'react'
+import { Box, Button, Card, CardContent, Typography } from '@mui/material'
+import { Ticker } from '../../api/Ticker'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faBiohazard, faTrash } from '@fortawesome/free-solid-svg-icons'
+import TickerResetModal from './TickerResetModal'
+import useAuth from '../useAuth'
+
+interface Props {
+ ticker: Ticker
+}
+const TickerDangerZoneCard: FC = ({ ticker }) => {
+ const { user } = useAuth()
+ const [resetOpen, setResetOpen] = useState(false)
+
+ return user?.roles.includes('admin') ? (
+
+
+
+ Danger Zone
+
+
+ setResetOpen(true)}
+ startIcon={}
+ variant="outlined"
+ >
+ Reset Ticker
+
+ setResetOpen(false)}
+ open={resetOpen}
+ ticker={ticker}
+ />
+
+
+
+ ) : null
+}
+
+export default TickerDangerZoneCard
diff --git a/src/components/ticker/TickerResetModal.tsx b/src/components/ticker/TickerResetModal.tsx
index bbead9a3..59026ecc 100644
--- a/src/components/ticker/TickerResetModal.tsx
+++ b/src/components/ticker/TickerResetModal.tsx
@@ -1,50 +1,60 @@
-import React, { FC, useCallback, useState } from 'react'
-import { Button, Modal } from 'semantic-ui-react'
+import React, { FC, useCallback } from 'react'
import { Ticker, useTickerApi } from '../../api/Ticker'
import useAuth from '../useAuth'
import { useQueryClient } from '@tanstack/react-query'
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Stack,
+} from '@mui/material'
+import { Close } from '@mui/icons-material'
interface Props {
+ onClose: () => void
+ open: boolean
ticker: Ticker
- trigger: React.ReactNode
}
-const TickerResetModal: FC = props => {
- const [open, setOpen] = useState(false)
+const TickerResetModal: FC = ({ onClose, open, ticker }) => {
const { token } = useAuth()
const { putTickerReset } = useTickerApi(token)
const queryClient = useQueryClient()
- const handleCancel = useCallback(() => {
- setOpen(false)
- }, [])
-
- const handleOpen = useCallback(() => {
- setOpen(true)
- }, [])
+ const handleClose = () => {
+ onClose()
+ }
const handleReset = useCallback(() => {
- putTickerReset(props.ticker)
+ putTickerReset(ticker)
.then(() => {
- queryClient.invalidateQueries(['messages', props.ticker.id])
- queryClient.invalidateQueries(['tickerUsers', props.ticker.id])
- queryClient.invalidateQueries(['ticker', props.ticker.id])
+ queryClient.invalidateQueries(['messages', ticker.id])
+ queryClient.invalidateQueries(['tickerUsers', ticker.id])
+ queryClient.invalidateQueries(['ticker', ticker.id])
})
.finally(() => {
- setOpen(false)
+ onClose()
})
- }, [props.ticker, putTickerReset, queryClient])
+ }, [onClose, putTickerReset, queryClient, ticker])
return (
-
- Reset Ticker
-
+
-
-
-
- No
+
+
+
+ Reset
+
+
+ Close
-
-
-
+
+
)
}
From 522a13c17057b97469400e24b20ad35a9524a2cb Mon Sep 17 00:00:00 2001
From: louis
Date: Wed, 18 Jan 2023 11:30:16 -0800
Subject: [PATCH 17/31] =?UTF-8?q?=F0=9F=92=84=20Small=20UI=20Improvements?=
=?UTF-8?q?=20for=20Messages?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/message/Message.tsx | 1 +
src/components/message/MessageMap.tsx | 7 ++++++-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/components/message/Message.tsx b/src/components/message/Message.tsx
index 7e9a5493..2508843b 100644
--- a/src/components/message/Message.tsx
+++ b/src/components/message/Message.tsx
@@ -38,6 +38,7 @@ const Message: FC = ({ message, ticker }) => {
dangerouslySetInnerHTML={{
__html: replaceMagic(message.text),
}}
+ style={{ paddingRight: theme.spacing(6) }}
/>
diff --git a/src/components/message/MessageMap.tsx b/src/components/message/MessageMap.tsx
index 85e5d46b..46dff56d 100644
--- a/src/components/message/MessageMap.tsx
+++ b/src/components/message/MessageMap.tsx
@@ -28,7 +28,12 @@ const MessageMap: FC = ({ message, ticker }) => {
}
return (
-
+
Date: Fri, 20 Jan 2023 15:05:21 -0800
Subject: [PATCH 18/31] =?UTF-8?q?=F0=9F=92=84=20Use=20MUI=20for=20Social?=
=?UTF-8?q?=20Connections?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/common/TabPanel.tsx | 23 +++
src/components/ticker/MastodonCard.tsx | 79 ++++++---
src/components/ticker/MastodonForm.tsx | 149 ++++++++--------
src/components/ticker/MastodonModalForm.tsx | 87 +++++-----
src/components/ticker/TelegramCard.tsx | 142 ++++++++-------
src/components/ticker/TelegramForm.tsx | 115 ++++++-------
src/components/ticker/TelegramModalForm.tsx | 87 +++++-----
src/components/ticker/TickerForm.tsx | 19 +++
src/components/ticker/TickerModalForm.tsx | 42 +++--
.../ticker/TickerSocialConnections.tsx | 28 +++
src/components/ticker/TwitterCard.tsx | 161 +++++++++---------
11 files changed, 524 insertions(+), 408 deletions(-)
create mode 100644 src/components/common/TabPanel.tsx
create mode 100644 src/components/ticker/TickerSocialConnections.tsx
diff --git a/src/components/common/TabPanel.tsx b/src/components/common/TabPanel.tsx
new file mode 100644
index 00000000..e1526332
--- /dev/null
+++ b/src/components/common/TabPanel.tsx
@@ -0,0 +1,23 @@
+import { Box } from '@mui/material'
+import React, { FC } from 'react'
+
+interface Props {
+ children?: React.ReactNode
+ index: number
+ value: number
+}
+
+const TabPanel: FC = ({ children, index, value }) => {
+ return (
+
+ {value === index && {children}}
+
+ )
+}
+
+export default TabPanel
diff --git a/src/components/ticker/MastodonCard.tsx b/src/components/ticker/MastodonCard.tsx
index a6348606..bbcfbb24 100644
--- a/src/components/ticker/MastodonCard.tsx
+++ b/src/components/ticker/MastodonCard.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useCallback } from 'react'
+import React, { FC, useCallback, useState } from 'react'
import { faMastodon } from '@fortawesome/free-brands-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useQueryClient } from '@tanstack/react-query'
@@ -14,6 +14,7 @@ import {
Stack,
Typography,
} from '@mui/material'
+import { faBan, faPause, faPlay } from '@fortawesome/free-solid-svg-icons'
interface Props {
ticker: Ticker
@@ -22,6 +23,8 @@ interface Props {
const MastodonCard: FC = ({ ticker }) => {
const { token } = useAuth()
const { deleteTickerMastodon, putTickerMastodon } = useTickerApi(token)
+ const [open, setOpen] = useState(false)
+
const queryClient = useQueryClient()
const mastodon = ticker.mastodon
@@ -48,22 +51,7 @@ const MastodonCard: FC = ({ ticker }) => {
)
- return mastodon.connected ? (
-
-
-
- Mastodon
-
- }
- variant="outlined"
- >
- Configure
-
-
-
- ) : (
+ return (
= ({ ticker }) => {
justifyContent="space-between"
>
- Mastodon
+ Mastodon
- }
- variant="outlined"
- >
+ setOpen(true)} size="small" variant="outlined">
Configure
-
- You are currently not connected to Mastodon. New messages will not be
- published to your account and old messages can not be deleted anymore.
-
+ {mastodon.connected ? (
+
+
+ You are connected to Mastodon.
+
+ Profile: {profileLink}
+
+ ) : (
+
+ You are currently not connected to Mastodon. New messages will not
+ be published to your account and old messages can not be deleted
+ anymore.
+
+ )}
-
+ {mastodon.connected ? (
+
+ {mastodon.active ? (
+ }
+ >
+ Disable
+
+ ) : (
+ }
+ >
+ Enable
+
+ )}
+ }
+ >
+ Disconnect
+
+
+ ) : null}
+ setOpen(false)}
+ open={open}
+ ticker={ticker}
+ />
)
}
diff --git a/src/components/ticker/MastodonForm.tsx b/src/components/ticker/MastodonForm.tsx
index f582cddb..afcc3210 100644
--- a/src/components/ticker/MastodonForm.tsx
+++ b/src/components/ticker/MastodonForm.tsx
@@ -1,12 +1,14 @@
+import {
+ Checkbox,
+ FormControlLabel,
+ FormGroup,
+ Grid,
+ TextField,
+ Typography,
+} from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
-import React, { ChangeEvent, FC, FormEvent, useCallback } from 'react'
+import React, { FC } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
-import {
- CheckboxProps,
- Form,
- InputOnChangeData,
- Message,
-} from 'semantic-ui-react'
import { Ticker, TickerMastodonFormData, useTickerApi } from '../../api/Ticker'
import useAuth from '../useAuth'
@@ -19,7 +21,7 @@ const MastodonForm: FC = ({ callback, ticker }) => {
const mastodon = ticker.mastodon
const { token } = useAuth()
const { putTickerMastodon } = useTickerApi(token)
- const { handleSubmit, setValue } = useForm({
+ const { handleSubmit, register } = useForm({
defaultValues: {
active: mastodon.active,
server: mastodon.server,
@@ -27,20 +29,6 @@ const MastodonForm: FC = ({ callback, ticker }) => {
})
const queryClient = useQueryClient()
- const onChange = useCallback(
- (
- e: ChangeEvent | FormEvent,
- { name, value, checked }: InputOnChangeData | CheckboxProps
- ) => {
- if (checked !== undefined) {
- setValue(name, checked)
- } else {
- setValue(name, value)
- }
- },
- [setValue]
- )
-
const onSubmit: SubmitHandler = data => {
putTickerMastodon(data, ticker).finally(() => {
queryClient.invalidateQueries(['ticker', ticker.id])
@@ -49,56 +37,73 @@ const MastodonForm: FC = ({ callback, ticker }) => {
}
return (
-
-
-
-
-
-
+
)
}
diff --git a/src/components/ticker/MastodonModalForm.tsx b/src/components/ticker/MastodonModalForm.tsx
index 9b3ed3fc..1a7c27c8 100644
--- a/src/components/ticker/MastodonModalForm.tsx
+++ b/src/components/ticker/MastodonModalForm.tsx
@@ -1,54 +1,59 @@
-import React, { FC, useCallback, useState } from 'react'
-import { Button, Modal } from 'semantic-ui-react'
+import React, { FC } from 'react'
+import { Close } from '@mui/icons-material'
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Stack,
+} from '@mui/material'
import { Ticker } from '../../api/Ticker'
import MastodonForm from './MastodonForm'
interface Props {
+ onClose: () => void
+ open: boolean
ticker: Ticker
- trigger: React.ReactNode
}
-const MastodonModalForm: FC = ({ ticker, trigger }) => {
- const [open, setOpen] = useState(false)
-
- const handleClose = useCallback(() => {
- setOpen(false)
- }, [])
-
- const handleOpen = useCallback(() => {
- setOpen(true)
- }, [])
+const MastodonModalForm: FC = ({ onClose, open, ticker }) => {
+ const handleClose = () => {
+ onClose()
+ }
return (
-
- Configure Mastodon
-
+
-
-
-
-
-
-
-
-
+
+
+
+ Save
+
+
+ Close
+
+
+
)
}
diff --git a/src/components/ticker/TelegramCard.tsx b/src/components/ticker/TelegramCard.tsx
index f82f21c5..b023e5f4 100644
--- a/src/components/ticker/TelegramCard.tsx
+++ b/src/components/ticker/TelegramCard.tsx
@@ -1,8 +1,17 @@
import { faTelegram } from '@fortawesome/free-brands-svg-icons'
+import { faBan, faPause, faPlay } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import {
+ Box,
+ Button,
+ Card,
+ CardActions,
+ CardContent,
+ Stack,
+ Typography,
+} from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
-import React, { FC, useCallback } from 'react'
-import { Button, Card, Container, Icon } from 'semantic-ui-react'
+import React, { FC, useCallback, useState } from 'react'
import { Ticker, useTickerApi } from '../../api/Ticker'
import useAuth from '../useAuth'
import TelegramModalForm from './TelegramModalForm'
@@ -14,6 +23,7 @@ interface Props {
const TelegramCard: FC = ({ ticker }) => {
const { token } = useAuth()
const { deleteTickerTelegram, putTickerTelegram } = useTickerApi(token)
+ const [open, setOpen] = useState(false)
const queryClient = useQueryClient()
const telegram = ticker.telegram
@@ -30,70 +40,72 @@ const TelegramCard: FC = ({ ticker }) => {
})
}, [deleteTickerTelegram, queryClient, ticker])
- return telegram.connected ? (
-
-
-
-
-
- {telegram.channel_name}
-
- Bot: {telegram.bot_username}
-
-
-
- {telegram.active ? (
-
- ) : (
-
- )}
+ return (
+
+
+
+
+ Telegram
+
+ setOpen(true)} size="small" variant="outlined">
+ Configure
+
+
+ {telegram.connected ? (
+
+
+ You are connected to Telegram.
+
+
+ Your Channel: {telegram.channel_name}
+
+
+ Bot: {telegram.bot_username}
+
+
+ ) : (
+ <>
+ You are currently not connected to Telegram. New messages will not
+ be published to your channel and old messages can not be deleted
+ anymore.
+ >
+ )}
+
+ {telegram.connected ? (
+
+ {telegram.active ? (
-
-
-
-
- ) : (
-
-
-
- You are currently not connected to Telegram. New messages will not be
- published to your channel and old messages can not be deleted anymore.
-
-
-
-
-
- }
- size="tiny"
- />
- }
- />
-
-
-
+ onClick={handleToggle}
+ startIcon={}
+ >
+ Disable
+
+ ) : (
+ }
+ >
+ Enable
+
+ )}
+ }
+ >
+ Disconnect
+
+
+ ) : null}
+ setOpen(false)}
+ open={open}
+ ticker={ticker}
+ />
+
)
}
diff --git a/src/components/ticker/TelegramForm.tsx b/src/components/ticker/TelegramForm.tsx
index fcea7fed..d4999de1 100644
--- a/src/components/ticker/TelegramForm.tsx
+++ b/src/components/ticker/TelegramForm.tsx
@@ -1,20 +1,16 @@
-import React, {
- ChangeEvent,
- FC,
- FormEvent,
- useCallback,
- useEffect,
-} from 'react'
+import React, { FC } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { useQueryClient } from '@tanstack/react-query'
-import {
- CheckboxProps,
- Form,
- InputOnChangeData,
- Message,
-} from 'semantic-ui-react'
import { Ticker, useTickerApi } from '../../api/Ticker'
import useAuth from '../useAuth'
+import {
+ Checkbox,
+ FormControlLabel,
+ FormGroup,
+ Grid,
+ TextField,
+ Typography,
+} from '@mui/material'
interface Props {
callback: () => void
@@ -30,12 +26,7 @@ const TelegramForm: FC = ({ callback, ticker }) => {
const telegram = ticker.telegram
const { token } = useAuth()
const { putTickerTelegram } = useTickerApi(token)
- const {
- formState: { errors },
- handleSubmit,
- register,
- setValue,
- } = useForm({
+ const { handleSubmit, register } = useForm({
defaultValues: {
active: telegram.active,
channel_name: telegram.channel_name,
@@ -43,26 +34,6 @@ const TelegramForm: FC = ({ callback, ticker }) => {
})
const queryClient = useQueryClient()
- useEffect(() => {
- register('channel_name', {
- pattern: { value: /@\w+/i, message: 'The Channel must start with an @' },
- })
- }, [register])
-
- const onChange = useCallback(
- (
- e: ChangeEvent | FormEvent,
- { name, value, checked }: InputOnChangeData | CheckboxProps
- ) => {
- if (checked !== undefined) {
- setValue(name, checked)
- } else {
- setValue(name, value)
- }
- },
- [setValue]
- )
-
const onSubmit: SubmitHandler = data => {
putTickerTelegram(data, ticker).finally(() => {
queryClient.invalidateQueries(['ticker', ticker.id])
@@ -71,34 +42,44 @@ const TelegramForm: FC = ({ callback, ticker }) => {
}
return (
-
-
-
+
)
}
diff --git a/src/components/ticker/TelegramModalForm.tsx b/src/components/ticker/TelegramModalForm.tsx
index 1f8d30cc..a84043b3 100644
--- a/src/components/ticker/TelegramModalForm.tsx
+++ b/src/components/ticker/TelegramModalForm.tsx
@@ -1,54 +1,59 @@
-import React, { FC, useCallback, useState } from 'react'
-import { Button, Modal } from 'semantic-ui-react'
+import React, { FC } from 'react'
+import { Close } from '@mui/icons-material'
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Stack,
+} from '@mui/material'
import { Ticker } from '../../api/Ticker'
import TelegramForm from './TelegramForm'
interface Props {
+ onClose: () => void
+ open: boolean
ticker: Ticker
- trigger: React.ReactNode
}
-const TelegramModalForm: FC = ({ ticker, trigger }) => {
- const [open, setOpen] = useState(false)
-
- const handleClose = useCallback(() => {
- setOpen(false)
- }, [])
-
- const handleOpen = useCallback(() => {
- setOpen(true)
- }, [])
+const TelegramModalForm: FC = ({ onClose, open, ticker }) => {
+ const handleClose = () => {
+ onClose()
+ }
return (
-
- Configure Telegram
-
+
-
-
-
-
-
-
-
-
+
+
+
+ Save
+
+
+ Close
+
+
+
)
}
diff --git a/src/components/ticker/TickerForm.tsx b/src/components/ticker/TickerForm.tsx
index d58fef90..455a57ba 100644
--- a/src/components/ticker/TickerForm.tsx
+++ b/src/components/ticker/TickerForm.tsx
@@ -8,6 +8,7 @@ import { MapContainer, Marker, TileLayer } from 'react-leaflet'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
Alert,
+ Box,
Button,
Checkbox,
FormControlLabel,
@@ -17,6 +18,7 @@ import {
Stack,
TextField,
Typography,
+ useTheme,
} from '@mui/material'
import {
faComputerMouse,
@@ -78,6 +80,7 @@ const TickerForm: FC = ({ callback, id, ticker }) => {
const { token } = useAuth()
const { postTicker, putTicker } = useTickerApi(token)
const queryClient = useQueryClient()
+ const theme = useTheme()
const onLocationChange = useCallback(
(result: Result) => {
@@ -307,6 +310,22 @@ const TickerForm: FC = ({ callback, id, ticker }) => {
) : null}
+
+
+
+ Save
+
+ callback()}>
+ Close
+
+
+
)
}
diff --git a/src/components/ticker/TickerModalForm.tsx b/src/components/ticker/TickerModalForm.tsx
index 0892fa18..3a795cf4 100644
--- a/src/components/ticker/TickerModalForm.tsx
+++ b/src/components/ticker/TickerModalForm.tsx
@@ -1,16 +1,18 @@
import { Close } from '@mui/icons-material'
import {
- Button,
Dialog,
- DialogActions,
DialogContent,
DialogTitle,
IconButton,
Stack,
+ Tab,
+ Tabs,
} from '@mui/material'
-import React, { FC } from 'react'
+import React, { FC, useState } from 'react'
import { Ticker } from '../../api/Ticker'
+import TabPanel from '../common/TabPanel'
import TickerForm from './TickerForm'
+import TickerSocialConnections from './TickerSocialConnections'
interface Props {
onClose: () => void
@@ -19,10 +21,16 @@ interface Props {
}
const TickerModalForm: FC = ({ onClose, open, ticker }) => {
+ const [tabValue, setTabValue] = useState(0)
+
const handleClose = () => {
onClose()
}
+ const handleTabChange = (e: React.SyntheticEvent, value: number) => {
+ setTabValue(value)
+ }
+
return (
)
}
diff --git a/src/components/ticker/TickerSocialConnections.tsx b/src/components/ticker/TickerSocialConnections.tsx
new file mode 100644
index 00000000..a32d38af
--- /dev/null
+++ b/src/components/ticker/TickerSocialConnections.tsx
@@ -0,0 +1,28 @@
+import { Grid } from '@mui/material'
+import React, { FC } from 'react'
+import { Ticker } from '../../api/Ticker'
+import MastodonCard from './MastodonCard'
+import TelegramCard from './TelegramCard'
+import TwitterCard from './TwitterCard'
+
+interface Props {
+ ticker: Ticker
+}
+
+const TickerSocialConnections: FC = ({ ticker }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default TickerSocialConnections
diff --git a/src/components/ticker/TwitterCard.tsx b/src/components/ticker/TwitterCard.tsx
index 0797e1f6..5cbfe590 100644
--- a/src/components/ticker/TwitterCard.tsx
+++ b/src/components/ticker/TwitterCard.tsx
@@ -1,12 +1,22 @@
import React, { FC, useCallback } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import TwitterLogin from 'react-twitter-auth'
-import { Button, Card, Container, Icon, Image } from 'semantic-ui-react'
import { ApiUrl } from '../../api/Api'
import { Ticker, useTickerApi } from '../../api/Ticker'
import useAuth from '../useAuth'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTwitter } from '@fortawesome/free-brands-svg-icons'
+import {
+ Box,
+ Button,
+ Card,
+ CardActions,
+ CardContent,
+ Icon,
+ Stack,
+ Typography,
+} from '@mui/material'
+import { faBan, faPause, faPlay } from '@fortawesome/free-solid-svg-icons'
interface Props {
ticker: Ticker
@@ -18,9 +28,9 @@ interface TwitterAuthResponseData {
}
const TwitterCard: FC = ({ ticker }) => {
- const queryClient = useQueryClient()
const { token } = useAuth()
const { deleteTickerTwitter, putTickerTwitter } = useTickerApi(token)
+ const queryClient = useQueryClient()
const twitter = ticker.twitter || {}
const requestTokenUrl = `${ApiUrl}/admin/auth/twitter/request_token?callback=${encodeURI(
@@ -62,83 +72,82 @@ const TwitterCard: FC = ({ ticker }) => {
alert(error)
}, [])
- return twitter.connected ? (
-
-
-
- {twitter.image_url != '' && (
-
- )}
-
-
- {twitter.name}
-
-
-
+
+
+
+ Twitter
+
+ {twitter.connected === false ? (
+ //TODO: Reimplement and remove the dependency
+
- @{twitter.screen_name}
-
-
- {twitter.description}
-
-
-
- {twitter.active ? (
-
- ) : (
-
- )}
+
+
+
+ Connect
+
+ ) : null}
+
+ {twitter.connected ? (
+
+
+
+ @{twitter.screen_name}
+
+
+ {twitter.description}
+
+ ) : (
+ <>
+ You are currently not connected to Twitter. New messages will not be
+ published to your account and old messages can not be deleted
+ anymore.
+ >
+ )}
+
+ {twitter.connected ? (
+
+ {twitter.active ? (
-
-
-
-
- ) : (
-
-
-
- You are currently not connected to Twitter. New messages will not be
- published to your account and old messages can not be deleted anymore.
-
-
- }
+ >
+ Disable
+
+ ) : (
+ }
+ >
+ Enable
+
+ )}
+ }
>
-
-
-
- Connect
-
-
-
-
+ Disconnect
+
+
+ ) : null}
+
)
}
From 61730136e863ff8d0bbe7e37a1996e1f544d2ba6 Mon Sep 17 00:00:00 2001
From: louis
Date: Fri, 20 Jan 2023 21:53:16 -0800
Subject: [PATCH 19/31] =?UTF-8?q?=F0=9F=92=84=20Remove=20some=20references?=
=?UTF-8?q?=20for=20semantic-ui?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/settings/RefreshIntervalForm.tsx | 3 +--
src/views/NotFoundView.tsx | 4 ++--
src/views/TickerView.tsx | 4 ++--
3 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/src/components/settings/RefreshIntervalForm.tsx b/src/components/settings/RefreshIntervalForm.tsx
index 9206da0a..53b422eb 100644
--- a/src/components/settings/RefreshIntervalForm.tsx
+++ b/src/components/settings/RefreshIntervalForm.tsx
@@ -3,8 +3,7 @@ import { SubmitHandler, useForm } from 'react-hook-form'
import { useQueryClient } from '@tanstack/react-query'
import { Setting, useSettingsApi } from '../../api/Settings'
import useAuth from '../useAuth'
-import { FormGroup, TextField } from '@mui/material'
-import { Grid } from 'semantic-ui-react'
+import { FormGroup, Grid, TextField } from '@mui/material'
interface Props {
name: string
diff --git a/src/views/NotFoundView.tsx b/src/views/NotFoundView.tsx
index bd827c02..45390921 100644
--- a/src/views/NotFoundView.tsx
+++ b/src/views/NotFoundView.tsx
@@ -1,11 +1,11 @@
+import { Alert } from '@mui/material'
import React, { FC } from 'react'
-import { Message } from 'semantic-ui-react'
import Layout from './Layout'
const NotFoundView: FC = () => {
return (
- Not Found
+ Not found
)
}
diff --git a/src/views/TickerView.tsx b/src/views/TickerView.tsx
index d8dfbcf7..ade0c0e6 100644
--- a/src/views/TickerView.tsx
+++ b/src/views/TickerView.tsx
@@ -1,5 +1,4 @@
import React, { FC } from 'react'
-import { Loader } from 'semantic-ui-react'
import { useTickerApi } from '../api/Ticker'
import { useQuery } from '@tanstack/react-query'
import { useParams } from 'react-router-dom'
@@ -7,6 +6,7 @@ import useAuth from '../components/useAuth'
import Ticker from '../components/ticker/Ticker'
import Layout from './Layout'
import ErrorView from './ErrorView'
+import Loader from '../components/Loader'
interface TickerViewParams {
tickerId: string
@@ -23,7 +23,7 @@ const TickerView: FC = () => {
)
if (isLoading) {
- return
+ return
}
if (error || data === undefined || data.status === 'error') {
From 5afda6b710dd5a7cb84a87a757ab5ea709963b1d Mon Sep 17 00:00:00 2001
From: louis
Date: Fri, 20 Jan 2023 21:53:50 -0800
Subject: [PATCH 20/31] =?UTF-8?q?=F0=9F=92=84=20Use=20MUI=20for=20TickerUs?=
=?UTF-8?q?erCard?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/ticker/Ticker.tsx | 4 +
src/components/ticker/TickerAddUserForm.tsx | 89 ++++++++++++++++++
src/components/ticker/TickerAddUserModal.tsx | 60 ++++++++++++
.../ticker/TickerDangerZoneCard.tsx | 4 +-
src/components/ticker/TickerUserAddForm.tsx | 93 -------------------
src/components/ticker/TickerUserCard.tsx | 60 ------------
src/components/ticker/TickerUserList.tsx | 20 ++--
src/components/ticker/TickerUserListItem.tsx | 46 ++++-----
src/components/ticker/TickerUserModalAdd.tsx | 58 ------------
.../ticker/TickerUserModalDelete.tsx | 69 +++++++++-----
src/components/ticker/TickerUsersCard.tsx | 71 ++++++++++++++
11 files changed, 304 insertions(+), 270 deletions(-)
create mode 100644 src/components/ticker/TickerAddUserForm.tsx
create mode 100644 src/components/ticker/TickerAddUserModal.tsx
delete mode 100644 src/components/ticker/TickerUserAddForm.tsx
delete mode 100644 src/components/ticker/TickerUserCard.tsx
delete mode 100644 src/components/ticker/TickerUserModalAdd.tsx
create mode 100644 src/components/ticker/TickerUsersCard.tsx
diff --git a/src/components/ticker/Ticker.tsx b/src/components/ticker/Ticker.tsx
index a63bb83c..5b2a8970 100644
--- a/src/components/ticker/Ticker.tsx
+++ b/src/components/ticker/Ticker.tsx
@@ -16,6 +16,7 @@ import {
} from '@mui/material'
import TickerModalForm from './TickerModalForm'
import TickerDangerZoneCard from './TickerDangerZoneCard'
+import TickerUsersCard from './TickerUsersCard'
interface Props {
ticker: Model
@@ -62,6 +63,9 @@ const Ticker: FC = ({ ticker }) => {
xs={12}
>
+
+
+
diff --git a/src/components/ticker/TickerAddUserForm.tsx b/src/components/ticker/TickerAddUserForm.tsx
new file mode 100644
index 00000000..da4d6d33
--- /dev/null
+++ b/src/components/ticker/TickerAddUserForm.tsx
@@ -0,0 +1,89 @@
+import React, { FC } from 'react'
+import { Controller, SubmitHandler, useForm } from 'react-hook-form'
+import { useQuery, useQueryClient } from '@tanstack/react-query'
+import { Ticker, useTickerApi } from '../../api/Ticker'
+import { User, useUserApi } from '../../api/User'
+import useAuth from '../useAuth'
+import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'
+
+interface Props {
+ ticker: Ticker
+ onSubmit: () => void
+ users: User[]
+}
+
+interface Option {
+ key: number
+ text: string
+ value: number
+}
+
+interface FormValues {
+ users: Array
+}
+
+const TickerAddUserForm: FC = ({ onSubmit, ticker, users }) => {
+ const { token } = useAuth()
+ const { getUsers } = useUserApi(token)
+ const { putTickerUsers } = useTickerApi(token)
+ const { isLoading, data, error } = useQuery(
+ ['tickerUsersAvailable'],
+ getUsers
+ )
+ const { control, handleSubmit } = useForm()
+ const queryClient = useQueryClient()
+
+ const updateTickerUsers: SubmitHandler = data => {
+ putTickerUsers(ticker, data.users).then(() => {
+ queryClient.invalidateQueries(['tickerUsers', ticker.id])
+ onSubmit()
+ })
+ }
+
+ if (isLoading) {
+ return <>Loading>
+ }
+
+ if (error || data === undefined) {
+ return <>error>
+ }
+
+ const options: Array