Skip to content

Commit

Permalink
feat: implement Contentful section [SPA-1176] (#21)
Browse files Browse the repository at this point in the history
* feat: add contentful section

* conditionally show section

* finetune hover/dragging interactions

* fix: prettify
  • Loading branch information
SofiaMargariti authored May 30, 2023
1 parent e09d8c9 commit 5517c4d
Show file tree
Hide file tree
Showing 11 changed files with 425 additions and 46 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"preview": "vite preview"
},
"dependencies": {
"@contentful/f36-core": "^4.40.6",
"@contentful/f36-tokens": "^4.0.1",
"@emotion/css": "^11.10.6",
"contentful-management": "^10.35.4",
Expand Down
75 changes: 75 additions & 0 deletions src/blocks/ContentfulSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Flex } from '@contentful/f36-core'
import tokens from '@contentful/f36-tokens'
import { css, cx } from '@emotion/css'
import React from 'react'
import { useInteraction } from '../hooks'

const styles = {
defaultStyles: css({
minHeight: '200px',
overflow: 'scroll',
flexWrap: 'wrap',
justifyContent: 'center',
}),
lineHorizontal: css({
margin: '10px',
height: '0px',
borderTop: `3px solid ${tokens.blue500}`,
}),
lineVertical: css({
width: '0px',
margin: '10px',
borderLeft: `3px solid ${tokens.blue500}`,
}),
}

interface StyleProps {
margin: string
padding: string
backgroundColor: string
width: string
height: string
flexDirection: 'row' | 'column'
border: string
gap: string
}

interface ContentfulSectionProps extends StyleProps {
onClick: () => void
isDragging: boolean
children: React.ReactNode
className?: string
}
export const ContentfulSection = ({
flexDirection,
margin,
padding,
backgroundColor,
width,
height,
border,
gap,
isDragging,
className,
...props
}: ContentfulSectionProps) => {
const { isMouseOver, onMouseOver, onMouseLeave } = useInteraction()

console.log({ margin, padding, backgroundColor, width, height, border, gap })

const styleOverrides = css({ margin, padding, backgroundColor, width, height, border, gap })

const lineStyles = flexDirection === 'row' ? styles.lineVertical : styles.lineHorizontal

return (
<Flex
flexDirection={flexDirection}
onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave}
className={cx(styles.defaultStyles, className, styleOverrides)}
{...props}>
{props.children}
{isDragging && isMouseOver && <div key="lineIndicator" className={lineStyles}></div>}
</Flex>
)
}
48 changes: 36 additions & 12 deletions src/blocks/EmptyContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { css } from '@emotion/css'
import { css, cx } from '@emotion/css'
import { ReactComponent as EmptyStateIcon } from './emptyState.svg'
import React from 'react'
import React, { useState } from 'react'
import tokens from '@contentful/f36-tokens'
import { useInteraction } from '../hooks'

