Files
emdash-patch-imageupload/templates/marketing/src/plugins/marketing-blocks/index.ts
kunthawat 2d1be52177 Emdash source with visual editor image upload fix
Fixes:
1. media.ts: wrap placeholder generation in try-catch
2. toolbar.ts: check r.ok, display error message in popover
2026-05-03 10:44:54 +07:00

219 lines
6.1 KiB
TypeScript

/**
* Marketing blocks plugin (inline, template-local).
*
* Registers the five marketing block types so editors can insert and edit them
* in the admin's Portable Text editor. Block Kit `fields` describe the form
* shown when inserting or editing a block.
*
* Constraints worth knowing:
*
* - Block Kit has no "object group" element, so nested object shapes (e.g. a
* CTA's { label, url }) are flattened to sibling fields like ctaLabel and
* ctaUrl. The site-side renderer reads the flat keys.
* - Repeater sub-fields are scalar only: text_input, number_input, select,
* toggle. Nested repeaters are not allowed -- list-of-strings becomes a
* single multiline text field, split on newline at render time (see
* Pricing.astro for the pattern).
* - There is no media picker element in the editor's plugin-block modal yet,
* so image fields (avatars, hero images) are URL strings entered by hand.
*
* Site-side rendering still goes through MarketingBlocks.astro --
* componentsEntry auto-wiring is a separate cleanup.
*/
import { definePlugin } from "emdash";
import type { PluginDefinition } from "emdash";
const ICON_OPTIONS = [
{ label: "Lightning", value: "zap" },
{ label: "Shield", value: "shield" },
{ label: "Users", value: "users" },
{ label: "Chart", value: "chart" },
{ label: "Code", value: "code" },
{ label: "Globe", value: "globe" },
{ label: "Heart", value: "heart" },
{ label: "Star", value: "star" },
{ label: "Check", value: "check" },
{ label: "Lock", value: "lock" },
{ label: "Clock", value: "clock" },
{ label: "Cloud", value: "cloud" },
];
const definition: PluginDefinition = {
id: "marketing-blocks",
version: "0.1.0",
admin: {
portableTextBlocks: [
{
type: "marketing.hero",
label: "Hero",
category: "Sections",
description: "Big headline section with optional CTAs",
fields: [
{ type: "text_input", action_id: "headline", label: "Headline" },
{
type: "text_input",
action_id: "subheadline",
label: "Subheadline",
multiline: true,
},
{ type: "text_input", action_id: "primaryCtaLabel", label: "Primary CTA label" },
{ type: "text_input", action_id: "primaryCtaUrl", label: "Primary CTA URL" },
{
type: "text_input",
action_id: "secondaryCtaLabel",
label: "Secondary CTA label",
},
{ type: "text_input", action_id: "secondaryCtaUrl", label: "Secondary CTA URL" },
{ type: "toggle", action_id: "centered", label: "Center the layout" },
],
},
{
type: "marketing.features",
label: "Features",
category: "Sections",
description: "Grid of feature cards with icons",
fields: [
{ type: "text_input", action_id: "headline", label: "Headline" },
{
type: "text_input",
action_id: "subheadline",
label: "Subheadline",
multiline: true,
},
{
type: "repeater",
action_id: "features",
label: "Features",
item_label: "Feature",
min_items: 1,
max_items: 12,
fields: [
{
type: "select",
action_id: "icon",
label: "Icon",
options: ICON_OPTIONS,
},
{ type: "text_input", action_id: "title", label: "Title" },
{
type: "text_input",
action_id: "description",
label: "Description",
multiline: true,
},
],
},
],
},
{
type: "marketing.testimonials",
label: "Testimonials",
category: "Sections",
description: "Customer testimonial cards",
fields: [
{ type: "text_input", action_id: "headline", label: "Headline" },
{
type: "repeater",
action_id: "testimonials",
label: "Testimonials",
item_label: "Testimonial",
min_items: 1,
fields: [
{ type: "text_input", action_id: "quote", label: "Quote", multiline: true },
{ type: "text_input", action_id: "author", label: "Author name" },
{ type: "text_input", action_id: "role", label: "Role / title" },
{ type: "text_input", action_id: "company", label: "Company" },
{ type: "text_input", action_id: "avatar", label: "Avatar URL" },
],
},
],
},
{
type: "marketing.pricing",
label: "Pricing",
category: "Sections",
description: "Pricing plan comparison cards",
fields: [
{ type: "text_input", action_id: "headline", label: "Headline" },
{
type: "repeater",
action_id: "plans",
label: "Plans",
item_label: "Plan",
min_items: 1,
max_items: 6,
fields: [
{ type: "text_input", action_id: "name", label: "Plan name" },
{
type: "text_input",
action_id: "price",
label: "Price",
placeholder: "$29 or Custom",
},
{
type: "text_input",
action_id: "period",
label: "Period",
placeholder: "/month",
},
{
type: "text_input",
action_id: "description",
label: "Description",
multiline: true,
},
{
type: "text_input",
action_id: "features",
label: "Features (one per line)",
multiline: true,
placeholder: "Unlimited projects\nPriority support\nSSO",
},
{ type: "text_input", action_id: "ctaLabel", label: "CTA label" },
{ type: "text_input", action_id: "ctaUrl", label: "CTA URL" },
{ type: "toggle", action_id: "highlighted", label: "Highlight this plan" },
],
},
],
},
{
type: "marketing.faq",
label: "FAQ",
category: "Sections",
description: "Frequently asked questions",
fields: [
{ type: "text_input", action_id: "headline", label: "Headline" },
{
type: "repeater",
action_id: "items",
label: "Questions",
item_label: "Question",
min_items: 1,
fields: [
{ type: "text_input", action_id: "question", label: "Question" },
{
type: "text_input",
action_id: "answer",
label: "Answer",
multiline: true,
},
],
},
],
},
],
},
};
export function createPlugin() {
return definePlugin(definition);
}
export default createPlugin;