diff --git a/.changeset/fifty-lemons-work.md b/.changeset/fifty-lemons-work.md
new file mode 100644
index 0000000000..3d4ae3fb3e
--- /dev/null
+++ b/.changeset/fifty-lemons-work.md
@@ -0,0 +1,8 @@
+---
+"@comet/cms-admin": minor
+"@comet/blocks-api": minor
+"@comet/cms-site": minor
+"@comet/cms-api": minor
+---
+
+Add PhoneLinkBlock and EmailLinkBlock
diff --git a/demo/admin/src/common/blocks/LinkBlock.tsx b/demo/admin/src/common/blocks/LinkBlock.tsx
index c81fcff1a7..3e6ac3173f 100644
--- a/demo/admin/src/common/blocks/LinkBlock.tsx
+++ b/demo/admin/src/common/blocks/LinkBlock.tsx
@@ -1,4 +1,4 @@
-import { createLinkBlock, DamFileDownloadLinkBlock, ExternalLinkBlock, InternalLinkBlock } from "@comet/cms-admin";
+import { createLinkBlock, DamFileDownloadLinkBlock, EmailLinkBlock, ExternalLinkBlock, InternalLinkBlock, PhoneLinkBlock } from "@comet/cms-admin";
import { NewsLinkBlock } from "@src/news/blocks/NewsLinkBlock";
export const LinkBlock = createLinkBlock({
@@ -7,5 +7,7 @@ export const LinkBlock = createLinkBlock({
external: ExternalLinkBlock,
news: NewsLinkBlock,
damFileDownload: DamFileDownloadLinkBlock,
+ email: EmailLinkBlock,
+ phone: PhoneLinkBlock,
},
});
diff --git a/demo/api/block-meta.json b/demo/api/block-meta.json
index 225dacfe22..87afb21b36 100644
--- a/demo/api/block-meta.json
+++ b/demo/api/block-meta.json
@@ -448,6 +448,23 @@
}
]
},
+ {
+ "name": "EmailLink",
+ "fields": [
+ {
+ "name": "email",
+ "kind": "String",
+ "nullable": true
+ }
+ ],
+ "inputFields": [
+ {
+ "name": "email",
+ "kind": "String",
+ "nullable": true
+ }
+ ]
+ },
{
"name": "ExternalLink",
"fields": [
@@ -796,7 +813,9 @@
"internal": "InternalLink",
"external": "ExternalLink",
"news": "NewsLink",
- "damFileDownload": "DamFileDownloadLink"
+ "damFileDownload": "DamFileDownloadLink",
+ "phone": "PhoneLink",
+ "email": "EmailLink"
},
"nullable": false
}
@@ -826,7 +845,9 @@
"internal": "InternalLink",
"external": "ExternalLink",
"news": "NewsLink",
- "damFileDownload": "DamFileDownloadLink"
+ "damFileDownload": "DamFileDownloadLink",
+ "phone": "PhoneLink",
+ "email": "EmailLink"
},
"nullable": false
}
@@ -1259,6 +1280,23 @@
}
]
},
+ {
+ "name": "PhoneLink",
+ "fields": [
+ {
+ "name": "phone",
+ "kind": "String",
+ "nullable": false
+ }
+ ],
+ "inputFields": [
+ {
+ "name": "phone",
+ "kind": "String",
+ "nullable": false
+ }
+ ]
+ },
{
"name": "PixelImage",
"fields": [
@@ -2148,7 +2186,7 @@
{
"name": "youtubeIdentifier",
"kind": "String",
- "nullable": false
+ "nullable": true
},
{
"name": "aspectRatio",
diff --git a/demo/api/src/common/blocks/linkBlock/link.block.ts b/demo/api/src/common/blocks/linkBlock/link.block.ts
index 5df9b0eb2c..f1b4163feb 100644
--- a/demo/api/src/common/blocks/linkBlock/link.block.ts
+++ b/demo/api/src/common/blocks/linkBlock/link.block.ts
@@ -1,7 +1,14 @@
-import { ExternalLinkBlock } from "@comet/blocks-api";
+import { EmailLinkBlock, ExternalLinkBlock, PhoneLinkBlock } from "@comet/blocks-api";
import { createLinkBlock, DamFileDownloadLinkBlock, InternalLinkBlock } from "@comet/cms-api";
import { NewsLinkBlock } from "@src/news/blocks/news-link.block";
export const LinkBlock = createLinkBlock({
- supportedBlocks: { internal: InternalLinkBlock, external: ExternalLinkBlock, news: NewsLinkBlock, damFileDownload: DamFileDownloadLinkBlock },
+ supportedBlocks: {
+ internal: InternalLinkBlock,
+ external: ExternalLinkBlock,
+ news: NewsLinkBlock,
+ damFileDownload: DamFileDownloadLinkBlock,
+ phone: PhoneLinkBlock,
+ email: EmailLinkBlock,
+ },
});
diff --git a/demo/site/src/blocks/LinkBlock.tsx b/demo/site/src/blocks/LinkBlock.tsx
index ba7a379d5b..f24a3bdc45 100644
--- a/demo/site/src/blocks/LinkBlock.tsx
+++ b/demo/site/src/blocks/LinkBlock.tsx
@@ -1,8 +1,10 @@
import {
DamFileDownloadLinkBlock,
+ EmailLinkBlock,
ExternalLinkBlock,
InternalLinkBlock,
OneOfBlock,
+ PhoneLinkBlock,
PropsWithData,
SupportedBlocks,
withPreview,
@@ -32,6 +34,16 @@ const supportedBlocks: SupportedBlocks = {
{children}
),
+ email: ({ children, title, ...props }) => (
+
+ {children}
+
+ ),
+ phone: ({ children, title, ...props }) => (
+
+ {children}
+
+ ),
};
interface LinkBlockProps extends PropsWithData {
diff --git a/packages/admin/cms-admin/src/blocks/EmailLinkBlock.tsx b/packages/admin/cms-admin/src/blocks/EmailLinkBlock.tsx
new file mode 100644
index 0000000000..4f22207523
--- /dev/null
+++ b/packages/admin/cms-admin/src/blocks/EmailLinkBlock.tsx
@@ -0,0 +1,80 @@
+import { Field, FinalFormInput } from "@comet/admin";
+import { BlockCategory, BlockInterface, BlocksFinalForm, createBlockSkeleton, SelectPreviewComponent } from "@comet/blocks-admin";
+import * as React from "react";
+import { FormattedMessage } from "react-intl";
+
+interface EmailLinkBlockData {
+ email?: string;
+}
+
+interface EmailLinkBlockInput {
+ email?: string;
+}
+
+export const EmailLinkBlock: BlockInterface = {
+ ...createBlockSkeleton(),
+
+ name: "Email",
+
+ displayName: ,
+
+ defaultValues: () => ({ email: undefined }),
+
+ category: BlockCategory.Navigation,
+
+ input2State: (state) => {
+ return state;
+ },
+
+ state2Output: (state) => {
+ return {
+ email: state.email,
+ };
+ },
+
+ output2State: async (output) => {
+ return {
+ email: output.email,
+ };
+ },
+
+ isValid: (state) => {
+ return state.email ? isEmail(state.email) : true;
+ },
+
+ AdminComponent: ({ state, updateState }) => {
+ return (
+
+ {
+ updateState((prevState) => ({ ...prevState, ...newState }));
+ }}
+ initialValues={state}
+ >
+ }
+ name="email"
+ component={FinalFormInput}
+ fullWidth
+ validate={(email: string) => {
+ if (email && !isEmail(email)) {
+ return ;
+ }
+ }}
+ />
+
+
+ );
+ },
+ previewContent: (state) => {
+ return state.email ? [{ type: "text", content: state.email }] : [];
+ },
+};
+
+const isEmail = (text: string) => {
+ return !!String(text)
+ .toLowerCase()
+ .match(
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
+ );
+};
diff --git a/packages/admin/cms-admin/src/blocks/PhoneLinkBlock.tsx b/packages/admin/cms-admin/src/blocks/PhoneLinkBlock.tsx
new file mode 100644
index 0000000000..e9cc596f24
--- /dev/null
+++ b/packages/admin/cms-admin/src/blocks/PhoneLinkBlock.tsx
@@ -0,0 +1,76 @@
+import { Field, FinalFormInput } from "@comet/admin";
+import { BlockCategory, BlockInterface, BlocksFinalForm, createBlockSkeleton, SelectPreviewComponent } from "@comet/blocks-admin";
+import * as React from "react";
+import { FormattedMessage } from "react-intl";
+
+interface PhoneLinkBlockData {
+ phone: string;
+}
+
+interface PhoneLinkBlockInput {
+ phone: string;
+}
+
+export const PhoneLinkBlock: BlockInterface = {
+ ...createBlockSkeleton(),
+
+ name: "Phone",
+
+ displayName: ,
+
+ defaultValues: () => ({ phone: "" }),
+
+ category: BlockCategory.Navigation,
+
+ input2State: (state) => {
+ return state;
+ },
+
+ state2Output: (state) => {
+ return {
+ phone: state.phone,
+ };
+ },
+
+ output2State: async (output) => {
+ return {
+ phone: output.phone,
+ };
+ },
+
+ isValid: (state) => {
+ return state.phone ? isPhone(state.phone) : true;
+ },
+
+ AdminComponent: ({ state, updateState }) => {
+ return (
+
+ {
+ updateState((prevState) => ({ ...prevState, ...newState }));
+ }}
+ initialValues={state}
+ >
+ }
+ name="phone"
+ component={FinalFormInput}
+ fullWidth
+ validate={(phone: string) => {
+ if (phone && !isPhone(phone)) {
+ return ;
+ }
+ }}
+ />
+
+
+ );
+ },
+ previewContent: (state) => {
+ return state.phone ? [{ type: "text", content: state.phone }] : [];
+ },
+};
+
+const isPhone = (text: string) => {
+ return !!String(text).match(/^[+]?[0-9]{4,20}$/im);
+};
diff --git a/packages/admin/cms-admin/src/index.ts b/packages/admin/cms-admin/src/index.ts
index 512dd1d371..08d8b0c16b 100644
--- a/packages/admin/cms-admin/src/index.ts
+++ b/packages/admin/cms-admin/src/index.ts
@@ -9,9 +9,11 @@ export type { TextImageBlockFactoryOptions } from "./blocks/createTextImageBlock
export { createTextImageBlock } from "./blocks/createTextImageBlock";
export { createTextLinkBlock } from "./blocks/createTextLinkBlock";
export { DamVideoBlock } from "./blocks/DamVideoBlock";
+export { EmailLinkBlock } from "./blocks/EmailLinkBlock";
export { ExternalLinkBlock } from "./blocks/ExternalLinkBlock";
export { EditImageDialog } from "./blocks/image/EditImageDialog";
export { InternalLinkBlock } from "./blocks/InternalLinkBlock";
+export { PhoneLinkBlock } from "./blocks/PhoneLinkBlock";
export { PixelImageBlock } from "./blocks/PixelImageBlock";
export { SvgImageBlock } from "./blocks/SvgImageBlock";
export { useCmsBlockContext } from "./blocks/useCmsBlockContext";
diff --git a/packages/api/blocks-api/block-meta.json b/packages/api/blocks-api/block-meta.json
index cc8f39e262..e90e7b04c1 100644
--- a/packages/api/blocks-api/block-meta.json
+++ b/packages/api/blocks-api/block-meta.json
@@ -1,4 +1,21 @@
[
+ {
+ "name": "EmailLink",
+ "fields": [
+ {
+ "name": "email",
+ "kind": "String",
+ "nullable": true
+ }
+ ],
+ "inputFields": [
+ {
+ "name": "email",
+ "kind": "String",
+ "nullable": true
+ }
+ ]
+ },
{
"name": "ExternalLink",
"fields": [
@@ -26,6 +43,23 @@
}
]
},
+ {
+ "name": "PhoneLink",
+ "fields": [
+ {
+ "name": "phone",
+ "kind": "String",
+ "nullable": false
+ }
+ ],
+ "inputFields": [
+ {
+ "name": "phone",
+ "kind": "String",
+ "nullable": false
+ }
+ ]
+ },
{
"name": "Space",
"fields": [
diff --git a/packages/api/blocks-api/src/blocks/email-link.block.ts b/packages/api/blocks-api/src/blocks/email-link.block.ts
new file mode 100644
index 0000000000..fdf93ed01d
--- /dev/null
+++ b/packages/api/blocks-api/src/blocks/email-link.block.ts
@@ -0,0 +1,22 @@
+import { IsEmail, IsOptional } from "class-validator";
+
+import { BlockData, BlockInput, createBlock, inputToData } from "./block";
+import { BlockField } from "./decorators/field";
+
+class EmailLinkBlockData extends BlockData {
+ @BlockField({ nullable: true })
+ email?: string;
+}
+
+class EmailLinkBlockInput extends BlockInput {
+ @IsOptional()
+ @IsEmail()
+ @BlockField({ nullable: true })
+ email?: string;
+
+ transformToBlockData(): EmailLinkBlockData {
+ return inputToData(EmailLinkBlockData, this);
+ }
+}
+
+export const EmailLinkBlock = createBlock(EmailLinkBlockData, EmailLinkBlockInput, "EmailLink");
diff --git a/packages/api/blocks-api/src/blocks/phone-link.block.ts b/packages/api/blocks-api/src/blocks/phone-link.block.ts
new file mode 100644
index 0000000000..fae94b243c
--- /dev/null
+++ b/packages/api/blocks-api/src/blocks/phone-link.block.ts
@@ -0,0 +1,21 @@
+import { IsString } from "class-validator";
+
+import { BlockData, BlockInput, createBlock, inputToData } from "./block";
+import { BlockField } from "./decorators/field";
+
+class PhoneLinkBlockData extends BlockData {
+ @BlockField()
+ phone: string;
+}
+
+class PhoneLinkBlockInput extends BlockInput {
+ @IsString()
+ @BlockField()
+ phone: string;
+
+ transformToBlockData(): PhoneLinkBlockData {
+ return inputToData(PhoneLinkBlockData, this);
+ }
+}
+
+export const PhoneLinkBlock = createBlock(PhoneLinkBlockData, PhoneLinkBlockInput, "PhoneLink");
diff --git a/packages/api/blocks-api/src/index.ts b/packages/api/blocks-api/src/index.ts
index 4e2024b2b1..16b20a8503 100644
--- a/packages/api/blocks-api/src/index.ts
+++ b/packages/api/blocks-api/src/index.ts
@@ -39,6 +39,7 @@ export { AnnotationBlockMeta, BlockField, getFieldKeys } from "./blocks/decorato
export { RootBlock } from "./blocks/decorators/root-block";
export { RootBlockEntity } from "./blocks/decorators/root-block-entity";
export { TransformDependencies } from "./blocks/dependencies";
+export { EmailLinkBlock } from "./blocks/email-link.block";
export { ExternalLinkBlock } from "./blocks/ExternalLinkBlock";
export { ColumnsBlockFactory } from "./blocks/factories/columns-block.factory";
export {
@@ -63,6 +64,7 @@ export { getMostSignificantPreviewImageUrlTemplate, getPreviewImageUrlTemplates
export { composeBlocks } from "./blocks/helpers/composeBlocks";
export { strictBlockDataFactoryDecorator } from "./blocks/helpers/strictBlockDataFactoryDecorator";
export { strictBlockInputFactoryDecorator } from "./blocks/helpers/strictBlockInputFactoryDecorator";
+export { PhoneLinkBlock } from "./blocks/phone-link.block";
export { SpaceBlock } from "./blocks/SpaceBlock/SpaceBlock";
export { transformToSaveIndex } from "./blocks/transformToSaveIndex/transformToSaveIndex";
export { YouTubeVideoBlock } from "./blocks/youtube-video.block";
diff --git a/packages/api/cms-api/block-meta.json b/packages/api/cms-api/block-meta.json
index 7dfdc23a4c..70bc695918 100644
--- a/packages/api/cms-api/block-meta.json
+++ b/packages/api/cms-api/block-meta.json
@@ -234,6 +234,23 @@
}
]
},
+ {
+ "name": "EmailLink",
+ "fields": [
+ {
+ "name": "email",
+ "kind": "String",
+ "nullable": true
+ }
+ ],
+ "inputFields": [
+ {
+ "name": "email",
+ "kind": "String",
+ "nullable": true
+ }
+ ]
+ },
{
"name": "ExternalLink",
"fields": [
@@ -335,7 +352,9 @@
"kind": "OneOfBlocks",
"blocks": {
"internal": "InternalLink",
- "external": "ExternalLink"
+ "external": "ExternalLink",
+ "email": "EmailLink",
+ "phone": "PhoneLink"
},
"nullable": false
}
@@ -363,7 +382,9 @@
"kind": "OneOfBlocks",
"blocks": {
"internal": "InternalLink",
- "external": "ExternalLink"
+ "external": "ExternalLink",
+ "email": "EmailLink",
+ "phone": "PhoneLink"
},
"nullable": false
}
@@ -414,6 +435,23 @@
}
]
},
+ {
+ "name": "PhoneLink",
+ "fields": [
+ {
+ "name": "phone",
+ "kind": "String",
+ "nullable": false
+ }
+ ],
+ "inputFields": [
+ {
+ "name": "phone",
+ "kind": "String",
+ "nullable": false
+ }
+ ]
+ },
{
"name": "PixelImage",
"fields": [
diff --git a/packages/api/cms-api/generate-block-meta.ts b/packages/api/cms-api/generate-block-meta.ts
index d2d28118f3..52921fb356 100644
--- a/packages/api/cms-api/generate-block-meta.ts
+++ b/packages/api/cms-api/generate-block-meta.ts
@@ -1,4 +1,4 @@
-import { createRichTextBlock, createTextLinkBlock, ExternalLinkBlock, getBlocksMeta } from "@comet/blocks-api";
+import { createRichTextBlock, createTextLinkBlock, EmailLinkBlock, ExternalLinkBlock, getBlocksMeta, PhoneLinkBlock } from "@comet/blocks-api";
import { promises as fs } from "fs";
import { createLinkBlock, createSeoBlock, createTextImageBlock, InternalLinkBlock } from "./src";
@@ -6,7 +6,9 @@ import { createLinkBlock, createSeoBlock, createTextImageBlock, InternalLinkBloc
async function generateBlockMeta(): Promise {
console.info("Generating block-meta.json...");
- const LinkBlock = createLinkBlock({ supportedBlocks: { internal: InternalLinkBlock, external: ExternalLinkBlock } });
+ const LinkBlock = createLinkBlock({
+ supportedBlocks: { internal: InternalLinkBlock, external: ExternalLinkBlock, email: EmailLinkBlock, phone: PhoneLinkBlock },
+ });
const TextBlock = createRichTextBlock({ link: LinkBlock });
diff --git a/packages/site/cms-site/src/blocks/EmailLinkBlock.tsx b/packages/site/cms-site/src/blocks/EmailLinkBlock.tsx
new file mode 100644
index 0000000000..1b873564bb
--- /dev/null
+++ b/packages/site/cms-site/src/blocks/EmailLinkBlock.tsx
@@ -0,0 +1,22 @@
+import * as React from "react";
+
+import { EmailLinkBlockData } from "../blocks.generated";
+import { PropsWithData } from "./PropsWithData";
+
+interface EmailLinkBlockProps extends PropsWithData {
+ children: React.ReactElement;
+ title?: string;
+}
+
+export const EmailLinkBlock = ({ data: { email }, children, title }: EmailLinkBlockProps) => {
+ if (!email) {
+ return children;
+ }
+
+ const childProps = {
+ href: `mailto:${email}`,
+ title,
+ };
+
+ return React.cloneElement(children, childProps);
+};
diff --git a/packages/site/cms-site/src/blocks/PhoneLinkBlock.tsx b/packages/site/cms-site/src/blocks/PhoneLinkBlock.tsx
new file mode 100644
index 0000000000..d5e0251044
--- /dev/null
+++ b/packages/site/cms-site/src/blocks/PhoneLinkBlock.tsx
@@ -0,0 +1,22 @@
+import * as React from "react";
+
+import { PhoneLinkBlockData } from "../blocks.generated";
+import { PropsWithData } from "./PropsWithData";
+
+interface PhoneLinkBlockProps extends PropsWithData {
+ children: React.ReactElement;
+ title?: string;
+}
+
+export const PhoneLinkBlock = ({ data: { phone }, children, title }: PhoneLinkBlockProps) => {
+ if (!phone) {
+ return children;
+ }
+
+ const childProps = {
+ href: `tel:${phone}`,
+ title,
+ };
+
+ return React.cloneElement(children, childProps);
+};
diff --git a/packages/site/cms-site/src/index.ts b/packages/site/cms-site/src/index.ts
index 927e1a6027..e1d0602063 100644
--- a/packages/site/cms-site/src/index.ts
+++ b/packages/site/cms-site/src/index.ts
@@ -1,4 +1,5 @@
export { DamFileDownloadLinkBlock } from "./blocks/DamFileDownloadLinkBlock";
+export { EmailLinkBlock } from "./blocks/EmailLinkBlock";
export { ExternalLinkBlock } from "./blocks/ExternalLinkBlock";
export { BlocksBlock } from "./blocks/factories/BlocksBlock";
export { ListBlock } from "./blocks/factories/ListBlock";
@@ -7,6 +8,7 @@ export { OptionalBlock } from "./blocks/factories/OptionalBlock";
export { SeoBlock } from "./blocks/factories/SeoBlock";
export type { SupportedBlocks } from "./blocks/factories/types";
export { InternalLinkBlock } from "./blocks/InternalLinkBlock";
+export { PhoneLinkBlock } from "./blocks/PhoneLinkBlock";
export { PixelImageBlock } from "./blocks/PixelImageBlock";
export type { PropsWithData } from "./blocks/PropsWithData";
export { hasRichTextBlockContent } from "./blocks/RichTextBlock";