From e15d50d2760d10d5b01efd2e8f0dbc25a2717290 Mon Sep 17 00:00:00 2001 From: Anne <36864084+batabana@users.noreply.github.com> Date: Fri, 24 Jun 2022 17:59:00 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rewrite=20Ticker=20View=20?= =?UTF-8?q?to=20TypeScript=20(#272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Rewrite Ticker View to TypeScript --- package.json | 3 +- src/App.tsx | 15 +- src/api/Message.js | 43 ---- src/api/Message.ts | 56 ++++++ src/components/Message.js | 194 ------------------- src/components/Message.tsx | 152 +++++++++++++++ src/components/MessageForm.js | 269 -------------------------- src/components/MessageForm.tsx | 110 +++++++++++ src/components/MessageFormCounter.tsx | 34 ++++ src/components/MessageList.tsx | 42 ++++ src/components/MessageModalDelete.tsx | 44 +++++ src/lib/helper.ts | 24 +++ src/views/TickerView.js | 162 ---------------- src/views/TickerView.tsx | 106 ++++++++++ yarn.lock | 34 +++- 15 files changed, 596 insertions(+), 692 deletions(-) delete mode 100644 src/api/Message.js create mode 100644 src/api/Message.ts delete mode 100644 src/components/Message.js create mode 100644 src/components/Message.tsx delete mode 100644 src/components/MessageForm.js create mode 100644 src/components/MessageForm.tsx create mode 100644 src/components/MessageFormCounter.tsx create mode 100644 src/components/MessageList.tsx create mode 100644 src/components/MessageModalDelete.tsx create mode 100644 src/lib/helper.ts delete mode 100644 src/views/TickerView.js create mode 100644 src/views/TickerView.tsx diff --git a/package.json b/package.json index 7857d837..ada5b0fe 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "react-dom": "^17.0.0", "react-hook-form": "^7.32.0", "react-image-lightbox": "^5.1.4", - "react-leaflet": "^2.6.3", + "react-leaflet": "^3.0.0", "react-leaflet-draw": "^0.19.0", "react-markdown": "^8.0.3", "react-moment": "^0.9.7", @@ -32,6 +32,7 @@ "@babel/preset-env": "^7.9.5", "@babel/preset-react": "^7.9.4", "@babel/preset-typescript": "^7.17.12", + "@types/leaflet": "^1.7.11", "@types/node": "^18.0.0", "@types/react-dom": "^17.0.17", "@types/react-router-dom": "^5.0.0", diff --git a/src/App.tsx b/src/App.tsx index d0ef4fb0..7f9ded5f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react' import { QueryClient, QueryClientProvider } from 'react-query' import { ReactQueryDevtools } from 'react-query/devtools' -import { BrowserRouter, Route, Switch, useParams } from 'react-router-dom' +import { BrowserRouter, Route, Switch } from 'react-router-dom' import HomeView from './views/HomeView' import LoginView from './views/LoginView' import SettingsView from './views/SettingsView' @@ -11,17 +11,6 @@ import 'semantic-ui-css/semantic.min.css' import './index.css' import '../leaflet.config.js' -interface TickerViewParams { - tickerId: string -} - -//TODO: Can be removed if TickerView is rewritten -const TickerViewWrapper: FC = () => { - const { tickerId } = useParams() - - return -} - const App: FC = () => { const queryClient = new QueryClient() @@ -31,7 +20,7 @@ const App: FC = () => { - + diff --git a/src/api/Message.js b/src/api/Message.js deleted file mode 100644 index 837007de..00000000 --- a/src/api/Message.js +++ /dev/null @@ -1,43 +0,0 @@ -import { ApiUrl } from './Api' -import AuthSingleton from '../components/AuthService' - -const Auth = AuthSingleton.getInstance() - -/** - * @param {string} ticker - * @returns {Promise} - */ -export function getMessages(ticker) { - return Auth.fetch(`${ApiUrl}/admin/tickers/${ticker}/messages`) -} - -/** - * - * @param {string} ticker - * @param {string} text - * @param {object} geoInformation - * @param {array} attachments - * @returns {Promise} - */ -export function postMessage(ticker, text, geoInformation, attachments) { - return Auth.fetch(`${ApiUrl}/admin/tickers/${ticker}/messages`, { - body: JSON.stringify({ - text: text, - geo_information: geoInformation, - attachments: attachments, - }), - method: 'POST', - }) -} - -/** - * - * @param {string} ticker - * @param {string} message - * @returns {Promise} - */ -export function deleteMessage(ticker, message) { - return Auth.fetch(`${ApiUrl}/admin/tickers/${ticker}/messages/${message}`, { - method: 'DELETE', - }) -} diff --git a/src/api/Message.ts b/src/api/Message.ts new file mode 100644 index 00000000..9c92f412 --- /dev/null +++ b/src/api/Message.ts @@ -0,0 +1,56 @@ +import { ApiUrl, Response } from './Api' +import AuthSingleton from '../components/AuthService' + +const Auth = AuthSingleton.getInstance() + +interface MessagesResponseData { + messages: Array +} + +interface MessageResponseData { + message: Message +} + +export interface Message { + id: number + ticker: number + text: string + creation_date: Date + tweet_id: string + tweet_user: string + geo_information: string + // TODO + attachments: any[] +} + +export function getMessages( + ticker: number +): Promise> { + return Auth.fetch(`${ApiUrl}/admin/tickers/${ticker}/messages`) +} + +// TODO: any +export function postMessage( + ticker: string, + text: string, + geoInformation: any, + attachments: any[] +): Promise> { + return Auth.fetch(`${ApiUrl}/admin/tickers/${ticker}/messages`, { + body: JSON.stringify({ + text: text, + geo_information: geoInformation, + attachments: attachments, + }), + method: 'POST', + }) +} + +export function deleteMessage( + ticker: string, + message: string +): Promise> { + return Auth.fetch(`${ApiUrl}/admin/tickers/${ticker}/messages/${message}`, { + method: 'DELETE', + }) +} diff --git a/src/components/Message.js b/src/components/Message.js deleted file mode 100644 index 6d4fc96b..00000000 --- a/src/components/Message.js +++ /dev/null @@ -1,194 +0,0 @@ -import React from 'react' -import { Card, Confirm, Icon, Image } from 'semantic-ui-react' -import { Map, TileLayer, GeoJSON } from 'react-leaflet' -import PropTypes from 'prop-types' -import Moment from 'react-moment' -import { deleteMessage } from '../api/Message' -import Lightbox from 'react-image-lightbox' -import 'react-image-lightbox/style.css' - -export default class Message extends React.Component { - constructor(props) { - super(props) - - this.state = { - showDeleteConfirm: false, - imageLightboxOpen: false, - imageIndex: 0, - } - } - - _replaceMagic(text) { - return text - .replace(/(https?:\/\/([^\s]+))/g, '$2') - .replace( - /#(\S+)/g, - '#$1' - ) - .replace( - / @(\S+)/g, - ' @$1' - ) - .replace(/(\w+@\w+.\w+)/g, '$1') - .replace(/(?:\r\n|\r|\n)/g, '
') - } - - deleteMessage() { - deleteMessage( - this.props.message.ticker.toString(), - this.props.message.id.toString() - ).then(() => { - this.props.loadMessages() - }) - } - - hasGeoInformation() { - let geoInformation = JSON.parse(this.props.message.geo_information) - - if (typeof geoInformation.features === 'undefined') { - return false - } - - return geoInformation.features.length >= 1 - } - - onGeoInformationAdded(event) { - const leafletLayer = event.target - const features = Object.values(leafletLayer._layers) - - if ( - features.length === 1 && - features[0].feature.geometry.type === 'Point' - ) { - const coords = features[0].feature.geometry.coordinates - leafletLayer._map.setView([coords[1], coords[0]], 13) - } else { - leafletLayer._map.fitBounds(leafletLayer.getBounds()) - } - } - - renderMap() { - if (!this.hasGeoInformation()) { - return null - } - - return ( - - - - - ) - } - - renderAttachments() { - const attachments = this.props.message.attachments - const { imageLightboxOpen, imageIndex } = this.state - - if (attachments === null || attachments.length === 0) { - return null - } - - const images = attachments.map((image, key) => ( - - this.setState({ imageLightboxOpen: true, imageIndex: key }) - } - rounded - src={image.url} - style={{ width: 200, height: 200, objectFit: 'cover' }} - /> - )) - const urls = attachments.map(image => image.url) - - return ( - - {imageLightboxOpen && ( - this.setState({ imageLightboxOpen: false })} - onMoveNextRequest={() => - this.setState({ - imageIndex: (imageIndex + 1) % urls.length, - }) - } - onMovePrevRequest={() => - this.setState({ - imageIndex: (imageIndex + urls.length - 1) % urls.length, - }) - } - prevSrc={images[(imageIndex + images.length - 1) % images.length]} - /> - )} - {images} - - ) - } - - renderTwitterIcon() { - if (this.props.message.tweet_id === '') { - return - } - - return ( - - - - ) - } - - render() { - return ( - - - this.setState({ showDeleteConfirm: true })} - style={{ float: 'right' }} - /> - this.setState({ showDeleteConfirm: false })} - onConfirm={() => this.deleteMessage()} - open={this.state.showDeleteConfirm} - /> -

- - {this.renderAttachments()} - {this.renderMap()} - - {this.renderTwitterIcon()} - {this.props.message.creation_date} - - - ) - } -} - -Message.propTypes = { - message: PropTypes.shape({ - id: PropTypes.number.isRequired, - ticker: PropTypes.number.isRequired, - text: PropTypes.string.isRequired, - creation_date: PropTypes.string.isRequired, - tweet_id: PropTypes.string, - tweet_user: PropTypes.string, - geo_information: PropTypes.string, - attachments: PropTypes.array, - }), - loadMessages: PropTypes.func.isRequired, -} diff --git a/src/components/Message.tsx b/src/components/Message.tsx new file mode 100644 index 00000000..71c99dd1 --- /dev/null +++ b/src/components/Message.tsx @@ -0,0 +1,152 @@ +import React, { FC, useCallback, useState } from 'react' +import { Card, Icon, Image } from 'semantic-ui-react' +import { MapContainer, TileLayer, GeoJSON } from 'react-leaflet' +import Moment from 'react-moment' +import { Message as MessageType } from '../api/Message' +import Lightbox from 'react-image-lightbox' +import 'react-image-lightbox/style.css' +import { replaceMagic } from '../lib/helper' +import MessageModalDelete from './MessageModalDelete' + +interface Props { + message: MessageType +} + +const Message: FC = ({ message }) => { + const [imageLightboxOpen, setImageLightboxOpen] = useState(false) + const [imageIndex, setImageIndex] = useState(0) + + const openImageLightbox = useCallback(() => setImageLightboxOpen(true), []) + const closeImageLightbox = useCallback(() => setImageLightboxOpen(false), []) + + const hasGeoInformation = () => { + const geoInformation = JSON.parse(message.geo_information) + + if (typeof geoInformation.features === 'undefined') { + return false + } + + return geoInformation.features.length >= 1 + } + + // const onGeoInformationAdded = event => { + // const leafletLayer = event.target + // const features = Object.values(leafletLayer._layers) + + // if ( + // features.length === 1 && + // features[0].feature.geometry.type === 'Point' + // ) { + // const coords = features[0].feature.geometry.coordinates + // leafletLayer._map.setView([coords[1], coords[0]], 13) + // } else { + // leafletLayer._map.fitBounds(leafletLayer.getBounds()) + // } + // } + + const renderMap = () => { + if (!hasGeoInformation()) { + return null + } + + return ( + + + + + ) + } + + 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) + } + /> + )} + {images} + + ) + } + + const renderTwitterIcon = () => { + if (message.tweet_id === '') { + return + } + + return ( + + + + ) + } + + return ( + + + + } + /> +

+ + {renderAttachments()} + {renderMap()} + + {renderTwitterIcon()} + {message.creation_date} + + + ) +} + +export default Message diff --git a/src/components/MessageForm.js b/src/components/MessageForm.js deleted file mode 100644 index 18ed5b47..00000000 --- a/src/components/MessageForm.js +++ /dev/null @@ -1,269 +0,0 @@ -import React from 'react' -import { postMessage } from '../api/Message' -import PropTypes from 'prop-types' -import { - Button, - Form, - Image, - Label, - Loader, - Message as Error, -} from 'semantic-ui-react' -import { postUpload } from '../api/Upload' -import EditMapModal from './EditMapModal' - -const initialState = { - message: '', - counter: 0, - counterColor: 'green', - geoInformation: {}, - attachments: [], - attachmentPreviews: 0, - showEditMapModal: false, - formError: false, - formErrorMessage: '', -} - -const MESSAGE_LIMIT = 280 - -export default class MessageForm extends React.Component { - constructor(props) { - super(props) - - this.fileInputRef = React.createRef() - - this.state = initialState - } - - handleInput(event, input) { - let color - let errorMessage = '' - let error = false - - //TODO: Calculate length for Twitter (cutting links to 20 characters) - if (input.value.length > MESSAGE_LIMIT) { - color = 'red' - errorMessage = `The message is too long. You must remove ${ - input.value.length - MESSAGE_LIMIT - } characters.` - error = true - } else if (input.value.length >= 260) { - color = 'orange' - } else if (input.value.length >= 220) { - color = 'yellow' - } else { - color = 'green' - } - - this.setState({ - message: input.value, - counter: input.value.length, - counterColor: color, - formError: error, - formErrorMessage: errorMessage, - }) - } - - submitMessage() { - let message = this.state.message, - id = this.props.ticker.id, - geoInformation = this.state.geoInformation, - attachments = [] - const { length } = message - if (length === 0 || length > MESSAGE_LIMIT) { - return - } - - if (this.state.attachments.length > 0) { - attachments = this.state.attachments.map(attachment => attachment.id) - } - - postMessage(id, message, geoInformation, attachments).then(response => { - if (response.data !== undefined && response.data.message !== undefined) { - this.setState(initialState) - this.props.callback() - } - }) - } - - uploadAttachment(e) { - e.preventDefault() - - const formData = new FormData() - for (let i = 0; i < e.target.files.length; i++) { - formData.append('files', e.target.files[i]) - } - this.setState({ attachmentPreviews: e.target.files.length }) - formData.append('ticker', this.props.ticker.id) - - postUpload(formData).then(response => { - const attachments = this.state.attachments - this.setState({ - attachments: attachments.concat(response.data.uploads), - attachmentPreviews: 0, - }) - }) - } - - renderAttachmentPreviews() { - if (this.state.attachmentPreviews === 0) { - return null - } - - let images = [] - - for (let i = 0; i < this.state.attachmentPreviews; i++) { - images[i] = ( -

- -
- ) - } - - return
{images}
- } - - renderAttachments() { - let attachments = this.state.attachments - - if (attachments.length === 0) { - return null - } - - const images = attachments.map((image, key) => ( -
- -
- )) - - return {images} - } - - renderEditMapModal() { - let position = [52, 12] - - if (undefined !== this.props.ticker.location) { - position = [ - this.props.ticker.location.lat, - this.props.ticker.location.lon, - ] - } - - return ( - this.setState({ showEditMapModal: false })} - onSubmit={geoInformation => - this.setState({ - showEditMapModal: false, - geoInformation: geoInformation, - }) - } - open={this.state.showEditMapModal} - position={position} - /> - ) - } - - render() { - const state = this.state - - return ( -
- {this.renderEditMapModal()} - - - - - - -