Skip to content

Commit 2319142

Browse files
committed
Add searchable publication and paper catalogs with static paper pages
1 parent 13d87e3 commit 2319142

14 files changed

Lines changed: 276 additions & 38 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ The live website is a static Next.js application in [`web/`](web/README.md). Git
99
See [development and content documentation](web/README.md) for local preview, synchronization, storage and deployment details.
1010

1111
To run locally with Docker, execute `docker compose up --build -d` from this directory, then open http://localhost:8080/. Stop it with `docker compose down`.
12+
13+
Browse all news at `/publicacoes/` and scientific papers at `/papers/`. Both catalogs include local text search. Each paper has a statically generated detail page with its stored bibliographic information and an original-source link when available.

‎web/app/globals.css‎

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,10 +1156,6 @@ footer {
11561156
line-height: 1.45;
11571157
letter-spacing: -0.35px;
11581158
font-weight: 500;
1159-
display: -webkit-box;
1160-
-webkit-line-clamp: 3;
1161-
-webkit-box-orient: vertical;
1162-
overflow: hidden;
11631159
margin: 0 0 12px;
11641160
}
11651161
.publication-preview-content > p {
@@ -1633,3 +1629,48 @@ footer {
16331629
gap: 4px;
16341630
}
16351631
}
1632+
1633+
.catalog-page {
1634+
max-width: 1280px;
1635+
margin: 0 auto;
1636+
padding: 48px 24px 80px;
1637+
}
1638+
.catalog-page .papers-section {
1639+
padding: 0;
1640+
}
1641+
.catalog-search {
1642+
display: flex;
1643+
flex-direction: column;
1644+
gap: 10px;
1645+
margin: 24px 0;
1646+
color: #264864;
1647+
font-size: 14px;
1648+
}
1649+
.catalog-search input {
1650+
width: 100%;
1651+
max-width: 560px;
1652+
border: 1px solid #cbd8e2;
1653+
border-radius: 4px;
1654+
padding: 12px 14px;
1655+
font: inherit;
1656+
background: white;
1657+
color: #15344e;
1658+
}
1659+
.catalog-search input:focus-visible {
1660+
outline: 2px solid #167ab4;
1661+
outline-offset: 2px;
1662+
}
1663+
.publication-grid {
1664+
display: grid;
1665+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
1666+
gap: 24px;
1667+
}
1668+
.publication-grid .publication-preview {
1669+
width: auto;
1670+
min-width: 0;
1671+
}
1672+
.publication-preview h3,
1673+
.publication-detail h1,
1674+
.paper-entry h3 {
1675+
overflow-wrap: anywhere;
1676+
}

