Files
thrilltrack-explorer/django/staticfiles/unfold/js/app.js
pacnpal d6ff4cc3a3 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.
2025-11-08 15:34:04 -05:00

307 lines
7.3 KiB
JavaScript

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();
});
};