Commit 346abc3d by PLN (Algolia)

feat(poems): Improvements

parent 426231d8
...@@ -12,25 +12,26 @@ function getAllContentData(name, sorted = false) { ...@@ -12,25 +12,26 @@ function getAllContentData(name, sorted = false) {
// Get file names under /content/{name} // Get file names under /content/{name}
const contentDirectory = getContentDirectory(name); const contentDirectory = getContentDirectory(name);
const fileNames = fs.readdirSync(contentDirectory); const fileNames = fs.readdirSync(contentDirectory);
// console.log(`gACD: total ${name}(s): ${fileNames.length}`);
const allContentData = fileNames
const allContentData = fileNames.map((fileName) => { .filter(fileName => fileName.endsWith('.md')) // Keep only .md files
// Remove ".md" from file name to get id .map((fileName) => {
const id = fileName.replace(/\.md$/, ""); // Remove ".md" from file name to get id
const id = fileName.replace(/\.md$/, "");
// Read markdown file as string
const fullPath = path.join(contentDirectory, fileName); // Read markdown file as string
const fileContents = fs.readFileSync(fullPath, "utf8"); const fullPath = path.join(contentDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, "utf8");
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents); // Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents);
// Combine the data with the id
return { // Combine the data with the id
id, return {
...matterResult.data, id,
}; ...matterResult.data,
}); };
});
return sorted return sorted
? allContentData.sort((a, b) => { ? allContentData.sort((a, b) => {
......
...@@ -43,8 +43,8 @@ export default function Home({ posts, talks }) { ...@@ -43,8 +43,8 @@ export default function Home({ posts, talks }) {
</p> </p>
</section> </section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}> <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<Link href="/parvagues/" className={utilStyles.sectionLink} legacyBehavior> <Link href="/parvagues/" className={utilStyles.sectionLink}>
…code into <em>music</em> <span>…code into <em>music</em></span>
</Link> </Link>
<p className="description"> <p className="description">
As <Link href="/parvagues/" className={utilStyles.inlineLink}>ParVagues</Link>, I write patterns that As <Link href="/parvagues/" className={utilStyles.inlineLink}>ParVagues</Link>, I write patterns that
...@@ -52,8 +52,8 @@ export default function Home({ posts, talks }) { ...@@ -52,8 +52,8 @@ export default function Home({ posts, talks }) {
</p> </p>
</section> </section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}> <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<Link href="/hydra/" className={utilStyles.sectionLink} legacyBehavior> <Link href="/hydra/" className={utilStyles.sectionLink}>
…code into <em>animated pixelscapes</em> <span>…code into <em>animated pixelscapes</em></span>
</Link> </Link>
<p className="description"> <p className="description">
Using <Link href="/hydra/" className={utilStyles.inlineLink}>Hydra</Link>, I create animations that Using <Link href="/hydra/" className={utilStyles.inlineLink}>Hydra</Link>, I create animations that
...@@ -61,16 +61,16 @@ export default function Home({ posts, talks }) { ...@@ -61,16 +61,16 @@ export default function Home({ posts, talks }) {
</p> </p>
</section> </section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}> <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<Link href="/poesie/" className={utilStyles.sectionLink} legacyBehavior> <Link href="/poesie/" className={utilStyles.sectionLink}>
…words into <em>thoughts</em> <span>…words into <em>thoughts</em></span>
</Link> </Link>
<p className="description"> <p className="description">
A selection of texts scattered across note books and note apps. A selection of texts scattered across note books and note apps.
</p> </p>
</section> </section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}> <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<Link href="/talks/" className={utilStyles.sectionLink} legacyBehavior> <Link href="/talks/" className={utilStyles.sectionLink}>
…ideas into <em>talks</em> <span>…ideas into <em>talks</em></span>
</Link> </Link>
<p className="description"> <p className="description">
I <Link href="/talks/" className={utilStyles.inlineLink}>speak</Link> about topics I care about: from 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 }) { ...@@ -82,8 +82,8 @@ export default function Home({ posts, talks }) {
<ul className={utilStyles.list}> <ul className={utilStyles.list}>
{talks.map(({ id, title, description }) => ( {talks.map(({ id, title, description }) => (
<li className={utilStyles.listItem} key={id}> <li className={utilStyles.listItem} key={id}>
<Link href={`/talks#${id}`} className={utilStyles.listItemLink} legacyBehavior> <Link href={`/talks#${id}`} className={utilStyles.listItemLink}>
{title} <span>{title}</span>
</Link> </Link>
{description && ( {description && (
<small className={utilStyles.lightText}>{description}</small> <small className={utilStyles.lightText}>{description}</small>
......
...@@ -23,7 +23,8 @@ export default function Poems({ poems }) { ...@@ -23,7 +23,8 @@ export default function Poems({ poems }) {
// Get unique languages and tags // Get unique languages and tags
const languages = ['all', ...new Set(poems.map(p => p.language || 'unknown'))]; 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 // Filter and sort poems
const filteredPoems = poems const filteredPoems = poems
...@@ -47,57 +48,113 @@ export default function Poems({ poems }) { ...@@ -47,57 +48,113 @@ export default function Poems({ poems }) {
return ( return (
<Layout> <Layout>
<Head> <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> </Head>
<div className={utilStyles.headingMd}> <div className={utilStyles.headingMd}>
<h1>Words into thoughts</h1> <h1>Words into thoughts</h1>
<p>A selection of texts scattered across note books and note apps.</p>
<div className={utilStyles.filters}> <div className={utilStyles.filtersContainer}>
<div> <div className={utilStyles.filtersGroup}>
<label>Sort by: </label> <div className={utilStyles.filterItem}>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}> <span className={utilStyles.filterLabel}>Sort by</span>
<option value="date">Date</option> <select
<option value="wordcount">Word count</option> className={utilStyles.filterSelect}
</select> value={sortBy}
<select value={sortOrder} onChange={(e) => setSortOrder(e.target.value)}> onChange={(e) => setSortBy(e.target.value)}
<option value="desc">Descending</option> aria-label="Sort poems by"
<option value="asc">Ascending</option> >
</select> <option value="date">Date</option>
</div> <option value="wordcount">Word count</option>
</select>
<div> </div>
<label>Language: </label>
<select value={filterLanguage} onChange={(e) => setFilterLanguage(e.target.value)}> <div className={utilStyles.filterItem}>
{languages.map(lang => ( <span className={utilStyles.filterLabel}>Order</span>
<option key={lang} value={lang}>{lang}</option> <select
))} className={utilStyles.filterSelect}
</select> value={sortOrder}
</div> onChange={(e) => setSortOrder(e.target.value)}
aria-label="Sort order"
<div> >
<label>Tag: </label> <option value="desc">Newest first</option>
<select value={filterTags} onChange={(e) => setFilterTags(e.target.value)}> <option value="asc">Oldest first</option>
{tags.map(tag => ( </select>
<option key={tag} value={tag}>{tag}</option> </div>
))}
</select> <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 === 'all' ? 'All languages' : lang}
</option>
))}
</select>
</div>
<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 === 'all' ? 'All tags' : tag}
</option>
))}
</select>
</div>
</div> </div>
</div> </div>
<ul className={utilStyles.list}> <div className={utilStyles.list}>
{sortedPoems.map(({ id, title, date, language, tags }) => ( {sortedPoems.length === 0 ? (
<li className={utilStyles.listItem} key={id}> <p>No poems match your current filters. Try adjusting your selection.</p>
<Link href={`/poesie/${id}`} className={utilStyles.listItemLink} legacyBehavior> ) : (
{title} sortedPoems.map(({ id, title, date, language, tags }) => (
</Link> <div className={utilStyles.poemCard} key={id}>
<small className={utilStyles.lightText}> <Link href={`/poesie/${id}`} className={utilStyles.listItemLink}>
<Date dateString={date} /> {title}
{language && ` • ${language}`} </Link>
{tags && tags.length > 0 && ` • ${tags.join(', ')}`}
</small> <div className={utilStyles.poemMeta}>
</li> {date && (
))} <span className={utilStyles.poemMetaItem}>
</ul> <Date dateString={date} />
</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>
))}
</div>
)}
</div>
))
)}
</div>
</div> </div>
</Layout> </Layout>
); );
......
...@@ -3,6 +3,7 @@ import Layout from '@/components/layout'; ...@@ -3,6 +3,7 @@ import Layout from '@/components/layout';
import Date from '@/components/date'; import Date from '@/components/date';
import utilStyles from '@/styles/utils.module.css'; import utilStyles from '@/styles/utils.module.css';
import { getPoemData, getAllPoemIds } from '@/lib/poems'; import { getPoemData, getAllPoemIds } from '@/lib/poems';
import Link from 'next/link';
export async function getStaticProps({ params }) { export async function getStaticProps({ params }) {
const poemData = await getPoemData(params.id); const poemData = await getPoemData(params.id);
...@@ -26,14 +27,43 @@ export default function Poem({ poemData }) { ...@@ -26,14 +27,43 @@ export default function Poem({ poemData }) {
<Layout> <Layout>
<Head> <Head>
<title>{poemData.title}</title> <title>{poemData.title}</title>
<meta name="description" content={`${poemData.title} - Poem by PLN`} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head> </Head>
<article>
<h1 className={utilStyles.headingXl}>{poemData.title}</h1> <Link href="/poesie" className={utilStyles.backLink}>
<div className={utilStyles.lightText}> Back to poems
<Date dateString={poemData.date} /> </Link>
{poemData.language && ` • ${poemData.language}`}
{poemData.tags && poemData.tags.length > 0 && ` • ${poemData.tags.join(', ')}`} <article className={utilStyles.poemContainer}>
</div> <header>
<h1 className={utilStyles.headingXl}>{poemData.title}</h1>
<div className={utilStyles.poemMeta}>
{poemData.date && (
<span className={utilStyles.poemMetaItem}>
<Date dateString={poemData.date} />
</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 <div
className={utilStyles.poemContent} className={utilStyles.poemContent}
dangerouslySetInnerHTML={{ __html: poemData.contentHtml }} dangerouslySetInnerHTML={{ __html: poemData.contentHtml }}
......
...@@ -65,20 +65,140 @@ ...@@ -65,20 +65,140 @@
border-radius: 4px; 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 { .poemContent {
margin-top: 2rem; margin-top: 2rem;
line-height: 1.8; line-height: 1.8;
white-space: pre-wrap; 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 { .poemContent p {
margin: 1rem 0; margin: 1.5rem 0;
} }
.poemContent img { .poemContent img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
margin: 1rem 0; margin: 1.5rem 0;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
.listItemLink { .listItemLink {
...@@ -115,3 +235,33 @@ ...@@ -115,3 +235,33 @@
.inlineLink:hover { .inlineLink:hover {
opacity: 0.8; 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