diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-01-20 11:57:16 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-01-20 11:57:16 +0100 |
| commit | f5ba1046b13acd239c472e361f345902937662fb (patch) | |
| tree | d2468fa51f3d2f9dda61eb9cdf6594ad8e618352 /src | |
| parent | 08855874397399459b281f6f0506fa5e91cdfdc0 (diff) | |
chore: add a page for projects
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/ProjectPreview/ProjectPreview.module.scss | 87 | ||||
| -rw-r--r-- | src/components/ProjectPreview/ProjectPreview.tsx | 55 | ||||
| -rw-r--r-- | src/components/ProjectsList/ProjectsList.module.scss | 25 | ||||
| -rw-r--r-- | src/components/ProjectsList/ProjectsList.tsx | 21 | ||||
| -rw-r--r-- | src/config/seo.ts | 4 | ||||
| m--------- | src/content | 0 | ||||
| -rw-r--r-- | src/pages/projets.tsx | 108 | ||||
| -rw-r--r-- | src/styles/pages/Projects.module.scss | 13 | ||||
| -rw-r--r-- | src/ts/types/app.ts | 17 | ||||
| -rw-r--r-- | src/utils/helpers/projects.ts | 72 |
10 files changed, 402 insertions, 0 deletions
diff --git a/src/components/ProjectPreview/ProjectPreview.module.scss b/src/components/ProjectPreview/ProjectPreview.module.scss new file mode 100644 index 0000000..f29dc49 --- /dev/null +++ b/src/components/ProjectPreview/ProjectPreview.module.scss @@ -0,0 +1,87 @@ +@use "@styles/abstracts/functions" as fun; + +.article { + display: flex; + flex-flow: column nowrap; + height: 100%; + padding: var(--spacing-md) var(--spacing-md) var(--spacing-lg); +} + +.cover { + height: fun.convert-px(150); + position: relative; +} + +.title { + flex: 1; + margin: var(--spacing-xs) 0; + background: none; + text-decoration: underline solid transparent 0; + text-shadow: none; + transition: all 0.3s linear 0s; +} + +.body { + margin: 0 0 var(--spacing-xs); +} + +.meta { + display: block; + + &__item { + display: flex; + flex-flow: row wrap; + gap: var(--spacing-2xs); + } +} + +.link { + display: block; + height: 100%; + background: var(--color-bg); + color: inherit; + text-decoration: none; + border: fun.convert-px(3) solid var(--color-primary); + border-radius: fun.convert-px(5); + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow), + fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2) + var(--color-shadow), + fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4) + var(--color-shadow); + transition: all 0.3s ease-in-out 0s; + + &:hover, + &:focus, + &:active { + color: inherit; + } + + &:hover, + &:focus { + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow-light), + fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2) + var(--color-shadow-light), + fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4) + var(--color-shadow-light), + fun.convert-px(7) fun.convert-px(10) fun.convert-px(12) fun.convert-px(-3) + var(--color-shadow-light); + transform: scale(1.05); + } + + &:focus { + .title { + text-decoration: underline solid var(--color-primary) 0.3ex; + } + } + + &:active { + box-shadow: 0 0 0 0 var(--color-shadow); + transform: scale(0.95); + + .title { + text-decoration: none; + } + } +} diff --git a/src/components/ProjectPreview/ProjectPreview.tsx b/src/components/ProjectPreview/ProjectPreview.tsx new file mode 100644 index 0000000..91969b0 --- /dev/null +++ b/src/components/ProjectPreview/ProjectPreview.tsx @@ -0,0 +1,55 @@ +import { t } from '@lingui/macro'; +import { Project } from '@ts/types/app'; +import { slugify } from '@utils/helpers/slugify'; +import Image from 'next/image'; +import Link from 'next/link'; +import styles from './ProjectPreview.module.scss'; + +const ProjectPreview = ({ project }: { project: Project }) => { + return ( + <Link href={`/projet/${project.slug}`}> + <a className={styles.link}> + <article className={styles.article}> + <header> + {project.cover && ( + <div className={styles.cover}> + <Image + src={project.cover} + layout="fill" + objectFit="contain" + objectPosition="center" + alt={`${project.meta.title} picture`} + /> + </div> + )} + <h2 className={styles.title}>{project.meta.title}</h2> + </header> + <div + className={styles.body} + dangerouslySetInnerHTML={{ __html: project.intro }} + ></div> + <footer> + <dl className={styles.meta}> + {project.meta.license && ( + <div className={styles.meta__item}> + <dt>{t`License:`}</dt> + <dd>{project.meta.license}</dd> + </div> + )} + {project.meta.technologies && ( + <div className={styles.meta__item}> + <dt>{t`Technologies:`}</dt> + {project.meta.technologies.map((techno) => ( + <dd key={slugify(techno)}>{techno}</dd> + ))} + </div> + )} + </dl> + </footer> + </article> + </a> + </Link> + ); +}; + +export default ProjectPreview; diff --git a/src/components/ProjectsList/ProjectsList.module.scss b/src/components/ProjectsList/ProjectsList.module.scss new file mode 100644 index 0000000..6a77aa9 --- /dev/null +++ b/src/components/ProjectsList/ProjectsList.module.scss @@ -0,0 +1,25 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/placeholders"; + +.list { + --items: 4; + --items-size: 35ch; + + @extend %reset-list; + + display: grid; + grid-template-columns: repeat( + auto-fit, + min(calc(100vw - (var(--spacing-md) * 2)), var(--items-size)) + ); + gap: var(--spacing-sm); + place-content: center; + width: min( + calc(100vw - (var(--spacing-md) * 2)), + calc( + (var(--items-size) * var(--items)) + + (var(--spacing-sm) * (var(--items) - 1)) + ) + ); + margin: var(--spacing-sm) auto 0; +} diff --git a/src/components/ProjectsList/ProjectsList.tsx b/src/components/ProjectsList/ProjectsList.tsx new file mode 100644 index 0000000..609d824 --- /dev/null +++ b/src/components/ProjectsList/ProjectsList.tsx @@ -0,0 +1,21 @@ +import ProjectPreview from '@components/ProjectPreview/ProjectPreview'; +import { Project } from '@ts/types/app'; +import styles from './ProjectsList.module.scss'; + +const ProjectsList = ({ projects }: { projects: Project[] }) => { + const getProjectItems = () => { + return projects.map((project) => { + return project.meta.title ? ( + <li className={styles.item} key={project.id}> + <ProjectPreview project={project} /> + </li> + ) : ( + '' + ); + }); + }; + + return <ul className={styles.list}>{getProjectItems()}</ul>; +}; + +export default ProjectsList; diff --git a/src/config/seo.ts b/src/config/seo.ts index 89e38c2..48487bb 100644 --- a/src/config/seo.ts +++ b/src/config/seo.ts @@ -25,4 +25,8 @@ export const seo = { title: t`Error 404: Page not found | Armand Philippot`, description: '', }, + projects: { + title: t`Projects | Armand Philippot`, + description: t`Discover Armand Philippot projects. Mostly related to web development and open source.`, + }, }; diff --git a/src/content b/src/content -Subproject c6f91781084aaf0b7d6f9b2a5d177e21e6d0d96 +Subproject 1dc7705299e9e4a05ecddd35dd1357d6426efe0 diff --git a/src/pages/projets.tsx b/src/pages/projets.tsx new file mode 100644 index 0000000..0c04024 --- /dev/null +++ b/src/pages/projets.tsx @@ -0,0 +1,108 @@ +import { getLayout } from '@components/Layouts/Layout'; +import PostHeader from '@components/PostHeader/PostHeader'; +import ProjectsList from '@components/ProjectsList/ProjectsList'; +import { seo } from '@config/seo'; +import { config } from '@config/website'; +import PageContent, { intro, meta } from '@content/pages/projects.mdx'; +import styles from '@styles/pages/Projects.module.scss'; +import { Project } from '@ts/types/app'; +import { loadTranslation } from '@utils/helpers/i18n'; +import { getSortedProjects } from '@utils/helpers/projects'; +import { GetStaticProps, GetStaticPropsContext } from 'next'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import { Article, Graph, WebPage } from 'schema-dts'; + +const Projects = ({ projects }: { projects: Project[] }) => { + const dates = { + publication: meta.publishedOn, + update: meta.updatedOn, + }; + const publicationDate = new Date(dates.publication); + const updateDate = new Date(dates.update); + const router = useRouter(); + const pageUrl = `${config.url}${router.asPath}`; + + const webpageSchema: WebPage = { + '@id': `${pageUrl}`, + '@type': 'WebPage', + breadcrumb: { '@id': `${config.url}/#breadcrumb` }, + name: seo.legalNotice.title, + description: seo.legalNotice.description, + inLanguage: config.locales.defaultLocale, + license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr', + reviewedBy: { '@id': `${config.url}/#branding` }, + url: `${pageUrl}`, + isPartOf: { + '@id': `${config.url}`, + }, + }; + + const articleSchema: Article = { + '@id': `${config.url}/#projects`, + '@type': 'Article', + name: meta.title, + description: intro, + author: { '@id': `${config.url}/#branding` }, + copyrightYear: publicationDate.getFullYear(), + creator: { '@id': `${config.url}/#branding` }, + dateCreated: publicationDate.toISOString(), + dateModified: updateDate.toISOString(), + datePublished: publicationDate.toISOString(), + editor: { '@id': `${config.url}/#branding` }, + inLanguage: config.locales.defaultLocale, + license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr', + mainEntityOfPage: { '@id': `${pageUrl}` }, + }; + + const schemaJsonLd: Graph = { + '@context': 'https://schema.org', + '@graph': [webpageSchema, articleSchema], + }; + + return ( + <> + <Head> + <title>{seo.projects.title}</title> + <meta name="description" content={seo.projects.description} /> + <meta property="og:url" content={`${pageUrl}`} /> + <meta property="og:type" content="article" /> + <meta property="og:title" content={meta.title} /> + <meta property="og:description" content={intro} /> + <script + type="application/ld+json" + dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} + ></script> + </Head> + <article id="projects" className={styles.article}> + <PostHeader title={meta.title} intro={<PageContent />} /> + <div className={styles.body}> + {projects.length > 0 && <ProjectsList projects={projects} />} + </div> + </article> + </> + ); +}; + +Projects.getLayout = getLayout; + +export const getStaticProps: GetStaticProps = async ( + context: GetStaticPropsContext +) => { + const translation = await loadTranslation( + context.locale!, + process.env.NODE_ENV === 'production' + ); + const breadcrumbTitle = meta.title; + const projects: Project[] = await getSortedProjects(); + + return { + props: { + breadcrumbTitle, + projects, + translation, + }, + }; +}; + +export default Projects; diff --git a/src/styles/pages/Projects.module.scss b/src/styles/pages/Projects.module.scss new file mode 100644 index 0000000..3fd74cb --- /dev/null +++ b/src/styles/pages/Projects.module.scss @@ -0,0 +1,13 @@ +.article { + composes: grid from "@styles/layout/_grid.scss"; + align-items: start; + + > header { + grid-column: 1 / -1; + } +} + +.body { + grid-column: 1 / -1; + margin-bottom: var(--spacing-xl); +} diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts index b5707a2..6f9bbce 100644 --- a/src/ts/types/app.ts +++ b/src/ts/types/app.ts @@ -89,11 +89,28 @@ export type Meta = { updatedOn: string; }; +export type ProjectMeta = Meta & { + license: string; + repos?: { + github?: string; + gitlab?: string; + }; + technologies?: string[]; +}; + export type PageInfo = { endCursor: string; hasNextPage: boolean; }; +export type Project = { + cover: string; + id: string; + intro: string; + meta: ProjectMeta; + slug: string; +}; + export type Slug = { slug: string; }; diff --git a/src/utils/helpers/projects.ts b/src/utils/helpers/projects.ts new file mode 100644 index 0000000..3736242 --- /dev/null +++ b/src/utils/helpers/projects.ts @@ -0,0 +1,72 @@ +import { Project, ProjectMeta } from '@ts/types/app'; +import { readdirSync } from 'fs'; +import path from 'path'; + +/** + * Retrieve the projects data from filenames. + * @param {string[]} filenames - An array of filenames. + * @returns {Promise<Project[]>} An array of projects. + */ +const getProjectsWithMeta = async (filenames: string[]): Promise<Project[]> => { + return Promise.all( + filenames.map(async (filename) => { + const id = filename.replace(/\.mdx$/, ''); + + try { + const { + image, + intro, + meta, + }: { image: string; intro: string; meta: ProjectMeta } = await import( + `../../content/projects/${filename}` + ); + + const projectMeta: ProjectMeta = meta + ? meta + : { + title: '', + publishedOn: '', + updatedOn: '', + license: '', + }; + const projectIntro = intro ? intro : ''; + const projectCover = image ? image : ''; + + return { + id, + cover: projectCover, + intro: projectIntro, + meta: projectMeta, + slug: id, + }; + } catch (err) { + console.error(err); + throw err; + } + }) + ); +}; + +/** + * Method to sort an array of projects by publication date. + * @param {Project} a - A single project. + * @param {Project} b - A single project. + * @returns The result used by Array.sort() method: 1 || -1 || 0. + */ +const sortProjectByPublicationDate = (a: Project, b: Project) => { + if (a.meta.publishedOn < b.meta.publishedOn) return 1; + if (a.meta.publishedOn > b.meta.publishedOn) return -1; + return 0; +}; + +/** + * Retrieve all projects in content folder sorted by publication date. + * @returns {Promise<Project[]>} An array of projects. + */ +export const getSortedProjects = async (): Promise<Project[]> => { + const projectsDirectory = path.join(process.cwd(), 'src/content/projects'); + const filenames = readdirSync(projectsDirectory); + const projects = await getProjectsWithMeta(filenames); + + return [...projects].sort(sortProjectByPublicationDate); +}; |
