From d67bb2fcddd9f9f5fbf67a3831361e8ed1888b57 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 21 Sep 2023 07:44:54 +0200 Subject: [PATCH] feat(web): scaffold multi-step wizard on main page --- packages_rs/nextclade-web/package.json | 1 + .../src/components/About/About.tsx | 7 + .../src/components/About/AboutContent.mdx | 43 +++ .../src/components/Layout/Layout.tsx | 4 +- .../src/components/Main/DatasetSelector.tsx | 34 +- .../components/Main/DatasetSelectorList.tsx | 43 ++- .../src/components/Main/Downloads.tsx | 168 +++++++++ .../src/components/Main/MainInputForm.tsx | 337 +++++++++++++++++- .../src/components/Main/MainPage.tsx | 13 +- .../src/components/Main/MainSectionInfo.tsx | 26 ++ .../src/components/Main/MainSectionTitle.tsx | 19 + .../src/components/Main/Title.tsx | 82 +++++ .../nextclade-web/src/pages/_error.tsx | 3 + packages_rs/nextclade-web/yarn.lock | 5 + 14 files changed, 747 insertions(+), 38 deletions(-) create mode 100644 packages_rs/nextclade-web/src/components/About/About.tsx create mode 100644 packages_rs/nextclade-web/src/components/About/AboutContent.mdx create mode 100644 packages_rs/nextclade-web/src/components/Main/Downloads.tsx create mode 100644 packages_rs/nextclade-web/src/components/Main/MainSectionInfo.tsx create mode 100644 packages_rs/nextclade-web/src/components/Main/MainSectionTitle.tsx create mode 100644 packages_rs/nextclade-web/src/components/Main/Title.tsx diff --git a/packages_rs/nextclade-web/package.json b/packages_rs/nextclade-web/package.json index 7c5b95673..3c6f8d524 100644 --- a/packages_rs/nextclade-web/package.json +++ b/packages_rs/nextclade-web/package.json @@ -143,6 +143,7 @@ "react-resize-detector": "7.0.0", "react-select": "5.3.0", "react-toggle": "4.1.2", + "react-use-wizard": "2.2.3", "react-virtualized-auto-sizer": "1.0.6", "react-virtualized-select": "3.1.3", "react-window": "1.8.7", diff --git a/packages_rs/nextclade-web/src/components/About/About.tsx b/packages_rs/nextclade-web/src/components/About/About.tsx new file mode 100644 index 000000000..7f15d6fde --- /dev/null +++ b/packages_rs/nextclade-web/src/components/About/About.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +import AboutContent from './AboutContent.mdx' + +export function About() { + return +} diff --git a/packages_rs/nextclade-web/src/components/About/AboutContent.mdx b/packages_rs/nextclade-web/src/components/About/AboutContent.mdx new file mode 100644 index 000000000..9f97e267a --- /dev/null +++ b/packages_rs/nextclade-web/src/components/About/AboutContent.mdx @@ -0,0 +1,43 @@ +import { CladeSchema } from 'src/components/Main/CladeSchema.tsx' + +## What is Nextclade? + +Nextclade is a tool that performs genetic sequence alignment, clade assignment, mutation calling, phylogenetic placement, and quality checks for SARS-CoV-2, Influenza (Flu), Mpox (Monkeypox), Respiratory Syncytial Virus (RSV) and other pathogens. + +Nextclade identifies differences between your sequences and a reference sequence, uses these differences to assign your sequences to clades, reports potential sequence quality issues in your data, and shows how the sequences are related to each other by placing them into an existing phylogenetic tree (we call it "phylogenetic placement"). You can use the tool to analyze sequences before you upload them to a database, or if you want to assign Nextstrain clades to a set of sequences. + +To analyze your data, drag a fasta file onto the upload box or paste sequences into the text box. These sequences will then be analyzed in your browser - data never leave your computer. Since your computer is doing the work rather than a server, it is advisable to analyze at most a few hundred sequences at a time. + +The Nextclade app and algorithms are opensource. The code is available on [GitHub](https://github.com/nextstrain/nextclade). The user manual is available at [docs.nextstrain.org/projects/nextclade](https://docs.nextstrain.org/projects/nextclade). + + +### What are the SARS-CoV-2 clades? + +Nextclade was originally developed during COVID-19 pandemic, primarily focused on SARS-CoV-2. This section describes clades with application to SARS-CoV-2, but Nextclade can analyse other pathogens too. + + + +Since its emergence in late 2019, SARS-CoV-2 has diversified into several different co-circulating variants. To facilitate discussion of these variants, we have grouped them into __clades__ which are defined by specific signature mutations. + +We currently define more than 30 clades (see [this blog post](https://nextstrain.org/blog/2021-01-06-updated-SARS-CoV-2-clade-naming) for details): + +- 19A and 19B emerged in Wuhan and have dominated the early outbreak +- 20A emerged from 19A out of dominated the European outbreak in March and has since spread globally +- 20B and 20C are large genetically distinct subclades 20A emerged in early 2020 +- 20D to 20J have emerged over the summer of 2020 and include three "Variants of Concern" (VoC). +- 21A to 21F include the VoC __delta__ and several Variants of Interest (VoI). +- 21K onwards are different clades within the diverse VoC __omicron__. + +Within Nextstrain, we define each clade by its combination of signature mutations. You can find the exact clade definition in [github.com/nextstrain/ncov/defaults/clades.tsv](https://github.com/nextstrain/ncov/blob/master/defaults/clades.tsv). When available, we will include [WHO labels for VoCs and VoIs](https://www.who.int/en/activities/tracking-SARS-CoV-2-variants/). + +Learn more about how Nextclade assigns clades in the [documentation](https://docs.nextstrain.org/projects/nextclade/en/stable/user/algorithm/). + +### Other pathogens + +Besides SARS-CoV-2, we provide Nextclade datasets to analyze the following other pathogens: + + * Seasonal Influenza viruses (HA and NA for A/H3N2, A/H1N1pdm, B/Vic, and B/Yam) + * Mpox virus (the overall clade structure, as well as fine-grained lineages within the recent sustained human-to-human transmission) + * Respiratory Syncytial Virus (RSV) (subtypes A and B) + +You can also put together your own dataset to analyse other pathogens. diff --git a/packages_rs/nextclade-web/src/components/Layout/Layout.tsx b/packages_rs/nextclade-web/src/components/Layout/Layout.tsx index ef0d56bb6..c160d0316 100644 --- a/packages_rs/nextclade-web/src/components/Layout/Layout.tsx +++ b/packages_rs/nextclade-web/src/components/Layout/Layout.tsx @@ -19,7 +19,9 @@ const HeaderWrapper = styled.header` ` const MainWrapper = styled.main` - flex: auto; + display: flex; + flex-direction: column; + flex: 1; overflow: hidden; height: 100%; width: 100%; diff --git a/packages_rs/nextclade-web/src/components/Main/DatasetSelector.tsx b/packages_rs/nextclade-web/src/components/Main/DatasetSelector.tsx index ca7f290b4..11a26febc 100644 --- a/packages_rs/nextclade-web/src/components/Main/DatasetSelector.tsx +++ b/packages_rs/nextclade-web/src/components/Main/DatasetSelector.tsx @@ -1,18 +1,23 @@ import React, { HTMLProps, useState } from 'react' -import { useRecoilState, useRecoilValue } from 'recoil' +import { useRecoilValue } from 'recoil' +import { Container as ContainerBase } from 'reactstrap' import styled from 'styled-components' import { ThreeDots } from 'react-loader-spinner' -import { SuggestionPanel } from 'src/components/Main/SuggestionPanel' +import type { Dataset } from 'src/types' import { useTranslationSafe } from 'src/helpers/useTranslationSafe' -import { datasetCurrentAtom, datasetsAtom } from 'src/state/dataset.state' +import { datasetsAtom } from 'src/state/dataset.state' import { SearchBox } from 'src/components/Common/SearchBox' import { DatasetSelectorList } from 'src/components/Main/DatasetSelectorList' -export function DatasetSelector() { +export interface DatasetSelectorProps { + datasetHighlighted?: Dataset + onDatasetHighlighted?(dataset?: Dataset): void +} + +export function DatasetSelector({ datasetHighlighted, onDatasetHighlighted }: DatasetSelectorProps) { const { t } = useTranslationSafe() const [searchTerm, setSearchTerm] = useState('') const { datasets } = useRecoilValue(datasetsAtom) - const [datasetCurrent, setDatasetCurrent] = useRecoilState(datasetCurrentAtom) const isBusy = datasets.length === 0 @@ -28,9 +33,9 @@ export function DatasetSelector() { {!isBusy && ( )} @@ -42,19 +47,14 @@ export function DatasetSelector() { )} - -
- -
) } -const Container = styled.div` +const Container = styled(ContainerBase)` display: flex; flex: 1; flex-direction: column; - height: 100%; overflow: hidden; margin-right: 10px; ` @@ -74,10 +74,10 @@ const Main = styled.div` overflow: hidden; ` -const Footer = styled.div` - display: flex; - flex: 0; -` +// const Footer = styled.div` +// display: flex; +// flex: 0; +// ` const Title = styled.h4` flex: 1; diff --git a/packages_rs/nextclade-web/src/components/Main/DatasetSelectorList.tsx b/packages_rs/nextclade-web/src/components/Main/DatasetSelectorList.tsx index 2bb5be734..0af09ef1c 100644 --- a/packages_rs/nextclade-web/src/components/Main/DatasetSelectorList.tsx +++ b/packages_rs/nextclade-web/src/components/Main/DatasetSelectorList.tsx @@ -12,16 +12,31 @@ import { autodetectRunStateAtom, groupByDatasets, } from 'src/state/autodetect.state' +import { minimizerIndexVersionAtom } from 'src/state/dataset.state' import type { Dataset } from 'src/types' import { areDatasetsEqual } from 'src/types' import styled from 'styled-components' +// HACK: dataset entry for 'autodetect' option. This is not a real dataset. +const DATASET_AUTODETECT: Dataset = { + path: 'autodetect', + enabled: true, + official: true, + attributes: { + name: { value: 'autodetect', valueFriendly: 'Autodetect' }, + reference: { value: 'autodetect', valueFriendly: 'Autodetect' }, + }, + files: { + reference: '', + pathogenJson: '', + }, +} + export interface DatasetSelectorListProps { datasets: Dataset[] searchTerm: string datasetHighlighted?: Dataset - - onDatasetHighlighted(dataset?: Dataset): void + onDatasetHighlighted?(dataset?: Dataset): void } export function DatasetSelectorList({ @@ -30,8 +45,8 @@ export function DatasetSelectorList({ datasetHighlighted, onDatasetHighlighted, }: DatasetSelectorListProps) { - const onItemClick = useCallback((dataset: Dataset) => () => onDatasetHighlighted(dataset), [onDatasetHighlighted]) - + const onItemClick = useCallback((dataset: Dataset) => () => onDatasetHighlighted?.(dataset), [onDatasetHighlighted]) + const minimizerIndexVersion = useRecoilValue(minimizerIndexVersionAtom) const autodetectResults = useRecoilValue(autodetectResultsAtom) const [autodetectRunState, setAutodetectRunState] = useRecoilState(autodetectRunStateAtom) @@ -89,14 +104,30 @@ export function DatasetSelectorList({ useEffect(() => { const topSuggestion = autodetectResult.itemsInclude[0] if (autodetectRunState === AutodetectRunState.Done) { - onDatasetHighlighted(topSuggestion) + onDatasetHighlighted?.(topSuggestion) setAutodetectRunState(AutodetectRunState.Idle) } }, [autodetectRunState, autodetectResult.itemsInclude, onDatasetHighlighted, setAutodetectRunState]) + const autodetectItem = useMemo(() => { + if (isNil(minimizerIndexVersion)) { + return null + } + + return ( + + ) + }, [datasetHighlighted, minimizerIndexVersion, onItemClick]) + const listItems = useMemo(() => { return ( <> + {autodetectItem} + {[itemsStartWith, itemsInclude].map((datasets) => datasets.map((dataset) => ( ) - }, [datasetHighlighted, itemsInclude, itemsNotInclude, itemsStartWith, onItemClick]) + }, [autodetectItem, datasetHighlighted, itemsInclude, itemsNotInclude, itemsStartWith, onItemClick]) return
    {listItems}
} diff --git a/packages_rs/nextclade-web/src/components/Main/Downloads.tsx b/packages_rs/nextclade-web/src/components/Main/Downloads.tsx new file mode 100644 index 000000000..d0c7a1597 --- /dev/null +++ b/packages_rs/nextclade-web/src/components/Main/Downloads.tsx @@ -0,0 +1,168 @@ +import React, { HTMLProps, ReactNode } from 'react' + +import { useTranslationSafe as useTranslation } from 'src/helpers/useTranslationSafe' +import { FaBook, FaDocker, FaDownload, FaGithub, FaGlobeAmericas } from 'react-icons/fa' +import { + Card as ReactstrapCard, + CardBody as ReactstrapCardBody, + CardHeader as ReactstrapCardHeader, + Col, + Row, +} from 'reactstrap' +import styled from 'styled-components' + +import { LinkExternal as LinkExternalBase } from 'src/components/Link/LinkExternal' + +const DownloadLinkList = styled.ul` + display: flex; + flex-direction: column; + list-style: none; + padding: 0; +` + +const DownloadLinkListItem = styled.li` + display: flex; + flex: 1; + margin: auto; +` + +const LinkExternal = styled(LinkExternalBase)` + width: 200px; + height: 55px; + margin: 0.25rem; + padding: 1rem; +` + +const Card = styled(ReactstrapCard)` + margin: 5px; + height: 100%; +` + +const CardBody = styled(ReactstrapCardBody)` + padding: 0.5rem; +` + +const CardHeader = styled(ReactstrapCardHeader)` + padding: 1rem; +` + +const iconDownload = +const iconGithub = +const iconDocker = +const iconBook = +const iconGlobe = + +export interface DownloadLinkProps extends HTMLProps { + Icon: ReactNode + text: string + url: string +} + +export function DownloadLink({ Icon, text, url }: DownloadLinkProps) { + return ( + + + {Icon} + {text} + + + ) +} + +export function Downloads() { + const { t } = useTranslation() + + return ( + + + + +

{t('For more advanced use-cases:')}

+ +
+ + + + + +

{'Nextclade CLI'}

+

{t('faster, more configurable command-line version of this application')}

+
+ + + + + + + + +
+ + + + + +

{'Nextalign CLI'}

+

+ {t('pairwise reference alignment and translation tool used by Nextclade')} +

+
+ + + + + + + + +
+ + + + + +

{'Nextstrain'}

+

+ {t('our parent project, an open-source initiative to harness the potential of pathogen genome data')} +

+
+ + + + + + + + + +
+ +
+ +
+ ) +} diff --git a/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx b/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx index 94b4b9e84..dd20bd1da 100644 --- a/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx +++ b/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx @@ -1,40 +1,351 @@ -import React from 'react' +import { Dataset } from '_SchemaRoot' +import React, { useCallback, useMemo, useState } from 'react' +import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil' +import { FlexRight } from 'src/components/FilePicker/FilePickerStyles' +import { DatasetCurrent } from 'src/components/Main/DatasetCurrent' +import { MainSectionTitle } from 'src/components/Main/MainSectionTitle' import { QuerySequenceFilePicker } from 'src/components/Main/QuerySequenceFilePicker' +import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import { useRunAnalysis } from 'src/hooks/useRunAnalysis' +import { useRecoilToggle } from 'src/hooks/useToggle' +import { AlgorithmInputDefault } from 'src/io/AlgorithmInput' +import { hasAutodetectResultsAtom } from 'src/state/autodetect.state' +import { datasetCurrentAtom } from 'src/state/dataset.state' +import { globalErrorAtom, hasInputErrorsAtom } from 'src/state/error.state' +import { hasRequiredInputsAtom, useQuerySeqInputs } from 'src/state/inputs.state' +import { canRunAtom } from 'src/state/results.state' +import { shouldRunAutomaticallyAtom } from 'src/state/settings.state' import styled from 'styled-components' -import { Col as ColBase, Row as RowBase } from 'reactstrap' +import { Button, Col as ColBase, Row as RowBase, Form as FormBase } from 'reactstrap' import { useUpdatedDatasetIndex } from 'src/io/fetchDatasets' import { DatasetSelector } from 'src/components/Main/DatasetSelector' +import { FaChevronLeft as IconLeft, FaChevronRight as IconRight } from 'react-icons/fa6' const Container = styled.div` - height: 100%; + display: flex; + flex: 1; + flex-direction: column; overflow: hidden; + margin-right: 10px; +` + +const Header = styled.div` + display: flex; + flex: 0; + padding-left: 10px; margin-top: 10px; + margin-bottom: 3px; +` + +const Main = styled.div` + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; +` + +const Footer = styled.div` + display: flex; + flex: 0; ` const Row = styled(RowBase)` overflow: hidden; - height: 100%; ` const Col = styled(ColBase)` overflow: hidden; - height: 100%; ` export function MainInputForm() { // This periodically fetches dataset index and updates the list of datasets. useUpdatedDatasetIndex() + return +} + +function WizardManualOrAuto() { + const dataset = useRecoilValue(datasetCurrentAtom) + const setDataset = useSetRecoilState(datasetCurrentAtom) + + const [datasetHighlighted, setDatasetHighlighted] = useState() + + const apply = useCallback(() => { + setDataset(datasetHighlighted) + }, [datasetHighlighted, setDataset]) + + const wizard = useMemo(() => { + if (!dataset) { + return ( + + ) + } + if (dataset.path === 'autodetect') { + return ( + + ) + } + return + }, [apply, dataset, datasetHighlighted]) + + return ( + +
{wizard}
+
+ ) +} + +export interface LandingProps { + datasetHighlighted?: Dataset + onDatasetHighlighted?(dataset?: Dataset): void + apply(): void +} + +function Landing({ datasetHighlighted, onDatasetHighlighted, apply }: LandingProps) { + return ( + +
+ +
+
+ +
+
+ +
+
+ ) +} + +function WizardManual() { + const { t } = useTranslationSafe() + + const [datasetCurrent, setDatasetCurrent] = useRecoilState(datasetCurrentAtom) + const { addQryInputs } = useQuerySeqInputs() + const hasRequiredInputs = useRecoilValue(hasRequiredInputsAtom) + const hasInputErrors = useRecoilValue(hasInputErrorsAtom) + const { state: shouldRunAutomatically } = useRecoilToggle(shouldRunAutomaticallyAtom) + const canRun = useRecoilValue(canRunAtom) + const runAnalysis = useRunAnalysis() + + const setExampleSequences = useCallback(() => { + if (datasetCurrent) { + addQryInputs([new AlgorithmInputDefault(datasetCurrent)]) + if (shouldRunAutomatically) { + runAnalysis() + } + } + }, [addQryInputs, datasetCurrent, runAnalysis, shouldRunAutomatically]) + + const { isRunButtonDisabled, runButtonColor, runButtonTooltip } = useMemo(() => { + const isRunButtonDisabled = !(canRun && hasRequiredInputs) || hasInputErrors + return { + isRunButtonDisabled, + runButtonColor: isRunButtonDisabled ? 'secondary' : 'success', + runButtonTooltip: isRunButtonDisabled + ? t('Please provide sequence data for the algorithm') + : t('Launch the algorithm!'), + } + }, [canRun, hasInputErrors, hasRequiredInputs, t]) + + const onPrev = useCallback(() => { + setDatasetCurrent(undefined) + }, [setDatasetCurrent]) + + return ( + +
+ + + + + + + + +
+
+ + + + {t('Previous')} + + + + + + + {t('Launch')} + + + + +
+
+ ) +} + +export interface WizardAutoProps { + datasetHighlighted?: Dataset + onDatasetHighlighted?(dataset?: Dataset): void + apply(): void +} + +function WizardAuto({ datasetHighlighted, onDatasetHighlighted, apply }: WizardAutoProps) { + const { t } = useTranslationSafe() + const resetDataset = useResetRecoilState(datasetCurrentAtom) + const hasAutodetectResults = useRecoilValue(hasAutodetectResultsAtom) + const hasErrors = !!useRecoilValue(globalErrorAtom) + + const { isRunButtonDisabled, runButtonColor, runButtonTooltip } = useMemo(() => { + const isRunButtonDisabled = !hasAutodetectResults || hasErrors + return { + isRunButtonDisabled, + runButtonColor: isRunButtonDisabled ? 'secondary' : 'success', + runButtonTooltip: isRunButtonDisabled + ? t('Please provide sequence data and select one of the datasets') + : t('Go to the next step!'), + } + }, [hasAutodetectResults, hasErrors, t]) + return ( - - - - - - - - +
+ + + + + + + + +
+
+ + + + {t('Previous')} + + + + + {t('Next')} + + + + +
) } + +// function WizardAutoFooter() { +// const { t } = useTranslationSafe() +// const { previousStep, nextStep, isLastStep } = useWizard() +// +// return ( +// +// ) +// } +// +// function WizardAutoStep1() { +// const [datasetHighlighted, setDatasetHighlighted] = useState() +// const setDataset = useSetRecoilState(datasetCurrentAtom) +// +// const apply = useCallback(() => { +// setDataset(datasetHighlighted) +// }, [datasetHighlighted, setDataset]) +// +// return ( +// +// ) +// } + +// function WizardAutoStep2() { +// return +// } + +export interface WizardNavigationBarProps { + prevDisabled?: boolean + nextDisabled?: boolean + onPrev?(): void + onNext?(): void +} + +export function WizardNavigationBar({ onPrev, onNext, prevDisabled, nextDisabled }: WizardNavigationBarProps) { + const { t } = useTranslationSafe() + + const prev = useMemo(() => { + if (!onPrev) { + return null + } + return ( + + + {t('Previous')} + + ) + }, [onPrev, prevDisabled, t]) + + const next = useMemo(() => { + if (!onNext) { + return null + } + return ( + + {t('Next')} + + + ) + }, [nextDisabled, onNext, t]) + + return ( + + {prev} + {next} + + ) +} + +const WizardNavigationForm = styled(FormBase)` + display: flex; + width: 100%; + height: 100%; + margin-top: auto; + padding: 10px; + border: 1px #ccc9 solid; + border-radius: 5px; +` + +const WizardNavigationButton = styled(Button)` + min-width: 140px; + min-height: 40px; + text-align: center; + vertical-align: middle; +` diff --git a/packages_rs/nextclade-web/src/components/Main/MainPage.tsx b/packages_rs/nextclade-web/src/components/Main/MainPage.tsx index be7f8592c..08a99fadf 100644 --- a/packages_rs/nextclade-web/src/components/Main/MainPage.tsx +++ b/packages_rs/nextclade-web/src/components/Main/MainPage.tsx @@ -2,11 +2,22 @@ import React from 'react' import { Layout } from 'src/components/Layout/Layout' import { MainInputForm } from 'src/components/Main/MainInputForm' +import styled from 'styled-components' + +const Main = styled.div` + display: flex; + flex: 1 1 100%; + overflow: hidden; + padding: 0; + margin: 0 auto; +` export function MainPage() { return ( - +
+ +
) } diff --git a/packages_rs/nextclade-web/src/components/Main/MainSectionInfo.tsx b/packages_rs/nextclade-web/src/components/Main/MainSectionInfo.tsx new file mode 100644 index 000000000..47669bece --- /dev/null +++ b/packages_rs/nextclade-web/src/components/Main/MainSectionInfo.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +import { Col, Row } from 'reactstrap' + +import { About } from 'src/components/About/About' +import { Downloads } from 'src/components/Main/Downloads' + +export function MainSectionInfo() { + return ( + + + + + + + + + + + + + + + + ) +} diff --git a/packages_rs/nextclade-web/src/components/Main/MainSectionTitle.tsx b/packages_rs/nextclade-web/src/components/Main/MainSectionTitle.tsx new file mode 100644 index 000000000..798444f96 --- /dev/null +++ b/packages_rs/nextclade-web/src/components/Main/MainSectionTitle.tsx @@ -0,0 +1,19 @@ +import React from 'react' + +import { useTranslationSafe as useTranslation } from 'src/helpers/useTranslationSafe' +import { Col, Row } from 'reactstrap' + +import { Subtitle, Title } from 'src/components/Main/Title' + +export function MainSectionTitle() { + const { t } = useTranslation() + + return ( + + + + <Subtitle>{t('Clade assignment, mutation calling, and sequence quality checks')}</Subtitle> + </Col> + </Row> + ) +} diff --git a/packages_rs/nextclade-web/src/components/Main/Title.tsx b/packages_rs/nextclade-web/src/components/Main/Title.tsx new file mode 100644 index 000000000..d1fd27a33 --- /dev/null +++ b/packages_rs/nextclade-web/src/components/Main/Title.tsx @@ -0,0 +1,82 @@ +import React from 'react' + +import styled from 'styled-components' + +import { TITLE_COLORS } from 'src/constants' + +// eslint-disable-next-line prefer-destructuring +const PACKAGE_VERSION = process.env.PACKAGE_VERSION + +// Borrowed with modifications from Nextstrain.org +// https://github.com/nextstrain/nextstrain.org/blob/master/static-site/src/components/splash/title.jsx + +const TitleH1 = styled.h1` + display: inline; + margin-top: 0; + margin-bottom: 0; + font-weight: 300; + letter-spacing: -1px; + font-size: 6rem; + + @media (max-width: 767.98px) { + font-size: 5rem; + } + + @media (max-width: 576px) { + font-size: 3.5rem; + } +` + +const VersionNumberBadge = styled.p` + display: inline; + font-size: 0.85rem; + color: #7b838a; + + @media (max-width: 767.98px) { + left: -35px; + font-size: 0.8rem; + } + + @media (max-width: 576px) { + left: -30px; + font-size: 0.75rem; + } +` + +const LetterSpan = styled.span<{ pos: number }>` + color: ${(props) => TITLE_COLORS[props.pos]}; +` + +export function Title() { + return ( + <span> + <TitleH1> + {'Nextclade'.split('').map((letter, i) => ( + // eslint-disable-next-line react/no-array-index-key + <LetterSpan key={`${i}_${letter}`} pos={i}> + {letter} + </LetterSpan> + ))} + </TitleH1> + {PACKAGE_VERSION && <VersionNumberBadge color="secondary">{`v${PACKAGE_VERSION}`}</VersionNumberBadge>} + </span> + ) +} + +export const Subtitle = styled.h2` + text-align: center; + font-size: 2rem; + font-weight: 300; + + @media (max-width: 991.98px) { + font-size: 1.5rem; + } + + @media (max-width: 767.98px) { + font-size: 1.2rem; + } + + @media (max-width: 576px) { + font-size: 1rem; + } +` diff --git a/packages_rs/nextclade-web/src/pages/_error.tsx b/packages_rs/nextclade-web/src/pages/_error.tsx index 4c50d1e22..5e32c2140 100644 --- a/packages_rs/nextclade-web/src/pages/_error.tsx +++ b/packages_rs/nextclade-web/src/pages/_error.tsx @@ -7,6 +7,7 @@ import { ErrorContent } from 'src/components/Error/ErrorContent' import { RestartButton } from 'src/components/Error/ErrorStyles' import { Layout } from 'src/components/Layout/Layout' import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import { MainSectionTitle } from 'src/components/Main/MainSectionTitle' import styled from 'styled-components' export const Container = styled(ContainerBase)` @@ -71,6 +72,8 @@ function ErrorPage({ statusCode, title, error }: ErrorPageProps) { return ( <Layout> <MainContent> + <MainSectionTitle /> + <Row noGutters> <Col className="text-center text-danger"> <h2>{titleText}</h2> diff --git a/packages_rs/nextclade-web/yarn.lock b/packages_rs/nextclade-web/yarn.lock index df7774898..0f5801e0b 100644 --- a/packages_rs/nextclade-web/yarn.lock +++ b/packages_rs/nextclade-web/yarn.lock @@ -13525,6 +13525,11 @@ react-transition-group@^4.3.0, react-transition-group@^4.4.2: loose-envify "^1.4.0" prop-types "^15.6.2" +react-use-wizard@2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-use-wizard/-/react-use-wizard-2.2.3.tgz#d2e57b299b6c35fcca90f3a6deb2d0a81318b466" + integrity sha512-Oh3EfpmWwF7vW1YZ2EgSLoU9VKJ13+TyLNmT56rBhLoXr3FXZoDEmtyxZ46GVkcicl/8qiHX0C42M6n2oKDkEQ== + react-virtualized-auto-sizer@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca"