From ab29e5e11249a56ed612acae438b5b2d60a63ae1 Mon Sep 17 00:00:00 2001 From: cohitre Date: Mon, 4 Mar 2024 07:40:13 -0800 Subject: [PATCH] Creating @usewaypoint/email-builder (#58) * Creating @usewaypoint/email-builder * Consuming @usewaypoint/email-builder --- .github/workflows/ci.yaml | 1 + packages/editor-sample/package-lock.json | 23 +++ packages/editor-sample/package.json | 1 + .../input-panels/ContainerSidebarPanel.tsx | 2 +- .../input-panels/EmailLayoutSidebarPanel.tsx | 17 +- .../src/App/TemplatePanel/HtmlPanel.tsx | 27 +-- .../src/App/TemplatePanel/index.tsx | 7 +- .../ColumnsContainerEditor.tsx | 49 +++++ .../blocks/ColumnsContainer/index.tsx | 84 --------- .../blocks/Container/ContainerEditor.tsx | 37 ++++ .../blocks/Container/ContainerPropsSchema.tsx | 4 +- .../src/documents/blocks/Container/index.tsx | 57 ------ .../blocks/EmailLayout/EmailLayoutEditor.tsx | 96 ++++++++++ .../EmailLayout/EmailLayoutPropsSchema.tsx | 39 +++- .../documents/blocks/EmailLayout/index.tsx | 121 ------------- .../helpers/EditorChildrenIds/index.tsx | 42 ++++- .../helpers/block-wrappers/TuneMenu.tsx | 12 +- .../src/documents/editor/core.tsx | 20 +-- .../src/documents/reader/ReaderBlock.tsx | 21 --- .../src/documents/reader/ReaderContext.tsx | 17 -- packages/email-builder/.npmignore | 9 + packages/email-builder/LICENSE | 19 ++ packages/email-builder/README.md | 1 + packages/email-builder/package-lock.json | 168 ++++++++++++++++++ packages/email-builder/package.json | 33 ++++ .../src/Reader}/core.tsx | 68 ++++--- .../ColumnsContainerPropsSchema.ts | 23 +++ .../ColumnsContainerReader.tsx | 17 ++ .../blocks/Container/ContainerPropsSchema.tsx | 15 ++ .../src/blocks/Container/ContainerReader.tsx | 18 ++ .../EmailLayout/EmailLayoutPropsSchema.tsx | 32 ++++ .../blocks/EmailLayout/EmailLayoutReader.tsx | 74 ++++++++ packages/email-builder/tsconfig.build.json | 4 + packages/email-builder/tsconfig.json | 9 + 34 files changed, 788 insertions(+), 379 deletions(-) create mode 100644 packages/editor-sample/src/documents/blocks/ColumnsContainer/ColumnsContainerEditor.tsx delete mode 100644 packages/editor-sample/src/documents/blocks/ColumnsContainer/index.tsx create mode 100644 packages/editor-sample/src/documents/blocks/Container/ContainerEditor.tsx delete mode 100644 packages/editor-sample/src/documents/blocks/Container/index.tsx create mode 100644 packages/editor-sample/src/documents/blocks/EmailLayout/EmailLayoutEditor.tsx delete mode 100644 packages/editor-sample/src/documents/blocks/EmailLayout/index.tsx delete mode 100644 packages/editor-sample/src/documents/reader/ReaderBlock.tsx delete mode 100644 packages/editor-sample/src/documents/reader/ReaderContext.tsx create mode 100644 packages/email-builder/.npmignore create mode 100644 packages/email-builder/LICENSE create mode 100644 packages/email-builder/README.md create mode 100644 packages/email-builder/package-lock.json create mode 100644 packages/email-builder/package.json rename packages/{editor-sample/src/documents/reader => email-builder/src/Reader}/core.tsx (62%) create mode 100644 packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerPropsSchema.ts create mode 100644 packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerReader.tsx create mode 100644 packages/email-builder/src/blocks/Container/ContainerPropsSchema.tsx create mode 100644 packages/email-builder/src/blocks/Container/ContainerReader.tsx create mode 100644 packages/email-builder/src/blocks/EmailLayout/EmailLayoutPropsSchema.tsx create mode 100644 packages/email-builder/src/blocks/EmailLayout/EmailLayoutReader.tsx create mode 100644 packages/email-builder/tsconfig.build.json create mode 100644 packages/email-builder/tsconfig.json diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 794b6c4..7362b45 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,6 +31,7 @@ jobs: (cd ./packages/block-text;pwd;npm ci) (cd ./packages/document-core;pwd;npm ci) (cd ./packages/editor-sample;pwd;npm ci) + (cd ./packages/email-builder;pwd;npm ci) - run: npx eslint . - run: npx prettier . --check - run: npm test diff --git a/packages/editor-sample/package-lock.json b/packages/editor-sample/package-lock.json index 91a82ab..0b3111d 100644 --- a/packages/editor-sample/package-lock.json +++ b/packages/editor-sample/package-lock.json @@ -23,6 +23,7 @@ "@usewaypoint/block-spacer": "^0.0.2", "@usewaypoint/block-text": "^0.0.2", "@usewaypoint/document-core": "^0.0.4", + "@usewaypoint/email-builder": "^0.0.2", "codemirror": "^6.0.1", "highlight.js": "^11.9.0", "js-beautify": "^1.15.1", @@ -2110,6 +2111,28 @@ "zod": "^1 || ^2 || ^3" } }, + "node_modules/@usewaypoint/email-builder": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@usewaypoint/email-builder/-/email-builder-0.0.2.tgz", + "integrity": "sha512-d15dlfVsGzyNTuaqp72NYWjHTVRmlwqiDm9TnxeryBTbSptED1Pf+kLGKDepFR/cK7qTZREvF0m6f3RxhdD0AA==", + "dependencies": { + "@usewaypoint/block-avatar": "^0.0.1", + "@usewaypoint/block-button": "^0.0.2", + "@usewaypoint/block-columns-container": "^0.0.2", + "@usewaypoint/block-container": "^0.0.1", + "@usewaypoint/block-divider": "^0.0.3", + "@usewaypoint/block-heading": "^0.0.2", + "@usewaypoint/block-html": "^0.0.2", + "@usewaypoint/block-image": "^0.0.4", + "@usewaypoint/block-spacer": "^0.0.2", + "@usewaypoint/block-text": "^0.0.2", + "@usewaypoint/document-core": "^0.0.4" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.6.0.tgz", diff --git a/packages/editor-sample/package.json b/packages/editor-sample/package.json index a6b64b8..88c4f51 100644 --- a/packages/editor-sample/package.json +++ b/packages/editor-sample/package.json @@ -23,6 +23,7 @@ "@usewaypoint/block-spacer": "^0.0.2", "@usewaypoint/block-text": "^0.0.2", "@usewaypoint/document-core": "^0.0.4", + "@usewaypoint/email-builder": "^0.0.2", "codemirror": "^6.0.1", "highlight.js": "^11.9.0", "js-beautify": "^1.15.1", diff --git a/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/ContainerSidebarPanel.tsx b/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/ContainerSidebarPanel.tsx index b33c0a8..c947545 100644 --- a/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/ContainerSidebarPanel.tsx +++ b/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/ContainerSidebarPanel.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { ContainerProps, ContainerPropsSchema } from '../../../../documents/blocks/Container/ContainerPropsSchema'; +import ContainerPropsSchema, { ContainerProps } from '../../../../documents/blocks/Container/ContainerPropsSchema'; import BaseSidebarPanel from './helpers/BaseSidebarPanel'; import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel'; diff --git a/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/EmailLayoutSidebarPanel.tsx b/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/EmailLayoutSidebarPanel.tsx index bf56e90..d915ff8 100644 --- a/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/EmailLayoutSidebarPanel.tsx +++ b/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/EmailLayoutSidebarPanel.tsx @@ -1,18 +1,17 @@ import React, { useState } from 'react'; -import { z } from 'zod'; import { Divider } from '@mui/material'; -import { EmailLayoutPropsSchema } from '../../../../documents/blocks/EmailLayout/EmailLayoutPropsSchema'; +import EmailLayoutPropsSchema, { + EmailLayoutProps, +} from '../../../../documents/blocks/EmailLayout/EmailLayoutPropsSchema'; import BaseSidebarPanel from './helpers/BaseSidebarPanel'; import ColorInput from './helpers/inputs/ColorInput'; -type EmailLayoutSidebarPanelProps = z.infer; - type EmailLayoutSidebarFieldsProps = { - data: EmailLayoutSidebarPanelProps; - setData: (v: EmailLayoutSidebarPanelProps) => void; + data: EmailLayoutProps; + setData: (v: EmailLayoutProps) => void; }; export default function EmailLayoutSidebarFields({ data, setData }: EmailLayoutSidebarFieldsProps) { const [, setErrors] = useState(null); @@ -31,18 +30,18 @@ export default function EmailLayoutSidebarFields({ data, setData }: EmailLayoutS updateData({ ...data, backdropColor })} /> updateData({ ...data, canvasColor })} /> updateData({ ...data, textColor })} /> diff --git a/packages/editor-sample/src/App/TemplatePanel/HtmlPanel.tsx b/packages/editor-sample/src/App/TemplatePanel/HtmlPanel.tsx index eba2c2e..6c38048 100644 --- a/packages/editor-sample/src/App/TemplatePanel/HtmlPanel.tsx +++ b/packages/editor-sample/src/App/TemplatePanel/HtmlPanel.tsx @@ -1,25 +1,26 @@ -import * as React from 'react'; +import React, { useMemo } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; +import Reader from '@usewaypoint/email-builder/dist/Reader/core'; + import { useDocument } from '../../documents/editor/EditorContext'; -import ReaderBlock from '../../documents/reader/ReaderBlock'; -import { ReaderProvider } from '../../documents/reader/ReaderContext'; import TextEditorPanel from './helper/TextEditorPanel'; export default function HtmlPanel() { const document = useDocument(); - const string = React.useMemo(() => { - return renderToStaticMarkup( - - - - - - - - + const string = useMemo(() => { + return ( + '' + + renderToStaticMarkup( + + + + + + + ) ); }, [document]); diff --git a/packages/editor-sample/src/App/TemplatePanel/index.tsx b/packages/editor-sample/src/App/TemplatePanel/index.tsx index bf212fb..9c7b0df 100644 --- a/packages/editor-sample/src/App/TemplatePanel/index.tsx +++ b/packages/editor-sample/src/App/TemplatePanel/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { MonitorOutlined, PhoneIphoneOutlined } from '@mui/icons-material'; import { Box, Stack, SxProps, ToggleButton, ToggleButtonGroup, Tooltip } from '@mui/material'; +import Reader from '@usewaypoint/email-builder/dist/Reader/core'; import EditorBlock from '../../documents/editor/EditorBlock'; import { @@ -10,8 +11,6 @@ import { useSelectedMainTab, useSelectedScreenSize, } from '../../documents/editor/EditorContext'; -import ReaderBlock from '../../documents/reader/ReaderBlock'; -import { ReaderProvider } from '../../documents/reader/ReaderContext'; import ToggleInspectorPanelButton from '../InspectorDrawer/ToggleInspectorPanelButton'; import ToggleSamplesPanelButton from '../SamplesDrawer/ToggleSamplesPanelButton'; @@ -61,9 +60,7 @@ export default function TemplatePanel() { case 'preview': return ( - - - + ); case 'html': diff --git a/packages/editor-sample/src/documents/blocks/ColumnsContainer/ColumnsContainerEditor.tsx b/packages/editor-sample/src/documents/blocks/ColumnsContainer/ColumnsContainerEditor.tsx new file mode 100644 index 0000000..145cd8a --- /dev/null +++ b/packages/editor-sample/src/documents/blocks/ColumnsContainer/ColumnsContainerEditor.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import { ColumnsContainer as BaseColumnsContainer } from '@usewaypoint/block-columns-container'; + +import { useCurrentBlockId } from '../../editor/EditorBlock'; +import { setDocument, setSelectedBlockId } from '../../editor/EditorContext'; +import EditorChildrenIds, { EditorChildrenChange } from '../helpers/EditorChildrenIds'; + +import ColumnsContainerPropsSchema, { ColumnsContainerProps } from './ColumnsContainerPropsSchema'; + +const EMPTY_COLUMNS = [{ childrenIds: [] }, { childrenIds: [] }, { childrenIds: [] }]; + +export default function ColumnsContainerEditor({ style, props }: ColumnsContainerProps) { + const currentBlockId = useCurrentBlockId(); + + const { columns, ...restProps } = props ?? {}; + const columnsValue = columns ?? EMPTY_COLUMNS; + + const updateColumn = (columnIndex: 0 | 1 | 2, { block, blockId, childrenIds }: EditorChildrenChange) => { + const nColumns = [...columnsValue]; + nColumns[columnIndex] = { childrenIds }; + setDocument({ + [blockId]: block, + [currentBlockId]: { + type: 'ColumnsContainer', + data: ColumnsContainerPropsSchema.parse({ + style, + props: { + ...restProps, + columns: nColumns, + }, + }), + }, + }); + setSelectedBlockId(blockId); + }; + + return ( + updateColumn(0, change)} />, + updateColumn(1, change)} />, + updateColumn(2, change)} />, + ]} + /> + ); +} diff --git a/packages/editor-sample/src/documents/blocks/ColumnsContainer/index.tsx b/packages/editor-sample/src/documents/blocks/ColumnsContainer/index.tsx deleted file mode 100644 index 777d596..0000000 --- a/packages/editor-sample/src/documents/blocks/ColumnsContainer/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; - -import { ColumnsContainer as BaseColumnsContainer } from '@usewaypoint/block-columns-container'; - -import { TEditorBlock } from '../../editor/core'; -import { useCurrentBlockId } from '../../editor/EditorBlock'; -import { setDocument, setSelectedBlockId } from '../../editor/EditorContext'; -import ReaderBlock from '../../reader/ReaderBlock'; -import EditorChildrenIds from '../helpers/EditorChildrenIds'; - -import ColumnsContainerPropsSchema, { ColumnsContainerProps } from './ColumnsContainerPropsSchema'; - -export function ColumnsContainer({ style, props }: ColumnsContainerProps) { - const { columns, ...restProps } = props ?? {}; - let cols = undefined; - if (columns) { - cols = columns.map((col) => col.childrenIds.map((childId) => )); - } - - return ; -} - -const EMPTY_COLUMNS = [{ childrenIds: [] }, { childrenIds: [] }, { childrenIds: [] }]; - -export function EditorColumnsContainer({ style, props }: ColumnsContainerProps) { - const blockId = useCurrentBlockId(); - - const { columns, ...restProps } = props ?? {}; - const columnsValue = columns ?? EMPTY_COLUMNS; - - const renderColumn = (columnIndex: 0 | 1 | 2) => ( - { - insertBlock(columnIndex, block, index); - }} - /> - ); - - const insertBlock = (columnIndex: 0 | 1 | 2, blockConfiguration: TEditorBlock, i: number | null) => { - const id = `block-${Date.now()}`; - - const getColumns = () => { - const columnsCopy = [...columnsValue]; - if (i === null) { - columnsCopy[columnIndex] = { - childrenIds: [...columnsValue[columnIndex].childrenIds, id], - }; - return columnsCopy; - } - columnsCopy[columnIndex] = { - childrenIds: [ - ...columnsValue[columnIndex].childrenIds.slice(0, i), - id, - ...columnsValue[columnIndex].childrenIds.slice(i), - ], - }; - return columnsCopy; - }; - - setDocument({ - [id]: blockConfiguration, - [blockId]: { - type: 'ColumnsContainer', - data: ColumnsContainerPropsSchema.parse({ - style, - props: { - ...restProps, - columns: getColumns(), - }, - }), - }, - }); - setSelectedBlockId(id); - }; - - return ( - - ); -} diff --git a/packages/editor-sample/src/documents/blocks/Container/ContainerEditor.tsx b/packages/editor-sample/src/documents/blocks/Container/ContainerEditor.tsx new file mode 100644 index 0000000..6e027a9 --- /dev/null +++ b/packages/editor-sample/src/documents/blocks/Container/ContainerEditor.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { Container as BaseContainer } from '@usewaypoint/block-container'; + +import { useCurrentBlockId } from '../../editor/EditorBlock'; +import { setDocument, setSelectedBlockId, useDocument } from '../../editor/EditorContext'; +import EditorChildrenIds from '../helpers/EditorChildrenIds'; + +import { ContainerProps } from './ContainerPropsSchema'; + +export default function ContainerEditor({ style, props }: ContainerProps) { + const childrenIds = props?.childrenIds ?? []; + + const document = useDocument(); + const currentBlockId = useCurrentBlockId(); + + return ( + + { + setDocument({ + [blockId]: block, + [currentBlockId]: { + type: 'Container', + data: { + ...document[currentBlockId].data, + props: { childrenIds: childrenIds }, + }, + }, + }); + setSelectedBlockId(blockId); + }} + /> + + ); +} diff --git a/packages/editor-sample/src/documents/blocks/Container/ContainerPropsSchema.tsx b/packages/editor-sample/src/documents/blocks/Container/ContainerPropsSchema.tsx index 3e353df..a0188f6 100644 --- a/packages/editor-sample/src/documents/blocks/Container/ContainerPropsSchema.tsx +++ b/packages/editor-sample/src/documents/blocks/Container/ContainerPropsSchema.tsx @@ -2,7 +2,7 @@ import { z } from 'zod'; import { ContainerPropsSchema as BaseContainerPropsSchema } from '@usewaypoint/block-container'; -export const ContainerPropsSchema = z.object({ +const ContainerPropsSchema = z.object({ style: BaseContainerPropsSchema.shape.style, props: z .object({ @@ -12,4 +12,6 @@ export const ContainerPropsSchema = z.object({ .nullable(), }); +export default ContainerPropsSchema; + export type ContainerProps = z.infer; diff --git a/packages/editor-sample/src/documents/blocks/Container/index.tsx b/packages/editor-sample/src/documents/blocks/Container/index.tsx deleted file mode 100644 index 0a7dba9..0000000 --- a/packages/editor-sample/src/documents/blocks/Container/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; - -import { Container as BaseContainer } from '@usewaypoint/block-container'; - -import { TEditorBlock } from '../../editor/core'; -import { useCurrentBlockId } from '../../editor/EditorBlock'; -import { setDocument, setSelectedBlockId, useDocument } from '../../editor/EditorContext'; -import ReaderBlock from '../../reader/ReaderBlock'; -import EditorChildrenIds from '../helpers/EditorChildrenIds'; - -import { ContainerProps } from './ContainerPropsSchema'; - -export function Container({ style, props }: ContainerProps) { - const childrenIds = props?.childrenIds ?? []; - return ( - - {childrenIds.map((childId) => ( - - ))} - - ); -} - -export function EditorContainer({ style, props }: ContainerProps) { - const childrenIds = props?.childrenIds ?? []; - - const document = useDocument(); - const blockId = useCurrentBlockId(); - - const insertBlock = (blockConfiguration: TEditorBlock, i: number | null) => { - const id = `block-${Date.now()}`; - let nChildrenIds: string[]; - if (i === null) { - nChildrenIds = [...childrenIds, id]; - } else { - nChildrenIds = [...childrenIds.slice(0, i), id, ...childrenIds.slice(i)]; - } - - setDocument({ - [id]: blockConfiguration, - [blockId]: { - type: 'Container', - data: { - ...document[blockId].data, - props: { childrenIds: nChildrenIds }, - }, - }, - }); - setSelectedBlockId(id); - }; - - return ( - - - - ); -} diff --git a/packages/editor-sample/src/documents/blocks/EmailLayout/EmailLayoutEditor.tsx b/packages/editor-sample/src/documents/blocks/EmailLayout/EmailLayoutEditor.tsx new file mode 100644 index 0000000..f08b067 --- /dev/null +++ b/packages/editor-sample/src/documents/blocks/EmailLayout/EmailLayoutEditor.tsx @@ -0,0 +1,96 @@ +import React from 'react'; + +import { useCurrentBlockId } from '../../editor/EditorBlock'; +import { setDocument, setSelectedBlockId, useDocument } from '../../editor/EditorContext'; +import EditorChildrenIds from '../helpers/EditorChildrenIds'; + +import { EmailLayoutProps } from './EmailLayoutPropsSchema'; + +function getFontFamily(fontFamily: EmailLayoutProps['fontFamily']) { + const f = fontFamily ?? 'MODERN_SANS'; + switch (f) { + case 'MODERN_SANS': + return '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif'; + case 'BOOK_SANS': + return 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif'; + case 'ORGANIC_SANS': + return 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif'; + case 'GEOMETRIC_SANS': + return 'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif'; + case 'HEAVY_SANS': + return 'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif'; + case 'ROUNDED_SANS': + return 'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif'; + case 'MODERN_SERIF': + return 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif'; + case 'BOOK_SERIF': + return '"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif'; + case 'MONOSPACE': + return '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace'; + } +} + +export default function EmailLayoutEditor(props: EmailLayoutProps) { + const childrenIds = props.childrenIds ?? []; + const document = useDocument(); + const currentBlockId = useCurrentBlockId(); + + return ( +
{ + setSelectedBlockId(null); + }} + style={{ + backgroundColor: props.backdropColor ?? '#EEEEEE', + color: props.textColor ?? '#242424', + fontFamily: getFontFamily(props.fontFamily), + fontSize: '16px', + fontWeight: '400', + letterSpacing: '0.15008px', + lineHeight: '1.5', + margin: '0', + padding: '32px 0', + minHeight: '100%', + width: '100%', + height: '100%', + }} + > + + + + + + +
+ { + setDocument({ + [blockId]: block, + [currentBlockId]: { + type: 'Container', + data: { + ...document[currentBlockId].data, + props: { childrenIds: childrenIds }, + }, + }, + }); + setSelectedBlockId(blockId); + }} + /> +
+
+ ); +} diff --git a/packages/editor-sample/src/documents/blocks/EmailLayout/EmailLayoutPropsSchema.tsx b/packages/editor-sample/src/documents/blocks/EmailLayout/EmailLayoutPropsSchema.tsx index d3e7fdf..5eef514 100644 --- a/packages/editor-sample/src/documents/blocks/EmailLayout/EmailLayoutPropsSchema.tsx +++ b/packages/editor-sample/src/documents/blocks/EmailLayout/EmailLayoutPropsSchema.tsx @@ -1,11 +1,34 @@ import { z } from 'zod'; -import { zColor, zFontFamily } from '../helpers/zod'; - -export const EmailLayoutPropsSchema = z.object({ - backdropColor: zColor(), - canvasColor: zColor(), - textColor: zColor(), - fontFamily: zFontFamily().default('MODERN_SANS'), - childrenIds: z.array(z.string()), +const COLOR_SCHEMA = z + .string() + .regex(/^#[0-9a-fA-F]{6}$/) + .nullable() + .optional(); + +const FONT_FAMILY_SCHEMA = z + .enum([ + 'MODERN_SANS', + 'BOOK_SANS', + 'ORGANIC_SANS', + 'GEOMETRIC_SANS', + 'HEAVY_SANS', + 'ROUNDED_SANS', + 'MODERN_SERIF', + 'BOOK_SERIF', + 'MONOSPACE', + ]) + .nullable() + .optional(); + +const EmailLayoutPropsSchema = z.object({ + backdropColor: COLOR_SCHEMA, + canvasColor: COLOR_SCHEMA, + textColor: COLOR_SCHEMA, + fontFamily: FONT_FAMILY_SCHEMA, + childrenIds: z.array(z.string()).optional().nullable(), }); + +export default EmailLayoutPropsSchema; + +export type EmailLayoutProps = z.infer; diff --git a/packages/editor-sample/src/documents/blocks/EmailLayout/index.tsx b/packages/editor-sample/src/documents/blocks/EmailLayout/index.tsx deleted file mode 100644 index 58c8074..0000000 --- a/packages/editor-sample/src/documents/blocks/EmailLayout/index.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React, { CSSProperties } from 'react'; -import { z } from 'zod'; - -import { TEditorBlock } from '../../editor/core'; -import { useCurrentBlockId } from '../../editor/EditorBlock'; -import { setDocument, setSelectedBlockId, useDocument } from '../../editor/EditorContext'; -import ReaderBlock from '../../reader/ReaderBlock'; -import EditorChildrenIds from '../helpers/EditorChildrenIds'; - -import { EmailLayoutPropsSchema } from './EmailLayoutPropsSchema'; - -// Based on https://modernfontstacks.com/#font-stacks -const FONT_FAMILY_MAPPINGS = { - MODERN_SANS: '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif', - BOOK_SANS: 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif', - ORGANIC_SANS: 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif', - GEOMETRIC_SANS: 'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif', - HEAVY_SANS: - 'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif', - ROUNDED_SANS: - 'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif', - MODERN_SERIF: 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif', - BOOK_SERIF: '"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif', - MONOSPACE: '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace', -}; - -export type EmailLayoutProps = z.infer; - -export function EmailLayout(props: EmailLayoutProps) { - return ( - - {props.childrenIds.map((childId) => ( - - ))} - - ); -} - -export function EditorEmailLayout(props: EmailLayoutProps) { - const document = useDocument(); - const blockId = useCurrentBlockId(); - const childrenIds = props.childrenIds; - - const insertBlock = (blockConfiguration: TEditorBlock, i: number | null) => { - const id = `block-${Date.now()}`; - const getChildrenIds = () => { - if (i === null) { - return [...childrenIds, id]; - } - return [...childrenIds.slice(0, i), id, ...childrenIds.slice(i)]; - }; - - setDocument({ - [id]: blockConfiguration, - [blockId]: { - type: 'EmailLayout', - data: { - ...document[blockId].data, - childrenIds: getChildrenIds(), - }, - } as TEditorBlock, - }); - setSelectedBlockId(id); - }; - - return ( -
{ - setSelectedBlockId(null); - }} - > - - - -
- ); -} - -type SharedLayoutProps = EmailLayoutProps & { - children: JSX.Element[] | JSX.Element; -}; -function SharedLayout({ children, fontFamily, backdropColor, textColor, canvasColor }: SharedLayoutProps) { - const backdropStyle: CSSProperties = { - backgroundColor: backdropColor, - color: textColor, - fontFamily: FONT_FAMILY_MAPPINGS[fontFamily], - fontSize: '16px', - fontWeight: '400', - letterSpacing: '0.15008px', - lineHeight: '1.5', - margin: '0', - padding: '32px 0', - minHeight: '100%', - width: '100%', - }; - const canvasStyle: CSSProperties = { - margin: '0 auto', - maxWidth: '600px', - backgroundColor: canvasColor, - }; - return ( -
- - - - - - -
{children}
-
- ); -} diff --git a/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/index.tsx b/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/index.tsx index fbee88c..a07c17d 100644 --- a/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/index.tsx +++ b/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/index.tsx @@ -5,11 +5,45 @@ import EditorBlock from '../../../editor/EditorBlock'; import AddBlockButton from './AddBlockMenu'; -type Props = { +export type EditorChildrenChange = { + blockId: string; + block: TEditorBlock; childrenIds: string[]; - insertBlock: (block: TEditorBlock, index: number | null) => void; }; -export default function EditorChildrenIds({ childrenIds, insertBlock }: Props) { + +function generateId() { + return `block-${Date.now()}`; +} + +export type EditorChildrenIdsProps = { + childrenIds: string[] | null | undefined; + onChange: (val: EditorChildrenChange) => void; +}; +export default function EditorChildrenIds({ childrenIds, onChange }: EditorChildrenIdsProps) { + const appendBlock = (block: TEditorBlock) => { + const blockId = generateId(); + return onChange({ + blockId, + block, + childrenIds: [...(childrenIds || []), blockId], + }); + }; + + const insertBlock = (block: TEditorBlock, index: number) => { + const blockId = generateId(); + const newChildrenIds = [...(childrenIds || [])]; + newChildrenIds.splice(index, 0, blockId); + return onChange({ + blockId, + block, + childrenIds: newChildrenIds, + }); + }; + + if (!childrenIds) { + return ; + } + return ( <> {childrenIds.map((childId, i) => ( @@ -18,7 +52,7 @@ export default function EditorChildrenIds({ childrenIds, insertBlock }: Props) { ))} - insertBlock(block, null)} /> + ); } diff --git a/packages/editor-sample/src/documents/blocks/helpers/block-wrappers/TuneMenu.tsx b/packages/editor-sample/src/documents/blocks/helpers/block-wrappers/TuneMenu.tsx index 50429f1..27c3e69 100644 --- a/packages/editor-sample/src/documents/blocks/helpers/block-wrappers/TuneMenu.tsx +++ b/packages/editor-sample/src/documents/blocks/helpers/block-wrappers/TuneMenu.tsx @@ -21,6 +21,12 @@ export default function TuneMenu({ blockId }: Props) { const document = useDocument(); const handleDeleteClick = () => { + const filterChildrenIds = (childrenIds: string[] | null | undefined) => { + if (!childrenIds) { + return childrenIds; + } + return childrenIds.filter((f) => f !== blockId); + }; const nDocument: typeof document = { ...document }; for (const [id, b] of Object.entries(nDocument)) { const block = b as TEditorBlock; @@ -33,7 +39,7 @@ export default function TuneMenu({ blockId }: Props) { ...block, data: { ...block.data, - childrenIds: block.data.childrenIds.filter((f) => f !== blockId), + childrenIds: filterChildrenIds(block.data.childrenIds), }, }; break; @@ -44,7 +50,7 @@ export default function TuneMenu({ blockId }: Props) { ...block.data, props: { ...block.data.props, - childrenIds: (block.data.props?.childrenIds ?? []).filter((f) => f !== blockId), + childrenIds: filterChildrenIds(block.data.props?.childrenIds), }, }, }; @@ -57,7 +63,7 @@ export default function TuneMenu({ blockId }: Props) { props: { ...block.data.props, columns: block.data.props?.columns?.map((c) => ({ - childrenIds: c.childrenIds.filter((f) => f !== blockId), + childrenIds: filterChildrenIds(c.childrenIds), })), }, } as ColumnsContainerProps, diff --git a/packages/editor-sample/src/documents/editor/core.tsx b/packages/editor-sample/src/documents/editor/core.tsx index 056119d..4db26f1 100644 --- a/packages/editor-sample/src/documents/editor/core.tsx +++ b/packages/editor-sample/src/documents/editor/core.tsx @@ -15,12 +15,12 @@ import { buildBlockConfigurationSchema, } from '@usewaypoint/document-core'; -import { EditorColumnsContainer } from '../blocks/ColumnsContainer'; +import ColumnsContainerEditor from '../blocks/ColumnsContainer/ColumnsContainerEditor'; import ColumnsContainerPropsSchema from '../blocks/ColumnsContainer/ColumnsContainerPropsSchema'; -import { EditorContainer } from '../blocks/Container'; -import { ContainerPropsSchema } from '../blocks/Container/ContainerPropsSchema'; -import { EditorEmailLayout } from '../blocks/EmailLayout'; -import { EmailLayoutPropsSchema } from '../blocks/EmailLayout/EmailLayoutPropsSchema'; +import ContainerEditor from '../blocks/Container/ContainerEditor'; +import ContainerPropsSchema from '../blocks/Container/ContainerPropsSchema'; +import EmailLayoutEditor from '../blocks/EmailLayout/EmailLayoutEditor'; +import EmailLayoutPropsSchema from '../blocks/EmailLayout/EmailLayoutPropsSchema'; import EditorBlockWrapper from '../blocks/helpers/block-wrappers/EditorBlockWrapper'; const EDITOR_DICTIONARY = buildBlockConfigurationDictionary({ @@ -44,7 +44,7 @@ const EDITOR_DICTIONARY = buildBlockConfigurationDictionary({ schema: ContainerPropsSchema, Component: (props) => ( - + ), }, @@ -52,7 +52,7 @@ const EDITOR_DICTIONARY = buildBlockConfigurationDictionary({ schema: ColumnsContainerPropsSchema, Component: (props) => ( - + ), }, @@ -99,11 +99,7 @@ const EDITOR_DICTIONARY = buildBlockConfigurationDictionary({ }, EmailLayout: { schema: EmailLayoutPropsSchema, - Component: (p) => ( -
- -
- ), + Component: (p) => , }, Spacer: { schema: SpacerPropsSchema, diff --git a/packages/editor-sample/src/documents/reader/ReaderBlock.tsx b/packages/editor-sample/src/documents/reader/ReaderBlock.tsx deleted file mode 100644 index 494518a..0000000 --- a/packages/editor-sample/src/documents/reader/ReaderBlock.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import { ReaderBlock as CoreReaderBlock } from './core'; -import { useReaderDocument } from './ReaderContext'; - -type ReaderBlockProps = { - id: string; -}; - -/** - * @param id - Block id - * @returns ReaderBlock component that loads data from the ReaderDocumentContext - */ -export default function ReaderBlock({ id }: ReaderBlockProps) { - const document = useReaderDocument(); - const block = document[id]; - if (!block) { - throw new Error('Could not find block'); - } - return ; -} diff --git a/packages/editor-sample/src/documents/reader/ReaderContext.tsx b/packages/editor-sample/src/documents/reader/ReaderContext.tsx deleted file mode 100644 index 6982c8f..0000000 --- a/packages/editor-sample/src/documents/reader/ReaderContext.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { createContext, useContext } from 'react'; - -import { TReaderDocument } from './core'; - -const ReaderContext = createContext({}); - -export function useReaderDocument() { - return useContext(ReaderContext); -} - -type ReaderProviderProps = { - value: TReaderDocument; - children: Parameters[0]['children']; -}; -export function ReaderProvider({ value, children }: ReaderProviderProps) { - return {children}; -} diff --git a/packages/email-builder/.npmignore b/packages/email-builder/.npmignore new file mode 100644 index 0000000..564b640 --- /dev/null +++ b/packages/email-builder/.npmignore @@ -0,0 +1,9 @@ +.editorconfig +.envrc +.eslintignore +.eslintrc.json +.prettierrc +jest.config.ts +src +tests +tsconfig.json \ No newline at end of file diff --git a/packages/email-builder/LICENSE b/packages/email-builder/LICENSE new file mode 100644 index 0000000..5a09b83 --- /dev/null +++ b/packages/email-builder/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 Carlos Rodriguez-Rosario + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/email-builder/README.md b/packages/email-builder/README.md new file mode 100644 index 0000000..dec98dd --- /dev/null +++ b/packages/email-builder/README.md @@ -0,0 +1 @@ +# usewaypoint/email-builder diff --git a/packages/email-builder/package-lock.json b/packages/email-builder/package-lock.json new file mode 100644 index 0000000..1fefc1a --- /dev/null +++ b/packages/email-builder/package-lock.json @@ -0,0 +1,168 @@ +{ + "name": "@usewaypoint/email-builder", + "version": "0.0.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@usewaypoint/email-builder", + "version": "0.0.2", + "license": "MIT", + "dependencies": { + "@usewaypoint/block-avatar": "^0.0.1", + "@usewaypoint/block-button": "^0.0.2", + "@usewaypoint/block-columns-container": "^0.0.2", + "@usewaypoint/block-container": "^0.0.1", + "@usewaypoint/block-divider": "^0.0.3", + "@usewaypoint/block-heading": "^0.0.2", + "@usewaypoint/block-html": "^0.0.2", + "@usewaypoint/block-image": "^0.0.4", + "@usewaypoint/block-spacer": "^0.0.2", + "@usewaypoint/block-text": "^0.0.2", + "@usewaypoint/document-core": "^0.0.4" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/@usewaypoint/block-avatar": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@usewaypoint/block-avatar/-/block-avatar-0.0.1.tgz", + "integrity": "sha512-RHpynXK6iLbejoZN8k+OXIaJGU22LZWHWcW9nEdsHwPaHxSbwh7UPcBrTntC5zRqMYDZlF7u3iYpSg02O4bxRQ==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/@usewaypoint/block-button": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@usewaypoint/block-button/-/block-button-0.0.2.tgz", + "integrity": "sha512-WzWlJoJBiVfI3Iak9JPey7u/hJIkxXuhuAI6Y10ef795Panyr1GxQbdH2//fZtajjGJ1SjJStXpz6nPEd8wArg==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/@usewaypoint/block-columns-container": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@usewaypoint/block-columns-container/-/block-columns-container-0.0.2.tgz", + "integrity": "sha512-W5rstBOb/uLJ0L43zilUbQEoc+H+2jA8L8RVlCt4unvZsaqYGdcyaqZ6bs6qpeHjB2LtzLvFEcaqSuevhg3uRQ==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/@usewaypoint/block-container": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@usewaypoint/block-container/-/block-container-0.0.1.tgz", + "integrity": "sha512-DsxXVVKLWv86CbQhCAbnvRDe/wJ0sj5XNma5hb0l1p7YqK42NVxKCRPF1Bw6m2WhEcXZFbz+8CIxHqjo6p1QIw==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/@usewaypoint/block-divider": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@usewaypoint/block-divider/-/block-divider-0.0.3.tgz", + "integrity": "sha512-ZtjBWVakxUg+YbI0yWIX/TO0mmj8pjSUqK/o6Je5bx+rfETXaWlxRTwhZGWMNnYeY1sTjIlcbg2zGgdmmgxvXg==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/@usewaypoint/block-heading": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@usewaypoint/block-heading/-/block-heading-0.0.2.tgz", + "integrity": "sha512-l5tCcyfYAp+sta93ZXs1pmz+F/3W6XP2aiyAr/6uVpV2mwL7MloWUlpHRzwZEEyIW/jIJwYkNexabRLrxK181A==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/@usewaypoint/block-html": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@usewaypoint/block-html/-/block-html-0.0.2.tgz", + "integrity": "sha512-QEaMF1DRk+MgJiC8s4TfQe4eBpJghzriIPyS/+hxRVAiDwbOmRencaPRZ4JpDBi34sVNwLvRJZa5KvUFN4w6hg==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/@usewaypoint/block-image": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@usewaypoint/block-image/-/block-image-0.0.4.tgz", + "integrity": "sha512-QNslGaV4zrgXj+VtbAktT7QBUxoelIyDMOgQtF8HsZzkfl66S28MtwwofodQpzKRtUrtegQ9o9hpL5zgjQUv0w==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/@usewaypoint/block-spacer": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@usewaypoint/block-spacer/-/block-spacer-0.0.2.tgz", + "integrity": "sha512-pH+QFmE2e0ULZeEGwh70qOtSKBdvmq/yue3IVPzx05fHymjr3fV/os1eSgEN2SRCcOC02wu3MvFX5LbV+vz4sQ==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/@usewaypoint/block-text": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@usewaypoint/block-text/-/block-text-0.0.2.tgz", + "integrity": "sha512-iX4hxq/Rql5syTuXQYB3BGm6vZDKns83FUHH66y06pQuSLPR7i0YvokOLm2Ez96S+i6NOh3XVTg/wcx2Hg2FgA==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/@usewaypoint/document-core": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@usewaypoint/document-core/-/document-core-0.0.4.tgz", + "integrity": "sha512-xlPM5zexUhuRDsSwPVZ8cvemmsffLDb4wxlaAVMXi4ayO7ZHHrc9SiLiBkR8cnfQ3OvnHlZaO8YDsGmnckGqYw==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/packages/email-builder/package.json b/packages/email-builder/package.json new file mode 100644 index 0000000..cdac964 --- /dev/null +++ b/packages/email-builder/package.json @@ -0,0 +1,33 @@ +{ + "name": "@usewaypoint/email-builder", + "version": "0.0.2", + "description": "React component to render email messages", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "target": "ES2022", + "files": [ + "dist" + ], + "scripts": { + "build": "npx tsc" + }, + "author": "carlos@usewaypoint.com", + "license": "MIT", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "zod": "^1 || ^2 || ^3" + }, + "dependencies": { + "@usewaypoint/block-avatar": "^0.0.1", + "@usewaypoint/block-button": "^0.0.2", + "@usewaypoint/block-columns-container": "^0.0.2", + "@usewaypoint/block-container": "^0.0.1", + "@usewaypoint/block-divider": "^0.0.3", + "@usewaypoint/block-heading": "^0.0.2", + "@usewaypoint/block-html": "^0.0.2", + "@usewaypoint/block-image": "^0.0.4", + "@usewaypoint/block-spacer": "^0.0.2", + "@usewaypoint/block-text": "^0.0.2", + "@usewaypoint/document-core": "^0.0.4" + } +} diff --git a/packages/editor-sample/src/documents/reader/core.tsx b/packages/email-builder/src/Reader/core.tsx similarity index 62% rename from packages/editor-sample/src/documents/reader/core.tsx rename to packages/email-builder/src/Reader/core.tsx index f40582b..7a98b23 100644 --- a/packages/editor-sample/src/documents/reader/core.tsx +++ b/packages/email-builder/src/Reader/core.tsx @@ -1,3 +1,4 @@ +import React, { createContext, useContext } from 'react'; import { z } from 'zod'; import { Avatar, AvatarPropsSchema } from '@usewaypoint/block-avatar'; @@ -14,14 +15,33 @@ import { buildBlockConfigurationSchema, } from '@usewaypoint/document-core'; -import { ColumnsContainer } from '../blocks/ColumnsContainer'; import ColumnsContainerPropsSchema from '../blocks/ColumnsContainer/ColumnsContainerPropsSchema'; -import { Container } from '../blocks/Container'; +import ColumnsContainerReader from '../blocks/ColumnsContainer/ColumnsContainerReader'; import { ContainerPropsSchema } from '../blocks/Container/ContainerPropsSchema'; -import { EmailLayout } from '../blocks/EmailLayout'; +import ContainerReader from '../blocks/Container/ContainerReader'; import { EmailLayoutPropsSchema } from '../blocks/EmailLayout/EmailLayoutPropsSchema'; +import EmailLayoutReader from '../blocks/EmailLayout/EmailLayoutReader'; + +const ReaderContext = createContext({}); + +function useReaderDocument() { + return useContext(ReaderContext); +} const READER_DICTIONARY = buildBlockConfigurationDictionary({ + ColumnsContainer: { + schema: ColumnsContainerPropsSchema, + Component: ColumnsContainerReader, + }, + Container: { + schema: ContainerPropsSchema, + Component: ContainerReader, + }, + EmailLayout: { + schema: EmailLayoutPropsSchema, + Component: EmailLayoutReader, + }, + // Avatar: { schema: AvatarPropsSchema, Component: Avatar, @@ -30,14 +50,6 @@ const READER_DICTIONARY = buildBlockConfigurationDictionary({ schema: ButtonPropsSchema, Component: Button, }, - ColumnsContainer: { - schema: ColumnsContainerPropsSchema, - Component: ColumnsContainer, - }, - Container: { - schema: ContainerPropsSchema, - Component: Container, - }, Divider: { schema: DividerPropsSchema, Component: Divider, @@ -54,25 +66,35 @@ const READER_DICTIONARY = buildBlockConfigurationDictionary({ schema: ImagePropsSchema, Component: Image, }, - Text: { - schema: TextPropsSchema, - Component: Text, - }, - EmailLayout: { - schema: EmailLayoutPropsSchema, - Component: EmailLayout, - }, Spacer: { schema: SpacerPropsSchema, Component: Spacer, }, + Text: { + schema: TextPropsSchema, + Component: Text, + }, }); const ReaderBlockSchema = buildBlockConfigurationSchema(READER_DICTIONARY); -const ReaderDocumentSchema = z.record(z.string(), ReaderBlockSchema); +export const ReaderDocumentSchema = z.record(z.string(), ReaderBlockSchema); -export const ReaderBlock = buildBlockComponent(READER_DICTIONARY); +const BaseReaderBlock = buildBlockComponent(READER_DICTIONARY); +export type TReaderBlockProps = { id: string }; +export function ReaderBlock({ id }: TReaderBlockProps) { + const document = useReaderDocument(); + return ; +} export type TReaderDocument = Record>; - -export default ReaderDocumentSchema; +export type TReaderProps = { + document: Record>; + rootBlockId: string; +}; +export default function Reader({ document, rootBlockId }: TReaderProps) { + return ( + + + + ); +} diff --git a/packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerPropsSchema.ts b/packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerPropsSchema.ts new file mode 100644 index 0000000..5ea3185 --- /dev/null +++ b/packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerPropsSchema.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +import { ColumnsContainerPropsSchema as BaseColumnsContainerPropsSchema } from '@usewaypoint/block-columns-container'; + +const BasePropsShape = BaseColumnsContainerPropsSchema.shape.props.unwrap().unwrap().shape; + +const ColumnsContainerPropsSchema = z.object({ + style: BaseColumnsContainerPropsSchema.shape.style, + props: z + .object({ + ...BasePropsShape, + columns: z.tuple([ + z.object({ childrenIds: z.array(z.string()) }), + z.object({ childrenIds: z.array(z.string()) }), + z.object({ childrenIds: z.array(z.string()) }), + ]), + }) + .optional() + .nullable(), +}); + +export default ColumnsContainerPropsSchema; +export type ColumnsContainerProps = z.infer; diff --git a/packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerReader.tsx b/packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerReader.tsx new file mode 100644 index 0000000..4310faf --- /dev/null +++ b/packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerReader.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import { ColumnsContainer as BaseColumnsContainer } from '@usewaypoint/block-columns-container'; + +import { ReaderBlock } from '../../Reader/core'; + +import { ColumnsContainerProps } from './ColumnsContainerPropsSchema'; + +export default function ColumnsContainerReader({ style, props }: ColumnsContainerProps) { + const { columns, ...restProps } = props ?? {}; + let cols = undefined; + if (columns) { + cols = columns.map((col) => col.childrenIds.map((childId) => )); + } + + return ; +} diff --git a/packages/email-builder/src/blocks/Container/ContainerPropsSchema.tsx b/packages/email-builder/src/blocks/Container/ContainerPropsSchema.tsx new file mode 100644 index 0000000..3e353df --- /dev/null +++ b/packages/email-builder/src/blocks/Container/ContainerPropsSchema.tsx @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +import { ContainerPropsSchema as BaseContainerPropsSchema } from '@usewaypoint/block-container'; + +export const ContainerPropsSchema = z.object({ + style: BaseContainerPropsSchema.shape.style, + props: z + .object({ + childrenIds: z.array(z.string()).optional().nullable(), + }) + .optional() + .nullable(), +}); + +export type ContainerProps = z.infer; diff --git a/packages/email-builder/src/blocks/Container/ContainerReader.tsx b/packages/email-builder/src/blocks/Container/ContainerReader.tsx new file mode 100644 index 0000000..7925e3d --- /dev/null +++ b/packages/email-builder/src/blocks/Container/ContainerReader.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { Container as BaseContainer } from '@usewaypoint/block-container'; + +import { ReaderBlock } from '../../Reader/core'; + +import { ContainerProps } from './ContainerPropsSchema'; + +export default function ContainerReader({ style, props }: ContainerProps) { + const childrenIds = props?.childrenIds ?? []; + return ( + + {childrenIds.map((childId) => ( + + ))} + + ); +} diff --git a/packages/email-builder/src/blocks/EmailLayout/EmailLayoutPropsSchema.tsx b/packages/email-builder/src/blocks/EmailLayout/EmailLayoutPropsSchema.tsx new file mode 100644 index 0000000..d06e5da --- /dev/null +++ b/packages/email-builder/src/blocks/EmailLayout/EmailLayoutPropsSchema.tsx @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +const COLOR_SCHEMA = z + .string() + .regex(/^#[0-9a-fA-F]{6}$/) + .nullable() + .optional(); + +const FONT_FAMILY_SCHEMA = z + .enum([ + 'MODERN_SANS', + 'BOOK_SANS', + 'ORGANIC_SANS', + 'GEOMETRIC_SANS', + 'HEAVY_SANS', + 'ROUNDED_SANS', + 'MODERN_SERIF', + 'BOOK_SERIF', + 'MONOSPACE', + ]) + .nullable() + .optional(); + +export const EmailLayoutPropsSchema = z.object({ + backdropColor: COLOR_SCHEMA, + canvasColor: COLOR_SCHEMA, + textColor: COLOR_SCHEMA, + fontFamily: FONT_FAMILY_SCHEMA, + childrenIds: z.array(z.string()).optional().nullable(), +}); + +export type EmailLayoutProps = z.infer; diff --git a/packages/email-builder/src/blocks/EmailLayout/EmailLayoutReader.tsx b/packages/email-builder/src/blocks/EmailLayout/EmailLayoutReader.tsx new file mode 100644 index 0000000..d10d428 --- /dev/null +++ b/packages/email-builder/src/blocks/EmailLayout/EmailLayoutReader.tsx @@ -0,0 +1,74 @@ +import React from 'react'; + +import { ReaderBlock } from '../../Reader/core'; + +import { EmailLayoutProps } from './EmailLayoutPropsSchema'; + +function getFontFamily(fontFamily: EmailLayoutProps['fontFamily']) { + const f = fontFamily ?? 'MODERN_SANS'; + switch (f) { + case 'MODERN_SANS': + return '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif'; + case 'BOOK_SANS': + return 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif'; + case 'ORGANIC_SANS': + return 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif'; + case 'GEOMETRIC_SANS': + return 'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif'; + case 'HEAVY_SANS': + return 'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif'; + case 'ROUNDED_SANS': + return 'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif'; + case 'MODERN_SERIF': + return 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif'; + case 'BOOK_SERIF': + return '"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif'; + case 'MONOSPACE': + return '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace'; + } +} + +export default function EmailLayoutReader(props: EmailLayoutProps) { + const childrenIds = props.childrenIds ?? []; + return ( +
+ + + + + + +
+ {childrenIds.map((childId) => ( + + ))} +
+
+ ); +} diff --git a/packages/email-builder/tsconfig.build.json b/packages/email-builder/tsconfig.build.json new file mode 100644 index 0000000..9dabb8e --- /dev/null +++ b/packages/email-builder/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["tests/**/*.spec.ts", "tests/**/*.spec.tsx", "jest.config.ts"] +} diff --git a/packages/email-builder/tsconfig.json b/packages/email-builder/tsconfig.json new file mode 100644 index 0000000..efac89d --- /dev/null +++ b/packages/email-builder/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "es2015", + "module": "esnext", + "outDir": "dist" + }, + "exclude": ["dist"] +}