277 lines
9.4 KiB
HTML
277 lines
9.4 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
|
|
=========================== */
|
|
(function () {
|
|
const el = document.getElementById("id_scripture_raw");
|
|
if (!el) return;
|
|
|
|
// Exact WOL abbreviations (case-sensitive)
|
|
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"
|
|
]);
|
|
|
|
// Normalize some gentle user quirks:
|
|
// - Allow trailing dot after the book token: "Ps." -> "Ps"
|
|
// - Allow a space after the number in numbered books: "1 Pe" -> "1Pe"
|
|
function normalizeBookToken(raw) {
|
|
let t = raw.trim();
|
|
// collapse internal multiple spaces
|
|
t = t.replace(/\s+/g, " ");
|
|
// remove trailing period on the book token
|
|
t = t.replace(/\.$/, "");
|
|
// join patterns like "1 Pe" or "2 Co" -> "1Pe" / "2Co"
|
|
t = t.replace(/^([1-3])\s+([A-Za-z]+)/, (_, n, b) => n + b);
|
|
return t;
|
|
}
|
|
|
|
// Validate one reference like:
|
|
// "Ro 12" (chapter only)
|
|
// "Ro 12:1" (single verse)
|
|
// "Ro 12:1, 3, 5-7" (lists and ranges)
|
|
// "Joh 3:16-4:2" (cross-chapter range)
|
|
// "Ps. 23" (trailing dot on book handled by normalization)
|
|
function isValidSingleRef(ref) {
|
|
const s = ref.trim();
|
|
if (!s) return false;
|
|
|
|
// Split book token from the rest (book is everything before first space)
|
|
const firstSpace = s.indexOf(" ");
|
|
if (firstSpace < 0) return false;
|
|
|
|
const rawBook = s.slice(0, firstSpace);
|
|
const book = normalizeBookToken(rawBook);
|
|
|
|
if (!WOL.has(book)) return false;
|
|
|
|
const rest = s.slice(firstSpace + 1).trim();
|
|
if (!rest) return false;
|
|
|
|
// Accept "chapter" only (e.g., "Ps 23")
|
|
if (/^\d{1,3}$/.test(rest)) return true;
|
|
|
|
// Accept "chapter:verses" where verses can be:
|
|
// - n
|
|
// - n-m
|
|
// - n, m, p-q
|
|
// and allow a cross-chapter range like 3:16-4:2
|
|
// Pattern:
|
|
// chap: ( part(, part)* )
|
|
// where part is either:
|
|
// v -> \d{1,3}
|
|
// v-v -> \d{1,3}\s*-\s*\d{1,3}
|
|
// v-ch:v2 -> \d{1,3}\s*-\s*\d{1,3}:\d{1,3}
|
|
const re = /^(\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}))?)*)$/;
|
|
if (re.test(rest)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function validateAll() {
|
|
const raw = el.value || "";
|
|
// split by ';' as multiple references
|
|
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);
|
|
}
|
|
|
|
// Wire events
|
|
el.addEventListener("input", validateAll);
|
|
el.addEventListener("change", validateAll);
|
|
// Run once on load (in case form is prefilled)
|
|
validateAll();
|
|
})();
|
|
</script>
|
|
|
|
{% endblock %} |