import React, { useEffect, useState } from 'react'; import type { PageContentType, PageTemplate, } from '../../service/visibility/visibility.interface'; import { createPageTemplate, updatePageTemplate, } from '../../service/visibility/visibility.service'; import { contentTypeLabel, PAGE_CONTENT_TYPES } from './helpers'; import Modal from './Modal'; /** Matches the backend ``default_instructions`` cap. */ const MAX_INSTRUCTIONS_CHARS = 4000; /** Props for {@link PageTemplateEditorModal}. */ interface PageTemplateEditorModalProps { open: boolean; onClose: () => void; clientId: string; token: string; brandId: string; /** Brand language, stamped on new templates. */ language: string; /** The template being edited, or ``null`` to create a new one. */ template: PageTemplate | null; /** Fired with the saved row so the parent can merge it into the gallery. */ onSaved: (saved: PageTemplate) => void; } /** Split a textarea's "one per line" value into a trimmed, non-empty list. */ const linesToList = (value: string): string[] => value .split('\n') .map(line => line.trim()) .filter(line => line.length > 0); /** * Create / edit modal for a saved page-content brief. Backs both the "+ Create * new" entry and each card's "Edit" action. Calls the visibility service * directly (client-side), mirroring the generate modals. * * @param {PageTemplateEditorModalProps} props - Modal props. * @returns {JSX.Element} The modal. */ const PageTemplateEditorModal = ({ open, onClose, clientId, token, brandId, language, template, onSaved, }: PageTemplateEditorModalProps): JSX.Element | null => { const [name, setName] = useState(''); const [contentType, setContentType] = useState('blog'); const [description, setDescription] = useState(''); const [instructions, setInstructions] = useState(''); const [targetPrompts, setTargetPrompts] = useState(''); const [keywords, setKeywords] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); // A saved template carries a non-empty id; a preset fork (id "") or a fresh // template does not, so it saves as a new merchant-owned row. const isEdit = Boolean(template?.template_id); // Re-seed from the edited row (or blank for create) each time the modal opens. useEffect(() => { if (!open) return; setName(template?.name ?? ''); setContentType(template?.content_type ?? 'blog'); setDescription(template?.description ?? ''); setInstructions(template?.default_instructions ?? ''); setTargetPrompts((template?.default_target_prompts ?? []).join('\n')); setKeywords((template?.default_keywords ?? []).join('\n')); setError(null); setBusy(false); }, [open, template]); const handleSave = async (): Promise => { if (name.trim().length === 0) { setError('Give the template a name.'); return; } setBusy(true); setError(null); const body = { content_type: contentType, name: name.trim(), description: description.trim(), default_instructions: instructions.trim(), default_target_prompts: linesToList(targetPrompts), default_keywords: linesToList(keywords), }; try { // Only a saved template (non-empty id) is patched; a preset fork or a // fresh template creates a new merchant-owned row. const saved = isEdit ? await updatePageTemplate( clientId, token, brandId, template!.template_id, body ) : await createPageTemplate(clientId, token, brandId, { ...body, language, }); onClose(); onSaved(saved); } catch (err) { setError( err instanceof Error ? err.message : 'Could not save the template. Try again.' ); } finally { setBusy(false); } }; return ( } >

Save a reusable brief: a page type plus a default directive you can replay from the gallery, tweaking it per run.

{error && (
{error}
)}
Page type
{PAGE_CONTENT_TYPES.map(type => ( ))}