This commit is contained in:
parent
13539d6300
commit
d14a81e523
1 changed files with 198 additions and 186 deletions
|
|
@ -17,6 +17,32 @@ const latestEntryDate = latestEntry?.data.published
|
||||||
: "No date";
|
: "No date";
|
||||||
const totalEvents = calendarEvents.length + recurringCalendarEvents.length;
|
const totalEvents = calendarEvents.length + recurringCalendarEvents.length;
|
||||||
const configuredServices = [contactFormEndpoint, plausibleDomain].filter(Boolean).length;
|
const configuredServices = [contactFormEndpoint, plausibleDomain].filter(Boolean).length;
|
||||||
|
const speechAdminEntries = speechEntries.map((entry) => {
|
||||||
|
const categories = entry.data.categories ?? [];
|
||||||
|
const tags = entry.data.tags ?? [];
|
||||||
|
const publishedDate = entry.data.published?.slice(0, 10) ?? today;
|
||||||
|
const speechYear = entry.data.speechYear ?? entry.id.match(/\b(1\d{3}|20\d{2})\b/)?.[1] ?? new Date().getFullYear().toString();
|
||||||
|
const categoryText = categories.join(" ").toLowerCase();
|
||||||
|
const collection = entry.data.speechCollection ?? "";
|
||||||
|
const speaker = categoryText.includes("hak ja han") || collection === "mrs-hak-ja-han-moon"
|
||||||
|
? "mrs"
|
||||||
|
: categoryText.includes("rev sun myung moon") || /^\d{4}$/.test(collection) || /^\d{4}$/.test(speechYear)
|
||||||
|
? "rev"
|
||||||
|
: "other";
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: entry.id,
|
||||||
|
path: `content/${entry.id}.md`,
|
||||||
|
title: entry.data.title ?? entry.id,
|
||||||
|
publishedDate,
|
||||||
|
speechYear,
|
||||||
|
speaker,
|
||||||
|
source: entry.data.source ?? "",
|
||||||
|
categories,
|
||||||
|
tags,
|
||||||
|
body: entry.body ?? "",
|
||||||
|
};
|
||||||
|
});
|
||||||
const adminConfig = {
|
const adminConfig = {
|
||||||
analyticsDashboardUrl,
|
analyticsDashboardUrl,
|
||||||
contactEndpointConfigured: Boolean(contactFormEndpoint),
|
contactEndpointConfigured: Boolean(contactFormEndpoint),
|
||||||
|
|
@ -390,7 +416,7 @@ const adminConfig = {
|
||||||
|
|
||||||
.admin-workspace {
|
.admin-workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 340px;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
@ -452,6 +478,42 @@ const adminConfig = {
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-speech-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 420px;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-speech-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--admin-panel-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-speech-row-title {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: 800;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-speech-row-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 3px;
|
||||||
|
color: var(--admin-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-form {
|
.admin-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
|
@ -684,11 +746,6 @@ const adminConfig = {
|
||||||
<h1>FamilyFed.ie Admin</h1>
|
<h1>FamilyFed.ie Admin</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav class="admin-actions" aria-label="Admin links">
|
|
||||||
<a class="admin-link-button" href="/index.html">Public Site</a>
|
|
||||||
<a class="admin-link-button" href="/events.html">Calendar</a>
|
|
||||||
<a class="admin-link-button" href="/speeches/">Speeches</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -741,54 +798,6 @@ const adminConfig = {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="admin-overview-grid">
|
|
||||||
<section class="admin-panel" aria-labelledby="overview-actions-title">
|
|
||||||
<div class="admin-panel-header">
|
|
||||||
<div class="admin-panel-title">
|
|
||||||
<p class="admin-section-label">Work queue</p>
|
|
||||||
<h2 id="overview-actions-title">Content Tools</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="admin-panel-body">
|
|
||||||
<div class="admin-mini-actions">
|
|
||||||
<button class="admin-button admin-button-primary" type="button" data-admin-tab-link="calendar">Add Calendar Event</button>
|
|
||||||
<button class="admin-button" type="button" data-admin-tab-link="speeches">Add Speech</button>
|
|
||||||
<a class="admin-link-button" href="/speeches/">View Public Speeches</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="admin-panel" aria-labelledby="overview-status-title">
|
|
||||||
<div class="admin-panel-header">
|
|
||||||
<div class="admin-panel-title">
|
|
||||||
<p class="admin-section-label">Readiness</p>
|
|
||||||
<h2 id="overview-status-title">Operations</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="admin-panel-body">
|
|
||||||
<ul class="admin-status-list">
|
|
||||||
<li>
|
|
||||||
<strong>Contact form</strong>
|
|
||||||
<span class={contactFormEndpoint ? "admin-ok" : "admin-missing"}>
|
|
||||||
{contactFormEndpoint ? "Endpoint configured" : "Needs PUBLIC_CONTACT_FORM_ENDPOINT"}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Visitor analytics</strong>
|
|
||||||
<span class={plausibleDomain ? "admin-ok" : "admin-missing"}>
|
|
||||||
{plausibleDomain ? `Tracking ${plausibleDomain}` : "Needs PUBLIC_PLAUSIBLE_DOMAIN"}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="admin-utility-card">
|
|
||||||
<strong>Static admin note</strong>
|
|
||||||
<p>The dashboard can generate clean content files, but it cannot write to the repository from the browser.</p>
|
|
||||||
<button class="admin-button" type="button" data-admin-tab-link="settings">Open Settings</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="admin-tab-panel" id="admin-panel-calendar" role="tabpanel" aria-labelledby="admin-tab-calendar" data-admin-panel="calendar" hidden>
|
<section class="admin-tab-panel" id="admin-panel-calendar" role="tabpanel" aria-labelledby="admin-tab-calendar" data-admin-panel="calendar" hidden>
|
||||||
|
|
@ -799,7 +808,7 @@ const adminConfig = {
|
||||||
<div class="admin-panel-title">
|
<div class="admin-panel-title">
|
||||||
<p class="admin-section-label">Calendar</p>
|
<p class="admin-section-label">Calendar</p>
|
||||||
<h2 id="event-builder-title">Add Calendar Event</h2>
|
<h2 id="event-builder-title">Add Calendar Event</h2>
|
||||||
<p class="admin-note">Generate a typed entry for <code>src/data/events.ts</code>.</p>
|
<p class="admin-note">Generate a typed calendar entry.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="admin-status-pill is-ok">Live preview</span>
|
<span class="admin-status-pill is-ok">Live preview</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -851,49 +860,55 @@ const adminConfig = {
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="admin-sidebar">
|
|
||||||
<div class="admin-utility-card">
|
|
||||||
<strong>Where this goes</strong>
|
|
||||||
<p>Copy the generated snippet into <code>src/data/events.ts</code> inside <code>calendarEvents</code>, then run the build.</p>
|
|
||||||
</div>
|
|
||||||
<section class="admin-panel" aria-labelledby="calendar-reference-title">
|
|
||||||
<div class="admin-panel-header">
|
|
||||||
<div class="admin-panel-title">
|
|
||||||
<p class="admin-section-label">Reference</p>
|
|
||||||
<h2 id="calendar-reference-title">Calendar Files</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="admin-panel-body">
|
|
||||||
<ul class="admin-link-list">
|
|
||||||
<li>
|
|
||||||
<strong>Events data</strong>
|
|
||||||
<span class="admin-path">src/data/events.ts</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Public calendar</strong>
|
|
||||||
<span class="admin-path">/events.html</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="admin-tab-panel" id="admin-panel-speeches" role="tabpanel" aria-labelledby="admin-tab-speeches" data-admin-panel="speeches" hidden>
|
<section class="admin-tab-panel" id="admin-panel-speeches" role="tabpanel" aria-labelledby="admin-tab-speeches" data-admin-panel="speeches" hidden>
|
||||||
<div class="admin-workspace">
|
<div class="admin-workspace">
|
||||||
<div class="admin-builder-stack">
|
<div class="admin-builder-stack">
|
||||||
|
<section class="admin-panel" aria-labelledby="speech-list-title">
|
||||||
|
<div class="admin-panel-header">
|
||||||
|
<div class="admin-panel-title">
|
||||||
|
<p class="admin-section-label">Speeches</p>
|
||||||
|
<h2 id="speech-list-title">Existing Speeches</h2>
|
||||||
|
<p class="admin-note">{speechAdminEntries.length} speeches available to edit.</p>
|
||||||
|
</div>
|
||||||
|
<button class="admin-button admin-button-primary" type="button" data-add-speech>Add Speech</button>
|
||||||
|
</div>
|
||||||
|
<div class="admin-panel-body">
|
||||||
|
<div class="admin-speech-list">
|
||||||
|
{speechAdminEntries.map((speech) => (
|
||||||
|
<div class="admin-speech-row">
|
||||||
|
<div>
|
||||||
|
<span class="admin-speech-row-title">{speech.title}</span>
|
||||||
|
<span class="admin-speech-row-meta">
|
||||||
|
<span>{speech.speechYear}</span>
|
||||||
|
<span>{speech.publishedDate}</span>
|
||||||
|
<span>{speech.path}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button class="admin-button" type="button" data-speech-edit={speech.id}>Edit</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="admin-panel" aria-labelledby="speech-builder-title">
|
<section class="admin-panel" aria-labelledby="speech-builder-title">
|
||||||
<div class="admin-panel-header">
|
<div class="admin-panel-header">
|
||||||
<div class="admin-panel-title">
|
<div class="admin-panel-title">
|
||||||
<p class="admin-section-label">Speeches</p>
|
<p class="admin-section-label">Speeches</p>
|
||||||
<h2 id="speech-builder-title">Add Speech</h2>
|
<h2 id="speech-builder-title" data-speech-editor-title>Add Speech</h2>
|
||||||
<p class="admin-note">Generate a Markdown file for <code>content/speeches</code>.</p>
|
<p class="admin-note" data-speech-editor-note>Generate a Markdown file for <code>content/speeches</code>.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="admin-status-pill is-ok">Download ready</span>
|
<span class="admin-status-pill is-ok">Download ready</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-panel-body">
|
<div class="admin-panel-body">
|
||||||
<form class="admin-form" data-speech-form>
|
<form class="admin-form" data-speech-form>
|
||||||
|
<input type="hidden" name="originalPath" />
|
||||||
|
<input type="hidden" name="source" />
|
||||||
|
<input type="hidden" name="categories" />
|
||||||
|
<input type="hidden" name="tags" />
|
||||||
<div class="admin-form-grid">
|
<div class="admin-form-grid">
|
||||||
<div class="admin-field">
|
<div class="admin-field">
|
||||||
<label for="speech-title">Title</label>
|
<label for="speech-title">Title</label>
|
||||||
|
|
@ -937,117 +952,23 @@ const adminConfig = {
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="admin-sidebar">
|
|
||||||
<div class="admin-utility-card">
|
|
||||||
<strong>Where this goes</strong>
|
|
||||||
<p>Download or copy the generated Markdown file into the matching <code>content/speeches</code> folder, then run the build.</p>
|
|
||||||
</div>
|
|
||||||
<section class="admin-panel" aria-labelledby="speech-reference-title">
|
|
||||||
<div class="admin-panel-header">
|
|
||||||
<div class="admin-panel-title">
|
|
||||||
<p class="admin-section-label">Reference</p>
|
|
||||||
<h2 id="speech-reference-title">Speech Files</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="admin-panel-body">
|
|
||||||
<ul class="admin-link-list">
|
|
||||||
<li>
|
|
||||||
<strong>Speech content</strong>
|
|
||||||
<span class="admin-path">content/speeches/<year>/<slug>.md</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Public archive</strong>
|
|
||||||
<span class="admin-path">/speeches/</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="admin-tab-panel" id="admin-panel-settings" role="tabpanel" aria-labelledby="admin-tab-settings" data-admin-panel="settings" hidden>
|
<section class="admin-tab-panel" id="admin-panel-settings" role="tabpanel" aria-labelledby="admin-tab-settings" data-admin-panel="settings" hidden></section>
|
||||||
<div class="admin-workspace">
|
|
||||||
<div class="admin-builder-stack">
|
|
||||||
<section class="admin-panel" aria-labelledby="operations-title">
|
|
||||||
<div class="admin-panel-header">
|
|
||||||
<div class="admin-panel-title">
|
|
||||||
<p class="admin-section-label">Status</p>
|
|
||||||
<h2 id="operations-title">Operations</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="admin-panel-body">
|
|
||||||
<ul class="admin-status-list">
|
|
||||||
<li>
|
|
||||||
<strong>Contact form</strong>
|
|
||||||
<span class={contactFormEndpoint ? "admin-ok" : "admin-missing"}>
|
|
||||||
{contactFormEndpoint ? "Endpoint configured" : "Needs PUBLIC_CONTACT_FORM_ENDPOINT"}
|
|
||||||
</span>
|
|
||||||
{contactSubmissionsUrl && <a class="admin-path" href={contactSubmissionsUrl}>Open contact submissions</a>}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Visitor analytics</strong>
|
|
||||||
<span class={plausibleDomain ? "admin-ok" : "admin-missing"}>
|
|
||||||
{plausibleDomain ? `Tracking ${plausibleDomain}` : "Needs PUBLIC_PLAUSIBLE_DOMAIN"}
|
|
||||||
</span>
|
|
||||||
{analyticsDashboardUrl && <a class="admin-path" href={analyticsDashboardUrl}>Open analytics dashboard</a>}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Admin hosting</strong>
|
|
||||||
<span>Serve <code>/admin/index.html</code> as the root for <code>admin.familyfed.ie</code> and protect it with the Pocket ID group <code>familyfed_admin</code>.</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="admin-panel" aria-labelledby="quick-files-title">
|
|
||||||
<div class="admin-panel-header">
|
|
||||||
<div class="admin-panel-title">
|
|
||||||
<p class="admin-section-label">Reference</p>
|
|
||||||
<h2 id="quick-files-title">Quick Files</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="admin-panel-body">
|
|
||||||
<ul class="admin-link-list">
|
|
||||||
<li>
|
|
||||||
<strong>Events data</strong>
|
|
||||||
<span class="admin-path">src/data/events.ts</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Speech content</strong>
|
|
||||||
<span class="admin-path">content/speeches/<year>/<slug>.md</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Admin notes</strong>
|
|
||||||
<span class="admin-path">docs/ADMIN.md</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Pocket ID auth</strong>
|
|
||||||
<span class="admin-path">docs/POCKET-ID-AUTH.md</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside class="admin-sidebar">
|
|
||||||
<div class="admin-utility-card">
|
|
||||||
<strong>Static admin note</strong>
|
|
||||||
<p>The dashboard can generate clean content files, but it cannot write to the repository from the browser.</p>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script is:inline define:vars={{ adminConfig }}>
|
<script is:inline define:vars={{ adminConfig, speechAdminEntries }}>
|
||||||
(() => {
|
(() => {
|
||||||
const eventForm = document.querySelector("[data-event-form]");
|
const eventForm = document.querySelector("[data-event-form]");
|
||||||
const speechForm = document.querySelector("[data-speech-form]");
|
const speechForm = document.querySelector("[data-speech-form]");
|
||||||
const speechDownload = document.querySelector("[data-download-speech]");
|
const speechDownload = document.querySelector("[data-download-speech]");
|
||||||
|
const speechEditorTitle = document.querySelector("[data-speech-editor-title]");
|
||||||
|
const speechEditorNote = document.querySelector("[data-speech-editor-note]");
|
||||||
const adminTabs = Array.from(document.querySelectorAll("[data-admin-tab]"));
|
const adminTabs = Array.from(document.querySelectorAll("[data-admin-tab]"));
|
||||||
const adminPanels = Array.from(document.querySelectorAll("[data-admin-panel]"));
|
const adminPanels = Array.from(document.querySelectorAll("[data-admin-panel]"));
|
||||||
|
const speechEntryMap = new Map(speechAdminEntries.map((speech) => [speech.id, speech]));
|
||||||
|
|
||||||
function activateTab(tabName, shouldFocus = false) {
|
function activateTab(tabName, shouldFocus = false) {
|
||||||
const nextTab = adminTabs.find((tab) => tab.dataset.adminTab === tabName) ?? adminTabs[0];
|
const nextTab = adminTabs.find((tab) => tab.dataset.adminTab === tabName) ?? adminTabs[0];
|
||||||
|
|
@ -1127,6 +1048,26 @@ const adminConfig = {
|
||||||
return String(new FormData(form).get(name) || "").trim();
|
return String(new FormData(form).get(name) || "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFormValue(form, name, value) {
|
||||||
|
const field = form?.elements[name];
|
||||||
|
if (field) {
|
||||||
|
field.value = value ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonList(value) {
|
||||||
|
if (!value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return Array.isArray(parsed) ? parsed.filter(Boolean) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setMessage(element, text, type = "") {
|
function setMessage(element, text, type = "") {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1261,22 +1202,27 @@ const adminConfig = {
|
||||||
? "Speeches"
|
? "Speeches"
|
||||||
: `Speeches of Rev Sun Myung Moon ${data.speechYear}`;
|
: `Speeches of Rev Sun Myung Moon ${data.speechYear}`;
|
||||||
const published = `${data.publishedDate}T12:00:00+00:00`;
|
const published = `${data.publishedDate}T12:00:00+00:00`;
|
||||||
const path = `content/speeches/${collection}/${slug}.md`;
|
const path = data.originalPath || `content/speeches/${collection}/${slug}.md`;
|
||||||
|
const categories = data.categories.length ? data.categories : [category];
|
||||||
|
const tags = data.tags.length
|
||||||
|
? ["tags:", ...data.tags.map((item) => ` - ${yamlString(item)}`)]
|
||||||
|
: ["tags: []"];
|
||||||
const markdown = [
|
const markdown = [
|
||||||
"---",
|
"---",
|
||||||
`title: ${yamlString(data.title)}`,
|
`title: ${yamlString(data.title)}`,
|
||||||
'type: "speech"',
|
'type: "speech"',
|
||||||
`published: ${yamlString(published)}`,
|
`published: ${yamlString(published)}`,
|
||||||
`updated: ${yamlString(published)}`,
|
`updated: ${yamlString(published)}`,
|
||||||
|
data.source ? `source: ${yamlString(data.source)}` : null,
|
||||||
`speechYear: ${yamlString(data.speechYear)}`,
|
`speechYear: ${yamlString(data.speechYear)}`,
|
||||||
"categories:",
|
"categories:",
|
||||||
` - ${yamlString(category)}`,
|
...categories.map((item) => ` - ${yamlString(item)}`),
|
||||||
"tags: []",
|
...tags,
|
||||||
"---",
|
"---",
|
||||||
"",
|
"",
|
||||||
data.body,
|
data.body,
|
||||||
"",
|
"",
|
||||||
].join("\n");
|
].filter((line) => line !== null).join("\n");
|
||||||
|
|
||||||
return { markdown, path, slug };
|
return { markdown, path, slug };
|
||||||
}
|
}
|
||||||
|
|
@ -1300,9 +1246,64 @@ const adminConfig = {
|
||||||
speechYear: formValue(speechForm, "speechYear"),
|
speechYear: formValue(speechForm, "speechYear"),
|
||||||
speaker: formValue(speechForm, "speaker"),
|
speaker: formValue(speechForm, "speaker"),
|
||||||
body: formValue(speechForm, "body"),
|
body: formValue(speechForm, "body"),
|
||||||
|
originalPath: formValue(speechForm, "originalPath"),
|
||||||
|
source: formValue(speechForm, "source"),
|
||||||
|
categories: parseJsonList(formValue(speechForm, "categories")),
|
||||||
|
tags: parseJsonList(formValue(speechForm, "tags")),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSpeechEditorMode(mode, speech = {}) {
|
||||||
|
if (speechEditorTitle) {
|
||||||
|
speechEditorTitle.textContent = mode === "edit" ? "Edit Speech" : "Add Speech";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (speechEditorNote) {
|
||||||
|
speechEditorNote.textContent = mode === "edit"
|
||||||
|
? `Editing ${speech.path}. Copy or download the Markdown when finished.`
|
||||||
|
: "Generate a Markdown file for content/speeches.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSpeechEditor(speech) {
|
||||||
|
if (!speechForm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormValue(speechForm, "title", speech.title);
|
||||||
|
setFormValue(speechForm, "publishedDate", speech.publishedDate);
|
||||||
|
setFormValue(speechForm, "speechYear", speech.speechYear);
|
||||||
|
setFormValue(speechForm, "speaker", speech.speaker);
|
||||||
|
setFormValue(speechForm, "body", speech.body);
|
||||||
|
setFormValue(speechForm, "originalPath", speech.path);
|
||||||
|
setFormValue(speechForm, "source", speech.source);
|
||||||
|
setFormValue(speechForm, "categories", JSON.stringify(speech.categories ?? []));
|
||||||
|
setFormValue(speechForm, "tags", JSON.stringify(speech.tags ?? []));
|
||||||
|
setSpeechEditorMode("edit", speech);
|
||||||
|
refreshSpeechOutput(false);
|
||||||
|
speechForm.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
speechForm.elements.title?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSpeechEditor() {
|
||||||
|
if (!speechForm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
speechForm.reset();
|
||||||
|
setFormValue(speechForm, "publishedDate", new Date().toISOString().slice(0, 10));
|
||||||
|
setFormValue(speechForm, "speechYear", new Date().getFullYear().toString());
|
||||||
|
setFormValue(speechForm, "speaker", "rev");
|
||||||
|
setFormValue(speechForm, "originalPath", "");
|
||||||
|
setFormValue(speechForm, "source", "");
|
||||||
|
setFormValue(speechForm, "categories", "");
|
||||||
|
setFormValue(speechForm, "tags", "");
|
||||||
|
setSpeechEditorMode("add");
|
||||||
|
refreshSpeechOutput(false);
|
||||||
|
speechForm.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
speechForm.elements.title?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
function refreshEventOutput(showSuccess = false) {
|
function refreshEventOutput(showSuccess = false) {
|
||||||
if (!eventForm) {
|
if (!eventForm) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1392,6 +1393,17 @@ const adminConfig = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelector("[data-add-speech]")?.addEventListener("click", resetSpeechEditor);
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-speech-edit]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const speech = speechEntryMap.get(button.dataset.speechEdit);
|
||||||
|
if (speech) {
|
||||||
|
loadSpeechEditor(speech);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
eventForm?.addEventListener("input", () => refreshEventOutput(false));
|
eventForm?.addEventListener("input", () => refreshEventOutput(false));
|
||||||
speechForm?.addEventListener("input", () => refreshSpeechOutput(false));
|
speechForm?.addEventListener("input", () => refreshSpeechOutput(false));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue