From 52ed142eebcb50daec1e15eab93892147a967e2f Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Sun, 28 Aug 2022 06:49:43 +0200 Subject: [PATCH 1/6] perf: lazy load, reduce size of portrait images --- .all-contributorsrc | 18 +++++++++--------- .../src/components/Team/TeamCredits.tsx | 6 +++--- .../components/Team/TeamCreditsContributor.tsx | 5 ++--- .../components/Team/TeamCreditsMaintainer.tsx | 2 +- .../src/components/Team/TeamCreditsStyles.tsx | 3 +-- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3358dd973..1b13f48ac 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -11,7 +11,7 @@ { "login": "emmahodcroft", "name": "Emma Hodcroft", - "avatar_url": "https://avatars1.githubusercontent.com/u/14290674?v=4", + "avatar_url": "https://avatars1.githubusercontent.com/u/14290674?v=4&s=48", "profile": "https://github.com/emmahodcroft", "contributions": [ "ideas", @@ -23,7 +23,7 @@ { "login": "tsibley", "name": "Thomas Sibley", - "avatar_url": "https://avatars2.githubusercontent.com/u/79913?v=4", + "avatar_url": "https://avatars2.githubusercontent.com/u/79913?v=4&s=48", "profile": "https://tsibley.net/", "contributions": [ "infra", @@ -33,7 +33,7 @@ { "login": "victorlin", "name": "Victor Lin", - "avatar_url": "https://avatars2.githubusercontent.com/u/13424970?v=4", + "avatar_url": "https://avatars2.githubusercontent.com/u/13424970?v=4&s=48", "profile": "https://github.com/victorlin", "contributions": [ "code", @@ -43,7 +43,7 @@ { "login": "theosanderson", "name": "Theo Sanderson", - "avatar_url": "https://avatars.githubusercontent.com/u/19732295?v=4", + "avatar_url": "https://avatars.githubusercontent.com/u/19732295?v=4&s=48", "profile": "http://theo.io/", "contributions": [ "code" @@ -52,7 +52,7 @@ { "login": "stroudn1", "name": "Natalie Stroud", - "avatar_url": "https://avatars3.githubusercontent.com/u/17433156?v=4", + "avatar_url": "https://avatars3.githubusercontent.com/u/17433156?v=4&s=48", "profile": "http://www.natalieastroud.com/", "contributions": [ "content", @@ -62,7 +62,7 @@ { "login": "iskandr", "name": "Alex Rubinsteyn", - "avatar_url": "https://avatars.githubusercontent.com/u/48441?v=4", + "avatar_url": "https://avatars.githubusercontent.com/u/48441?v=4&s=48", "profile": "http://www.rubinsteyn.com/", "contributions": [ "doc" @@ -71,7 +71,7 @@ { "login": "molecules", "name": "Christopher Bottoms", - "avatar_url": "https://avatars.githubusercontent.com/u/345060?v=4", + "avatar_url": "https://avatars.githubusercontent.com/u/345060?v=4&s=48", "profile": "https://github.com/molecules", "contributions": [ "doc" @@ -80,7 +80,7 @@ { "login": "fmaguire", "name": "Finlay Maguire", - "avatar_url": "https://avatars.githubusercontent.com/u/1698629?v=4", + "avatar_url": "https://avatars.githubusercontent.com/u/1698629?v=4&s=48", "profile": "http://finlaymagui.re/", "contributions": [ "doc" @@ -89,7 +89,7 @@ { "login": "dnanto", "name": "dnanto", - "avatar_url": "https://avatars.githubusercontent.com/u/49757922?v=4", + "avatar_url": "https://avatars.githubusercontent.com/u/49757922?v=4&s=48", "profile": "https://github.com/dnanto", "contributions": [ "code", diff --git a/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx b/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx index 5a95fcf43..73e7cfb0e 100644 --- a/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx +++ b/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx @@ -19,7 +19,7 @@ import { FlexCol, FlexContributors, TeamCreditsH1 } from './TeamCreditsStyles' const maintainers: MaintainerInfo[] = [ { name: 'Ivan Aksamentov', - portraitUrl: 'https://avatars3.githubusercontent.com/u/9403403?s=400', + portraitUrl: 'https://avatars3.githubusercontent.com/u/9403403?v=4&s=100', title: 'Senior Software Engineer', affiliations: ['NeherLab, Biozentrum, University of Basel', 'Swiss Institute of Bioinformatics'], links: [ @@ -33,7 +33,7 @@ const maintainers: MaintainerInfo[] = [ }, { name: 'Richard Neher', - portraitUrl: 'https://avatars3.githubusercontent.com/u/8379168?s=400', + portraitUrl: 'https://avatars3.githubusercontent.com/u/8379168?v=4&s=100', title: 'Principal Investigator', affiliations: ['NeherLab, Biozentrum, University of Basel', 'Swiss Institute of Bioinformatics'], links: [ @@ -59,7 +59,7 @@ const maintainers: MaintainerInfo[] = [ }, { name: 'Cornelius Roemer', - portraitUrl: 'https://avatars1.githubusercontent.com/u/25161793?v=4', + portraitUrl: 'https://avatars1.githubusercontent.com/u/25161793?v=4&s=100', title: 'Staff Scientist', affiliations: ['NeherLab, Biozentrum, University of Basel', 'Swiss Institute of Bioinformatics'], links: [ diff --git a/packages_rs/nextclade-web/src/components/Team/TeamCreditsContributor.tsx b/packages_rs/nextclade-web/src/components/Team/TeamCreditsContributor.tsx index b2db21cd4..79d3432cb 100644 --- a/packages_rs/nextclade-web/src/components/Team/TeamCreditsContributor.tsx +++ b/packages_rs/nextclade-web/src/components/Team/TeamCreditsContributor.tsx @@ -35,8 +35,7 @@ const LinkExternal = styled(LinkExternalBase)` const Portrait = styled.img` margin: auto; - width: 66px; - border-radius: 50px; + border-radius: ${(props) => props.width}px; ` const NameText = styled.h2` @@ -54,7 +53,7 @@ export function TeamCreditsContributor({ contributor }: ContributorProps) { - + {contributor.name} diff --git a/packages_rs/nextclade-web/src/components/Team/TeamCreditsMaintainer.tsx b/packages_rs/nextclade-web/src/components/Team/TeamCreditsMaintainer.tsx index 72eaac62b..0fcdb2eb8 100644 --- a/packages_rs/nextclade-web/src/components/Team/TeamCreditsMaintainer.tsx +++ b/packages_rs/nextclade-web/src/components/Team/TeamCreditsMaintainer.tsx @@ -32,7 +32,7 @@ export function Maintainer({ maintainer }: MaintainerProps) { ) return ( - + {name} {title} {affiliationComponents} diff --git a/packages_rs/nextclade-web/src/components/Team/TeamCreditsStyles.tsx b/packages_rs/nextclade-web/src/components/Team/TeamCreditsStyles.tsx index 3cb8e0dc4..491583cc7 100644 --- a/packages_rs/nextclade-web/src/components/Team/TeamCreditsStyles.tsx +++ b/packages_rs/nextclade-web/src/components/Team/TeamCreditsStyles.tsx @@ -72,8 +72,7 @@ export const AffiliationText = styled.small` export const Portrait = styled.img` margin: 0 auto; - width: 100px; - border-radius: 100px; + border-radius: ${(props) => props.width}px; ` export const FlexContributors = styled.section` From 1233150f99afe9e02df83dadecd5af568e7255be Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Sun, 28 Aug 2022 07:11:35 +0200 Subject: [PATCH 2/6] perf: don't show changelog by default to reduce initial markup size --- packages_rs/nextclade-web/src/state/settings.state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages_rs/nextclade-web/src/state/settings.state.ts b/packages_rs/nextclade-web/src/state/settings.state.ts index ce85f08de..1a73d6020 100644 --- a/packages_rs/nextclade-web/src/state/settings.state.ts +++ b/packages_rs/nextclade-web/src/state/settings.state.ts @@ -44,7 +44,7 @@ export const changelogIsShownAtom = atom({ export const changelogShouldShowOnUpdatesAtom = atom({ key: 'changelogShouldShowOnUpdates', - default: true, + default: false, effects: [persistAtom], }) From a79330a5af4f9e8c22f264eef363bd8720924ee6 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Sun, 28 Aug 2022 17:40:28 +0200 Subject: [PATCH 3/6] perf: enable prerendering of main page --- packages_rs/nextclade-web/package.json | 14 ++++--- .../src/components/Common/BrowserWarning.tsx | 16 +++++-- .../src/components/Main/MainInputForm.tsx | 2 +- .../src/components/Main/MainPage.tsx | 41 ++++++++++++++++-- .../src/components/Results/ResultsPage.tsx | 5 ++- .../src/components/Team/TeamCredits.tsx | 5 ++- .../src/components/Tree/TreePage.tsx | 9 ++++ packages_rs/nextclade-web/src/pages/_app.tsx | 29 +------------ .../src/types/auspice/index.d.ts | 1 + packages_rs/nextclade-web/yarn.lock | 42 +++++++++---------- 10 files changed, 98 insertions(+), 66 deletions(-) diff --git a/packages_rs/nextclade-web/package.json b/packages_rs/nextclade-web/package.json index b6142c2f6..3b090637e 100644 --- a/packages_rs/nextclade-web/package.json +++ b/packages_rs/nextclade-web/package.json @@ -117,9 +117,9 @@ "pretty-bytes": "5.6.0", "prop-types": "15.8.1", "re-reselect": "4.0.0", - "react": "18.1.0", + "react": "18.2.0", "react-awesome-popover": "6.1.1", - "react-dom": "18.1.0", + "react-dom": "18.2.0", "react-error-boundary": "3.1.4", "react-file-icon": "1.1.0", "react-helmet": "6.1.0", @@ -137,8 +137,8 @@ "react-window": "1.8.7", "reactstrap": "8.10.1", "recharts": "2.1.9", - "recoil": "0.7.2", - "recoil-persist": "4.1.0", + "recoil": "0.7.5", + "recoil-persist": "4.2.0", "redux": "4.2.0", "redux-saga": "1.1.3", "redux-thunk": "2.4.1", @@ -405,11 +405,13 @@ "papaparse": "5.2.0", "postcss-loader": "6.2.1", "pretty-bytes": "5.6.0", - "react": "18.1.0", - "react-dom": "18.1.0", + "react": "18.2.0", + "react-dom": "18.2.0", "react-error-boundary": "2.2.3", "react-i18next": "11.16.7", "react-redux": "7.2.8", + "recoil": "0.7.5", + "recoil-persist": "4.2.0", "redux": "4.2.0", "reflect-metadata": "0.1.13", "serialize-javascript": "6.0.0", diff --git a/packages_rs/nextclade-web/src/components/Common/BrowserWarning.tsx b/packages_rs/nextclade-web/src/components/Common/BrowserWarning.tsx index f629c9eae..566337fb8 100644 --- a/packages_rs/nextclade-web/src/components/Common/BrowserWarning.tsx +++ b/packages_rs/nextclade-web/src/components/Common/BrowserWarning.tsx @@ -1,19 +1,25 @@ -import Bowser from 'bowser' import React, { useCallback, useMemo, useState } from 'react' +import dynamic from 'next/dynamic' +import { isNil } from 'lodash' +import Bowser from 'bowser' import { useTranslation } from 'react-i18next' import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap' -import { ModalHeader } from 'src/components/Error/ErrorPopup' -import { LinkExternal } from 'src/components/Link/LinkExternal' import { notUndefinedOrNull } from 'src/helpers/notUndefined' +import { ModalHeader } from 'src/components/Error/ErrorPopup' +import { LinkExternal } from 'src/components/Link/LinkExternal' -export function BrowserWarning() { +export function BrowserWarningAsync() { const { t } = useTranslation() const [isOpen, setIsOpen] = useState(true) const dismiss = useCallback(() => setIsOpen(false), [setIsOpen]) const nameAndVersion = useMemo(() => { + if (typeof window === 'undefined' || isNil(window?.navigator?.userAgent)) { + return null + } + const browser = Bowser.getParser(window?.navigator?.userAgent) const isSupportedBrowser = browser.satisfies({ chrome: '>60', @@ -60,3 +66,5 @@ export function BrowserWarning() { ) } + +export const BrowserWarning = dynamic(() => Promise.resolve(BrowserWarningAsync), { ssr: false }) diff --git a/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx b/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx index 8e62de067..5a9300f8a 100644 --- a/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx +++ b/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx @@ -29,7 +29,7 @@ export const Centered = styled.section` max-width: 800px; ` -export function MainInputForm() { +export default function MainInputForm() { const [searchTerm, setSearchTerm] = useState('') const currentDataset = useRecoilValue(datasetCurrentAtom) diff --git a/packages_rs/nextclade-web/src/components/Main/MainPage.tsx b/packages_rs/nextclade-web/src/components/Main/MainPage.tsx index 1af901864..344512ad9 100644 --- a/packages_rs/nextclade-web/src/components/Main/MainPage.tsx +++ b/packages_rs/nextclade-web/src/components/Main/MainPage.tsx @@ -1,17 +1,50 @@ -import React from 'react' +import React, { HTMLProps, Suspense, useMemo } from 'react' +import dynamic from 'next/dynamic' +import styled from 'styled-components' +import { ThreeDots } from 'react-loader-spinner' import { LayoutMain } from 'src/components/Layout/LayoutMain' - -import { MainInputForm } from 'src/components/Main/MainInputForm' import { MainSectionInfo } from 'src/components/Main/MainSectionInfo' import { MainSectionTitle } from 'src/components/Main/MainSectionTitle' import { TeamCredits } from 'src/components/Team/TeamCredits' +const MainInputForm = dynamic(() => import('src/components/Main/MainInputForm'), { ssr: false }) + +const SpinnerWrapper = styled.div>` + width: 100%; + height: 100%; + display: flex; +` + +const SpinnerWrapperInternal = styled.div` + margin: auto; +` + +const Spinner = styled(ThreeDots)` + flex: 1; + margin: auto; + height: 100%; +` + +function Loading() { + return ( + + + + + + ) +} + export function MainPage() { + const fallback = useMemo(() => , []) + return ( - + + + diff --git a/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx b/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx index df801592d..063d4ef7e 100644 --- a/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx +++ b/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx @@ -1,3 +1,4 @@ +import dynamic from 'next/dynamic' import React, { Suspense } from 'react' import { useRecoilValue } from 'recoil' import styled from 'styled-components' @@ -59,7 +60,7 @@ const Footer = styled.footer` flex-shrink: 0; ` -export function ResultsPage() { +function ResultsPageAsync() { const totalWidth = useRecoilValue(resultsTableTotalWidthAtom) return ( @@ -106,3 +107,5 @@ export function ResultsPage() { ) } + +export const ResultsPage = dynamic(() => Promise.resolve(ResultsPageAsync), { ssr: false }) diff --git a/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx b/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx index 73e7cfb0e..8722927e5 100644 --- a/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx +++ b/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx @@ -1,6 +1,7 @@ /* eslint-disable sonarjs/no-duplicate-string */ import { shuffle } from 'lodash' import React, { useMemo } from 'react' +import dynamic from 'next/dynamic' import { Col, Row } from 'reactstrap' import { Maintainer } from 'src/components/Team/TeamCreditsMaintainer' @@ -81,7 +82,7 @@ const maintainers: MaintainerInfo[] = [ export const contributors = getContributors() -export function TeamCredits() { +function TeamCreditsAsync() { const maintainerComponents = useMemo( () => shuffle(maintainers).map((maintainer) => ), [], @@ -125,3 +126,5 @@ export function TeamCredits() { ) } + +export const TeamCredits = dynamic(() => Promise.resolve(TeamCreditsAsync), { ssr: false }) diff --git a/packages_rs/nextclade-web/src/components/Tree/TreePage.tsx b/packages_rs/nextclade-web/src/components/Tree/TreePage.tsx index a6669f103..6b121f58c 100644 --- a/packages_rs/nextclade-web/src/components/Tree/TreePage.tsx +++ b/packages_rs/nextclade-web/src/components/Tree/TreePage.tsx @@ -1,3 +1,4 @@ +import { useRouter } from 'next/router' import React, { useMemo } from 'react' import styled from 'styled-components' import { I18nextProvider } from 'react-i18next' @@ -85,6 +86,7 @@ const LogoGisaid = styled(LogoGisaidBase)` export interface TreePageProps { treeMeta?: AuspiceMetadata + loaded?: boolean } const mapStateToProps = (state: State) => ({ @@ -94,11 +96,18 @@ const mapStateToProps = (state: State) => ({ export const TreePage = connect(mapStateToProps, null)(TreePageDisconnected) function TreePageDisconnected({ treeMeta }: TreePageProps) { + const router = useRouter() + const hasData = useMemo(() => treeMeta?.loaded, [treeMeta]) const isDataFromGisaid = useMemo( () => treeMeta?.dataProvenance?.some((provenance) => provenance.name?.toLowerCase() === 'gisaid'), [treeMeta], ) + if (!hasData) { + void router.replace('/') // eslint-disable-line no-void + return null + } + return ( diff --git a/packages_rs/nextclade-web/src/pages/_app.tsx b/packages_rs/nextclade-web/src/pages/_app.tsx index 29e93c1de..596f809ee 100644 --- a/packages_rs/nextclade-web/src/pages/_app.tsx +++ b/packages_rs/nextclade-web/src/pages/_app.tsx @@ -7,7 +7,6 @@ import React, { useEffect, Suspense, useMemo } from 'react' import { RecoilRoot, useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil' import { AppProps } from 'next/app' import { useRouter } from 'next/router' -import dynamic from 'next/dynamic' import { BrowserWarning } from 'src/components/Common/BrowserWarning' import { sanitizeError } from 'src/helpers/sanitizeError' import { useRunAnalysis } from 'src/hooks/useRunAnalysis' @@ -163,7 +162,7 @@ export function RecoilStateInitializer() { const mdxComponents = { a: LinkExternal } -export function MyApp({ Component, pageProps, router }: AppProps) { +export default function MyApp({ Component, pageProps, router }: AppProps) { const queryClient = useMemo(() => new QueryClient(), []) const { store } = useMemo(() => configureStore(), []) const fallback = useMemo(() => , []) @@ -208,29 +207,3 @@ export function MyApp({ Component, pageProps, router }: AppProps) { ) } - -// NOTE: This disables server-side rendering (SSR) entirely, to avoid fatal error -// 'Hydration failed because the initial UI does not match what was rendered on the server'. -// This is probably a bug in React 18, or in Next.js, or in the app. There's been reports that improper -// nesting of HTML and SVG elements can cause the mismatch. -// -// See: -// - https://github.com/facebook/react/issues/22833 -// - https://github.com/facebook/react/issues/24519 -// - https://github.com/vercel/next.js/discussions/35773 -// - https://nextjs.org/docs/messages/react-hydration-error -// - https://stackoverflow.com/questions/71706064/react-18-hydration-failed-because-the-initial-ui-does-not-match-what-was-render -// -// -// NOTE: does not seem to be working properly when SSR is enabled. The server hangs forever -// on the first unresolved Recoil atom, i.e. an atom without a default value. Such atoms, if accessed -// before they are initialized, trigger Suspense. However the server hangs, failing to make any progress past this -// point, even if this atom is initialized shortly after by another component. One example is the dataset atom. It is -// initially in suspended state, and unlocks after the dataset is fetched. However server stops at the dataset selector -// component and does not proceed further. We might not be handling the initialization of Recoil atoms properly, -// or it is simply not yet implemented well in Next.js or Recoil. -// -// -// TODO: When the hydration error, and the loading of Recoil atoms are sorted out we may want to remove this line, -// exporting the app component directly, reenabling SSR. -export default dynamic(() => Promise.resolve(MyApp), { ssr: false }) diff --git a/packages_rs/nextclade-web/src/types/auspice/index.d.ts b/packages_rs/nextclade-web/src/types/auspice/index.d.ts index 5caad5c8f..df85b481d 100644 --- a/packages_rs/nextclade-web/src/types/auspice/index.d.ts +++ b/packages_rs/nextclade-web/src/types/auspice/index.d.ts @@ -206,6 +206,7 @@ declare module 'auspice' { } export declare interface AuspiceMetadata { + loaded?: boolean title?: string description?: string buildUrl?: string diff --git a/packages_rs/nextclade-web/yarn.lock b/packages_rs/nextclade-web/yarn.lock index 30a1ab2c6..20668abcc 100644 --- a/packages_rs/nextclade-web/yarn.lock +++ b/packages_rs/nextclade-web/yarn.lock @@ -11715,13 +11715,13 @@ react-collapsible@^2.8.4: resolved "https://registry.yarnpkg.com/react-collapsible/-/react-collapsible-2.8.4.tgz#319ff7471138c4381ce0afa3ac308ccde7f4e09f" integrity sha512-oG4yOk6AGKswe0OD/8t3/nf4Rgj4UhlZUUvqL5jop0/ez02B3dBDmNvs3sQz0PcTpJvt0ai8zF7Atd1SzN/UNw== -react-dom@18.1.0, react-dom@^16.8.6: - version "18.1.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f" - integrity sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w== +react-dom@18.2.0, react-dom@^16.8.6: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== dependencies: loose-envify "^1.1.0" - scheduler "^0.22.0" + scheduler "^0.23.0" react-dropzone@12.1.0: version "12.1.0" @@ -12075,10 +12075,10 @@ react-window@1.8.7: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@18.1.0, react@^16.8.6, react@^16.9.0: - version "18.1.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890" - integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ== +react@18.2.0, react@^16.8.6, react@^16.9.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== dependencies: loose-envify "^1.1.0" @@ -12224,15 +12224,15 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" -recoil-persist@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/recoil-persist/-/recoil-persist-4.1.0.tgz#7406ce14f448783fbff9f9011da8235c84838249" - integrity sha512-7ruOsLhrEqBpl8EM6j5OITSKJXvH7BfSDxO5m1Heb3dJo6mnCJ3t4ln1KDJhZrvFOkqyO37Ufv3VB1wU06tfCA== +recoil-persist@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/recoil-persist/-/recoil-persist-4.2.0.tgz#9fbb4a8c158cbb83ceb9dedf795aee24ed15395d" + integrity sha512-MHVfML9GxJP3RpkKR4F5rp7DtvzIvjWhowtMao/b7h2k4afMio/4sMAdUtltIrDaeVegH0Iga8Sx5XQ3oD7CzA== -recoil@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.7.2.tgz#37aafc9e0674abae639263354a11c910e71bf78a" - integrity sha512-OT4pI7FOUHcIoRtjsL5Lqq+lFFzQfir4MIbUkqyJ3nqv3WfBP1pHepyurqTsK5gw+T+I2R8+uOD28yH+Lg5o4g== +recoil@0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.7.5.tgz#9a33a03350cfd99e08bdd5b73bfc8b8b9ee751b9" + integrity sha512-GVShsj5+M/2GULWBs5WBJGcsNis/d3YvDiaKjYh3mLKXftjtmk9kfaQ8jwjoIXySCwn8/RhgJ4Sshwgzj2UpFA== dependencies: hamt_plus "1.0.2" @@ -12764,10 +12764,10 @@ scheduler@^0.19.1: loose-envify "^1.1.0" object-assign "^4.1.1" -scheduler@^0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8" - integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ== +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== dependencies: loose-envify "^1.1.0" From a3ac1b68d2ddd1433d22ca06cc3e5960f9a90ee7 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Sun, 28 Aug 2022 17:59:03 +0200 Subject: [PATCH 4/6] perf: split results page bundle away from main page bundle --- .../nextclade-web/src/components/Results/ResultsPage.tsx | 4 +--- packages_rs/nextclade-web/src/pages/results.tsx | 6 +++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx b/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx index 063d4ef7e..1288ae3b9 100644 --- a/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx +++ b/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx @@ -60,7 +60,7 @@ const Footer = styled.footer` flex-shrink: 0; ` -function ResultsPageAsync() { +export default function ResultsPage() { const totalWidth = useRecoilValue(resultsTableTotalWidthAtom) return ( @@ -107,5 +107,3 @@ function ResultsPageAsync() { ) } - -export const ResultsPage = dynamic(() => Promise.resolve(ResultsPageAsync), { ssr: false }) diff --git a/packages_rs/nextclade-web/src/pages/results.tsx b/packages_rs/nextclade-web/src/pages/results.tsx index 6fa2153e3..be09451c8 100644 --- a/packages_rs/nextclade-web/src/pages/results.tsx +++ b/packages_rs/nextclade-web/src/pages/results.tsx @@ -1 +1,5 @@ -export { ResultsPage as default } from 'src/components/Results/ResultsPage' +import dynamic from 'next/dynamic' + +const ResultsPage = dynamic(() => import('src/components/Results/ResultsPage'), { ssr: false }) + +export default ResultsPage From be5e3952995fe5a75b901958830a7d6b4f8793ab Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Sun, 28 Aug 2022 18:40:31 +0200 Subject: [PATCH 5/6] perf: split modal dialogs code away from main bundle --- packages_rs/nextclade-web/package.json | 2 + .../components/Citation/CitationButton.tsx | 116 +------- .../components/Citation/CitationDialog.tsx | 106 ++++++++ .../src/components/Layout/SettingsButton.tsx | 135 +-------- .../src/components/Layout/SettingsDialog.tsx | 133 +++++++++ .../src/components/Layout/WhatsNewButton.tsx | 257 ++---------------- .../src/components/Layout/WhatsNewDialog.tsx | 232 ++++++++++++++++ .../src/components/Results/ResultsPage.tsx | 1 - .../nextclade-web/tools/server/dataServer.ts | 4 +- .../nextclade-web/tools/server/server.ts | 4 +- packages_rs/nextclade-web/yarn.lock | 11 +- 11 files changed, 534 insertions(+), 467 deletions(-) create mode 100644 packages_rs/nextclade-web/src/components/Citation/CitationDialog.tsx create mode 100644 packages_rs/nextclade-web/src/components/Layout/SettingsDialog.tsx create mode 100644 packages_rs/nextclade-web/src/components/Layout/WhatsNewDialog.tsx diff --git a/packages_rs/nextclade-web/package.json b/packages_rs/nextclade-web/package.json index 3b090637e..c0c82f22c 100644 --- a/packages_rs/nextclade-web/package.json +++ b/packages_rs/nextclade-web/package.json @@ -175,6 +175,7 @@ "@testing-library/user-event": "14.1.1", "@types/classnames": "2.3.0", "@types/compare-versions": "3.3.0", + "@types/compression": "1.7.2", "@types/compression-webpack-plugin": "9.0.0", "@types/connect-history-api-fallback": "1.3.5", "@types/copy-webpack-plugin": "6.4.3", @@ -225,6 +226,7 @@ "allow-methods": "3.1.0", "babel-plugin-parameter-decorator": "1.0.16", "babel-plugin-transform-typescript-metadata": "0.3.2", + "compression": "1.7.4", "compression-webpack-plugin": "9.2.0", "connect-history-api-fallback": "1.6.0", "conventional-changelog-cli": "2.2.2", diff --git a/packages_rs/nextclade-web/src/components/Citation/CitationButton.tsx b/packages_rs/nextclade-web/src/components/Citation/CitationButton.tsx index f40ec91b9..77fe1878a 100644 --- a/packages_rs/nextclade-web/src/components/Citation/CitationButton.tsx +++ b/packages_rs/nextclade-web/src/components/Citation/CitationButton.tsx @@ -1,22 +1,13 @@ -import React, { useCallback, useState } from 'react' - -import { useTranslation } from 'react-i18next' -import { - Button, - ButtonProps, - Col, - Container, - Modal as ReactstrapModal, - ModalBody as ReactstrapModalBody, - ModalFooter as ReactstrapModalFooter, - ModalHeader as ReactstrapModalHeader, - Row, -} from 'reactstrap' +import React, { useCallback, useMemo, useState } from 'react' +import dynamic from 'next/dynamic' import styled from 'styled-components' +import { useTranslation } from 'react-i18next' import { HiOutlineAcademicCap } from 'react-icons/hi' +import { ButtonProps } from 'reactstrap' import { ButtonTransparent } from 'src/components/Common/ButtonTransparent' -import { Citation } from 'src/components/Citation/Citation' + +const CitationDialog = dynamic(() => import('src/components/Citation/CitationDialog'), { ssr: false }) export const ButtonCitationBase = styled(ButtonTransparent)` margin: 2px 2px; @@ -29,103 +20,26 @@ export const ButtonCitationBase = styled(ButtonTransparent)` } ` -export const ButtonOk = styled(Button)` - width: 100px; -` - -export const ModalHeader = styled(ReactstrapModalHeader)` - .modal-title { - width: 100%; - } - - @media (max-width: 992px) { - padding: 0.25rem; - margin: 0.5rem; - margin-bottom: 0; - } -` - -export const Modal = styled(ReactstrapModal)` - height: 100%; - - // fullscreen on mobile - @media (max-width: 992px) { - max-width: unset; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: 0; - padding: 0; - - .modal-content { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: 0; - } - } -` - -export const ModalBody = styled(ReactstrapModalBody)` - @media (min-width: 991.98px) { - max-height: 66vh; - margin: auto; - } - - @media (max-width: 992px) { - margin: 1rem 0; - padding: 0; - } - - overflow-y: auto; -` - -export const ModalFooter = styled(ReactstrapModalFooter)` - margin: 0; - padding: 0; -` - export function CitationButton() { const { t } = useTranslation() const [showCitation, setShowCitation] = useState(false) - const toggleOpen = useCallback(() => setShowCitation((showCitation) => !showCitation), []) - const open = useCallback(() => setShowCitation(true), []) - const close = useCallback(() => setShowCitation(false), []) + const totggle = useCallback(() => setShowCitation((showCitation) => !showCitation), []) const text = t('Citation') - const closeText = t('Close this window') + const dialog = useMemo(() => { + if (!showCitation) { + return null + } + return + }, [showCitation, totggle]) return ( <> - + {text} - - -

{text}

-
- - - - - - - - - - - {t('OK')} - - - - - -
+ {dialog} ) } diff --git a/packages_rs/nextclade-web/src/components/Citation/CitationDialog.tsx b/packages_rs/nextclade-web/src/components/Citation/CitationDialog.tsx new file mode 100644 index 000000000..70b28c119 --- /dev/null +++ b/packages_rs/nextclade-web/src/components/Citation/CitationDialog.tsx @@ -0,0 +1,106 @@ +import React from 'react' +import { + Button, + ButtonProps, + Col, + Container, + Modal as ReactstrapModal, + ModalBody as ReactstrapModalBody, + ModalFooter as ReactstrapModalFooter, + ModalHeader as ReactstrapModalHeader, + Row, +} from 'reactstrap' +import { Citation } from 'src/components/Citation/Citation' +import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import styled from 'styled-components' + +export const ButtonOk = styled(Button)` + width: 100px; +` +export const ModalHeader = styled(ReactstrapModalHeader)` + .modal-title { + width: 100%; + } + + @media (max-width: 992px) { + padding: 0.25rem; + margin: 0.5rem; + margin-bottom: 0; + } +` +export const Modal = styled(ReactstrapModal)` + height: 100%; + + // fullscreen on mobile + @media (max-width: 992px) { + max-width: unset; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: 0; + padding: 0; + + .modal-content { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: 0; + } + } +` +export const ModalBody = styled(ReactstrapModalBody)` + @media (min-width: 991.98px) { + max-height: 66vh; + margin: auto; + } + + @media (max-width: 992px) { + margin: 1rem 0; + padding: 0; + } + + overflow-y: auto; +` +export const ModalFooter = styled(ReactstrapModalFooter)` + margin: 0; + padding: 0; +` + +export interface CitationDialogProps { + isOpen: boolean + toggle: () => void +} + +export default function CitationDialog({ isOpen, toggle }: CitationDialogProps) { + const { t } = useTranslationSafe() + const text = t('Citation') + const closeText = t('Close this window') + + return ( + + +

{text}

+
+ + + + + + + + + + + {t('Ok')} + + + + + +
+ ) +} diff --git a/packages_rs/nextclade-web/src/components/Layout/SettingsButton.tsx b/packages_rs/nextclade-web/src/components/Layout/SettingsButton.tsx index ac37ef46e..27dc2bff2 100644 --- a/packages_rs/nextclade-web/src/components/Layout/SettingsButton.tsx +++ b/packages_rs/nextclade-web/src/components/Layout/SettingsButton.tsx @@ -1,28 +1,15 @@ import React, { useCallback, useMemo } from 'react' - -import { - Button, - ButtonProps, - Col, - Container, - FormGroup, - Modal as ReactstrapModal, - ModalBody as ReactstrapModalBody, - ModalFooter as ReactstrapModalFooter, - ModalHeader as ReactstrapModalHeader, - Row, -} from 'reactstrap' +import dynamic from 'next/dynamic' import { useRecoilState } from 'recoil' import styled from 'styled-components' import { IoMdSettings } from 'react-icons/io' +import { ButtonProps } from 'reactstrap' -import { CardL2, CardL2Body, CardL2Header } from 'src/components/Common/Card' -import { Toggle } from 'src/components/Common/Toggle' -import { SeqViewSettings } from 'src/components/Settings/SeqViewSettings' -import { SystemSettings } from 'src/components/Settings/SystemSettings' -import { changelogShouldShowOnUpdatesAtom, isSettingsDialogOpenAtom } from 'src/state/settings.state' import { ButtonTransparent } from 'src/components/Common/ButtonTransparent' import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import { isSettingsDialogOpenAtom } from 'src/state/settings.state' + +const SettingsDialog = dynamic(() => import('src/components/Layout/SettingsDialog'), { ssr: false }) export const ButtonSettingsBase = styled(ButtonTransparent)` margin: 2px 2px; @@ -35,52 +22,10 @@ export const ButtonSettingsBase = styled(ButtonTransparent)` } ` -export const ButtonOk = styled(Button)` - width: 100px; -` - -export const ModalHeader = styled(ReactstrapModalHeader)` - .modal-title { - width: 100%; - } - padding: 1rem; -` - -export const Modal = styled(ReactstrapModal)` - @media (max-width: 1200px) { - min-width: 50vw; - } -` - -export const ModalBody = styled(ReactstrapModalBody)` - max-height: 66vh; - min-height: 300px; - - padding: 1rem 0; - - overflow-y: auto; - - // prettier-ignore - background: - linear-gradient(#ffffff 33%, rgba(255,255,255, 0)), - linear-gradient(rgba(255,255,255, 0), #ffffff 66%) 0 100%, - radial-gradient(farthest-side at 50% 0, rgba(119,119,119, 0.5), rgba(0,0,0,0)), - radial-gradient(farthest-side at 50% 100%, rgba(119,119,119, 0.5), rgba(0,0,0,0)) 0 100%; - background-color: #ffffff; - background-repeat: no-repeat; - background-attachment: local, local, scroll, scroll; - background-size: 100% 24px, 100% 24px, 100% 8px, 100% 8px; -` - -export const ModalFooter = styled(ReactstrapModalFooter)` - padding: 0; -` - export function SettingsButton() { const { t } = useTranslationSafe() const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useRecoilState(isSettingsDialogOpenAtom) - const [showWhatsnewOnUpdate, setShowWhatsnewOnUpdate] = useRecoilState(changelogShouldShowOnUpdatesAtom) const toggleOpen = useCallback( () => setIsSettingsDialogOpen(!isSettingsDialogOpen), @@ -88,7 +33,14 @@ export function SettingsButton() { ) const text = useMemo(() => t('Settings'), [t]) - const closeText = useMemo(() => t('Close this window'), [t]) + + const dialog = useMemo(() => { + if (!isSettingsDialogOpen) { + return null + } + + return + }, [isSettingsDialogOpen, toggleOpen]) return ( <> @@ -97,66 +49,7 @@ export function SettingsButton() { {text} - - -

{text}

-
- - - - - - - {t('System')} - - - - - - - - - - {t('Sequence view markers')} - - - - - - - - - - {t('Other settings')} - - - - {t(`Show "What's new" dialog after each update`)} - - - - - - - - - - - - - - - {t('OK')} - - - - - -
+ {dialog} ) } diff --git a/packages_rs/nextclade-web/src/components/Layout/SettingsDialog.tsx b/packages_rs/nextclade-web/src/components/Layout/SettingsDialog.tsx new file mode 100644 index 000000000..5d6f7322f --- /dev/null +++ b/packages_rs/nextclade-web/src/components/Layout/SettingsDialog.tsx @@ -0,0 +1,133 @@ +import React, { useMemo } from 'react' +import { + Button, + ButtonProps, + Col, + Container, + FormGroup, + Modal as ReactstrapModal, + ModalBody as ReactstrapModalBody, + ModalFooter as ReactstrapModalFooter, + ModalHeader as ReactstrapModalHeader, + Row, +} from 'reactstrap' +import { useRecoilState } from 'recoil' +import { CardL2, CardL2Body, CardL2Header } from 'src/components/Common/Card' +import { Toggle } from 'src/components/Common/Toggle' +import { SeqViewSettings } from 'src/components/Settings/SeqViewSettings' +import { SystemSettings } from 'src/components/Settings/SystemSettings' +import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import { changelogShouldShowOnUpdatesAtom } from 'src/state/settings.state' +import styled from 'styled-components' + +export const ButtonOk = styled(Button)` + width: 100px; +` +export const ModalHeader = styled(ReactstrapModalHeader)` + .modal-title { + width: 100%; + } + padding: 1rem; +` +export const Modal = styled(ReactstrapModal)` + @media (max-width: 1200px) { + min-width: 50vw; + } +` +export const ModalBody = styled(ReactstrapModalBody)` + max-height: 66vh; + min-height: 300px; + + padding: 1rem 0; + + overflow-y: auto; + + // prettier-ignore + background: + linear-gradient(#ffffff 33%, rgba(255,255,255, 0)), + linear-gradient(rgba(255,255,255, 0), #ffffff 66%) 0 100%, + radial-gradient(farthest-side at 50% 0, rgba(119,119,119, 0.5), rgba(0,0,0,0)), + radial-gradient(farthest-side at 50% 100%, rgba(119,119,119, 0.5), rgba(0,0,0,0)) 0 100%; + background-color: #ffffff; + background-repeat: no-repeat; + background-attachment: local, local, scroll, scroll; + background-size: 100% 24px, 100% 24px, 100% 8px, 100% 8px; +` +export const ModalFooter = styled(ReactstrapModalFooter)` + padding: 0; +` + +export interface SettingsDialogProps { + isOpen: boolean + toggleOpen: () => void +} + +export default function SettingsDialog({ isOpen, toggleOpen }: SettingsDialogProps) { + const { t } = useTranslationSafe() + const [showWhatsnewOnUpdate, setShowWhatsnewOnUpdate] = useRecoilState(changelogShouldShowOnUpdatesAtom) + const text = useMemo(() => t('Settings'), [t]) + const closeText = useMemo(() => t('Close this window'), [t]) + + return ( + + +

{text}

+
+ + + + + + + {t('System')} + + + + + + + + + + {t('Sequence view markers')} + + + + + + + + + + {t('Other settings')} + + + + {t(`Show "What's new" dialog after each update`)} + + + + + + + + + + + + + + + {t('OK')} + + + + + +
+ ) +} diff --git a/packages_rs/nextclade-web/src/components/Layout/WhatsNewButton.tsx b/packages_rs/nextclade-web/src/components/Layout/WhatsNewButton.tsx index 8dc79e214..fad2a11f4 100644 --- a/packages_rs/nextclade-web/src/components/Layout/WhatsNewButton.tsx +++ b/packages_rs/nextclade-web/src/components/Layout/WhatsNewButton.tsx @@ -1,27 +1,15 @@ -import { MDXProvider } from '@mdx-js/react' -import React, { useCallback } from 'react' - -import { useTranslation } from 'react-i18next' -import { - Button, - ButtonProps, - Col, - Container, - Modal as ReactstrapModal, - ModalBody as ReactstrapModalBody, - ModalFooter as ReactstrapModalFooter, - ModalHeader as ReactstrapModalHeader, - Row, -} from 'reactstrap' +import dynamic from 'next/dynamic' +import React, { useCallback, useMemo } from 'react' import { useRecoilState } from 'recoil' -import { changelogIsShownAtom, changelogShouldShowOnUpdatesAtom } from 'src/state/settings.state' +import type { ButtonProps } from 'reactstrap' import styled from 'styled-components' +import { useTranslation } from 'react-i18next' import { FaListUl } from 'react-icons/fa' import { ButtonTransparent } from 'src/components/Common/ButtonTransparent' -import { LinkExternal } from 'src/components/Link/LinkExternal' -import { Toggle } from 'src/components/Common/Toggle' -import Changelog from '../../../../../CHANGELOG.md' +import { changelogIsShownAtom } from 'src/state/settings.state' + +const WhatsNewDialog = dynamic(() => import('src/components/Layout/WhatsNewDialog'), { ssr: false }) export const ButtonWhatsNewBase = styled(ButtonTransparent)` margin: 2px 2px; @@ -34,243 +22,32 @@ export const ButtonWhatsNewBase = styled(ButtonTransparent)` } ` -export const ButtonOk = styled(Button)` - width: 100px; -` - -export const ModalHeader = styled(ReactstrapModalHeader)` - .modal-title { - width: 100%; - } - - @media (max-width: 992px) { - padding: 0.25rem; - margin: 0.5rem; - margin-bottom: 0; - } -` - -export const Modal = styled(ReactstrapModal)` - height: 100%; - - @media (max-width: 1200px) { - min-width: 80vw; - } - - @media (min-width: 1199.98px) { - min-width: 957px; - } - - @media (min-width: 991.98px) { - margin: 0.1vh auto; - } - - // fullscreen on mobile - @media (max-width: 992px) { - max-width: unset; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: 0; - padding: 0; - - .modal-content { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: 0; - } - } -` - -export const ModalBody = styled(ReactstrapModalBody)` - max-width: 100%; - - @media (min-width: 991.98px) { - max-height: 66vh; - margin: auto; - } - - overflow-y: auto; - - // prettier-ignore - background: - linear-gradient(#ffffff 33%, rgba(255,255,255, 0)), - linear-gradient(rgba(255,255,255, 0), #ffffff 66%) 0 100%, - radial-gradient(farthest-side at 50% 0, rgba(119,119,119, 0.5), rgba(0,0,0,0)), - radial-gradient(farthest-side at 50% 100%, rgba(119,119,119, 0.5), rgba(0,0,0,0)) 0 100%; - background-color: #ffffff; - background-repeat: no-repeat; - background-attachment: local, local, scroll, scroll; - background-size: 100% 24px, 100% 24px, 100% 8px, 100% 8px; - - h1:first-child, - h2:first-child { - border-top: none; - padding-top: 0; - margin-top: 0; - } - - code { - padding: 2px; - background-color: #eaeaea; - border-radius: 2px; - overflow-wrap: break-word; - white-space: pre-wrap; - } - - pre { - padding: 2px; - background-color: #eaeaea; - border-radius: 2px; - overflow-wrap: break-word; - white-space: pre-wrap; - } -` - -export const ModalFooter = styled(ReactstrapModalFooter)` - margin: 0; - padding: 0; -` - -export const H1 = styled.h1` - font-size: 2.5rem; - font-weight: bold; - - @media (max-width: 992px) { - font-size: 2rem; - } -` - -export const H2 = styled.h2` - border-top: #ccc solid 1px; - padding-top: 1rem; - font-size: 2rem; - font-weight: bold; - margin-top: 2rem; - - @media (max-width: 992px) { - font-size: 1.75rem; - margin-top: 1.25rem; - } -` - -export const H3 = styled.h3` - font-size: 1.75rem; - font-weight: bold; - margin-top: 2.5rem; - - @media (max-width: 992px) { - font-size: 1.5rem; - margin-top: 1.5rem; - } -` - -export const H4 = styled.h4` - font-size: 1.33rem; - font-weight: bold; - margin-top: 2rem; - - @media (max-width: 992px) { - font-size: 1.2rem; - margin-top: 1.2rem; - } -` - -export const H5 = styled.h5` - font-size: 1.1rem; - font-weight: bold; - margin-top: 1.1rem; - - @media (max-width: 992px) { - font-size: 1rem; - margin-top: 1.1rem; - } -` - -export const H6 = styled.h6` - font-size: 1rem; - font-weight: bold; -` - -export const Blockquote = styled.blockquote` - padding: 6px 8px; - border-radius: 3px; - background-color: #f4ebbd; -` - -const components = { h1: H1, h2: H2, h3: H3, h4: H4, h5: H5, h6: H6, a: LinkExternal, blockquote: Blockquote } - export function WhatsNewButton() { const { t } = useTranslation() const [showChangelog, setShowChangelog] = useRecoilState(changelogIsShownAtom) - const [showChangelogOnUpdate, setShowChangelogOnUpdate] = useRecoilState(changelogShouldShowOnUpdatesAtom) - const toggleOpen = useCallback(() => { + const toggle = useCallback(() => { setShowChangelog((showChangelog) => !showChangelog) }, [setShowChangelog]) - const open = useCallback(() => { - setShowChangelog(true) - }, [setShowChangelog]) - - const close = useCallback(() => { - setShowChangelog(false) - }, [setShowChangelog]) - const text = t("What's new") - const closeText = t('Close this window') + + const dialog = useMemo(() => { + if (!showChangelog) { + return null + } + return + }, [showChangelog, toggle]) return ( <> - + {text} - - -

{text}

-
- - - - - - - - - - - -
- - {t('Show when a new version is available')} - -
- -
- - - - - {t('OK')} - - - -
-
-
+ {dialog} ) } diff --git a/packages_rs/nextclade-web/src/components/Layout/WhatsNewDialog.tsx b/packages_rs/nextclade-web/src/components/Layout/WhatsNewDialog.tsx new file mode 100644 index 000000000..d217fd931 --- /dev/null +++ b/packages_rs/nextclade-web/src/components/Layout/WhatsNewDialog.tsx @@ -0,0 +1,232 @@ +import { MDXProvider } from '@mdx-js/react' +import React from 'react' +import { + Button, + ButtonProps, + Col, + Container, + Modal as ReactstrapModal, + ModalBody as ReactstrapModalBody, + ModalFooter as ReactstrapModalFooter, + ModalHeader as ReactstrapModalHeader, + Row, +} from 'reactstrap' +import { useRecoilState } from 'recoil' +import { Toggle } from 'src/components/Common/Toggle' +import { LinkExternal } from 'src/components/Link/LinkExternal' +import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import { changelogShouldShowOnUpdatesAtom } from 'src/state/settings.state' +import styled from 'styled-components' +import Changelog from '../../../../../CHANGELOG.md' + +export const ButtonOk = styled(Button)` + width: 100px; +` +export const ModalHeader = styled(ReactstrapModalHeader)` + .modal-title { + width: 100%; + } + + @media (max-width: 992px) { + padding: 0.25rem; + margin: 0.5rem; + margin-bottom: 0; + } +` +export const Modal = styled(ReactstrapModal)` + height: 100%; + + @media (max-width: 1200px) { + min-width: 80vw; + } + + @media (min-width: 1199.98px) { + min-width: 957px; + } + + @media (min-width: 991.98px) { + margin: 0.1vh auto; + } + + // fullscreen on mobile + @media (max-width: 992px) { + max-width: unset; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: 0; + padding: 0; + + .modal-content { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: 0; + } + } +` +export const ModalBody = styled(ReactstrapModalBody)` + max-width: 100%; + + @media (min-width: 991.98px) { + max-height: 66vh; + margin: auto; + } + + overflow-y: auto; + + // prettier-ignore + background: + linear-gradient(#ffffff 33%, rgba(255,255,255, 0)), + linear-gradient(rgba(255,255,255, 0), #ffffff 66%) 0 100%, + radial-gradient(farthest-side at 50% 0, rgba(119,119,119, 0.5), rgba(0,0,0,0)), + radial-gradient(farthest-side at 50% 100%, rgba(119,119,119, 0.5), rgba(0,0,0,0)) 0 100%; + background-color: #ffffff; + background-repeat: no-repeat; + background-attachment: local, local, scroll, scroll; + background-size: 100% 24px, 100% 24px, 100% 8px, 100% 8px; + + h1:first-child, + h2:first-child { + border-top: none; + padding-top: 0; + margin-top: 0; + } + + code { + padding: 2px; + background-color: #eaeaea; + border-radius: 2px; + overflow-wrap: break-word; + white-space: pre-wrap; + } + + pre { + padding: 2px; + background-color: #eaeaea; + border-radius: 2px; + overflow-wrap: break-word; + white-space: pre-wrap; + } +` +export const ModalFooter = styled(ReactstrapModalFooter)` + margin: 0; + padding: 0; +` +export const H1 = styled.h1` + font-size: 2.5rem; + font-weight: bold; + + @media (max-width: 992px) { + font-size: 2rem; + } +` +export const H2 = styled.h2` + border-top: #ccc solid 1px; + padding-top: 1rem; + font-size: 2rem; + font-weight: bold; + margin-top: 2rem; + + @media (max-width: 992px) { + font-size: 1.75rem; + margin-top: 1.25rem; + } +` +export const H3 = styled.h3` + font-size: 1.75rem; + font-weight: bold; + margin-top: 2.5rem; + + @media (max-width: 992px) { + font-size: 1.5rem; + margin-top: 1.5rem; + } +` +export const H4 = styled.h4` + font-size: 1.33rem; + font-weight: bold; + margin-top: 2rem; + + @media (max-width: 992px) { + font-size: 1.2rem; + margin-top: 1.2rem; + } +` +export const H5 = styled.h5` + font-size: 1.1rem; + font-weight: bold; + margin-top: 1.1rem; + + @media (max-width: 992px) { + font-size: 1rem; + margin-top: 1.1rem; + } +` +export const H6 = styled.h6` + font-size: 1rem; + font-weight: bold; +` +export const Blockquote = styled.blockquote` + padding: 6px 8px; + border-radius: 3px; + background-color: #f4ebbd; +` +export const components = { h1: H1, h2: H2, h3: H3, h4: H4, h5: H5, h6: H6, a: LinkExternal, blockquote: Blockquote } + +export interface WhatsNewDialogProps { + isOpen: boolean + toggle: () => void +} + +export default function WhatsNewDialog({ isOpen, toggle }: WhatsNewDialogProps) { + const { t } = useTranslationSafe() + const [showChangelogOnUpdate, setShowChangelogOnUpdate] = useRecoilState(changelogShouldShowOnUpdatesAtom) + const text = t("What's new") + const closeText = t('Close this window') + + return ( + + +

{text}

+
+ + + + + + + + + + + +
+ + {t('Show when a new version is available')} + +
+ +
+ + + + + {t('OK')} + + + +
+
+
+ ) +} diff --git a/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx b/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx index 1288ae3b9..b870e31df 100644 --- a/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx +++ b/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx @@ -1,4 +1,3 @@ -import dynamic from 'next/dynamic' import React, { Suspense } from 'react' import { useRecoilValue } from 'recoil' import styled from 'styled-components' diff --git a/packages_rs/nextclade-web/tools/server/dataServer.ts b/packages_rs/nextclade-web/tools/server/dataServer.ts index 2176dc3a0..600c3d843 100644 --- a/packages_rs/nextclade-web/tools/server/dataServer.ts +++ b/packages_rs/nextclade-web/tools/server/dataServer.ts @@ -14,7 +14,7 @@ import type { ServerResponse } from 'http' import path from 'path' import express from 'express' - +import compression from 'compression' import allowMethods from 'allow-methods' import history from 'connect-history-api-fallback' import expressStaticGzip from 'express-static-gzip' @@ -36,6 +36,8 @@ export interface NewHeaders { function main() { const app = express() + app.use(compression()) + app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { const newHeaders = modifyHeaders({ request: req, response: res }) as NewHeaders Object.entries(newHeaders).forEach(([header, arr]) => { diff --git a/packages_rs/nextclade-web/tools/server/server.ts b/packages_rs/nextclade-web/tools/server/server.ts index cb1bdd689..9532984c7 100644 --- a/packages_rs/nextclade-web/tools/server/server.ts +++ b/packages_rs/nextclade-web/tools/server/server.ts @@ -14,7 +14,7 @@ import type { ServerResponse } from 'http' import path from 'path' import express from 'express' - +import compression from 'compression' import allowMethods from 'allow-methods' // import history from 'connect-history-api-fallback' import expressStaticGzip from 'express-static-gzip' @@ -54,6 +54,8 @@ function main() { }, } + app.use(compression()) + app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { const newHeaders = modifyHeaders({ request: req, response: res }) as NewHeaders Object.entries(newHeaders).forEach(([header, arr]) => { diff --git a/packages_rs/nextclade-web/yarn.lock b/packages_rs/nextclade-web/yarn.lock index 20668abcc..fbf76f473 100644 --- a/packages_rs/nextclade-web/yarn.lock +++ b/packages_rs/nextclade-web/yarn.lock @@ -2240,6 +2240,13 @@ tapable "^2.2.0" webpack "^5.51.0" +"@types/compression@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@types/compression/-/compression-1.7.2.tgz#7cc1cdb01b4730eea284615a68fc70a2cdfd5e71" + integrity sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg== + dependencies: + "@types/express" "*" + "@types/connect-history-api-fallback@1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" @@ -2340,7 +2347,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@4.17.13": +"@types/express@*", "@types/express@4.17.13": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -4720,7 +4727,7 @@ compression-webpack-plugin@9.2.0, compression-webpack-plugin@^3.0.1: schema-utils "^4.0.0" serialize-javascript "^6.0.0" -compression@^1.7.3: +compression@1.7.4, compression@^1.7.3: version "1.7.4" resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== From fbaa6461bfa75ab2f70126b8a30bb6ec5b404c77 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Sun, 28 Aug 2022 19:34:01 +0200 Subject: [PATCH 6/6] perf: reduce dom depth by coverting inline svgs into img tags --- .../nextclade-web/config/next/withSvg.ts | 3 ++- .../src/components/Common/LogoGisaid.tsx | 2 +- .../src/components/Layout/Footer.tsx | 14 +++++------ .../components/Layout/LanguageSwitcher.tsx | 5 ++-- .../src/components/Layout/NavigationBar.tsx | 2 +- .../src/components/Loading/Loading.tsx | 2 +- .../src/components/Main/CladeSchema.tsx | 3 ++- .../src/components/Team/TeamCredits.tsx | 2 +- .../components/Tree/LogoPoweredByAuspice.tsx | 2 +- packages_rs/nextclade-web/src/i18n/i18n.ts | 24 +++++++++---------- .../nextclade-web/src/types/svg-module.d.ts | 5 ++-- 11 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages_rs/nextclade-web/config/next/withSvg.ts b/packages_rs/nextclade-web/config/next/withSvg.ts index e4a0ddebf..625732084 100644 --- a/packages_rs/nextclade-web/config/next/withSvg.ts +++ b/packages_rs/nextclade-web/config/next/withSvg.ts @@ -11,9 +11,10 @@ export default function withSvg(nextConfig: NextConfig) { loader: '@svgr/webpack', options: { removeViewbox: false, - typescript: false, + typescript: true, }, }, + 'url-loader', ], })) } diff --git a/packages_rs/nextclade-web/src/components/Common/LogoGisaid.tsx b/packages_rs/nextclade-web/src/components/Common/LogoGisaid.tsx index 107775164..be54d8143 100644 --- a/packages_rs/nextclade-web/src/components/Common/LogoGisaid.tsx +++ b/packages_rs/nextclade-web/src/components/Common/LogoGisaid.tsx @@ -2,7 +2,7 @@ import React, { ReactNode } from 'react' import styled from 'styled-components' -import GisaidLogoBase from 'src/assets/img/gisaid-logo.svg' +import { ReactComponent as GisaidLogoBase } from 'src/assets/img/gisaid-logo.svg' import { LinkExternal } from 'src/components/Link/LinkExternal' export interface LogoGisaidProps { diff --git a/packages_rs/nextclade-web/src/components/Layout/Footer.tsx b/packages_rs/nextclade-web/src/components/Layout/Footer.tsx index 1568721c2..5f3e46249 100644 --- a/packages_rs/nextclade-web/src/components/Layout/Footer.tsx +++ b/packages_rs/nextclade-web/src/components/Layout/Footer.tsx @@ -9,14 +9,12 @@ import { getCopyrightYearRange } from 'src/helpers/getCopyrightYearRange' import { LinkExternal } from 'src/components/Link/LinkExternal' import { getVersionString } from 'src/helpers/getVersionString' -import LogoBedfordlab from 'src/assets/img/bedfordlab.svg' -import LogoBiozentrum from 'src/assets/img/biozentrum_square.svg' -import LogoSib from 'src/assets/img/sib.logo.svg' -import LogoFredHutch from 'src/assets/img/fred_hutch.svg' -import LogoNeherlab from 'src/assets/img/neherlab.svg' -// impoas from from 'src/assets/img/nextstrain_logo.svg' -// impoas from from 'src/assets/img/unibas.svg' -import LogoVercel from 'src/assets/img/powered-by-vercel.svg' +import { ReactComponent as LogoBedfordlab } from 'src/assets/img/bedfordlab.svg' +import { ReactComponent as LogoBiozentrum } from 'src/assets/img/biozentrum_square.svg' +import { ReactComponent as LogoSib } from 'src/assets/img/sib.logo.svg' +import { ReactComponent as LogoFredHutch } from 'src/assets/img/fred_hutch.svg' +import { ReactComponent as LogoNeherlab } from 'src/assets/img/neherlab.svg' +import { ReactComponent as LogoVercel } from 'src/assets/img/powered-by-vercel.svg' const FooterContainer = styled(Container)` background-color: #2a2a2a; diff --git a/packages_rs/nextclade-web/src/components/Layout/LanguageSwitcher.tsx b/packages_rs/nextclade-web/src/components/Layout/LanguageSwitcher.tsx index 5b59f0f05..6e5a2de07 100644 --- a/packages_rs/nextclade-web/src/components/Layout/LanguageSwitcher.tsx +++ b/packages_rs/nextclade-web/src/components/Layout/LanguageSwitcher.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @next/next/no-img-element */ import React, { useCallback, useState } from 'react' import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem, DropdownProps } from 'reactstrap' import { useRecoilState } from 'recoil' @@ -33,10 +34,10 @@ export function LanguageSwitcher({ ...restProps }: LanguageSwitcherProps) { } export function LanguageSwitcherItem({ locale }: { locale: Locale }) { - const { Flag, name } = locale + const { flagIconUrl, name } = locale return ( <> - + {name} {name} ) diff --git a/packages_rs/nextclade-web/src/components/Layout/NavigationBar.tsx b/packages_rs/nextclade-web/src/components/Layout/NavigationBar.tsx index 4e17dbd8e..bac647f86 100644 --- a/packages_rs/nextclade-web/src/components/Layout/NavigationBar.tsx +++ b/packages_rs/nextclade-web/src/components/Layout/NavigationBar.tsx @@ -10,7 +10,7 @@ import { useTranslationSafe } from 'src/helpers/useTranslationSafe' import { Link } from 'src/components/Link/Link' import { LinkExternal } from 'src/components/Link/LinkExternal' -import BrandLogo from 'src/assets/img/nextstrain_logo.svg' +import { ReactComponent as BrandLogo } from 'src/assets/img/nextstrain_logo.svg' import { CitationButton } from 'src/components/Citation/CitationButton' import { WhatsNewButton } from './WhatsNewButton' diff --git a/packages_rs/nextclade-web/src/components/Loading/Loading.tsx b/packages_rs/nextclade-web/src/components/Loading/Loading.tsx index 8292503c3..68403d0e5 100644 --- a/packages_rs/nextclade-web/src/components/Loading/Loading.tsx +++ b/packages_rs/nextclade-web/src/components/Loading/Loading.tsx @@ -2,7 +2,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import LogoNextstrain from 'src/assets/img/nextstrain_logo.svg' +import { ReactComponent as LogoNextstrain } from 'src/assets/img/nextstrain_logo.svg' import styled from 'styled-components' const Container = styled.div` diff --git a/packages_rs/nextclade-web/src/components/Main/CladeSchema.tsx b/packages_rs/nextclade-web/src/components/Main/CladeSchema.tsx index c27be8c77..9570ace61 100644 --- a/packages_rs/nextclade-web/src/components/Main/CladeSchema.tsx +++ b/packages_rs/nextclade-web/src/components/Main/CladeSchema.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @next/next/no-img-element */ import React from 'react' import { useTranslation } from 'react-i18next' @@ -27,7 +28,7 @@ export function CladeSchema() { return ( - + Clade schema diff --git a/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx b/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx index 8722927e5..66b53c022 100644 --- a/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx +++ b/packages_rs/nextclade-web/src/components/Team/TeamCredits.tsx @@ -14,7 +14,7 @@ import { getContributors } from 'src/io/getContributors' import { LinkExternal } from 'src/components/Link/LinkExternal' import type { MaintainerInfo } from 'src/components/Team/TeamCreditsMaintainer' import { TeamCreditsContributor } from 'src/components/Team/TeamCreditsContributor' -import NextstrainLogo from 'src/assets/img/nextstrain_logo.svg' +import { ReactComponent as NextstrainLogo } from 'src/assets/img/nextstrain_logo.svg' import { FlexCol, FlexContributors, TeamCreditsH1 } from './TeamCreditsStyles' const maintainers: MaintainerInfo[] = [ diff --git a/packages_rs/nextclade-web/src/components/Tree/LogoPoweredByAuspice.tsx b/packages_rs/nextclade-web/src/components/Tree/LogoPoweredByAuspice.tsx index 74b8834d6..da2e7be73 100644 --- a/packages_rs/nextclade-web/src/components/Tree/LogoPoweredByAuspice.tsx +++ b/packages_rs/nextclade-web/src/components/Tree/LogoPoweredByAuspice.tsx @@ -1,7 +1,7 @@ import React from 'react' import styled from 'styled-components' -import LogoAuspiceSvg from 'src/assets/img/auspice-logo.svg' +import { ReactComponent as LogoAuspiceSvg } from 'src/assets/img/auspice-logo.svg' import { LinkExternal } from 'src/components/Link/LinkExternal' export const LogoPoweredByAuspiceContainer = styled(LinkExternal)` diff --git a/packages_rs/nextclade-web/src/i18n/i18n.ts b/packages_rs/nextclade-web/src/i18n/i18n.ts index 51b339aba..dad8e7d5b 100644 --- a/packages_rs/nextclade-web/src/i18n/i18n.ts +++ b/packages_rs/nextclade-web/src/i18n/i18n.ts @@ -1,5 +1,3 @@ -import { ElementType, FC } from 'react' - import type { StrictOmit } from 'ts-essentials' import { mapValues } from 'lodash' @@ -50,20 +48,20 @@ export interface Locale { readonly key: LocaleKey readonly full: string readonly name: string - readonly Flag: ElementType + readonly flagIconUrl: string } export const locales: Record = { - en: { key: 'en', full: 'en-US', name: languages.en.native, Flag: GB as FC }, - ar: { key: 'ar', full: 'ar-SA', name: languages.ar.native, Flag: SA as FC }, - de: { key: 'de', full: 'de-DE', name: languages.de.native, Flag: DE as FC }, - es: { key: 'es', full: 'es-ES', name: languages.es.native, Flag: ES as FC }, - fr: { key: 'fr', full: 'fr-FR', name: languages.fr.native, Flag: FR as FC }, - it: { key: 'it', full: 'it-IT', name: languages.it.native, Flag: IT as FC }, - ko: { key: 'ko', full: 'ko-KR', name: languages.ko.native, Flag: KR as FC }, - pt: { key: 'pt', full: 'pt-PT', name: languages.pt.native, Flag: PT as FC }, - ru: { key: 'ru', full: 'ru-RU', name: languages.ru.native, Flag: RU as FC }, - zh: { key: 'zh', full: 'zh-CN', name: languages.zh.native, Flag: CN as FC }, + en: { key: 'en', full: 'en-US', name: languages.en.native, flagIconUrl: GB as string }, + ar: { key: 'ar', full: 'ar-SA', name: languages.ar.native, flagIconUrl: SA as string }, + de: { key: 'de', full: 'de-DE', name: languages.de.native, flagIconUrl: DE as string }, + es: { key: 'es', full: 'es-ES', name: languages.es.native, flagIconUrl: ES as string }, + fr: { key: 'fr', full: 'fr-FR', name: languages.fr.native, flagIconUrl: FR as string }, + it: { key: 'it', full: 'it-IT', name: languages.it.native, flagIconUrl: IT as string }, + ko: { key: 'ko', full: 'ko-KR', name: languages.ko.native, flagIconUrl: KR as string }, + pt: { key: 'pt', full: 'pt-PT', name: languages.pt.native, flagIconUrl: PT as string }, + ru: { key: 'ru', full: 'ru-RU', name: languages.ru.native, flagIconUrl: RU as string }, + zh: { key: 'zh', full: 'zh-CN', name: languages.zh.native, flagIconUrl: CN as string }, } as const export const localeKeys = Object.keys(locales) diff --git a/packages_rs/nextclade-web/src/types/svg-module.d.ts b/packages_rs/nextclade-web/src/types/svg-module.d.ts index 5b3be85ac..973c348ad 100644 --- a/packages_rs/nextclade-web/src/types/svg-module.d.ts +++ b/packages_rs/nextclade-web/src/types/svg-module.d.ts @@ -1,6 +1,7 @@ declare module '*.svg' { import type { FC, SVGProps } from 'react' - declare const SVG: FC> - export default SVG + declare const ReactComponent: FC> + declare const url: string + export { ReactComponent, url as default } }