const styles = {
emptyContainer: css({
container: css({
height: '200px',
display: 'flex',
alignItems: 'center',
Expand All @@ -14,25 +15,48 @@ const styles = {
fontSize: tokens.fontSizeM,
fontFamily: tokens.fontStackPrimary,
border: `1px dashed ${tokens.gray500}`,
'&:hover': {
border: `1px dashed ${tokens.blue500}`,
backgroundColor: tokens.blue100,
},
}),
highlight: css({
border: `1px dashed ${tokens.blue500}`,
backgroundColor: tokens.blue100,
}),
icon: css({
marginLeft: tokens.spacingS,
}),
}

export const EmptyContainer = ({ onComponentDropped }: any) => {
export interface EmptyContainerProps {
isFirst?: boolean
isDragging?: boolean
isHoveringOnRoot?: boolean
}

export const EmptyContainer = ({
isFirst = true,
isDragging = false,
isHoveringOnRoot = false,
}: EmptyContainerProps) => {
const { onComponentDropped, isMouseOver, onMouseOver, onMouseLeave } = useInteraction()

const showContent = isFirst ? !isDragging || (isDragging && !isMouseOver) : false

const isHighlighted = isDragging && (isHoveringOnRoot || isMouseOver)

return (
<div
className={styles.emptyContainer}
data-type="empty-container"
onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave}
className={isHighlighted ? cx(styles.container, styles.highlight) : styles.container}
onMouseUp={() => {
onComponentDropped({ node: { data: { id: 'root' } } })
onComponentDropped({ node: { type: 'root', data: { id: 'root' } } })
}}>
<EmptyStateIcon />
<span className={styles.icon}>Add components to begin</span>
{showContent ? (
<>
<EmptyStateIcon />
<span className={styles.icon}>Add components to begin</span>
</>
) : null}
</div>
)
}
31 changes: 12 additions & 19 deletions src/blocks/VisualEditorBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import tokens from '@contentful/f36-tokens'
import { css, cx } from '@emotion/css'
import { css } from '@emotion/css'
import React, { useMemo, useRef } from 'react'
import get from 'lodash.get'
import {
Expand All @@ -16,29 +16,24 @@ import { Link } from 'contentful-management'
const styles = {
hover: css({
':hover': {
border: `3px solid ${tokens.blue500}`,
border: `1px solid ${tokens.blue500}`,
},
}),
emptyContainer: css({
padding: tokens.spacing4Xl,
}),
container: css({
backgroundColor: '#ffffff',
opacity: 0.8,
backgroundImage:
'repeating-linear-gradient(45deg, #f6f6f6 25%, transparent 25%, transparent 75%, #f6f6f6 75%, #f6f6f6), repeating-linear-gradient(45deg, #f6f6f6 25%, #ffffff 25%, #ffffff 75%, #f6f6f6 75%, #f6f6f6)',
backgroundPosition: '0 0, 10px 10px',
backgroundSize: '20px 20px',
}),
}

type VisualEditorBlockProps = {
node: CompositionComponentNode
locale: string
dataSource: LocalizedDataSource
isDragging: boolean
}

export const VisualEditorBlock = ({ node, locale, dataSource }: VisualEditorBlockProps) => {
export const VisualEditorBlock = ({
node,
locale,
dataSource,
isDragging,
}: VisualEditorBlockProps) => {
const { sendMessage } = useCommunication()
const { getComponent } = useComponents()
const { onComponentDropped } = useInteraction()
Expand Down Expand Up @@ -118,6 +113,7 @@ export const VisualEditorBlock = ({ node, locale, dataSource }: VisualEditorBloc
key={childNode.data.id}
locale={locale}
dataSource={dataSource}
isDragging={isDragging}
/>
)
})
Expand All @@ -135,11 +131,8 @@ export const VisualEditorBlock = ({ node, locale, dataSource }: VisualEditorBloc
wasMousePressed.current = true
sendMessage(OutgoingExperienceBuilderEvent.COMPONENT_SELECTED, { node })
},
className: cx(
styles.hover,
componentDefinition.children && !children?.length ? styles.emptyContainer : undefined,
componentDefinition.children ? styles.container : undefined
),
className: styles.hover,
isDragging,
...props,
},
children
Expand Down
51 changes: 43 additions & 8 deletions src/blocks/VisualEditorRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import tokens from '@contentful/f36-tokens'
import { css } from '@emotion/css'
import React from 'react'
import React, { useState } from 'react'
import { Experience } from '../types'
import { useInteraction } from '../hooks/useInteraction'
import { VisualEditorBlock } from './VisualEditorBlock'
import { EmptyContainer } from './EmptyContainer'
import { useContentfulSection } from '../hooks/useContentfulSection'

const styles = {
root: css({
Expand All @@ -13,9 +14,9 @@ const styles = {
overflow: 'scroll',
}),
hover: css({
border: `3px solid transparent`,
border: `1px solid transparent`,
'&:hover': {
border: `3px solid ${tokens.blue500}`,
border: `1px solid ${tokens.blue500}`,
},
}),
}
Expand All @@ -27,23 +28,57 @@ type VisualEditorRootProps = {

export const VisualEditorRoot = ({ experience, locale }: VisualEditorRootProps) => {
const { onComponentDropped } = useInteraction()
useContentfulSection()
const [isHoveringOnRoot, setIsHoveringOnRoot] = useState(false)
console.log('isHoveringOnRoot', isHoveringOnRoot)

const { tree, dataSource } = experience
const { tree, dataSource, isDragging } = experience

const onMouseOver = (e: React.MouseEvent) => {
if (!(e.currentTarget instanceof HTMLElement)) {
return
}
if (['root', 'empty-container'].includes(e.currentTarget.dataset.type || '')) {
setIsHoveringOnRoot(true)
}
}

if (!tree?.root.children.length) {
return React.createElement(EmptyContainer, { onComponentDropped }, [])
return React.createElement(EmptyContainer, { isDragging }, [])
}

const sectionOutline =
isDragging && isHoveringOnRoot ? (
<EmptyContainer
key="section-outline"
isFirst={false}
isDragging={isDragging}
isHoveringOnRoot={isHoveringOnRoot}
/>
) : null

return React.createElement(
'div',
{
className: styles.root,
onMouseUp: () => {
onComponentDropped({ node: tree.root })
},
onMouseOver,
onMouseOut: () => setIsHoveringOnRoot(false),
'data-type': 'root',
},
tree.root.children.map((node: any) => (
<VisualEditorBlock key={node.data.id} node={node} locale={locale} dataSource={dataSource} />
))
[
tree.root.children.map((node: any) => (
<VisualEditorBlock
key={node.data.id}
node={node}
locale={locale}
dataSource={dataSource}
isDragging={isDragging}
/>
)),
sectionOutline,
]
)
}
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const CONTENTFUL_WEB_APP_ORIGIN = 'https://app.contentful.com'

export const CONTENTFUL_SECTION_ID = 'contentful-section'
65 changes: 65 additions & 0 deletions src/hooks/useContentfulSection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useComponents } from './useComponents'
import { ContentfulSection } from '../blocks/ContentfulSection'
import { CONTENTFUL_SECTION_ID } from '../constants'
import { useEffect } from 'react'

export const useContentfulSection = () => {
const { defineComponent } = useComponents()
useEffect(() => {
defineComponent(ContentfulSection, {
id: CONTENTFUL_SECTION_ID,
name: 'Contentful Section',
variables: {
margin: {
type: 'Text',
group: 'style',
description: 'The margin of the section',
defaultValue: '0',
},
padding: {
type: 'Text',
group: 'style',
description: 'The padding of the section',
defaultValue: '0',
},
backgroundColor: {
type: 'Text',
group: 'style',
description: 'The background color of the section',
defaultValue: 'transparent',
},
width: {
type: 'Text',
group: 'style',
description: 'The width of the section',
defaultValue: '100%',
},
height: {
type: 'Text',
group: 'style',
description: 'The height of the section',
defaultValue: 'auto',
},
flexDirection: {
type: 'Text',
group: 'style',
description: 'The orientation of the section',
defaultValue: 'row',
},
border: {
type: 'Text',
group: 'style',
description: 'The border of the section',
defaultValue: '0',
},
gap: {
type: 'Text',
group: 'style',
description: 'The spacing between the elements of the section',
defaultValue: '0px',
},
},
children: true,
})
}, [])
}
Loading

0 comments on commit 5517c4d

Please sign in to comment.