From 6894f574b80e644f6f25cb1c11f316e7b2d1524e Mon Sep 17 00:00:00 2001 From: louis <louis@systemli.org> Date: Sun, 27 Mar 2022 10:49:31 +0200 Subject: [PATCH] :art: Change indent to 2 --- .editorconfig | 3 - .eslintrc.json | 73 +++++---- .github/release-drafter.yml | 60 +++---- .github/workflows/integration.yaml | 2 +- .prettierrc.json | 2 +- jest.config.ts | 32 ++-- package.json | 198 +++++++++++------------ postcss.config.js | 22 +-- src/App.test.tsx | 180 ++++++++++----------- src/App.tsx | 150 ++++++++--------- src/__mocks__/react-markdown.js | 2 +- src/components/About.tsx | 142 ++++++++-------- src/components/Attachments.tsx | 143 ++++++++--------- src/components/Credits.tsx | 28 ++-- src/components/DescriptionItem.tsx | 120 +++++++------- src/components/Map.tsx | 118 +++++++------- src/components/Message.tsx | 82 +++++----- src/components/MessageList.test.tsx | 32 ++-- src/components/MessageList.tsx | 240 ++++++++++++++-------------- src/components/ReloadInfo.tsx | 36 ++--- src/components/UpdateMessage.tsx | 44 ++--- src/leaflet.config.js | 6 +- src/lib/api.ts | 50 +++--- src/lib/helper.ts | 50 +++--- src/lib/theme.ts | 8 +- src/lib/types.ts | 76 ++++----- src/views/ActiveView.tsx | 94 ++++++----- src/views/ErrorView.tsx | 38 ++--- src/views/InactiveView.tsx | 124 +++++++------- tsconfig.json | 48 +++--- webpack.common.config.ts | 96 +++++------ webpack.dev.config.ts | 26 +-- webpack.prod.config.ts | 14 +- 33 files changed, 1159 insertions(+), 1180 deletions(-) diff --git a/.editorconfig b/.editorconfig index cc664cd..124645f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,9 +6,6 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true indent_style = space -indent_size = 4 - -[*.yaml] indent_size = 2 [*.md] diff --git a/.eslintrc.json b/.eslintrc.json index 9cbb687..60150f6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,37 +1,44 @@ { - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:react-hooks/recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended" + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + }, + "plugins": [ + "react", + "react-hooks", + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "warn", + "no-console": "warn", + "react/jsx-boolean-value": [ + "warn", + "never" + ], + "react/jsx-no-bind": "warn", + "react/jsx-sort-props": [ + "warn", + { + "reservedFirst": true + } ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - } - }, - "plugins": ["react", "react-hooks", "@typescript-eslint"], - "rules": { - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "warn", - "no-console": "warn", - "react/jsx-boolean-value": ["warn", "never"], - "react/jsx-no-bind": "warn", - "react/jsx-sort-props": [ - "warn", - { - "reservedFirst": true - } - ], - "react/prop-types": "off", - "react/react-in-jsx-scope": "off" - }, - "settings": { - "react": { - "version": "detect" - } + "react/prop-types": "off", + "react/react-in-jsx-scope": "off" + }, + "settings": { + "react": { + "version": "detect" } + } } diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 04156bc..1270a61 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,37 +1,37 @@ name-template: '$RESOLVED_VERSION' tag-template: '$RESOLVED_VERSION' categories: - - title: '๐ Features' - labels: - - 'feature' - - 'enhancement' - - title: '๐ Bug Fixes' - labels: - - 'fix' - - 'bugfix' - - 'bug' - - title: '๐งน Maintenance' - labels: - - 'chore' - - 'dependencies' + - title: '๐ Features' + labels: + - 'feature' + - 'enhancement' + - title: '๐ Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '๐งน Maintenance' + labels: + - 'chore' + - 'dependencies' version-resolver: - major: - labels: - - 'feature' - minor: - labels: - - 'enhancement' - patch: - labels: - - 'fix' - - 'bugfix' - - 'bug' - - 'chore' - - 'dependencies' - default: patch + major: + labels: + - 'feature' + minor: + labels: + - 'enhancement' + patch: + labels: + - 'fix' + - 'bugfix' + - 'bug' + - 'chore' + - 'dependencies' + default: patch template: | - ## Changes + ## Changes - $CHANGES + $CHANGES - **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...$RESOLVED_VERSION + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...$RESOLVED_VERSION diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 1716b7a..ead9385 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -34,7 +34,7 @@ jobs: build: name: Build runs-on: ubuntu-20.04 - needs: [test] + needs: [ test ] strategy: matrix: node-version: [ '14', '16' ] diff --git a/.prettierrc.json b/.prettierrc.json index 6db2173..ad53c05 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,5 +2,5 @@ "arrowParens": "avoid", "semi": false, "singleQuote": true, - "tabWidth": 4 + "tabWidth": 2 } diff --git a/jest.config.ts b/jest.config.ts index 21b4e2d..544a256 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,21 +1,21 @@ import type { Config } from '@jest/types' const config: Config.InitialOptions = { - verbose: false, - testEnvironment: 'jsdom', - preset: 'ts-jest', - moduleNameMapper: { - 'react-markdown': '<rootDir>/src/__mocks_/react-markdown.js', - '^.+.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': - 'jest-transform-stub', - }, - transform: { - '^.+\\.(ts|tsx)$': 'ts-jest', - '^.+\\.(js|jsx)$': 'babel-jest', - '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': - 'jest-transform-stub', - }, - transformIgnorePatterns: ['<rootDir>/node_modules/(?!react-markdown/)'], - setupFilesAfterEnv: ['./jest-setup.ts'], + verbose: false, + testEnvironment: 'jsdom', + preset: 'ts-jest', + moduleNameMapper: { + 'react-markdown': '<rootDir>/src/__mocks_/react-markdown.js', + '^.+.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': + 'jest-transform-stub', + }, + transform: { + '^.+\\.(ts|tsx)$': 'ts-jest', + '^.+\\.(js|jsx)$': 'babel-jest', + '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': + 'jest-transform-stub', + }, + transformIgnorePatterns: ['<rootDir>/node_modules/(?!react-markdown/)'], + setupFilesAfterEnv: ['./jest-setup.ts'], } export default config diff --git a/package.json b/package.json index 0ce7bee..67c0d45 100644 --- a/package.json +++ b/package.json @@ -1,105 +1,99 @@ { - "name": "ticker-frontend", - "private": true, - "dependencies": { - "@semantic-ui-react/css-patch": "^1.0.0", - "@types/leaflet": "^1.7.0", - "@types/react": "^17.0.40", - "@types/styled-components": "^5.1.24", - "dayjs": "^1.11.0", - "leaflet": "^1.7.1", - "pure-react-carousel": "^1.27.6", - "react": "^17.0.2", - "react-app-polyfill": "^3.0.0", - "react-leaflet": "^3.1.0", - "react-markdown": "^8.0.1", - "react-refresh": "^0.8.3", - "semantic-ui-css": "^2.4.1", - "semantic-ui-react": "^2.1.2", - "styled-components": "^5.2.3", - "typescript": "^4.6.2" + "name": "ticker-frontend", + "private": true, + "scripts": { + "start": "webpack serve --config webpack.dev.config.ts", + "build": "webpack --config webpack.prod.config.ts", + "test": "jest", + "lint": "eslint --ext=ts,tsx src", + "postinstall": "semantic-ui-css-patch" + }, + "dependencies": { + "@semantic-ui-react/css-patch": "^1.0.0", + "@types/leaflet": "^1.7.0", + "@types/react": "^17.0.40", + "@types/styled-components": "^5.1.24", + "dayjs": "^1.11.0", + "leaflet": "^1.7.1", + "pure-react-carousel": "^1.27.6", + "react": "^17.0.2", + "react-app-polyfill": "^3.0.0", + "react-leaflet": "^3.1.0", + "react-markdown": "^8.0.1", + "react-refresh": "^0.8.3", + "semantic-ui-css": "^2.4.1", + "semantic-ui-react": "^2.1.2", + "styled-components": "^5.2.3", + "typescript": "^4.6.2" + }, + "devDependencies": { + "@babel/core": "^7.17.7", + "@babel/plugin-transform-runtime": "^7.17.0", + "@babel/preset-env": "^7.16.11", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.16.7", + "@testing-library/dom": "^8.11.3", + "@testing-library/jest-dom": "^5.16.2", + "@testing-library/react": "^12.1.3", + "@types/html-webpack-plugin": "^3.2.6", + "@types/jest": "^27.4.1", + "@types/node": "^17.0.21", + "@types/react-dom": "^17.0.13", + "@typescript-eslint/eslint-plugin": "^4.5.0", + "@typescript-eslint/parser": "^4.22.0", + "babel-eslint": "^10.1.0", + "babel-jest": "^26.6.0", + "babel-loader": "^8.2.3", + "babel-plugin-named-asset-import": "^0.3.7", + "babel-plugin-styled-components": "^2.0.6", + "babel-preset-react-app": "^10.0.0", + "css-loader": "^6.7.1", + "cssnano": "^5.1.4", + "dotenv": "8.2.0", + "dotenv-expand": "5.1.0", + "dotenv-webpack": "^7.1.0", + "eslint": "^7.29.0", + "eslint-config-prettier": "^8.5.0", + "eslint-config-react-app": "^6.0.0", + "eslint-plugin-flowtype": "^5.2.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jest": "^24.1.0", + "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "^7.29.3", + "eslint-plugin-react-hooks": "^4.2.0", + "eslint-plugin-testing-library": "^3.9.2", + "eslint-webpack-plugin": "^2.5.2", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^27.5.1", + "jest-dom": "^4.0.0", + "jest-transform-stub": "^2.0.0", + "postcss": "^8.4.12", + "postcss-loader": "^6.2.1", + "postcss-remove-google-fonts": "^1.1.9", + "prettier": "^2.6.0", + "react-dev-utils": "^12.0.0", + "react-dom": "^17.0.2", + "resolve": "^1.22.0", + "resolve-url-loader": "^5.0.0", + "semver": "^7.3.5", + "style-loader": "^3.3.1", + "terser-webpack-plugin": "^5.3.1", + "ts-jest": "^27.1.3", + "ts-loader": "^9.2.8", + "ts-node": "^10.7.0", + "ts-pnp": "^1.2.0", + "webpack": "^5.70.0", + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.7.4", + "webpack-manifest-plugin": "^5.0.0", + "webpack-merge": "^5.8.0" }, - "devDependencies": { - "@babel/core": "^7.17.7", - "@babel/plugin-transform-runtime": "^7.17.0", - "@babel/preset-env": "^7.16.11", - "@babel/preset-react": "^7.16.7", - "@babel/preset-typescript": "^7.16.7", - "@testing-library/dom": "^8.11.3", - "@testing-library/jest-dom": "^5.16.2", - "@testing-library/react": "^12.1.3", - "@types/html-webpack-plugin": "^3.2.6", - "@types/jest": "^27.4.1", - "@types/node": "^17.0.21", - "@types/react-dom": "^17.0.13", - "@typescript-eslint/eslint-plugin": "^4.5.0", - "@typescript-eslint/parser": "^4.22.0", - "babel-eslint": "^10.1.0", - "babel-jest": "^26.6.0", - "babel-loader": "^8.2.3", - "babel-plugin-named-asset-import": "^0.3.7", - "babel-plugin-styled-components": "^2.0.6", - "babel-preset-react-app": "^10.0.0", - "css-loader": "^6.7.1", - "cssnano": "^5.1.4", - "dotenv": "8.2.0", - "dotenv-expand": "5.1.0", - "dotenv-webpack": "^7.1.0", - "eslint": "^7.29.0", - "eslint-config-prettier": "^8.5.0", - "eslint-config-react-app": "^6.0.0", - "eslint-plugin-flowtype": "^5.2.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jest": "^24.1.0", - "eslint-plugin-jsx-a11y": "^6.3.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.3", - "eslint-plugin-react-hooks": "^4.2.0", - "eslint-plugin-testing-library": "^3.9.2", - "eslint-webpack-plugin": "^2.5.2", - "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.5.0", - "identity-obj-proxy": "^3.0.0", - "jest": "^27.5.1", - "jest-dom": "^4.0.0", - "jest-transform-stub": "^2.0.0", - "postcss": "^8.4.12", - "postcss-loader": "^6.2.1", - "postcss-remove-google-fonts": "^1.1.9", - "prettier": "^2.6.0", - "react-dev-utils": "^12.0.0", - "react-dom": "^17.0.2", - "resolve": "^1.22.0", - "resolve-url-loader": "^5.0.0", - "semver": "^7.3.5", - "style-loader": "^3.3.1", - "terser-webpack-plugin": "^5.3.1", - "ts-jest": "^27.1.3", - "ts-loader": "^9.2.8", - "ts-node": "^10.7.0", - "ts-pnp": "^1.2.0", - "webpack": "^5.70.0", - "webpack-cli": "^4.9.2", - "webpack-dev-server": "^4.7.4", - "webpack-manifest-plugin": "^5.0.0", - "webpack-merge": "^5.8.0" - }, - "scripts": { - "start": "webpack serve --config webpack.dev.config.ts", - "build": "webpack --config webpack.prod.config.ts", - "test": "jest", - "lint": "eslint --ext=ts,tsx src", - "postinstall": "semantic-ui-css-patch" - }, - "coverageReporters": [ - "clover", - "json", - "lcov", - "text" - ], - "browserslist": [ - ">0.2%", - "not dead", - "not op_mini all" - ] + "browserslist": [ + ">0.2%", + "not dead", + "not op_mini all" + ] } diff --git a/postcss.config.js b/postcss.config.js index 3fd9dc8..417b2e7 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,15 +1,15 @@ module.exports = { - plugins: { - cssnano: { - preset: [ - 'default', - { - discardComments: { - removeAll: true, - }, - }, - ], + plugins: { + cssnano: { + preset: [ + 'default', + { + discardComments: { + removeAll: true, + }, }, - 'postcss-remove-google-fonts': {}, + ], }, + 'postcss-remove-google-fonts': {}, + }, } diff --git a/src/App.test.tsx b/src/App.test.tsx index af11f28..6fd6c6a 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -5,108 +5,110 @@ import * as api from './lib/api' import { Settings, Ticker } from './lib/types' describe('App', function () { - const initSettings = { - refresh_interval: 1000, - inactive_settings: { - author: 'Systemli Ticker Team', - email: 'admin@systemli.org', - homepage: '', - twitter: '', - headline: 'The ticker is currently inactive.', - sub_headline: 'Please contact us if you want to use it.', - description: '...', - }, - } as Settings - const ticker = { - id: '1', - active: true, - creation_date: new Date(), - title: 'Ticker Title', - description: 'Ticker Description', - domain: 'example.com', - information: { - author: 'Systemli Ticker Team', - url: 'https://demoticker.org', - email: 'admin@demoticker.org', - twitter: 'systemli', - facebook: 'betternot', - }, - } as Ticker + const initSettings = { + refresh_interval: 1000, + inactive_settings: { + author: 'Systemli Ticker Team', + email: 'admin@systemli.org', + homepage: '', + twitter: '', + headline: 'The ticker is currently inactive.', + sub_headline: 'Please contact us if you want to use it.', + description: '...', + }, + } as Settings + const ticker = { + id: '1', + active: true, + creation_date: new Date(), + title: 'Ticker Title', + description: 'Ticker Description', + domain: 'example.com', + information: { + author: 'Systemli Ticker Team', + url: 'https://demoticker.org', + email: 'admin@demoticker.org', + twitter: 'systemli', + facebook: 'betternot', + }, + } as Ticker - test('renders OfflineView', async function () { - jest.spyOn(api, 'getInit').mockRejectedValue(new TypeError()) - render(<App />) + test('renders OfflineView', async function () { + jest.spyOn(api, 'getInit').mockRejectedValue(new TypeError()) + render(<App />) - expect(screen.getByText('Loading')).toBeInTheDocument() + expect(screen.getByText('Loading')).toBeInTheDocument() - expect( - await screen.findByText('It seems that you are offline.') - ).toBeInTheDocument() - }) + expect( + await screen.findByText('It seems that you are offline.') + ).toBeInTheDocument() + }) - test('renders ErrorView', async function () { - jest.spyOn(api, 'getInit').mockRejectedValue( - new Error( - 'The server responses with an error: Internal Server Error (500)' - ) + test('renders ErrorView', async function () { + jest + .spyOn(api, 'getInit') + .mockRejectedValue( + new Error( + 'The server responses with an error: Internal Server Error (500)' ) - render(<App />) + ) + render(<App />) + + expect(screen.getByText('Loading')).toBeInTheDocument() - expect(screen.getByText('Loading')).toBeInTheDocument() + expect( + await screen.findByText( + 'There seems to be a problem connecting to the server.' + ) + ).toBeInTheDocument() + }) - expect( - await screen.findByText( - 'There seems to be a problem connecting to the server.' - ) - ).toBeInTheDocument() + test('renders InactiveView', async function () { + jest.spyOn(api, 'getInit').mockResolvedValue({ + data: { + settings: initSettings, + ticker: null, + }, }) + render(<App />) - test('renders InactiveView', async function () { - jest.spyOn(api, 'getInit').mockResolvedValue({ - data: { - settings: initSettings, - ticker: null, - }, - }) - render(<App />) + expect(screen.getByText('Loading')).toBeInTheDocument() - expect(screen.getByText('Loading')).toBeInTheDocument() + expect( + await screen.findByText('The ticker is currently inactive.') + ).toBeInTheDocument() + }) - expect( - await screen.findByText('The ticker is currently inactive.') - ).toBeInTheDocument() + test('renders ActiveView', async function () { + jest.spyOn(api, 'getInit').mockResolvedValue({ + data: { + settings: initSettings, + ticker: ticker, + }, }) + jest.spyOn(api, 'getTimeline').mockResolvedValue({ + data: { + messages: [], + }, + }) + const intersectionObserverMock = () => ({ + observe: () => null, + }) + window.IntersectionObserver = jest + .fn() + .mockImplementation(intersectionObserverMock) + render(<App />) - test('renders ActiveView', async function () { - jest.spyOn(api, 'getInit').mockResolvedValue({ - data: { - settings: initSettings, - ticker: ticker, - }, - }) - jest.spyOn(api, 'getTimeline').mockResolvedValue({ - data: { - messages: [], - }, - }) - const intersectionObserverMock = () => ({ - observe: () => null, - }) - window.IntersectionObserver = jest - .fn() - .mockImplementation(intersectionObserverMock) - render(<App />) - - expect(screen.getByText('Loading')).toBeInTheDocument() + expect(screen.getByText('Loading')).toBeInTheDocument() - expect( - await screen.findByText( - 'The messages update automatically. There is no need to reload the entire page.' - ) - ).toBeInTheDocument() + expect( + await screen.findByText( + 'The messages update automatically. There is no need to reload the entire page.' + ) + ).toBeInTheDocument() - expect( - screen.getByText('We dont have any messages at the moment.') - ).toBeInTheDocument() - }) + expect( + screen.getByText('We dont have any messages at the moment.') + ).toBeInTheDocument() + }) }) diff --git a/src/App.tsx b/src/App.tsx index 7162cfc..68871d9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,93 +6,93 @@ import { ActiveView, ErrorView, InactiveView } from './views' import { getInit } from './lib/api' const App: FC = () => { - const [ticker, setTicker] = useState<Ticker | null>(null) - const [settings, setSettings] = useState<Settings>() - const [isLoading, setIsLoading] = useState<boolean>(true) - const [isOffline, setIsOffline] = useState<boolean>(false) - const [gotError, setGotError] = useState<boolean>(false) + const [ticker, setTicker] = useState<Ticker | null>(null) + const [settings, setSettings] = useState<Settings>() + const [isLoading, setIsLoading] = useState<boolean>(true) + const [isOffline, setIsOffline] = useState<boolean>(false) + const [gotError, setGotError] = useState<boolean>(false) - // TODO: install and configure offline plugin - // const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false) - const isUpdateAvailable = false + // TODO: install and configure offline plugin + // const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false) + const isUpdateAvailable = false - // OfflinePluginRuntime.install({ - // onUpdateReady: () => OfflinePluginRuntime.applyUpdate(), - // onUpdated: () => setIsUpdateAvailable(true), - // }) + // OfflinePluginRuntime.install({ + // onUpdateReady: () => OfflinePluginRuntime.applyUpdate(), + // onUpdated: () => setIsUpdateAvailable(true), + // }) - const fetchInit = () => { - getInit() - .then(response => { - if (response.data.settings) { - setSettings(response.data.settings) - } + const fetchInit = () => { + getInit() + .then(response => { + if (response.data.settings) { + setSettings(response.data.settings) + } - if (response.data.ticker?.active) { - setTicker(response.data.ticker) - if (ticker?.title) { - document.title = ticker.title - } - } + if (response.data.ticker?.active) { + setTicker(response.data.ticker) + if (ticker?.title) { + document.title = ticker.title + } + } - setIsLoading(false) - }) - .catch(error => { - if (error instanceof TypeError) { - setIsOffline(true) - } else { - setGotError(true) - } - setIsLoading(false) - }) - } + setIsLoading(false) + }) + .catch(error => { + if (error instanceof TypeError) { + setIsOffline(true) + } else { + setGotError(true) + } + setIsLoading(false) + }) + } - useEffect(() => { - fetchInit() - // This should only be executed once on load (~ componentDidMount) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + useEffect(() => { + fetchInit() + // This should only be executed once on load (~ componentDidMount) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - if (isLoading) { - return ( - <Container> - <Dimmer active> - <Loader content="Loading" size="large" /> - </Dimmer> - </Container> - ) - } + if (isLoading) { + return ( + <Container> + <Dimmer active> + <Loader content="Loading" size="large" /> + </Dimmer> + </Container> + ) + } - if (gotError) { - return ( - <ErrorView message="There seems to be a problem connecting to the server." /> - ) - } + if (gotError) { + return ( + <ErrorView message="There seems to be a problem connecting to the server." /> + ) + } - if (isOffline) { - return <ErrorView message="It seems that you are offline." /> - } + if (isOffline) { + return <ErrorView message="It seems that you are offline." /> + } - if (ticker?.active) { - return ( - <ActiveView - refreshInterval={settings?.refresh_interval || 0} - ticker={ticker} - update={isUpdateAvailable} - /> - ) - } + if (ticker?.active) { + return ( + <ActiveView + refreshInterval={settings?.refresh_interval || 0} + ticker={ticker} + update={isUpdateAvailable} + /> + ) + } - if (ticker === null && settings?.inactive_settings !== undefined) { - return ( - <InactiveView - settings={settings.inactive_settings} - update={isUpdateAvailable} - /> - ) - } + if (ticker === null && settings?.inactive_settings !== undefined) { + return ( + <InactiveView + settings={settings.inactive_settings} + update={isUpdateAvailable} + /> + ) + } - return <div>...</div> + return <div>...</div> } export default App diff --git a/src/__mocks__/react-markdown.js b/src/__mocks__/react-markdown.js index f0484ec..3f57155 100644 --- a/src/__mocks__/react-markdown.js +++ b/src/__mocks__/react-markdown.js @@ -1,4 +1,4 @@ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export default function ReactMarkdown({ children }) { - return <>{children}</> + return <>{children}</> } diff --git a/src/components/About.tsx b/src/components/About.tsx index 4affda7..b00f282 100644 --- a/src/components/About.tsx +++ b/src/components/About.tsx @@ -6,83 +6,83 @@ import DescriptionItem from './DescriptionItem' import { DescriptionTypes, Ticker } from '../lib/types' interface Props { - ticker: Ticker - isModal?: boolean + ticker: Ticker + isModal?: boolean } const About: FC<Props> = props => { - const renderDescriptionList = () => ( - <List> - {props.ticker.information.author && ( - <DescriptionItem - info={props.ticker.information.author} - type={DescriptionTypes.Author} - /> - )} - {props.ticker.information.email && ( - <DescriptionItem - info={props.ticker.information.email} - type={DescriptionTypes.Email} - /> - )} - {props.ticker.information.url && ( - <DescriptionItem - info={props.ticker.information.url} - type={DescriptionTypes.Homepage} - /> - )} - {props.ticker.information.twitter && ( - <DescriptionItem - info={props.ticker.information.twitter} - type={DescriptionTypes.Twitter} - /> - )} - {props.ticker.information.facebook && ( - <DescriptionItem - info={props.ticker.information.facebook} - type={DescriptionTypes.Facebook} - /> - )} - </List> - ) - - if (props.isModal) { - return ( - <Modal - closeIcon - dimmer={'blurring'} - trigger={ - <Button circular color={'blue'} floated={'right'} icon> - <Icon name={'info'} /> - </Button> - } - > - <Modal.Header>About</Modal.Header> - <Modal.Content> - <ReactMarkdown>{props.ticker.description}</ReactMarkdown> - </Modal.Content> - <Modal.Content> - {renderDescriptionList()} - <Credits /> - </Modal.Content> - </Modal> - ) - } + const renderDescriptionList = () => ( + <List> + {props.ticker.information.author && ( + <DescriptionItem + info={props.ticker.information.author} + type={DescriptionTypes.Author} + /> + )} + {props.ticker.information.email && ( + <DescriptionItem + info={props.ticker.information.email} + type={DescriptionTypes.Email} + /> + )} + {props.ticker.information.url && ( + <DescriptionItem + info={props.ticker.information.url} + type={DescriptionTypes.Homepage} + /> + )} + {props.ticker.information.twitter && ( + <DescriptionItem + info={props.ticker.information.twitter} + type={DescriptionTypes.Twitter} + /> + )} + {props.ticker.information.facebook && ( + <DescriptionItem + info={props.ticker.information.facebook} + type={DescriptionTypes.Facebook} + /> + )} + </List> + ) + if (props.isModal) { return ( - <div> - <Card fluid> - <Card.Content> - <Card.Header>About</Card.Header> - </Card.Content> - <Card.Content> - <ReactMarkdown>{props.ticker.description}</ReactMarkdown> - </Card.Content> - <Card.Content>{renderDescriptionList()}</Card.Content> - </Card> - <Credits /> - </div> + <Modal + closeIcon + dimmer={'blurring'} + trigger={ + <Button circular color={'blue'} floated={'right'} icon> + <Icon name={'info'} /> + </Button> + } + > + <Modal.Header>About</Modal.Header> + <Modal.Content> + <ReactMarkdown>{props.ticker.description}</ReactMarkdown> + </Modal.Content> + <Modal.Content> + {renderDescriptionList()} + <Credits /> + </Modal.Content> + </Modal> ) + } + + return ( + <div> + <Card fluid> + <Card.Content> + <Card.Header>About</Card.Header> + </Card.Content> + <Card.Content> + <ReactMarkdown>{props.ticker.description}</ReactMarkdown> + </Card.Content> + <Card.Content>{renderDescriptionList()}</Card.Content> + </Card> + <Credits /> + </div> + ) } export default About diff --git a/src/components/Attachments.tsx b/src/components/Attachments.tsx index aba46f5..11afd3c 100644 --- a/src/components/Attachments.tsx +++ b/src/components/Attachments.tsx @@ -1,12 +1,12 @@ import { FC } from 'react' import { - ButtonBack, - ButtonNext, - CarouselProvider, - Dot, - Image, - Slide, - Slider, + ButtonBack, + ButtonNext, + CarouselProvider, + Dot, + Image, + Slide, + Slider, } from 'pure-react-carousel' import styled from 'styled-components' import { Attachment } from '../lib/types' @@ -14,87 +14,80 @@ import 'pure-react-carousel/dist/react-carousel.es.css' import { Button } from 'semantic-ui-react' const Wrapper = styled(CarouselProvider)` - position: relative; + position: relative; ` const DotWrapper = styled.div` - position: absolute; - bottom: 5px; - width: 100%; - display: block; - text-align: center; + position: absolute; + bottom: 5px; + width: 100%; + display: block; + text-align: center; ` interface Props { - attachments: Attachment[] + attachments: Attachment[] } const Attachments: FC<Props> = props => { - const renderButtonsAndDots = () => { - // FIXME: This doesn't feel like a good pattern. - // Tried to use styled components instead which didn't work right away. - const style = { - position: 'absolute', - bottom: '50%', - transform: 'translateY(50%)', - } - - return ( - <> - <DotWrapper> - <Button.Group size="mini"> - {[...Array(props.attachments.length).keys()].map( - slide => ( - <Button - key={slide} - as={Dot} - icon="circle" - slide={slide} - /> - ) - )} - </Button.Group> - </DotWrapper> - <Button - as={ButtonBack} - color="grey" - floated="left" - icon="arrow circle left" - size="large" - style={{ ...style, ...{ left: 5 } }} - /> - <Button - as={ButtonNext} - color="grey" - floated="right" - icon="arrow circle right" - size="large" - style={{ ...style, ...{ right: 5 } }} - /> - </> - ) + const renderButtonsAndDots = () => { + // FIXME: This doesn't feel like a good pattern. + // Tried to use styled components instead which didn't work right away. + const style = { + position: 'absolute', + bottom: '50%', + transform: 'translateY(50%)', } return ( - <Wrapper - naturalSlideHeight={0.75} - naturalSlideWidth={1} - totalSlides={props.attachments.length} - > - <Slider> - {props.attachments.map((image, key) => ( - <Slide key={key} index={key}> - <Image - hasMasterSpinner={false} - src={image.url} - style={{ objectFit: 'scale-down' }} - /> - </Slide> - ))} - </Slider> - {props.attachments.length > 1 && renderButtonsAndDots()} - </Wrapper> + <> + <DotWrapper> + <Button.Group size="mini"> + {[...Array(props.attachments.length).keys()].map(slide => ( + <Button key={slide} as={Dot} icon="circle" slide={slide} /> + ))} + </Button.Group> + </DotWrapper> + <Button + as={ButtonBack} + color="grey" + floated="left" + icon="arrow circle left" + size="large" + style={{ ...style, ...{ left: 5 } }} + /> + <Button + as={ButtonNext} + color="grey" + floated="right" + icon="arrow circle right" + size="large" + style={{ ...style, ...{ right: 5 } }} + /> + </> ) + } + + return ( + <Wrapper + naturalSlideHeight={0.75} + naturalSlideWidth={1} + totalSlides={props.attachments.length} + > + <Slider> + {props.attachments.map((image, key) => ( + <Slide key={key} index={key}> + <Image + hasMasterSpinner={false} + src={image.url} + style={{ objectFit: 'scale-down' }} + /> + </Slide> + ))} + </Slider> + {props.attachments.length > 1 && renderButtonsAndDots()} + </Wrapper> + ) } export default Attachments diff --git a/src/components/Credits.tsx b/src/components/Credits.tsx index 9d9f753..dc8a2c2 100644 --- a/src/components/Credits.tsx +++ b/src/components/Credits.tsx @@ -3,23 +3,23 @@ import { Icon } from 'semantic-ui-react' import styled from 'styled-components' const Wrapper = styled.div` - color: rgba(0, 0, 0, 0.5); - text-align: right; + color: rgba(0, 0, 0, 0.5); + text-align: right; ` const Credits: FC = () => { - return ( - <Wrapper> - <Icon name="code" /> with <Icon color="red" name="heart" /> by{' '} - <a - href="https://www.systemli.org" - rel="noopener noreferrer" - target="_blank" - > - systemli.org - </a> - </Wrapper> - ) + return ( + <Wrapper> + <Icon name="code" /> with <Icon color="red" name="heart" /> by{' '} + <a + href="https://www.systemli.org" + rel="noopener noreferrer" + target="_blank" + > + systemli.org + </a> + </Wrapper> + ) } export default Credits diff --git a/src/components/DescriptionItem.tsx b/src/components/DescriptionItem.tsx index 1b92d33..1ede22a 100644 --- a/src/components/DescriptionItem.tsx +++ b/src/components/DescriptionItem.tsx @@ -3,71 +3,69 @@ import { List } from 'semantic-ui-react' import { DescriptionTypes } from '../lib/types' interface Props { - info: string - type: DescriptionTypes + info: string + type: DescriptionTypes } const DescriptionItem: FC<Props> = props => { - switch (props.type) { - case DescriptionTypes.Author: - return ( - <List.Item> - <List.Icon name="users" /> - <List.Content>{props.info}</List.Content> - </List.Item> - ) - case DescriptionTypes.Email: - return ( - <List.Item> - <List.Icon name="mail" /> - <List.Content> - <a - href={`mailto: + switch (props.type) { + case DescriptionTypes.Author: + return ( + <List.Item> + <List.Icon name="users" /> + <List.Content>{props.info}</List.Content> + </List.Item> + ) + case DescriptionTypes.Email: + return ( + <List.Item> + <List.Icon name="mail" /> + <List.Content> + <a + href={`mailto: ${props.info}`} - > - {props.info} - </a> - </List.Content> - </List.Item> - ) - case DescriptionTypes.Facebook: - return ( - <List.Item> - <List.Icon name="facebook" /> - <List.Content> - <a - href={`https://fb.com/${props.info}`} - rel="noopener noreferrer" - target="_blank" - > - fb.com/{props.info} - </a> - </List.Content> - </List.Item> - ) - case DescriptionTypes.Homepage: - return ( - <List.Item> - <List.Icon name="linkify" /> - <List.Content> - <a href={props.info}>{props.info}</a> - </List.Content> - </List.Item> - ) - case DescriptionTypes.Twitter: - return ( - <List.Item> - <List.Icon name="twitter" /> - <List.Content> - <a href={`https://twitter.com/${props.info}`}> - @{props.info} - </a> - </List.Content> - </List.Item> - ) - default: - return null - } + > + {props.info} + </a> + </List.Content> + </List.Item> + ) + case DescriptionTypes.Facebook: + return ( + <List.Item> + <List.Icon name="facebook" /> + <List.Content> + <a + href={`https://fb.com/${props.info}`} + rel="noopener noreferrer" + target="_blank" + > + fb.com/{props.info} + </a> + </List.Content> + </List.Item> + ) + case DescriptionTypes.Homepage: + return ( + <List.Item> + <List.Icon name="linkify" /> + <List.Content> + <a href={props.info}>{props.info}</a> + </List.Content> + </List.Item> + ) + case DescriptionTypes.Twitter: + return ( + <List.Item> + <List.Icon name="twitter" /> + <List.Content> + <a href={`https://twitter.com/${props.info}`}>@{props.info}</a> + </List.Content> + </List.Item> + ) + default: + return null + } } export default DescriptionItem diff --git a/src/components/Map.tsx b/src/components/Map.tsx index f99d864..606a28a 100644 --- a/src/components/Map.tsx +++ b/src/components/Map.tsx @@ -1,5 +1,5 @@ -import { FC, useState, useCallback } from 'react' -import { MapContainer, GeoJSON, TileLayer } from 'react-leaflet' +import { FC, useCallback, useState } from 'react' +import { GeoJSON, MapContainer, TileLayer } from 'react-leaflet' import { Button } from 'semantic-ui-react' import styled from 'styled-components' import { LeafletEvent } from 'leaflet' @@ -7,84 +7,84 @@ import '../leaflet.config.js' import { zIndex } from '../lib/theme' const MapWrapper = styled(MapContainer)` - height: 100px; + height: 100px; ` const MapWrapperExpanded = styled(MapWrapper)` - height: 400px; + height: 400px; ` const ExpandButton = styled(Button)` - &.ui.icon.button { - position: absolute; - z-index: ${zIndex.expandButtonOnLeafletMap}; - top: 10px; - right: 5px; - padding: 7px; - background: #fff; - color: #000; - cursor: pointer; + &.ui.icon.button { + position: absolute; + z-index: ${zIndex.expandButtonOnLeafletMap}; + top: 10px; + right: 5px; + padding: 7px; + background: #fff; + color: #000; + cursor: pointer; - &:hover { - background: #f4f4f4; - } + &:hover { + background: #f4f4f4; } + } ` interface Props { - // Stringified GeoJSON.FeatureCollection - featureCollection: string + // Stringified GeoJSON.FeatureCollection + featureCollection: string } const Map: FC<Props> = props => { - const [mapExpanded, setMapExpanded] = useState<boolean>(false) - const featureCollection: GeoJSON.FeatureCollection = JSON.parse( - props.featureCollection - ) + const [mapExpanded, setMapExpanded] = useState<boolean>(false) + const featureCollection: GeoJSON.FeatureCollection = JSON.parse( + props.featureCollection + ) - const handleIconClick = useCallback(() => { - setMapExpanded(!mapExpanded) - }, [mapExpanded]) + const handleIconClick = useCallback(() => { + setMapExpanded(!mapExpanded) + }, [mapExpanded]) - if (featureCollection.features.length === 0) { - return null - } + if (featureCollection.features.length === 0) { + return null + } - const handleDataAdd = (event: LeafletEvent) => { - const leafletLayer = event.target - const features = Object.values(leafletLayer._layers) + const handleDataAdd = (event: LeafletEvent) => { + const leafletLayer = event.target + const features = Object.values(leafletLayer._layers) - if ( - features.length === 1 && - // FIXME: Type is currently not defined by DefinitelyTyped? - // @ts-ignore - features[0].feature.geometry.type === 'Point' - ) { - // @ts-ignore - const coords = features[0].feature.geometry.coordinates - leafletLayer._map.setView([coords[1], coords[0]], 13) - } else { - leafletLayer._map.fitBounds(leafletLayer.getBounds()) - } + if ( + features.length === 1 && + // FIXME: Type is currently not defined by DefinitelyTyped? + // @ts-ignore + features[0].feature.geometry.type === 'Point' + ) { + // @ts-ignore + const coords = features[0].feature.geometry.coordinates + leafletLayer._map.setView([coords[1], coords[0]], 13) + } else { + leafletLayer._map.fitBounds(leafletLayer.getBounds()) } + } - const Wrapper = mapExpanded ? MapWrapperExpanded : MapWrapper + const Wrapper = mapExpanded ? MapWrapperExpanded : MapWrapper - return ( - <Wrapper center={[0, 0]} scrollWheelZoom={false} zoom={1}> - <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> - <GeoJSON - data={featureCollection} - eventHandlers={{ - add: handleDataAdd, - }} - /> - <ExpandButton - icon={mapExpanded ? 'compress' : 'expand'} - onClick={handleIconClick} - /> - </Wrapper> - ) + return ( + <Wrapper center={[0, 0]} scrollWheelZoom={false} zoom={1}> + <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> + <GeoJSON + data={featureCollection} + eventHandlers={{ + add: handleDataAdd, + }} + /> + <ExpandButton + icon={mapExpanded ? 'compress' : 'expand'} + onClick={handleIconClick} + /> + </Wrapper> + ) } export default Map diff --git a/src/components/Message.tsx b/src/components/Message.tsx index 6979104..337601f 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -13,54 +13,54 @@ dayjs.extend(relativeTime) dayjs.extend(localizedFormat) const AttachmentsWrapper = styled(Card.Content)` - padding: 0; + padding: 0; ` interface Props { - message: MessageType + message: MessageType } const Message: FC<Props> = props => { - const relativeCreationDate = ( - <div> - <Icon name="clock" /> - {dayjs(props.message.creation_date).fromNow()} - </div> - ) - const creationDate = dayjs(props.message.creation_date).format('LLLL') + const relativeCreationDate = ( + <div> + <Icon name="clock" /> + {dayjs(props.message.creation_date).fromNow()} + </div> + ) + const creationDate = dayjs(props.message.creation_date).format('LLLL') - return ( - <Card fluid> - <CardContent> - <div - dangerouslySetInnerHTML={{ - __html: replaceMagic(props.message.text), - }} - /> - </CardContent> - {props.message.attachments && ( - <AttachmentsWrapper> - <Attachments attachments={props.message.attachments} /> - </AttachmentsWrapper> - )} - <Map featureCollection={props.message.geo_information} /> - <Card.Content extra> - <Grid> - <Grid.Row> - <Grid.Column width={10}> - <Popup - content={creationDate} - flowing - inverted - size="tiny" - trigger={relativeCreationDate} - /> - </Grid.Column> - </Grid.Row> - </Grid> - </Card.Content> - </Card> - ) + return ( + <Card fluid> + <CardContent> + <div + dangerouslySetInnerHTML={{ + __html: replaceMagic(props.message.text), + }} + /> + </CardContent> + {props.message.attachments && ( + <AttachmentsWrapper> + <Attachments attachments={props.message.attachments} /> + </AttachmentsWrapper> + )} + <Map featureCollection={props.message.geo_information} /> + <Card.Content extra> + <Grid> + <Grid.Row> + <Grid.Column width={10}> + <Popup + content={creationDate} + flowing + inverted + size="tiny" + trigger={relativeCreationDate} + /> + </Grid.Column> + </Grid.Row> + </Grid> + </Card.Content> + </Card> + ) } export default Message diff --git a/src/components/MessageList.test.tsx b/src/components/MessageList.test.tsx index f4e1d78..3b665ff 100644 --- a/src/components/MessageList.test.tsx +++ b/src/components/MessageList.test.tsx @@ -4,22 +4,22 @@ import MessageList from './MessageList' import { render, screen } from '@testing-library/react' describe('MessageList', function () { - test('renders empty Messages', async function () { - jest.spyOn(api, 'getTimeline').mockResolvedValue({ - data: { messages: [] }, - }) - const intersectionObserverMock = () => ({ - observe: () => null, - }) - window.IntersectionObserver = jest - .fn() - .mockImplementation(intersectionObserverMock) + test('renders empty Messages', async function () { + jest.spyOn(api, 'getTimeline').mockResolvedValue({ + data: { messages: [] }, + }) + const intersectionObserverMock = () => ({ + observe: () => null, + }) + window.IntersectionObserver = jest + .fn() + .mockImplementation(intersectionObserverMock) - render(<MessageList refreshInterval={10} />) + render(<MessageList refreshInterval={10} />) - expect(screen.getByText('Loading messages')).toBeInTheDocument() - expect( - await screen.findByText('We dont have any messages at the moment.') - ).toBeInTheDocument() - }) + expect(screen.getByText('Loading messages')).toBeInTheDocument() + expect( + await screen.findByText('We dont have any messages at the moment.') + ).toBeInTheDocument() + }) }) diff --git a/src/components/MessageList.tsx b/src/components/MessageList.tsx index ab429f0..49ce98f 100644 --- a/src/components/MessageList.tsx +++ b/src/components/MessageList.tsx @@ -5,143 +5,139 @@ import Message from './Message' import { getTimeline } from '../lib/api' interface Props { - refreshInterval: number + refreshInterval: number } const MessageList: FC<Props> = props => { - const [isLoading, setIsLoading] = useState<boolean>(true) - const [messages, setMessages] = useState<MessageType[]>([]) - const [lastMessageReceived, setLastMessageReceived] = - useState<boolean>(false) - - const loadMoreSpinnerRef = useRef<HTMLDivElement>(null) - - const fetchMessages = useCallback(() => { - const after = messages[0]?.id - - getTimeline({ after: after ? after : null }) - .then(response => { - if (response.data.messages) { - setMessages([...response.data.messages, ...messages]) - } - setIsLoading(false) - }) - .catch(error => { - // eslint-disable-next-line no-console - console.error(error) - setIsLoading(false) - }) - }, [messages]) - - const fetchOlderMessages = useCallback(() => { - const oldestMessage = messages[messages.length - 1] - if (oldestMessage !== undefined) { - getTimeline({ before: oldestMessage.id }) - .then(response => { - if (response.data.messages !== null) { - setMessages([...messages, ...response.data.messages]) - } else { - setLastMessageReceived(true) - } - }) - .catch(error => { - // eslint-disable-next-line no-console - console.error(error) - }) - } - }, [messages]) - - // FIXME: possibly triggers unnecessary rerenders - // eslint-disable-next-line react-hooks/exhaustive-deps - const intersectionObserverOptions = { - root: null, - rootMargin: '0px', - threshold: 1.0, - } - - const fetchOlderMessagesCallback = useCallback( - (entries: IntersectionObserverEntry[]) => { - if (entries[0].isIntersecting) { - fetchOlderMessages() - } - }, - [fetchOlderMessages] - ) - - useEffect(() => { - fetchMessages() + const [isLoading, setIsLoading] = useState<boolean>(true) + const [messages, setMessages] = useState<MessageType[]>([]) + const [lastMessageReceived, setLastMessageReceived] = useState<boolean>(false) - // This should only be executed once on load (~ componentDidMount) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + const loadMoreSpinnerRef = useRef<HTMLDivElement>(null) - useEffect(() => { - const observer = new IntersectionObserver( - fetchOlderMessagesCallback, - intersectionObserverOptions - ) - const currentRef = loadMoreSpinnerRef.current + const fetchMessages = useCallback(() => { + const after = messages[0]?.id - if (currentRef) { - observer.observe(currentRef) + getTimeline({ after: after ? after : null }) + .then(response => { + if (response.data.messages) { + setMessages([...response.data.messages, ...messages]) } + setIsLoading(false) + }) + .catch(error => { + // eslint-disable-next-line no-console + console.error(error) + setIsLoading(false) + }) + }, [messages]) + + const fetchOlderMessages = useCallback(() => { + const oldestMessage = messages[messages.length - 1] + if (oldestMessage !== undefined) { + getTimeline({ before: oldestMessage.id }) + .then(response => { + if (response.data.messages !== null) { + setMessages([...messages, ...response.data.messages]) + } else { + setLastMessageReceived(true) + } + }) + .catch(error => { + // eslint-disable-next-line no-console + console.error(error) + }) + } + }, [messages]) + + // FIXME: possibly triggers unnecessary rerenders + // eslint-disable-next-line react-hooks/exhaustive-deps + const intersectionObserverOptions = { + root: null, + rootMargin: '0px', + threshold: 1.0, + } + + const fetchOlderMessagesCallback = useCallback( + (entries: IntersectionObserverEntry[]) => { + if (entries[0].isIntersecting) { + fetchOlderMessages() + } + }, + [fetchOlderMessages] + ) + + useEffect(() => { + fetchMessages() + + // This should only be executed once on load (~ componentDidMount) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - return () => { - if (currentRef) { - observer.unobserve(currentRef) - } - } - }, [ - fetchOlderMessagesCallback, - intersectionObserverOptions, - loadMoreSpinnerRef, - ]) - - // periodically fetch new messages - useEffect(() => { - const interval = setInterval( - () => fetchMessages(), - props.refreshInterval - ) - - return () => clearInterval(interval) - }, [fetchMessages, messages, props.refreshInterval]) - - const renderPlaceholder = () => ( - <Segment placeholder> - <Header icon> - <Icon color={'grey'} name="hourglass half" /> - We dont have any messages at the moment. - </Header> - </Segment> + useEffect(() => { + const observer = new IntersectionObserver( + fetchOlderMessagesCallback, + intersectionObserverOptions ) + const currentRef = loadMoreSpinnerRef.current - if (isLoading) { - return ( - <Dimmer active inverted> - <Loader inverted size="small"> - Loading messages - </Loader> - </Dimmer> - ) + if (currentRef) { + observer.observe(currentRef) } - if (!messages.length) { - return renderPlaceholder() + return () => { + if (currentRef) { + observer.unobserve(currentRef) + } } - + }, [ + fetchOlderMessagesCallback, + intersectionObserverOptions, + loadMoreSpinnerRef, + ]) + + // periodically fetch new messages + useEffect(() => { + const interval = setInterval(() => fetchMessages(), props.refreshInterval) + + return () => clearInterval(interval) + }, [fetchMessages, messages, props.refreshInterval]) + + const renderPlaceholder = () => ( + <Segment placeholder> + <Header icon> + <Icon color={'grey'} name="hourglass half" /> + We dont have any messages at the moment. + </Header> + </Segment> + ) + + if (isLoading) { return ( - <div> - {messages.map(message => ( - <Message key={message.id} message={message} /> - ))} - {!lastMessageReceived && ( - <div ref={loadMoreSpinnerRef}> - <Loader active inline="centered" size="tiny" /> - </div> - )} - </div> + <Dimmer active inverted> + <Loader inverted size="small"> + Loading messages + </Loader> + </Dimmer> ) + } + + if (!messages.length) { + return renderPlaceholder() + } + + return ( + <div> + {messages.map(message => ( + <Message key={message.id} message={message} /> + ))} + {!lastMessageReceived && ( + <div ref={loadMoreSpinnerRef}> + <Loader active inline="centered" size="tiny" /> + </div> + )} + </div> + ) } export default MessageList diff --git a/src/components/ReloadInfo.tsx b/src/components/ReloadInfo.tsx index 3d6bb88..8d1a639 100644 --- a/src/components/ReloadInfo.tsx +++ b/src/components/ReloadInfo.tsx @@ -1,26 +1,26 @@ -import { FC, useState, useCallback } from 'react' +import { FC, useCallback, useState } from 'react' import { Message } from 'semantic-ui-react' const ReloadInfo: FC = () => { - const [showReloadInfo, setShowReloadInfo] = useState<boolean>( - localStorage.getItem('showReloadInfo') === null - ) + const [showReloadInfo, setShowReloadInfo] = useState<boolean>( + localStorage.getItem('showReloadInfo') === null + ) - const handleDismiss = useCallback(() => { - setShowReloadInfo(false) - localStorage.setItem('showReloadInfo', '0') - }, []) + const handleDismiss = useCallback(() => { + setShowReloadInfo(false) + localStorage.setItem('showReloadInfo', '0') + }, []) - return ( - <Message - color="teal" - content="The messages update automatically. There is no need to reload the entire page." - header="Information" - hidden={!showReloadInfo} - icon="info" - onDismiss={handleDismiss} - /> - ) + return ( + <Message + color="teal" + content="The messages update automatically. There is no need to reload the entire page." + header="Information" + hidden={!showReloadInfo} + icon="info" + onDismiss={handleDismiss} + /> + ) } export default ReloadInfo diff --git a/src/components/UpdateMessage.tsx b/src/components/UpdateMessage.tsx index e5535d4..58b3298 100644 --- a/src/components/UpdateMessage.tsx +++ b/src/components/UpdateMessage.tsx @@ -4,39 +4,39 @@ import styled from 'styled-components' import { spacing, zIndex } from '../lib/theme' const Wrapper = styled.div` - position: fixed; - left: ${spacing.normal}; - bottom: ${spacing.normal}; - right: ${spacing.normal}; - text-aling: center; - z-index: ${zIndex.updateMessage}; + position: fixed; + left: ${spacing.normal}; + bottom: ${spacing.normal}; + right: ${spacing.normal}; + text-aling: center; + z-index: ${zIndex.updateMessage}; ` const Link = styled.a` - cursor: pointer; + cursor: pointer; ` interface Props { - update: boolean + update: boolean } const UpdateMessage: FC<Props> = props => { - const handleClick = useCallback(() => { - window.location.reload() - }, []) + const handleClick = useCallback(() => { + window.location.reload() + }, []) - if (!props.update) { - return null - } + if (!props.update) { + return null + } - return ( - <Wrapper> - <Message color={'yellow'} negative> - An update is available. Click{' '} - <Link onClick={handleClick}>here</Link> to update the App. - </Message> - </Wrapper> - ) + return ( + <Wrapper> + <Message color={'yellow'} negative> + An update is available. Click <Link onClick={handleClick}>here</Link> to + update the App. + </Message> + </Wrapper> + ) } export default UpdateMessage diff --git a/src/leaflet.config.js b/src/leaflet.config.js index e917e48..e294840 100644 --- a/src/leaflet.config.js +++ b/src/leaflet.config.js @@ -10,7 +10,7 @@ import markerShadow from 'leaflet/dist/images/marker-shadow.png' delete L.Icon.Default.prototype._getIconUrl L.Icon.Default.mergeOptions({ - iconRetinaUrl: marker2x, - iconUrl: marker, - shadowUrl: markerShadow, + iconRetinaUrl: marker2x, + iconUrl: marker, + shadowUrl: markerShadow, }) diff --git a/src/lib/api.ts b/src/lib/api.ts index c803595..53efa38 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -3,55 +3,55 @@ import { Message, Settings, Ticker } from './types' const apiUrl = process.env.REACT_APP_API_URL type InitResponseData = { - settings: Settings - ticker: Ticker | null + settings: Settings + ticker: Ticker | null } export type InitResponse = { - data: InitResponseData + data: InitResponseData } type TimelineResponseData = { - messages: Message[] + messages: Message[] } export type TimelineResponse = { - data: TimelineResponseData + data: TimelineResponseData } async function get<T>(path: string, config?: RequestInit): Promise<T> { - const init = { method: 'get', ...config } - const request = new Request(path, init) - const response = await fetch(request) + const init = { method: 'get', ...config } + const request = new Request(path, init) + const response = await fetch(request) - if (!response.ok) { - throw new Error( - `The server responses with an error: ${response.statusText} (${response.status})` - ) - } + if (!response.ok) { + throw new Error( + `The server responses with an error: ${response.statusText} (${response.status})` + ) + } - return response.json().catch(() => ({})) + return response.json().catch(() => ({})) } export async function getInit(): Promise<InitResponse> { - return get(`${apiUrl}/init`) + return get(`${apiUrl}/init`) } export type TimelineOpts = { - after?: string | null - before?: string | null + after?: string | null + before?: string | null } export async function getTimeline( - opts: TimelineOpts + opts: TimelineOpts ): Promise<TimelineResponse> { - if (opts.after != null) { - return get(`${apiUrl}/timeline?after=${opts.after}`) - } + if (opts.after != null) { + return get(`${apiUrl}/timeline?after=${opts.after}`) + } - if (opts.before != null) { - return get(`${apiUrl}/timeline?before=${opts.before}`) - } + if (opts.before != null) { + return get(`${apiUrl}/timeline?before=${opts.before}`) + } - return get(`${apiUrl}/timeline`) + return get(`${apiUrl}/timeline`) } diff --git a/src/lib/helper.ts b/src/lib/helper.ts index c5fa7e3..cc57d62 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -3,34 +3,34 @@ import { breakpoints } from './theme' // FIXME: Not sure if checking the width once and using it to decide upon // what markup to render actually is the best solution here. export const isMobile = (): boolean => { - const width = - window.innerWidth || - document.documentElement.clientWidth || - document.getElementsByTagName('body')[0].clientWidth - return width < breakpoints.mobile + const width = + window.innerWidth || + document.documentElement.clientWidth || + document.getElementsByTagName('body')[0].clientWidth + return width < breakpoints.mobile } // FIXME: Might be better to use a library like validator.js // to catch more cases. export const replaceMagic = (text: string): string => { - return text - ? text - .replace( - /(https?:\/\/([a-zA-Z0-9._\-/]+))/g, - '<a href="$1" target="_blank" rel="noopener noreferrer">$2</a>' - ) - .replace( - /#(\S+)/g, - '<a target="_blank" rel="noopener noreferrer" href="https://twitter.com/search?q=%23$1">#$1</a>' - ) - .replace( - / @(\S+)/g, - ' <a target="_blank" rel="noopener noreferrer" href="https://twitter.com/$1">@$1</a>' - ) - .replace( - /([a-zA-Z0-9_\-.]+@[a-zA-Z0-9_-]+.[a-zA-Z]+)/g, - '<a href="mailto:$1">$1</a>' - ) - .replace(/(?:\r\n|\r|\n)/g, '<br/>') - : '' + return text + ? text + .replace( + /(https?:\/\/([a-zA-Z0-9._\-/]+))/g, + '<a href="$1" target="_blank" rel="noopener noreferrer">$2</a>' + ) + .replace( + /#(\S+)/g, + '<a target="_blank" rel="noopener noreferrer" href="https://twitter.com/search?q=%23$1">#$1</a>' + ) + .replace( + / @(\S+)/g, + ' <a target="_blank" rel="noopener noreferrer" href="https://twitter.com/$1">@$1</a>' + ) + .replace( + /([a-zA-Z0-9_\-.]+@[a-zA-Z0-9_-]+.[a-zA-Z]+)/g, + '<a href="mailto:$1">$1</a>' + ) + .replace(/(?:\r\n|\r|\n)/g, '<br/>') + : '' } diff --git a/src/lib/theme.ts b/src/lib/theme.ts index a0e72ba..2b9300a 100644 --- a/src/lib/theme.ts +++ b/src/lib/theme.ts @@ -1,12 +1,12 @@ export const spacing = { - normal: '1em', + normal: '1em', } export const breakpoints = { - mobile: 768, + mobile: 768, } export const zIndex = { - expandButtonOnLeafletMap: 1000, - updateMessage: 1001, + expandButtonOnLeafletMap: 1000, + updateMessage: 1001, } diff --git a/src/lib/types.ts b/src/lib/types.ts index 99d9431..51d4016 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,58 +1,58 @@ export type Ticker = { - active: boolean - creation_date: Date - description: string - domain: string - id: string - title: string - information: TickerInformation + active: boolean + creation_date: Date + description: string + domain: string + id: string + title: string + information: TickerInformation } type TickerInformation = { - author: string - email: string - facebook: string - twitter: string - url: string + author: string + email: string + facebook: string + twitter: string + url: string } export type Settings = { - refresh_interval: number - inactive_settings: InactiveSettings + refresh_interval: number + inactive_settings: InactiveSettings } export type InactiveSettings = { - author: string - email: string - homepage: string - twitter: string - headline: string - sub_headline: string - description: string + author: string + email: string + homepage: string + twitter: string + headline: string + sub_headline: string + description: string } export enum DescriptionTypes { - Author = 'AUTHOR', - Email = 'EMAIL', - Facebook = 'FACEBOOK', - Homepage = 'HOMEPAGE', - Twitter = 'TWITTER', + Author = 'AUTHOR', + Email = 'EMAIL', + Facebook = 'FACEBOOK', + Homepage = 'HOMEPAGE', + Twitter = 'TWITTER', } export type Message = { - id: string - text: string - ticker: number - creation_date: Date - tweet_id: string - tweet_user: string - attachments: Attachment[] - // Stringified GeoJSON.FeatureCollection - geo_information: string + id: string + text: string + ticker: number + creation_date: Date + tweet_id: string + tweet_user: string + attachments: Attachment[] + // Stringified GeoJSON.FeatureCollection + geo_information: string } export type Attachment = { - // FIXME: Enum? - content_type: string - url: string + // FIXME: Enum? + content_type: string + url: string } diff --git a/src/views/ActiveView.tsx b/src/views/ActiveView.tsx index bd630b2..3752fba 100644 --- a/src/views/ActiveView.tsx +++ b/src/views/ActiveView.tsx @@ -7,69 +7,65 @@ import { Ticker } from '../lib/types' import { isMobile } from '../lib/helper' const Wrapper = styled(Container)` - padding: ${spacing.normal} 0; + padding: ${spacing.normal} 0; ` const HeaderWrapper = styled(Header)` - margin: 0 0 ${spacing.normal} !important; + margin: 0 0 ${spacing.normal} !important; ` interface Props { - ticker: Ticker - update: boolean - refreshInterval: number + ticker: Ticker + update: boolean + refreshInterval: number } const ActiveView: FC<Props> = props => { - const [stickyContext, setStickyContext] = useState() + const [stickyContext, setStickyContext] = useState() - const headline = - props.ticker === null || props.ticker.title == undefined - ? 'Ticker' - : props.ticker.title + const headline = + props.ticker === null || props.ticker.title == undefined + ? 'Ticker' + : props.ticker.title - // FIXME - const handleContextRef = useCallback((stickyContextValue: any) => { - setStickyContext(stickyContextValue) - }, []) - - if (isMobile()) { - return ( - <Wrapper> - <UpdateMessage update={props.update} /> - <About isModal ticker={props.ticker} /> - {headline && ( - <HeaderWrapper content={headline} size={'large'} /> - )} - <ReloadInfo /> - <MessageList refreshInterval={props.refreshInterval} /> - </Wrapper> - ) - } + // FIXME + const handleContextRef = useCallback((stickyContextValue: any) => { + setStickyContext(stickyContextValue) + }, []) + if (isMobile()) { return ( - <Wrapper> - <UpdateMessage update={props.update} /> - {headline && <HeaderWrapper content={headline} size={'large'} />} - <ReloadInfo /> - <Grid divided={'vertically'}> - <Grid.Row columns={2}> - <Grid.Column computer={10} tablet={10}> - <div ref={handleContextRef}> - <MessageList - refreshInterval={props.refreshInterval} - /> - </div> - </Grid.Column> - <Grid.Column computer={6} tablet={6}> - <Sticky context={stickyContext} offset={30}> - <About ticker={props.ticker} /> - </Sticky> - </Grid.Column> - </Grid.Row> - </Grid> - </Wrapper> + <Wrapper> + <UpdateMessage update={props.update} /> + <About isModal ticker={props.ticker} /> + {headline && <HeaderWrapper content={headline} size={'large'} />} + <ReloadInfo /> + <MessageList refreshInterval={props.refreshInterval} /> + </Wrapper> ) + } + + return ( + <Wrapper> + <UpdateMessage update={props.update} /> + {headline && <HeaderWrapper content={headline} size={'large'} />} + <ReloadInfo /> + <Grid divided={'vertically'}> + <Grid.Row columns={2}> + <Grid.Column computer={10} tablet={10}> + <div ref={handleContextRef}> + <MessageList refreshInterval={props.refreshInterval} /> + </div> + </Grid.Column> + <Grid.Column computer={6} tablet={6}> + <Sticky context={stickyContext} offset={30}> + <About ticker={props.ticker} /> + </Sticky> + </Grid.Column> + </Grid.Row> + </Grid> + </Wrapper> + ) } export default ActiveView diff --git a/src/views/ErrorView.tsx b/src/views/ErrorView.tsx index 956f10a..aae3215 100644 --- a/src/views/ErrorView.tsx +++ b/src/views/ErrorView.tsx @@ -5,32 +5,32 @@ import { Credits } from '../components' import { spacing } from '../lib/theme' const Wrapper = styled(Container)` - padding-top: ${spacing.normal}; + padding-top: ${spacing.normal}; ` interface Props { - message: string + message: string } const ErrorView: FC<Props> = props => { - const handleClick = useCallback(() => { - window.location.reload() - }, []) + const handleClick = useCallback(() => { + window.location.reload() + }, []) - return ( - <Wrapper> - <Segment placeholder> - <Header icon> - <Icon name="ban" /> - {props.message} - </Header> - <Button onClick={handleClick} primary> - Try reload - </Button> - </Segment> - <Credits /> - </Wrapper> - ) + return ( + <Wrapper> + <Segment placeholder> + <Header icon> + <Icon name="ban" /> + {props.message} + </Header> + <Button onClick={handleClick} primary> + Try reload + </Button> + </Segment> + <Credits /> + </Wrapper> + ) } export default ErrorView diff --git a/src/views/InactiveView.tsx b/src/views/InactiveView.tsx index 4ac658b..9da8b2c 100644 --- a/src/views/InactiveView.tsx +++ b/src/views/InactiveView.tsx @@ -6,77 +6,73 @@ import { Credits, DescriptionItem, UpdateMessage } from '../components' import { DescriptionTypes, InactiveSettings } from '../lib/types' const Wrapper = styled(Container)` - padding-top: 50px; + padding-top: 50px; ` interface Props { - settings: InactiveSettings - update: boolean + settings: InactiveSettings + update: boolean } const InactiveView: FC<Props> = props => { - const renderHeader = () => ( - <Header icon size="huge" textAlign="center"> - <Icon name="hide" /> - <Header.Content> - {props.settings.headline} - <Header.Subheader> - {props.settings.sub_headline} - </Header.Subheader> - </Header.Content> - </Header> - ) + const renderHeader = () => ( + <Header icon size="huge" textAlign="center"> + <Icon name="hide" /> + <Header.Content> + {props.settings.headline} + <Header.Subheader>{props.settings.sub_headline}</Header.Subheader> + </Header.Content> + </Header> + ) - return ( - <Wrapper> - <UpdateMessage update={props.update} /> - <Grid centered> - <Grid.Column computer={8} mobile={16} tablet={8}> - {props.settings.headline && - props.settings.sub_headline && - renderHeader()} - <Card fluid> - <Card.Content> - {props.settings.description && ( - <ReactMarkdown> - {props.settings.description} - </ReactMarkdown> - )} - </Card.Content> - <Card.Content> - <List> - {props.settings.author && ( - <DescriptionItem - info={props.settings.author} - type={DescriptionTypes.Author} - /> - )} - {props.settings.email && ( - <DescriptionItem - info={props.settings.email} - type={DescriptionTypes.Email} - /> - )} - {props.settings.homepage && ( - <DescriptionItem - info={props.settings.homepage} - type={DescriptionTypes.Homepage} - /> - )} - {props.settings.twitter && ( - <DescriptionItem - info={props.settings.twitter} - type={DescriptionTypes.Twitter} - /> - )} - </List> - </Card.Content> - </Card> - <Credits /> - </Grid.Column> - </Grid> - </Wrapper> - ) + return ( + <Wrapper> + <UpdateMessage update={props.update} /> + <Grid centered> + <Grid.Column computer={8} mobile={16} tablet={8}> + {props.settings.headline && + props.settings.sub_headline && + renderHeader()} + <Card fluid> + <Card.Content> + {props.settings.description && ( + <ReactMarkdown>{props.settings.description}</ReactMarkdown> + )} + </Card.Content> + <Card.Content> + <List> + {props.settings.author && ( + <DescriptionItem + info={props.settings.author} + type={DescriptionTypes.Author} + /> + )} + {props.settings.email && ( + <DescriptionItem + info={props.settings.email} + type={DescriptionTypes.Email} + /> + )} + {props.settings.homepage && ( + <DescriptionItem + info={props.settings.homepage} + type={DescriptionTypes.Homepage} + /> + )} + {props.settings.twitter && ( + <DescriptionItem + info={props.settings.twitter} + type={DescriptionTypes.Twitter} + /> + )} + </List> + </Card.Content> + </Card> + <Credits /> + </Grid.Column> + </Grid> + </Wrapper> + ) } export default InactiveView diff --git a/tsconfig.json b/tsconfig.json index 1ce1de6..e69be03 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,26 +1,26 @@ { - "compilerOptions": { - "allowJs": true, - "allowSyntheticDefaultImports": true, - "downlevelIteration": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "jsx": "react-jsx", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "module": "commonjs", - "moduleResolution": "node", - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true, - "target": "es5" - }, - "include": [ - "src", - "./jest-setup.ts" - ] + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": true, + "downlevelIteration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "module": "commonjs", + "moduleResolution": "node", + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "es5" + }, + "include": [ + "src", + "./jest-setup.ts" + ] } diff --git a/webpack.common.config.ts b/webpack.common.config.ts index 1d498b0..7d020ed 100644 --- a/webpack.common.config.ts +++ b/webpack.common.config.ts @@ -5,55 +5,55 @@ import { Configuration, DefinePlugin } from 'webpack' dotenv.config() const baseConfig: Configuration = { - entry: './src/index.tsx', - output: { - publicPath: '/', - filename: '[name].js?[contenthash]', - clean: true, - }, - module: { - rules: [ - { - test: /\.(ts|js)x?$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader', - }, - }, - { - test: /\.css$/i, - use: ['style-loader', 'css-loader', 'postcss-loader'], - }, - { - test: /\.(png|svg|jpg|jpeg|gif)$/i, - type: 'asset/resource', - generator: { - filename: 'images/[name][ext][query]', - }, - }, - { - test: /\.(woff|woff2|eot|ttf|otf)$/i, - type: 'asset/resource', - generator: { - filename: 'fonts/[name][ext][query]', - }, - }, - ], - }, - resolve: { - extensions: ['.tsx', '.ts', '.js'], - }, - plugins: [ - new DefinePlugin({ - 'process.env.REACT_APP_API_URL': JSON.stringify( - process.env.REACT_APP_API_URL || 'http://localhost:8080/v1' - ), - }), - new HtmlWebpackPlugin({ - template: 'public/index.html', - favicon: 'public/favicon.ico', - }), + entry: './src/index.tsx', + output: { + publicPath: '/', + filename: '[name].js?[contenthash]', + clean: true, + }, + module: { + rules: [ + { + test: /\.(ts|js)x?$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + }, + }, + { + test: /\.css$/i, + use: ['style-loader', 'css-loader', 'postcss-loader'], + }, + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: 'asset/resource', + generator: { + filename: 'images/[name][ext][query]', + }, + }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/i, + type: 'asset/resource', + generator: { + filename: 'fonts/[name][ext][query]', + }, + }, ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + plugins: [ + new DefinePlugin({ + 'process.env.REACT_APP_API_URL': JSON.stringify( + process.env.REACT_APP_API_URL || 'http://localhost:8080/v1' + ), + }), + new HtmlWebpackPlugin({ + template: 'public/index.html', + favicon: 'public/favicon.ico', + }), + ], } export default baseConfig diff --git a/webpack.dev.config.ts b/webpack.dev.config.ts index 56c3747..7aa8057 100644 --- a/webpack.dev.config.ts +++ b/webpack.dev.config.ts @@ -1,27 +1,27 @@ import path = require('path') import { - Configuration as WebpackConfiguration, - HotModuleReplacementPlugin, + Configuration as WebpackConfiguration, + HotModuleReplacementPlugin, } from 'webpack' import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server' import baseConfig from './webpack.common.config' import merge from 'webpack-merge' interface Configuration extends WebpackConfiguration { - devServer?: WebpackDevServerConfiguration + devServer?: WebpackDevServerConfiguration } const config: Configuration = merge(baseConfig, { - mode: 'development', - devtool: 'inline-source-map', - devServer: { - static: path.join(__dirname, 'dist'), - historyApiFallback: true, - port: 4000, - open: true, - hot: true, - }, - plugins: [new HotModuleReplacementPlugin()], + mode: 'development', + devtool: 'inline-source-map', + devServer: { + static: path.join(__dirname, 'dist'), + historyApiFallback: true, + port: 4000, + open: true, + hot: true, + }, + plugins: [new HotModuleReplacementPlugin()], }) export default config diff --git a/webpack.prod.config.ts b/webpack.prod.config.ts index b55a4e4..41073cf 100644 --- a/webpack.prod.config.ts +++ b/webpack.prod.config.ts @@ -4,13 +4,13 @@ import merge from 'webpack-merge' import TerserPlugin = require('terser-webpack-plugin') const config: Configuration = merge(baseConfig, { - mode: 'production', - devtool: 'source-map', - optimization: { - runtimeChunk: 'single', - minimize: true, - minimizer: [new TerserPlugin()], - }, + mode: 'production', + devtool: 'source-map', + optimization: { + runtimeChunk: 'single', + minimize: true, + minimizer: [new TerserPlugin()], + }, }) export default config