This commit is contained in:
Joshua Laymon
2025-08-12 21:53:03 -05:00
parent 97da3bd6c5
commit 2fb9e7c39c
31 changed files with 554 additions and 419 deletions
+39 -32
View File
@@ -1,4 +1,3 @@
<!DOCTYPE html>
<html lang="en">
<head>
@@ -6,32 +5,39 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}Illustrations DB{% endblock %}</title>
<style>
:root {
--blue:#1f6cd8;
--light:#f6f8fb;
--panel:#ffffff;
--line:#e5e9f2;
--text:#1a1a1a;
--muted:#6b7280;
}
body { margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica, Arial, sans-serif; background:var(--light); color:var(--text); }
.topbar { display:flex; align-items:center; justify-content:space-between; padding:12px 16px; background:#fff; border-bottom:1px solid var(--line); position:sticky; top:0; z-index:10;}
.brand { font-weight:700; color:var(--blue); letter-spacing:.2px; }
.menu a { margin-left:14px; text-decoration:none; color:var(--blue); }
.container { max-width: 1100px; margin: 24px auto; padding: 0 16px; }
.panel { background:var(--panel); border:1px solid var(--line); border-radius:12px; box-shadow: 0 8px 20px rgba(0,0,0,0.04); padding: 18px; }
.btn { display:inline-block; padding:10px 14px; border-radius:10px; border:1px solid var(--line); background:#fff; color:#0d1b2a; text-decoration:none; cursor:pointer; }
.btn.primary { background:var(--blue); color:#fff; border-color:var(--blue); }
.btn.danger { background:#d61f1f; color:#fff; border-color:#d61f1f; }
.flash { margin: 16px 0; padding: 12px; border-radius:10px; background:#eaf2ff; color:#0b3d91; }
input[type=text], input[type=password], input[type=date], textarea { width:100%; padding:10px; border:1px solid var(--line); border-radius:10px; background:#fff; }
label { font-size: 13px; color:#333; display:block; margin-bottom:6px; }
.row { display:grid; grid-template-columns: 1fr 1fr; gap:16px; }
.row3 { display:grid; grid-template-columns: 1fr 1fr 1fr; gap:16px; }
.muted { color: var(--muted); }
@media (max-width: 900px){ .row, .row3{ grid-template-columns: 1fr; } }
.stats { display:flex; gap:12px; align-items:center; font-size:14px; color:var(--muted); }
.nav { display:flex; gap:8px; align-items:center; }
:root { --blue:#1f6cd8; --light:#f6f8fb; --panel:#ffffff; --line:#e5e9f2; --text:#1a1a1a; }
body { margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica, Arial, sans-serif; background:var(--light); color:var(--text); }
.topbar { display:flex; align-items:center; justify-content:space-between; padding:14px 18px; background:#fff; border-bottom:1px solid var(--line); position:sticky; top:0; z-index:10; }
.brand { font-weight:700; color:var(--blue); font-size: 18px; }
.menu { display:flex; align-items:center; gap:8px; }
.menu a { text-decoration:none; color:var(--blue); padding:6px 10px; border-radius:6px; border:1px solid transparent; }
.menu a:hover { background:#eef4ff; }
.container { max-width: 1100px; margin: 24px auto; padding: 0 16px; }
.panel { background:var(--panel); border:1px solid var(--line); border-radius:12px; box-shadow: 0 8px 20px rgba(0,0,0,0.04); padding: 18px; }
.btn { display:inline-block; padding:10px 14px; border-radius:8px; border:1px solid var(--line); background:#fff; color:#0d1b2a; text-decoration:none; cursor:pointer; }
.btn.primary { background:var(--blue); color:#fff; border-color:var(--blue); }
.btn.danger { background:#e11d48; color:#fff; border-color:#e11d48; }
.flash { margin: 16px 0; padding: 12px; border-radius:10px; background:#eaf2ff; color:#0b3d91; }
input[type=file], input[type=text], input[type=password], textarea, select { width:100%; padding:10px; border:1px solid var(--line); border-radius:10px; background:#fff; }
label { font-size:14px; color:#333; display:block; margin-bottom:6px; }
.row { display:grid; grid-template-columns: 1fr 1fr; gap:16px; }
@media (max-width: 800px){ .row{ grid-template-columns: 1fr; } }
.cards { display:grid; grid-template-columns: repeat(3, 1fr); gap:16px; }
@media (max-width: 1000px){ .cards{ grid-template-columns: repeat(2, 1fr);} }
@media (max-width: 640px){ .cards{ grid-template-columns: 1fr;} }
.card { background:#fff; border:1px solid var(--line); border-radius:12px; padding:16px; box-shadow: 0 6px 14px rgba(0,0,0,0.03); }
.card:hover { box-shadow: 0 10px 24px rgba(0,0,0,0.05); transform: translateY(-1px); transition: all .15s ease; }
.stat { padding:16px; text-align:center; border:1px solid var(--line); border-radius:12px; background:#fff; }
.badge { display:inline-block; padding:2px 8px; border-radius:6px; border:1px solid var(--line); background:#f7faff; color:#1f4bb6; font-size:12px; }
.chips { display:flex; flex-wrap:wrap; gap:6px; }
.chip { padding:2px 8px; border-radius:6px; border:1px solid var(--line); background:#f1f5ff; color:#1a45a0; font-size:12px; }
.toolbar { display:flex; align-items:center; gap:8px; justify-content:space-between; margin-bottom:12px; flex-wrap:wrap; }
.spacer { height:8px; }
.small { font-size:12px; color:#444; }
h1 { margin:0 0 12px 0; font-size:24px; }
h2 { margin:0 0 12px 0; font-size:18px; }
</style>
</head>
<body>
@@ -40,16 +46,17 @@
<div class="brand">Illustrations Database</div>
<div class="menu">
<a href="/search/">Search</a>
{% if request.user.is_staff %}
<a href="/import/">Import Data</a>
<a href="/export/csv/">Download Backup</a>
{% endif %}
<a href="/import/">Import</a>
<a href="/export/csv/">Backup</a>
<a href="/stats/">Statistics</a>
<a href="/logout/">Logout</a>
</div>
</div>
{% endif %}
<div class="container">
{% for message in messages %}<div class="flash">{{ message }}</div>{% endfor %}
{% for message in messages %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</div>
</body>
+12
View File
@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}Delete Entry - Illustrations DB{% endblock %}
{% block content %}
<div class="panel">
<h2>Delete Entry</h2>
<p>Are you sure you want to permanently delete <strong>{{ entry.talk_title|default:'(untitled)' }}</strong> (Code: {{ entry.entry_code }})?</p>
<form method="post">{% csrf_token %}
<a class="btn" href="/entry/{{ entry.id }}/">Cancel</a>
<button class="btn danger" type="submit">Delete</button>
</form>
</div>
{% endblock %}
+56
View File
@@ -0,0 +1,56 @@
{% extends "base.html" %}
{% block title %}Edit Entry - Illustrations DB{% endblock %}
{% block content %}
<div class="toolbar">
<div class="small">Editing: Record {{ position }} of {{ count }}</div>
<div>
<a class="btn" href="/entry/{{ entry.id }}/">Cancel</a>
</div>
</div>
<form method="post">{% csrf_token %}
<div class="panel">
<h2 style="margin-top:0;">Edit Entry</h2>
<div class="row">
<div>
<label>Talk Title</label>
{{ form.talk_title }}
</div>
<div>
<label>Talk Number</label>
{{ form.talk_number }}
</div>
</div>
<div class="row">
<div>
<label>Source</label>
{{ form.source }}
</div>
<div>
<label>Code</label>
{{ form.entry_code }}
</div>
</div>
<label>Subject</label>
{{ form.subject }}
<label>Illustration</label>
{{ form.illustration }}
<label>Application</label>
{{ form.application }}
<div class="row">
<div>
<label>Scripture</label>
{{ form.scripture_raw }}
</div>
<div>
<label>Date Added</label>
{{ form.date_added }}
<label>Date Edited</label>
{{ form.date_edited }}
</div>
</div>
<div style="margin-top:16px; display:flex; gap:10px; justify-content:flex-end;">
<button class="btn primary" type="submit">Save Changes</button>
</div>
</div>
</form>
{% endblock %}
+42
View File
@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block title %}Entry - Illustrations DB{% endblock %}
{% block content %}
<div class="toolbar">
<div class="small">Viewing: Record {{ position }} of {{ count }}</div>
<div>
<a class="btn" href="/nav/prev/?i={{ position|add:-1 }}">← Prev</a>
<a class="btn" href="/nav/next/?i={{ position|add:-1 }}">Next →</a>
<a class="btn" href="/search/">Back to Search</a>
<a class="btn primary" href="/entry/{{ entry.id }}/edit/">Unlock to Edit</a>
<a class="btn danger" href="/entry/{{ entry.id }}/delete/">Delete</a>
</div>
</div>
<div class="panel">
<h2 style="margin-top:0;">{{ entry.talk_title|default:"(untitled)" }}</h2>
<div class="small">Code: {{ entry.entry_code }} • Source: {{ entry.source }} • Talk # {{ entry.talk_number }}</div>
<div class="spacer"></div>
<div class="row">
<div>
<label>Subject</label>
<div class="chips">
{% for t in entry.subject.split(',') %}{% if t.strip %}<span class="chip">{{ t.strip }}</span>{% endif %}{% endfor %}
</div>
</div>
<div>
<label>Scripture</label>
<div class="chips">
{% for t in entry.scripture_raw.split(';') %}{% if t.strip %}<span class="chip" style="background:#eef4ff;">{{ t.strip }}</span>{% endif %}{% endfor %}
</div>
</div>
</div>
<div class="spacer"></div>
<label>Illustration</label>
<div class="card">{{ entry.illustration|linebreaksbr }}</div>
<div class="spacer"></div>
<label>Application</label>
<div class="card">{{ entry.application|linebreaksbr }}</div>
<div class="spacer"></div>
<div class="small">Date Added: {{ entry.date_added }} • Date Edited: {{ entry.date_edited }}</div>
</div>
{% endblock %}
+1 -2
View File
@@ -1,4 +1,3 @@
{% extends "base.html" %}
{% block title %}Import Result - Illustrations DB{% endblock %}
{% block content %}
@@ -14,7 +13,7 @@
</ul>
{% if report.errors and report.errors|length %}
<h3>Errors</h3>
<pre style="white-space:pre-wrap;">{{ report.errors|join("\n") }}</pre>
<pre>{{ report.errors|join("\n") }}</pre>
{% endif %}
<div style="margin-top:16px;">
<a class="btn" href="/import/">Run again</a>
-1
View File
@@ -1,4 +1,3 @@
{% extends "base.html" %}
{% block title %}Import Data - Illustrations DB{% endblock %}
{% block content %}
-1
View File
@@ -1,4 +1,3 @@
{% extends "base.html" %}
{% block title %}Sign in - Illustrations DB{% endblock %}
{% block content %}
-101
View File
@@ -1,101 +0,0 @@
{% extends "base.html" %}
{% block title %}Record - Illustrations DB{% endblock %}
{% block content %}
<div class="stats" style="margin-bottom:8px;">
<div>Total: <strong>{{ total }}</strong></div>
<div>Results: <strong>{{ results_count }}</strong></div>
<div>Viewing: <strong>{{ position }}</strong> of <strong>{{ results_count|default:1 }}</strong></div>
</div>
<div class="panel">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div class="nav">
<a class="btn" href="/nav/prev/">&larr; Prev</a>
<a class="btn" href="/nav/next/">Next &rarr;</a>
</div>
<div>
<button class="btn" id="unlockBtn">Unlock to Edit</button>
<form method="post" action="/record/{{ entry.id }}/delete/" style="display:inline;" onsubmit="return confirm('Are you sure you want to permanently delete this entry?');">
{% csrf_token %}
<button class="btn danger">Delete</button>
</form>
</div>
</div>
<form id="entryForm" method="post" action="/record/{{ entry.id }}/save/" style="margin-top:14px;">
{% csrf_token %}
<div class="row">
<div>
<label>Subject</label>
<input type="text" name="subject" value="{{ entry.subject|default:'' }}" readonly />
</div>
<div>
<label>Scripture</label>
<input type="text" name="scripture_raw" value="{{ entry.scripture_raw|default:'' }}" readonly />
</div>
</div>
<div style="margin-top:12px;">
<label>Illustration</label>
<textarea name="illustration" rows="5" readonly>{{ entry.illustration|default:'' }}</textarea>
</div>
<div style="margin-top:12px;">
<label>Application</label>
<textarea name="application" rows="5" readonly>{{ entry.application|default:'' }}</textarea>
</div>
<div class="row3" style="margin-top:12px;">
<div>
<label>Source</label>
<input type="text" name="source" value="{{ entry.source|default:'' }}" readonly />
</div>
<div>
<label>Talk Number</label>
<input type="text" name="talk_number" value="{{ entry.talk_number|default:'' }}" readonly />
</div>
<div>
<label>Code</label>
<input type="text" name="entry_code" value="{{ entry.entry_code|default:'' }}" readonly />
</div>
</div>
<div class="row" style="margin-top:12px;">
<div>
<label>Talk Title</label>
<input type="text" name="talk_title" value="{{ entry.talk_title|default:'' }}" readonly />
</div>
<div class="row" style="grid-template-columns: 1fr 1fr; gap:12px;">
<div>
<label>Date</label>
<input type="text" name="date_added" value="{{ entry.date_added|default:'' }}" readonly />
</div>
<div>
<label>Date Edited</label>
<input type="text" name="date_edited" value="{{ entry.date_edited|default:'' }}" readonly />
</div>
</div>
</div>
<div style="margin-top:16px; display:flex; gap:10px; justify-content:flex-end;">
<a class="btn" href="/search/">Back to Search</a>
<button class="btn primary" id="saveBtn" type="submit" disabled>Save Changes</button>
</div>
</form>
</div>
<script>
const unlockBtn = document.getElementById('unlockBtn');
const form = document.getElementById('entryForm');
const saveBtn = document.getElementById('saveBtn');
unlockBtn.addEventListener('click', function(){
form.querySelectorAll('input, textarea').forEach(el=>{
if(el.name !== 'csrfmiddlewaretoken'){ el.removeAttribute('readonly'); }
});
saveBtn.disabled = false;
unlockBtn.disabled = true;
unlockBtn.textContent = 'Unlocked';
});
</script>
{% endblock %}
+22 -17
View File
@@ -1,36 +1,41 @@
{% extends "base.html" %}
{% block title %}Search - Illustrations DB{% endblock %}
{% block content %}
<div class="panel">
<h1>Search</h1>
<p class="small"><strong>How to search:</strong> Type words or phrases, use wildcards, and choose which fields to search.
<br/>Examples: <code>faith</code> finds entries containing “faith”; <code>*faith*</code> uses wildcards; <code>"exact phrase"</code> matches the phrase; multiple words are ANDed (e.g., <code>faith loyalty</code>).</p>
<form method="get" action="/search/">
<div class="row">
<div>
<label>Search (supports * and ?)</label>
<input type="text" name="q" value="{{ q|default:'' }}" placeholder="e.g., Default, Organization or Matt 12:30 or *loyal*" />
<label>Search terms</label>
<input type="text" name="q" value="{{ q|default:'' }}" placeholder="Type words, phrases, or wildcards…" />
</div>
<div>
<label>Fields to search</label>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:6px; padding-top:6px;">
{% for f in fields %}
<label><input type="checkbox" name="fields" value="{{ f }}" {% if f in selected %}checked{% endif %}> {{ f }}</label>
<div class="chips">
{% for field,label,checked in [
('subject','Subject', selected.subject),
('illustration','Illustration', selected.illustration),
('application','Application', selected.application),
('scripture_raw','Scripture', selected.scripture_raw),
('source','Source', selected.source),
('talk_title','Talk Title', selected.talk_title),
('talk_number','Talk Number', selected.talk_number),
('entry_code','Code', selected.entry_code),
] %}
<label class="badge">
<input type="checkbox" name="{{ field }}" {% if checked %}checked{% endif %}/> {{ label }}
</label>
{% endfor %}
</div>
</div>
</div>
<div style="margin-top:12px; display:flex; gap:10px; justify-content:flex-end;">
<a class="btn" href="/search/">Clear</a>
<div style="margin-top:16px; display:flex; gap:10px; justify-content:flex-end;">
<button class="btn primary" type="submit">Search</button>
</div>
</form>
</div>
<div class="stats" style="margin-top:12px;">
<div>Total entries: <strong>{{ total }}</strong></div>
{% if results_count %}<div>Results: <strong>{{ results_count }}</strong></div>{% endif %}
</div>
{% if results_count %}
<div class="panel" style="margin-top:12px;">
<p>Opening first result…</p>
</div>
{% endif %}
<div class="spacer"></div>
<div class="small">Total entries in database: <strong>{{ total }}</strong></div>
{% endblock %}
+39
View File
@@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}Statistics - Illustrations DB{% endblock %}
{% block content %}
<div class="panel">
<h1>Statistics</h1>
<div class="row">
<div class="stat"><div class="small">Total entries</div><div style="font-size:28px; font-weight:700;">{{ total }}</div></div>
<div class="stat"><div class="small">New in last 30 days</div><div style="font-size:28px; font-weight:700;">{{ last30 }}</div></div>
<div class="stat"><div class="small">New in last 365 days</div><div style="font-size:28px; font-weight:700;">{{ last365 }}</div></div>
</div>
<div class="spacer"></div>
<h2>Trend (last 12 months)</h2>
<div class="card">
<div class="small">Entries per month (by Date Added)</div>
<div style="display:flex; gap:6px; align-items:flex-end; height:120px; margin-top:8px;">
{% with maxv=series|last|slice:":1" %}{% endwith %}
{% with peak=series|map:'1' %}{% endwith %}
{% for label, value in series %}
{% with h=(value|add:0) %}
<div title="{{ label }}: {{ value }}" style="width:24px; background:#dbe7ff; border:1px solid #c8d6ff; height: {{ (value|floatformat:0) }}px;"></div>
{% endwith %}
{% endfor %}
</div>
<div class="small" style="display:flex; gap:8px; flex-wrap:wrap; margin-top:6px;">
{% for label, value in series %}<span>{{ label }}</span>{% endfor %}
</div>
</div>
<div class="spacer"></div>
<h2>Top Subjects</h2>
<div class="cards">
{% for item in top_subjects %}
<div class="card">
<div style="font-weight:600;">{{ item.name }}</div>
<div class="small">{{ item.count }} entries</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}