Illustrations/web/templates/settings/home.html

442 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% load static %}
{# Let base.html control gradients via theme #}
{% block body_class %}{% endblock %}
{% block content %}
<div class="container settings-console">
<!-- Header -->
<div class="cc-header">
<h1 class="cc-title">Command Center</h1>
<div class="cc-subtitle">Personalization · Privacy · Security</div>
</div>
<!-- Quick Controls Strip -->
<div class="cc-quick card">
<div class="qc-item">
<div class="qc-label">Theme</div>
<form method="post" action="{% url 'set_theme' %}" class="qc-inline">
{% csrf_token %}
<select id="theme" name="theme" class="qc-select">
{% for t in available_themes %}
<option value="{{ t }}" {% if request.session.theme|default:'classic' == t %}selected{% endif %}>{{ t|capfirst }}</option>
{% endfor %}
</select>
<button class="btn btn-primary qc-btn">Save</button>
</form>
</div>
<div class="qc-divider" role="separator"></div>
<div class="qc-item">
<div class="qc-label">Highlight Hits</div>
<label class="switch">
<input id="highlightHitsToggle" type="checkbox">
<span class="slider"></span>
</label>
</div>
<div class="qc-divider" role="separator"></div>
<div class="qc-item">
<div class="qc-label">Clear History</div>
<button id="clear-history-btn" class="btn btn-danger">Clear</button>
</div>
</div>
<!-- 3-Column Grid -->
<div class="cc-grid">
<!-- Appearance -->
<section class="card cc-panel">
<div class="cc-panel-head">
<div class="cc-kicker">Appearance</div>
<h2 class="cc-panel-title">Theme & Surface</h2>
</div>
<div class="cc-panel-body">
<p class="muted small">Pick a theme you like; preview applies instantly.</p>
<form method="post" action="{% url 'set_theme' %}" class="cc-form">
{% csrf_token %}
<label for="theme" class="cc-label">Theme</label>
<select id="theme-dup" class="form-control" style="max-width:320px;">
{% for t in available_themes %}
<option value="{{ t }}" {% if request.session.theme|default:'classic' == t %}selected{% endif %}>
{{ t|capfirst }}
</option>
{% endfor %}
</select>
<div class="cc-actions">
<button class="btn btn-primary" onclick="return saveThemeFromDuplicate()">Save</button>
<small class="text-muted">Applies across the site</small>
</div>
</form>
<div class="swatch-grid">
{% for t in available_themes %}
<button class="swatch" data-theme="{{ t }}" aria-label="Preview {{ t|capfirst }}">
<span class="swatch-name">{{ t|capfirst }}</span>
</button>
{% endfor %}
</div>
</div>
</section>
<!-- Reading -->
<section class="card cc-panel">
<div class="cc-panel-head">
<div class="cc-kicker">Reading</div>
<h2 class="cc-panel-title">Search Highlights</h2>
</div>
<div class="cc-panel-body">
<div class="cc-row">
<label class="cc-label">Highlight search hits</label>
<label class="switch">
<input id="highlightHitsToggle-dup" type="checkbox">
<span class="slider"></span>
</label>
</div>
<p class="small muted">Lightly marks terms you searched for on entry pages.</p>
</div>
</section>
<!-- Privacy -->
<section class="card cc-panel">
<div class="cc-panel-head">
<div class="cc-kicker">Privacy</div>
<h2 class="cc-panel-title">Your Footprint</h2>
</div>
<div class="cc-panel-body">
<p class="small muted">Remove Recent Searches and Recently Viewed.</p>
<button id="clear-history-btn-dup" class="btn btn-danger">Clear My History</button>
</div>
</section>
{% if user.is_authenticated and user.is_superuser %}
<!-- Security (accented) -->
<section class="card cc-panel cc-sec">
<div class="cc-sec-bg"></div>
<div class="cc-panel-head">
<div class="cc-kicker cc-kicker-sec">Security</div>
<h2 class="cc-panel-title">Monitoring</h2>
</div>
<div class="cc-panel-body">
<div class="sec-grid">
<a class="sec-tile" href="{% url 'login_attempts' %}">
<div class="sec-icon">
<!-- shield -->
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path fill="currentColor" d="M12 2l7 3v6c0 5-3.8 9.7-7 11-3.2-1.3-7-6-7-11V5l7-3z"/></svg>
</div>
<div class="sec-meta">
<div class="sec-title">Login Attempts</div>
<div class="sec-sub">Last 7 days</div>
</div>
<div class="sec-cta">Open →</div>
</a>
<a class="sec-tile" href="{% url 'audit_log' %}">
<div class="sec-icon">
<!-- activity -->
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path fill="currentColor" d="M3 12h3l2 7 4-14 3 10h6"/></svg>
</div>
<div class="sec-meta">
<div class="sec-title">Audit Log</div>
<div class="sec-sub">Latest 100</div>
</div>
<div class="sec-cta">Open →</div>
</a>
</div>
<p class="tiny muted" style="margin-top:10px;">Superuser tools</p>
</div>
</section>
<!-- Release Announcement (compact) -->
<section class="card cc-panel">
<div class="cc-panel-head">
<div class="cc-kicker">Comms</div>
<h2 class="cc-panel-title">Release Announcement</h2>
</div>
<div class="cc-panel-body">
<form method="post" action="{% url 'announcement_tools' %}" class="cc-form">
{% csrf_token %}
{% if announcement_form %}
{{ announcement_form.as_p }}
{% else %}
<label class="cc-label">Title</label>
<input type="text" name="title" class="tool-input">
<label class="cc-label">Message</label>
<textarea name="message" rows="5" class="tool-input" placeholder="Whats new…"></textarea>
<label class="cc-check"><input type="checkbox" name="is_active" checked> Active</label>
{% endif %}
<div class="cc-actions">
<button class="btn btn-primary">Publish</button>
</div>
</form>
{% if announcements_recent %}
<div class="cc-table-wrap">
<table class="table small">
<thead>
<tr><th>ID</th><th>Title</th><th>Active?</th><th>Window</th><th>Created</th></tr>
</thead>
<tbody>
{% for a in announcements_recent %}
<tr>
<td>#{{ a.id }}</td>
<td>{{ a.title|default:"(untitled)" }}</td>
<td>{{ a.is_current|yesno:"✅,—" }}</td>
<td>
{{ a.start_at|date:"Y-m-d H:i" }}
{% if a.end_at %} → {{ a.end_at|date:"Y-m-d H:i" }}{% endif %}
</td>
<td>{{ a.created_at|date:"Y-m-d H:i" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</section>
{% endif %}
</div>
</div>
<!-- Toast -->
<div id="toast-clear-history"
style="position:fixed; right:16px; bottom:16px; padding:10px 14px; border-radius:10px;
background:#111827; color:#fff; box-shadow:0 6px 20px rgba(0,0,0,.25);
opacity:0; pointer-events:none; transition:opacity .25s;">
History cleared.
</div>
<style>
/* ---------- Console Layout ---------- */
.settings-console .cc-title{margin:0;font-size:24px;font-weight:700;color:#0f172a}
.settings-console .cc-subtitle{color:#64748b;margin-top:6px}
.cc-header{margin:6px 0 14px}
.cc-quick{
display:flex; align-items:center; gap:14px; padding:14px 16px; margin-bottom:18px;
}
.qc-item{display:flex; align-items:center; gap:10px}
.qc-label{font-size:13px; text-transform:uppercase; letter-spacing:.06em; color:#64748b}
.qc-inline{display:flex; gap:8px; align-items:center}
.qc-select{min-width:180px}
.qc-btn{padding:8px 12px}
.qc-divider{width:1px; height:26px; background:var(--border,#e5e7eb); opacity:.8}
/* 3-column; collapse on small screens */
.cc-grid{
display:grid;
grid-template-columns: repeat(3, minmax(0,1fr));
gap:16px;
}
@media (max-width: 1020px){ .cc-grid{ grid-template-columns: 1fr 1fr; } }
@media (max-width: 720px){ .cc-grid{ grid-template-columns: 1fr; } }
.cc-panel{padding:16px 16px 14px}
.cc-panel-head{margin-bottom:8px}
.cc-kicker{font-size:12px; text-transform:uppercase; letter-spacing:.08em; color:#64748b}
.cc-kicker-sec{color:#22c55e}
.cc-panel-title{margin:2px 0 0; font-size:18px; font-weight:700; color:#0f172a}
.cc-panel-body{margin-top:8px}
.cc-label{display:block; font-weight:600; margin:8px 0 6px; color:#0f172a}
.cc-actions{display:flex; align-items:center; gap:10px; margin-top:10px}
.cc-form .tool-input{border:1px solid var(--border,#d1d5db); border-radius:10px; padding:8px 10px; font-size:14px}
/* Switch */
.switch{position:relative; display:inline-block; width:46px; height:26px}
.switch input{display:none}
.slider{
position:absolute; cursor:pointer; inset:0; background:#e5e7eb; border-radius:999px; transition:all .15s;
}
.slider:before{
content:""; position:absolute; height:20px; width:20px; left:3px; top:3px; background:white; border-radius:50%; transition:all .15s; box-shadow:0 2px 6px rgba(0,0,0,.15);
}
.switch input:checked + .slider{ background:#22c55e; }
.switch input:checked + .slider:before{ transform:translateX(20px); }
/* Swatches */
.swatch-grid{
display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; margin-top:12px;
}
@media (max-width:720px){ .swatch-grid{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
.swatch{
position:relative; display:flex; align-items:flex-end; justify-content:flex-start;
height:64px; border:1px solid var(--border,#e5e7eb); border-radius:12px; padding:8px;
background:linear-gradient(135deg,#eef2f7,#dfe7f2); cursor:pointer;
}
.swatch[data-theme="classic"]{ background:linear-gradient(110deg,#d7b592 0%,#e7e3db 35%,#8fbfe0 100%); }
.swatch[data-theme="dawn"]{ background:linear-gradient(135deg,#ffd9a0 0%,#ffb6b9 40%,#a3d5ff 100%); }
.swatch[data-theme="midnight"]{ background:linear-gradient(135deg,#0b1220,#1c2741); }
.swatch[data-theme="forest"]{ background:linear-gradient(135deg,#d7f3e2,#92c7a3); }
.swatch[data-theme="sandstone"]{ background:linear-gradient(135deg,#f7efe4,#e4d2b6); }
.swatch-name{
background:rgba(255,255,255,.8); padding:2px 6px; border-radius:8px; font-size:12px; color:#0f172a;
}
/* Security panel accent */
.cc-sec{ position:relative; overflow:hidden; color:#0f172a; }
.cc-sec .cc-panel-title{ color:#0f172a; }
.cc-sec .cc-kicker{ color:#16a34a; }
.cc-sec-bg{
position:absolute; inset:-1px; z-index:0;
background: radial-gradient(800px 300px at 90% -20%, rgba(34,197,94,.25), transparent 55%),
linear-gradient(135deg, rgba(34,197,94,.10), rgba(59,130,246,.06));
pointer-events:none;
}
.cc-sec > * { position:relative; z-index:1; }
.sec-grid{ display:grid; grid-template-columns:1fr; gap:10px; }
.sec-tile{
display:flex; align-items:center; gap:12px; padding:12px; border:1px solid rgba(34,197,94,.25);
border-radius:12px; background:rgba(255,255,255,.8); text-decoration:none; color:inherit;
transition:transform .12s, box-shadow .12s, border-color .12s;
}
.sec-tile:hover{
transform: translateY(-2px);
box-shadow:0 10px 24px rgba(16,24,40,.08);
border-color: rgba(34,197,94,.45);
}
.sec-icon{
display:inline-flex; align-items:center; justify-content:center; width:36px; height:36px;
border-radius:10px; background:rgba(34,197,94,.18); color:#065f46;
}
.sec-title{ font-weight:700; }
.sec-sub{ font-size:12px; color:#64748b; }
.sec-meta{ flex:1; }
.sec-cta{ font-size:13px; color:#0f172a; opacity:.7 }
/* Tiny helper */
.tiny{ font-size:12px }
/* Make the quick strip responsive */
@media (max-width:720px){
.cc-quick{ flex-direction:column; align-items:stretch; gap:10px; }
.qc-divider{ display:none; }
.qc-inline{ width:100%; }
}
</style>
<script>
(function(){
// --- Shared helpers ---
function getCookie(name) {
const m = document.cookie.match("(^|;)\\s*" + name + "\\s*=\\s*([^;]+)");
return m ? m.pop() : "";
}
function setThemeHref(name){
var link = document.getElementById('theme-css');
if (link) link.href = '{% static "themes/" %}' + name + '.css';
document.documentElement.setAttribute('data-theme', name);
try { localStorage.setItem('theme', name); } catch(e){}
// Classic gradient body toggle (matches base.html behavior)
if (name === 'classic') { document.body.classList.add('themed-bg'); }
else { document.body.classList.remove('themed-bg'); }
}
// --- Clear History (both buttons) ---
function wireClear(btnId){
var btn = document.getElementById(btnId);
if (!btn) return;
btn.addEventListener("click", function(){
fetch("{% url 'clear_history' %}", { method:"POST", headers:{ "X-CSRFToken": getCookie("csrftoken") }})
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => {
const keys = ["recent_searches","recent_entries","recent_viewed","recently_viewed","recent_results","recentSearches","recentEntries"];
try {
keys.forEach(k => {
localStorage.removeItem(k);
sessionStorage.removeItem(k);
document.cookie = k + "=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
});
} catch(e){}
var toast = document.getElementById("toast-clear-history");
if (toast){ toast.style.opacity = "1"; setTimeout(() => toast.style.opacity = "0", 1500); }
if (data && data.cleared) console.log("Cleared server keys:", data.cleared);
})
.catch(() => {});
});
}
wireClear("clear-history-btn");
wireClear("clear-history-btn-dup");
// --- Theme: quick strip select (id="theme") ---
var themeSel = document.getElementById('theme');
if (themeSel) {
themeSel.addEventListener('change', function(e){
setThemeHref(e.target.value);
});
}
// --- Theme: panel select (id="theme-dup") + save button ---
window.saveThemeFromDuplicate = function(){
var dup = document.getElementById('theme-dup');
if (!dup) return false;
var name = dup.value;
setThemeHref(name);
// Also mirror back to the quick select if present
if (themeSel) themeSel.value = name;
// Submit a hidden form post to persist session theme
var f = document.createElement('form');
f.method = 'POST';
f.action = "{% url 'set_theme' %}";
var csrf = document.createElement('input');
csrf.type = 'hidden'; csrf.name = 'csrfmiddlewaretoken'; csrf.value = getCookie('csrftoken');
var t = document.createElement('input');
t.type = 'hidden'; t.name = 'theme'; t.value = name;
f.appendChild(csrf); f.appendChild(t);
document.body.appendChild(f);
f.submit();
return false; // prevent default
};
// --- Theme swatches (instant preview) ---
document.querySelectorAll('.swatch').forEach(btn => {
btn.addEventListener('click', () => {
const name = btn.getAttribute('data-theme');
setThemeHref(name);
// sync selects
if (themeSel) themeSel.value = name;
var dup = document.getElementById('theme-dup');
if (dup) dup.value = name;
});
});
// --- Highlight toggle (quick + panel are synced) ---
async function loadPrefsAndWire(){
let initial = false;
try {
const res = await fetch("{% url 'api_get_prefs' %}");
const data = await res.json();
if (data && typeof data.highlight_search_hits !== "undefined") {
initial = !!data.highlight_search_hits;
}
} catch(e){}
const toggles = [document.getElementById("highlightHitsToggle"), document.getElementById("highlightHitsToggle-dup")].filter(Boolean);
toggles.forEach(t => t.checked = initial);
function save(val){
const form = new FormData();
form.append("enabled", val ? "true" : "false");
fetch("{% url 'api_set_highlight_hits' %}", { method:"POST", body:form, headers:{ "X-CSRFToken": getCookie("csrftoken") }})
.catch(() => alert("Could not save the setting. Please try again."));
}
toggles.forEach(t => t.addEventListener('change', () => {
const val = t.checked;
toggles.forEach(o => { if (o !== t) o.checked = val; });
save(val);
}));
}
loadPrefsAndWire();
})();
</script>
{% endblock %}