Skip to content

Commit

Permalink
Implement Cooccurrence display (#561)
Browse files Browse the repository at this point in the history
refactor(components): move SideSection layout prop parent wrapper
- so that layout can be performed on pages, enabling different layouts

feat(pages): load cooccurrence data and implement CooccurrenceSection

## Screenshots
https://dev.cofacts.tw/article/5pT69owBXtQmmerovXlq

### Desktop
![圖片](https://github.com/cofacts/rumors-site/assets/108608/5d26e698-afb7-4ffb-b0cb-d6cd4bee16c1)

### Mobile
![圖片](https://github.com/cofacts/rumors-site/assets/108608/8d4b76e9-bcc3-409f-a1be-b508b0a66f41)
  • Loading branch information
MrOrz authored Jan 11, 2024
2 parents a89a0dc + 4f389b6 commit 2b7f44f
Show file tree
Hide file tree
Showing 11 changed files with 321 additions and 135 deletions.
131 changes: 131 additions & 0 deletions components/CooccurrenceSection/CooccurrenceSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { useMemo } from 'react';
import Link from 'next/link';
import { ngettext, msgid, t } from 'ttag';
import gql from 'graphql-tag';
import { CooccurrenceSectionDataFragment } from 'typegen/graphql';
import { makeStyles } from '@material-ui/core/styles';

import Infos, { TimeInfo } from 'components/Infos';
import {
SideSection,
SideSectionHeader,
SideSectionLinks,
SideSectionLink,
SideSectionText,
} from 'components/SideSection';
import Thumbnail from 'components/Thumbnail';

export const fragments = {
CooccurrenceSectionData: gql`
fragment CooccurrenceSectionData on Cooccurrence {
createdAt
articles {
id
articleType
text
...ThumbnailArticleData
}
}
${Thumbnail.fragments.ThumbnailArticleData}
`,
};

const useStyles = makeStyles(theme => ({
timeInfo: {
margin: '8px 0 0',
[theme.breakpoints.up('md')]: {
margin: '0 0 16px',
},
},
}));

type Props = {
/** Ignore the current article */
currentArticleId: string;
cooccurrences: CooccurrenceSectionDataFragment[];
};

function CooccurrenceSection({ currentArticleId, cooccurrences }: Props) {
const classes = useStyles();
// Group cooccurrences by same articles
// then sort by count (more first) and then last createdAt (latest first)
const cooccurrenceEntries = useMemo(() => {
const entries = new Map<
string,
{
key: string;
articles: CooccurrenceSectionDataFragment['articles'];
count: number;
lastCooccurred: Date;
}
>();
cooccurrences.forEach(cooccurrence => {
const key = cooccurrence.articles
.map(article => article.id)
.sort()
.join(',');
const lastCooccurred = new Date(cooccurrence.createdAt);
const entry = entries.get(key);
const lastCooccurredInEntry = entry ? entry.lastCooccurred : null;
entries.set(key, {
key,
articles: cooccurrence.articles.filter(
({ id }) => id !== currentArticleId
),
count: entry ? entry.count + 1 : 1,
lastCooccurred:
lastCooccurredInEntry === null ||
lastCooccurredInEntry < lastCooccurred
? lastCooccurred
: lastCooccurredInEntry,
});
});
return Array.from(entries.values()).sort((a, b) => {
if (a.count !== b.count) return b.count - a.count;
return +b.lastCooccurred - +a.lastCooccurred;
});
}, [cooccurrences, currentArticleId]);

if (!cooccurrences.length) return null;

return (
<>
{cooccurrenceEntries.map(entry => (
<SideSection key={entry.key}>
<SideSectionHeader>
{ngettext(
msgid`Sent together ${entry.count} time`,
`Sent together ${entry.count} times`,
entry.count
)}
</SideSectionHeader>
<SideSectionLinks>
{entry.articles.map(article => (
<Link
key={article.id}
href="/article/[id]"
as={`/article/${article.id}`}
passHref
>
<SideSectionLink>
{article.articleType !== 'TEXT' ? (
<Thumbnail article={article} />
) : (
<SideSectionText>{article.text}</SideSectionText>
)}
</SideSectionLink>
</Link>
))}
</SideSectionLinks>
<Infos className={classes.timeInfo}>
<TimeInfo time={entry.lastCooccurred}>
{time => t`Last reported at ${time}`}
</TimeInfo>
</Infos>
</SideSection>
))}
</>
);
}

export default CooccurrenceSection;
5 changes: 5 additions & 0 deletions components/CooccurrenceSection/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import CooccurrenceSection, { fragments } from './CooccurrenceSection';

export { fragments };

export default CooccurrenceSection;
1 change: 1 addition & 0 deletions components/Infos/Infos.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const useStyles = makeStyles(theme => ({
* Displays "A | B | C" information; separates all its children using '|'.
* Supports text children, fragments, and skips falsy childrens like `false` or `null`.
*
* @param {object?} props
* @param {React.ReactChild} props.children
* @param {string?} props.className
* @returns {React.ReactElement}
Expand Down
3 changes: 2 additions & 1 deletion components/Infos/TimeInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ export function formatDate(date) {
/**
* Add tooltip and renders date in preferred format
*
* @param {object} props
* @param {Date | string | number} props.time
* @param {(t: string) => React.ReactChild} props.children - Render of string
* @param {(t: string) => React.ReactChild=} props.children - Render of string
*/
function TimeInfo({ time, children = t => t }) {
const date = useMemo(() => (time instanceof Date ? time : new Date(time)), [
Expand Down
2 changes: 0 additions & 2 deletions components/SideSection.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import cx from 'clsx';
export const SideSection = withStyles(theme => ({
aside: {
[theme.breakpoints.up('md')]: {
minWidth: 0,
flex: 1,
padding: '0 20px',
background: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
Expand Down
4 changes: 2 additions & 2 deletions components/Thumbnail.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function Thumbnail({ article, className }) {
}
case 'VIDEO':
return !article.thumbnailUrl ? (
t`A video` + ` (${t`Preview not supported yet`})`
<>{t`A video` + ` (${t`Preview not supported yet`})`}</>
) : (
<video
className={thumbnailCls}
Expand All @@ -47,7 +47,7 @@ function Thumbnail({ article, className }) {
);
case 'AUDIO':
return !article.thumbnailUrl ? (
t`An audio` + ` (${t`Preview not supported yet`})`
<>{t`An audio` + ` (${t`Preview not supported yet`})`}</>
) : (
<audio src={article.thumbnailUrl} controls />
);
Expand Down
Loading

0 comments on commit 2b7f44f

Please sign in to comment.