From 4b67cb51d7279201b2146a16187c6f6b85bc8e0e Mon Sep 17 00:00:00 2001 From: David Lange Date: Thu, 3 Oct 2024 11:51:08 +0100 Subject: [PATCH 1/2] Add lint action (#42) - Fix some remaining lint issues - Add an action to run on pull requests and pushes to main. For now it runs `npm run lint` --- .github/workflows/node.js.yml | 27 +++++++++++++++++++++++++++ src/app/_graphql/contentFromAuthor.ts | 2 +- src/app/_utilities/fetcher.ts | 2 +- src/app/_utilities/filterArticles.ts | 15 ++++++++------- src/payload/globals/Socials.ts | 2 -- src/server.default.ts | 7 ------- 6 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/node.js.yml diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..09cc5a6 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,27 @@ +name: Lint + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + run-linters: + name: Run linters + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install Node.js dependencies + run: npm install + + - name: Run linters + run: npm run lint diff --git a/src/app/_graphql/contentFromAuthor.ts b/src/app/_graphql/contentFromAuthor.ts index 8defc52..4cd5836 100644 --- a/src/app/_graphql/contentFromAuthor.ts +++ b/src/app/_graphql/contentFromAuthor.ts @@ -1,4 +1,4 @@ -import { MEDIA_FIELDS } from '@/app/_graphql/media' +import { MEDIA_FIELDS } from './media' export const CONTENT_FROM_AUTHOR = ` query GetContent($authorID: JSON!) { diff --git a/src/app/_utilities/fetcher.ts b/src/app/_utilities/fetcher.ts index 5a753e9..97d189a 100644 --- a/src/app/_utilities/fetcher.ts +++ b/src/app/_utilities/fetcher.ts @@ -4,7 +4,7 @@ import type { ContentTypeArrays } from '@/app/_interfaces/ContentTypeArrays' export async function fetcher(args: { query: string - variables?: Record + variables?: Record }): Promise { const { query, variables } = args diff --git a/src/app/_utilities/filterArticles.ts b/src/app/_utilities/filterArticles.ts index 0663cba..02939de 100644 --- a/src/app/_utilities/filterArticles.ts +++ b/src/app/_utilities/filterArticles.ts @@ -1,5 +1,3 @@ -import type { ContentTypeArrays } from '../_interfaces/ContentTypeArrays' - import type { Blogpost, CaseStudy, @@ -24,11 +22,14 @@ export function filterArticles({ articles, filter = 'All' }: ArticleFilterProps) if (filter === 'All') { const keys = Object.keys(articles) as Array - return keys.flatMap(articleType => - articles[articleType].map(article => ({ - contentType: articleType, - content: article, - })),) + return keys.flatMap( + articleType => + articles[articleType].map(article => ({ + contentType: articleType, + content: article, + })), + undefined, + ) } return articles[filter]?.map(article => ({ diff --git a/src/payload/globals/Socials.ts b/src/payload/globals/Socials.ts index b998c22..086dfb6 100644 --- a/src/payload/globals/Socials.ts +++ b/src/payload/globals/Socials.ts @@ -1,7 +1,5 @@ import type { GlobalConfig } from 'payload/types' -import link from '../fields/link' - export const Socials: GlobalConfig = { slug: 'socials', access: { diff --git a/src/server.default.ts b/src/server.default.ts index 49f05c6..c5f8d4d 100644 --- a/src/server.default.ts +++ b/src/server.default.ts @@ -3,8 +3,6 @@ import express from 'express' import path from 'path' import payload from 'payload' -import { seed } from './payload/seed' - // This file is used to replace `server.ts` when ejecting i.e. `yarn eject` // See `../eject.ts` for exact details on how this file is used // See `./README.md#eject` for more information @@ -30,11 +28,6 @@ const start = async (): Promise => { }, }) - if (process.env.PAYLOAD_SEED === 'true') { - await seed(payload) - process.exit() - } - app.listen(PORT, async () => { payload.logger.info(`App URL: ${process.env.PAYLOAD_PUBLIC_SERVER_URL}`) }) From 4046bfd864b51f329b78f91ee73d5a9128fcfe59 Mon Sep 17 00:00:00 2001 From: Rui Sousa <149320958+rccsousa@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:32:09 +0100 Subject: [PATCH 2/2] style blogpost page (#44) Why: - blogposts page was missing the correct styling; How: - added new components to encapsulate different parts of blog posts and new methods to manipulate `dangerouslySetInnerHTML` and ensure all headings are enforced to h5 to maintain format consistency as well as labeling chapters; --- package.json | 4 +- src/app/(pages)/blogposts/[slug]/page.tsx | 66 +++------ .../blogposts/[slug]/styles.module.css | 58 ++++++++ .../(pages)/podcast-episodes/[slug]/page.tsx | 4 +- src/app/_blocks/Blogpost/index.tsx | 17 --- src/app/_blocks/BlogpostChapters/index.tsx | 55 ++++++++ .../BlogpostChapters/styles.module.css | 30 ++++ src/app/_blocks/BlogpostContent/index.tsx | 19 +++ .../_blocks/BlogpostContent/styles.module.css | 25 ++++ .../index.tsx | 13 +- .../_blocks/RelatedContent/styles.module.css | 15 ++ src/app/_blocks/Subscribe/styles.module.css | 6 +- src/app/_components/BackButton/index.tsx | 6 +- .../BlogPostArchiveButton/index.tsx | 16 ++- src/app/_components/Categories/index.tsx | 16 +++ .../_components/Categories/styles.module.css | 11 ++ src/app/_components/FeaturedImage/index.tsx | 21 ++- .../FeaturedImage/styles.module.css | 4 + src/app/_components/MiniContentCard/index.tsx | 26 ++++ .../MiniContentCard/styles.module.css | 15 ++ src/app/_components/PostSummary/index.tsx | 22 +-- .../_components/PostSummary/styles.module.css | 51 +++++++ .../_components/RecommendedPosts/index.tsx | 15 ++ .../RecommendedPosts/styles.module.css | 19 +++ src/app/_components/Share/index.tsx | 70 ++++++++++ src/app/_components/Share/styles.module.css | 63 +++++++++ src/app/_css/globals.css | 5 + src/app/_graphql/blogposts.ts | 13 ++ src/app/_icons/BackArrow.tsx | 10 +- src/app/_icons/LinkedInIcon.tsx | 11 -- src/app/_icons/icons.tsx | 130 +++++++++--------- src/app/_icons/socialIcons.tsx | 35 +++++ src/app/_utilities/sanitizeAndAddChapters.ts | 22 +++ yarn.lock | 19 ++- 34 files changed, 734 insertions(+), 178 deletions(-) create mode 100644 src/app/(pages)/blogposts/[slug]/styles.module.css delete mode 100644 src/app/_blocks/Blogpost/index.tsx create mode 100644 src/app/_blocks/BlogpostChapters/index.tsx create mode 100644 src/app/_blocks/BlogpostChapters/styles.module.css create mode 100644 src/app/_blocks/BlogpostContent/index.tsx create mode 100644 src/app/_blocks/BlogpostContent/styles.module.css rename src/app/_blocks/{RecommendedContent => RelatedContent}/index.tsx (53%) create mode 100644 src/app/_blocks/RelatedContent/styles.module.css create mode 100644 src/app/_components/Categories/index.tsx create mode 100644 src/app/_components/Categories/styles.module.css create mode 100644 src/app/_components/FeaturedImage/styles.module.css create mode 100644 src/app/_components/MiniContentCard/index.tsx create mode 100644 src/app/_components/MiniContentCard/styles.module.css create mode 100644 src/app/_components/PostSummary/styles.module.css create mode 100644 src/app/_components/RecommendedPosts/index.tsx create mode 100644 src/app/_components/RecommendedPosts/styles.module.css create mode 100644 src/app/_components/Share/index.tsx create mode 100644 src/app/_components/Share/styles.module.css delete mode 100644 src/app/_icons/LinkedInIcon.tsx create mode 100644 src/app/_icons/socialIcons.tsx create mode 100644 src/app/_utilities/sanitizeAndAddChapters.ts diff --git a/package.json b/package.json index 7cbf071..a02d3cc 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@payloadcms/plugin-seo": "^1.0.10", "@payloadcms/richtext-lexical": "^0.11.3", "@payloadcms/richtext-slate": "^1.0.0", + "clsx": "^2.1.1", "cross-env": "^7.0.3", "dotenv": "^8.2.0", "escape-html": "^1.0.3", @@ -41,7 +42,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "7.45.4", - "react-router-dom": "5.3.4" + "react-router-dom": "5.3.4", + "react-share": "^5.1.0" }, "devDependencies": { "@next/eslint-plugin-next": "^13.1.6", diff --git a/src/app/(pages)/blogposts/[slug]/page.tsx b/src/app/(pages)/blogposts/[slug]/page.tsx index 70361e1..6dbf69b 100644 --- a/src/app/(pages)/blogposts/[slug]/page.tsx +++ b/src/app/(pages)/blogposts/[slug]/page.tsx @@ -2,12 +2,17 @@ import { notFound } from 'next/navigation' import { Blogpost } from '../../../../payload/payload-types' import { fetchDoc } from '../../../_api/fetchDoc' -import BlogpostContent from '../../../_blocks/Blogpost' -import { RecommendedContent } from '../../../_blocks/RecommendedContent' +import BlogpostChapters from '../../../_blocks/BlogpostChapters' +import BlogpostContent from '../../../_blocks/BlogpostContent' +import { RelatedContent } from '../../../_blocks/RelatedContent' import { Subscribe } from '../../../_blocks/Subscribe' import BackButton from '../../../_components/BackButton' -import ContentCard from '../../../_components/ContentCard' +import Categories from '../../../_components/Categories' import PostSummary from '../../../_components/PostSummary' +import RecommendedPosts from '../../../_components/RecommendedPosts' +import Share from '../../../_components/Share' +import { getChapters } from '../../../_utilities/sanitizeAndAddChapters' +import styles from './styles.module.css' export default async function BlogpostPage({ params: { slug } }) { const blogpost: Blogpost | null = await fetchDoc({ @@ -16,63 +21,36 @@ export default async function BlogpostPage({ params: { slug } }) { }) // TODO: implement a fetcher for related content to populate related cards - if (!blogpost) { notFound() } - const { relatedPosts } = blogpost + // instead of destructuring post multiple times, destructure just once here? + const { content_html, categories, relatedPosts } = blogpost + const chapters = getChapters(content_html) + return (
-
+
{/* Head Block*/} - +
-
+
{/* Left column: Navigation */} -
-

Table of contents block

-
+ {/* Middle column: Content block */} -
- -
+ {/* Right column: Social sharing & recommended */} -
-
-

Share block goes here

-

SocialMedia block with links

-
-
-

Category block

-
-
-

Recommended Block

- - -
+
+ + +
- +
) diff --git a/src/app/(pages)/blogposts/[slug]/styles.module.css b/src/app/(pages)/blogposts/[slug]/styles.module.css new file mode 100644 index 0000000..7717e37 --- /dev/null +++ b/src/app/(pages)/blogposts/[slug]/styles.module.css @@ -0,0 +1,58 @@ + +.headContainer { + background-color: var(--sub-purple-400); + color: var(--soft-white-100); + border-bottom-right-radius: 16px; + border-bottom-left-radius: 16px; + padding-left: 15px; + padding-right: 15px; +} + +.backButton { + display: block; + font-family: Colfax, sans-serif; + padding-top: 40px; + padding-left: 15px; + padding-bottom: 40px; +} + + +.contentContainer { + display: grid; + grid-template-rows: 2fr; + font-family: var(--colfax); + padding-bottom: 40px; +} + +.sharingAndCategories { + grid-row-start: 1; +} + +@media (min-width: 1080px) { + + .headContainer { + width: calc(100% - 40px); + margin: 0 auto; + } + + .backButton { + padding-left: 95px; + color: var(--soft-white-100); + } + + .contentContainer { + width: calc(100% - 40px); + margin: 20px auto; + display: grid; + grid-template-columns: 0.3fr 0.6fr 0.3fr; + } + + .sharingAndCategories { + justify-content: left; + padding-left: 40px; + grid-column: 3 / 3; + } + +} + + diff --git a/src/app/(pages)/podcast-episodes/[slug]/page.tsx b/src/app/(pages)/podcast-episodes/[slug]/page.tsx index 5768a52..eb8cdf3 100644 --- a/src/app/(pages)/podcast-episodes/[slug]/page.tsx +++ b/src/app/(pages)/podcast-episodes/[slug]/page.tsx @@ -3,7 +3,7 @@ import { notFound } from 'next/navigation' import { fetchDoc } from '../../../_api/fetchDoc' import EpisodeContent from '../../../_blocks/EpisodeContent' import EpisodeHead from '../../../_blocks/EpisodeHead' -import { RecommendedContent } from '../../../_blocks/RecommendedContent' +import { RelatedContent } from '../../../_blocks/RelatedContent' import { Subscribe } from '../../../_blocks/Subscribe' export default async function PodcastEpisodesPage({ params: { slug } }) { @@ -31,7 +31,7 @@ export default async function PodcastEpisodesPage({ params: { slug } }) {
- +
diff --git a/src/app/_blocks/Blogpost/index.tsx b/src/app/_blocks/Blogpost/index.tsx deleted file mode 100644 index 2c0059f..0000000 --- a/src/app/_blocks/Blogpost/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import DOMPurify from 'isomorphic-dompurify' - -import { Blogpost } from '../../../payload/payload-types' -import EpisodeFeaturedImage from '../../_components/EpisodeFeaturedImage' - -export default function BlogpostContent({ post }: { post: Blogpost }) { - const { summary, content_html, featuredImage } = post - const sanitizedContent = DOMPurify.sanitize(content_html) - - return ( -
- -
{summary}
-
-
- ) -} diff --git a/src/app/_blocks/BlogpostChapters/index.tsx b/src/app/_blocks/BlogpostChapters/index.tsx new file mode 100644 index 0000000..152c2f6 --- /dev/null +++ b/src/app/_blocks/BlogpostChapters/index.tsx @@ -0,0 +1,55 @@ +'use client' + +import { useEffect, useState } from 'react' + +import styles from './styles.module.css' + +export default function BlogpostChapters({ chapters }) { + const [visibleChapter, setVisibleChapter] = useState(null) + + useEffect(() => { + const observer = new IntersectionObserver( + entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + setVisibleChapter(entry.target.id) // Update state with the visible chapter's ID + } + }) + }, + { threshold: 0.1 }, // Trigger when 10% of the element is visible + ) + + const chapters = document.querySelectorAll('[id^="chapter"]') + chapters.forEach(chapter => observer.observe(chapter)) + + // unmount + return () => { + chapters.forEach(chapter => observer.unobserve(chapter)) + } + }, []) + + return ( +
+
+

CHAPTER

+ +
+
+ ) +} diff --git a/src/app/_blocks/BlogpostChapters/styles.module.css b/src/app/_blocks/BlogpostChapters/styles.module.css new file mode 100644 index 0000000..7232743 --- /dev/null +++ b/src/app/_blocks/BlogpostChapters/styles.module.css @@ -0,0 +1,30 @@ +.container { + display: none; +} + +@media(min-width: 1024px) { + .container { + display: flex; + flex-direction: column; + grid-template-columns: repeat(2, 1fr); + width: 100%; + gap: 20px; + padding: 20px 50px; + } + + .container ul { + margin-top: 20px; + list-style: none; + padding: 0; + } + + .container li { + border-left: 4px solid; + padding: 10px 20px; + } + + .navbar { + position: sticky; + top: 20px; + } +} diff --git a/src/app/_blocks/BlogpostContent/index.tsx b/src/app/_blocks/BlogpostContent/index.tsx new file mode 100644 index 0000000..740b2c8 --- /dev/null +++ b/src/app/_blocks/BlogpostContent/index.tsx @@ -0,0 +1,19 @@ +'use client' + +import { Blogpost } from '../../../payload/payload-types' +import FeaturedImage from '../../_components/FeaturedImage' +import { sanitizeAndAddChapters } from '../../_utilities/sanitizeAndAddChapters' +import styles from './styles.module.css' + +export default function BlogpostContent({ post }: { post: Blogpost }) { + const { summary, content_html, featuredImage } = post + const sanitizedContent = sanitizeAndAddChapters(content_html) + + return ( +
+ +
{summary}
+
+
+ ) +} diff --git a/src/app/_blocks/BlogpostContent/styles.module.css b/src/app/_blocks/BlogpostContent/styles.module.css new file mode 100644 index 0000000..04e129f --- /dev/null +++ b/src/app/_blocks/BlogpostContent/styles.module.css @@ -0,0 +1,25 @@ +.container { + width: 100%; + padding: 20px; +} + +.featuredImage { + margin: auto; + border-radius: 45px; +} + +.summary { + font-size: var(--size-18); + margin: 20px auto; +} + +.content { + display: flex; + flex-direction: column; + gap: 20px; + text-align: justify; + word-wrap: break-word; + white-space: normal; + overflow-wrap: break-word; +} + diff --git a/src/app/_blocks/RecommendedContent/index.tsx b/src/app/_blocks/RelatedContent/index.tsx similarity index 53% rename from src/app/_blocks/RecommendedContent/index.tsx rename to src/app/_blocks/RelatedContent/index.tsx index 73d991d..d90bf92 100644 --- a/src/app/_blocks/RecommendedContent/index.tsx +++ b/src/app/_blocks/RelatedContent/index.tsx @@ -1,9 +1,12 @@ import Link from 'next/link' -export function RecommendedContent({ relatedContent }) { +import styles from './styles.module.css' + +import ContentCard from '@/app/_components/ContentCard' +export function RelatedContent({ relatedContent }) { return ( -
-

You may also like

+
+
You may also like
{relatedContent.map((contentPiece, i) => ( -
- Content card placeholder +
+
{contentPiece.title}
diff --git a/src/app/_blocks/RelatedContent/styles.module.css b/src/app/_blocks/RelatedContent/styles.module.css new file mode 100644 index 0000000..e946d65 --- /dev/null +++ b/src/app/_blocks/RelatedContent/styles.module.css @@ -0,0 +1,15 @@ +.container { + display: flex; + flex-direction: column; + background-color: var(--soft-white-100); + font-family: var(--colfax); + padding: 40px 20px; + border-radius: 45px; +} + +.contentCard { + border: 1px solid var(--dark-rock-800); + border-radius: 25px; + padding: 20px; + height: min-content; +} diff --git a/src/app/_blocks/Subscribe/styles.module.css b/src/app/_blocks/Subscribe/styles.module.css index bffd0a0..2821e65 100644 --- a/src/app/_blocks/Subscribe/styles.module.css +++ b/src/app/_blocks/Subscribe/styles.module.css @@ -6,11 +6,9 @@ background-color: var(--sub-purple-50); color: var(--sub-purple-400); border-radius: 25px; - margin: 0 auto; - margin-top: 40px; - margin-bottom: 40px; + margin: 40px auto; padding: 20px; - width: 328px; + width: var(--content-width); font-family: var(--colfax); } diff --git a/src/app/_components/BackButton/index.tsx b/src/app/_components/BackButton/index.tsx index 3116ccf..d0fa8cf 100644 --- a/src/app/_components/BackButton/index.tsx +++ b/src/app/_components/BackButton/index.tsx @@ -4,12 +4,12 @@ import Link from 'next/link' import BackArrow from '../../_icons/BackArrow' import styles from './styles.module.css' -export default function BackButton({ className }) { +export default function BackButton({ className, color }) { return ( <> - - Back + + Back ) diff --git a/src/app/_components/BlogPostArchiveButton/index.tsx b/src/app/_components/BlogPostArchiveButton/index.tsx index c3846db..5ece1d3 100644 --- a/src/app/_components/BlogPostArchiveButton/index.tsx +++ b/src/app/_components/BlogPostArchiveButton/index.tsx @@ -1,14 +1,20 @@ -export default function ArchiveButton({ collection }: { collection: string }) { +export default function ArchiveButton({ + collection, + color, +}: { + collection: string + color: string +}) { function formatArchiveButton(text: string): string { // TODO: Extend to format talks-and-roundtables to Talks & Roundtables // TODO: Extend to fromat case-studies to Case Studies - return text.charAt(0).toLowerCase() + text.slice(1) + return text.charAt(0).toUpperCase() + text.slice(1) } return ( - + + {formatArchiveButton(collection)} + ) } diff --git a/src/app/_components/Categories/index.tsx b/src/app/_components/Categories/index.tsx new file mode 100644 index 0000000..cc7bd6d --- /dev/null +++ b/src/app/_components/Categories/index.tsx @@ -0,0 +1,16 @@ +import styles from './styles.module.css' + +import CategoryPill from '@/app/_components/CategoryPill' + +export default function Categories({ categories }) { + return ( +
+

CATEGORY

+
+ {categories.map((category, i) => ( + + ))} +
+
+ ) +} diff --git a/src/app/_components/Categories/styles.module.css b/src/app/_components/Categories/styles.module.css new file mode 100644 index 0000000..b693b85 --- /dev/null +++ b/src/app/_components/Categories/styles.module.css @@ -0,0 +1,11 @@ +.categoriesContainer { + display: flex; + flex-direction: column; + gap: 20px; + padding: 20px; +} + +.categories { + display: flex; + gap: 20px; +} diff --git a/src/app/_components/FeaturedImage/index.tsx b/src/app/_components/FeaturedImage/index.tsx index f3770bf..c53fc4d 100644 --- a/src/app/_components/FeaturedImage/index.tsx +++ b/src/app/_components/FeaturedImage/index.tsx @@ -1,7 +1,20 @@ -import { className } from 'postcss-selector-parser' - import { getImage } from '../../_utilities/getImage' +import styles from './styles.module.css' + +import { Media } from '@/payload/payload-types' -export default function FeaturedImage({ src, className }) { - return +export default function FeaturedImage({ + src, + className, +}: { + className?: string + src: string | Media +}) { + return ( + {src.alt} + ) } diff --git a/src/app/_components/FeaturedImage/styles.module.css b/src/app/_components/FeaturedImage/styles.module.css new file mode 100644 index 0000000..a417354 --- /dev/null +++ b/src/app/_components/FeaturedImage/styles.module.css @@ -0,0 +1,4 @@ +.featuredImage { + margin: auto; + border-radius: 45px; +} diff --git a/src/app/_components/MiniContentCard/index.tsx b/src/app/_components/MiniContentCard/index.tsx new file mode 100644 index 0000000..2c3491c --- /dev/null +++ b/src/app/_components/MiniContentCard/index.tsx @@ -0,0 +1,26 @@ +import styles from './styles.module.css' + +import ArchiveButton from '@/app/_components/BlogPostArchiveButton' +import CategoryPill from '@/app/_components/CategoryPill' +import { formatDateTime } from '@/app/_utilities/formatDateTime' + +export default function MiniContentCard({ post }) { + const { title, categories, publishedAt } = post + return ( +
+ +
{title}
+
    + {categories.map(category => ( +
  • + +
  • + ))} +
+
+ {formatDateTime(publishedAt)} + 20 min read +
+
+ ) +} diff --git a/src/app/_components/MiniContentCard/styles.module.css b/src/app/_components/MiniContentCard/styles.module.css new file mode 100644 index 0000000..b6f92c7 --- /dev/null +++ b/src/app/_components/MiniContentCard/styles.module.css @@ -0,0 +1,15 @@ + .miniCard { + display: flex; + flex-direction: column; + gap:20px; + padding: 30px; + border: 1px solid black; + border-radius: 45px; + font-size: var(--size-14); + } + + .readTime { + display: flex; + align-items: center; + gap: 20px; + } diff --git a/src/app/_components/PostSummary/index.tsx b/src/app/_components/PostSummary/index.tsx index a7ab4e5..2d769cd 100644 --- a/src/app/_components/PostSummary/index.tsx +++ b/src/app/_components/PostSummary/index.tsx @@ -3,25 +3,27 @@ import { estimateReadTime } from '../../_utilities/estimateReadTime' import { formatDateTime } from '../../_utilities/formatDateTime' import AuthorPill from '../AuthorPill' import ArchiveButton from '../BlogPostArchiveButton' - +import styles from './styles.module.css' export default function PostSummary({ post }: { post: Blogpost }) { const { title, publishedAt, content, authors } = post return ( -
- {/* Left column */} -
- +
+ {/* Post info */} +
+
{title}
-
- {formatDateTime(publishedAt)} | {estimateReadTime('Placeholder')} +
+ {formatDateTime(publishedAt)} + {estimateReadTime('Placeholder')}
- {/* Right column */} -
-

WRITTEN BY

+ {/* Author info */} +
+

WRITTEN BY

+ Social Links
) diff --git a/src/app/_components/PostSummary/styles.module.css b/src/app/_components/PostSummary/styles.module.css new file mode 100644 index 0000000..17cc77f --- /dev/null +++ b/src/app/_components/PostSummary/styles.module.css @@ -0,0 +1,51 @@ +.gridContainer { + padding-bottom: 40px; + display: grid; + color: var(--dark-rock-800); + grid-template-rows: 1fr 1fr; + font-family: var(--colfax); + color: var(--soft-white-100); + row-gap: 20px; +} + +.leftColumn { + display: flex; + flex-direction: column; + gap: 16px; + padding-left: 16px; + vertical-align: middle; +} + +.postMeta { + display: flex; + gap: 20px; +} + +.featuredImage { + width: 70px; + height: 70px; + border-radius: 50%; + vertical-align: middle; +} + +.authorInfo { + display: flex; + flex-direction: column; + padding-left: 16px; + gap: 20px; +} + +.authorBio { + padding-left: 15px; + padding-right: 15px; +} + +@media(min-width: 1080px) { + .gridContainer { + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + padding-left: 80px; + padding-right: 80px; + } +} + diff --git a/src/app/_components/RecommendedPosts/index.tsx b/src/app/_components/RecommendedPosts/index.tsx new file mode 100644 index 0000000..dc61e72 --- /dev/null +++ b/src/app/_components/RecommendedPosts/index.tsx @@ -0,0 +1,15 @@ +import styles from './styles.module.css' + +import MiniContentCard from '@/app/_components/MiniContentCard' + +export default function RecommendedPosts({ posts }) { + return ( +
+

Recommended

+ + {posts.map((post, i) => ( + + ))} +
+ ) +} diff --git a/src/app/_components/RecommendedPosts/styles.module.css b/src/app/_components/RecommendedPosts/styles.module.css new file mode 100644 index 0000000..5514588 --- /dev/null +++ b/src/app/_components/RecommendedPosts/styles.module.css @@ -0,0 +1,19 @@ + +.container { + display: none; +} + +@media(min-width: 1024px) { +.container { + margin-top: 200px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.title { + font-weight: bold; + padding-left: 20px; +} + +} diff --git a/src/app/_components/Share/index.tsx b/src/app/_components/Share/index.tsx new file mode 100644 index 0000000..9bd219c --- /dev/null +++ b/src/app/_components/Share/index.tsx @@ -0,0 +1,70 @@ +'use client' + +import { useEffect, useState } from 'react' +import { FacebookShareButton, LinkedinShareButton, TwitterShareButton } from 'react-share' + +import styles from './styles.module.css' + +import { LinkIcon, ShareIcon } from '@/app/_icons/icons' +import { FacebookIcon, LinkedInIcon, TwitterIcon } from '@/app/_icons/socialIcons' + +export default function Share() { + const copyURLToClipboard = () => { + navigator.clipboard.writeText(document.URL) + } + + const shareTo = async () => { + await navigator.share({ + title: 'cenas', + text: 'mais cenas', + url: window.location.href, + }) + } + + const [url, setUrl] = useState('') + useEffect(() => { + setUrl(window.location.href) + + return () => { + setUrl('') + } + }, []) + + return ( +
+
+ {/* ToDo: test navigator.share on local SSL */} + + +
+
+

SHARE ARTICLE

+
+ + + + + + + + + +
+
copyURLToClipboard()}> + + Copy +
+
+
+ ) +} diff --git a/src/app/_components/Share/styles.module.css b/src/app/_components/Share/styles.module.css new file mode 100644 index 0000000..3f4ef66 --- /dev/null +++ b/src/app/_components/Share/styles.module.css @@ -0,0 +1,63 @@ +.container { + padding: 20px; + margin: 20px auto; + color: var(--dark-rock-800); +} + +.button { + background-color: transparent; + border: none; + +} + +.button div { + display: flex; + align-items: center; + vertical-align: middle; + border: 1px solid var(--dark-rock-800); + border-radius: 100px; + padding: 8px 15px; + gap: 5px; + transition: opacity 0.5s ease; +} + +.button div:hover { + opacity: 0.7; +} + +.desktopShare { + grid-row: 1; + display: none; +} + +@media (min-width: 1024px) { + + .container { + padding: 0; + } + + .mobileShare { + display: none; + } + + .desktopShare { + display: flex; + flex-direction: column; + gap: 20px; + } + + .desktopShareBar { + display: flex; + gap: 20px; + } + + .desktopCopyButton { + margin-top: 20px; + display: flex; + align-items: center; + gap: 5px; + cursor: pointer; + + + } +} diff --git a/src/app/_css/globals.css b/src/app/_css/globals.css index 12c0981..d4c5c2d 100644 --- a/src/app/_css/globals.css +++ b/src/app/_css/globals.css @@ -67,6 +67,10 @@ margin: 0; } +.outline { + letter-spacing: 0.1rem; +} + body { max-width: 1728px; margin: 0 auto; @@ -75,6 +79,7 @@ body { line-height: 1.5; -webkit-font-smoothing: antialiased; font-family: var(--colfax); + color: var(--dark-rock-800); } img, picture, video, canvas, svg { diff --git a/src/app/_graphql/blogposts.ts b/src/app/_graphql/blogposts.ts index 0e0bb00..fb46f97 100644 --- a/src/app/_graphql/blogposts.ts +++ b/src/app/_graphql/blogposts.ts @@ -26,6 +26,19 @@ export const BLOGPOST = ` id slug title + summary + authors { + name + featuredImage { + filename + } + } + categories { + title + } + featuredImage { + filename + } } publishedAt featuredImage { diff --git a/src/app/_icons/BackArrow.tsx b/src/app/_icons/BackArrow.tsx index 5c26fd0..e00a870 100644 --- a/src/app/_icons/BackArrow.tsx +++ b/src/app/_icons/BackArrow.tsx @@ -1,6 +1,6 @@ import { className } from 'postcss-selector-parser' -export default function BackArrow({ className }) { +export default function BackArrow({ className, color = 'var(--dark-rock-800)' }) { return ( - + - + ) } diff --git a/src/app/_icons/LinkedInIcon.tsx b/src/app/_icons/LinkedInIcon.tsx deleted file mode 100644 index 13c90b5..0000000 --- a/src/app/_icons/LinkedInIcon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export default function LinkedInIcon() { - return ( - - - - - ) -} diff --git a/src/app/_icons/icons.tsx b/src/app/_icons/icons.tsx index 68d3d1f..f8b9bbe 100644 --- a/src/app/_icons/icons.tsx +++ b/src/app/_icons/icons.tsx @@ -12,8 +12,8 @@ export function MagnifyingGlass({ x, y, width = '24', height = '24', stroke = 'b ) @@ -33,30 +33,30 @@ export function AllIcon({ x, y, width = '24', height = '24', stroke = 'black' }) ) @@ -76,9 +76,9 @@ export function BlogpostIcon({ x, y, width = '24', height = '24', stroke = 'blac ) @@ -98,9 +98,9 @@ export function PodcastIcon({ x, y, width = '24', height = '24', stroke = 'black ) @@ -120,9 +120,9 @@ export function CaseStudiesIcon({ x, y, width = '24', height = '24', stroke = 'b ) @@ -166,23 +166,23 @@ export function SpectaclesIcon({ x, y }) { ) } -export function ShareIcon({ x, y }) { +export function ShareIcon({ x, y, width = '24', height = '24', color = 'var(--dark-rock-800)' }) { return ( ) @@ -199,7 +199,7 @@ export function XIcon({ x, y }) { fill="none" xmlns="http://www.w3.org/2000/svg" > - + ) } @@ -218,9 +218,9 @@ export function ChevronUpIcon({ x, y }) { ) @@ -240,9 +240,9 @@ export function ChevronDownIcon({ x, y }) { ) @@ -262,9 +262,9 @@ export function RSSIcon({ x, y }) { ) @@ -284,9 +284,9 @@ export function MuteIcon({ x, y }) { ) @@ -306,9 +306,9 @@ export function LowerVolume({ x, y }) { ) @@ -328,9 +328,9 @@ export function RaiseVolumeIcon({ x, y }) { ) @@ -350,7 +350,7 @@ export function PlayIcon({ x, y }) { ) @@ -370,9 +370,9 @@ export function MoveFifteenIcon({ x, y }) { ) } -export function LinkIcon({ x, y }) { +export function LinkIcon({ x, y, width = '24', height = '24', color = 'var(--dark-rock-800)' }) { return ( ) diff --git a/src/app/_icons/socialIcons.tsx b/src/app/_icons/socialIcons.tsx new file mode 100644 index 0000000..5a3580f --- /dev/null +++ b/src/app/_icons/socialIcons.tsx @@ -0,0 +1,35 @@ +export function LinkedInIcon() { + return ( + + + + + ) +} + +export function TwitterIcon() { + return ( + + + + + ) +} + +export function FacebookIcon() { + return ( + + + + + ) +} diff --git a/src/app/_utilities/sanitizeAndAddChapters.ts b/src/app/_utilities/sanitizeAndAddChapters.ts new file mode 100644 index 0000000..14be352 --- /dev/null +++ b/src/app/_utilities/sanitizeAndAddChapters.ts @@ -0,0 +1,22 @@ +import DOMPurify from 'isomorphic-dompurify' + +// TODO: add the actual chapter names instead chapter1, 2, 3... +export function sanitizeAndAddChapters(content_html: string) { + let sectionCounter = 1 + + return DOMPurify.sanitize(content_html) + .replace(//g, () => { + return `
` + }) + .replace(/<\/h[1-6]>/g, '
') + .replace(/%nbsp;/g, ' ') + .replace(/

\s*<\/p>/g, '') +} + +export function getChapters(content_html: string) { + const sanitizedContent = sanitizeAndAddChapters(content_html) + return [...sanitizedContent.matchAll(/(.*?)<\/h[1-6]>/g)].map(match => ({ + id: match[1], + title: match[2], + })) +} diff --git a/yarn.lock b/yarn.lock index 2b0ff5c..258c7f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3452,7 +3452,7 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -clsx@^2.1.0: +clsx@^2.1.0, clsx@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== @@ -3826,7 +3826,7 @@ debounce@^1.2.1: resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@2, debug@2.6.9, debug@^2.6.9: +debug@2, debug@2.6.9, debug@^2.1.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -5929,6 +5929,13 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/jsonp/-/jsonp-0.2.1.tgz#a65b4fa0f10bda719a05441ea7b94c55f3e15bae" + integrity sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw== + dependencies: + debug "^2.1.3" + jsonwebtoken@9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#81d8c901c112c24e497a55daf6b2be1225b40145" @@ -7792,6 +7799,14 @@ react-select@5.7.4: react-transition-group "^4.3.0" use-isomorphic-layout-effect "^1.1.2" +react-share@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/react-share/-/react-share-5.1.0.tgz#27eff763e5c233c8765cacf595b039093cb9b408" + integrity sha512-OvyfMtj/0UzH1wi90OdHhZVJ6WUC/+IeWvBwppeZozwIGyAjQgyR0QXlHOrxVHVECqnGvcpBaFTXVrqouTieaw== + dependencies: + classnames "^2.3.2" + jsonp "^0.2.1" + react-side-effect@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a"