Skip to content

Commit

Permalink
Merge pull request #168 from ChtiJS/refactor/app_router_migration
Browse files Browse the repository at this point in the history
refactor(core): switch to the app router
  • Loading branch information
nfroidure authored Dec 16, 2023
2 parents 2eded1b + 8b2159c commit 529d97f
Show file tree
Hide file tree
Showing 41 changed files with 1,201 additions and 1,215 deletions.
77 changes: 77 additions & 0 deletions src/app/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { join as pathJoin } from 'path';
import ContentBlock from '../../components/contentBlock';
import Paragraph from '../../components/p';
import Anchor from '../../components/a';
import { parseMarkdown, renderMarkdown } from '../../utils/markdown';
import { readEntry, readPaths } from '../../utils/frontmatter';
import { toASCIIString } from '../../utils/ascii';
import buildMetadata from '../../utils/metadata';
import type { MarkdownRootNode } from '../../utils/markdown';

type Metadata = {
date: string;
title: string;
description: string;
author: string;
illustration?: {
href: string;
alt: string;
};
lang: string;
location: string;
};
type Entry = {
id: string;
content: MarkdownRootNode;
} & Metadata;

type Params = { id: string };

export async function generateMetadata({ params }: { params: Params }) {
const result = await readEntry<Metadata>(
pathJoin('contents', 'pages', (params?.id as string) + '.md')
);
const entry: Entry = {
...result.attributes,
id: toASCIIString(result.attributes.title),
content: parseMarkdown(result.body) as MarkdownRootNode,
};

return buildMetadata({
pathname: `/${entry.id}`,
title: entry.title,
description: entry.description,
image: entry.illustration?.href,
});
}

export default async function Page({ params }: { params: Params }) {
const result = await readEntry<Metadata>(
pathJoin('contents', 'pages', (params?.id as string) + '.md')
);
const entry: Entry = {
...result.attributes,
id: toASCIIString(result.attributes.title),
content: parseMarkdown(result.body) as MarkdownRootNode,
};

return (
<ContentBlock>
{renderMarkdown({ index: 0 }, entry.content)}
<div className="clear"></div>
<Paragraph>
<Anchor href="/">Retour</Anchor>
</Paragraph>
</ContentBlock>
);
}

export async function generateStaticParams() {
const paths = (await readPaths(pathJoin('.', 'contents', 'pages'))).map(
(path) => ({
id: path.replace('.md', ''),
})
);

return paths;
}
14 changes: 14 additions & 0 deletions src/app/conferences/[id]/conference.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.root {
:global(p.illustration) {
float: left;
width: var(--block);
margin: 0 var(--gutter) 0 0;
}
img {
width: 100%;
margin: 0;
}
.clear {
clear: both;
}
}
72 changes: 72 additions & 0 deletions src/app/conferences/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { join as pathJoin } from 'path';
import { entriesToBaseProps } from '../page';
import { readEntries } from '../../../utils/frontmatter';
import ContentBlock from '../../../components/contentBlock';
import Paragraph from '../../../components/p';
import Anchor from '../../../components/a';
import { fixText } from '../../../utils/text';
import { renderMarkdown } from '../../../utils/markdown';
import buildMetadata from '../../../utils/metadata';
import styles from './conference.module.scss';
import type { Metadata } from '../page';
import type { Entry } from '../page';

type Params = { id: string };

export async function generateMetadata({ params }: { params: Params }) {
const baseProps = entriesToBaseProps(
await readEntries<Metadata>(pathJoin('.', 'contents', 'conferences'))
);
const entry = baseProps.entries.find(
({ id }) => id === (params || {}).id
) as Entry;

return buildMetadata({
title: fixText(entry.title),
description: fixText(entry.description),
image: entry.illustration?.url,
pathname: `/conference/${params.id}`,
});
}

