Commit 346abc3d by PLN (Algolia)

feat(poems): Improvements

parent 426231d8
......@@ -12,9 +12,10 @@ function getAllContentData(name, sorted = false) {
// Get file names under /content/{name}
const contentDirectory = getContentDirectory(name);
const fileNames = fs.readdirSync(contentDirectory);
// console.log(`gACD: total ${name}(s): ${fileNames.length}`);
const allContentData = fileNames.map((fileName) => {
const allContentData = fileNames
.filter(fileName => fileName.endsWith('.md')) // Keep only .md files
.map((fileName) => {
// Remove ".md" from file name to get id
const id = fileName.replace(/\.md$/, "");
......
......@@ -43,8 +43,8 @@ export default function Home({ posts, talks }) {
</p>
</section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<Link href="/parvagues/" className={utilStyles.sectionLink} legacyBehavior>
…code into <em>music</em>
<Link href="/parvagues/" className={utilStyles.sectionLink}>
<span>…code into <em>music</em></span>
</Link>
<p className="description">
As <Link href="/parvagues/" className={utilStyles.inlineLink}>ParVagues</Link>, I write patterns that
......@@ -52,8 +52,8 @@ export default function Home({ posts, talks }) {
</p>
</section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<Link href="/hydra/" className={utilStyles.sectionLink} legacyBehavior>
…code into <em>animated pixelscapes</em>
<Link href="/hydra/" className={utilStyles.sectionLink}>
<span>…code into <em>animated pixelscapes</em></span>
</Link>
<p className="description">
Using <Link href="/hydra/" className={utilStyles.inlineLink}>Hydra</Link>, I create animations that
......@@ -61,16 +61,16 @@ export default function Home({ posts, talks }) {
</p>
</section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<Link href="/poesie/" className={utilStyles.sectionLink} legacyBehavior>
…words into <em>thoughts</em>
<Link href="/poesie/" className={utilStyles.sectionLink}>
<span>…words into <em>thoughts</em></span>
</Link>
<p className="description">
A selection of texts scattered across note books and note apps.
</p>
</section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<Link href="/talks/" className={utilStyles.sectionLink} legacyBehavior>
…ideas into <em>talks</em>
<Link href="/talks/" className={utilStyles.sectionLink}>
<span>…ideas into <em>talks</em></span>
</Link>
<p className="description">
I <Link href="/talks/" className={utilStyles.inlineLink}>speak</Link> about topics I care about: from
......@@ -82,8 +82,8 @@ export default function Home({ posts, talks }) {
<ul className={utilStyles.list}>
{talks.map(({ id, title, description }) => (
<li className={utilStyles.listItem} key={id}>
<Link href={`/talks#${id}`} className={utilStyles.listItemLink} legacyBehavior>
{title}
<Link href={`/talks#${id}`} className={utilStyles.listItemLink}>
<span>{title}</span>
</Link>
{description && (
<small className={utilStyles.lightText}>{description}</small>
......
......@@ -23,7 +23,8 @@ export default function Poems({ poems }) {
// Get unique languages and tags
const languages = ['all', ...new Set(poems.map(p => p.language || 'unknown'))];
const tags = ['all', ...new Set(poems.flatMap(p => p.tags || []))];
const allTags = [...new Set(poems.flatMap(p => p.tags || []))];
const tags = ['all', ...allTags];
// Filter and sort poems
const filteredPoems = poems
......@@ -47,57 +48,113 @@ export default function Poems({ poems }) {
return (
<Layout>
<Head>
<title>Poems</title>
<title>Poems | Words into thoughts</title>
<meta name="description" content="A collection of poems and thoughts" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<div className={utilStyles.headingMd}>
<h1>Words into thoughts</h1>
<p>A selection of texts scattered across note books and note apps.</p>
<div className={utilStyles.filters}>
<div>
<label>Sort by: </label>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<div className={utilStyles.filtersContainer}>
<div className={utilStyles.filtersGroup}>
<div className={utilStyles.filterItem}>
<span className={utilStyles.filterLabel}>Sort by</span>
<select
className={utilStyles.filterSelect}
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
aria-label="Sort poems by"
>
<option value="date">Date</option>
<option value="wordcount">Word count</option>
</select>
<select value={sortOrder} onChange={(e) => setSortOrder(e.target.value)}>
<option value="desc">Descending</option>
<option value="asc">Ascending</option>
</div>
<div className={utilStyles.filterItem}>
<span className={utilStyles.filterLabel}>Order</span>
<select
className={utilStyles.filterSelect}
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
aria-label="Sort order"
>
<option value="desc">Newest first</option>
<option value="asc">Oldest first</option>
</select>
</div>
<div>
<label>Language: </label>
<select value={filterLanguage} onChange={(e) => setFilterLanguage(e.target.value)}>
<div className={utilStyles.filterItem}>
<span className={utilStyles.filterLabel}>Language</span>
<select
className={utilStyles.filterSelect}
value={filterLanguage}
onChange={(e) => setFilterLanguage(e.target.value)}
aria-label="Filter by language"
>
{languages.map(lang => (
<option key={lang} value={lang}>{lang}</option>
<option key={lang} value={lang}>
{lang === 'all' ? 'All languages' : lang}
</option>
))}
</select>
</div>
<div>
<label>Tag: </label>
<select value={filterTags} onChange={(e) => setFilterTags(e.target.value)}>
<div className={utilStyles.filterItem}>
<span className={utilStyles.filterLabel}>Tags</span>
<select
className={utilStyles.filterSelect}
value={filterTags}
onChange={(e) => setFilterTags(e.target.value)}
aria-label="Filter by tag"
>
{tags.map(tag => (
<option key={tag} value={tag}>{tag}</option>
<option key={tag} value={tag}>
{tag === 'all' ? 'All tags' : tag}
</option>
))}
</select>
</div>
</div>
</div>
<ul className={utilStyles.list}>
{sortedPoems.map(({ id, title, date, language, tags }) => (
<li className={utilStyles.listItem} key={id}>
<Link href={`/poesie/${id}`} className={utilStyles.listItemLink} legacyBehavior>
<div className={utilStyles.list}>
{sortedPoems.length === 0 ? (
<p>No poems match your current filters. Try adjusting your selection.</p>
) : (
sortedPoems.map(({ id, title, date, language, tags }) => (
<div className={utilStyles.poemCard} key={id}>
<Link href={`/poesie/${id}`} className={utilStyles.listItemLink}>
{title}
</Link>
<small className={utilStyles.lightText}>
<div className={utilStyles.poemMeta}>
{date && (
<span className={utilStyles.poemMetaItem}>
<Date dateString={date} />
{language && ` • ${language}`}
{tags && tags.length > 0 && ` • ${tags.join(', ')}`}
</small>
</li>
</span>
)}
{language && (
<span className={utilStyles.poemMetaItem}>
{language}
</span>
)}
</div>
{tags && tags.length > 0 && (
<div className={utilStyles.tagList}>
{tags.map(tag => (
<span key={tag} className={utilStyles.tag}>
{tag}
</span>
))}
</ul>
</div>
)}
</div>
))
)}
</div>
</div>
</Layout>
);
......
......@@ -3,6 +3,7 @@ import Layout from '@/components/layout';
import Date from '@/components/date';
import utilStyles from '@/styles/utils.module.css';
import { getPoemData, getAllPoemIds } from '@/lib/poems';
import Link from 'next/link';
export async function getStaticProps({ params }) {
const poemData = await getPoemData(params.id);
......@@ -26,14 +27,43 @@ export default function Poem({ poemData }) {
<Layout>
<Head>
<title>{poemData.title}</title>
<meta name="description" content={`${poemData.title} - Poem by PLN`} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<article>
<Link href="/poesie" className={utilStyles.backLink}>
Back to poems
</Link>
<article className={utilStyles.poemContainer}>
<header>
<h1 className={utilStyles.headingXl}>{poemData.title}</h1>
<div className={utilStyles.lightText}>
<div className={utilStyles.poemMeta}>
{poemData.date && (
<span className={utilStyles.poemMetaItem}>
<Date dateString={poemData.date} />
{poemData.language && ` • ${poemData.language}`}
{poemData.tags && poemData.tags.length > 0 && ` • ${poemData.tags.join(', ')}`}
</span>
)}
{poemData.language && (
<span className={utilStyles.poemMetaItem}>
{poemData.language}
</span>
)}
</div>
{poemData.tags && poemData.tags.length > 0 && (
<div className={utilStyles.tagList}>
{poemData.tags.map(tag => (
<span key={tag} className={utilStyles.tag}>
{tag}
</span>
))}
</div>
)}
</header>
<div
className={utilStyles.poemContent}
dangerouslySetInnerHTML={{ __html: poemData.contentHtml }}
......
......@@ -65,20 +65,140 @@
border-radius: 4px;
}
.poemCard {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
margin-bottom: 1.5rem;
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.poemCard:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.poemMeta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
color: #666;
font-size: 0.9rem;
}
.poemMetaItem {
display: inline-flex;
align-items: center;
}
.poemMetaItem:not(:last-child)::after {
content: "•";
margin-left: 0.5rem;
opacity: 0.6;
}
.tagList {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
}
.tag {
background-color: #f0f0f0;
border-radius: 12px;
padding: 0.25rem 0.75rem;
font-size: 0.85rem;
color: #555;
transition: background-color 0.2s;
}
.tag:hover {
background-color: #e0e0e0;
}
.filtersContainer {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.filtersGroup {
display: flex;
flex-direction: column;
gap: 1rem;
}
@media (min-width: 768px) {
.filtersGroup {
flex-direction: row;
flex-wrap: wrap;
}
}
.filterItem {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.filterLabel {
font-weight: 600;
color: #333;
min-width: 80px;
}
.filterSelect {
padding: 0.5rem 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
min-width: 120px;
font-size: 0.9rem;
color: #333;
transition: border-color 0.2s, box-shadow 0.2s;
}
.filterSelect:focus {
border-color: #0070f3;
box-shadow: 0 0 0 2px rgba(0, 112, 243, 0.2);
outline: none;
}
.filterSelect:hover {
border-color: #999;
}
.poemContent {
margin-top: 2rem;
line-height: 1.8;
white-space: pre-wrap;
font-size: 1.1rem;
color: #333;
max-width: 70ch;
}
@media (max-width: 768px) {
.poemContent {
font-size: 1rem;
line-height: 1.7;
}
}
.poemContent p {
margin: 1rem 0;
margin: 1.5rem 0;
}
.poemContent img {
max-width: 100%;
height: auto;
margin: 1rem 0;
margin: 1.5rem 0;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.listItemLink {
......@@ -115,3 +235,33 @@
.inlineLink:hover {
opacity: 0.8;
}
.poemContainer {
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.backLink {
display: inline-block;
margin: 1rem 0;
color: #666;
text-decoration: none;
font-size: 0.9rem;
transition: color 0.2s;
}
.backLink:hover {
color: #0070f3;
text-decoration: underline;
}
@media (max-width: 768px) {
.poemContainer {
padding: 1.5rem;
margin: 1rem auto;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment