Skip to content

Commit

Permalink
Feature/pre print (#608)
Browse files Browse the repository at this point in the history
* load both issues and articles in Articles Page

* fix article grid tooltip

* make collapsible and show description in Issue ocmponent

* fix boundaries for Article fingerprint to fix tooltip position

* Update WindowEvents.js

bugfix

* add ordering and refine ArticleGrid component to allow draft issues
  • Loading branch information
danieleguido authored Feb 8, 2024
1 parent 9f9a9a8 commit 9007625
Show file tree
Hide file tree
Showing 11 changed files with 449 additions and 256 deletions.
235 changes: 235 additions & 0 deletions src/components/Articles/ArticlesGrid.js
Original file line number Diff line number Diff line change
@@ -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 (
<Container ref={ref} className="Articles Issue page ">
<ArticleFingerprintTooltip forwardedRef={animatedRef} animatedProps={animatedProps} />
<Row className="mb-3">
<Col {...BootstrapColumLayout}>
<h1 className="mt-5">{t('pages.articles.title')}</h1>
<div className="d-flex align-items-center mb-2">
{showFilters && <p className="me-2 mb-0">{t('pages.articles.subheading')}</p>}
<OrderByDropdown
selectedValue={orderBy}
values={Object.keys(AvailablesOrderByComparators).map((value) => ({
value,
label: t(`orderBy${value}`),
}))}
title={t(`orderBy${orderBy}`)}
onChange={({ value }) => setQuery({ [OrderByQueryParam]: value })}
/>
</div>
</Col>
</Row>
{showFilters && (
<Row className="mb-3">
<Col md={{ offset: 1, span: 10 }}>
{status === StatusSuccess && (
<ArticlesFacets
items={data}
onSelect={onFacetsSelectHandler}
className="Articles_facets"
/>
)}
</Col>
</Row>
)}

{orderBy === OrderByIssue &&
issues.map((issue) => {
// const issue = articlesByIssue[id][0].issue

return (
<React.Fragment key={issue.pid}>
<hr />
<a className="anchor" id={`anchor-${issue.pid}`} />

<IssueArticles
selected={selected}
data={articlesByIssue[issue.pid] || []}
onArticleMouseMove={onArticleMouseMoveHandler}
onArticleClick={onArticleClickHandler}
onArticleMouseOut={onArticleMouseOutHandler}
>
<Issue item={issue} className="my-2" />
</IssueArticles>
</React.Fragment>
)
})}
{[OrderByPublicationDateAsc, OrderByPublicationDateDesc].includes(orderBy) && (
<IssueArticles
selected={selected}
data={articles}
onArticleMouseMove={onArticleMouseMoveHandler}
onArticleClick={onArticleClickHandler}
onArticleMouseOut={onArticleMouseOutHandler}
respectOrdering
/>
)}
</Container>
)
}

export default ArticlesGrid
81 changes: 72 additions & 9 deletions src/components/Issue/Issue.js
Original file line number Diff line number Diff line change
@@ -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 (
<>
<span>{item.pid.replace(/jdh0+(\d+)/, (m,n) => t('numbers.issue', {n}))}</span> &middot; <b>{new Date(item.publication_date).getFullYear()}</b>
<h2 >{item.name}</h2>
{item.description ? (
<h3><span className="text-muted">{item.pid}</span>&nbsp;{item.description}</h3>
):null}
</>
<Row className={`Issue align-items-start ${className}`}>
<Col md={{ span: 11 }} lg={{ span: 5 }} ref={ref}>
{label} {item.status !== 'PUBLISHED' ? <b>&mdash; {t('status.' + item.status)}</b> : null}
<h2>{item.name}</h2>
</Col>

<a.div
className="col col-md-12 col-lg-6 position-relative "
style={{ overflow: 'hidden', height }}
>
<p
className="m-0 position-absolute top-0"
ref={descriptionRef}
dangerouslySetInnerHTML={{ __html: item.description || ' ' }}
/>
</a.div>
<Col md={{ span: 1 }} lg={{ span: 1 }} className="d-flex justify-content-end">
<button
className="btn btn-outline-secondary btn-sm btn-rounded"
ref={buttonRef}
onClick={toggleHeight}
>
more...
</button>
</Col>
</Row>
)
}

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
30 changes: 24 additions & 6 deletions src/components/Issue/IssueArticles.js
Original file line number Diff line number Diff line change
@@ -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 },
Expand All @@ -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 : []

Expand All @@ -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 (
<Row ref={ref}>
{children}
{editorials.map((article, i) => {
if (Array.isArray(selected) && selected.indexOf(article.idx) === -1) {
return null
Expand Down
Loading

0 comments on commit 9007625

Please sign in to comment.