PageEditor.jsx
import * as React from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { apiGet, apiPost, apiPut, apiDelete } from '../hooks/useAPI.js';
import { useChat } from '../layouts/AdminLayout.jsx';
import { MonacoEditor } from '../components/MonacoEditor.jsx';
export function PageEditor() {
const { toggleChat } = useChat() || {};
const { id } = useParams();
const navigate = useNavigate();
const [page, setPage] = useState(null);
const [pages, setPages] = useState([]);
const [versions, setVersions] = useState([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [tab, setTab] = useState('preview');
const [showEditModal, setShowEditModal] = useState(false);
const [showVersionDialog, setShowVersionDialog] = useState(false);
const [versionDialogTitle, setVersionDialogTitle] = useState('');
const [versionDialogContent, setVersionDialogContent] = useState(null);
const [codeValue, setCodeValue] = useState('');
const [originalCode, setOriginalCode] = useState('');
const [codeDirty, setCodeDirty] = useState(false);
const [showTemplateHelp, setShowTemplateHelp] = useState(false);
const iframeRef = useRef(null);
const fetchPage = useCallback(() => {
if (!id) return;
setLoading(true);
Promise.all([
apiGet(`/api/admin/pages/${id}`),
apiGet('/api/admin/pages'),
apiGet(`/api/admin/pages/${id}/versions`).catch(() => ({ items: [] })),
]).then(([pageData, pagesData, versionsData]) => {
setPage(pageData);
setPages(pagesData?.items || []);
setVersions(versionsData?.items || []);
setCodeValue(pageData?.html || '');
setOriginalCode(pageData?.html || '');
setCodeDirty(false);
}).finally(() => setLoading(false));
}, [id]);
useEffect(() => {
fetchPage();
}, [fetchPage]);
useEffect(() => {
setCodeDirty(codeValue !== originalCode);
}, [codeValue, originalCode]);
const togglePublish = async () => {
if (!id) return;
setSaving(true);
try {
await apiPost(`/api/admin/pages/${id}/publish`);
fetchPage();
} finally {
setSaving(false);
}
};
const saveCode = async () => {
if (!id || !codeDirty) return;
setSaving(true);
try {
await apiPost(`/api/admin/pages/${id}/code`, { html: codeValue });
setOriginalCode(codeValue);
setCodeDirty(false);
fetchPage();
} finally {
setSaving(false);
}
};
const handleEditSubmit = async (e) => {
e.preventDefault();
if (!id) return;
const form = e.target;
const formData = new FormData(form);
setSaving(true);
try {
await apiPut(`/api/admin/pages/${id}`, {
title: formData.get('title'),
description: formData.get('description'),
parentId: formData.get('parent_id') || '',
published: formData.get('published') === 'on',
});
setShowEditModal(false);
fetchPage();
} finally {
setSaving(false);
}
};
const deletePage = async () => {
if (!id || !confirm('Are you sure you want to delete this page?')) return;
await apiDelete(`/api/admin/pages/${id}`);
navigate('/pages');
};
const showVersionPreview = async (contentId, title) => {
setVersionDialogTitle(`Preview: ${title}`);
setVersionDialogContent(<div className="loading loading-spinner loading-lg mx-auto block" />);
setShowVersionDialog(true);
try {
const data = await apiGet(`/api/admin/pages/${id}/content/${contentId}`);
setVersionDialogContent(
<div className="prose prose-sm max-w-none bg-base-200 p-4 rounded-lg overflow-auto">
<pre className="whitespace-pre-wrap text-xs"><code>{data.html || 'No content'}</code></pre>
</div>
);
} catch (err) {
setVersionDialogContent(<div className="text-error">Failed to load version: {err.message}</div>);
}
};
const showVersionDiff = async (contentId, title) => {
setVersionDialogTitle(`Changes in: ${title}`);
setVersionDialogContent(<div className="loading loading-spinner loading-lg mx-auto block" />);
setShowVersionDialog(true);
try {
const [current, old] = await Promise.all([
apiGet(`/api/admin/pages/${id}/content/latest`),
apiGet(`/api/admin/pages/${id}/content/${contentId}`),
]);
const currentHtml = current.html || '';
const oldHtml = old.html || '';
// Simple line-by-line diff
const currentLines = currentHtml.split('\n');
const oldLines = oldHtml.split('\n');
setVersionDialogContent(
<div>
<div className="font-mono text-xs space-y-0 bg-base-200 rounded-lg p-2 overflow-auto max-h-96">
{oldLines.map((line, i) => {
const inCurrent = currentLines.includes(line);
if (!inCurrent) {
return (
<div key={`old-${i}`} className="bg-error/20 text-error line-through px-2 py-0.5">
<span className="select-none mr-2">-</span>{line}
</div>
);
}
return null;
})}
{currentLines.map((line, i) => {
const inOld = oldLines.includes(line);
return (
<div key={`cur-${i}`} className={`px-2 py-0.5 ${!inOld ? 'bg-success/20 text-success' : 'text-base-content/60'}`}>
<span className="select-none mr-2">{!inOld ? '+' : ' '}</span>{line}
</div>
);
})}
</div>
<p className="text-xs text-base-content/50 mt-2">Showing changes from selected version to current version. Green = added, Red = removed.</p>
</div>
);
} catch (err) {
setVersionDialogContent(<div className="text-error">Failed to load diff: {err.message}</div>);
}
};
const restoreVersion = async (contentId) => {
if (!confirm('Restore this version?')) return;
await apiPost(`/api/admin/pages/${id}/restore/${contentId}`);
fetchPage();
};
const handleIframeLoad = async () => {
try {
const iframe = iframeRef.current;
if (!iframe) return;
const iframePath = iframe.contentWindow?.location?.pathname;
if (!iframePath) return;
// Handle /preview/{id} URLs
if (iframePath.startsWith('/preview/')) {
const newPageId = iframePath.replace('/preview/', '').split('/')[0];
if (newPageId && newPageId !== id) {
navigate(`/pages/${newPageId}`);
}
return;
}
// Handle regular page URLs - find page by path
// First check our loaded pages
const matchedPage = pages.find(p => p.path === iframePath);
if (matchedPage && matchedPage.id !== id) {
navigate(`/pages/${matchedPage.id}`);
return;
}
// Fallback: path "/" = home, "/about" = about, etc.
const guessedId = iframePath === '/' ? 'home' : iframePath.replace(/^\//, '').replace(/\/$/, '');
if (guessedId && guessedId !== id && !guessedId.includes('/')) {
// Verify page exists before navigating
try {
await apiGet(`/api/admin/pages/${guessedId}`);
navigate(`/pages/${guessedId}`);
} catch {
// Page doesn't exist with this ID, ignore
}
}
} catch {
// Cross-origin access may fail, ignore
}
};
if (loading || !page) {
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between px-4 py-3 border-b border-base-300 bg-base-200/50">
<div className="skeleton h-6 w-48" />
</div>
<div className="flex-1 flex justify-center items-center">
<span className="loading loading-spinner loading-lg" />
</div>
</div>
);
}
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-base-300 bg-base-200/50">
<div className="flex items-center gap-3">
<h2 className="font-semibold">{page.title}</h2>
<button
onClick={togglePublish}
disabled={saving}
className={`badge badge-soft ${page.published ? 'badge-success' : 'badge-warning'} badge-sm cursor-pointer hover:opacity-80 transition-opacity`}
>
{page.published ? 'Published' : 'Draft'}
</button>
</div>
<div className="flex items-center gap-2">
{/* Tabs */}
<div className="tabs tabs-bordered">
<a className={`tab ${tab === 'preview' ? 'tab-active' : ''}`} onClick={() => setTab('preview')}>Preview</a>
<a className={`tab ${tab === 'code' ? 'tab-active' : ''}`} onClick={() => setTab('code')}>Code</a>
</div>
<button onClick={() => setShowEditModal(true)} className="btn btn-sm btn-ghost gap-1">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit
</button>
{toggleChat && (
<button onClick={toggleChat} className="btn btn-soft btn-primary btn-sm gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
AI Assistant
</button>
)}
</div>
</div>
{/* Preview Content */}
{tab === 'preview' && (
<div className="flex-1 overflow-hidden">
<iframe
ref={iframeRef}
src={`/preview/${id}`}
className="w-full h-full border-0 bg-white"
onLoad={handleIframeLoad}
/>
</div>
)}
{/* Code Content */}
{tab === 'code' && (
<div className="flex-1 overflow-hidden flex flex-col">
{/* Code Editor Toolbar */}
<div className="flex items-center justify-between px-4 py-2 bg-base-300/50 border-b border-base-300">
<div className="flex items-center gap-2">
<span className="text-xs text-base-content/50">Go HTML Template</span>
{codeDirty && <span className="badge badge-warning badge-xs">Unsaved</span>}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setShowTemplateHelp(true)}
className="btn btn-ghost btn-xs gap-1"
title="Template Reference"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Help
</button>
{codeDirty && (
<button onClick={saveCode} disabled={saving} className="btn btn-primary btn-sm gap-1">
{saving ? (
<span className="loading loading-spinner loading-sm" />
) : (
<>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
Save
</>
)}
</button>
)}
</div>
</div>
{/* Code Editor */}
<div className="flex-1 overflow-hidden relative">
<MonacoEditor
value={codeValue}
onChange={setCodeValue}
onSave={saveCode}
language="gohtml"
theme="gohtml-dark"
/>
</div>
</div>
)}
{/* Version History */}
{versions.length > 1 && (
<div className="w-full border-t border-base-300 bg-base-200/50">
<details className="collapse collapse-arrow">
<summary className="collapse-title text-sm font-medium py-2 min-h-0">
Version History ({versions.length})
</summary>
<div className="collapse-content px-0 w-full">
<ul className="menu menu-sm p-0 max-h-48 overflow-y-auto w-full">
{versions.map((v, i) => (
<li key={v.id}>
<div className="flex justify-between items-center">
<div className="flex flex-col">
<span className="text-xs font-medium">
{v.title}
{i === 0 && <span className="badge badge-success badge-xs ml-1">Current</span>}
</span>
<span className="text-xs text-base-content/50">
{new Date(v.created).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}
{v.creatorName && ` · ${v.creatorName}`}
</span>
</div>
{i !== 0 && (
<div className="flex gap-1">
<button onClick={() => showVersionPreview(v.id, v.title)} className="btn btn-ghost btn-xs">Preview</button>
<button onClick={() => showVersionDiff(v.id, v.title)} className="btn btn-ghost btn-xs">Diff</button>
<button onClick={() => restoreVersion(v.id)} className="btn btn-ghost btn-xs text-warning">Restore</button>
</div>
)}
</div>
</li>
))}
</ul>
</div>
</details>
</div>
)}
{/* Edit Page Modal */}
{showEditModal && (
<dialog className="modal modal-open">
<div className="modal-box w-11/12 max-w-2xl">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onClick={() => setShowEditModal(false)}>✕</button>
<h3 className="font-bold text-lg mb-4">Edit Page</h3>
<form onSubmit={handleEditSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="form-control">
<label className="floating-label">
<span>Title</span>
<input type="text" name="title" placeholder="Page Title" className="input input-bordered w-full" defaultValue={page.title} required autoFocus />
</label>
</div>
<div className="form-control">
<label className="floating-label">
<span>Slug</span>
<input type="text" name="slug" placeholder="page-slug" className="input input-bordered w-full bg-base-200" defaultValue={page.id} readOnly />
</label>
<label className="label">
<span className="label-text-alt text-base-content/50">Slug cannot be changed</span>
</label>
</div>
<div className="form-control md:col-span-2">
<label className="floating-label">
<span>Meta Description</span>
<input type="text" name="description" placeholder="Brief description for search engines" className="input input-bordered w-full" defaultValue={page.description || ''} />
</label>
</div>
<div className="form-control">
<label className="label">
<span className="label-text text-xs">Parent Page</span>
</label>
<select name="parent_id" className="select select-bordered select-sm w-full" defaultValue={page.parentId || ''}>
<option value="">No Parent (Root)</option>
{pages.filter(p => p.id !== page.id).map((p) => (
<option key={p.id} value={p.id}>{p.title}</option>
))}
</select>
</div>
<div className="form-control flex-row items-center gap-3 pt-5">
<input type="checkbox" name="published" className="checkbox checkbox-primary checkbox-sm" defaultChecked={page.published} />
<span className="text-sm">Published</span>
</div>
</div>
<div className="flex justify-end gap-2 pt-4">
<button type="button" onClick={deletePage} className="btn btn-ghost btn-sm text-error mr-auto">Delete</button>
<button type="button" className="btn btn-ghost btn-sm" onClick={() => setShowEditModal(false)}>Cancel</button>
<button type="submit" className="btn btn-primary btn-sm" disabled={saving}>
{saving ? <span className="loading loading-spinner loading-sm" /> : 'Save Changes'}
</button>
</div>
</form>
</div>
<div className="modal-backdrop" onClick={() => setShowEditModal(false)} />
</dialog>
)}
{/* Version Preview/Diff Dialog */}
{showVersionDialog && (
<dialog className="modal modal-open">
<div className="modal-box w-11/12 max-w-4xl max-h-[85vh]">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onClick={() => setShowVersionDialog(false)}>✕</button>
<h3 className="font-bold text-lg mb-4">{versionDialogTitle}</h3>
<div className="overflow-auto max-h-[65vh]">
{versionDialogContent}
</div>
<div className="flex justify-end gap-2 mt-4 pt-4 border-t border-base-300">
<button type="button" className="btn btn-ghost btn-sm" onClick={() => setShowVersionDialog(false)}>Close</button>
</div>
</div>
<div className="modal-backdrop" onClick={() => setShowVersionDialog(false)} />
</dialog>
)}
{/* Template Help Modal */}
{showTemplateHelp && (
<dialog className="modal modal-open">
<div className="modal-box w-11/12 max-w-4xl max-h-[85vh]">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onClick={() => setShowTemplateHelp(false)}>✕</button>
<h3 className="font-bold text-lg mb-4">Template Reference</h3>
<div className="overflow-auto max-h-[65vh] space-y-6">
{/* Syntax highlighting color classes */}
<style>{`
.hl-delim { color: #C586C0; font-weight: bold; }
.hl-keyword { color: #C586C0; }
.hl-func { color: #4EC9B0; }
.hl-builtin { color: #569CD6; }
.hl-var { color: #9CDCFE; }
.hl-string { color: #CE9178; }
.hl-number { color: #B5CEA8; }
.hl-tag { color: #569CD6; }
.hl-attr { color: #9CDCFE; }
.hl-code { background: #1e1e1e; padding: 0.75rem; border-radius: 0.5rem; }
`}</style>
{/* Basic Syntax */}
<div>
<h4 className="font-semibold text-sm mb-2 text-primary">Basic Syntax</h4>
<div className="hl-code font-mono text-xs space-y-1">
<div><span className="hl-delim">{'{{'}</span> expression <span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Output a value</span></div>
<div><span className="hl-delim">{'{{-'}</span> expression <span className="hl-delim">{'-}}'}</span> <span className="text-base-content/50">- Trim whitespace</span></div>
</div>
</div>
{/* Control Flow */}
<div>
<h4 className="font-semibold text-sm mb-2 text-primary">Control Flow</h4>
<div className="hl-code font-mono text-xs space-y-2">
<div className="border-b border-base-content/10 pb-2">
<div className="text-base-content/50 mb-1">Conditionals:</div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-keyword">if</span> <span className="hl-var">.Published</span><span className="hl-delim">{'}}'}</span>...<span className="hl-delim">{'{{'}</span><span className="hl-keyword">else</span><span className="hl-delim">{'}}'}</span>...<span className="hl-delim">{'{{'}</span><span className="hl-keyword">end</span><span className="hl-delim">{'}}'}</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-keyword">if</span> <span className="hl-builtin">eq</span> <span className="hl-var">.Status</span> <span className="hl-string">"active"</span><span className="hl-delim">{'}}'}</span>...<span className="hl-delim">{'{{'}</span><span className="hl-keyword">end</span><span className="hl-delim">{'}}'}</span></div>
</div>
<div className="border-b border-base-content/10 pb-2">
<div className="text-base-content/50 mb-1">Loops:</div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-keyword">range</span> <span className="hl-var">$item</span> := <span className="hl-var">.Items</span><span className="hl-delim">{'}}'}</span>...<span className="hl-delim">{'{{'}</span><span className="hl-keyword">end</span><span className="hl-delim">{'}}'}</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-keyword">range</span> <span className="hl-var">$i</span>, <span className="hl-var">$item</span> := <span className="hl-var">.Items</span><span className="hl-delim">{'}}'}</span>...<span className="hl-delim">{'{{'}</span><span className="hl-keyword">end</span><span className="hl-delim">{'}}'}</span></div>
</div>
<div>
<div className="text-base-content/50 mb-1">Scoping:</div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-keyword">with</span> <span className="hl-var">.Author</span><span className="hl-delim">{'}}'}</span><span className="hl-delim">{'{{'}</span><span className="hl-var">.Name</span><span className="hl-delim">{'}}'}</span><span className="hl-delim">{'{{'}</span><span className="hl-keyword">end</span><span className="hl-delim">{'}}'}</span></div>
</div>
</div>
</div>
{/* Site Functions */}
<div>
<h4 className="font-semibold text-sm mb-2 text-primary">Site Functions</h4>
<div className="hl-code font-mono text-xs space-y-1">
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">site_name</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Site name from settings</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">site_description</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Site description</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">user</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Current logged-in user (or nil)</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">path</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Current URL path</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">query</span> <span className="hl-string">"param"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Get query parameter</span></div>
</div>
</div>
{/* Data Functions */}
<div>
<h4 className="font-semibold text-sm mb-2 text-primary">Data Functions</h4>
<div className="hl-code font-mono text-xs space-y-2">
<div className="border-b border-base-content/10 pb-2">
<div className="text-base-content/50 mb-1">Pages:</div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">pages</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- All root pages</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">pages</span> <span className="hl-string">"parent-id"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Child pages</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">published_pages</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Published pages only</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">page</span> <span className="hl-string">"about"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Get page by ID</span></div>
</div>
<div className="border-b border-base-content/10 pb-2">
<div className="text-base-content/50 mb-1">Collections:</div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">documents</span> <span className="hl-string">"posts"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- All documents</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">documents</span> <span className="hl-string">"posts"</span> <span className="hl-string">"ORDER BY CreatedAt DESC"</span><span className="hl-delim">{'}}'}</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">document</span> <span className="hl-string">"doc-id"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Single document</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">collection</span> <span className="hl-string">"posts"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Collection info</span></div>
</div>
<div>
<div className="text-base-content/50 mb-1">Partials:</div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">partial</span> <span className="hl-string">"header"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Include partial</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">partial</span> <span className="hl-string">"nav"</span> <span className="hl-var">.</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- With context</span></div>
</div>
</div>
</div>
{/* Document Fields */}
<div>
<h4 className="font-semibold text-sm mb-2 text-primary">Document Fields</h4>
<div className="hl-code font-mono text-xs space-y-1">
<div><span className="hl-delim">{'{{'}</span><span className="hl-var">$doc</span><span className="hl-var">.GetString</span> <span className="hl-string">"title"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Get text field</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-var">$doc</span><span className="hl-var">.GetInt</span> <span className="hl-string">"count"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Get number</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-var">$doc</span><span className="hl-var">.GetBool</span> <span className="hl-string">"published"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Get boolean</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-var">$doc</span><span className="hl-var">.GetTime</span> <span className="hl-string">"date"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Get date/time</span></div>
</div>
</div>
{/* String Functions */}
<div>
<h4 className="font-semibold text-sm mb-2 text-primary">String Functions</h4>
<div className="hl-code font-mono text-xs space-y-1">
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">upper</span> <span className="hl-var">.Name</span><span className="hl-delim">{'}}'}</span> / <span className="hl-delim">{'{{'}</span><span className="hl-func">lower</span> <span className="hl-var">.Name</span><span className="hl-delim">{'}}'}</span> / <span className="hl-delim">{'{{'}</span><span className="hl-func">title</span> <span className="hl-var">.Name</span><span className="hl-delim">{'}}'}</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">truncate</span> <span className="hl-number">100</span> <span className="hl-var">.Description</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Truncate with "..."</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">substr</span> <span className="hl-var">.Text</span> <span className="hl-number">0</span> <span className="hl-number">50</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Substring</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">trim</span> <span className="hl-var">.Text</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Remove whitespace</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">replace</span> <span className="hl-string">"_"</span> <span className="hl-string">" "</span> <span className="hl-var">.Status</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Replace text</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">contains</span> <span className="hl-var">.Tags</span> <span className="hl-string">"featured"</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Check contains</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">join</span> <span className="hl-var">.Items</span> <span className="hl-string">", "</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Join array</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-var">.Content</span> | <span className="hl-func">html</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Render as HTML</span></div>
</div>
</div>
{/* Math & Numbers */}
<div>
<h4 className="font-semibold text-sm mb-2 text-primary">Math & Numbers</h4>
<div className="hl-code font-mono text-xs space-y-1">
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">add</span> <span className="hl-number">1</span> <span className="hl-number">2</span><span className="hl-delim">{'}}'}</span> / <span className="hl-delim">{'{{'}</span><span className="hl-func">sub</span> <span className="hl-number">5</span> <span className="hl-number">3</span><span className="hl-delim">{'}}'}</span> / <span className="hl-delim">{'{{'}</span><span className="hl-func">mul</span> <span className="hl-number">2</span> <span className="hl-number">4</span><span className="hl-delim">{'}}'}</span> / <span className="hl-delim">{'{{'}</span><span className="hl-func">div</span> <span className="hl-number">10</span> <span className="hl-number">2</span><span className="hl-delim">{'}}'}</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">mod</span> <span className="hl-number">7</span> <span className="hl-number">3</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Modulo</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">seq</span> <span className="hl-number">1</span> <span className="hl-number">10</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Sequence 1 to 10</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">number</span> <span className="hl-number">1234567</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Format: 1,234,567</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">currency</span> <span className="hl-number">99.99</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Format: $99.99</span></div>
</div>
</div>
{/* Date & Time */}
<div>
<h4 className="font-semibold text-sm mb-2 text-primary">Date & Time</h4>
<div className="hl-code font-mono text-xs space-y-1">
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">now</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Current time</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">date</span> <span className="hl-string">"2006-01-02"</span> <span className="hl-var">.CreatedAt</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Format date</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">date</span> <span className="hl-string">"Jan 2, 2006"</span> <span className="hl-var">.CreatedAt</span><span className="hl-delim">{'}}'}</span></div>
<div className="text-base-content/50 mt-1">Use Go reference time: Mon Jan 2 15:04:05 MST 2006</div>
</div>
</div>
{/* Comparisons */}
<div>
<h4 className="font-semibold text-sm mb-2 text-primary">Comparisons</h4>
<div className="hl-code font-mono text-xs space-y-1">
<div><span className="hl-delim">{'{{'}</span><span className="hl-builtin">eq</span> <span className="hl-var">.A</span> <span className="hl-var">.B</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Equal</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-builtin">ne</span> <span className="hl-var">.A</span> <span className="hl-var">.B</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Not equal</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-builtin">lt</span> <span className="hl-var">.A</span> <span className="hl-var">.B</span><span className="hl-delim">{'}}'}</span> / <span className="hl-delim">{'{{'}</span><span className="hl-builtin">le</span> <span className="hl-var">.A</span> <span className="hl-var">.B</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Less than (or equal)</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-builtin">gt</span> <span className="hl-var">.A</span> <span className="hl-var">.B</span><span className="hl-delim">{'}}'}</span> / <span className="hl-delim">{'{{'}</span><span className="hl-builtin">ge</span> <span className="hl-var">.A</span> <span className="hl-var">.B</span><span className="hl-delim">{'}}'}</span> <span className="text-base-content/50">- Greater than (or equal)</span></div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-builtin">and</span> <span className="hl-var">.A</span> <span className="hl-var">.B</span><span className="hl-delim">{'}}'}</span> / <span className="hl-delim">{'{{'}</span><span className="hl-builtin">or</span> <span className="hl-var">.A</span> <span className="hl-var">.B</span><span className="hl-delim">{'}}'}</span> / <span className="hl-delim">{'{{'}</span><span className="hl-builtin">not</span> <span className="hl-var">.A</span><span className="hl-delim">{'}}'}</span></div>
</div>
</div>
{/* Common Patterns */}
<div>
<h4 className="font-semibold text-sm mb-2 text-primary">Common Patterns</h4>
<div className="hl-code font-mono text-xs space-y-3">
<div>
<div className="text-base-content/50 mb-1">Navigation menu:</div>
<pre className="text-xs whitespace-pre-wrap"><span className="hl-delim">{'{{'}</span><span className="hl-keyword">range</span> <span className="hl-var">$p</span> := <span className="hl-func">published_pages</span><span className="hl-delim">{'}}'}</span>{'\n'} <span className="hl-tag"><a</span> <span className="hl-attr">href</span>=<span className="hl-string">"</span><span className="hl-delim">{'{{'}</span><span className="hl-var">$p</span><span className="hl-var">.Path</span><span className="hl-delim">{'}}'}</span><span className="hl-string">"</span><span className="hl-tag">></span><span className="hl-delim">{'{{'}</span><span className="hl-var">$p</span><span className="hl-var">.Title</span><span className="hl-delim">{'}}'}</span><span className="hl-tag"></a></span>{'\n'}<span className="hl-delim">{'{{'}</span><span className="hl-keyword">end</span><span className="hl-delim">{'}}'}</span></pre>
</div>
<div>
<div className="text-base-content/50 mb-1">Blog posts:</div>
<pre className="text-xs whitespace-pre-wrap"><span className="hl-delim">{'{{'}</span><span className="hl-keyword">range</span> <span className="hl-var">$post</span> := <span className="hl-func">documents</span> <span className="hl-string">"posts"</span> <span className="hl-string">"ORDER BY CreatedAt DESC"</span><span className="hl-delim">{'}}'}</span>{'\n'} <span className="hl-tag"><article></span>{'\n'} <span className="hl-tag"><h2></span><span className="hl-delim">{'{{'}</span><span className="hl-var">$post</span><span className="hl-var">.GetString</span> <span className="hl-string">"title"</span><span className="hl-delim">{'}}'}</span><span className="hl-tag"></h2></span>{'\n'} <span className="hl-tag"><p></span><span className="hl-delim">{'{{'}</span><span className="hl-func">truncate</span> <span className="hl-number">200</span> (<span className="hl-var">$post</span><span className="hl-var">.GetString</span> <span className="hl-string">"content"</span>)<span className="hl-delim">{'}}'}</span><span className="hl-tag"></p></span>{'\n'} <span className="hl-tag"></article></span>{'\n'}<span className="hl-delim">{'{{'}</span><span className="hl-keyword">end</span><span className="hl-delim">{'}}'}</span></pre>
</div>
<div>
<div className="text-base-content/50 mb-1">Default value:</div>
<div><span className="hl-delim">{'{{'}</span><span className="hl-func">default</span> <span className="hl-string">"Untitled"</span> <span className="hl-var">.Title</span><span className="hl-delim">{'}}'}</span></div>
</div>
</div>
</div>
{/* JavaScript API */}
<div>
<h4 className="font-semibold text-sm mb-2 text-primary">JavaScript API (for forms)</h4>
<div className="hl-code font-mono text-xs space-y-2">
<div className="text-base-content/50 mb-1">Submit form data to a collection:</div>
<pre className="text-xs whitespace-pre-wrap text-base-content/80">{`<form onsubmit="submitForm(event, 'ideas')">
<input name="title" required>
<textarea name="content"></textarea>
<button type="submit">Submit</button>
</form>
<script>
async function submitForm(e, collection) {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.target));
const res = await fetch('/api/collections/' + collection + '/records', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
e.target.reset();
alert('Submitted!');
}
}
</script>`}</pre>
<div className="border-t border-base-content/10 pt-2 mt-2">
<div className="text-base-content/50 mb-1">API Endpoints:</div>
<div className="space-y-1">
<div><span className="hl-keyword">GET</span> <span className="hl-string">/api/collections/{'{id}'}/records</span> <span className="text-base-content/50">- List records</span></div>
<div><span className="hl-keyword">POST</span> <span className="hl-string">/api/collections/{'{id}'}/records</span> <span className="text-base-content/50">- Create record</span></div>
<div><span className="hl-keyword">PATCH</span> <span className="hl-string">/api/collections/{'{id}'}/records/{'{recordId}'}</span> <span className="text-base-content/50">- Update</span></div>
<div><span className="hl-keyword">DELETE</span> <span className="hl-string">/api/collections/{'{id}'}/records/{'{recordId}'}</span> <span className="text-base-content/50">- Delete</span></div>
</div>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 mt-4 pt-4 border-t border-base-300">
<button type="button" className="btn btn-ghost btn-sm" onClick={() => setShowTemplateHelp(false)}>Close</button>
</div>
</div>
<div className="modal-backdrop" onClick={() => setShowTemplateHelp(false)} />
</dialog>
)}
</div>
);
}