300 lines
11 KiB
HTML
300 lines
11 KiB
HTML
{% extends "base.html" %}
|
|
{% block body_class %}themed-bg{% endblock %}
|
|
{% load static %}
|
|
|
|
{% block content %}
|
|
<div class="page">
|
|
<!-- Top bar -->
|
|
<div class="result-toolbar">
|
|
<div class="rt-left">
|
|
<a class="btn btn-secondary" href="{% url 'entry_view' entry.id %}">← Back to Entry</a>
|
|
<span class="rt-count">Editing: #{{ entry.id }}</span>
|
|
</div>
|
|
<div class="rt-right">
|
|
<a class="btn" href="{% url 'entry_view' entry.id %}">Cancel</a>
|
|
<button form="entry-edit-form" class="btn btn-primary">Save</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card -->
|
|
<div class="card entry-form modern-form" style="padding: 24px; margin-bottom: 60px;">
|
|
<form id="entry-edit-form" method="post">
|
|
{% csrf_token %}
|
|
|
|
<div class="f-grid">
|
|
<div class="f-row">
|
|
<div class="f-label" style="font-weight: 600; font-size: 1.05em;">Subject</div>
|
|
<div class="f-control">
|
|
{{ form.subject }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="f-row tall">
|
|
<div class="f-label" style="font-weight: 600; font-size: 1.05em;">Illustration</div>
|
|
<div class="f-control">
|
|
{{ form.illustration }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="f-row tall">
|
|
<div class="f-label" style="font-weight: 600; font-size: 1.05em;">Application</div>
|
|
<div class="f-control">
|
|
{{ form.application }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="f-row">
|
|
<div class="f-label" style="font-weight: 600; font-size: 1.05em;">Scripture</div>
|
|
<div class="f-control">
|
|
{{ form.scripture_raw }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="f-row">
|
|
<div class="f-label" style="font-weight: 600; font-size: 1.05em;">Source</div>
|
|
<div class="f-control">
|
|
{{ form.source }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="f-row">
|
|
<div class="f-label" style="font-weight: 600; font-size: 1.05em;">Talk Title</div>
|
|
<div class="f-control">
|
|
{{ form.talk_title }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="f-row">
|
|
<div class="f-label" style="font-weight: 600; font-size: 1.05em;">Talk Number</div>
|
|
<div class="f-control">
|
|
{{ form.talk_number }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Combined row for Entry Code + Date Added + Date Edited -->
|
|
<div class="f-row" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px;">
|
|
<div>
|
|
<div class="f-label" style="font-weight: 600; font-size: 1.05em;">Entry Code</div>
|
|
<div class="f-control">
|
|
{{ form.entry_code }}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="f-label" style="font-weight: 600; font-size: 1.05em;">Date Added</div>
|
|
<div class="f-control">
|
|
{{ form.date_added }}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="f-label" style="font-weight: 600; font-size: 1.05em;">Date Edited</div>
|
|
<div class="f-control">
|
|
{{ form.date_edited }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- bottom actions -->
|
|
<div class="form-actions bottom-actions" style="margin-top: 20px;">
|
|
<a class="btn btn-secondary" href="{% url 'entry_view' entry.id %}">Cancel</a>
|
|
<button class="btn btn-primary">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Talk title auto-fill & auto-date -->
|
|
<script>
|
|
(function () {
|
|
const talksUrl = "{% static 'talks.json' %}";
|
|
|
|
function wireAutofill(talkMap) {
|
|
const numberEl = document.getElementById("id_talk_number");
|
|
const titleEl = document.getElementById("id_talk_title");
|
|
if (!numberEl || !titleEl) return;
|
|
|
|
const isEffectivelyEmpty = (s) => {
|
|
const t = (s || "").trim();
|
|
return t === "" || t === "-" || t === "—";
|
|
};
|
|
|
|
let userTyped = false;
|
|
|
|
titleEl.addEventListener("input", () => {
|
|
userTyped = !isEffectivelyEmpty(titleEl.value);
|
|
if (!userTyped) titleEl.dataset.autofilled = "0";
|
|
});
|
|
|
|
function maybeAutofill() {
|
|
const n = numberEl.value;
|
|
const mapped = talkMap && talkMap[n] ? talkMap[n] : "";
|
|
|
|
if (!userTyped) {
|
|
if (mapped) {
|
|
titleEl.value = mapped;
|
|
titleEl.dataset.autofilled = "1";
|
|
} else if (titleEl.dataset.autofilled === "1") {
|
|
titleEl.value = "";
|
|
titleEl.dataset.autofilled = "0";
|
|
}
|
|
}
|
|
}
|
|
|
|
numberEl.addEventListener("change", maybeAutofill);
|
|
|
|
if (isEffectivelyEmpty(titleEl.value)) {
|
|
userTyped = false;
|
|
maybeAutofill();
|
|
} else {
|
|
userTyped = true;
|
|
}
|
|
}
|
|
|
|
fetch(talksUrl, {cache: "no-store"})
|
|
.then(r => r.ok ? r.json() : {})
|
|
.then(map => wireAutofill(map))
|
|
.catch(() => wireAutofill({}));
|
|
|
|
// Auto-fill today's date for Date Edited if empty
|
|
const dateEditedEl = document.getElementById("id_date_edited");
|
|
if (dateEditedEl && !dateEditedEl.value) {
|
|
const today = new Date();
|
|
const yyyy = today.getFullYear();
|
|
const mm = String(today.getMonth() + 1).padStart(2, '0');
|
|
const dd = String(today.getDate()).padStart(2, '0');
|
|
dateEditedEl.value = `${yyyy}-${mm}-${dd}`;
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<style>
|
|
.scripture-valid,
|
|
textarea.scripture-valid,
|
|
input.scripture-valid { background-color: hsl(140 80% 92% / 0.8); transition: background-color .15s ease; }
|
|
.scripture-invalid,
|
|
textarea.scripture-invalid,
|
|
input.scripture-invalid { background-color: hsl(0 80% 94% / 0.8); transition: background-color .15s ease; }
|
|
</style>
|
|
|
|
<!-- Scripture Validation -->
|
|
<script>
|
|
(function () {
|
|
const el = document.getElementById("id_scripture_raw");
|
|
if (!el) return;
|
|
|
|
// Exact WOL abbreviations
|
|
const WOL = new Set([
|
|
"Ge","Ex","Le","Nu","De","Jos","Jg","Ru","1Sa","2Sa","1Ki","2Ki","1Ch","2Ch","Ezr","Ne","Es","Job","Ps","Pr","Ec","Ca","Isa","Jer","La","Eze","Da","Ho","Joe","Am","Ob","Jon","Mic","Na","Hab","Zep","Hag","Zec","Mal",
|
|
"Mt","Mr","Lu","Joh","Ac","Ro","1Co","2Co","Ga","Eph","Php","Col","1Th","2Th","1Ti","2Ti","Tit","Phm","Heb","Jas","1Pe","2Pe","1Jo","2Jo","3Jo","Jude","Re"
|
|
]);
|
|
|
|
// Full names → WOL code (case-insensitive)
|
|
const FULL = new Map([
|
|
// OT
|
|
["genesis","Ge"],["exodus","Ex"],["leviticus","Le"],["numbers","Nu"],["deuteronomy","De"],
|
|
["joshua","Jos"],["judges","Jg"],["ruth","Ru"],
|
|
["1 samuel","1Sa"],["2 samuel","2Sa"],["1 kings","1Ki"],["2 kings","2Ki"],
|
|
["1 chronicles","1Ch"],["2 chronicles","2Ch"],
|
|
["ezra","Ezr"],["nehemiah","Ne"],["esther","Es"],["job","Job"],["psalms","Ps"],["psalm","Ps"],
|
|
["proverbs","Pr"],["ecclesiastes","Ec"],["song of solomon","Ca"],["song of songs","Ca"],
|
|
["isaiah","Isa"],["jeremiah","Jer"],["lamentations","La"],["ezekiel","Eze"],["daniel","Da"],
|
|
["hosea","Ho"],["joel","Joe"],["amos","Am"],["obadiah","Ob"],["jonah","Jon"],["micah","Mic"],
|
|
["nahum","Na"],["habakkuk","Hab"],["zephaniah","Zep"],["haggai","Hag"],["zechariah","Zec"],["malachi","Mal"],
|
|
// NT
|
|
["matthew","Mt"],["mark","Mr"],["luke","Lu"],["john","Joh"],["acts","Ac"],["romans","Ro"],
|
|
["1 corinthians","1Co"],["2 corinthians","2Co"],["galatians","Ga"],["ephesians","Eph"],
|
|
["philippians","Php"],["colossians","Col"],
|
|
["1 thessalonians","1Th"],["2 thessalonians","2Th"],
|
|
["1 timothy","1Ti"],["2 timothy","2Ti"],
|
|
["titus","Tit"],["philemon","Phm"],["hebrews","Heb"],["james","Jas"],
|
|
["1 peter","1Pe"],["2 peter","2Pe"],
|
|
["1 john","1Jo"],["2 john","2Jo"],["3 john","3Jo"],
|
|
["jude","Jude"],["revelation","Re"]
|
|
]);
|
|
|
|
// Numbered-series acceptance (prose abbreviations like "2 Sam.", "1 Chron.", "2 Cor.", "1 Thes.", "2 Tim.", "1 Pet.", "1 Jn.")
|
|
const NUMBERED = [
|
|
{ prefixes: ["sam","samu","samuel"], codes: {1:"1Sa",2:"2Sa"} },
|
|
{ prefixes: ["ki","king","kings","kgs"], codes: {1:"1Ki",2:"2Ki"} },
|
|
{ prefixes: ["chron","chr","ch","chronicles"], codes: {1:"1Ch",2:"2Ch"} },
|
|
{ prefixes: ["cor","corin","corinth","corinthians","co","c"], codes: {1:"1Co",2:"2Co"} },
|
|
{ prefixes: ["thes","thess","thessalon","thessalonians","th"], codes: {1:"1Th",2:"2Th"} },
|
|
{ prefixes: ["tim","ti","timothy","t"], codes: {1:"1Ti",2:"2Ti"} },
|
|
{ prefixes: ["pet","pe","peter","pt","p"], codes: {1:"1Pe",2:"2Pe"} },
|
|
{ prefixes: ["jo","jn","joh","john","jno","jhn"], codes: {1:"1Jo",2:"2Jo",3:"3Jo"} },
|
|
];
|
|
|
|
// Split “book chapter[:verses…]”
|
|
function splitBookAndRest(s) {
|
|
const m = s.match(/^(.+?)\s+(\d{1,3}(?:\s*:\s*.*)?)$/);
|
|
return m ? {book: m[1], rest: m[2]} : null;
|
|
}
|
|
|
|
function lookupBookCode(bookRaw) {
|
|
// Trim and strip trailing period(s), collapse whitespace
|
|
let b = bookRaw.trim().replace(/\.+$/, "").replace(/\s+/g, " ");
|
|
|
|
// 1) Full-name match (case-insensitive)
|
|
const fullKey = b.toLowerCase();
|
|
if (FULL.has(fullKey)) return FULL.get(fullKey);
|
|
|
|
// 2) Numbered-series prose abbreviations: e.g., "2 Sam", "1 Chron", "2 Cor", "1 Thes", "2 Tim", "1 Pet", "1 Jn"
|
|
const m = b.toLowerCase().match(/^([1-3])\s*([a-z]+)$/);
|
|
if (m) {
|
|
const num = parseInt(m[1], 10);
|
|
const base = m[2];
|
|
for (const series of NUMBERED) {
|
|
if (series.prefixes.some(p => base.startsWith(p))) {
|
|
const code = series.codes[num];
|
|
if (code) return code;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3) WOL abbreviation (allow optional space after number and allow trailing period)
|
|
const abbr = b.replace(/^([1-3])\s+([A-Za-z]+)/, (_, n, letters) => n + letters);
|
|
if (WOL.has(abbr)) return abbr;
|
|
// Also try without spaces just in case
|
|
const abbrTight = b.replace(/\s+/g, "");
|
|
if (WOL.has(abbrTight)) return abbrTight;
|
|
|
|
return null;
|
|
}
|
|
|
|
// Chapter only | chapter:verses | lists | ranges | cross-chapter range
|
|
const versesRe =
|
|
/^(\d{1,3})$|^(\d{1,3}):\s*(\d{1,3}(?:\s*-\s*(?:\d{1,3}|\d{1,3}:\d{1,3}))?(?:\s*,\s*\d{1,3}(?:\s*-\s*(?:\d{1,3}|\d{1,3}:\d{1,3}))?)*)$/;
|
|
|
|
function isValidSingleRef(ref) {
|
|
const s = ref.trim();
|
|
if (!s) return false;
|
|
const parts = splitBookAndRest(s);
|
|
if (!parts) return false;
|
|
|
|
const code = lookupBookCode(parts.book);
|
|
if (!code) return false;
|
|
|
|
const rest = (parts.rest || "").trim();
|
|
if (!rest) return false;
|
|
|
|
return versesRe.test(rest);
|
|
}
|
|
|
|
function validateAll() {
|
|
const raw = el.value || "";
|
|
const parts = raw.split(";").map(t => t.trim()).filter(Boolean);
|
|
if (parts.length === 0) {
|
|
el.classList.remove("scripture-valid","scripture-invalid");
|
|
return;
|
|
}
|
|
const allValid = parts.every(isValidSingleRef);
|
|
el.classList.toggle("scripture-valid", allValid);
|
|
el.classList.toggle("scripture-invalid", !allValid);
|
|
}
|
|
|
|
el.addEventListener("input", validateAll);
|
|
el.addEventListener("change", validateAll);
|
|
validateAll();
|
|
})();
|
|
</script>
|
|
{% endblock %} |