import { documentToReactComponents, Options } from '@contentful/rich-text-react-renderer' import { Block, BLOCKS, Document, Inline, INLINES, MARKS } from '@contentful/rich-text-types' import { WIDTH } from '@helpers/Dimensions' import { AssetBlock } from '@typings/contentful' import React, { FC, ReactNode, useLayoutEffect, useState } from 'react' import { Image, Linking } from 'react-native' import styled from 'styled-components/native' import { constants, fonts } from '../styles/themes' import { IconBold } from './iconRegular' const HyperLink = ({ href, children }: { href: string children: ReactNode }) => { const onPress = () => { Linking.openURL(href) } // eslint-disable-next-line jsx-a11y/anchor-is-valid return <Link onPress={onPress}>{children}</Link> } const getContentType = ( node: Inline ): { type: string; path: string; title: string } => { switch (node.data.target.sys.contentType.sys.id) { case 'habit': { const path = node?.data?.target?.fields?.slug const name = node?.data?.target?.fields?.title return { type: 'habit', path: `/habit/${path}`, title: name } } case 'lesson': { const path = node?.data?.target?.fields?.slug const name = node?.data?.target?.fields?.lessonName return { type: 'lesson', path: `/lesson/${path}`, title: name } } default: return { type: 'unknown', path: '', title: '' } } } const defaultInline: (type: string, node: Inline) => ReactNode = ( _, node: Inline ) => { const contentType = getContentType(node) return <Underline>{contentType.title}</Underline> } interface Props { content?: Document assets?: { block: AssetBlock[] } } const RichText: FC<Props> = ({ content, assets }) => { if (!content) return null console.log(assets) const options: Options = { renderMark: { [MARKS.BOLD]: function bold(text: ReactNode) { return <Bold>{text}</Bold> }, [MARKS.ITALIC]: function italic(text: ReactNode) { return <Italic>{text}</Italic> }, [MARKS.UNDERLINE]: function underline(text: ReactNode) { return <Underline>{text}</Underline> }, [MARKS.CODE]: function code(text: ReactNode) { return <Code>{text}</Code> } }, renderNode: { [BLOCKS.PARAGRAPH]: function paragraph( _: Block | Inline, children: ReactNode ) { return <Paragraph>{children}</Paragraph> }, [BLOCKS.UL_LIST]: function ulList( node: Block | Inline, children: ReactNode ) { return <List node={node}>{children}</List> }, [BLOCKS.OL_LIST]: function olList( node: Block | Inline, children: ReactNode ) { return <List node={node}>{children}</List> }, [BLOCKS.EMBEDDED_ASSET]: function embeddedAsset(node: Block | Inline) { return <ImageBlock type="" assets={assets} node={node} /> }, [BLOCKS.HR]: function hr(_: Block | Inline) { return <Line /> }, [BLOCKS.LIST_ITEM]: function listItem( _: Block | Inline, children: ReactNode ) { return <ListItemText>{children}</ListItemText> }, [BLOCKS.HEADING_1]: function h1(_: Block | Inline, children: ReactNode) { return <H1>{children}</H1> }, [BLOCKS.HEADING_2]: function h2(_: Block | Inline, children: ReactNode) { return <H2>{children}</H2> }, [BLOCKS.HEADING_3]: function h3(_: Block | Inline, children: ReactNode) { return <H3>{children}</H3> }, [BLOCKS.HEADING_4]: function h4(_: Block | Inline, children: ReactNode) { return <H4>{children}</H4> }, [BLOCKS.HEADING_5]: function h5(_: Block | Inline, children: ReactNode) { return <H5>{children}</H5> }, [BLOCKS.HEADING_6]: function h5(_: Block | Inline, children: ReactNode) { return <H6>{children}</H6> }, [BLOCKS.QUOTE]: function quote(_: Block | Inline, children: ReactNode) { return <BlockQuote>{children}</BlockQuote> }, [INLINES.HYPERLINK]: function hyperlink( node: Block | Inline, children: ReactNode ) { return <HyperLink href={node?.data?.uri}>{children}</HyperLink> }, [INLINES.EMBEDDED_ENTRY]: (node: Block | Inline) => defaultInline(INLINES.EMBEDDED_ENTRY, node as Inline) } } const components = documentToReactComponents(content, options) return <Container>{components}</Container> } export default RichText const Container = styled.View`` const Bold = styled.Text` font-size: 15px; font-family: ${fonts.bold}; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; ` Bold.displayName = 'Bold' const Italic = styled.Text` font-family: ${({ theme }) => theme.FONT_MEDIUM}; font-style: italic; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; ` Italic.displayName = 'Italic' const Underline = styled.Text` text-decoration: underline; font-family: ${({ theme }) => theme.FONT_MEDIUM}; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; ` const Code = styled.Text` font-family: ${({ theme }) => theme.FONT_MEDIUM}; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; ` const Paragraph = styled.Text` font-size: 15px; line-height: 25px; margin-bottom: 20px; font-family: ${({ theme }) => theme.FONT_MEDIUM}; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; ` const H1 = styled.Text` font-family: ${fonts.bold}; font-size: 28px; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; margin-bottom: 20px; ` const H2 = styled.Text` font-family: ${fonts.bold}; font-size: 25px; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; margin-bottom: 20px; ` const H3 = styled.Text` font-family: ${fonts.bold}; font-size: 20px; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; margin-bottom: 20px; ` const H4 = styled.Text` font-family: ${fonts.bold}; font-size: 20px; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; margin-bottom: 20px; ` const H5 = styled.Text` font-family: ${fonts.bold}; font-size: 18px; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; margin-bottom: 20px; ` const H6 = styled.Text` font-family: ${fonts.bold}; font-size: 15px; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; margin-bottom: 20px; ` const Line = styled.View` width: 100%; height: ${constants.hairlineWidth}px; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; ` const Link = styled(Paragraph)` color: ${({ theme }) => theme.accent}; ` const List = ({ node: { nodeType }, children }: { node: Block | Inline children: ReactNode[] | ReactNode }) => { const ordered = nodeType === 'ordered-list' if (!Array.isArray(children)) return null const mapped = children?.map((child, index: number) => ( <ListItemContainer ordered={ordered} key={`${index}`}> {ordered ? <Number>{index + 1}.</Number> : <Dot />} <ListItemText>{child}</ListItemText> </ListItemContainer> )) return <Container>{mapped}</Container> } const Number = styled.Text` font-size: 15px; line-height: 25px; font-family: ${fonts.bold}; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; ` const Dot = styled(IconBold).attrs(({ theme }) => ({ height: '8', name: 'circle', width: '8', fill: theme.PRIMARY_TEXT_COLOR }))`` const ListItemContainer = styled.View` flex-direction: row; align-items: ${({ ordered }: { ordered: boolean }) => ordered ? 'flex-start' : 'center'}; margin-bottom: 15px; ` const ListItemText = styled.Text` margin-left: 10px; font-size: 15px; flex: 1; line-height: 25px; font-family: ${({ theme }) => theme.FONT_MEDIUM}; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; ` const BlockQuote = styled.View` padding-left: 10px; border-left-color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; border-left-width: 2px; ` type ImageProps = { type: string node: Block | Inline assets?: { block: AssetBlock[] } } const ImageBlock: FC<ImageProps> = ({ node, assets }) => { const img = assets?.block.find((i) => i.sys.id === node.data.target.sys.id) const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) useLayoutEffect(() => { Image.getSize( `${img?.url}?fm=jpg&fl=progressive&w=${WIDTH * 2}`, (width, height) => setDimensions({ width, height }), () => null ) }, [img?.url]) return ( <ImageContainer> <Img resizeMode="contain" width={dimensions.width / 2} height={dimensions.height / 2} source={{ uri: `${img?.url}?fm=jpg&fl=progressive&w=${WIDTH * 2}` }} /> <ImageTitle>{img?.title}</ImageTitle> <ImageDescription>{img?.description}</ImageDescription> </ImageContainer> ) } const ImageContainer = styled.View` background-color: ${({ theme }) => theme.SECONDARY_BACKGROUND_COLOR}; margin: 30px -20px; padding: 0px 0px 20px; flex: 1; ` const ImageTitle = styled.Text` margin: 10px 20px; font-family: ${({ theme }) => theme.FONT_BOLD}; color: ${({ theme }) => theme.PRIMARY_TEXT_COLOR}; font-size: 17px; ` const ImageDescription = styled.Text` margin: 0px 20px; font-family: ${({ theme }) => theme.FONT_MEDIUM}; color: ${({ theme }) => theme.SECONDARY_TEXT_COLOR}; font-size: 14px; ` const Img = styled.Image` height: ${({ height }) => height}px; width: ${({ width }) => width}px; `