Illustrations/web/templates/entry_add.html

282 lines
10 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% block title %}New Entry - Illustrations DB{% endblock %}
{% block body_class %}themed-bg{% endblock %}
{% block content %}
<div class="container">
<h1 class="page-title">Record New Illustration</h1>
<p class="page-subtitle">Use this form to save a new illustration that you encountered. Be sure to properly catagorize your entry with comma seperated subjects and to use the standard bible book abbreviations when recording a scripture.</p>
<form method="post" class="search-form card add-form-card">
{% csrf_token %}
<div class="form-grid">
<div class="form-row">
<label class="f-label">Subject</label>
{{ form.subject }}
{% if form.subject.errors %}<div class="err">{{ form.subject.errors|striptags }}</div>{% endif %}
</div>
<div class="form-row">
<label class="f-label">Illustration</label>
{{ form.illustration }}
{% if form.illustration.errors %}<div class="err">{{ form.illustration.errors|striptags }}</div>{% endif %}
</div>
<div class="form-row">
<label class="f-label">Application</label>
{{ form.application }}
{% if form.application.errors %}<div class="err">{{ form.application.errors|striptags }}</div>{% endif %}
</div>
<div class="form-row two">
<div>
<label class="f-label">Scripture</label>
{{ form.scripture_raw }}
{% if form.scripture_raw.errors %}<div class="err">{{ form.scripture_raw.errors|striptags }}</div>{% endif %}
</div>
<div>
<label class="f-label">Source</label>
{{ form.source }}
{% if form.source.errors %}<div class="err">{{ form.source.errors|striptags }}</div>{% endif %}
</div>
</div>
<div class="form-row two">
<div>
<label class="f-label">Talk Title</label>
{{ form.talk_title }}
{% if form.talk_title.errors %}<div class="err">{{ form.talk_title.errors|striptags }}</div>{% endif %}
</div>
<div>
<label class="f-label">Talk Number</label>
{{ form.talk_number }}
{% if form.talk_number.errors %}<div class="err">{{ form.talk_number.errors|striptags }}</div>{% endif %}
</div>
</div>
<!-- Combined row: Entry Code + Date Added + Date Edited -->
<div class="form-row three">
<div>
<label class="f-label">Entry Code</label>
{{ form.entry_code }}
{% if form.entry_code.errors %}<div class="err">{{ form.entry_code.errors|striptags }}</div>{% endif %}
</div>
<div>
<label class="f-label">Date Added</label>
{{ form.date_added }}
{% if form.date_added.errors %}<div class="err">{{ form.date_added.errors|striptags }}</div>{% endif %}
</div>
<div>
<label class="f-label">Date Edited</label>
{{ form.date_edited }}
{% if form.date_edited.errors %}<div class="err">{{ form.date_edited.errors|striptags }}</div>{% endif %}
</div>
</div>
</div>
<div class="result-toolbar" style="margin-top:18px;">
<div class="rt-left"></div>
<div class="rt-right">
<a class="btn btn-secondary" href="{% url 'search' %}">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</div>
</form>
</div>
<style>
/* Card padding + breathing room */
.add-form-card {
padding: 22px 22px 18px;
}
.form-grid { display: grid; gap: 16px; }
.form-row { display: block; }
.form-row.two { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.form-row.three { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
/* Bigger, bold labels (but not huge) */
.f-label {
display: block;
font-weight: 700;
font-size: 15px;
margin-bottom: 6px;
color: #111827;
}
/* Nudge inputs away from edges a bit more */
.search-input.input-hero { padding: 12px 14px; }
.textarea-hero { min-height: 140px; }
@media (max-width: 860px) {
.form-row.two, .form-row.three { grid-template-columns: 1fr; }
}
/* --- Subtle validation colors for Scripture field --- */
.scripture-valid { background-color: hsl(140 80% 92% / 0.8); transition: background-color .15s ease; }
.scripture-invalid { background-color: hsl(0 80% 94% / 0.8); transition: background-color .15s ease; }
</style>
<script>
(function () {
// ----- Talk Number -> Talk Title auto-fill -----
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;
let userTyped = false;
titleEl.addEventListener("input", () => {
userTyped = titleEl.value.trim().length > 0;
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 (titleEl.value.trim() === "") {
userTyped = false;
maybeAutofill();
} else {
userTyped = true;
}
}
fetch(talksUrl, {cache: "no-store"})
.then(r => r.ok ? r.json() : {})
.then(map => wireAutofill(map))
.catch(() => wireAutofill({}));
// ----- Auto-set today's date for Date Added (only if empty) -----
const dateAddedEl = document.getElementById("id_date_added");
if (dateAddedEl && !dateAddedEl.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");
dateAddedEl.value = `${yyyy}-${mm}-${dd}`;
}
})();
// ----- Prefill Entry Code with current username -----
const entryCodeEl = document.getElementById("id_entry_code");
if (entryCodeEl && !entryCodeEl.value) {
entryCodeEl.value = "{{ request.user.username }}";
}
<!-- ===========================
Live Scripture Validation (WOL + full names; space allowed in numbered)
=========================== -->
<script>
(function () {
const el = document.getElementById("id_scripture_raw");
if (!el) return;
// WOL abbreviations (must match exactly; numbered allow optional space before letters)
const WOL = new Set([
// OT
"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",
// NT
"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 canonical names → WOL abbreviation (accept full names case-insensitively)
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"]
]);
// 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;
}
// Accept either exact WOL abbr (with optional space between leading number and letters),
// or the full canonical name (case-insensitive).
function lookupBookCode(bookRaw) {
let b = bookRaw.trim();
// 1) Full name check (case-insensitive, as typed, including numbered with a space)
const fullKey = b.toLowerCase().replace(/\s+/g, " ");
if (FULL.has(fullKey)) return FULL.get(fullKey);
// 2) Abbreviation check
// Allow optional space between leading number and letters: e.g., "1 Pe" → "1Pe"
const maybeAbbr = b.replace(/^([1-3])\s+([A-Za-z]+)/, (_, n, letters) => n + letters);
if (WOL.has(maybeAbbr)) return maybeAbbr;
return null;
}
// Verse pattern: 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 %}