Template Builder Implementation Plan
Overview
Implement a visual email template builder using Unlayer's React Email Editor alongside the existing HTML code editor, giving users two options for creating templates.
Phase 1: UI/UX Improvements
1.1 Templates Page - Skeleton Loaders
File: apps/dashboard/src/pages/Templates.tsx
Changes:
- Add skeleton loader while templates are loading
- Use Shadcn skeleton component
- Show 3-5 skeleton cards matching template card layout
- Replace loading state with skeleton
Components Needed:
<Skeleton />from Shadcn UI
1.2 Template Type Selection Modal
New Component: apps/dashboard/src/components/TemplateTypeModal.tsx
Design:
┌─────────────────────────────────────────┐
│ Choose Template Type │
├─────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 📝 HTML │ │ 🎨 Builder │ │
│ │ │ │ │ │
│ │ Code-based │ │ Visual │ │
│ │ editor │ │ drag-drop │ │
│ │ │ │ │ │
│ │ Advanced │ │ Easy to │ │
│ │ developers │ │ use │ │
│ │ │ │ │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ [Cancel] │
└─────────────────────────────────────────┘
Features:
- Two large, clickable cards
- Visual icons for each type
- Brief description of each option
- Hover effects
- Responsive design
Props:
interface TemplateTypeModalProps {
open: boolean;
onClose: () => void;
onSelect: (type: 'HTML' | 'BUILDER') => void;
}
Phase 2: Database Schema Updates
2.1 Update Template Model
File: packages/shared/prisma/schema.prisma
Changes:
model Template {
// ... existing fields
// Add new fields
editorType TemplateEditorType @default(HTML) // 'HTML' or 'BUILDER'
designJson Json? // Unlayer design JSON (only for BUILDER type)
// ... rest of fields
}
enum TemplateEditorType {
HTML
BUILDER
}
Migration:
- Create migration:
pnpm prisma migrate dev --name add_template_editor_type - Existing templates default to
HTMLtype
Phase 3: Unlayer Integration
3.1 Install Dependencies
cd apps/dashboard
pnpm add react-email-editor
pnpm add -D @types/react-email-editor
3.2 Create Visual Builder Page
New File: apps/dashboard/src/pages/TemplateBuilderVisual.tsx
Features:
- Full-screen Unlayer editor
- Save button to save design + export HTML
- Back button to return to templates
- Auto-save draft functionality
- Template name input
- Template type selector (TRANSACTIONAL/MARKETING)
Layout:
┌─────────────────────────────────────────────────────┐
│ [← Back] Template Name: [________] [Save] [Test] │
├─────────────────────────────────────────────────────┤
│ │
│ │
│ Unlayer Email Editor │
│ (Full Height) │
│ │
│ │
└─────────────────────────────────────────────────────┘
Code Structure:
const TemplateBuilderVisual = () => {
const emailEditorRef = useRef(null);
const [templateName, setTemplateName] = useState('');
const [templateType, setTemplateType] = useState('TRANSACTIONAL');
// Load existing design if editing
useEffect(() => {
if (templateId && existingDesign) {
emailEditorRef.current?.editor.loadDesign(existingDesign);
}
}, []);
const handleSave = async () => {
emailEditorRef.current?.editor.exportHtml((data) => {
const { design, html } = data;
// Save to API
await apiClient.createTemplate({
name: templateName,
type: templateType,
editorType: 'BUILDER',
htmlBody: html,
designJson: design,
// ... other fields
});
});
};
return (
<div className="h-screen flex flex-col">
<Header />
<EmailEditor
ref={emailEditorRef}
onReady={onEditorReady}
minHeight="calc(100vh - 64px)"
/>
</div>
);
};
Phase 4: Routing Updates
4.1 Update App Routes
File: apps/dashboard/src/App.tsx
Add Routes:
// Add new route for visual builder
<Route
path="templates/new/builder"
element={
<Suspense fallback={<PageLoader />}>
<TemplateBuilderVisual />
</Suspense>
}
/>
<Route
path="templates/:templateId/edit/builder"
element={
<Suspense fallback={<PageLoader />}>
<TemplateBuilderVisual />
</Suspense>
}
/>
// Keep existing HTML editor routes
<Route path="templates/new" element={...} />
<Route path="templates/:templateId/edit" element={...} />
4.2 Update Templates Page Navigation
File: apps/dashboard/src/pages/Templates.tsx
Changes:
const handleCreateTemplate = () => {
setShowTypeModal(true); // Show modal instead of navigating directly
};
const handleTypeSelect = (type: 'HTML' | 'BUILDER') => {
setShowTypeModal(false);
if (type === 'HTML') {
navigate('/templates/new');
} else {
navigate('/templates/new/builder');
}
};
// When editing template, check editorType
const handleEditTemplate = (template) => {
if (template.editorType === 'BUILDER') {
navigate(`/templates/${template.id}/edit/builder`);
} else {
navigate(`/templates/${template.id}/edit`);
}
};
Phase 5: API Updates
5.1 Update Template Controller
File: packages/api/src/api/controllers/template.controller.ts
Changes:
- Accept
editorTypein create/update - Accept
designJsonfor BUILDER templates - Validate designJson is provided for BUILDER type
5.2 Update Template Service
File: packages/api/src/services/template.service.ts
Changes:
interface CreateTemplateInput {
// ... existing fields
editorType?: 'HTML' | 'BUILDER';
designJson?: any; // Unlayer design JSON
}
async function createTemplate(data: CreateTemplateInput) {
// Validate
if (data.editorType === 'BUILDER' && !data.designJson) {
throw new Error('Design JSON required for BUILDER templates');
}
// Create
return prisma.template.create({
data: {
// ... existing fields
editorType: data.editorType || 'HTML',
designJson: data.designJson,
}
});
}
5.3 Update API Client
File: apps/dashboard/src/api/client.ts
Changes:
interface CreateTemplateData {
// ... existing fields
editorType?: 'HTML' | 'BUILDER';
designJson?: any;
}
async createTemplate(data: CreateTemplateData) {
// ... implementation
}
Phase 6: Template Display Updates
6.1 Template Cards - Show Editor Type Badge
File: apps/dashboard/src/pages/Templates.tsx
Changes:
- Add badge showing "HTML" or "Visual Builder"
- Different icon for each type
- Filter by editor type (optional)
UI:
┌──────────────────────────────┐
│ Welcome Email [HTML] │ ← Badge
│ TRANSACTIONAL │
│ Created: 2 days ago │
│ [Edit] [Preview] [Delete] │
└──────────────────────────────┘
┌──────────────────────────────┐
│ Newsletter [🎨 Builder] │ ← Badge
│ MARKETING │
│ Created: 5 days ago │
│ [Edit] [Preview] [Delete] │
└──────────────────────────────┘
6.2 Template Detail Page
File: apps/dashboard/src/pages/TemplateDetail.tsx
Changes:
- Show editor type
- For BUILDER templates, show "Edit in Visual Builder" button
- For HTML templates, show "Edit Code" button
Phase 7: Features & Polish
7.1 Auto-Save Drafts
- Save design JSON to localStorage every 30 seconds
- Restore from localStorage if user navigates away
- Clear localStorage on successful save
7.2 Template Preview
- For BUILDER templates, export HTML and show preview
- For HTML templates, show existing preview functionality
7.3 Test Email
- Export HTML from Unlayer
- Send test email with exported HTML
- Show success/error toast
7.4 Unlayer Configuration
Customize Unlayer editor:
const unlayerOptions = {
projectId: YOUR_PROJECT_ID, // Get from Unlayer dashboard
displayMode: 'email',
appearance: {
theme: 'modern_light',
},
features: {
textEditor: {
spellChecker: true,
},
},
locale: 'en-US',
tools: {
// Customize available tools
},
blocks: {
// Custom content blocks
},
};
Implementation Order
Sprint 1: Foundation (Day 1-2)
- ✅ Add skeleton loaders to Templates page
- ✅ Create TemplateTypeModal component
- ✅ Update database schema
- ✅ Run migration
Sprint 2: Unlayer Integration (Day 3-4)
- ✅ Install Unlayer package
- ✅ Create TemplateBuilderVisual page
- ✅ Add routing for visual builder
- ✅ Basic save/load functionality
Sprint 3: API Integration (Day 5)
- ✅ Update API to accept editorType and designJson
- ✅ Update frontend API client
- ✅ Connect visual builder to API
Sprint 4: Polish & Testing (Day 6-7)
- ✅ Add editor type badges to template cards
- ✅ Update edit navigation logic
- ✅ Test entire flow (create, edit, preview, send)
- ✅ Add auto-save functionality
- ✅ Documentation
Technical Considerations
1. Design JSON Storage
- Store Unlayer design JSON in
designJsonfield (JSONB in Postgres) - This allows re-editing in visual builder
- HTML is generated from design JSON on save
2. Editor Type Immutability
- Once template is created with a type, it cannot be changed
- Prevents confusion and data loss
- Show warning if user tries to switch
3. Backward Compatibility
- All existing templates default to
editorType: 'HTML' designJsonis nullable- HTML templates work exactly as before
4. Performance
- Lazy load Unlayer editor (code splitting)
- Only load on builder routes
- Keep bundle size manageable
5. Data Migration
-- Set all existing templates to HTML type
UPDATE "Template" SET "editorType" = 'HTML' WHERE "editorType" IS NULL;
File Structure
apps/dashboard/src/
├── components/
│ └── TemplateTypeModal.tsx (NEW)
├── pages/
│ ├── Templates.tsx (UPDATED - add skeleton)
│ ├── TemplateBuilderVisual.tsx (NEW)
│ └── TemplateEditor.tsx (EXISTING - no changes)
└── App.tsx (UPDATED - add routes)
packages/api/src/
├── api/
│ └── controllers/
│ └── template.controller.ts (UPDATED)
└── services/
└── template.service.ts (UPDATED)
packages/shared/
└── prisma/
└── schema.prisma (UPDATED)
Testing Checklist
Functional Testing
- Create HTML template (existing flow still works)
- Create BUILDER template (new flow)
- Edit HTML template
- Edit BUILDER template
- Cannot edit HTML template in visual builder
- Cannot edit BUILDER template in code editor
- Preview both template types
- Send test email with both types
- Delete both template types
- Skeleton loaders show correctly
- Type selection modal works
- Navigation between types works
Edge Cases
- Navigate away without saving (auto-save)
- Restore from auto-save
- Large design JSON (performance)
- Invalid design JSON
- Template with no content
- Very long template names
Browser Testing
- Chrome
- Firefox
- Safari
- Edge
Success Criteria
- ✅ Users can choose between HTML and Visual Builder
- ✅ Skeleton loaders improve perceived performance
- ✅ Visual builder is intuitive and easy to use
- ✅ Templates created in visual builder can be re-edited
- ✅ HTML templates continue to work as before
- ✅ No breaking changes to existing functionality
- ✅ Performance remains acceptable
- ✅ All tests pass
Future Enhancements
Phase 2 Features (Future)
- Template preview thumbnails
- Template cloning (copy design JSON)
- Template versioning
- Shared template library
- Custom Unlayer blocks
- Template marketplace
- A/B testing variants
- Template analytics
Questions to Resolve
- Unlayer License: Free tier limitations? Need paid plan?
- Project ID: Where to store Unlayer project ID?
- Custom Blocks: Do we need custom content blocks initially?
- Auto-save: Save to backend or just localStorage?
- Template Conversion: Allow converting HTML → Builder? (complex)
Next Steps
- Review this plan
- Answer questions above
- Get approval to proceed
- Start Sprint 1 (skeleton loaders + modal)