From 311666360dc1c8061a3612384a09d7cd154b2ae1 Mon Sep 17 00:00:00 2001 From: louis Date: Sat, 29 Oct 2022 00:38:05 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20Use=20MUI=20for=20HomeView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ticker/LocationSearch.tsx | 163 ++------ src/components/ticker/TickerForm.tsx | 395 +++++++++++--------- src/components/ticker/TickerList.tsx | 122 +++--- src/components/ticker/TickerListItem.tsx | 145 +++++-- src/components/ticker/TickerListItems.tsx | 6 +- src/components/ticker/TickerModalDelete.tsx | 66 ++-- src/components/ticker/TickerModalForm.tsx | 87 +++-- src/views/HomeView.tsx | 126 +++---- 8 files changed, 583 insertions(+), 527 deletions(-) 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/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 ( -
- - - - - - -
Information
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Location
- - 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..57333108 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 && 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. + + + + ) + } -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..1ea51a62 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} - - - + + + ) } 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 ( - - - -
Available Configurations
-
-
- - - - - + + + + + Tickers + + {user?.roles.includes('admin') ? ( + <> + + { + setFormModalOpen(false) + }} + open={formModalOpen} + /> + + ) : null} + + + + + + +
)