import React from 'react' import { Head, Link, usePage } from '@inertiajs/react' function requestJson(url, { method = 'GET', body } = {}) { return fetch(url, { method, credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '', 'X-Requested-With': 'XMLHttpRequest', }, body: body ? JSON.stringify(body) : undefined, }).then(async (response) => { const payload = await response.json().catch(() => ({})) if (!response.ok) throw new Error(payload?.message || 'Request failed') return payload }) } export default function NovaCardsTemplateAdmin() { const { props } = usePage() const [templates, setTemplates] = React.useState(props.templates || []) const [selectedId, setSelectedId] = React.useState(null) const [form, setForm] = React.useState({ slug: '', name: '', description: '', supported_formats: ['square'], active: true, official: true, order_num: templates.length, config_json: { font_preset: 'modern-sans', gradient_preset: 'midnight-nova', text_align: 'center', layout: 'quote_heavy', text_color: '#ffffff', overlay_style: 'dark-soft', }, }) const endpoints = props.endpoints || {} const formats = props.editorOptions?.formats || [] const fonts = props.editorOptions?.font_presets || [] const gradients = props.editorOptions?.gradient_presets || [] function loadTemplate(template) { setSelectedId(template.id) setForm({ slug: template.slug, name: template.name, description: template.description || '', preview_image: template.preview_image || null, supported_formats: template.supported_formats || [], active: Boolean(template.active), official: Boolean(template.official), order_num: template.order_num || 0, config_json: template.config_json || {}, }) } function resetForm() { setSelectedId(null) setForm({ slug: '', name: '', description: '', supported_formats: ['square'], active: true, official: true, order_num: templates.length, config_json: { font_preset: 'modern-sans', gradient_preset: 'midnight-nova', text_align: 'center', layout: 'quote_heavy', text_color: '#ffffff', overlay_style: 'dark-soft', }, }) } async function saveTemplate() { const isExisting = Boolean(selectedId) const url = isExisting ? String(endpoints.updatePattern || '').replace('__TEMPLATE__', String(selectedId)) : endpoints.store const response = await requestJson(url, { method: isExisting ? 'PATCH' : 'POST', body: form, }) if (isExisting) { setTemplates((current) => current.map((template) => (template.id === selectedId ? response.template : template))) } else { setTemplates((current) => [...current, response.template]) setSelectedId(response.template.id) } } function toggleFormat(key) { setForm((current) => { const exists = current.supported_formats.includes(key) return { ...current, supported_formats: exists ? current.supported_formats.filter((item) => item !== key) : [...current.supported_formats, key], } }) } return (

Template system

Official Nova Cards templates

Keep starter templates config-driven so the editor and render pipeline stay aligned as new card styles ship.

Back to cards
Existing templates
{templates.map((template) => ( ))}
Template editor
setForm((current) => ({ ...current, name: event.target.value }))} placeholder="Template name" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" /> setForm((current) => ({ ...current, slug: event.target.value }))} placeholder="Slug" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />