Skip to content

Commit

Permalink
feat: Image + RichTextWithImage components with placement
Browse files Browse the repository at this point in the history
1. Renames RichTextParagraph to just RichText
2. Creates an Image component that uses GatsbyImage to render gatsbyImageData
3. Does some fun with typing so that gatsbyImageData isn't undefined (see the bug at
cometkim/gatsby-plugin-typegen#146)
4. Loads images for projects & sections' paragraphs & displays them
  • Loading branch information
dgattey committed Apr 28, 2021
1 parent 9e355c8 commit 00b8de3
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 9 deletions.
50 changes: 50 additions & 0 deletions src/templates/Image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { GatsbyImage, IGatsbyImageData } from 'gatsby-plugin-image'
import * as React from 'react'

/**
* Shared type to describe what image data looks like coming from Contentful. Note that because Scalars[JSON]
* is defined as `never`, the type of Pick<... 'gatsbyImageData'> is undefined and I need a separate type
*/
export type RawImageData = GatsbyTypes.Maybe<
Pick<
GatsbyTypes.ContentfulAsset,
'id' | 'title' | 'gatsbyImageData' | 'description'
>
>

/**
* Internal type to describe what image data looks like coming from Contentful with a transformation applied
* manually. Note that because Scalars[JSON] is defined as `never`, this separate type is necessary.
*/
type ImageData = GatsbyTypes.Maybe<
Pick<GatsbyTypes.ContentfulAsset, 'id' | 'title' | 'description'> & {
gatsbyImageData: IGatsbyImageData
}
>

/**
* Just image data + alt text that we need
*/
type PropTypes = {
image: RawImageData
}

/**
* A simple image using the gatsby image data passed to it
* @param props An object containing `PropTypes`
* @returns An element for the image and all associated content
*/
const Image = ({ image }: PropTypes): JSX.Element => {
const typedImageData = (image as ImageData)?.gatsbyImageData
if (!image || !typedImageData?.images.sources) {
throw TypeError('Malformed image data')
}
return (
<GatsbyImage
image={typedImageData}
alt={image.description ?? image.title ?? image.id}
/>
)
}

export default Image
9 changes: 9 additions & 0 deletions src/templates/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export const query = graphql`
internal {
type
}
image {
id
gatsbyImageData(placeholder: BLURRED)
description
title
}
}
fragment Section on ContentfulSection {
Expand All @@ -59,6 +65,9 @@ export const query = graphql`
type
thumbnail {
id
gatsbyImageData(placeholder: BLURRED)
description
title
}
internal {
type
Expand Down
9 changes: 5 additions & 4 deletions src/templates/Project.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react'
import RichTextParagraph from './RichTextParagraph'
import Image from './Image'
import RichText from './RichText'

/**
* Top level page has a list of blocks we pass in to start the recursion
Expand All @@ -14,14 +15,14 @@ type PageProps = {
* @returns An element for the project as a wrapper
*/
const Project = ({ project }: PageProps): JSX.Element => {
if (!project.title || !project.type) {
if (!project.title || !project.type || !project.thumbnail?.gatsbyImageData) {
throw TypeError('Badly formatted project')
}
return (
<>
{project.title} | {project.creationDate} | {project.type} |{' '}
{project.thumbnail?.id}
<RichTextParagraph richText={project.description} />
<Image image={project.thumbnail} />
<RichText richText={project.description} />
</>
)
}
Expand Down
16 changes: 13 additions & 3 deletions src/templates/RichTextParagraph.tsx → src/templates/RichText.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
import * as React from 'react'

/**
* Shared type for raw text as it comes from Contentful
*/
export type RawRichText = GatsbyTypes.Maybe<
Pick<GatsbyTypes.ContentfulTextText, 'raw'>
>

/**
* All we need is rich text to display this element
*/
type PropTypes = {
richText: GatsbyTypes.Maybe<Pick<GatsbyTypes.ContentfulTextText, 'raw'>>
richText: RawRichText
}

/**
Expand All @@ -12,11 +22,11 @@ type PropTypes = {
* @param props An object containing `PropTypes`
* @returns An element for the rich text, composed of other items inside it
*/
const RichTextParagraph = ({ richText }: PropTypes): JSX.Element => {
const RichText = ({ richText }: PropTypes): JSX.Element => {
if (!richText || !richText?.raw) {
throw TypeError('Malformed rich text')
}
return <>{documentToReactComponents(JSON.parse(richText.raw))}</>
}

export default RichTextParagraph
export default RichText
67 changes: 67 additions & 0 deletions src/templates/RichTextWithImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from 'react'
import Image, { RawImageData } from './Image'
import RichText, { RawRichText } from './RichText'

/**
* Shared type for raw image type without yet being an enum
*/
type ImageTypeData = GatsbyTypes.Maybe<string>

/**
* Describes the supported values for image placement. If not one of these, either
* we need to support a new value or the existing data is incorrect.
*/
enum ImagePlacement {
None = 'none',
Left = 'left',
Right = 'right',
}

/**
* Rich text, image data, and information about how to position that image
*/
type PropTypes = {
richText: RawRichText
imageType: ImageTypeData
image: RawImageData
}

/**
* A rich text paragraph using the default settings to render the elements because it's
* really tough to set up elements that work right for custom elements.
* See https://www.contentful.com/developers/docs/concepts/rich-text/
* @param props An object containing `PropTypes`
* @returns An element for the rich text, composed of other items inside it
*/
const RichTextWithImage = ({
richText,
image,
imageType,
}: PropTypes): JSX.Element => {
const placement = imageType as ImagePlacement
if (!richText || !imageType || !placement) {
throw TypeError('Malformed rich text with image data')
}
const textElement = () => <RichText richText={richText} />
const imageElement = () => <Image image={image} />
switch (placement) {
case ImagePlacement.None:
return <>{textElement()}</>
case ImagePlacement.Left:
return (
<>
{imageElement()}
{textElement()}
</>
)
case ImagePlacement.Right:
return (
<>
{textElement()}
{imageElement()}
</>
)
}
}

export default RichTextWithImage
11 changes: 9 additions & 2 deletions src/templates/Section.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react'
import Link from './Link'
import Project from './Project'
import RichTextParagraph from './RichTextParagraph'
import RichTextWithImage from './RichTextWithImage'

/**
* This is the array of section type that is nested within BlockType
Expand Down Expand Up @@ -60,7 +60,14 @@ const generateElement = (block: BlockType): JSX.Element => {
throw TypeError("Section wasn't defined")
}
if (isTextFragment(block)) {
return <RichTextParagraph key={block.id} richText={block.text} />
return (
<RichTextWithImage
key={block.id}
richText={block.text}
imageType={block.imageType}
image={block.image}
/>
)
} else if (isLinkFragment(block)) {
return <Link key={block.id} link={block} />
} else if (isProjectFragment(block)) {
Expand Down

0 comments on commit 00b8de3

Please sign in to comment.