import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/locales'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router'; import type { FieldConfig } from '../lib/templates'; import { templates } from '../lib/templates'; import { viewports } from '../lib/viewports'; import { PropFields } from './prop-fields'; type Theme = 'light' | 'dark'; const GROUP_ORDER = ['Documents', 'Recipients', 'Organisations', 'Teams', 'Account', 'Admin'] as const; const LANGUAGE_LABELS: Record = { en: 'English', de: 'German', fr: 'French', es: 'Spanish', it: 'Italian', nl: 'Dutch', pl: 'Polish', 'pt-BR': 'Portuguese (Brazil)', ja: 'Japanese', ko: 'Korean', zh: 'Chinese', }; const DEFAULT_COLORS = { primary: '#a2e771', primaryForeground: '#162c07', background: '#ffffff', foreground: '#0f172a', }; type PlaygroundProps = { slug: string; fields: Record; defaultProps: Record; }; export const EmailPlayground = ({ slug, fields, defaultProps }: PlaygroundProps) => { const navigate = useNavigate(); const [props, setProps] = useState(defaultProps); const [html, setHtml] = useState(''); const [loading, setLoading] = useState(false); const [theme, setTheme] = useState('light'); const [viewportIndex, setViewportIndex] = useState(2); const [lang, setLang] = useState('en'); const [brandingEnabled, setBrandingEnabled] = useState(false); const [colors, setColors] = useState(DEFAULT_COLORS); const debounceRef = useRef>(undefined); const groupedTemplates = useMemo(() => { const entries = Object.entries(templates); return GROUP_ORDER.map((group) => ({ group, entries: entries.filter(([, def]) => def.group === group), })).filter((section) => section.entries.length > 0); }, []); const fetchHtml = useCallback( async (currentProps: Record, currentLang: string, brandColors: typeof colors | null) => { setLoading(true); try { const response = await fetch('/api/render', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug, props: currentProps, lang: currentLang, colors: brandColors, assetBaseUrl: window.location.origin, }), }); if (response.ok) { setHtml(await response.text()); } } finally { setLoading(false); } }, [slug], ); // Reset props when navigating to a different template. useEffect(() => { setProps(defaultProps); }, [defaultProps]); // Re-render on any input change (debounced). useEffect(() => { clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { void fetchHtml(props, lang, brandingEnabled ? colors : null); }, 250); return () => clearTimeout(debounceRef.current); }, [props, lang, brandingEnabled, colors, fetchHtml]); const handlePropChange = (key: string, value: unknown) => { setProps((prev) => ({ ...prev, [key]: value })); }; const handleColorChange = (key: keyof typeof colors, value: string) => { setColors((prev) => ({ ...prev, [key]: value })); }; // Force dark mode inside the iframe by neutralising the prefers-color-scheme // media query (color-scheme alone doesn't trigger it inside an iframe). const displayHtml = theme === 'dark' && html ? html.replaceAll(/prefers-color-scheme:\s*dark/g, 'min-width:0') : html; const viewport = viewports[viewportIndex]; return (
{/* Sidebar */} {/* Props panel */}

Props

{/* Main */}