Add email templates for user notifications and account management

- Created a base email template (base.html) for consistent styling across all emails.
- Added moderation approval email template (moderation_approved.html) to notify users of approved submissions.
- Added moderation rejection email template (moderation_rejected.html) to inform users of required changes for their submissions.
- Created password reset email template (password_reset.html) for users requesting to reset their passwords.
- Developed a welcome email template (welcome.html) to greet new users and provide account details and tips for using ThrillWiki.
This commit is contained in:
pacnpal
2025-11-08 15:34:04 -05:00
parent 9c46ef8b03
commit d6ff4cc3a3
335 changed files with 61926 additions and 73 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
(()=>{function d(t){let n=()=>{let r,a;try{a=localStorage}catch(i){console.error(i),console.warn("Alpine: $persist is using temporary storage since localStorage is unavailable.");let e=new Map;a={getItem:e.get.bind(e),setItem:e.set.bind(e)}}return t.interceptor((i,e,l,s,f)=>{let o=r||`_x_${s}`,u=g(o,a)?p(o,a):i;return l(u),t.effect(()=>{let c=e();m(o,c,a),l(c)}),u},i=>{i.as=e=>(r=e,i),i.using=e=>(a=e,i)})};Object.defineProperty(t,"$persist",{get:()=>n()}),t.magic("persist",n),t.persist=(r,{get:a,set:i},e=localStorage)=>{let l=g(r,e)?p(r,e):a();i(l),t.effect(()=>{let s=a();m(r,s,e),i(s)})}}function g(t,n){return n.getItem(t)!==null}function p(t,n){let r=n.getItem(t,n);if(r!==void 0)return JSON.parse(r)}function m(t,n,r){r.setItem(t,JSON.stringify(n))}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(d)});})();

View File

