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/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/package.json b/package.json
index 6e7979b6..ad1cd8f2 100644
--- a/package.json
+++ b/package.json
@@ -3,15 +3,18 @@
"scripts": {
"start": "webpack serve --config webpack.dev.config.ts",
"test": "jest",
- "build": "webpack --config webpack.prod.config.ts",
- "postinstall": "semantic-ui-css-patch"
+ "build": "webpack --config webpack.prod.config.ts"
},
"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",
- "@semantic-ui-react/css-patch": "^1.1.2",
+ "@mui/icons-material": "^5.10.9",
+ "@mui/material": "^5.10.10",
"@tanstack/react-query": "^4.10.3",
"@tanstack/react-query-devtools": "^4.11.0",
"jwt-decode": "^3.1.2",
@@ -38,6 +41,8 @@
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.2.1",
"@types/jwt-decode": "^3.1.0",
"@types/leaflet": "^1.9.0",
@@ -75,6 +80,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",
@@ -82,8 +88,6 @@
"react-dev-utils": "^12.0.1",
"resolve": "^1.22.1",
"resolve-url-loader": "^5.0.0",
- "semantic-ui-css": "^2.5.0",
- "semantic-ui-react": "^2.1.3",
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.3.3",
"ts-jest": "^29.0.3",
diff --git a/src/App.tsx b/src/App.tsx
index 4cc84b73..b9dd7079 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -10,10 +10,9 @@ 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'
+import ThemeProvider from './theme/ThemeProvider'
+import '../leaflet.config.js'
const App: FC = () => {
const queryClient = new QueryClient({
@@ -21,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/__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/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
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/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/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/Message.tsx b/src/components/message/Message.tsx
index e841dc19..9de32da7 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,35 @@ 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/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/MessageFooter.tsx b/src/components/message/MessageFooter.tsx
new file mode 100644
index 00000000..bf053333
--- /dev/null
+++ b/src/components/message/MessageFooter.tsx
@@ -0,0 +1,53 @@
+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, Link, Stack, Typography, useTheme } 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 }) => {
+ const theme = useTheme()
+
+ return url ? (
+
+
+
+ ) : null
+}
+
+export default MessageFooter
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/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/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 (
-
+
) => 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
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
)
}
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/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/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..f38b6a3d
--- /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/components/settings/InactiveSettingsCard.tsx b/src/components/settings/InactiveSettingsCard.tsx
index 19efab5d..0cd34095 100644
--- a/src/components/settings/InactiveSettingsCard.tsx
+++ b/src/components/settings/InactiveSettingsCard.tsx
@@ -1,21 +1,25 @@
-import React, { FC } from 'react'
-import ReactMarkdown from 'react-markdown'
+import React, { FC, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
+import { useSettingsApi } from '../../api/Settings'
+import ErrorView from '../../views/ErrorView'
+import useAuth from '../useAuth'
+import Loader from '../Loader'
import {
+ Box,
Button,
Card,
- Dimmer,
- Header,
- Icon,
- List,
- Loader,
-} from 'semantic-ui-react'
-import { useSettingsApi } from '../../api/Settings'
-import ErrorView from '../../views/ErrorView'
+ CardContent,
+ Divider,
+ Grid,
+ Typography,
+} from '@mui/material'
+import { Stack } from '@mui/system'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faPencil } from '@fortawesome/free-solid-svg-icons'
import InactiveSettingsModalForm from './InactiveSettingsModalForm'
-import useAuth from '../useAuth'
const InactiveSettingsCard: FC = () => {
+ const [formOpen, setFormOpen] = useState(false)
const { token } = useAuth()
const { getInactiveSettings } = useSettingsApi(token)
const { isLoading, error, data } = useQuery(
@@ -23,74 +27,112 @@ const InactiveSettingsCard: FC = () => {
getInactiveSettings
)
+ const handleFormOpen = () => {
+ setFormOpen(true)
+ }
+
+ const handleFormClose = () => {
+ setFormOpen(false)
+ }
+
if (isLoading) {
- return (
-
- Loading
-
- )
+ return
}
if (error || data === undefined || data.status === 'error') {
- return Unable to fetch inactive settings from server.
+ return (
+
+ Unable to fetch inactive settings from server.
+
+ )
}
const setting = data.data.setting
return (
-
-
-
- Inactive Settings
-
-
+
+
+
+ Inactive Settings
+
+ }
+ >
+ Edit
+
+
+
These settings have affect for inactive or non-configured tickers.
-
-
-
-
-
-
- Headline
- {setting.value.headline}
-
-
- Subheadline
- {setting.value.sub_headline}
-
-
- Description
- {setting.value.description}
-
-
-
-
-
-
- {setting.value.author}
-
-
-
- {setting.value.email}
-
-
-
- {setting.value.homepage}
-
-
-
- {setting.value.twitter}
-
-
-
-
-
+
+
+
+
+
+
+ Headline
+
+ {setting.value.headline}
+
+
+
+ Subheadline
+
+ {setting.value.sub_headline}
+
+
+
+ Description
+
+ {setting.value.description}
+
+
+
+
+
+ Author
+
+ {setting.value.author}
+
+
+
+
+
+ Homepage
+
+ {setting.value.homepage}
+
+
+
+
+
+ E-Mail
+
+ {setting.value.email}
+
+
+
+
+
+ Twitter
+
+ {setting.value.twitter}
+
+
+
}
/>
-
+
)
}
diff --git a/src/components/settings/InactiveSettingsForm.tsx b/src/components/settings/InactiveSettingsForm.tsx
index 98c0f1e9..21300109 100644
--- a/src/components/settings/InactiveSettingsForm.tsx
+++ b/src/components/settings/InactiveSettingsForm.tsx
@@ -1,22 +1,12 @@
-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 {
- Form,
- Header,
- InputOnChangeData,
- TextAreaProps,
-} from 'semantic-ui-react'
import { InactiveSetting, Setting, useSettingsApi } from '../../api/Settings'
import useAuth from '../useAuth'
+import { FormGroup, Grid, TextField } from '@mui/material'
interface Props {
+ name: string
setting: Setting
callback: () => void
}
@@ -31,9 +21,8 @@ interface FormValues {
twitter: string
}
-const InactiveSettingsForm: FC = props => {
- const setting = props.setting
- const { handleSubmit, register, setValue } = useForm({
+const InactiveSettingsForm: FC = ({ name, setting, callback }) => {
+ const { handleSubmit, register } = useForm({
defaultValues: {
headline: setting.value.headline,
sub_headline: setting.value.sub_headline,
@@ -48,95 +37,105 @@ const InactiveSettingsForm: FC = props => {
const { putInactiveSettings } = useSettingsApi(token)
const queryClient = useQueryClient()
- const onChange = useCallback(
- (
- e: FormEvent | ChangeEvent,
- { name, value }: InputOnChangeData | TextAreaProps
- ) => {
- setValue(name, value)
- },
- [setValue]
- )
-
const onSubmit: SubmitHandler = data => {
putInactiveSettings(data)
.then(() => queryClient.invalidateQueries(['inactive_settings']))
- .finally(() => props.callback())
+ .finally(() => callback())
}
- useEffect(() => {
- register('headline')
- register('sub_headline')
- register('description')
- register('author')
- register('email')
- register('homepage')
- register('twitter')
- })
-
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
)
}
diff --git a/src/components/settings/InactiveSettingsModalForm.tsx b/src/components/settings/InactiveSettingsModalForm.tsx
index 9c5fdf0e..74df2082 100644
--- a/src/components/settings/InactiveSettingsModalForm.tsx
+++ b/src/components/settings/InactiveSettingsModalForm.tsx
@@ -1,51 +1,63 @@
-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 { InactiveSetting, Setting } from '../../api/Settings'
import InactiveSettingsForm from './InactiveSettingsForm'
interface Props {
+ open: boolean
+ onClose: () => void
setting: Setting
- trigger: React.ReactNode
}
-const InactiveSettingsModalForm: FC = props => {
- const [open, setOpen] = useState(false)
-
- const handleClose = useCallback(() => {
- setOpen(false)
- }, [])
-
- const handleOpen = useCallback(() => {
- setOpen(true)
- }, [])
+const InactiveSettingsModalForm: FC = ({ open, onClose, setting }) => {
+ const handleClose = () => {
+ onClose()
+ }
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
)
}
diff --git a/src/components/settings/RefreshIntervalCard.tsx b/src/components/settings/RefreshIntervalCard.tsx
index 4272ff82..2adc33a5 100644
--- a/src/components/settings/RefreshIntervalCard.tsx
+++ b/src/components/settings/RefreshIntervalCard.tsx
@@ -1,12 +1,24 @@
-import React, { FC } from 'react'
+import React, { FC, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
-import { Button, Card, Dimmer, Icon, List, Loader } from 'semantic-ui-react'
import { useSettingsApi } from '../../api/Settings'
import ErrorView from '../../views/ErrorView'
-import RefreshIntervalModalForm from './RefreshIntervalModalForm'
import useAuth from '../useAuth'
+import {
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Divider,
+ Stack,
+ Typography,
+} from '@mui/material'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faPencil } from '@fortawesome/free-solid-svg-icons'
+import Loader from '../Loader'
+import RefreshIntervalModalForm from './RefreshIntervalModalForm'
const RefreshIntervalCard: FC = () => {
+ const [formOpen, setFormOpen] = useState(false)
const { token } = useAuth()
const { getRefreshInterval } = useSettingsApi(token)
const { isLoading, error, data } = useQuery(
@@ -14,17 +26,21 @@ const RefreshIntervalCard: FC = () => {
getRefreshInterval
)
+ const handleFormOpen = () => {
+ setFormOpen(true)
+ }
+
+ const handleFormClose = () => {
+ setFormOpen(false)
+ }
+
if (isLoading) {
- return (
-
- Loading
-
- )
+ return
}
if (error || data === undefined || data.status === 'error') {
return (
-
+
Unable to fetch refresh interval setting from server.
)
@@ -34,31 +50,42 @@ const RefreshIntervalCard: FC = () => {
return (
-
-
-
- Refresh Interval
-
-
- These setting configures the reload interval for the frontend
-
-
-
-
-
-
-
- {setting.value} ms
-
-
-
-
-
+
+
+
+ Refresh Interval
+
+ }
+ >
+ Edit
+
+
+
+ These settings have affect for inactive or non-configured tickers.
+
+
+
+
+
+
+ Refresh Interval
+
+ {setting.value} ms
+
}
/>
-
+
)
}
diff --git a/src/components/settings/RefreshIntervalForm.tsx b/src/components/settings/RefreshIntervalForm.tsx
index 8dab6e35..53b422eb 100644
--- a/src/components/settings/RefreshIntervalForm.tsx
+++ b/src/components/settings/RefreshIntervalForm.tsx
@@ -1,11 +1,12 @@
-import React, { 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 { Form, InputOnChangeData } from 'semantic-ui-react'
import { Setting, useSettingsApi } from '../../api/Settings'
import useAuth from '../useAuth'
+import { FormGroup, Grid, TextField } from '@mui/material'
interface Props {
+ name: string
setting: Setting
callback: () => void
}
@@ -14,9 +15,8 @@ interface FormValues {
refresh_interval: number
}
-const RefreshIntervalForm: FC = props => {
- const setting = props.setting
- const { handleSubmit, register, setValue } = useForm({
+const RefreshIntervalForm: FC = ({ name, setting, callback }) => {
+ const { handleSubmit, register } = useForm({
defaultValues: {
refresh_interval: parseInt(setting.value, 10),
},
@@ -25,33 +25,30 @@ const RefreshIntervalForm: FC = props => {
const { putRefreshInterval } = useSettingsApi(token)
const queryClient = useQueryClient()
- const onChange = useCallback(
- (e: FormEvent, { name, value }: InputOnChangeData) => {
- setValue(name, value)
- },
- [setValue]
- )
-
const onSubmit: SubmitHandler = data => {
putRefreshInterval(data.refresh_interval)
.then(() => queryClient.invalidateQueries(['refresh_interval_setting']))
- .finally(() => props.callback())
+ .finally(() => callback())
}
- useEffect(() => {
- register('refresh_interval', { valueAsNumber: true })
- })
-
return (
-
-
+
)
}
diff --git a/src/components/settings/RefreshIntervalModalForm.tsx b/src/components/settings/RefreshIntervalModalForm.tsx
index cad247fc..65a0f189 100644
--- a/src/components/settings/RefreshIntervalModalForm.tsx
+++ b/src/components/settings/RefreshIntervalModalForm.tsx
@@ -1,51 +1,63 @@
-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,
+} from '@mui/material'
+import { Stack } from '@mui/system'
+import React, { FC } from 'react'
import { Setting } from '../../api/Settings'
import RefreshIntervalForm from './RefreshIntervalForm'
interface Props {
+ open: boolean
+ onClose: () => void
setting: Setting
- trigger: React.ReactNode
}
-const RefreshIntervalModalForm: FC = props => {
- const [open, setOpen] = useState(false)
-
- const handleClose = useCallback(() => {
- setOpen(false)
- }, [])
-
- const handleOpen = useCallback(() => {
- setOpen(true)
- }, [])
+const RefreshIntervalModalForm: FC = ({ open, onClose, setting }) => {
+ const handleClose = () => {
+ onClose()
+ }
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
)
}
diff --git a/src/components/ticker/LocationSearch.tsx b/src/components/ticker/LocationSearch.tsx
index dbdf17b7..01de1682 100644
--- a/src/components/ticker/LocationSearch.tsx
+++ b/src/components/ticker/LocationSearch.tsx
@@ -1,12 +1,5 @@
-import React, {
- FC,
- MouseEvent,
- useCallback,
- useEffect,
- useReducer,
- useRef,
-} from 'react'
-import { Search, SearchProps } from 'semantic-ui-react'
+import React, { FC, useRef, useState } from 'react'
+import { Autocomplete, TextField } from '@mui/material'
interface SearchResult {
place_id: number
@@ -21,136 +14,60 @@ export interface Result {
lon: number
}
-interface State {
- loading: boolean
- results: Result[]
- value: string
-}
-
-const initialState: State = {
- loading: false,
- results: [],
- value: '',
-}
-
-enum SearchActionType {
- CLEAN_QUERY = 'CLEAN_QUERY',
- START_SEARCH = 'START_SEARCH',
- FINISH_SEARCH = 'FINISH_SEARCH',
- UPDATE_SELECTION = 'UPDATE_SELECTION',
-}
-
-interface SearchAction {
- type: SearchActionType
- query?: string
- results?: Result[]
- selection?: string
-}
-
-async function api(value: string): Promise {
+async function api(
+ value: string,
+ signal: AbortSignal
+): Promise {
const url =
'https://nominatim.openstreetmap.org/search?format=json&limit=5&q=' + value
- const timeout = (time: number) => {
- const controller = new AbortController()
- setTimeout(() => controller.abort(), time * 1000)
- return controller
- }
-
- return fetch(url, { signal: timeout(30).signal }).then(res => res.json())
-}
-
-function searchReducer(state: any, action: SearchAction) {
- switch (action.type) {
- case SearchActionType.CLEAN_QUERY:
- return initialState
- case SearchActionType.START_SEARCH:
- return { ...state, loading: true, value: action.query, results: [] }
- case SearchActionType.FINISH_SEARCH:
- return { ...state, loading: false, results: action.results }
- case SearchActionType.UPDATE_SELECTION:
- return { ...state, value: action.selection }
-
- default:
- throw new Error()
- }
+ return fetch(url, { signal }).then(res => res.json())
}
interface Props {
callback: (result: Result) => void
}
-const LocationSearch: FC = props => {
- const [state, dispatch] = useReducer(searchReducer, initialState)
- const { loading, results, value } = state
- const timeoutRef: { current: NodeJS.Timeout | null } = useRef(null)
-
- const handleResultSelect = useCallback(
- (event: MouseEvent, data: any) => {
- dispatch({
- type: SearchActionType.UPDATE_SELECTION,
- selection: data.result.title,
- })
-
- props.callback(data.result)
- },
- [props]
- )
-
- const handleSearchChange = useCallback(
- (event: MouseEvent, data: SearchProps) => {
- const value = data.value
- if (value === undefined) {
- return
- }
+const LocationSearch: FC = ({ callback }) => {
+ const [options, setOptions] = useState([])
+ const previousController = useRef()
- clearTimeout(timeoutRef.current as NodeJS.Timeout)
- dispatch({ type: SearchActionType.START_SEARCH, query: value })
+ const handleInputChange = (event: React.SyntheticEvent, value: string) => {
+ if (previousController.current) {
+ previousController.current.abort()
+ }
- timeoutRef.current = setTimeout(() => {
- if (value.length === 0) {
- dispatch({ type: SearchActionType.CLEAN_QUERY })
- return
- }
+ const controller = new AbortController()
+ const signal = controller.signal
+ previousController.current = controller
- const results: Result[] = []
- api(value)
- .then(data => {
- data.forEach(entry => {
- results.push({
- title: entry.display_name,
- lat: entry.lat,
- lon: entry.lon,
- })
- })
- })
- .finally(() => {
- dispatch({
- type: SearchActionType.FINISH_SEARCH,
- results: results,
- })
- })
- }, 300)
- },
- []
- )
+ api(value, signal)
+ .then(options => setOptions(options))
+ .catch(() => {
+ // We ignore the error
+ })
+ }
- useEffect(() => {
- return () => {
- clearTimeout(timeoutRef.current as NodeJS.Timeout)
+ const handleChange = (
+ event: React.SyntheticEvent,
+ value: SearchResult | null
+ ) => {
+ if (value) {
+ callback({ title: value?.display_name, lat: value?.lat, lon: value?.lon })
}
- }, [])
+ }
return (
-
-
-
+ option.display_name}
+ onChange={handleChange}
+ onInputChange={handleInputChange}
+ options={options}
+ renderInput={params => (
+
+ )}
+ />
)
}
diff --git a/src/components/ticker/MastodonCard.tsx b/src/components/ticker/MastodonCard.tsx
index 81245236..8cb9e7d2 100644
--- a/src/components/ticker/MastodonCard.tsx
+++ b/src/components/ticker/MastodonCard.tsx
@@ -1,11 +1,27 @@
+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'
-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,
+ Divider,
+ Link,
+ Stack,
+ Typography,
+} from '@mui/material'
+import {
+ faBan,
+ faGear,
+ faPause,
+ faPlay,
+} from '@fortawesome/free-solid-svg-icons'
interface Props {
ticker: Ticker
@@ -14,6 +30,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
@@ -31,85 +49,83 @@ const MastodonCard: FC = ({ ticker }) => {
}, [mastodon.active, putTickerMastodon, queryClient, ticker])
const profileLink = (
-
@{mastodon.name}@{mastodon.server.replace(/^https?:\/\//, '')}
-
+
)
- return mastodon.connected ? (
-
-
-
- {mastodon.image_url != '' && (
-
- )}
-
-
- {mastodon.screen_name}
-
- {profileLink}
-
-
-
- {mastodon.active ? (
-
- ) : (
-
- )}
+ return (
+
+
+
+
+ Mastodon
+
+
+
+
+
+
+ {mastodon.connected ? (
+
+
+ You are connected with Mastodon.
+
+ Your 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 ? (
-
-
-
-
- ) : (
-
-
-
- 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"
- />
- }
- />
-
-
-
+ onClick={handleToggle}
+ startIcon={}
+ >
+ 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
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
)
}
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/TelegramCard.tsx b/src/components/ticker/TelegramCard.tsx
index f82f21c5..0daa760d 100644
--- a/src/components/ticker/TelegramCard.tsx
+++ b/src/components/ticker/TelegramCard.tsx
@@ -1,8 +1,24 @@
import { faTelegram } from '@fortawesome/free-brands-svg-icons'
+import {
+ faBan,
+ faGear,
+ faPause,
+ faPlay,
+} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import {
+ Box,
+ Button,
+ Card,
+ CardActions,
+ CardContent,
+ Divider,
+ Link,
+ 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 +30,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 +47,86 @@ const TelegramCard: FC = ({ ticker }) => {
})
}, [deleteTickerTelegram, queryClient, ticker])
- return telegram.connected ? (
-
-
-
-
-
- {telegram.channel_name}
-
- Bot: {telegram.bot_username}
-
-
-
- {telegram.active ? (
-
- ) : (
-
- )}
+ const channelLink = (
+
+ {telegram.channel_name}
+
+ )
+
+ return (
+
+
+
+
+ Telegram
+
+
+
+
+
+
+ {telegram.connected ? (
+
+
+ You are connected with Telegram.
+
+
+ Your Channel: {channelLink} (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 ? (
+ }
+ >
+ Disable
+
+ ) : (
-
-
-
-
- ) : (
-
-
-
- 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={}
+ >
+ 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
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
)
}
diff --git a/src/components/ticker/Ticker.tsx b/src/components/ticker/Ticker.tsx
index 2e790d73..5b2a8970 100644
--- a/src/components/ticker/Ticker.tsx
+++ b/src/components/ticker/Ticker.tsx
@@ -1,92 +1,87 @@
-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'
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Grid,
+ Stack,
+ Typography,
+} from '@mui/material'
+import TickerModalForm from './TickerModalForm'
+import TickerDangerZoneCard from './TickerDangerZoneCard'
+import TickerUsersCard from './TickerUsersCard'
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 (
-
-
-
-
-
+
+
+
+
+ Ticker
+
+
+ {
+ setFormModalOpen(false)
+ }}
+ open={formModalOpen}
+ ticker={ticker}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- {twitter_enabled && (
- <>
-
-
- >
- )}
-
- {telegram_enabled && (
- <>
-
-
- >
- )}
- {user?.roles.includes('admin') && (
-
-
-
-
-
- }
- />
-
- )}
-
-
+
+
)
}
diff --git a/src/components/ticker/TickerCard.tsx b/src/components/ticker/TickerCard.tsx
index 8833d332..71dc1a1d 100644
--- a/src/components/ticker/TickerCard.tsx
+++ b/src/components/ticker/TickerCard.tsx
@@ -1,62 +1,77 @@
import React, { FC } from 'react'
-import { Button, Card, Icon, Label } from 'semantic-ui-react'
-import ReactMarkdown from 'react-markdown'
-import TickerModalForm from './TickerModalForm'
+import { Box, Card, CardContent, Link, 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 { faGear } from '@fortawesome/free-solid-svg-icons'
+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}
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/components/ticker/TickerDangerZoneCard.tsx b/src/components/ticker/TickerDangerZoneCard.tsx
new file mode 100644
index 00000000..bbea9d8d
--- /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(false)}
+ open={resetOpen}
+ ticker={ticker}
+ />
+
+
+
+ ) : null
+}
+
+export default TickerDangerZoneCard
diff --git a/src/components/ticker/TickerForm.tsx b/src/components/ticker/TickerForm.tsx
index a9e8a689..d58fef90 100644
--- a/src/components/ticker/TickerForm.tsx
+++ b/src/components/ticker/TickerForm.tsx
@@ -1,29 +1,36 @@
-import React, {
- ChangeEvent,
- FC,
- FormEvent,
- useCallback,
- useEffect,
-} from 'react'
-import {
- Button,
- CheckboxProps,
- Form,
- Header,
- Icon,
- Input,
- InputOnChangeData,
- Message,
- TextAreaProps,
-} from 'semantic-ui-react'
+import React, { FC, useCallback, useEffect } from 'react'
import { Ticker, useTickerApi } from '../../api/Ticker'
import { SubmitHandler, useForm } from 'react-hook-form'
import { useQueryClient } from '@tanstack/react-query'
import useAuth from '../useAuth'
import LocationSearch, { Result } from './LocationSearch'
import { MapContainer, Marker, TileLayer } from 'react-leaflet'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import {
+ Alert,
+ Button,
+ Checkbox,
+ FormControlLabel,
+ FormGroup,
+ Grid,
+ InputAdornment,
+ Stack,
+ TextField,
+ Typography,
+} from '@mui/material'
+import {
+ faComputerMouse,
+ faEnvelope,
+ faUser,
+} from '@fortawesome/free-solid-svg-icons'
+import {
+ faFacebook,
+ faTelegram,
+ faTwitter,
+} from '@fortawesome/free-brands-svg-icons'
interface Props {
+ id: string
ticker?: Ticker
callback: () => void
}
@@ -47,8 +54,7 @@ interface FormValues {
}
}
-const TickerForm: FC = props => {
- const ticker = props.ticker
+const TickerForm: FC = ({ callback, id, ticker }) => {
const { handleSubmit, register, setValue, watch } = useForm({
defaultValues: {
title: ticker?.title,
@@ -69,7 +75,7 @@ const TickerForm: FC = props => {
},
},
})
- const { token, user } = useAuth()
+ const { token } = useAuth()
const { postTicker, putTicker } = useTickerApi(token)
const queryClient = useQueryClient()
@@ -91,35 +97,17 @@ const TickerForm: FC = props => {
[setValue]
)
- const onChange = useCallback(
- (
- e: ChangeEvent | FormEvent,
- {
- name,
- value,
- checked,
- }: InputOnChangeData | CheckboxProps | TextAreaProps
- ) => {
- if (checked !== undefined) {
- setValue(name, checked)
- } else {
- setValue(name, value)
- }
- },
- [setValue]
- )
-
const onSubmit: SubmitHandler = data => {
if (ticker) {
putTicker(data, ticker.id).finally(() => {
queryClient.invalidateQueries(['tickers'])
queryClient.invalidateQueries(['ticker', ticker.id])
- props.callback()
+ callback()
})
} else {
postTicker(data).finally(() => {
queryClient.invalidateQueries(['tickers'])
- props.callback()
+ callback()
})
}
}
@@ -132,143 +120,194 @@ const TickerForm: FC = props => {
const position = watch('location')
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- You can add a default location to the ticker. This will help you to have
- a pre-selected location when you add a map to a message.
- Current Location: {position.lat.toFixed(2)},{position.lon.toFixed(2)}
-
-
-
-
-
-
-
-
-
- {position.lat !== 0 && position.lon !== 0 ? (
-
-
-
-
- ) : null}
-
+
)
}
diff --git a/src/components/ticker/TickerList.tsx b/src/components/ticker/TickerList.tsx
index a4eaaf9a..0800ed62 100644
--- a/src/components/ticker/TickerList.tsx
+++ b/src/components/ticker/TickerList.tsx
@@ -1,54 +1,92 @@
import React, { FC } from 'react'
-import { Ticker } from '../../api/Ticker'
-import { Button, Table } from 'semantic-ui-react'
-import TickerModalForm from './TickerModalForm'
+import { useTickerApi } from '../../api/Ticker'
import TickerListItems from './TickerListItems'
import useAuth from '../useAuth'
+import { useQuery } from '@tanstack/react-query'
+import Loader from '../Loader'
+import ErrorView from '../../views/ErrorView'
+import { Navigate } from 'react-router'
+import {
+ Card,
+ CardContent,
+ Table,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Typography,
+} from '@mui/material'
-interface Props {
- tickers: Ticker[]
-}
+const TickerList: FC = () => {
+ const { token, user } = useAuth()
+ const { getTickers } = useTickerApi(token)
+ const { isLoading, error, data } = useQuery(['tickers'], getTickers)
+
+ if (isLoading) {
+ return
+ }
+
+ if (error || data === undefined || data.status === 'error') {
+ return (
+
+ Unable to fetch tickers from server.
+
+ )
+ }
+
+ const tickers = data.data.tickers
+
+ 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.
+
+
+
+ )
+ }
+ }
-const TickerList: FC = props => {
- const { user } = useAuth()
+ if (tickers.length === 1 && !user?.roles.includes('admin')) {
+ return
+ }
return (
-
+
-
-
-
- Title
- Domain
-
-
-
-
- {user?.roles.includes('admin') ? (
-
-
-
-
-
-
-
- }
- />
-
-
-
- ) : null}
+
+
+
+ Active
+
+ Title
+ Domain
+
+
+
+
-
+
)
}
diff --git a/src/components/ticker/TickerListItem.tsx b/src/components/ticker/TickerListItem.tsx
index 27b56882..3b61447c 100644
--- a/src/components/ticker/TickerListItem.tsx
+++ b/src/components/ticker/TickerListItem.tsx
@@ -1,10 +1,27 @@
-import React, { FC, useCallback } from 'react'
+import React, { FC, useState } from 'react'
import { useNavigate } from 'react-router'
-import { Button, Icon, Table } from 'semantic-ui-react'
import { Ticker } from '../../api/Ticker'
+import useAuth from '../useAuth'
+import {
+ colors,
+ IconButton,
+ MenuItem,
+ Popover,
+ TableCell,
+ TableRow,
+ Typography,
+} from '@mui/material'
+import { MoreVert } from '@mui/icons-material'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import {
+ faCheck,
+ faHandPointer,
+ faPencil,
+ faTrash,
+ faXmark,
+} from '@fortawesome/free-solid-svg-icons'
import TickerModalDelete from './TickerModalDelete'
import TickerModalForm from './TickerModalForm'
-import useAuth from '../useAuth'
interface Props {
ticker: Ticker
@@ -13,43 +30,99 @@ interface Props {
const TickerListItem: FC = ({ ticker }: Props) => {
const { user } = useAuth()
const navigate = useNavigate()
+ 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)
+ }
+
+ const handleUse = () => {
+ navigate(`/ticker/${ticker.id}`)
+ }
return (
-
-
-
-
- {ticker.title}
- {ticker.domain}
-
-
-
-
-
+
+ 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..3d3d2cdb 100644
--- a/src/components/ticker/TickerModalForm.tsx
+++ b/src/components/ticker/TickerModalForm.tsx
@@ -1,52 +1,82 @@
-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,
+ Tab,
+ Tabs,
+} from '@mui/material'
+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
+ open: boolean
ticker?: Ticker
- trigger: React.ReactNode
}
-const TickerModalForm: FC = props => {
- const [open, setOpen] = useState(false)
+const TickerModalForm: FC = ({ onClose, open, ticker }) => {
+ const [tabValue, setTabValue] = useState(0)
- const handleClose = useCallback(() => {
- setOpen(false)
- }, [])
+ const handleClose = () => {
+ onClose()
+ }
- const handleOpen = useCallback(() => {
- setOpen(true)
- }, [])
+ const handleTabChange = (e: React.SyntheticEvent, value: number) => {
+ setTabValue(value)
+ }
return (
-
- {' '}
-
- {props.ticker ? `Edit ${props.ticker.title}` : 'Create Ticker'}
-
-
-
-
-
-
+
-
-
+ variant="contained"
+ >
+ Save
+
+ ) : null}
+
+
+
)
}
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
-
+
-
-
-
-
+
+
)
}
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/TickerUserAddForm.tsx b/src/components/ticker/TickerUserAddForm.tsx
deleted file mode 100644
index 744b774b..00000000
--- a/src/components/ticker/TickerUserAddForm.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React, { FC, useCallback, useEffect } from 'react'
-import { SubmitHandler, useForm } from 'react-hook-form'
-import { useQuery, useQueryClient } from '@tanstack/react-query'
-import { Dropdown, DropdownProps, Form } from 'semantic-ui-react'
-import { Ticker, useTickerApi } from '../../api/Ticker'
-import { User, useUserApi } from '../../api/User'
-import useAuth from '../useAuth'
-
-interface Props {
- ticker: Ticker
- callback: () => void
- users: User[] | null
-}
-
-interface Option {
- key: number
- text: string
- value: number
-}
-
-interface FormValues {
- users: Array
-}
-
-const TickerUserAddForm: FC = props => {
- const { token } = useAuth()
- const { getUsers } = useUserApi(token)
- const { putTickerUsers } = useTickerApi(token)
- const { isLoading, data, error } = useQuery(
- ['tickerUsersAvailable'],
- getUsers
- )
- const { handleSubmit, register, setValue } = useForm()
- const queryClient = useQueryClient()
-
- const onSubmit: SubmitHandler = data => {
- putTickerUsers(props.ticker, data.users).then(() => {
- queryClient.invalidateQueries(['tickerUsers', props.ticker.id])
- props.callback()
- })
- }
-
- const handleChange = useCallback(
- (event: React.SyntheticEvent, data: DropdownProps) => {
- // @ts-ignore
- setValue('users', data.value)
- },
- [setValue]
- )
-
- useEffect(() => {
- register('users')
- })
-
- if (isLoading) {
- return <>Loading>
- }
-
- if (error || data === undefined) {
- return <>error>
- }
-
- const options: Array