diff --git a/.env b/.env index 03f61966..ff067f69 100644 --- a/.env +++ b/.env @@ -13,9 +13,10 @@ REACT_APP_GITHUB_WIKI_FAQ=https://raw.githubusercontent.com/wiki/c2dh/journal-of REACT_APP_WIKI_VIDEO_RELEASES=https://raw.githubusercontent.com/wiki/c2dh/journal-of-digital-history/Video-Releases.md REACT_APP_WIKI_TERMS_OF_USE=https://raw.githubusercontent.com/wiki/c2dh/journal-of-digital-history/Terms-Of-Use.md REACT_APP_WIKI_HOMEPAGE=https://raw.githubusercontent.com/wiki/c2dh/journal-of-digital-history/Homepage.md -REACT_APP_WIKI_ABOUT=https://raw.githubusercontent.com/wiki/c2dh/journal-of-digital-history/About.md +REACT_APP_WIKI_ABOUT=https://raw.githubusercontent.com/wiki/c2dh/journal-of-digital-history/About-v2.md REACT_APP_WIKI_FINGERPRINT_EXPLAINED=https://raw.githubusercontent.com/wiki/c2dh/journal-of-digital-history/Fingerprint-explained.md -REACT_APP_WIKI_EVENTS=https://raw.githubusercontent.com/wiki/c2dh/journal-of-digital-history/Events.md +REACT_APP_WIKI_EVENTS=https://raw.githubusercontent.com/wiki/c2dh/journal-of-digital-history/Events-v2.md +REACT_APP_WIKI_REVIEW_POLICY=https://raw.githubusercontent.com/wiki/c2dh/journal-of-digital-history/Journal-Of-Digital-History-Peer-Review-Ethics-Declaration.md REACT_APP_NOTEBOOK_GUIDELINES_URL=/proxy-githubusercontent/C2DH/jdh-notebook/master/examples/Author_Guideline/skim-AuthorGuideline.ipynb REACT_APP_NOTEBOOK_CFP_BASE_URL=/proxy-githubusercontent/C2DH/jdh-notebook/master/cfp REACT_APP_NOTEBOOK_FINGERPRINT_EXPLAINED_URL=/proxy-githubusercontent/C2DH/jdh-notebook/master/examples/hermeneutic-layer.ipynb diff --git a/.gitignore b/.gitignore index a294589b..890a205a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ node_modules .vscode/settings.json storybook-static # _redirects +journal-of-digital-history.code-workspace diff --git a/package.json b/package.json index 7ca3ad56..4a527786 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jdh", - "version": "4.4.1", + "version": "4.4.2", "private": true, "dependencies": { "@auth0/auth0-react": "^1.1.0", diff --git a/_redirects b/public/_redirects similarity index 100% rename from _redirects rename to public/_redirects diff --git a/src/App.js b/src/App.js index e922bace..80758247 100644 --- a/src/App.js +++ b/src/App.js @@ -78,6 +78,7 @@ const Fingerprint = lazy(() => import('./pages/Fingerprint')) const FingerprintViewer = lazy(() => import('./pages/FingerprintViewer')) const FingerprintExplained = lazy(() => import('./pages/FingerprintExplained')) const ReleaseNotes = lazy(() => import('./pages/ReleaseNotes')) +const ReviewPolicy = lazy(() => import('./pages/ReviewPolicy')) const Faq = lazy(() => import('./pages/Faq')) const { startLangShort, lang } = getStartLang() @@ -162,6 +163,7 @@ function LangRoutes() { + diff --git a/src/components/Articles/ArticlesFacets.js b/src/components/Articles/ArticlesFacets.js index 17849418..6cd07dd8 100644 --- a/src/components/Articles/ArticlesFacets.js +++ b/src/components/Articles/ArticlesFacets.js @@ -57,7 +57,7 @@ const IssueListItem = (props) => { const issue = props.items[group.indices[0]].issue return ( - + ({group.count}) ) @@ -105,7 +105,7 @@ const Dimensions = [ }, ] -const ArticlesFacets = ({ items, onSelect, className }) => { +const ArticlesFacets = ({ items, onSelect, onShowMore, className }) => { return ( { onSelect={onSelect} onInit={(args) => console.debug('[ArticlesFacets] @init', args)} ShowMoreLabel={ShowMoreLabel} + onShowMore={onShowMore} className={className} /> ) diff --git a/src/components/Articles/ArticlesGrid.js b/src/components/Articles/ArticlesGrid.js new file mode 100644 index 00000000..2be31ce4 --- /dev/null +++ b/src/components/Articles/ArticlesGrid.js @@ -0,0 +1,271 @@ +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, a } from '@react-spring/web' +import { useHistory } from 'react-router' +import { useBoundingClientRect } from '../../hooks/graphics' +import { useWindowStore } from '../../store' + +const ArticlesGrid = ({ + items = [], + url, + issueId, + issues = [], + status, + // tag ategories to keep + categories = ['narrative', 'tool', 'issue'], +}) => { + const facetsRef = useRef() + const timerRef = useRef() + 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, + })) + // animation properties to slide up and down the articleFacets block + const [facetsAnimatedProps, setFacetsAnimatedProps] = useSpring(() => ({ + height: 0, + })) + 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, + selected: selected?.includes(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, selected, 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]) + + useLayoutEffect(() => { + if (status === StatusSuccess) { + setFacetsAnimatedProps.start({ + height: facetsRef.current.firstChild.scrollHeight, + delay: 1000, + }) + return useWindowStore.subscribe(() => { + clearTimeout(timerRef.current) + timerRef.current = setTimeout(() => { + setFacetsAnimatedProps.start({ + height: facetsRef.current.firstChild.scrollHeight, + delay: 0, + }) + }, 0) + }) + } + }, [status]) + + return ( + + + + + {t('pages.articles.title')} + + {showFilters && {t('pages.articles.subheading')}} + ({ + value, + label: t(`orderBy${value}`), + }))} + title={t(`orderBy${orderBy}`)} + onChange={({ value }) => setQuery({ [OrderByQueryParam]: value })} + /> + + + + + + + {status === StatusSuccess && ( + { + console.info('[ArticlesGrid] @showMore') + clearTimeout(timerRef.current) + setTimeout(() => { + setFacetsAnimatedProps.start({ + height: facetsRef.current.firstChild.scrollHeight, + delay: 0, + }) + }, 0) + }} + onSelect={onFacetsSelectHandler} + className="Articles_facets " + /> + )} + + + {orderBy === OrderByIssue && + issues.map((issue) => { + const numArticles = articlesByIssue[issue.pid]?.length + const numSelectedArticles = articlesByIssue[issue.pid]?.filter((d) => d.selected).length + + return ( + + + + + + + + ) + })} + {[OrderByPublicationDateAsc, OrderByPublicationDateDesc].includes(orderBy) && ( + + )} + + ) +} + +export default ArticlesGrid diff --git a/src/components/Facets/Dimension.js b/src/components/Facets/Dimension.js index 0739cc8f..b98d8528 100644 --- a/src/components/Facets/Dimension.js +++ b/src/components/Facets/Dimension.js @@ -80,6 +80,7 @@ const Dimension = ({ onInit, onSelect, onMouseEnter, + onShowMore, children, ListItem = DimensionGroupListItem, }) => { @@ -174,7 +175,15 @@ const Dimension = ({ ))} {restGroups.length > 0 && ( - setShowMore(!showMore)}> + { + if (typeof onShowMore === 'function') { + onShowMore(!showMore) + } + setShowMore(!showMore) + }} + > {t(showMore ? 'dimensions.actions.showLess' : 'dimensions.actions.showMore', { n: restGroups.length, diff --git a/src/components/Facets/Facets.js b/src/components/Facets/Facets.js index 2edacacd..af504825 100644 --- a/src/components/Facets/Facets.js +++ b/src/components/Facets/Facets.js @@ -146,6 +146,7 @@ const Facets = ({ onSelect, onInit, onMouseEnter, + onShowMore, className, style, }) => { @@ -242,6 +243,7 @@ const Facets = ({ onSelect={onSelectHandler} onInit={onInitHandler} onMouseEnter={onMouseEnterHandler} + onShowMore={onShowMore} ListItem={dimension.ListItem} > {reset === true && dims[dimension.name].selected.length > 0 && ( diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js index 5fbb7cce..6952fe10 100644 --- a/src/components/Footer/Footer.js +++ b/src/components/Footer/Footer.js @@ -13,6 +13,7 @@ import { AboutRoute, ReleaseNotesRoute, TermsOfUseRoute, + ReviewPolicy, FaqRoute } from '../../constants' import { Twitter, Facebook, GitHub } from 'react-feather' @@ -44,7 +45,7 @@ const Footer = ({ hideOnRoutes=[]}) => { - {[HomeRoute, AbstractSubmissionRoute, AboutRoute].map((route, i) => ( + {[HomeRoute, AbstractSubmissionRoute, AboutRoute, ReviewPolicy].map((route, i) => ( {t(route.label)} diff --git a/src/components/HomeMilestones/HomeMilestones.js b/src/components/HomeMilestones/HomeMilestones.js index f456f365..eafcf8f6 100644 --- a/src/components/HomeMilestones/HomeMilestones.js +++ b/src/components/HomeMilestones/HomeMilestones.js @@ -12,17 +12,35 @@ const HomeMilestones = ({ isPortrait, extent }) => { raw: true, allowCached: true, }) - + console.debug( + '[HomeMilestones]', + status, + status === StatusSuccess, + error, + 'data:', + data, + process.env.REACT_APP_WIKI_EVENTS, + ) + let jsondata = {} let values = [] + let jsondataExtent = [...extent] try { if (status === StatusSuccess) { - values = JSON.parse(data.replace(/^```json\n/, '').replace(/\n```$/, '')) + jsondata = JSON.parse(data.replace(/^```json\n/, '').replace(/\n```$/, '')) + values = jsondata.values } } catch (e) { console.warn('Error loading timeline data:', e) } - console.debug('[HomeMilestones]', status, status === StatusSuccess, error, 'values:', values) + if (jsondata.startDate && jsondata.endDate) { + const startDate = new Date(jsondata.startDate) + const endDate = new Date(jsondata.endDate) + jsondataExtent = [startDate, endDate] + values = values.filter((d) => new Date(d.date) >= startDate && new Date(d.date) <= endDate) + } + + console.debug('[HomeMilestones]', status, status === StatusSuccess, error, 'values:', jsondata) useEffect(() => { if (isIntersecting && !seenOnce) { @@ -33,7 +51,7 @@ const HomeMilestones = ({ isPortrait, extent }) => { return ( {status === StatusSuccess && ( - + )} ) diff --git a/src/components/Issue/Issue.css b/src/components/Issue/Issue.css new file mode 100644 index 00000000..40a65db2 --- /dev/null +++ b/src/components/Issue/Issue.css @@ -0,0 +1,15 @@ +.Issue.disabled > div { + display: none !important; +} +.Issue { + border-top: 1px solid var(--gray-400); + position: sticky; + top: 99px; + background: var(--gray-100); + z-index: 1; +} +.Issue__numArticles b { + border: 1px solid; + padding: 0 6px 0 5px; + border-radius: 5px; +} diff --git a/src/components/Issue/Issue.js b/src/components/Issue/Issue.js index 4743e7a1..073e3384 100644 --- a/src/components/Issue/Issue.js +++ b/src/components/Issue/Issue.js @@ -1,17 +1,116 @@ -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' +import './Issue.css' -const Issue = ({ item }) => { +const Issue = ({ + item, + numArticles = -1, + numSelectedArticles = -1, + isInFilterMode = false, + className = '', +}) => { + const ref = useRef() const { t } = useTranslation() + const descriptionRef = useRef() + const buttonRef = useRef() + const isOpen = useRef(false) + + const [{ height }, api] = useSpring(() => ({ height: 0 })) + const label = item.pid.replace(/jdh0+(\d+)/, (m, n) => t('numbers.issue', { n })) + + const activeClass = isInFilterMode && numSelectedArticles > 0 ? 'active' : '' + const disabledClass = isInFilterMode && numSelectedArticles < 1 ? 'disabled' : '' + + 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} - > + + + {item.status !== 'PUBLISHED' ? {t('status.' + item.status)} : label} + {isInFilterMode && numSelectedArticles > 0 ? ( + <> + — + + > + ) : null} + {!isInFilterMode && numArticles > 0 ? ( + <> + — + {' '} + > + ) : null} + {item.name} + + + + + + + + more... + + + ) } +Issue.propTypes = { + item: PropTypes.shape({ + pid: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string, + status: PropTypes.string.isRequired, + }).isRequired, + numArticles: PropTypes.number, + numSelectedArticles: PropTypes.number, + isInFilterMode: PropTypes.bool, + className: PropTypes.string, +} + export default Issue diff --git a/src/components/Issue/IssueArticleGridItem.js b/src/components/Issue/IssueArticleGridItem.js index 49d740ea..4cdc8fd6 100644 --- a/src/components/Issue/IssueArticleGridItem.js +++ b/src/components/Issue/IssueArticleGridItem.js @@ -12,6 +12,8 @@ import '../../styles/components/IssueArticleGridItem.scss' import { ArrowRightCircle } from 'react-feather' import IssueLabel from './IssueLabel' +const JustAddedTimeInterval = 3600000 * 24 * 240 + const IssueArticleGridItem = ({ article = {}, isFake = false, @@ -25,8 +27,20 @@ const IssueArticleGridItem = ({ const [{ width: size }, ref] = useBoundingClientRect() const { title, keywords, excerpt, contributor } = extractMetadataFromArticle(article) const { t } = useTranslation() + const isPrettyRecent = new Date() - new Date(article.publication_date) < JustAddedTimeInterval + console.debug( + '[IssueArticleGridItem]', + article.publication_date, + new Date() - new Date(article.publication_date), + JustAddedTimeInterval, + isPrettyRecent, + ) return ( - + { - const [{ top, left }, ref] = useBoundingClientRect() + const ref = React.useRef() + const bboxRef = React.useRef() const editorials = [] const articles = respectOrdering ? data : [] @@ -54,13 +56,31 @@ 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} + {data.length > 0 && } {editorials.map((article, i) => { if (Array.isArray(selected) && selected.indexOf(article.idx) === -1) { return null diff --git a/src/components/Issue/IssueLabel.js b/src/components/Issue/IssueLabel.js index 36d73821..110664f6 100644 --- a/src/components/Issue/IssueLabel.js +++ b/src/components/Issue/IssueLabel.js @@ -1,15 +1,26 @@ import React from 'react' +import PropTypes from 'prop-types' import { useTranslation } from 'react-i18next' -const IssueLabel = ({ pid, publication_date }) => { +const IssueLabel = ({ pid = '', name = '' }) => { const { t } = useTranslation() + const shortName = name.length > 16 ? name.slice(0, 16) + '...' : name return ( {pid.replace(/jdh0+(\d+)/, (m, n) => t('numbers.issue', { n }))} - · - {new Date(publication_date).getFullYear()} + {name.length > 0 ? ( + <> + · + {shortName} + > + ) : null} ) } +IssueLabel.propTypes = { + pid: PropTypes.string, + name: PropTypes.string, +} + export default IssueLabel 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 ac050874..09b5426f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -56,6 +56,10 @@ export const ReleaseNotesRoute = { to: '/release-notes', label: 'navigation.releaseNotes', } +export const ReviewPolicy = { + to: '/review-policy', + label: 'navigation.reviewPolicy', +} export const FaqRoute = { to: '/faq', label: 'navigation.faq' } export const PrimaryRoutes = [ @@ -250,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/About.css b/src/pages/About.css new file mode 100644 index 00000000..bc4d2200 --- /dev/null +++ b/src/pages/About.css @@ -0,0 +1,35 @@ +.About table { + width: 100%; + border-collapse: collapse; + border: 0px solid transparent; +} + +.About tbody { + border: 0px solid transparent; + display: block; +} + +.About thead { + display: none; +} + +.About tr { + display: inline-flex; + flex-wrap: wrap; + width: 100%; +} + +.About h2 { + margin-bottom: var(--spacer-3); + margin-top: var(--spacer-5); +} +.About td { + width: 100%; + margin-bottom: var(--spacer-3); +} + +@media screen and (min-width: 768px) { + .About td { + width: 33%; + } +} diff --git a/src/pages/About.js b/src/pages/About.js index 26451de7..2ca3d1db 100644 --- a/src/pages/About.js +++ b/src/pages/About.js @@ -1,17 +1,18 @@ import React from 'react' import WikiStaticPage from './WikiStaticPage' import { useTranslation } from 'react-i18next' - +import './About.css' const About = () => { const { t } = useTranslation() return ( - - + + {t('pages.about.subheading')} ) diff --git a/src/pages/Articles.js b/src/pages/Articles.js index d5e5f75f..aa3c1a2e 100644 --- a/src/pages/Articles.js +++ b/src/pages/Articles.js @@ -1,253 +1,100 @@ -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' +import React, { useEffect } from 'react' -const OrderByQueryParam = 'orderBy' -const FilterByQueryparam = 'f' -const OrderByIssue = 'issue' -const OrderByPublicationDateAsc = 'dateAsc' -const OrderByPublicationDateDesc = 'dateDesc' +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 AvailablesOrderByComparators = { - [OrderByIssue]: () => {}, - [OrderByPublicationDateAsc]: (a, b) => a.publication_date - b.publication_date, - [OrderByPublicationDateDesc]: (a, b) => b.publication_date - a.publication_date, -} +const ProgressLoadingId = 'articles' -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, ProgressLoadingId, 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?limit=500', { + timeout: 10000, + onDownloadProgress: (e) => setLoadingProgressFromEvent(e, ProgressLoadingId, 0.5, 0.5), }) + .then((res) => res.data.results), + enabled: statusIssues === StatusSuccess, + }) + + useEffect(() => { + if (statusIssues === StatusFetching) { + setLoadingProgress(0.05, ProgressLoadingId) + } else if (statusArticles === StatusSuccess) { + setLoadingProgress(1, ProgressLoadingId) + } else if (statusArticles === StatusError) { + setLoadingProgress(0, ProgressLoadingId) } - }, [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/pages/ReviewPolicy.js b/src/pages/ReviewPolicy.js new file mode 100644 index 00000000..c684902b --- /dev/null +++ b/src/pages/ReviewPolicy.js @@ -0,0 +1,20 @@ +import React from 'react' +import WikiStaticPage from './WikiStaticPage' +import { useTranslation } from 'react-i18next' + + +const ReviewPolicy = () => { + const { t } = useTranslation() + return ( + + + {t('pages.reviewPolicy.subheading')} + + ) +} + +export default ReviewPolicy \ No newline at end of file diff --git a/src/pages/WikiStaticPage.js b/src/pages/WikiStaticPage.js index f8bce8e8..a881fb48 100644 --- a/src/pages/WikiStaticPage.js +++ b/src/pages/WikiStaticPage.js @@ -4,34 +4,42 @@ import { Container, Row, Col } from 'react-bootstrap' import StaticPageLoader from './StaticPageLoader' import { BootstrapFullColumLayout, StatusSuccess } from '../constants' import '../styles/pages/WikiStaticPage.scss' +import PropTypes from 'prop-types' const markdownParser = MarkdownIt({ html: false, linkify: true, - typographer: true + typographer: true, }) -const WikiStaticPage = ({ url='', children }) => ( +const WikiStaticPage = ({ url = '', className = '', children }) => ( ( - + Component={({ data = '...', status }) => ( + - - {children} - + {children} - + )} /> ) +WikiStaticPage.propTypes = { + url: PropTypes.string.isRequired, + className: PropTypes.string, + children: PropTypes.node, +} + export default WikiStaticPage 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/components/IssueArticleGridItem.scss b/src/styles/components/IssueArticleGridItem.scss index d7a86546..7999a800 100644 --- a/src/styles/components/IssueArticleGridItem.scss +++ b/src/styles/components/IssueArticleGridItem.scss @@ -2,6 +2,8 @@ .IssueArticleGridItem { margin: var(--spacer-3) 0; border-top: 1px solid var(--secondary); + border-radius: 10px; + padding: 10px; svg { cursor: pointer; } @@ -20,6 +22,28 @@ padding: var(--spacer-1); } +.IssueArticleGridItem.just-added { + position: relative; + border: 1px solid var(--accent); + box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px; +} +.IssueArticleGridItem.just-added::after { + position: absolute; + top: -15px; + left: 6px; + transform: rotate(-3deg); + font-size: 0.8em; + content: 'new'; + color: var(--white); + text-transform: uppercase; + padding: 2px 8px; + letter-spacing: 1px; + border-radius: 5px; + font-weight: bold; + background-color: var(--accent); + box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px; +} + .IssueArticleGridItem_contributor { display: inline-block; a { @@ -59,7 +83,7 @@ .IssueArticleGridItem { margin: 0; margin-bottom: var(--spacer-5); - padding: 0; + // padding: 0; border-top-color: transparent; } } 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 629ce592..801fcfc3 100644 --- a/src/translations.json +++ b/src/translations.json @@ -15,7 +15,8 @@ "articleData": "data", "issue": "Articles & Issues", "releaseNotes": "Release notes", - "faq": "Frequently asked questions" + "faq": "Frequently asked questions", + "reviewPolicy": "Peer Review Ethics Declaration" }, "title": "Journal of Digital History", "titleInline": "Journal of Digital History", @@ -42,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...", @@ -155,6 +159,10 @@ "notFound": { "title": "not Found", "subheading": "..." + }, + "reviewPolicy": { + "title": "Journal Of Digital History Peer Review Ethics Declaration", + "subheading": "" } }, "forms": { @@ -256,6 +264,8 @@ "currentCharactersWithCount": "(now: {{count}})", "itemIdx": "(item #{{idx}})", "articles": "{{ n }} articles", + "articlesInIssue": "{{ n }} articles", + "articlesFiltered": "{{ n }} of {{ total }} articles matching filters", "datatableRows": "{{ total }} rows", "datatableRowsFiltered": "{{ n }} / {{ total }} rows (filtered)", "datatableShowAllRows": "show all rows ({{ n }})", @@ -308,6 +318,12 @@ "directions": "" } }, + "status": { + "PEER_REVIEW": "Peer Review", + "PUBLISHED": "Published", + "DRAFT": "Coming soon", + "INTERNAL_REVIEW": "Internal Review" + }, "welcomeBack": "Logged in as" } }
{t('pages.articles.subheading')}