diff --git a/config/datocms/lib/fields.ts b/config/datocms/lib/fields.ts new file mode 100644 index 0000000..c30a4ee --- /dev/null +++ b/config/datocms/lib/fields.ts @@ -0,0 +1,23 @@ +import type { SimpleSchemaTypes } from '@datocms/cli/lib/cma-client-node'; + +export const actionStyleField: SimpleSchemaTypes.FieldCreateSchema = { + label: 'Style', + field_type: 'string', + api_key: 'style', + validators: { + required: {}, + enum: { values: ['default', 'primary', 'secondary'] }, + }, + appearance: { + addons: [], + editor: 'string_select', + parameters: { + options: [ + { hint: '', label: 'Default action', value: 'default' }, + { hint: '', label: 'Primary action', value: 'primary' }, + { hint: '', label: 'Secondary action', value: 'secondary' }, + ], + }, + }, + default_value: 'default', +}; diff --git a/config/datocms/migrations/1734363388_actionBlock.ts b/config/datocms/migrations/1734363388_actionBlock.ts index cf68e68..12794bc 100644 --- a/config/datocms/migrations/1734363388_actionBlock.ts +++ b/config/datocms/migrations/1734363388_actionBlock.ts @@ -1,4 +1,5 @@ import { Client } from '@datocms/cli/lib/cma-client-node'; +import { actionStyleField } from '../lib/fields'; export default async function (client: Client) { console.log('Create new models/block models'); @@ -102,25 +103,7 @@ export default async function (client: Client) { ); await client.fields.create('GWnhoQDqQoGJj4-sQTVttw', { id: 'S3JQgijhRmalePX3GeugPg', - label: 'Style', - field_type: 'string', - api_key: 'style', - validators: { - required: {}, - enum: { values: ['default', 'primary', 'secondary'] }, - }, - appearance: { - addons: [], - editor: 'string_select', - parameters: { - options: [ - { hint: '', label: 'Default action', value: 'default' }, - { hint: '', label: 'Primary action', value: 'primary' }, - { hint: '', label: 'Secondary action', value: 'secondary' }, - ], - }, - }, - default_value: 'default', + ...actionStyleField, }); console.log('Update existing fields/fieldsets'); diff --git a/config/datocms/migrations/1735337726_externalLinks.ts b/config/datocms/migrations/1735337726_externalLinks.ts new file mode 100644 index 0000000..c935d06 --- /dev/null +++ b/config/datocms/migrations/1735337726_externalLinks.ts @@ -0,0 +1,93 @@ +import type { Client } from '@datocms/cli/lib/cma-client-node'; +import { actionStyleField } from '../lib/fields'; + +export default async function (client: Client) { + console.log('Create new models/block models'); + + console.log( + 'Create block model "\uD83D\uDD17 External Link" (`external_link`)' + ); + await client.itemTypes.create( + { + id: 'Yk1ge9eTTf25Iwph1Dx3_g', + name: '\uD83D\uDD17 External Link', + api_key: 'external_link', + modular_block: true, + inverse_relationships_enabled: false, + }, + { + skip_menu_item_creation: true, + schema_menu_item_id: 'OiBwyNPrR82ZWMawg47csg', + } + ); + + console.log('Creating new fields/fieldsets'); + + console.log( + 'Create Single-line string field "Title" (`title`) in block model "\uD83D\uDD17 External Link" (`external_link`)' + ); + await client.fields.create('Yk1ge9eTTf25Iwph1Dx3_g', { + id: 'Epmmtd7MTfeqpiwLP23D1Q', + label: 'Title', + field_type: 'string', + api_key: 'title', + validators: { required: {} }, + appearance: { + addons: [], + editor: 'single_line', + parameters: { heading: false, placeholder: null }, + }, + default_value: '', + }); + + console.log( + 'Create Single-line string field "URL" (`url`) in block model "\uD83D\uDD17 External Link" (`external_link`)' + ); + await client.fields.create('Yk1ge9eTTf25Iwph1Dx3_g', { + id: 'bUvYLCENQ3SP_vGhoN07nA', + label: 'URL', + field_type: 'string', + api_key: 'url', + validators: { required: {}, format: { predefined_pattern: 'url' } }, + appearance: { + addons: [], + editor: 'single_line', + parameters: { heading: false, placeholder: null }, + }, + default_value: '', + }); + + console.log( + 'Create Boolean field "Open in new tab" (`open_in_new_tab`) in block model "\uD83D\uDD17 External Link" (`external_link`)' + ); + await client.fields.create('Yk1ge9eTTf25Iwph1Dx3_g', { + id: 'AlPRQFQdRlixBp4Tgz5qsQ', + label: 'Open in new tab', + field_type: 'boolean', + api_key: 'open_in_new_tab', + appearance: { addons: [], editor: 'boolean', parameters: {} }, + default_value: false, + }); + + console.log( + 'Create Single-line string field "Style" (`style`) in block model "\uD83D\uDD17 External Link" (`external_link`)' + ); + await client.fields.create('Yk1ge9eTTf25Iwph1Dx3_g', { + id: 'TNl63OntSe6ZLD3wCYxK-g', + ...actionStyleField, + }); + + console.log('Update existing fields/fieldsets'); + + console.log( + 'Update Modular Content (Multiple blocks) field "Items" (`items`) in block model "\uD83C\uDF9B\uFE0F Action Block" (`action_block`)' + ); + await client.fields.update('dAUckF8qR0edf_f7zam6hA', { + validators: { + rich_text_blocks: { + item_types: ['GWnhoQDqQoGJj4-sQTVttw', 'Yk1ge9eTTf25Iwph1Dx3_g'], + }, + size: { min: 1 }, + }, + }); +} diff --git a/config/datocms/migrations/1735398092_emailAndPhoneLinks.ts b/config/datocms/migrations/1735398092_emailAndPhoneLinks.ts new file mode 100644 index 0000000..fab34fd --- /dev/null +++ b/config/datocms/migrations/1735398092_emailAndPhoneLinks.ts @@ -0,0 +1,235 @@ +import type { Client } from '@datocms/cli/lib/cma-client-node'; +import { actionStyleField } from '../lib/fields'; + +export default async function (client: Client) { + console.log('Manage upload filters'); + + console.log('Install plugin "Dropdown Conditional Fields"'); + await client.plugins.create({ + id: 'Srdwo4YOREmRtvMAV2otlQ', + package_name: 'datocms-plugin-dropdown-conditional-fields', + }); + await client.plugins.update('Srdwo4YOREmRtvMAV2otlQ', { + parameters: { developmentMode: false }, + }); + + console.log('Create new models/block models'); + + console.log('Create block model "\uD83D\uDCDE Phone Link" (`phone_link`)'); + await client.itemTypes.create( + { + id: 'C5fWG5CYRJ69oqaP6CjYdA', + name: '\uD83D\uDCDE Phone Link', + api_key: 'phone_link', + modular_block: true, + inverse_relationships_enabled: false, + }, + { + skip_menu_item_creation: true, + schema_menu_item_id: 'YNbDA4R-Srqf0u4-Rya3Ug', + } + ); + + console.log('Create block model "\uD83D\uDCE7 Email Link" (`email_link`)'); + await client.itemTypes.create( + { + id: 'b90_c2zeS6auRELEzZHNcA', + name: '\uD83D\uDCE7 Email Link', + api_key: 'email_link', + modular_block: true, + inverse_relationships_enabled: false, + }, + { + skip_menu_item_creation: true, + schema_menu_item_id: 'Vgf9ZeHvTqeh-tMAks2jFw', + } + ); + + console.log('Creating new fields/fieldsets'); + + console.log( + 'Create Single-line string field "Title" (`title`) in block model "\uD83D\uDCDE Phone Link" (`phone_link`)' + ); + await client.fields.create('C5fWG5CYRJ69oqaP6CjYdA', { + id: 'fp9Cugu8QlKqawis3QTnPA', + label: 'Title', + field_type: 'string', + api_key: 'title', + validators: { required: {} }, + appearance: { + addons: [], + editor: 'single_line', + parameters: { heading: false, placeholder: null }, + }, + default_value: '', + }); + + console.log( + 'Create Single-line string field "Phone number" (`phone_number`) in block model "\uD83D\uDCDE Phone Link" (`phone_link`)' + ); + await client.fields.create('C5fWG5CYRJ69oqaP6CjYdA', { + id: 'bfnOOp5cSM-x8S5RKEpKsw', + label: 'Phone number', + field_type: 'string', + api_key: 'phone_number', + hint: 'Best to use international notation: +31 20 2610954', + validators: { required: {} }, + appearance: { + addons: [], + editor: 'single_line', + parameters: { heading: false, placeholder: null }, + }, + default_value: '', + }); + + console.log( + 'Create Single-line string field "Action" (`action`) in block model "\uD83D\uDCDE Phone Link" (`phone_link`)' + ); + await client.fields.create('C5fWG5CYRJ69oqaP6CjYdA', { + id: 'ZbVIU9fdQgG8IFcEqIQ54A', + label: 'Action', + field_type: 'string', + api_key: 'action', + validators: { required: {}, enum: { values: ['call', 'sms', 'whatsapp'] } }, + appearance: { + addons: [ + { + id: 'Srdwo4YOREmRtvMAV2otlQ', + parameters: { + dependencies: + '{\n "sms": [\n "text"\n ],\n "whatsapp": [\n "text"\n ]\n}', + }, + }, + ], + editor: 'string_select', + parameters: { + options: [ + { hint: '', label: 'Call', value: 'call' }, + { hint: '', label: 'Text (sms)', value: 'sms' }, + { hint: '', label: 'WhatsApp', value: 'whatsapp' }, + ], + }, + }, + default_value: 'call', + }); + + console.log( + 'Create Multiple-paragraph text field "Text" (`text`) in block model "\uD83D\uDCDE Phone Link" (`phone_link`)' + ); + await client.fields.create('C5fWG5CYRJ69oqaP6CjYdA', { + id: 'FKNWfZ5FS-KwBcWn9H8C7A', + label: 'Text', + field_type: 'text', + api_key: 'text', + validators: { sanitized_html: { sanitize_before_validation: true } }, + appearance: { + addons: [], + editor: 'textarea', + parameters: { placeholder: null }, + type: 'textarea', + }, + default_value: '', + }); + + console.log( + 'Create Single-line string field "Style" (`style`) in block model "\uD83D\uDCDE Phone Link" (`phone_link`)' + ); + await client.fields.create('C5fWG5CYRJ69oqaP6CjYdA', { + id: 'GnHv18BPRyCsFsCAGmX0cQ', + ...actionStyleField, + }); + + console.log( + 'Create Single-line string field "Title" (`title`) in block model "\uD83D\uDCE7 Email Link" (`email_link`)' + ); + await client.fields.create('b90_c2zeS6auRELEzZHNcA', { + id: 'RM3FgSTnT4W5lYs16ndS9Q', + label: 'Title', + field_type: 'string', + api_key: 'title', + validators: { required: {} }, + appearance: { + addons: [], + editor: 'single_line', + parameters: { heading: false, placeholder: null }, + }, + default_value: '', + }); + + console.log( + 'Create Single-line string field "Email address" (`email_address`) in block model "\uD83D\uDCE7 Email Link" (`email_link`)' + ); + await client.fields.create('b90_c2zeS6auRELEzZHNcA', { + id: 'VpbcpEhCS5OjLjrdUU8Z6w', + label: 'Email address', + field_type: 'string', + api_key: 'email_address', + validators: { required: {}, format: { predefined_pattern: 'email' } }, + appearance: { + addons: [], + editor: 'single_line', + parameters: { heading: false, placeholder: null }, + }, + default_value: '', + }); + + console.log( + 'Create Single-line string field "Email Subject" (`email_subject`) in block model "\uD83D\uDCE7 Email Link" (`email_link`)' + ); + await client.fields.create('b90_c2zeS6auRELEzZHNcA', { + id: 'LMloAtp6SxOTDuaxCQ7L9g', + label: 'Email Subject', + field_type: 'string', + api_key: 'email_subject', + appearance: { + addons: [], + editor: 'single_line', + parameters: { heading: false, placeholder: null }, + }, + default_value: '', + }); + + console.log( + 'Create Multiple-paragraph text field "Email Body" (`email_body`) in block model "\uD83D\uDCE7 Email Link" (`email_link`)' + ); + await client.fields.create('b90_c2zeS6auRELEzZHNcA', { + id: 'DvV-XIMvT-eioHVaY6AOeQ', + label: 'Email Body', + field_type: 'text', + api_key: 'email_body', + appearance: { + addons: [], + editor: 'textarea', + parameters: { placeholder: null }, + type: 'textarea', + }, + default_value: '', + }); + + console.log( + 'Create Single-line string field "Style" (`style`) in block model "\uD83D\uDCE7 Email Link" (`email_link`)' + ); + await client.fields.create('b90_c2zeS6auRELEzZHNcA', { + id: 'OAAORe89QV-DoOO1GfG5Jw', + ...actionStyleField, + }); + + console.log('Update existing fields/fieldsets'); + + console.log( + 'Update Modular Content (Multiple blocks) field "Items" (`items`) in block model "\uD83C\uDF9B\uFE0F Action Block" (`action_block`)' + ); + await client.fields.update('dAUckF8qR0edf_f7zam6hA', { + validators: { + rich_text_blocks: { + item_types: [ + 'C5fWG5CYRJ69oqaP6CjYdA', + 'GWnhoQDqQoGJj4-sQTVttw', + 'Yk1ge9eTTf25Iwph1Dx3_g', + 'b90_c2zeS6auRELEzZHNcA', + ], + }, + size: { min: 1 }, + }, + }); +} diff --git a/datocms-environment.ts b/datocms-environment.ts index c66777e..5861f28 100644 --- a/datocms-environment.ts +++ b/datocms-environment.ts @@ -3,5 +3,5 @@ * @see docs/getting-started.md on how to use this file * @see docs/decision-log/2023-10-24-datocms-env-file.md on why file is preferred over env vars */ -export const datocmsEnvironment = 'action-block'; +export const datocmsEnvironment = 'email-and-phone-links'; export const datocmsBuildTriggerId = '30535'; diff --git a/src/blocks/ActionBlock/ActionBlock.astro b/src/blocks/ActionBlock/ActionBlock.astro index 94b0475..69f58d1 100644 --- a/src/blocks/ActionBlock/ActionBlock.astro +++ b/src/blocks/ActionBlock/ActionBlock.astro @@ -1,24 +1,58 @@ --- import type { ActionBlockFragment } from '@lib/datocms/types'; +import Link from '@components/Link/Link.astro'; import LinkToRecord from '@components/LinkToRecord/LinkToRecord.astro'; +import EmailLink from './EmailLink.astro'; +import PhoneLink from './PhoneLink.astro'; export interface Props { block: ActionBlockFragment; } const { block } = Astro.props; const { items } = block; +const actionClassList = (style: string) => ['action', `action--${style}`]; ---
{ - items.map((item) => ( - - {item.title} - - )) + items.map((item) => + item.__typename === 'InternalLinkRecord' ? ( + + {item.title} + + ) : item.__typename === 'ExternalLinkRecord' ? ( + + {item.title} + + ) : item.__typename === 'EmailLinkRecord' ? ( + + {item.title} + + ) : item.__typename === 'PhoneLinkRecord' ? ( + + {item.title} + + ) : ( + + ) + ) }
diff --git a/src/blocks/ActionBlock/ActionBlock.fragment.graphql b/src/blocks/ActionBlock/ActionBlock.fragment.graphql index e8a9c91..d661821 100644 --- a/src/blocks/ActionBlock/ActionBlock.fragment.graphql +++ b/src/blocks/ActionBlock/ActionBlock.fragment.graphql @@ -1,4 +1,7 @@ #import './InternalLink.fragment.graphql' +#import './ExternalLink.fragment.graphql' +#import './EmailLink.fragment.graphql' +#import './PhoneLink.fragment.graphql' fragment ActionBlock on ActionBlockRecord { __typename @@ -8,5 +11,14 @@ fragment ActionBlock on ActionBlockRecord { ... on InternalLinkRecord { ...InternalLink } + ... on ExternalLinkRecord { + ...ExternalLink + } + ... on EmailLinkRecord { + ...EmailLink + } + ... on PhoneLinkRecord { + ...PhoneLink + } } } diff --git a/src/blocks/ActionBlock/ActionBlock.test.ts b/src/blocks/ActionBlock/ActionBlock.test.ts index f5a9acb..6b1f370 100644 --- a/src/blocks/ActionBlock/ActionBlock.test.ts +++ b/src/blocks/ActionBlock/ActionBlock.test.ts @@ -1,9 +1,13 @@ import { renderToFragment } from '@lib/renderer'; import { describe, expect, test } from 'vitest'; import InlineBlock, { type Props } from './ActionBlock.astro'; -import type { ActionBlockFragment, InternalLinkFragment, SiteLocale } from '@lib/datocms/types'; +import type { ActionBlockFragment, ExternalLinkFragment, InternalLinkFragment, SiteLocale } from '@lib/datocms/types'; import { locales } from '@lib/i18n'; +type ActionBlockItem = + | InternalLinkFragment + | ExternalLinkFragment; + const createInternalLinkFragment = (title: string, slug: string, style: string) => ({ '__typename': 'InternalLinkRecord', 'id': `${slug}-123`, @@ -19,14 +23,23 @@ const createInternalLinkFragment = (title: string, slug: string, style: string) } } satisfies InternalLinkFragment); -const createActionBlockFragment = (items: InternalLinkFragment[]) => ({ +const createExternalLinkFragment = (title: string, url: string, style: string) => ({ + '__typename': 'ExternalLinkRecord', + 'id': `${url}-123`, + 'title': title, + 'style': style, + 'openInNewTab': false, + 'url': url +} satisfies ExternalLinkFragment); + +const createActionBlockFragment = (items: ActionBlockItem[]) => ({ '__typename': 'ActionBlockRecord', 'id': 'PL9XQGyWQjuyHpdDNsXCNg', 'items': items } satisfies ActionBlockFragment); describe('ActionBlock', () => { - test('Block is rendered', async () => { + test('Block is rendered with internal links', async () => { const blockWithTwoItems = createActionBlockFragment([ createInternalLinkFragment('First item', 'first-item', 'primary'), createInternalLinkFragment('Second item', 'second-item', 'secondary'), @@ -42,4 +55,20 @@ describe('ActionBlock', () => { expect(fragment.querySelector('a.action--secondary')?.textContent).toBe('Second item'); }); + test('Block is rendered with external links', async () => { + const blockWithTwoItems = createActionBlockFragment([ + createExternalLinkFragment('First item', 'https://example.com/first', 'primary'), + createExternalLinkFragment('Second item', 'https://example.com/second', 'secondary'), + ]); + const fragment = await renderToFragment(InlineBlock, { + props: { + block: blockWithTwoItems, + } + }); + + expect(fragment.querySelectorAll('a.action').length).toBe(2); + expect(fragment.querySelector('a.action--primary')?.textContent).toBe('First item'); + expect(fragment.querySelector('a.action--secondary')?.textContent).toBe('Second item'); + }); + }); diff --git a/src/blocks/ActionBlock/EmailLink.astro b/src/blocks/ActionBlock/EmailLink.astro new file mode 100644 index 0000000..8e95e9e --- /dev/null +++ b/src/blocks/ActionBlock/EmailLink.astro @@ -0,0 +1,22 @@ +--- +import type { HTMLAttributes } from 'astro/types'; +import type { EmailLinkFragment } from '@lib/datocms/types'; + +type EmailLinkProps = Pick< + EmailLinkFragment, + 'emailAddress' | 'emailSubject' | 'emailBody' +>; +export type Props = HTMLAttributes<'a'> & EmailLinkProps; +const { emailAddress, emailSubject, emailBody, ...props } = Astro.props; + +const getHref = ({ emailAddress, emailSubject, emailBody }: EmailLinkProps) => { + const url = new URL(`mailto:${emailAddress}`); + if (emailSubject) url.searchParams.set('subject', emailSubject); + if (emailBody) url.searchParams.set('body', emailBody); + return url.href; +}; + +const href = getHref({ emailAddress, emailSubject, emailBody }); +--- + + diff --git a/src/blocks/ActionBlock/EmailLink.fragment.graphql b/src/blocks/ActionBlock/EmailLink.fragment.graphql new file mode 100644 index 0000000..c9ee979 --- /dev/null +++ b/src/blocks/ActionBlock/EmailLink.fragment.graphql @@ -0,0 +1,9 @@ +fragment EmailLink on EmailLinkRecord { + __typename + id + title + emailAddress + emailSubject + emailBody + style +} diff --git a/src/blocks/ActionBlock/ExternalLink.fragment.graphql b/src/blocks/ActionBlock/ExternalLink.fragment.graphql new file mode 100644 index 0000000..fe308c0 --- /dev/null +++ b/src/blocks/ActionBlock/ExternalLink.fragment.graphql @@ -0,0 +1,8 @@ +fragment ExternalLink on ExternalLinkRecord { + __typename + id + title + url + openInNewTab + style +} diff --git a/src/blocks/ActionBlock/PhoneLink.astro b/src/blocks/ActionBlock/PhoneLink.astro new file mode 100644 index 0000000..67eba7c --- /dev/null +++ b/src/blocks/ActionBlock/PhoneLink.astro @@ -0,0 +1,38 @@ +--- +import type { HTMLAttributes } from 'astro/types'; +import type { PhoneLinkFragment } from '@lib/datocms/types'; + +type PhoneLinkProps = Pick< + PhoneLinkFragment, + 'action' | 'phoneNumber' | 'text' +>; +export type Props = HTMLAttributes<'a'> & PhoneLinkProps; +const { action, phoneNumber, text, ...props } = Astro.props; + +const formatPhoneNumber = (phoneNumber: string) => { + return phoneNumber.replace(/\s/g, ''); +}; + +const getHref = ({ action, phoneNumber, text }: PhoneLinkProps) => { + if (action === 'call') { + return `tel:${formatPhoneNumber(phoneNumber)}`; + } + if (action === 'sms') { + const smsNumber = formatPhoneNumber(phoneNumber); + return text + ? `sms:${smsNumber}?body=${encodeURIComponent(text)}` + : `sms:${smsNumber}`; + } + if (action === 'whatsapp') { + const whatsAppNumber = formatPhoneNumber(phoneNumber).replace('+', ''); + return text + ? `https://wa.me/${whatsAppNumber}?text=${encodeURIComponent(text)}` + : `https://wa.me/${whatsAppNumber}`; + } + return ''; +}; + +const href = getHref({ action, phoneNumber, text }); +--- + + diff --git a/src/blocks/ActionBlock/PhoneLink.fragment.graphql b/src/blocks/ActionBlock/PhoneLink.fragment.graphql new file mode 100644 index 0000000..16983e6 --- /dev/null +++ b/src/blocks/ActionBlock/PhoneLink.fragment.graphql @@ -0,0 +1,9 @@ +fragment PhoneLink on PhoneLinkRecord { + __typename + id + title + phoneNumber + action + text + style +}