@@ -0,0 +1,306 @@
window.addEventListener("load", (e) => {
submitSearch();
fileInputUpdatePath();
dateTimeShortcutsOverlay();
renderCharts();
filterForm();
warnWithoutSaving();
});
/*************************************************************
* Warn without saving
*************************************************************/
const warnWithoutSaving = () => {
let formChanged = false;
const form = document.querySelector("form.warn-unsaved-form");
const checkFormChanged = () => {
const elements = document.querySelectorAll(
"form.warn-unsaved-form input, form.warn-unsaved-form select, form.warn-unsaved-form textarea"
);
Array.from(elements).forEach((field) => {
field.addEventListener("input", (e) => (formChanged = true));
});
};
if (!form) {
return;
}
new MutationObserver((mutationsList, observer) => {
checkFormChanged();
}).observe(form, { attributes: true, childList: true, subtree: true });
checkFormChanged();
preventLeaving = (e) => {
if (formChanged) {
e.preventDefault();
}
};
form.addEventListener("submit", (e) => {
window.removeEventListener("beforeunload", preventLeaving);
});
window.addEventListener("beforeunload", preventLeaving);
};
/*************************************************************
* Filter form
*************************************************************/
const filterForm = () => {
const filterForm = document.getElementById("filter-form");
if (!filterForm) {
return;
}
filterForm.addEventListener("formdata", (event) => {
Array.from(event.formData.entries()).forEach(([key, value]) => {
if (value === "") event.formData.delete(key);
});
});
};
/*************************************************************
* Class watcher
*************************************************************/
const watchClassChanges = (selector, callback) => {
const body = document.querySelector(selector);
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (
mutation.type === "attributes" &&
mutation.attributeName === "class"
) {
callback();
}
}
});
observer.observe(body, { attributes: true, attributeFilter: ["class"] });
};
/*************************************************************
* Calendar & clock
*************************************************************/
const dateTimeShortcutsOverlay = () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutationRecord) => {
const display = mutationRecord.target.style.display;
const overlay = document.getElementById("modal-overlay");
if (display === "block") {
overlay.style.display = "block";
} else {
overlay.style.display = "none";
}
});
});
const targets = document.querySelectorAll(".calendarbox, .clockbox");
Array.from(targets).forEach((target) => {
observer.observe(target, {
attributes: true,
attributeFilter: ["style"],
});
});
};
/*************************************************************
* File upload path
*************************************************************/
const fileInputUpdatePath = () => {
Array.from(document.querySelectorAll("input[type=file]")).forEach((input) => {
input.addEventListener("change", (e) => {
const parts = e.target.value.split("\\");
const placeholder =
input.parentNode.parentNode.parentNode.querySelector(
"input[type=text]"
);
placeholder.setAttribute("value", parts[parts.length - 1]);
});
});
};
/*************************************************************
* Search form on changelist view
*************************************************************/
const submitSearch = () => {
const searchbar = document.getElementById("searchbar");
const searchbarSubmit = document.getElementById("searchbar-submit");
const getQueryParams = (searchString) => {
const queryParams = window.location.search
.replace("?", "")
.split("&")
.map((param) => param.split("="))
.reduce((values, [key, value]) => {
if (key && key !== "q") {
values[key] = value;
}
return values;
}, {});
if (searchString) {
queryParams["q"] = searchString;
}
const result = Object.entries(queryParams)
.map(([key, value]) => `${key}=${value}`)
.join("&");
return `?${result}`;
};
if (searchbar !== null) {
searchbar.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
window.location = getQueryParams(e.target.value);
e.preventDefault();
}
});
}
if (searchbarSubmit !== null && searchbar !== null) {
searchbarSubmit.addEventListener("click", (e) => {
e.preventDefault();
window.location = getQueryParams(searchbar.value);
});
}
};
/*************************************************************
* Chart
*************************************************************/
const DEFAULT_CHART_OPTIONS = {
animation: false,
barPercentage: 1,
base: 0,
grouped: false,
maxBarThickness: 4,
responsive: true,
maintainAspectRatio: false,
datasets: {
bar: {
borderRadius: 12,
border: {
width: 0,
},
borderSkipped: "middle",
},
line: {
borderWidth: 2,
pointBorderWidth: 0,
pointStyle: false,
},
},
plugins: {
legend: {
align: "end",
display: false,
position: "top",
labels: {
boxHeight: 5,
boxWidth: 5,
color: "#9ca3af",
pointStyle: "circle",
usePointStyle: true,
},
},
tooltip: {
enabled: true,
},
},
scales: {
x: {
border: {
dash: [5, 5],
dashOffset: 2,
width: 0,
},
ticks: {
color: "#9ca3af",
display: true,
},
grid: {
display: true,
tickWidth: 0,
},
},
y: {
border: {
dash: [5, 5],
dashOffset: 5,
width: 0,
},
ticks: {
display: false,
font: {
size: 13,
},
},
grid: {
lineWidth: function (context) {
if (context.tick.value === 0) {
return 1;
}
return 0;
},
tickWidth: 0,
},
},
},
};
const renderCharts = () => {
let charts = [];
const changeDarkModeSettings = () => {
const hasDarkClass = document
.querySelector("html")
.classList.contains("dark");
charts.forEach((chart) => {
chart.options.scales.x.grid.color = hasDarkClass ? "#374151" : "#d1d5db";
chart.options.scales.y.grid.color = hasDarkClass ? "#374151" : "#d1d5db";
chart.update();
});
};
Array.from(document.querySelectorAll(".chart")).forEach((chart) => {
const ctx = chart.getContext("2d");
const data = chart.dataset.value;
const type = chart.dataset.type;
const options = chart.dataset.options;
if (!data) {
return;
}
charts.push(
new Chart(ctx, {
type: type || "bar",
data: JSON.parse(chart.dataset.value),
options: options ? JSON.parse(options) : DEFAULT_CHART_OPTIONS,
})
);
});
changeDarkModeSettings();
watchClassChanges("html", () => {
changeDarkModeSettings();
});
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
"use strict";
{
const $ = django.jQuery;
$.fn.djangoCustomSelect2 = function () {
$.each(this, function (i, element) {
$(element).select2();
});
return this;
};
$(function () {
$(".admin-autocomplete").djangoCustomSelect2();
});
}

File diff suppressed because one or more lines are too long