1009 lines
24 KiB
Markdown
1009 lines
24 KiB
Markdown
# Block Library
|
|
|
|
Reusable page component system with JSON schema definitions, default values, and campaign-specific customization.
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The Block Library provides a database-driven system for managing reusable page components (blocks) in the GrapesJS editor. Administrators can use pre-configured blocks or create custom ones tailored to their campaign needs.
|
|
|
|
### Key Features
|
|
|
|
- **Database-Driven**: Blocks stored in PostgreSQL (PageBlock model)
|
|
- **JSON Schema**: Define configurable properties for each block type
|
|
- **Default Values**: Pre-populate blocks with campaign-specific content
|
|
- **Category Organization**: Group blocks (Headers, Content, Actions, etc.)
|
|
- **Sort Order**: Control block position in editor panel
|
|
- **6 Default Blocks**: Hero, Text, Features, CTA, Testimonials, Contact Form
|
|
- **Custom Blocks**: Create campaign-specific blocks via admin API
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
graph LR
|
|
A[(PageBlock Table)] -->|GET /api/page-blocks| B[API Service]
|
|
B --> C[LandingPageEditor]
|
|
C --> D[GrapesJSEditor]
|
|
D --> E[BlockManager]
|
|
E --> F[Left Panel]
|
|
|
|
G[Admin] -->|POST /api/page-blocks| B
|
|
G -->|Define Schema| H[JSON Schema]
|
|
G -->|Set Defaults| I[Default Values]
|
|
H --> A
|
|
I --> A
|
|
|
|
style A fill:#3498db
|
|
style E fill:#9d4edd
|
|
style F fill:#2ecc71
|
|
```
|
|
|
|
**Flow:**
|
|
|
|
1. **Seed**: Default blocks created in `api/prisma/seed.ts`
|
|
2. **Fetch**: Editor loads all blocks via `GET /api/page-blocks`
|
|
3. **Register**: GrapesJSEditor registers each block with BlockManager
|
|
4. **Render**: Blocks appear in left panel (grouped by category)
|
|
5. **Customize**: Admin creates custom blocks via API (future enhancement)
|
|
|
|
---
|
|
|
|
## Database Model
|
|
|
|
### PageBlock Table
|
|
|
|
**Schema:**
|
|
|
|
```typescript
|
|
model PageBlock {
|
|
id String @id @default(uuid())
|
|
type String @unique // Block type identifier (e.g., 'hero', 'text')
|
|
label String // Display name in editor ("Hero Section")
|
|
category String? // Group blocks ("Headers", "Content", "Actions")
|
|
sortOrder Int @default(0) // Position in left panel
|
|
schema Json // JSON schema for configurable properties
|
|
defaults Json // Default values for schema fields
|
|
thumbnail String? // Preview image URL (future enhancement)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([category, sortOrder])
|
|
}
|
|
```
|
|
|
|
**Fields:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `id` | String (UUID) | Primary key |
|
|
| `type` | String | Unique identifier (e.g., `"hero"`, `"features"`) |
|
|
| `label` | String | Human-readable name shown in editor |
|
|
| `category` | String? | Group blocks in collapsible sections |
|
|
| `sortOrder` | Int | Order within category (lower = higher in list) |
|
|
| `schema` | JSON | Property definitions (field name, type, label) |
|
|
| `defaults` | JSON | Default values for each schema field |
|
|
| `thumbnail` | String? | Preview image URL (not implemented) |
|
|
|
|
**Indexes:**
|
|
|
|
- `type` (unique)
|
|
- `category + sortOrder` (composite, for sorted listing)
|
|
|
|
---
|
|
|
|
## Default Blocks
|
|
|
|
### 1. Hero Section
|
|
|
|
**Type:** `hero`
|
|
|
|
**Category:** Headers
|
|
|
|
**Schema:**
|
|
|
|
```json
|
|
{
|
|
"title": { "type": "string", "label": "Title" },
|
|
"subtitle": { "type": "string", "label": "Subtitle" },
|
|
"backgroundImage": { "type": "string", "label": "Background Image URL" },
|
|
"ctaText": { "type": "string", "label": "Button Text" },
|
|
"ctaUrl": { "type": "string", "label": "Button URL" }
|
|
}
|
|
```
|
|
|
|
**Defaults:**
|
|
|
|
```json
|
|
{
|
|
"title": "Welcome to Our Campaign",
|
|
"subtitle": "Join us in making a difference in your community.",
|
|
"backgroundImage": "",
|
|
"ctaText": "Get Involved",
|
|
"ctaUrl": "#"
|
|
}
|
|
```
|
|
|
|
**Rendered HTML:**
|
|
|
|
```html
|
|
<section style="padding: 80px 40px; text-align: center; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: #fff;">
|
|
<h1 style="font-size: 2.5rem; margin-bottom: 16px;">Welcome to Our Campaign</h1>
|
|
<p style="font-size: 1.25rem; opacity: 0.85; margin-bottom: 32px;">Join us in making a difference in your community.</p>
|
|
<a href="#" style="display: inline-block; padding: 12px 32px; background: #9d4edd; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 600;">Get Involved</a>
|
|
</section>
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Text Block
|
|
|
|
**Type:** `text`
|
|
|
|
**Category:** Content
|
|
|
|
**Schema:**
|
|
|
|
```json
|
|
{
|
|
"heading": { "type": "string", "label": "Heading" },
|
|
"body": { "type": "text", "label": "Body Text" }
|
|
}
|
|
```
|
|
|
|
**Defaults:**
|
|
|
|
```json
|
|
{
|
|
"heading": "About Us",
|
|
"body": "Tell your story here. Explain your mission, values, and what drives your campaign forward."
|
|
}
|
|
```
|
|
|
|
**Rendered HTML:**
|
|
|
|
```html
|
|
<section style="padding: 60px 40px; max-width: 800px; margin: 0 auto;">
|
|
<h2 style="font-size: 1.75rem; margin-bottom: 16px;">About Us</h2>
|
|
<p style="font-size: 1rem; line-height: 1.7; opacity: 0.85;">Tell your story here. Explain your mission, values, and what drives your campaign forward.</p>
|
|
</section>
|
|
```
|
|
|
|
---
|
|
|
|
### 3. Features Grid
|
|
|
|
**Type:** `features`
|
|
|
|
**Category:** Content
|
|
|
|
**Schema:**
|
|
|
|
```json
|
|
{
|
|
"features": {
|
|
"type": "array",
|
|
"label": "Features",
|
|
"items": {
|
|
"title": "string",
|
|
"description": "string",
|
|
"icon": "string"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Defaults:**
|
|
|
|
```json
|
|
{
|
|
"features": [
|
|
{ "title": "Community Action", "description": "Organize local events and initiatives.", "icon": "" },
|
|
{ "title": "Advocacy", "description": "Email your representatives directly.", "icon": "" },
|
|
{ "title": "Volunteer", "description": "Sign up for shifts and make a difference.", "icon": "" }
|
|
]
|
|
}
|
|
```
|
|
|
|
**Rendered HTML:**
|
|
|
|
```html
|
|
<section style="padding: 60px 40px;">
|
|
<div style="display: flex; flex-wrap: wrap; gap: 24px; justify-content: center;">
|
|
<div style="flex: 1; min-width: 250px; padding: 24px; text-align: center;">
|
|
<h3 style="font-size: 1.25rem; margin-bottom: 8px;">Community Action</h3>
|
|
<p style="opacity: 0.8;">Organize local events and initiatives.</p>
|
|
</div>
|
|
<div style="flex: 1; min-width: 250px; padding: 24px; text-align: center;">
|
|
<h3 style="font-size: 1.25rem; margin-bottom: 8px;">Advocacy</h3>
|
|
<p style="opacity: 0.8;">Email your representatives directly.</p>
|
|
</div>
|
|
<div style="flex: 1; min-width: 250px; padding: 24px; text-align: center;">
|
|
<h3 style="font-size: 1.25rem; margin-bottom: 8px;">Volunteer</h3>
|
|
<p style="opacity: 0.8;">Sign up for shifts and make a difference.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
```
|
|
|
|
---
|
|
|
|
### 4. Call to Action
|
|
|
|
**Type:** `cta`
|
|
|
|
**Category:** Actions
|
|
|
|
**Schema:**
|
|
|
|
```json
|
|
{
|
|
"heading": { "type": "string", "label": "Heading" },
|
|
"description": { "type": "string", "label": "Description" },
|
|
"buttonText": { "type": "string", "label": "Button Text" },
|
|
"buttonUrl": { "type": "string", "label": "Button URL" }
|
|
}
|
|
```
|
|
|
|
**Defaults:**
|
|
|
|
```json
|
|
{
|
|
"heading": "Ready to Take Action?",
|
|
"description": "Join thousands of community members making their voices heard.",
|
|
"buttonText": "Join Now",
|
|
"buttonUrl": "#"
|
|
}
|
|
```
|
|
|
|
**Rendered HTML:**
|
|
|
|
```html
|
|
<section style="padding: 60px 40px; text-align: center; background: linear-gradient(135deg, #9d4edd 0%, #7b2cbf 100%); color: #fff;">
|
|
<h2 style="font-size: 2rem; margin-bottom: 12px;">Ready to Take Action?</h2>
|
|
<p style="font-size: 1.1rem; margin-bottom: 24px; opacity: 0.9;">Join thousands of community members making their voices heard.</p>
|
|
<a href="#" style="display: inline-block; padding: 12px 32px; background: #fff; color: #9d4edd; text-decoration: none; border-radius: 6px; font-weight: 600;">Join Now</a>
|
|
</section>
|
|
```
|
|
|
|
---
|
|
|
|
### 5. Testimonials
|
|
|
|
**Type:** `testimonials`
|
|
|
|
**Category:** Content
|
|
|
|
**Schema:**
|
|
|
|
```json
|
|
{
|
|
"quotes": {
|
|
"type": "array",
|
|
"label": "Quotes",
|
|
"items": {
|
|
"text": "string",
|
|
"author": "string",
|
|
"role": "string"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Defaults:**
|
|
|
|
```json
|
|
{
|
|
"quotes": [
|
|
{ "text": "This platform made it so easy to contact my representatives.", "author": "Jane D.", "role": "Community Member" },
|
|
{ "text": "I signed up for a volunteer shift and it changed my perspective.", "author": "Mark S.", "role": "Volunteer" }
|
|
]
|
|
}
|
|
```
|
|
|
|
**Rendered HTML:**
|
|
|
|
```html
|
|
<section style="padding: 60px 40px;">
|
|
<div style="display: flex; flex-wrap: wrap; gap: 24px; justify-content: center;">
|
|
<div style="flex: 1; min-width: 280px; padding: 24px; background: rgba(255,255,255,0.05); border-radius: 8px;">
|
|
<p style="font-style: italic; margin-bottom: 12px;">"This platform made it so easy to contact my representatives."</p>
|
|
<p style="font-weight: 600; margin-bottom: 2px;">Jane D.</p>
|
|
<p style="font-size: 0.85rem; opacity: 0.7;">Community Member</p>
|
|
</div>
|
|
<div style="flex: 1; min-width: 280px; padding: 24px; background: rgba(255,255,255,0.05); border-radius: 8px;">
|
|
<p style="font-style: italic; margin-bottom: 12px;">"I signed up for a volunteer shift and it changed my perspective."</p>
|
|
<p style="font-weight: 600; margin-bottom: 2px;">Mark S.</p>
|
|
<p style="font-size: 0.85rem; opacity: 0.7;">Volunteer</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
```
|
|
|
|
---
|
|
|
|
### 6. Contact Form
|
|
|
|
**Type:** `contact-form`
|
|
|
|
**Category:** Actions
|
|
|
|
**Schema:**
|
|
|
|
```json
|
|
{
|
|
"heading": { "type": "string", "label": "Heading" },
|
|
"fields": {
|
|
"type": "array",
|
|
"label": "Fields",
|
|
"items": {
|
|
"name": "string",
|
|
"type": "string",
|
|
"required": "boolean"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Defaults:**
|
|
|
|
```json
|
|
{
|
|
"heading": "Get in Touch",
|
|
"fields": [
|
|
{ "name": "name", "type": "text", "required": true },
|
|
{ "name": "email", "type": "email", "required": true },
|
|
{ "name": "message", "type": "textarea", "required": true }
|
|
]
|
|
}
|
|
```
|
|
|
|
**Rendered HTML:**
|
|
|
|
```html
|
|
<section style="padding: 60px 40px; max-width: 600px; margin: 0 auto;">
|
|
<h2 style="text-align: center; margin-bottom: 24px;">Get in Touch</h2>
|
|
<form style="display: flex; flex-direction: column; gap: 16px;">
|
|
<input type="text" placeholder="Name" style="padding: 10px 14px; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.05); color: inherit;" />
|
|
<input type="email" placeholder="Email" style="padding: 10px 14px; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.05); color: inherit;" />
|
|
<textarea placeholder="Message" rows="4" style="padding: 10px 14px; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.05); color: inherit; resize: vertical;"></textarea>
|
|
<button type="submit" style="padding: 12px 24px; background: #9d4edd; color: #fff; border: none; border-radius: 6px; font-weight: 600; cursor: pointer;">Send Message</button>
|
|
</form>
|
|
</section>
|
|
```
|
|
|
|
**Note:** Form submission not wired (static HTML). Use grapesjs-plugin-forms for backend integration.
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
### Admin Routes
|
|
|
|
**Prefix:** `/api/page-blocks`
|
|
|
|
**Authentication:** Requires admin role (SUPER_ADMIN, INFLUENCE_ADMIN, or MAP_ADMIN)
|
|
|
|
#### List Blocks
|
|
|
|
```http
|
|
GET /api/page-blocks?category=Headers
|
|
```
|
|
|
|
**Query Parameters:**
|
|
|
|
- `category` (string?) — Filter by category
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
[
|
|
{
|
|
"id": "default-hero",
|
|
"type": "hero",
|
|
"label": "Hero Section",
|
|
"category": "Headers",
|
|
"sortOrder": 1,
|
|
"schema": {
|
|
"title": { "type": "string", "label": "Title" },
|
|
"subtitle": { "type": "string", "label": "Subtitle" }
|
|
},
|
|
"defaults": {
|
|
"title": "Welcome to Our Campaign",
|
|
"subtitle": "Join us in making a difference."
|
|
},
|
|
"thumbnail": null,
|
|
"createdAt": "2026-01-10T00:00:00Z",
|
|
"updatedAt": "2026-01-10T00:00:00Z"
|
|
}
|
|
]
|
|
```
|
|
|
|
**Sorting:**
|
|
|
|
- Results ordered by `category ASC, sortOrder ASC`
|
|
- Blocks in same category appear in sortOrder sequence
|
|
|
|
#### Get Block
|
|
|
|
```http
|
|
GET /api/page-blocks/:id
|
|
```
|
|
|
|
**Response:** Single `PageBlock` object
|
|
|
|
**Errors:**
|
|
|
|
- `404 BLOCK_NOT_FOUND` — Block doesn't exist
|
|
|
|
#### Create Block
|
|
|
|
```http
|
|
POST /api/page-blocks
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"type": "campaign-stats",
|
|
"label": "Campaign Stats",
|
|
"category": "Campaign",
|
|
"sortOrder": 10,
|
|
"schema": {
|
|
"volunteers": { "type": "number", "label": "Volunteers" },
|
|
"emails": { "type": "number", "label": "Emails Sent" }
|
|
},
|
|
"defaults": {
|
|
"volunteers": 1250,
|
|
"emails": 5400
|
|
}
|
|
}
|
|
```
|
|
|
|
**Request Body:**
|
|
|
|
- `type` (string, required) — Unique type identifier (alphanumeric + hyphens)
|
|
- `label` (string, required) — Display name
|
|
- `category` (string?) — Group name (default: `null`)
|
|
- `sortOrder` (number?, default: 0) — Position in list
|
|
- `schema` (JSON, required) — Property definitions
|
|
- `defaults` (JSON, required) — Default values matching schema
|
|
- `thumbnail` (string?) — Preview image URL
|
|
|
|
**Response:** Created `PageBlock` object (201 status)
|
|
|
|
**Errors:**
|
|
|
|
- `400 VALIDATION_ERROR` — Invalid schema or type collision
|
|
|
|
#### Update Block
|
|
|
|
```http
|
|
PUT /api/page-blocks/:id
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"label": "Updated Label",
|
|
"defaults": {
|
|
"volunteers": 2000
|
|
}
|
|
}
|
|
```
|
|
|
|
**Request Body:** (all fields optional except constraints)
|
|
|
|
- `type` (string?) — Cannot change after creation (immutable)
|
|
- `label` (string?)
|
|
- `category` (string?)
|
|
- `sortOrder` (number?)
|
|
- `schema` (JSON?)
|
|
- `defaults` (JSON?)
|
|
|
|
**Response:** Updated `PageBlock` object
|
|
|
|
**Errors:**
|
|
|
|
- `404 BLOCK_NOT_FOUND` — Block doesn't exist
|
|
- `400 VALIDATION_ERROR` — Invalid schema or defaults
|
|
|
|
#### Delete Block
|
|
|
|
```http
|
|
DELETE /api/page-blocks/:id
|
|
```
|
|
|
|
**Response:** 204 No Content
|
|
|
|
**Errors:**
|
|
|
|
- `404 BLOCK_NOT_FOUND` — Block doesn't exist
|
|
|
|
**Side Effects:**
|
|
|
|
- Pages using this block will still render (HTML is cached)
|
|
- Block removed from editor panel for new pages
|
|
|
|
---
|
|
|
|
## Schema Format
|
|
|
|
### Property Types
|
|
|
|
**Supported Types:**
|
|
|
|
| Type | Description | Example |
|
|
|------|-------------|---------|
|
|
| `string` | Short text field | Title, subtitle, URL |
|
|
| `text` | Multi-line text | Body paragraph |
|
|
| `number` | Numeric value | Volunteer count, price |
|
|
| `boolean` | True/false toggle | Show/hide element |
|
|
| `array` | List of items | Features, testimonials |
|
|
|
|
### Simple Property
|
|
|
|
```json
|
|
{
|
|
"title": {
|
|
"type": "string",
|
|
"label": "Title"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Rendered in GrapesJS:** Text input labeled "Title"
|
|
|
|
### Array Property
|
|
|
|
```json
|
|
{
|
|
"features": {
|
|
"type": "array",
|
|
"label": "Features",
|
|
"items": {
|
|
"title": "string",
|
|
"description": "string",
|
|
"icon": "string"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Rendered in GrapesJS:**
|
|
|
|
- Repeatable item group
|
|
- Add/remove buttons
|
|
- Each item has 3 fields (title, description, icon)
|
|
|
|
### Defaults Matching
|
|
|
|
**Schema:**
|
|
|
|
```json
|
|
{
|
|
"heading": { "type": "string", "label": "Heading" },
|
|
"count": { "type": "number", "label": "Count" }
|
|
}
|
|
```
|
|
|
|
**Valid Defaults:**
|
|
|
|
```json
|
|
{
|
|
"heading": "Our Impact",
|
|
"count": 42
|
|
}
|
|
```
|
|
|
|
**Invalid Defaults:**
|
|
|
|
```json
|
|
{
|
|
"heading": 123, // Type mismatch (should be string)
|
|
"count": "foo" // Type mismatch (should be number)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Admin Workflow
|
|
|
|
### Using Default Blocks
|
|
|
|
1. **Open Editor:** Admin → Pages → Click "Edit" on any page
|
|
2. **Locate Block:** Left panel → Expand "Headers" category
|
|
3. **Drag Block:** Drag "Hero Section" to canvas
|
|
4. **Configure:** Click block → Right panel shows properties
|
|
- Title: `"Join the Movement"`
|
|
- Subtitle: `"Together we can make a difference."`
|
|
- CTA Text: `"Sign Up"`
|
|
- CTA URL: `"/shifts"`
|
|
5. **Save:** Press `Ctrl+S` → Block HTML stored in database
|
|
|
|
### Creating Custom Blocks
|
|
|
|
**Note:** Custom block creation UI not implemented. Use API directly.
|
|
|
|
**Example: Campaign Stats Block**
|
|
|
|
```bash
|
|
curl -X POST http://localhost:4000/api/page-blocks \
|
|
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"type": "campaign-stats",
|
|
"label": "Campaign Stats",
|
|
"category": "Campaign",
|
|
"sortOrder": 10,
|
|
"schema": {
|
|
"volunteers": { "type": "number", "label": "Volunteers" },
|
|
"emails": { "type": "number", "label": "Emails Sent" },
|
|
"events": { "type": "number", "label": "Events" }
|
|
},
|
|
"defaults": {
|
|
"volunteers": 1250,
|
|
"emails": 5400,
|
|
"events": 32
|
|
}
|
|
}'
|
|
```
|
|
|
|
**Result:**
|
|
|
|
- New block appears in left panel under "Campaign" category
|
|
- Dragging block inserts HTML (requires `generateBlockHtml` update)
|
|
|
|
### Updating Block Defaults
|
|
|
|
**Use Case:** Update hero CTA text for all new pages
|
|
|
|
```bash
|
|
curl -X PUT http://localhost:4000/api/page-blocks/default-hero \
|
|
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"defaults": {
|
|
"title": "Welcome to Our 2026 Campaign",
|
|
"subtitle": "Join us in making a difference.",
|
|
"ctaText": "Get Started Today",
|
|
"ctaUrl": "/shifts"
|
|
}
|
|
}'
|
|
```
|
|
|
|
**Effect:**
|
|
|
|
- New pages using hero block get updated defaults
|
|
- Existing pages unchanged (HTML already rendered)
|
|
|
|
---
|
|
|
|
## Code Examples
|
|
|
|
### Fetching Blocks for Editor
|
|
|
|
```typescript
|
|
import { api } from '@/lib/api';
|
|
import type { PageBlock } from '@/types/api';
|
|
|
|
async function loadBlocks(): Promise<PageBlock[]> {
|
|
const { data } = await api.get<PageBlock[]>('/page-blocks');
|
|
return data.sort((a, b) => {
|
|
// Sort by category, then sortOrder
|
|
const catCompare = (a.category || '').localeCompare(b.category || '');
|
|
return catCompare !== 0 ? catCompare : a.sortOrder - b.sortOrder;
|
|
});
|
|
}
|
|
```
|
|
|
|
### Creating Custom Block
|
|
|
|
```typescript
|
|
async function createCampaignStatsBlock() {
|
|
const { data } = await api.post<PageBlock>('/page-blocks', {
|
|
type: 'campaign-stats',
|
|
label: 'Campaign Stats',
|
|
category: 'Campaign',
|
|
sortOrder: 10,
|
|
schema: {
|
|
volunteers: { type: 'number', label: 'Volunteers' },
|
|
emails: { type: 'number', label: 'Emails Sent' },
|
|
events: { type: 'number', label: 'Events' },
|
|
},
|
|
defaults: {
|
|
volunteers: 1250,
|
|
emails: 5400,
|
|
events: 32,
|
|
},
|
|
});
|
|
|
|
console.log('Created block:', data.id);
|
|
return data;
|
|
}
|
|
```
|
|
|
|
### Extending generateBlockHtml()
|
|
|
|
```typescript
|
|
// In admin/src/components/GrapesJSEditor.tsx
|
|
|
|
function generateBlockHtml(type: string, defaults: Record<string, unknown>): string {
|
|
switch (type) {
|
|
// ... existing cases ...
|
|
|
|
case 'campaign-stats': {
|
|
const volunteers = defaults.volunteers || 0;
|
|
const emails = defaults.emails || 0;
|
|
const events = defaults.events || 0;
|
|
|
|
return `
|
|
<section style="padding: 60px 40px; background: #f8f9fa; text-align: center;">
|
|
<h2 style="margin-bottom: 32px; font-size: 2rem;">Our Impact</h2>
|
|
<div style="display: flex; gap: 48px; justify-content: center; flex-wrap: wrap;">
|
|
<div>
|
|
<div style="font-size: 3rem; font-weight: 700; color: #9d4edd;">${(volunteers as number).toLocaleString()}</div>
|
|
<div style="font-size: 1rem; color: #666;">Volunteers</div>
|
|
</div>
|
|
<div>
|
|
<div style="font-size: 3rem; font-weight: 700; color: #9d4edd;">${(emails as number).toLocaleString()}</div>
|
|
<div style="font-size: 1rem; color: #666;">Emails Sent</div>
|
|
</div>
|
|
<div>
|
|
<div style="font-size: 3rem; font-weight: 700; color: #9d4edd;">${events}</div>
|
|
<div style="font-size: 1rem; color: #666;">Events</div>
|
|
</div>
|
|
</div>
|
|
</section>`;
|
|
}
|
|
|
|
default:
|
|
return `<section style="padding: 40px; text-align: center;"><p>Custom block: ${type}</p></section>`;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Problem: Block Not Appearing in Editor
|
|
|
|
**Symptoms:**
|
|
|
|
- Created block via API
|
|
- Not visible in left panel
|
|
- Other blocks show correctly
|
|
|
|
**Causes:**
|
|
|
|
1. GrapesJSEditor not re-fetching blocks
|
|
2. `generateBlockHtml()` missing case
|
|
3. Category name mismatch
|
|
|
|
**Solutions:**
|
|
|
|
1. **Reload editor:**
|
|
- Close page editor → Re-open
|
|
- Blocks fetched on mount
|
|
|
|
2. **Add HTML generation case:**
|
|
```typescript
|
|
case 'my-new-block':
|
|
return `<section>My block HTML</section>`;
|
|
```
|
|
|
|
3. **Check category:**
|
|
```sql
|
|
SELECT category FROM page_blocks WHERE type = 'my-new-block';
|
|
-- Category should match GrapesJS panel (case-sensitive)
|
|
```
|
|
|
|
4. **Verify API response:**
|
|
```bash
|
|
curl -H "Authorization: Bearer $TOKEN" http://localhost:4000/api/page-blocks
|
|
# Should include new block in response
|
|
```
|
|
|
|
---
|
|
|
|
### Problem: Default Values Not Applying
|
|
|
|
**Symptoms:**
|
|
|
|
- Drag block to canvas → Fields are empty
|
|
- Expected pre-filled title/subtitle
|
|
|
|
**Causes:**
|
|
|
|
1. Defaults not matching schema keys
|
|
2. HTML template ignores defaults
|
|
3. Type mismatch (string vs number)
|
|
|
|
**Solutions:**
|
|
|
|
1. **Verify defaults match schema:**
|
|
```json
|
|
// Schema
|
|
{ "title": { "type": "string" } }
|
|
|
|
// Defaults (good)
|
|
{ "title": "Welcome" }
|
|
|
|
// Defaults (bad - key mismatch)
|
|
{ "heading": "Welcome" }
|
|
```
|
|
|
|
2. **Check HTML template:**
|
|
```typescript
|
|
// Good - uses defaults
|
|
return `<h1>${defaults.title || 'Fallback'}</h1>`;
|
|
|
|
// Bad - ignores defaults
|
|
return `<h1>Hardcoded Title</h1>`;
|
|
```
|
|
|
|
3. **Fix type mismatch:**
|
|
```typescript
|
|
// If schema says "number", defaults must be number
|
|
{ "count": { "type": "number" } }
|
|
{ "count": 42 } // Good
|
|
{ "count": "42" } // Bad
|
|
```
|
|
|
|
---
|
|
|
|
### Problem: Block HTML Not Rendering
|
|
|
|
**Symptoms:**
|
|
|
|
- Block appears in panel
|
|
- Dragging to canvas shows nothing or error
|
|
|
|
**Causes:**
|
|
|
|
1. `generateBlockHtml()` returns invalid HTML
|
|
2. Inline styles have syntax errors
|
|
3. Missing closing tags
|
|
|
|
**Solutions:**
|
|
|
|
1. **Validate HTML:**
|
|
```typescript
|
|
const html = generateBlockHtml('my-block', defaults);
|
|
console.log(html); // Check for malformed tags
|
|
```
|
|
|
|
2. **Test inline styles:**
|
|
```html
|
|
<!-- Bad - missing quotes -->
|
|
<div style=padding: 20px>
|
|
|
|
<!-- Good - quoted attribute -->
|
|
<div style="padding: 20px;">
|
|
```
|
|
|
|
3. **Use template literals carefully:**
|
|
```typescript
|
|
// Ensure all ${} expressions return strings
|
|
return `<div>${defaults.title || ''}</div>`;
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Considerations
|
|
|
|
### Block Count Impact
|
|
|
|
**Threshold:** 50+ blocks in library
|
|
|
|
**Symptoms:**
|
|
|
|
- Slow editor initialization (~1s+)
|
|
- Left panel laggy on scroll
|
|
|
|
**Mitigations:**
|
|
|
|
1. **Category filtering:**
|
|
- Only fetch blocks for specific category
|
|
- Lazy-load categories on expand
|
|
|
|
2. **Pagination:**
|
|
- Load first 20 blocks, fetch more on scroll
|
|
- Not implemented in current version
|
|
|
|
3. **Caching:**
|
|
- Store blocks in localStorage
|
|
- Refresh only when version changes
|
|
|
|
### Schema Complexity
|
|
|
|
**Issue:** Deeply nested array schemas (3+ levels) slow GrapesJS rendering
|
|
|
|
**Example:**
|
|
|
|
```json
|
|
{
|
|
"sections": {
|
|
"type": "array",
|
|
"items": {
|
|
"features": {
|
|
"type": "array",
|
|
"items": {
|
|
"details": {
|
|
"type": "array"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Alternative:** Flatten structure or use CODE mode
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
### Admin-Only Access
|
|
|
|
**Protection:** All `/api/page-blocks` endpoints require admin role
|
|
|
|
```typescript
|
|
router.use(authenticate);
|
|
router.use(requireRole(UserRole.SUPER_ADMIN, UserRole.INFLUENCE_ADMIN, UserRole.MAP_ADMIN));
|
|
```
|
|
|
|
**Risk:** Malicious admin creates XSS block with `<script>` tags
|
|
|
|
**Mitigation:**
|
|
|
|
- **Accepted risk:** Admins are trusted users
|
|
- Blocks only render on admin-authored pages (not user-submitted)
|
|
- Public pages use admin-created HTML (already trusted)
|
|
|
|
### Type Validation
|
|
|
|
**Attack:** Submit block with `type` containing SQL injection
|
|
|
|
**Protection:**
|
|
|
|
```typescript
|
|
// Zod schema in pages.schemas.ts
|
|
type: z.string()
|
|
.min(1)
|
|
.max(50)
|
|
.regex(/^[a-z0-9-]+$/, 'Type must be lowercase alphanumeric with hyphens'),
|
|
```
|
|
|
|
**Safe types:** `hero`, `text-block`, `campaign-stats-2026`
|
|
|
|
**Rejected:** `'; DROP TABLE--`, `<script>alert(1)</script>`
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
### Frontend Components
|
|
|
|
- **[GrapesJSEditor](/v2/frontend/components/GrapesJSEditor)** — Block registration logic
|
|
- **[LandingPageEditor](/v2/frontend/pages/LandingPageEditor)** — Fetches blocks for editor
|
|
|
|
### Backend Modules
|
|
|
|
- **[blocks.routes](/v2/backend/modules/pages/blocks.routes)** — CRUD endpoints
|
|
- **[blocks.service](/v2/backend/modules/pages/blocks.service)** — Business logic
|
|
- **[pages.schemas](/v2/backend/modules/pages/pages.schemas)** — Zod schemas
|
|
|
|
### Database
|
|
|
|
- **[PageBlock Model](/v2/database/models/pages)** — Schema + indexes
|
|
|
|
### Features
|
|
|
|
- **[Page Builder](page-builder.md)** — Landing page system
|
|
- **[GrapesJS Editor](grapes-editor.md)** — Editor integration
|
|
|
|
### Seed Data
|
|
|
|
- **[api/prisma/seed.ts](https://github.com/changemaker-lite/changemaker.lite/blob/v2/api/prisma/seed.ts)** — Default blocks definition
|