|
|
|
|
@@ -1,89 +1,422 @@
|
|
|
|
|
{% extends "admin/base_admin.html" %}
|
|
|
|
|
{% set admin_page = "articles" %}
|
|
|
|
|
|
|
|
|
|
{% block title %}{% if editing %}Edit{% else %}New{% endif %} Article - Admin - {{ config.APP_NAME }}{% endblock %}
|
|
|
|
|
{% block title %}{% if editing %}Edit{% else %}New{% endif %} Article — Admin — {{ config.APP_NAME }}{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block head %}{{ super() }}
|
|
|
|
|
<style>
|
|
|
|
|
/* Override admin-main so the split editor fills the column */
|
|
|
|
|
.admin-main {
|
|
|
|
|
padding: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Editor shell ──────────────────────────────────────────── */
|
|
|
|
|
.ae-shell {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Toolbar ────────────────────────────────────────────────── */
|
|
|
|
|
.ae-toolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
padding: 0.625rem 1.25rem;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-bottom: 1px solid #E2E8F0;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.ae-toolbar__back {
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
color: #64748B;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
transition: color 0.1s;
|
|
|
|
|
}
|
|
|
|
|
.ae-toolbar__back:hover { color: #0F172A; }
|
|
|
|
|
.ae-toolbar__sep {
|
|
|
|
|
width: 1px; height: 1.25rem;
|
|
|
|
|
background: #E2E8F0;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.ae-toolbar__title {
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #0F172A;
|
|
|
|
|
flex: 1;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
.ae-toolbar__status {
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
padding: 0.2rem 0.55rem;
|
|
|
|
|
border-radius: 9999px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.ae-toolbar__status--draft {
|
|
|
|
|
background: #F1F5F9;
|
|
|
|
|
color: #64748B;
|
|
|
|
|
}
|
|
|
|
|
.ae-toolbar__status--published {
|
|
|
|
|
background: #DCFCE7;
|
|
|
|
|
color: #16A34A;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Metadata strip ─────────────────────────────────────────── */
|
|
|
|
|
#ae-form {
|
|
|
|
|
display: contents; /* form participates in flex layout as transparent wrapper */
|
|
|
|
|
}
|
|
|
|
|
.ae-meta {
|
|
|
|
|
padding: 0.75rem 1.25rem;
|
|
|
|
|
background: #F8FAFC;
|
|
|
|
|
border-bottom: 1px solid #E2E8F0;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.ae-meta__row {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0.625rem;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
align-items: end;
|
|
|
|
|
}
|
|
|
|
|
.ae-meta__row + .ae-meta__row { margin-top: 0.5rem; }
|
|
|
|
|
|
|
|
|
|
.ae-field {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 0.2rem;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
.ae-field--flex1 { flex: 1; min-width: 120px; }
|
|
|
|
|
.ae-field--flex2 { flex: 2; min-width: 180px; }
|
|
|
|
|
.ae-field--flex3 { flex: 3; min-width: 220px; }
|
|
|
|
|
.ae-field--fixed80 { flex: 0 0 80px; }
|
|
|
|
|
.ae-field--fixed120 { flex: 0 0 120px; }
|
|
|
|
|
.ae-field--fixed160 { flex: 0 0 160px; }
|
|
|
|
|
|
|
|
|
|
.ae-field label {
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.07em;
|
|
|
|
|
color: #94A3B8;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
.ae-field input,
|
|
|
|
|
.ae-field select {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 0.3rem 0.5rem;
|
|
|
|
|
border: 1px solid #E2E8F0;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
font-family: var(--font-sans);
|
|
|
|
|
color: #0F172A;
|
|
|
|
|
background: #fff;
|
|
|
|
|
outline: none;
|
|
|
|
|
transition: border-color 0.15s, box-shadow 0.15s;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
.ae-field input:focus,
|
|
|
|
|
.ae-field select:focus {
|
|
|
|
|
border-color: #1D4ED8;
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(29,78,216,0.1);
|
|
|
|
|
}
|
|
|
|
|
.ae-field input[readonly] {
|
|
|
|
|
background: #F1F5F9;
|
|
|
|
|
color: #94A3B8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Split pane ─────────────────────────────────────────────── */
|
|
|
|
|
.ae-split {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
.ae-pane {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
.ae-pane--editor { border-right: 1px solid #E2E8F0; }
|
|
|
|
|
|
|
|
|
|
.ae-pane__header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 0.375rem 0.875rem;
|
|
|
|
|
background: #1E293B;
|
|
|
|
|
border-bottom: 1px solid #0F172A;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.ae-pane--preview .ae-pane__header {
|
|
|
|
|
background: #F8FAFC;
|
|
|
|
|
border-bottom: 1px solid #E2E8F0;
|
|
|
|
|
}
|
|
|
|
|
.ae-pane__label {
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.09em;
|
|
|
|
|
color: #94A3B8;
|
|
|
|
|
}
|
|
|
|
|
.ae-pane--editor .ae-pane__label { color: #475569; }
|
|
|
|
|
.ae-pane__hint {
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
color: #475569;
|
|
|
|
|
}
|
|
|
|
|
.ae-pane--preview .ae-pane__hint { color: #94A3B8; }
|
|
|
|
|
|
|
|
|
|
/* The markdown textarea */
|
|
|
|
|
.ae-editor {
|
|
|
|
|
flex: 1;
|
|
|
|
|
resize: none;
|
|
|
|
|
border: none;
|
|
|
|
|
outline: none;
|
|
|
|
|
padding: 1.125rem 1.25rem;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
line-height: 1.75;
|
|
|
|
|
background: #0F172A;
|
|
|
|
|
color: #CBD5E1;
|
|
|
|
|
caret-color: #3B82F6;
|
|
|
|
|
tab-size: 2;
|
|
|
|
|
}
|
|
|
|
|
.ae-editor::placeholder { color: #334155; }
|
|
|
|
|
.ae-editor:focus { outline: none; }
|
|
|
|
|
|
|
|
|
|
/* Preview pane */
|
|
|
|
|
.ae-preview {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 1.5rem 2rem;
|
|
|
|
|
background: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Preview typography — maps rendered markdown */
|
|
|
|
|
.ae-preview .preview-body { max-width: 42rem; }
|
|
|
|
|
.ae-preview .preview-body h1 {
|
|
|
|
|
font-family: var(--font-display);
|
|
|
|
|
font-size: 1.625rem; font-weight: 700;
|
|
|
|
|
color: #0F172A; margin: 0 0 1rem;
|
|
|
|
|
line-height: 1.25;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body h2 {
|
|
|
|
|
font-family: var(--font-display);
|
|
|
|
|
font-size: 1.25rem; font-weight: 600;
|
|
|
|
|
color: #0F172A; margin: 1.75rem 0 0.625rem;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body h3 {
|
|
|
|
|
font-size: 1.0625rem; font-weight: 600;
|
|
|
|
|
color: #0F172A; margin: 1.25rem 0 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body h4 {
|
|
|
|
|
font-size: 0.9375rem; font-weight: 600;
|
|
|
|
|
color: #334155; margin: 1rem 0 0.375rem;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body p { margin: 0 0 0.875rem; color: #1E293B; line-height: 1.75; }
|
|
|
|
|
.ae-preview .preview-body ul,
|
|
|
|
|
.ae-preview .preview-body ol { margin: 0 0 0.875rem 1.375rem; color: #1E293B; }
|
|
|
|
|
.ae-preview .preview-body li { margin: 0.3rem 0; line-height: 1.65; }
|
|
|
|
|
.ae-preview .preview-body code {
|
|
|
|
|
font-family: var(--font-mono); font-size: 0.8125rem;
|
|
|
|
|
background: #F1F5F9; color: #1D4ED8;
|
|
|
|
|
padding: 0.1rem 0.35rem; border-radius: 3px;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body pre {
|
|
|
|
|
background: #0F172A; color: #CBD5E1;
|
|
|
|
|
padding: 1rem 1.125rem; border-radius: 6px;
|
|
|
|
|
overflow-x: auto; margin: 0 0 0.875rem;
|
|
|
|
|
font-size: 0.8125rem; line-height: 1.65;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body pre code {
|
|
|
|
|
background: none; color: inherit; padding: 0;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body blockquote {
|
|
|
|
|
border-left: 3px solid #1D4ED8;
|
|
|
|
|
padding-left: 1rem; margin: 0 0 0.875rem;
|
|
|
|
|
color: #475569;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body a { color: #1D4ED8; }
|
|
|
|
|
.ae-preview .preview-body hr {
|
|
|
|
|
border: none; border-top: 1px solid #E2E8F0;
|
|
|
|
|
margin: 1.5rem 0;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body strong { font-weight: 600; color: #0F172A; }
|
|
|
|
|
.ae-preview .preview-body table {
|
|
|
|
|
width: 100%; border-collapse: collapse;
|
|
|
|
|
font-size: 0.875rem; margin: 0 0 0.875rem;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body th {
|
|
|
|
|
background: #F8FAFC; font-weight: 600;
|
|
|
|
|
padding: 0.5rem 0.75rem; text-align: left;
|
|
|
|
|
border: 1px solid #E2E8F0; color: #0F172A;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body td {
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
border: 1px solid #E2E8F0; color: #1E293B;
|
|
|
|
|
}
|
|
|
|
|
.ae-preview .preview-body tr:nth-child(even) td { background: #F8FAFC; }
|
|
|
|
|
|
|
|
|
|
.preview-placeholder {
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
color: #94A3B8;
|
|
|
|
|
font-style: italic;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* HTMX loading indicator — htmx toggles .htmx-request on the element */
|
|
|
|
|
.ae-loading {
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
color: #94A3B8;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.15s;
|
|
|
|
|
}
|
|
|
|
|
.ae-loading.htmx-request { opacity: 1; }
|
|
|
|
|
</style>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block admin_content %}
|
|
|
|
|
<div style="max-width: 48rem; margin: 0 auto;">
|
|
|
|
|
<a href="{{ url_for('admin.articles') }}" class="text-sm text-slate">← Back to articles</a>
|
|
|
|
|
<h1 class="text-2xl mt-4 mb-6">{% if editing %}Edit{% else %}New{% endif %} Article</h1>
|
|
|
|
|
<div class="ae-shell">
|
|
|
|
|
|
|
|
|
|
<form method="post" class="card">
|
|
|
|
|
<!-- Toolbar -->
|
|
|
|
|
<div class="ae-toolbar">
|
|
|
|
|
<a href="{{ url_for('admin.articles') }}" class="ae-toolbar__back">← Articles</a>
|
|
|
|
|
<div class="ae-toolbar__sep"></div>
|
|
|
|
|
<span class="ae-toolbar__title">
|
|
|
|
|
{% if editing %}{{ data.get('title', 'Edit Article') }}{% else %}New Article{% endif %}
|
|
|
|
|
</span>
|
|
|
|
|
{% if editing %}
|
|
|
|
|
<span class="ae-toolbar__status ae-toolbar__status--{{ data.get('status', 'draft') }}">
|
|
|
|
|
{{ data.get('status', 'draft') }}
|
|
|
|
|
</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
<button form="ae-form" type="submit" class="btn btn-sm">
|
|
|
|
|
{% if editing %}Save Changes{% else %}Create Article{% endif %}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Form wraps everything below the toolbar -->
|
|
|
|
|
<form id="ae-form" method="post" style="display:contents;">
|
|
|
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
|
|
|
|
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;" class="mb-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label" for="title">Title</label>
|
|
|
|
|
<input type="text" id="title" name="title" value="{{ data.get('title', '') }}" class="form-input" required>
|
|
|
|
|
<!-- Metadata strip -->
|
|
|
|
|
<div class="ae-meta">
|
|
|
|
|
<div class="ae-meta__row">
|
|
|
|
|
<div class="ae-field ae-field--flex3">
|
|
|
|
|
<label for="title">Title</label>
|
|
|
|
|
<input type="text" id="title" name="title" value="{{ data.get('title', '') }}"
|
|
|
|
|
required placeholder="Article title…">
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label" for="slug">Slug</label>
|
|
|
|
|
<input type="text" id="slug" name="slug" value="{{ data.get('slug', '') }}" class="form-input"
|
|
|
|
|
placeholder="auto-generated from title" {% if editing %}readonly{% endif %}>
|
|
|
|
|
<div class="ae-field ae-field--flex2">
|
|
|
|
|
<label for="slug">Slug</label>
|
|
|
|
|
<input type="text" id="slug" name="slug" value="{{ data.get('slug', '') }}"
|
|
|
|
|
placeholder="auto-generated" {% if editing %}readonly{% endif %}>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ae-field ae-field--flex2">
|
|
|
|
|
<label for="url_path">URL Path</label>
|
|
|
|
|
<input type="text" id="url_path" name="url_path" value="{{ data.get('url_path', '') }}"
|
|
|
|
|
placeholder="/slug">
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
<label class="form-label" for="url_path">URL Path</label>
|
|
|
|
|
<input type="text" id="url_path" name="url_path" value="{{ data.get('url_path', '') }}" class="form-input"
|
|
|
|
|
placeholder="e.g. /padel-court-cost-miami">
|
|
|
|
|
<p class="form-hint">Defaults to /slug. Must not conflict with existing routes.</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
<label class="form-label" for="meta_description">Meta Description</label>
|
|
|
|
|
<input type="text" id="meta_description" name="meta_description" value="{{ data.get('meta_description', '') }}"
|
|
|
|
|
class="form-input" maxlength="160">
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem;" class="mb-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label" for="country">Country</label>
|
|
|
|
|
<input type="text" id="country" name="country" value="{{ data.get('country', '') }}" class="form-input"
|
|
|
|
|
placeholder="e.g. US">
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label" for="region">Region</label>
|
|
|
|
|
<input type="text" id="region" name="region" value="{{ data.get('region', '') }}" class="form-input"
|
|
|
|
|
placeholder="e.g. North America">
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label" for="og_image_url">OG Image URL</label>
|
|
|
|
|
<input type="text" id="og_image_url" name="og_image_url" value="{{ data.get('og_image_url', '') }}" class="form-input">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
<label class="form-label" for="body">Body (Markdown)</label>
|
|
|
|
|
<textarea id="body" name="body" rows="20" class="form-input"
|
|
|
|
|
style="font-family: var(--font-mono); font-size: 0.8125rem;" {% if not editing %}required{% endif %}>{{ data.get('body', '') }}</textarea>
|
|
|
|
|
<p class="form-hint">Use [scenario:slug] to embed scenario widgets. Sections: :capex, :operating, :cashflow, :returns, :full</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem;" class="mb-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label" for="language">Language</label>
|
|
|
|
|
<select id="language" name="language" class="form-input">
|
|
|
|
|
<option value="en" {% if data.get('language', 'en') == 'en' %}selected{% endif %}>English (en)</option>
|
|
|
|
|
<option value="de" {% if data.get('language') == 'de' %}selected{% endif %}>German (de)</option>
|
|
|
|
|
<div class="ae-field ae-field--fixed80">
|
|
|
|
|
<label for="language">Language</label>
|
|
|
|
|
<select id="language" name="language">
|
|
|
|
|
<option value="en" {% if data.get('language', 'en') == 'en' %}selected{% endif %}>EN</option>
|
|
|
|
|
<option value="de" {% if data.get('language') == 'de' %}selected{% endif %}>DE</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label" for="status">Status</label>
|
|
|
|
|
<select id="status" name="status" class="form-input">
|
|
|
|
|
<option value="draft" {% if data.get('status') == 'draft' %}selected{% endif %}>Draft</option>
|
|
|
|
|
<div class="ae-field ae-field--fixed120">
|
|
|
|
|
<label for="status">Status</label>
|
|
|
|
|
<select id="status" name="status">
|
|
|
|
|
<option value="draft" {% if data.get('status', 'draft') == 'draft' %}selected{% endif %}>Draft</option>
|
|
|
|
|
<option value="published" {% if data.get('status') == 'published' %}selected{% endif %}>Published</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label" for="published_at">Publish Date</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ae-meta__row">
|
|
|
|
|
<div class="ae-field ae-field--flex3">
|
|
|
|
|
<label for="meta_description">Meta Description</label>
|
|
|
|
|
<input type="text" id="meta_description" name="meta_description"
|
|
|
|
|
value="{{ data.get('meta_description', '') }}" maxlength="160"
|
|
|
|
|
placeholder="160 chars max…">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ae-field ae-field--flex1">
|
|
|
|
|
<label for="country">Country</label>
|
|
|
|
|
<input type="text" id="country" name="country" value="{{ data.get('country', '') }}"
|
|
|
|
|
placeholder="e.g. US">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ae-field ae-field--flex1">
|
|
|
|
|
<label for="region">Region</label>
|
|
|
|
|
<input type="text" id="region" name="region" value="{{ data.get('region', '') }}"
|
|
|
|
|
placeholder="e.g. North America">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ae-field ae-field--flex2">
|
|
|
|
|
<label for="og_image_url">OG Image URL</label>
|
|
|
|
|
<input type="text" id="og_image_url" name="og_image_url"
|
|
|
|
|
value="{{ data.get('og_image_url', '') }}">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ae-field ae-field--fixed160">
|
|
|
|
|
<label for="published_at">Publish Date</label>
|
|
|
|
|
<input type="datetime-local" id="published_at" name="published_at"
|
|
|
|
|
value="{{ data.get('published_at', '')[:16] if data.get('published_at') else '' }}" class="form-input">
|
|
|
|
|
<p class="form-hint">Leave blank for now. Future date = scheduled.</p>
|
|
|
|
|
value="{{ data.get('published_at', '')[:16] if data.get('published_at') else '' }}">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button type="submit" class="btn" style="width: 100%;">{% if editing %}Update Article{% else %}Create Article{% endif %}</button>
|
|
|
|
|
<!-- Split: editor | preview -->
|
|
|
|
|
<div class="ae-split">
|
|
|
|
|
|
|
|
|
|
<!-- Left — Markdown editor -->
|
|
|
|
|
<div class="ae-pane ae-pane--editor">
|
|
|
|
|
<div class="ae-pane__header">
|
|
|
|
|
<span class="ae-pane__label">Markdown</span>
|
|
|
|
|
<span class="ae-pane__hint">[scenario:slug] · [product:slug]</span>
|
|
|
|
|
</div>
|
|
|
|
|
<textarea
|
|
|
|
|
id="body" name="body"
|
|
|
|
|
class="ae-editor"
|
|
|
|
|
{% if not editing %}required{% endif %}
|
|
|
|
|
placeholder="Start writing in Markdown…"
|
|
|
|
|
hx-post="{{ url_for('admin.article_preview') }}"
|
|
|
|
|
hx-trigger="input delay:500ms"
|
|
|
|
|
hx-target="#ae-preview-content"
|
|
|
|
|
hx-include="[name=csrf_token]"
|
|
|
|
|
hx-indicator="#ae-loading"
|
|
|
|
|
>{{ data.get('body', '') }}</textarea>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Right — Rendered preview -->
|
|
|
|
|
<div class="ae-pane ae-pane--preview">
|
|
|
|
|
<div class="ae-pane__header">
|
|
|
|
|
<span class="ae-pane__label">Preview</span>
|
|
|
|
|
<span id="ae-loading" class="ae-loading">Rendering…</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ae-preview">
|
|
|
|
|
<div id="ae-preview-content">
|
|
|
|
|
{% if body_html %}
|
|
|
|
|
<div class="preview-body">{{ body_html }}</div>
|
|
|
|
|
{% else %}
|
|
|
|
|
<p class="preview-placeholder">Start writing to see a preview.</p>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|