‎web/app/linkedin-posts.js‎

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ import Link from 'next/link';
44
import { useRef, useState } from 'react';
55
import { ArrowLeft, ArrowRight, ArrowUpRight } from 'lucide-react';
66
import PublicationImage from './publication-image';
7-
export default function LinkedinPosts({ news }) {
7+
export default function LinkedinPosts({ news, listing = false }) {
88
const track = useRef(null),
99
[limit, setLimit] = useState(24);
10-
const posts = news.posts;
10+
const [query, setQuery] = useState('');
11+
const posts = news.posts.filter((post) =>
12+
`${post.title} ${post.text} ${post.author.name}`
13+
.toLocaleLowerCase()
14+
.includes(query.toLocaleLowerCase().trim()),
15+
);
1116
function loadNearEnd(event) {
1217
const element = event.currentTarget;
1318
if (
@@ -32,24 +37,38 @@ export default function LinkedinPosts({ news }) {
3237
{news.lastCollectedAt &&
3338
` · last collected on ${new Date(news.lastCollectedAt).toLocaleDateString('en-US')}`}
3439
</p>
35-
<div className="shelf-arrows">
36-
<button onClick={() => move(-1)} aria-label="Previous publications">
37-
<ArrowLeft size={17} />
38-
</button>
39-
<button onClick={() => move(1)} aria-label="Next publications">
40-
<ArrowRight size={17} />
41-
</button>
42-
</div>
40+
{!listing && (
41+
<div className="shelf-arrows">
42+
<button onClick={() => move(-1)} aria-label="Previous publications">
43+
<ArrowLeft size={17} />
44+
</button>
45+
<button onClick={() => move(1)} aria-label="Next publications">
46+
<ArrowRight size={17} />
47+
</button>
48+
</div>
49+
)}
4350
</div>
51+
{listing && (
52+
<label className="catalog-search">
53+
Search publications
54+
<input
55+
type="search"
56+
value={query}
57+
placeholder="Text or author"
58+
onChange={(event) => setQuery(event.target.value)}
59+
/>
60+
<span aria-live="polite">{posts.length} publications</span>
61+
</label>
62+
)}
4463
<div
45-
className="publication-horizontal-track"
64+
className={listing ? 'publication-grid' : 'publication-horizontal-track'}
4665
ref={track}
4766
onScroll={loadNearEnd}
4867
role="region"
49-
aria-label="Recent publications, horizontal scrolling"
68+
aria-label="Publications"
5069
tabIndex={0}
5170
>
52-
{posts.slice(0, limit).map((post) => (
71+
{posts.slice(0, listing ? posts.length : limit).map((post) => (
5372
<article className="publication-preview" key={post.id}>
5473
<Link
5574
className="publication-cover-link"

‎web/app/page.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Link from 'next/link';
12
import { ArrowUpRight, ArrowRight, Github } from 'lucide-react';
23
import { getGithub, getNews } from '../lib/data';
34
import Projects from './projects';
@@ -77,6 +78,9 @@ export default async function Home() {
7778
<h2>Publications and news</h2>
7879
</div>
7980
</div>
81+
<Link className="publication-back" href="/publicacoes/">
82+
View all publications <ArrowRight size={15} />
83+
</Link>
8084
<LinkedinPosts news={news} />
8185
</section>
8286
<People people={people} />

‎web/app/papers.js‎

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use client';
2+
import Link from 'next/link';
23
import { useRef, useState } from 'react';
34
import { ArrowLeft, ArrowRight, ArrowUpRight } from 'lucide-react';
45
const PAGE_SIZE = 10;
@@ -8,18 +9,23 @@ const pageWindow = (page, pages) =>
89
[...new Set([0, page - 1, page, page + 1, pages - 1])]
910
.filter((i) => i >= 0 && i < pages)
1011
.sort((a, b) => a - b);
11-
function Authors({ authors }) {
12+
export function Authors({ authors }) {
1213
const names = authors.slice(0, 10).map((author) => author.name);
1314
return names.join('; ') + (authors.length > 10 ? '; et al.' : '');
1415
}
15-
export default function Papers({ papers }) {
16+
export default function Papers({ papers, listing = false }) {
1617
const section = useRef(null);
18+
const [query, setQuery] = useState('');
1719
const [year, setYear] = useState('');
1820
const [page, setPage] = useState(0);
1921
const years = [...new Set(papers.papers.map((paper) => paper.year))];
20-
const filtered = year
21-
? papers.papers.filter((paper) => paper.year === Number(year))
22-
: papers.papers;
22+
const filtered = papers.papers.filter(
23+
(paper) =>
24+
(!year || paper.year === Number(year)) &&
25+
`${paper.title} ${paper.venue} ${paper.authors.map((author) => author.name).join(' ')}`
26+
.toLocaleLowerCase()
27+
.includes(query.toLocaleLowerCase().trim()),
28+
);
2329
const pages = Math.ceil(filtered.length / PAGE_SIZE);
2430
function go(next) {
2531
setPage(next);
@@ -56,19 +62,33 @@ export default function Papers({ papers }) {
5662
`${papers.lattesUpdatedAt}T12:00:00Z`,
5763
).toLocaleDateString('en-US')}`}
5864
</p>
65+
{listing ? (
66+
<label className="catalog-search">
67+
Search papers
68+
<input
69+
type="search"
70+
value={query}
71+
placeholder="Title, author or venue"
72+
onChange={(event) => {
73+
setQuery(event.target.value);
74+
setPage(0);
75+
}}
76+
/>
77+
</label>
78+
) : (
79+
<Link className="publication-back" href="/papers/">
80+
View all papers <ArrowRight size={15} />
81+
</Link>
82+
)}
5983
<ol className="papers-list">
6084
{filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE).map((paper) => (
6185
<li key={`${paper.kind}-${paper.year}-${paper.title}`} className="paper-entry">
6286
<span className="paper-year">{paper.year}</span>
6387
<div>
6488
<h3>
65-
{paper.url ? (
66-
<a href={paper.url} target="_blank" rel="noreferrer">
67-
{paper.title} <ArrowUpRight size={14} />
68-
</a>
69-
) : (
70-
paper.title
71-
)}
89+
<Link href={`/papers/${paper.slug}/`}>
90+
{paper.title} <ArrowUpRight size={14} />
91+
</Link>
7292
</h3>
7393
<p className="paper-authors">
7494
<Authors authors={paper.authors} />

‎web/app/papers/[slug]/page.js‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Link from 'next/link';
2+
import { notFound } from 'next/navigation';
3+
import { ArrowLeft, ArrowUpRight } from 'lucide-react';
4+
import SiteHeader from '../../site-header';
5+
import { readPapers } from '../../../lib/papers-store';
6+
export const dynamicParams = false;
7+
export async function generateStaticParams() {
8+
const { papers } = await readPapers();
9+
return papers.map(({ slug }) => ({ slug }));
10+
}
11+
async function findPaper(params) {
12+
const { slug } = await params;
13+
const { papers } = await readPapers();
14+
return papers.find((paper) => paper.slug === slug);
15+
}
16+
export async function generateMetadata({ params }) {
17+
const paper = await findPaper(params);
18+
return { title: paper ? `${paper.title} — UFFeScience` : 'Paper not found — UFFeScience' };
19+
}
20+
export default async function PaperPage({ params }) {
21+
const paper = await findPaper(params);
22+
if (!paper) notFound();
23+
return (
24+
<>
25+
<SiteHeader />
26+
<main className="publication-detail">
27+
<Link href="/papers/" className="publication-back">
28+
<ArrowLeft size={15} /> All papers
29+
</Link>
30+
<article>
31+
<div className="publication-detail-meta">
32+
<span>
33+
{paper.kind === 'journal'
34+
? 'Journal article'
35+
: paper.kind === 'accepted'
36+
? 'Accepted paper'
37+
: 'Conference paper'}
38+
</span>
39+
<span>{paper.year}</span>
40+
</div>
41+
<h1>{paper.title}</h1>
42+
<div className="publication-full-text">
43+
<h2>Authors</h2>
44+
<p>{paper.authors.map((author) => author.name).join('; ')}</p>
45+
<h2>Published in</h2>
46+
<p>{paper.venue}</p>
47+
{paper.abstract && (
48+
<>
49+
<h2>Abstract</h2>
50+
<p>{paper.abstract}</p>
51+
</>
52+
)}
53+
{paper.text && (
54+
<>
55+
<h2>Content</h2>
56+
<p>{paper.text}</p>
57+
</>
58+
)}
59+
</div>
60+
{paper.url ? (
61+
<a className="publication-original" href={paper.url} target="_blank" rel="noreferrer">
62+
Read original paper <ArrowUpRight size={16} />
63+
</a>
64+
) : (
65+
<p className="section-description">
66+
An original paper link is not available in the stored record.
67+
</p>
68+
)}
69+
</article>
70+
</main>
71+
</>
72+
);
73+
}

‎web/app/papers/page.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import SiteHeader from '../site-header';
2+
import Papers from '../papers';
3+
import { readPapers } from '../../lib/papers-store';
4+
export const metadata = { title: 'All papers — UFFeScience' };
5+
export default async function PapersPage() {
6+
return (
7+
<>
8+
<SiteHeader />
9+
<main className="catalog-page">
10+
<Papers papers={await readPapers()} listing />
11+
</main>
12+
</>
13+
);
14+
}

‎web/app/publicacoes/[slug]/page.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assetPath, homePath } from '../../../lib/site-path';
1+
import { assetPath } from '../../../lib/site-path';
22
import Link from 'next/link';
33
import { notFound } from 'next/navigation';
44
import { ArrowLeft, ArrowUpRight } from 'lucide-react';
@@ -29,7 +29,7 @@ export default async function PublicationPage({ params }) {
2929
<>
3030
<SiteHeader />
3131
<main className="publication-detail">
32-
<Link href={homePath('#novidades')} className="publication-back">
32+
<Link href={'/publicacoes/'} className="publication-back">
3333
<ArrowLeft size={15} /> All publications
3434
</Link>
3535
<article>
@@ -98,7 +98,7 @@ export default async function PublicationPage({ params }) {
9898
)}
9999
</article>
100100
<div className="publication-detail-footer">
101-
<Link href={homePath('#novidades')}>Back to publications</Link>
101+
<Link href={'/publicacoes/'}>Back to publications</Link>
102102
{next && (
103103
<Link href={`/publicacoes/${next.slug}`}>
104104
Next publication <ArrowUpRight size={14} />

‎web/app/publicacoes/page.js‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import SiteHeader from '../site-header';
2+
import LinkedinPosts from '../linkedin-posts';
3+
import { readPublications } from '../../lib/publication-store';
4+
export const metadata = { title: 'All publications — UFFeScience' };
5+
export default async function PublicationsPage() {
6+
return (
7+
<>
8+
<SiteHeader />
9+
<main className="catalog-page">
10+
<div className="section-heading">
11+
<div>
12+
<div className="kicker">NEWS / EVENTS / RESULTS</div>
13+
<h1>Publications</h1>
14+
</div>
15+
</div>
16+
<LinkedinPosts news={await readPublications()} listing />
17+
</main>
18+
</>
19+
);
20+
}

‎web/app/site-header.js‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { assetPath, homePath } from '../lib/site-path';
2+
import Link from 'next/link';
23
import { Github, ArrowUpRight } from 'lucide-react';
34
export default function SiteHeader() {
45
return (
@@ -10,8 +11,8 @@ export default function SiteHeader() {
1011
</span>
1112
</a>
1213
<nav aria-label="Main navigation">
13-
<a href={homePath('#novidades')}>Publications</a>
14-
<a href={homePath('#artigos')}>Papers</a>
14+
<Link href="/publicacoes/">Publications</Link>
15+
<Link href="/papers/">Papers</Link>
1516
<a href={homePath('#projetos')}>Projects</a>
1617
<a href={homePath('#pessoas')}>Team</a>
1718
</nav>

0 commit comments

Comments
 (0)