diff --git a/src/components/Articles/ArticlesGrid.js b/src/components/Articles/ArticlesGrid.js new file mode 100644 index 00000000..2c06fe77 --- /dev/null +++ b/src/components/Articles/ArticlesGrid.js @@ -0,0 +1,235 @@ +import React, { useMemo, useLayoutEffect, useState, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { sort } from 'd3-array' +import { useQueryParams, withDefault } from 'use-query-params' +import { asEnumParam, asRegexArrayParam } from '../../logic/params' +import { + AvailablesOrderByComparators, + FilterByQueryparam, + OrderByIssue, + OrderByPublicationDateAsc, + OrderByPublicationDateDesc, + OrderByQueryParam, + BootstrapColumLayout, + DisplayLayerCellIdxQueryParam, + DisplayLayerQueryParam, + LayerNarrative, + LayerHermeneutics, + StatusSuccess, +} from '../../constants' +import IssueArticles from '../Issue/IssueArticles' +import OrderByDropdown from '../OrderByDropdown' +import Article from '../../models/Article' +import ArticlesFacets from '../Articles/ArticlesFacets' +import Issue from '../Issue' +import ArticleFingerprintTooltip from '../ArticleV2/ArticleFingerprintTooltip' + +import groupBy from 'lodash/groupBy' +import { Container, Row, Col } from 'react-bootstrap' +import { useSpring, config } from 'react-spring' +import { useHistory } from 'react-router' +import { useBoundingClientRect } from '../../hooks/graphics' + +const ArticlesGrid = ({ + items = [], + url, + issueId, + issues = [], + status, + // tag ategories to keep + categories = ['narrative', 'tool', 'issue'], +}) => { + const { t } = useTranslation() + const [{ [OrderByQueryParam]: orderBy }, setQuery] = useQueryParams({ + [OrderByQueryParam]: withDefault( + asEnumParam(Object.keys(AvailablesOrderByComparators)), + OrderByIssue, + ), + [FilterByQueryparam]: asRegexArrayParam(), + }) + + const [selected, setSelected] = useState(null) + // pagination api contains results in data + + const [{ width }, ref] = useBoundingClientRect() + const history = useHistory() + const animatedRef = useRef({ idx: '', length: '', datum: {} }) + const [animatedProps, setAnimatedProps] = useSpring(() => ({ + from: { x: 0, y: 0, id: '0-0', color: 'red', backgroundColor: 'transparent' }, + x: 0, + y: 0, + opacity: 0, + id: '0-0', + color: 'var(--white)', + backgroundColor: 'var(--secondary)', + config: config.stiff, + })) + const data = (items || []).map((d, idx) => new Article({ ...d, idx })) + const articles = sort(data, AvailablesOrderByComparators[orderBy]) + const { articlesByIssue, showFilters } = useMemo(() => { + if (status !== StatusSuccess) { + return { + articlesByIssue: {}, + issues: [], + sortedItems: [], + showFilters: false, + } + } + const sortedItems = data.map((item, idx) => ({ + ...item, + idx, + })) + const articlesByIssue = groupBy(sortedItems, 'issue.pid') + + const showFilters = data.reduce((acc, d) => { + return acc || d.tags.some((t) => categories.includes(t.category)) + }, false) + return { articlesByIssue, showFilters } + }, [url, status]) + + const onArticleMouseMoveHandler = (e, datum, idx, article, bounds) => { + if (!isNaN(idx) && animatedRef.current.idx !== idx) { + animatedRef.current.idx = idx + animatedRef.current.length = article.fingerprint.cells.length + animatedRef.current.datum = datum + } + const x = bounds.left + Math.min(width - 200, e.clientX - bounds.left) + const y = e.clientY + 50 + // this will change only animated toltip stuff + setAnimatedProps.start({ + x, + y, + id: [article.abstract.id || 0, isNaN(idx) ? 0 : idx].join('-'), + color: + datum.type === 'code' + ? 'var(--white)' + : datum.isHermeneutic + ? 'var(--secondary)' + : 'var(--white)', + backgroundColor: + datum.type === 'code' + ? 'var(--accent)' + : datum.isHermeneutic + ? 'var(--primary)' + : 'var(--secondary)', + opacity: 1, + }) + } + const onArticleMouseOutHandler = () => { + setAnimatedProps.start({ opacity: 0 }) + } + const onArticleClickHandler = (e, datum, idx, article) => { + console.debug('@onArticleClickHandler', datum, idx, article) + e.stopPropagation() + // link to specific cell in article + const url = idx + ? `/en/article/${ + article.abstract.pid + }?${DisplayLayerCellIdxQueryParam}=${idx}&${DisplayLayerQueryParam}=${ + datum.isHermeneutic ? LayerHermeneutics : LayerNarrative + }` + : `/en/article/${article.abstract.pid}` + history.push(url) + } + + const onFacetsSelectHandler = (name, indices) => { + console.debug('[Articles] @onFacetsSelectHandler', name, indices) + setSelected(indices) + } + + useLayoutEffect(() => { + // go to issueId as soon as it's ready. + if (issueId && status === StatusSuccess) { + console.debug('[Articles] goto issueId:', issueId) + const element = document.getElementById('anchor-' + issueId) + element && + element.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'nearest', + }) + } + }, [status]) + + useLayoutEffect(() => { + setAnimatedProps.start({ opacity: 0 }) + }, [selected]) + console.debug( + '[Articles] \n- articles:', + Array.isArray(articles), + articles, + '\n- issueId:', + issueId, + selected, + ) + + return ( + + + + +

{t('pages.articles.title')}

+
+ {showFilters &&

{t('pages.articles.subheading')}

} + ({ + value, + label: t(`orderBy${value}`), + }))} + title={t(`orderBy${orderBy}`)} + onChange={({ value }) => setQuery({ [OrderByQueryParam]: value })} + /> +
+ +
+ {showFilters && ( + + + {status === StatusSuccess && ( + + )} + + + )} + + {orderBy === OrderByIssue && + issues.map((issue) => { + // const issue = articlesByIssue[id][0].issue + + return ( + +
+ + + + + +
+ ) + })} + {[OrderByPublicationDateAsc, OrderByPublicationDateDesc].includes(orderBy) && ( + + )} +
+ ) +} + +export default ArticlesGrid diff --git a/src/components/Issue/Issue.js b/src/components/Issue/Issue.js index 4743e7a1..ba2c35bc 100644 --- a/src/components/Issue/Issue.js +++ b/src/components/Issue/Issue.js @@ -1,17 +1,80 @@ -import React from 'react' +import React, { useLayoutEffect, useRef } from 'react' +import PropTypes from 'prop-types' import { useTranslation } from 'react-i18next' +import { Col, Row } from 'react-bootstrap' +import { a, useSpring } from '@react-spring/web' -const Issue = ({ item }) => { +const Issue = ({ item, className = '' }) => { + const ref = useRef() + const descriptionRef = useRef() + const buttonRef = useRef() + const isOpen = useRef(false) + + const [{ height }, api] = useSpring(() => ({ height: 0 })) const { t } = useTranslation() + const label = item.pid.replace(/jdh0+(\d+)/, (m, n) => t('numbers.issue', { n })) + + const toggleHeight = () => { + isOpen.current = !isOpen.current + // change label on the button + buttonRef.current.textContent = isOpen.current ? 'less' : 'more ...' + + api.start({ + height: isOpen.current + ? descriptionRef.current.offsetHeight + : Math.min(ref.current.offsetHeight, descriptionRef.current.offsetHeight), + }) + } + + useLayoutEffect(() => { + if (ref.current) { + if (ref.current.offsetHeight < descriptionRef.current.offsetHeight) { + buttonRef.current.style.display = 'block' + } else { + buttonRef.current.style.display = 'none' + } + api.set({ height: Math.min(ref.current.offsetHeight, descriptionRef.current.offsetHeight) }) + } + }, [ref]) + return ( - <> - {item.pid.replace(/jdh0+(\d+)/, (m,n) => t('numbers.issue', {n}))} · {new Date(item.publication_date).getFullYear()} -

{item.name}

- {item.description ? ( -

{item.pid} {item.description}

- ):null} - + + + {label} {item.status !== 'PUBLISHED' ? — {t('status.' + item.status)} : null} +

{item.name}

+ + + +

+ + + + + ) } +Issue.propTypes = { + item: PropTypes.shape({ + pid: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string, + status: PropTypes.string.isRequired, + }).isRequired, + className: PropTypes.string, +} + export default Issue diff --git a/src/components/Issue/IssueArticles.js b/src/components/Issue/IssueArticles.js index 7ed2e3b6..eefd5557 100644 --- a/src/components/Issue/IssueArticles.js +++ b/src/components/Issue/IssueArticles.js @@ -1,7 +1,6 @@ -import React from 'react' +import React, { useLayoutEffect } from 'react' import { Row, Col } from 'react-bootstrap' import IssueArticleGridItem from './IssueArticleGridItem' -import { useBoundingClientRect } from '../../hooks/graphics' const BootstrapColumLayout = Object.freeze({ lg: { span: 4, offset: 0 }, @@ -26,8 +25,10 @@ const IssueArticles = ({ onArticleMouseOut, respectOrdering = false, + children, }) => { - const [{ top, left }, ref] = useBoundingClientRect() + const ref = React.useRef() + const bboxRef = React.useRef() const editorials = [] const articles = respectOrdering ? data : [] @@ -54,13 +55,30 @@ const IssueArticles = ({ } // eslint-disable-next-line no-unused-vars const onMouseMoveHandler = (e, datum, idx, article) => { - if (typeof onArticleMouseMove === 'function') { - onArticleMouseMove(e, datum, idx, article, { top, left }) + if (typeof onArticleMouseMove === 'function' && bboxRef.current) { + onArticleMouseMove(e, datum, idx, article, { + top: bboxRef.current.top, + left: bboxRef.current.left, + }) } } - console.debug('[IssueArticles] selected', selected, articles) + const updateBboxRef = () => { + bboxRef.current = ref.current.getBoundingClientRect() + } + + useLayoutEffect(() => { + if (!ref.current) return + bboxRef.current = ref.current.getBoundingClientRect() + window.addEventListener('resize', updateBboxRef) + return () => { + window.removeEventListener('resize', updateBboxRef) + } + }, [ref]) + console.debug('[IssueArticles] rendered') + return ( + {children} {editorials.map((article, i) => { if (Array.isArray(selected) && selected.indexOf(article.idx) === -1) { return null diff --git a/src/components/WindowEvents.js b/src/components/WindowEvents.js index aa249617..22b48e4e 100644 --- a/src/components/WindowEvents.js +++ b/src/components/WindowEvents.js @@ -1,6 +1,8 @@ import { useEffect } from 'react' import { debounce } from '../logic/viewport' import { useWindowStore } from '../store' +const setScrollPosition = useWindowStore.getState().setScrollPosition +const setWindowDimensions = useWindowStore.getState().setWindowDimensions /** * React hook that reacts to window resize and scrolling events, and implements debounce to prevent too many calls for both events. * @param {Object} options - An object containing optional parameters. @@ -22,7 +24,7 @@ const WindowEvents = ({ debounceTime = 150, debounceResize = true, debounceScrol '\n - window.innerHeight', window.innerHeight, ) - useWindowStore.setWindowDimensions(window.innerWidth, window.innerHeight) + setWindowDimensions(window.innerWidth, window.innerHeight) }, debounceTime) const handleScroll = debounce(() => { console.debug( @@ -32,20 +34,20 @@ const WindowEvents = ({ debounceTime = 150, debounceResize = true, debounceScrol '\n - window.scrollY', window.scrollY, ) - useWindowStore.setScrollPosition(window.scrollX, window.scrollY) + setScrollPosition(window.scrollX, window.scrollY) }, debounceTime) if (debounceResize) { window.addEventListener('resize', handleResize) } else { window.addEventListener('resize', () => { - useWindowStore.setWindowDimensions(window.innerWidth, window.innerHeight) + setWindowDimensions(window.innerWidth, window.innerHeight) }) } if (debounceScroll) { window.addEventListener('scroll', handleScroll) } else { window.addEventListener('scroll', () => { - useWindowStore.setScrollPosition(window.scrollX, window.scrollY) + setScrollPosition(window.scrollX, window.scrollY) }) } return () => { diff --git a/src/constants.js b/src/constants.js index 131fb728..09b5426f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -254,3 +254,15 @@ export const ArticleCellContainerClassNames = [ 'alert-danger', 'alert-warning', ] + +export const OrderByQueryParam = 'orderBy' +export const FilterByQueryparam = 'f' +export const OrderByIssue = 'issue' +export const OrderByPublicationDateAsc = 'dateAsc' +export const OrderByPublicationDateDesc = 'dateDesc' + +export const AvailablesOrderByComparators = { + [OrderByIssue]: () => {}, + [OrderByPublicationDateAsc]: (a, b) => a.publication_date - b.publication_date, + [OrderByPublicationDateDesc]: (a, b) => b.publication_date - a.publication_date, +} diff --git a/src/pages/Articles.js b/src/pages/Articles.js index d5e5f75f..e92c3597 100644 --- a/src/pages/Articles.js +++ b/src/pages/Articles.js @@ -1,253 +1,98 @@ -import React, { useRef, useState, useMemo, useLayoutEffect } from 'react' -import { useTranslation } from 'react-i18next' -import groupBy from 'lodash/groupBy' -import { Container, Row, Col } from 'react-bootstrap' -import { useSpring, config } from 'react-spring' -import { useHistory } from 'react-router' -import StaticPageLoader from './StaticPageLoader' -import IssueArticles from '../components/Issue/IssueArticles' -import Issue from '../components/Issue' -import ArticleFingerprintTooltip from '../components/ArticleV2/ArticleFingerprintTooltip' -import { - BootstrapColumLayout, - DisplayLayerCellIdxQueryParam, - DisplayLayerQueryParam, - LayerNarrative, - LayerHermeneutics, - StatusSuccess, -} from '../constants' -import { useBoundingClientRect } from '../hooks/graphics' -import '../styles/pages/Articles.scss' -import { useQueryParams, withDefault } from 'use-query-params' -import { asEnumParam, asRegexArrayParam } from '../logic/params' -import OrderByDropdown from '../components/OrderByDropdown' -import { sort } from 'd3-array' -import Article from '../models/Article' -import ArticlesFacets from '../components/Articles/ArticlesFacets' - -const OrderByQueryParam = 'orderBy' -const FilterByQueryparam = 'f' -const OrderByIssue = 'issue' -const OrderByPublicationDateAsc = 'dateAsc' -const OrderByPublicationDateDesc = 'dateDesc' +import React, { useEffect } from 'react' -const AvailablesOrderByComparators = { - [OrderByIssue]: () => {}, - [OrderByPublicationDateAsc]: (a, b) => a.publication_date - b.publication_date, - [OrderByPublicationDateDesc]: (a, b) => b.publication_date - a.publication_date, -} +import '../styles/pages/Articles.scss' +import PropTypes from 'prop-types' +import ArticlesGrid from '../components/Articles/ArticlesGrid' +import { useQuery } from '@tanstack/react-query' +import axios from 'axios' +import { usePropsStore } from '../store' +import { StatusError, StatusFetching, StatusSuccess } from '../constants' +import ErrorViewer from './ErrorViewer' -const ArticlesGrid = ({ - data: response = [], - url, - issueId, - status, - // tag ategories to keep - categories = ['narrative', 'tool', 'issue'], +const Articles = ({ + match: { + params: { id: issueId }, + }, }) => { - const { t } = useTranslation() - const [{ [OrderByQueryParam]: orderBy }, setQuery] = useQueryParams({ - [OrderByQueryParam]: withDefault( - asEnumParam(Object.keys(AvailablesOrderByComparators)), - OrderByIssue, - ), - [FilterByQueryparam]: asRegexArrayParam(), + console.debug('[Articles] match.params.id/issueId:', issueId) + const [setLoadingProgress, setLoadingProgressFromEvent] = usePropsStore((state) => [ + state.setLoadingProgress, + state.setLoadingProgressFromEvent, + ]) + const { + data: issues, + error: errorIssues, + status: statusIssues, + } = useQuery({ + queryKey: ['/api/issues'], + queryFn: () => + axios + .get('/api/issues?ordering=-pid', { + timeout: 10000, + onDownloadProgress: (e) => setLoadingProgressFromEvent(e, 'articles', 0.5, 0), + }) + .then((res) => res.data.results), }) - - const [selected, setSelected] = useState(null) - // pagination api contains results in data - - const [{ width }, ref] = useBoundingClientRect() - const history = useHistory() - const animatedRef = useRef({ idx: '', length: '', datum: {} }) - const [animatedProps, setAnimatedProps] = useSpring(() => ({ - from: { x: 0, y: 0, id: '0-0', color: 'red', backgroundColor: 'transparent' }, - x: 0, - y: 0, - opacity: 0, - id: '0-0', - color: 'var(--white)', - backgroundColor: 'var(--secondary)', - config: config.stiff, - })) - const data = (response.results || []).map((d, idx) => new Article({ ...d, idx })) - const articles = sort(data, AvailablesOrderByComparators[orderBy]) - const { articlesByIssue, issues, showFilters } = useMemo(() => { - if (status !== StatusSuccess) { - return { - articlesByIssue: {}, - issues: [], - sortedItems: [], - showFilters: false, - } - } - const sortedItems = data.map((item, idx) => ({ - ...item, - idx, - })) - const articlesByIssue = groupBy(sortedItems, 'issue.pid') - const issues = Object.keys(articlesByIssue).sort((a, b) => { - return articlesByIssue[a][0].issue.pid < articlesByIssue[b][0].issue.pid - }) - const showFilters = data.reduce((acc, d) => { - return acc || d.tags.some((t) => categories.includes(t.category)) - }, false) - return { articlesByIssue, issues, showFilters } - }, [url, status]) - - const onArticleMouseMoveHandler = (e, datum, idx, article, bounds) => { - if (!isNaN(idx) && animatedRef.current.idx !== idx) { - animatedRef.current.idx = idx - animatedRef.current.length = article.fingerprint.cells.length - animatedRef.current.datum = datum - } - const x = Math.min(width - 250, e.clientX - bounds.left) - const y = e.clientY + 50 - // this will change only animated toltip stuff - setAnimatedProps.start({ - x, - y, - id: [article.abstract.id || 0, isNaN(idx) ? 0 : idx].join('-'), - color: - datum.type === 'code' - ? 'var(--white)' - : datum.isHermeneutic - ? 'var(--secondary)' - : 'var(--white)', - backgroundColor: - datum.type === 'code' - ? 'var(--accent)' - : datum.isHermeneutic - ? 'var(--primary)' - : 'var(--secondary)', - opacity: 1, - }) - } - const onArticleMouseOutHandler = () => { - setAnimatedProps.start({ opacity: 0 }) - } - const onArticleClickHandler = (e, datum, idx, article) => { - console.debug('@onArticleClickHandler', datum, idx, article) - e.stopPropagation() - // link to specific cell in article - const url = idx - ? `/en/article/${ - article.abstract.pid - }?${DisplayLayerCellIdxQueryParam}=${idx}&${DisplayLayerQueryParam}=${ - datum.isHermeneutic ? LayerHermeneutics : LayerNarrative - }` - : `/en/article/${article.abstract.pid}` - history.push(url) - } - - const onFacetsSelectHandler = (name, indices) => { - console.debug('[Articles] @onFacetsSelectHandler', name, indices) - setSelected(indices) - } - - useLayoutEffect(() => { - // go to issueId as soon as it's ready. - if (issueId && status === StatusSuccess) { - console.debug('[Articles] goto issueId:', issueId) - const element = document.getElementById('anchor-' + issueId) - element && - element.scrollIntoView({ - behavior: 'smooth', - block: 'start', - inline: 'nearest', + const { + data: articles, + error: errorArticles, + status: statusArticles, + } = useQuery({ + queryKey: ['/api/articles'], + queryFn: () => + axios + .get('/api/articles', { + timeout: 10000, + onDownloadProgress: (e) => setLoadingProgressFromEvent(e, 'articles', 0.5, 0.5), }) + .then((res) => res.data.results), + enabled: statusIssues === StatusSuccess, + }) + + useEffect(() => { + if (statusIssues === StatusFetching) { + setLoadingProgress(0.05, 'articles') + } else if (statusArticles === StatusSuccess) { + setLoadingProgress(1, 'articles') + } else if (statusArticles === StatusError) { + setLoadingProgress(0, 'articles') } - }, [status]) + }, [statusIssues, statusArticles]) - useLayoutEffect(() => { - setAnimatedProps.start({ opacity: 0 }) - }, [selected]) console.debug( - '[Articles] \n- articles:', + '[Articles] \n- statusIssues:', + statusIssues, + '\n- issues:', + Array.isArray(issues), + issues, + '\n- statusArticles:', + statusArticles, + '\n- articles:', Array.isArray(articles), articles, - '\n- issueId:', - issueId, ) return ( - - - - -

{t('pages.articles.title')}

-
- {showFilters &&

{t('pages.articles.subheading')}

} - ({ - value, - label: t(`orderBy${value}`), - }))} - title={t(`orderBy${orderBy}`)} - onChange={({ value }) => setQuery({ [OrderByQueryParam]: value })} - /> -
- -
- {showFilters && ( - - - {status === StatusSuccess && ( - - )} - - + <> + {statusIssues === StatusError && ( + )} - - {orderBy === OrderByIssue && - issues.map((id) => { - const issue = articlesByIssue[id][0].issue - return ( - - - -
- - - - - - ) - })} - {[OrderByPublicationDateAsc, OrderByPublicationDateDesc].includes(orderBy) && ( - + {statusArticles === StatusError && ( + + )} + {statusIssues !== StatusError && statusArticles !== StatusError && ( + )} - + ) } -const Articles = ({ - match: { - params: { id: issueId }, - }, -}) => { - console.debug('[Articles] issueId', issueId) - return ( - - ) +Articles.propTypes = { + match: PropTypes.shape({ + params: PropTypes.shape({ + id: PropTypes.string, + }).isRequired, + }).isRequired, } export default Articles diff --git a/src/setupProxy.js b/src/setupProxy.js index 47e956bc..627da878 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -6,7 +6,9 @@ const apiPath = process.env.REACT_APP_API_ROOT || '/api' fs.appendFile( './setupProxy.log', - `${new Date().toISOString()} target=${target} apiPath=${apiPath}\n`, + `${new Date().toISOString()} target=${target} apiPath=${apiPath} REACT_APP_PROXY=${ + process.env.REACT_APP_PROXY + } \n`, (err) => console.error(err), ) diff --git a/src/store.js b/src/store.js index bbdaa9a0..8b8032d9 100644 --- a/src/store.js +++ b/src/store.js @@ -154,6 +154,11 @@ export const usePropsStore = create((set) => ({ loadingLabel: '', setLoadingProgress: (loadingProgress, loadingLabel = '') => set({ loadingProgress, loadingLabel }), + setLoadingProgressFromEvent: (e, loadingLabel = '', ratio = 1, initial = 0) => { + const { total, loaded } = e + const loadingProgress = Math.max(1, initial + ratio * (total ? loaded / total : 0)) + set({ loadingProgress, loadingLabel }) + }, })) export const useWindowStore = create((set) => ({ diff --git a/src/styles/article.scss b/src/styles/article.scss index 4c30230c..3042b1ac 100644 --- a/src/styles/article.scss +++ b/src/styles/article.scss @@ -736,7 +736,7 @@ svg.ArticleFingerprint { padding: 9px 15px; pointer-events: none; width: 200px; - left: 100px; + left: 0px; } .ArticleFingerprintTooltip_heading { diff --git a/src/styles/index.scss b/src/styles/index.scss index 2c5f7b60..530d6d84 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -98,7 +98,9 @@ html, box-shadow: 0px 1px 0px var(--primary); } } - +.border-double { + border-width: 2px !important; +} .rounded { border-radius: 0.25rem !important; } @@ -110,9 +112,9 @@ html, } .navbar .nav-link { font-size: 0.9rem; - // color: white; box-shadow: none; } + .navbar-brand { box-shadow: none; } diff --git a/src/translations.json b/src/translations.json index 5c6539e9..ee7dfc7e 100644 --- a/src/translations.json +++ b/src/translations.json @@ -43,7 +43,10 @@ }, "articles": { "title": "Articles & Issues", - "subheading": "Filter by keywords, libraries or browse the articles." + "subheading": "Filter by keywords, libraries or browse the articles.", + "status": { + "PEER_REVIEW": "Currently under Peer Review" + } }, "loading": { "title": "loading...", @@ -313,6 +316,12 @@ "directions": "" } }, + "status": { + "PEER_REVIEW": "Peer Review", + "PUBLISHED": "Published", + "DRAFT": "Coming soon", + "INTERNAL_REVIEW": "Internal Review" + }, "welcomeBack": "Logged in as" } }