summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-01-20 11:57:16 +0100
committerArmand Philippot <git@armandphilippot.com>2022-01-20 11:57:16 +0100
commitf5ba1046b13acd239c472e361f345902937662fb (patch)
treed2468fa51f3d2f9dda61eb9cdf6594ad8e618352 /src
parent08855874397399459b281f6f0506fa5e91cdfdc0 (diff)
chore: add a page for projects
Diffstat (limited to 'src')
-rw-r--r--src/components/ProjectPreview/ProjectPreview.module.scss87
-rw-r--r--src/components/ProjectPreview/ProjectPreview.tsx55
-rw-r--r--src/components/ProjectsList/ProjectsList.module.scss25
-rw-r--r--src/components/ProjectsList/ProjectsList.tsx21
-rw-r--r--src/config/seo.ts4
m---------src/content0
-rw-r--r--src/pages/projets.tsx108
-rw-r--r--src/styles/pages/Projects.module.scss13
-rw-r--r--src/ts/types/app.ts17
-rw-r--r--src/utils/helpers/projects.ts72
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);
+};