-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(epic): marketing blog page (#59)
- Loading branch information
1 parent
68b15d1
commit f6f1485
Showing
38 changed files
with
1,779 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
src/app/(routes)/blog/[slug]/_components/article-content.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { | ||
documentToReactComponents, | ||
RenderMark, | ||
RenderNode, | ||
} from "@contentful/rich-text-react-renderer"; | ||
import { BLOCKS, Document, INLINES, MARKS } from "@contentful/rich-text-types"; | ||
import Link from "next/link"; | ||
import { isExternal } from "util/types"; | ||
import Divider from "./divider"; | ||
import { IframeContainer } from "./iframe-container"; | ||
import { Text } from "@/app/_components/text"; | ||
import { Asset } from "contentful"; | ||
import ContentfulImage from "./contentful-image"; | ||
|
||
// Map text-format types to custom components | ||
|
||
const markRenderers: RenderMark = { | ||
[MARKS.BOLD]: (text) => <strong>{text}</strong>, | ||
[MARKS.ITALIC]: (text) => <em>{text}</em>, | ||
[MARKS.UNDERLINE]: (text) => <span className="underline">{text}</span>, | ||
[MARKS.CODE]: (text) => <code>{text}</code>, | ||
[MARKS.SUPERSCRIPT]: (text) => <sup>{text}</sup>, | ||
[MARKS.SUBSCRIPT]: (text) => <sub>{text}</sub>, | ||
}; | ||
|
||
const nodeRenderers: RenderNode = { | ||
[INLINES.HYPERLINK]: (node, children) => { | ||
const href = node.data.uri as string; | ||
if ( | ||
href.includes("youtube.com/embed") || | ||
href.includes("player.vimeo.com") || | ||
children?.toString().toLowerCase().includes("iframe") // to handle uncommon cases, creator can set the text to "iframe" | ||
) { | ||
return ( | ||
<IframeContainer> | ||
<iframe | ||
width="100%" | ||
height="100%" | ||
src={href} | ||
title="YouTube video player" | ||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" | ||
allowFullScreen | ||
style={{ | ||
position: "absolute", | ||
top: 0, | ||
left: 0, | ||
clipPath: "inset(0% 0% 0% 0% round 16px)", | ||
}} | ||
></iframe> | ||
</IframeContainer> | ||
); | ||
} | ||
return ( | ||
<Link | ||
target={isExternal(href) ? "_blank" : undefined} | ||
className="hover:text-text underline" | ||
href={href} | ||
type="external" | ||
> | ||
{children} | ||
</Link> | ||
); | ||
}, | ||
[BLOCKS.DOCUMENT]: (_, children) => children, | ||
[BLOCKS.PARAGRAPH]: (_, children) => ( | ||
<Text variant="body"> | ||
<p>{children}</p> | ||
</Text> | ||
), | ||
[BLOCKS.HEADING_1]: (_, children) => ( | ||
<Text variant="heading-1" className="py-4"> | ||
<h1>{children}</h1> | ||
</Text> | ||
), | ||
[BLOCKS.HEADING_2]: (_, children) => ( | ||
<Text variant="heading-3" className="py-4"> | ||
<h2>{children}</h2> | ||
</Text> | ||
), | ||
[BLOCKS.HEADING_3]: (_, children) => ( | ||
<Text variant="heading-4"> | ||
<h3>{children}</h3> | ||
</Text> | ||
), | ||
[BLOCKS.HEADING_4]: (_, children) => ( | ||
<Text variant="body"> | ||
<h4>{children}</h4> | ||
</Text> | ||
), | ||
[BLOCKS.HEADING_5]: (_, children) => ( | ||
<Text variant="body"> | ||
<h5>{children}</h5> | ||
</Text> | ||
), | ||
[BLOCKS.HEADING_6]: (_, children) => ( | ||
<Text variant="body"> | ||
<h6>{children}</h6> | ||
</Text> | ||
), | ||
[BLOCKS.EMBEDDED_RESOURCE]: (_, children) => <div>{children}</div>, | ||
[BLOCKS.UL_LIST]: (_, children) => <ul className="list-disc pl-8">{children}</ul>, | ||
[BLOCKS.OL_LIST]: (_, children) => <ol className="list-decimal pl-8">{children}</ol>, | ||
[BLOCKS.LIST_ITEM]: (_, children) => <li>{children}</li>, | ||
[BLOCKS.QUOTE]: (_, children) => <blockquote>{children}</blockquote>, | ||
[BLOCKS.HR]: () => <Divider />, | ||
[BLOCKS.TABLE]: (_, children) => ( | ||
<table> | ||
<tbody>{children}</tbody> | ||
</table> | ||
), | ||
[BLOCKS.TABLE_ROW]: (_, children) => <tr>{children}</tr>, | ||
[BLOCKS.TABLE_HEADER_CELL]: (_, children) => <th>{children}</th>, | ||
[BLOCKS.TABLE_CELL]: (_, children) => <td>{children}</td>, | ||
[BLOCKS.EMBEDDED_ASSET]: (node) => { | ||
const data = node.data.target as Asset<"WITHOUT_UNRESOLVABLE_LINKS", string>; | ||
const { file, description, title } = data.fields; | ||
const mimeGroup = file?.contentType.split("/")[0]; // image / video etc | ||
switch (mimeGroup) { | ||
case "image": | ||
return <ContentfulImage image={data} />; | ||
// TODO: test this, make custom component if necessary | ||
case "video": | ||
return ( | ||
<video title={title} aria-description={description} src={`https:${file?.url}`}> | ||
{description} | ||
</video> | ||
); | ||
// TODO: add other asset types, handle them | ||
default: | ||
return <p>unknown file type</p>; | ||
} | ||
}, | ||
}; | ||
|
||
const options = { | ||
renderNode: nodeRenderers, | ||
renderMark: markRenderers, | ||
preserveWhitespace: true, | ||
}; | ||
|
||
export default function ArticleContent({ content }: { content: Document }) { | ||
return ( | ||
<article className="flex flex-col gap-4"> | ||
{documentToReactComponents(content, options)} | ||
</article> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import Link from "next/link"; | ||
import { ChevronDownIcon } from "@/app/_components/icons"; | ||
|
||
export default function Breadcrumb({ fullTitle }: { fullTitle: string }) { | ||
// Max title to 40 characters | ||
const title = fullTitle.length > 40 ? fullTitle.slice(0, 40) + "..." : fullTitle; | ||
return ( | ||
<div className="flex items-center gap-2"> | ||
<Link href="/blog" className="text-sm font-lighter leading-tight "> | ||
Blog | ||
</Link> | ||
<ChevronDownIcon className="-rotate-90" /> | ||
<div className="text-sm font-lighter leading-tight text-aqua-100">{title}</div> | ||
</div> | ||
); | ||
} |
49 changes: 49 additions & 0 deletions
49
src/app/(routes)/blog/[slug]/_components/contentful-image.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { Asset } from "contentful"; | ||
import Image from "next/image"; | ||
import { object } from "zod"; | ||
|
||
export default function ContentfulImage({ | ||
image, | ||
borderless, | ||
displayDescription, | ||
fillDisplay, | ||
}: { | ||
image?: Asset<"WITHOUT_UNRESOLVABLE_LINKS", string>; | ||
borderless?: boolean; | ||
displayDescription?: boolean; | ||
fillDisplay?: boolean; | ||
}) { | ||
if (!image) { | ||
return null; | ||
} | ||
|
||
const { file, description, title } = image.fields; | ||
const url = file?.url; | ||
if (!url) { | ||
return null; | ||
} | ||
const urlWithProtocol = `https:${url}`; | ||
|
||
const classes = borderless ? "" : "rounded-3xl border border-white-translucent"; | ||
|
||
const props = fillDisplay | ||
? { fill: true, objectFit: "cover" } | ||
: { | ||
height: file.details.image?.height, | ||
width: file.details.image?.width, | ||
}; | ||
|
||
return ( | ||
<div className="relative flex h-full w-full flex-col items-center gap-4"> | ||
<Image | ||
src={urlWithProtocol} | ||
alt={description ?? "description"} | ||
title={title} | ||
className={classes} | ||
aria-description={description} | ||
{...props} | ||
/> | ||
{description && displayDescription && <p>{description}</p>} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { twMerge } from "@/app/_lib/tw-merge"; | ||
|
||
export default function Divider({ className }: { className?: string }) { | ||
return ( | ||
<div | ||
className={twMerge("h-0 w-full border-t border-white-translucent", className)} | ||
></div> | ||
); | ||
} |
13 changes: 13 additions & 0 deletions
13
src/app/(routes)/blog/[slug]/_components/iframe-container.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { twMerge } from "@/app/_lib/tw-merge"; | ||
|
||
type Props = { | ||
className?: string; | ||
}; | ||
|
||
export function IframeContainer({ className, children }: React.PropsWithChildren<Props>) { | ||
return ( | ||
<span className={twMerge("relative mx-auto block aspect-video w-full ", className)}> | ||
{children} | ||
</span> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { Text } from "@/app/_components/text"; | ||
import { getReadingTime } from "@/app/_lib/contentful"; | ||
import { Document } from "@contentful/rich-text-types"; | ||
import { DateTime } from "luxon"; | ||
import { twMerge } from "tailwind-merge"; | ||
|
||
export function MetaInfo({ | ||
isoCreatedDate, | ||
content, | ||
preventCenter, | ||
compact, | ||
}: { | ||
isoCreatedDate: string; | ||
content: Document; | ||
preventCenter?: boolean; | ||
compact?: boolean; | ||
}) { | ||
const dateString = DateTime.fromISO(isoCreatedDate).toFormat("MMM dd, yyyy"); | ||
const minutesToRead = getReadingTime(content); | ||
return ( | ||
<div | ||
className={twMerge( | ||
"flex items-center justify-center gap-3 text-grey-400 sm:justify-start", | ||
preventCenter ? ["justify-start"] : ["justify-center", "sm:justify-start"], | ||
)} | ||
> | ||
<Text variant={compact ? "cap-case-xs" : "cap-case-sm"}>{dateString}</Text>• | ||
<Text variant={compact ? "cap-case-xs" : "cap-case-sm"}> | ||
{minutesToRead} min read | ||
</Text> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.