aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2021-12-16 18:22:08 +0100
committerArmand Philippot <git@armandphilippot.com>2021-12-16 18:22:08 +0100
commit0ef4f77954ba54b52b96c70a8bffe96804bd222d (patch)
tree7fe68b05271625f72f750786e1a94b71942d2cd5
parenta4f865224e0e395c7e29bb3b7279a5101b554d2c (diff)
chore: display featuredImage and meta on posts list
-rw-r--r--next.config.js5
-rw-r--r--src/components/PostMeta/PostMeta.module.scss17
-rw-r--r--src/components/PostPreview/PostPreview.module.scss125
-rw-r--r--src/components/PostPreview/PostPreview.tsx61
-rw-r--r--src/components/PostsList/PostsList.module.scss6
-rw-r--r--src/components/PostsList/PostsList.tsx47
-rw-r--r--src/services/graphql/blog.ts3
-rw-r--r--src/services/graphql/post.ts6
8 files changed, 236 insertions, 34 deletions
diff --git a/next.config.js b/next.config.js
index 2faf64a..8492d0d 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,12 +1,17 @@
const path = require('path');
const { locales } = require('./lingui.config');
+const backendDomain = process.env.BACKEND_URL.split('//')[1];
+
/** @type {import('next').NextConfig} */
module.exports = {
i18n: {
locales,
defaultLocale: 'fr',
},
+ images: {
+ domains: [backendDomain, 'secure.gravatar.com'],
+ },
poweredByHeader: false,
reactStrictMode: true,
sassOptions: {
diff --git a/src/components/PostMeta/PostMeta.module.scss b/src/components/PostMeta/PostMeta.module.scss
new file mode 100644
index 0000000..f997ffa
--- /dev/null
+++ b/src/components/PostMeta/PostMeta.module.scss
@@ -0,0 +1,17 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ font-size: var(--font-size-sm);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ display: flex;
+ flex-flow: column nowrap;
+ margin: 0;
+ composes: meta from "@components/PostPreview/PostPreview.module.scss";
+ }
+ }
+}
diff --git a/src/components/PostPreview/PostPreview.module.scss b/src/components/PostPreview/PostPreview.module.scss
new file mode 100644
index 0000000..d188b18
--- /dev/null
+++ b/src/components/PostPreview/PostPreview.module.scss
@@ -0,0 +1,125 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-md);
+ border: fun.convert-px(1) solid var(--color-border);
+ border-radius: fun.convert-px(3);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow),
+ fun.convert-px(1) fun.convert-px(1) fun.convert-px(3) 0 var(--color-shadow);
+ transition: all 0.2s ease-in-out 0s, border 0s;
+
+ &:hover,
+ &:focus-within {
+ box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0
+ var(--color-shadow-light),
+ fun.convert-px(3) fun.convert-px(3) fun.convert-px(3) 0
+ var(--color-shadow-light),
+ fun.convert-px(3) fun.convert-px(3) fun.convert-px(5) fun.convert-px(1)
+ var(--color-shadow-lighter);
+ transform: scale(1.01);
+ }
+}
+
+.cover {
+ width: auto;
+ height: fun.convert-px(100);
+ margin: 0 auto var(--spacing-sm);
+ position: relative;
+ border: fun.convert-px(1) solid var(--color-border);
+}
+
+.read-more {
+ display: block;
+ width: max-content;
+ margin: var(--spacing-md) auto var(--spacing-lg);
+ padding: var(--spacing-2xs) var(--spacing-sm);
+ background: var(--color-bg);
+ border: fun.convert-px(3) solid var(--color-primary);
+ border-radius: fun.convert-px(3);
+ 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);
+ color: var(--color-primary);
+ font-weight: 600;
+ text-decoration: none;
+ transition: all 0.3s ease-in-out 0s;
+
+ .icon {
+ width: fun.convert-px(20);
+ margin-left: var(--spacing-2xs);
+ fill: var(--color-primary);
+ transition: all 0.25s ease-in-out 0s;
+ }
+
+ &:hover,
+ &:focus {
+ text-decoration: underline var(--color-primary) fun.convert-px(2);
+
+ .icon {
+ margin-left: var(--spacing-xs);
+ transform: scaleX(1.3);
+ }
+ }
+
+ &:active {
+ color: var(--color-primary-dark);
+ text-decoration: none;
+
+ .icon {
+ margin-left: 0;
+ transform: scaleX(0);
+ width: 0;
+ }
+ }
+}
+
+@include mix.media("screen") {
+ @include mix.dimensions("xs") {
+ .read-more {
+ font-size: var(--font-size-sm);
+ }
+ }
+
+ @include mix.dimensions("sm") {
+ .wrapper {
+ display: grid;
+ grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
+ grid-template-rows: repeat(3, max-content);
+ column-gap: var(--spacing-md);
+ }
+
+ .cover {
+ grid-column: 2;
+ grid-row: 1;
+ margin: 0 0 var(--spacing-sm);
+ }
+
+ .header {
+ grid-column: 1;
+ grid-row: 1;
+ }
+
+ .meta {
+ grid-column: 2;
+ grid-row: 2 / 4;
+ }
+
+ .body {
+ grid-column: 1;
+ grid-row: 2;
+ }
+
+ .footer {
+ grid-column: 1;
+ grid-row: 3;
+ }
+
+ .read-more {
+ margin: 0;
+ }
+ }
+}
diff --git a/src/components/PostPreview/PostPreview.tsx b/src/components/PostPreview/PostPreview.tsx
new file mode 100644
index 0000000..8f3e0da
--- /dev/null
+++ b/src/components/PostPreview/PostPreview.tsx
@@ -0,0 +1,61 @@
+import PostMeta from '@components/PostMeta/PostMeta';
+import { t } from '@lingui/macro';
+import { ArticlePreview } from '@ts/types/articles';
+import Link from 'next/link';
+import ArrowRightIcon from '@assets/images/icon-arrow-right.svg';
+import styles from './PostPreview.module.scss';
+import Image from 'next/image';
+
+const PostPreview = ({
+ post,
+ TitleTag,
+}: {
+ post: ArticlePreview;
+ TitleTag: keyof JSX.IntrinsicElements;
+}) => {
+ return (
+ <article className={styles.wrapper}>
+ {post.featuredImage && (
+ <div className={styles.cover}>
+ <Image
+ src={post.featuredImage.sourceUrl}
+ alt={post.featuredImage.altText}
+ layout="fill"
+ objectFit="contain"
+ />
+ </div>
+ )}
+ <header className={styles.header}>
+ <TitleTag>
+ <Link href={`/article/${post.slug}`}>
+ <a>{post.title}</a>
+ </Link>
+ </TitleTag>
+ </header>
+ <div
+ className={styles.body}
+ dangerouslySetInnerHTML={{ __html: post.content }}
+ ></div>
+ <footer className={styles.footer}>
+ <Link href={post.slug}>
+ <a className={styles['read-more']}>
+ {t`Read more`}
+ <span className="screen-reader-text">
+ {' '}
+ {t({ message: `about ${post.title}`, comment: 'Post title' })}
+ </span>
+ <ArrowRightIcon className={styles.icon} />
+ </a>
+ </Link>
+ </footer>
+ <PostMeta
+ commentCount={post.commentCount}
+ publicationDate={post.date.publication}
+ updateDate={post.date.update}
+ thematics={post.thematics}
+ />
+ </article>
+ );
+};
+
+export default PostPreview;
diff --git a/src/components/PostsList/PostsList.module.scss b/src/components/PostsList/PostsList.module.scss
index 9f78f68..fe63cbc 100644
--- a/src/components/PostsList/PostsList.module.scss
+++ b/src/components/PostsList/PostsList.module.scss
@@ -2,4 +2,10 @@
.wrapper {
@extend %reset-ordered-list;
+
+ margin: var(--spacing-md) auto;
+}
+
+li.item {
+ margin: var(--spacing-md) 0;
}
diff --git a/src/components/PostsList/PostsList.tsx b/src/components/PostsList/PostsList.tsx
index 55ca232..31cdbf6 100644
--- a/src/components/PostsList/PostsList.tsx
+++ b/src/components/PostsList/PostsList.tsx
@@ -1,23 +1,18 @@
-import Link from 'next/link';
import useSWRInfinite from 'swr/infinite';
import { t } from '@lingui/macro';
import { config } from '@config/website';
import { getPublishedPosts } from '@services/graphql/blog';
-import { ArticlePreview } from '@ts/types/articles';
-import { PageInfo } from '@ts/types/pagination';
+import { PostsList as PostsListData } from '@ts/types/blog';
import styles from './PostsList.module.scss';
+import PostPreview from '@components/PostPreview/PostPreview';
+import { Button } from '@components/Buttons';
type TitleLevel = 2 | 3 | 4 | 5 | 6;
-type DataType = {
- posts: ArticlePreview;
- pageInfo: PageInfo;
-};
-
const PostsList = ({ titleLevel }: { titleLevel: TitleLevel }) => {
const TitleTag = `h${titleLevel}` as keyof JSX.IntrinsicElements;
- const getKey = (pageIndex: number, previousData: DataType) => {
+ const getKey = (pageIndex: number, previousData: PostsListData) => {
if (previousData && !previousData.posts) return null;
const args =
@@ -41,27 +36,18 @@ const PostsList = ({ titleLevel }: { titleLevel: TitleLevel }) => {
isLoadingInitialData ||
(size > 0 && data !== undefined && typeof data[size - 1] === 'undefined');
- const getPostsList = () => {
- if (error) return <div>{t`Failed to load.`}</div>;
- if (!data) return <div>{t`Loading...`}</div>;
+ if (error) return <div>{t`Failed to load.`}</div>;
+ if (!data) return <div>{t`Loading...`}</div>;
+ const getPostsList = () => {
return data.map((page) => {
if (page.posts.length === 0) {
return t`No results found.`;
} else {
return page.posts.map((post) => {
return (
- <li key={post.id}>
- <article>
- <header>
- <TitleTag>
- <Link href={`/article/${post.slug}`}>
- <a>{post.title}</a>
- </Link>
- </TitleTag>
- </header>
- <div dangerouslySetInnerHTML={{ __html: post.content }}></div>
- </article>
+ <li key={post.id} className={styles.item}>
+ <PostPreview post={post} TitleTag={TitleTag} />
</li>
);
});
@@ -72,16 +58,15 @@ const PostsList = ({ titleLevel }: { titleLevel: TitleLevel }) => {
const hasNextPage = data && data[data.length - 1].pageInfo.hasNextPage;
return (
- <ol className={styles.wrapper}>
- {getPostsList()}
+ <>
+ <ol className={styles.wrapper}>{getPostsList()}</ol>
{hasNextPage && (
- <button
- disabled={isLoadingMore}
- type="button"
- onClick={() => setSize(size + 1)}
- >{t`Load more?`}</button>
+ <Button
+ isDisabled={isLoadingMore}
+ clickHandler={() => setSize(size + 1)}
+ >{t`Load more?`}</Button>
)}
- </ol>
+ </>
);
};
diff --git a/src/services/graphql/blog.ts b/src/services/graphql/blog.ts
index 6b92b5a..27b972b 100644
--- a/src/services/graphql/blog.ts
+++ b/src/services/graphql/blog.ts
@@ -99,12 +99,14 @@ export const getPublishedPosts: GetPostsListReturn = async ({
contentParts,
databaseId,
date,
+ featuredImage,
id,
modified,
slug,
title,
} = post.node;
const content = contentParts.beforeMore;
+ const cover = featuredImage ? featuredImage.node : null;
const dates = { publication: date, update: modified };
const subjects =
acfPosts.postsInSubject && acfPosts.postsInSubject?.length > 0
@@ -120,6 +122,7 @@ export const getPublishedPosts: GetPostsListReturn = async ({
content,
databaseId,
date: dates,
+ featuredImage: cover,
id,
slug,
subjects,
diff --git a/src/services/graphql/post.ts b/src/services/graphql/post.ts
index c7144fc..2ce918b 100644
--- a/src/services/graphql/post.ts
+++ b/src/services/graphql/post.ts
@@ -108,7 +108,7 @@ export const getPostBySlug: GetPostByReturn = async (slug: string) => {
const content = rawPost.postBy.contentParts.afterMore;
const featuredImage = rawPost.postBy.featuredImage
? rawPost.postBy.featuredImage.node
- : {};
+ : null;
const date = {
publication: rawPost.postBy.date,
update: rawPost.postBy.modified,
@@ -117,8 +117,8 @@ export const getPostBySlug: GetPostByReturn = async (slug: string) => {
const subjects = rawPost.postBy.acfPosts.postsInSubject
? rawPost.postBy.acfPosts.postsInSubject
: [];
- const thematics = rawPost.postBy.acfPosts.postsInThematics
- ? rawPost.postBy.acfPosts.postsInThematics
+ const thematics = rawPost.postBy.acfPosts.postsInThematic
+ ? rawPost.postBy.acfPosts.postsInThematic
: [];
const formattedPost: Article = {