export async function BlogPost({ params }: { params: Params }) {
const baseProps = entriesToBaseProps(
await readEntries<Metadata>(pathJoin('.', 'contents', 'conferences'))
);
const entry = baseProps.entries.find(
({ id }) => id === (params || {}).id
) as Entry;

return (
<ContentBlock className={styles.root}>
{renderMarkdown({ index: 0 }, entry.content)}
<Paragraph>
Publié le{' '}
{new Date(entry.date).toLocaleDateString(undefined, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
.
</Paragraph>
<div className={styles.clear}></div>
<Paragraph>
<Anchor href="/conferences">Retour</Anchor>
</Paragraph>
</ContentBlock>
);
}

export default BlogPost;

export async function generateStaticParams() {
const baseProps = entriesToBaseProps(
await readEntries<Metadata>(pathJoin('.', 'contents', 'conferences'))
);
const paths = baseProps.entries.map((entry) => ({
id: entry.id,
}));

return paths;
}
61 changes: 61 additions & 0 deletions src/app/conferences/conferences.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
@import '../../variables.scss';

.root {
img {
width: 100%;
}
label {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: var(--gutter);
font-weight: bold;
}
input {
height: var(--vRythm);
width: var(--block);
padding: 0 calc(var(--gutter) / 2);
}
:global(.entry_title) {
margin-top: 0 !important;
}
:global(.entry_title a) {
text-decoration: none !important;
}
:global(.entry_illustration) {
margin: 0 !important;
}
:global(.entry_description) {
margin: 0 !important;
}
}
.entry_item {
padding: var(--vRythm) 0;
border-bottom: var(--border) solid var(--dark);
}
.entry_item:first-child {
padding: 0 0 var(--vRythm) 0;
}
.entry_item:last-child {
border: none;
padding: var(--vRythm) 0 0 0;
}
.pagination {
display: flex;
gap: var(--gutter);
align-items: center;
justify-content: center;
padding: var(--vRythm) 0 0 0;
}
@media screen and (min-width: $CSS_BREAKPOINT_START_L) {
.root {
img {
float: left;
width: var(--block);
margin-right: var(--gutter);
}
.clear {
clear: left;
}
}
}
168 changes: 168 additions & 0 deletions src/app/conferences/entries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
'use client';

import styles from './conferences.module.scss';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
import lunr, { type Index } from 'lunr';
import ContentBlock from '../../components/contentBlock';
import Heading1 from '../../components/h1';
import Heading2 from '../../components/h2';
import Paragraph from '../../components/p';
import Anchor from '../../components/a';
import type { MarkdownRootNode } from '../../utils/markdown';

export type Metadata = {
leafname?: string;
title: string;
description: string;
date: string;
draft: boolean;
tags: string[];
categories: string[];
illustration?: {
url: string;
alt: string;
};
lang: string;
location: string;
};
export type Entry = {
id: string;
content: MarkdownRootNode;
} & Metadata;

export type EntriesProps = {
entries: Entry[];
pagesCount: number;
page: number;
};

const fetcher = (url: string) => fetch(url).then((res) => res.json());

export const Entries = ({ entries, page, pagesCount }: EntriesProps) => {
const [search, setSearch] = useState('');
const [searchIndex, setSearchIndex] = useState<Index>();
const [searchResults, setSearchResults] = useState<
{ id: string; title: string; description: string }[]
>([]);
const { data, error, isLoading } = useSWR(
// Only load the search data when searching 🤷
search ? '/conferencesSearchIndex.json' : null,
fetcher
);

useEffect(() => {
if (data) {
setSearchIndex(lunr.Index.load(data.index));
} else {
setSearchIndex(undefined);
}
}, [data]);

useEffect(() => {
if (searchIndex && search) {
setSearchResults(
searchIndex.search(search).map((result) => ({
result,
id: result.ref,
...data.metadata[result.ref],
}))
);
} else {
setSearchResults([]);
}
}, [data, searchIndex, search]);

return (
<ContentBlock className={styles.root}>
<Heading1>Résumés des rencontres</Heading1>
<Paragraph>
Découvrez le résumé de chacune de nos rencontres ChtiJS.
</Paragraph>
{page === 1 ? (
<Paragraph>
<label>
<span>Recherche :</span>
<input
type="search"
placeholder="Rechercher une conference"
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
</label>
</Paragraph>
) : null}
{search ? (
error ? (
<Paragraph>Impossible de charger l’index de recherche.</Paragraph>
) : isLoading ? (
<Paragraph>Chargement de l’index de recherche...</Paragraph>
) : searchResults.length ? (
<Paragraph>
{searchResults.length} résultat
{searchResults.length > 1 ? 's' : ''} pour la recherche “{search}
”.
</Paragraph>
) : (
<Paragraph>Aucun résultat pour la recherche “{search}”.</Paragraph>
)
) : null}
<div>
{(
(search ? searchResults : entries) as Pick<
Entry,
'id' | 'title' | 'description' | 'illustration'
>[]
).map((entry) => (
<div className={styles.entry_item} key={entry.id}>
{entry.illustration ? (
<Paragraph className={styles.entry_illustration}>
<Anchor href={`/conferences/${entry.id}`}>
<img
src={entry.illustration.url}
alt={entry.illustration.alt}
/>
</Anchor>
</Paragraph>
) : null}
<Heading2 className={styles.entry_title}>
<Anchor href={`/conferences/${entry.id}`}>{entry.title}</Anchor>
</Heading2>
<Paragraph className={styles.entry_title}>
{entry.description}
</Paragraph>
<Anchor href={`/conferences/${entry.id}`}>Lire la suite</Anchor>
<div className={styles.clear}></div>
</div>
))}
</div>
{!search ? (
<nav className={styles.pagination}>
{page > 1 ? (
<Anchor
icon="arrow-left"
href={
page > 2 ? `/conferences/pages/${page - 1}` : '/conferences'
}
rel="previous"
>
Précédent
</Anchor>
) : null}{' '}
{page < pagesCount ? (
<Anchor
icon="arrow-right"
iconPosition="last"
href={`/conferences/pages/${page + 1}`}
rel="next"
>
Suivant
</Anchor>
) : null}
</nav>
) : null}
</ContentBlock>
);
};
Loading

0 comments on commit 529d97f

Please sign in to comment.