383 lines
17 KiB
JavaScript
383 lines
17 KiB
JavaScript
|
|
const formatTimestamp = timestamp => {
|
|
let d;
|
|
if (typeof timestamp === "string") {
|
|
d = new Date(timestamp.replace(" ", "T"));
|
|
}
|
|
else {
|
|
d = new Date(timestamp);
|
|
}
|
|
|
|
const now = new Date();
|
|
|
|
// If younger than a year, show time
|
|
// otherwise show the year
|
|
return now - d < 31536000000
|
|
? `${d.getDate()} ${d.toLocaleString("en-GB", { month: "short" })}, ${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`
|
|
: `${d.getDate()} ${d.toLocaleString("en-GB", { month: "short" })} ${d.getFullYear()}`;
|
|
}
|
|
|
|
//#region Table
|
|
|
|
const emptyTableHtml = `
|
|
<div class="max-w-md w-full min-h-[400px] flex flex-col justify-center mx-auto px-6 py-4">
|
|
<div class="flex justify-center items-center size-[46px] bg-gray-100 rounded-lg dark:bg-neutral-800">
|
|
<svg class="shrink-0 size-6 text-gray-600 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg>
|
|
</div>
|
|
<h2 class="mt-5 font-semibold text-gray-800 dark:text-white">
|
|
No results found
|
|
</h2>
|
|
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
|
|
Create a subscription and it will appear here.
|
|
</p>
|
|
|
|
<div class="mt-5 flex flex-col sm:flex-row gap-2">
|
|
<button type="button" class="openSubModal-js py-2 px-3 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none" data-hs-overlay="#subModal">
|
|
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
|
Create a subscription
|
|
</button>
|
|
<button type="button" onclick="alert('not implemented');" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 focus:outline-none focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
|
Use a Template
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
var table;
|
|
const defineTable = () => {
|
|
table = new HSDataTable("#table", {
|
|
ajax: {
|
|
url: `/guild/${guildId}/subscriptions/api/datatable`,
|
|
dataSrc: "data",
|
|
data: (d) => {
|
|
if (d === undefined) { return ;}
|
|
|
|
d.filters = {};
|
|
const active = $("input[name='filterActive']:checked").val();
|
|
d.filters.active = active;
|
|
}
|
|
},
|
|
serverSide: true,
|
|
processing: true,
|
|
selecting: true,
|
|
pagingOptions: {
|
|
pageBtnClasses: "hidden"
|
|
},
|
|
rowSelectingOptions: {
|
|
selectAllSelector: "#selectAllBox"
|
|
},
|
|
language: {
|
|
zeroRecords: emptyTableHtml,
|
|
emptyTable: emptyTableHtml,
|
|
loading: "Placeholder Loading Message...",
|
|
},
|
|
rowCallback: (row, data, index) => {
|
|
$(row).addClass("bg-white dark:bg-neutral-900");
|
|
},
|
|
drawCallback: () => {
|
|
HSDropdown.autoInit();
|
|
},
|
|
select: {
|
|
style: "multi",
|
|
selector: "td:first-child input[type='checkbox']"
|
|
},
|
|
columnDefs: [
|
|
{
|
|
// Row select checkbox
|
|
targets: 0,
|
|
orderable: false,
|
|
searchable: false,
|
|
render: (data, type, row) => {
|
|
return `
|
|
<td class="size-px whitespace-nowrap">
|
|
<div class="ps-6 py-4">
|
|
<label for="rowSelect${row.id}-js" class="flex">
|
|
<input type="checkbox" id="rowSelect${row.id}-js" class="form-checkbox shrink-0 border-gray-300 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" data-hs-datatable-row-selecting-individual="">
|
|
<span class="sr-only">Checkbox</span>
|
|
</label>
|
|
</div>
|
|
</td>
|
|
`;
|
|
}
|
|
},
|
|
{
|
|
// Name
|
|
targets: 1,
|
|
data: "name",
|
|
orderable: true,
|
|
searchable: true,
|
|
render: data => {
|
|
return `
|
|
<td class="size-px whitespace-nowrap align-top">
|
|
<a href="#" class="block px-6 py-4 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 text-nowrap">
|
|
${data}
|
|
</a>
|
|
</td>
|
|
`;
|
|
}
|
|
},
|
|
{
|
|
// Url
|
|
targets: 2,
|
|
data: "url",
|
|
orderable: true,
|
|
searchable: true,
|
|
render: data => {
|
|
return `
|
|
<td class="size-px whitespace-nowrap align-top">
|
|
<a href="${data}" class="block px-6 py-4 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 text-nowrap" target="_blank">
|
|
${data}
|
|
</a>
|
|
</td>
|
|
`;
|
|
}
|
|
},
|
|
{
|
|
// Channels
|
|
target: 3,
|
|
data: "channels",
|
|
orderable: false,
|
|
searchable: false,
|
|
render: (data, type, row) => {
|
|
if (type !== "display") { return data; }
|
|
if (!data.length) { return ""; }
|
|
|
|
const wrapper = $("<div>").addClass("flex flex-nowrap gap-1 px-6 py-4");
|
|
const tag = $("<span>").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 border text-xs rounded-md bg-white dark:bg-neutral-800 border-gray-200 dark:border-neutral-700 text-gray-800 dark:text-neutral-200");
|
|
|
|
const firstChannelName = "# " + channels.find(c => c.id === data[0]).name;
|
|
wrapper.append(tag.clone().text(firstChannelName));
|
|
|
|
// No need to run the dropdown code if there's no more to show
|
|
if (data.length === 1) {
|
|
return wrapper.get(0);
|
|
}
|
|
else if (data.length <= 2) {
|
|
const secondChannelName = "# " + channels.find(c => c.id === data[1]).name;
|
|
wrapper.append(tag.clone().text(secondChannelName));
|
|
data.shift();
|
|
return wrapper.get(0);
|
|
}
|
|
|
|
// drop the first element to exclude it from the dropdown
|
|
data.shift();
|
|
|
|
const dropdown = $("<div>").addClass("hs-dropdown inline-block");
|
|
const dropdownBtn = $("<button>").attr("id", `channelDrop-${row.id}`).attr("type", "button").addClass("cursor-pointer inline-flex items-center gap-1 py-1 px-2.5 border text-xs rounded-md bg-white dark:bg-neutral-800 border-gray-200 dark:border-neutral-700 text-gray-800 dark:text-neutral-200");
|
|
const dropdownMenu = $("<div>").addClass("hs-dropdown-menu hidden opacity-0 hs-dropdown-open:opacity-100 transition-[opacity,margin] overflow-hidden z-10 w-fit max-w-64 border p-2 rounded-md bg-gray-200 dark:bg-neutral-700 border-gray-300 dark:border-neutral-600");
|
|
|
|
dropdown.append(dropdownBtn.text(`+${data.length}`));
|
|
|
|
data.forEach(channelId => {
|
|
channelName = "# " + channels.find(c => c.id === channelId).name;
|
|
dropdownMenu.append(tag.clone().text(channelName));
|
|
});
|
|
|
|
dropdown.append(dropdownMenu);
|
|
wrapper.append(dropdown);
|
|
return wrapper.get(0);
|
|
}
|
|
},
|
|
{
|
|
// Filters
|
|
target: 4,
|
|
data: "filters",
|
|
orderable: false,
|
|
searchable: false,
|
|
render: (data, type, row) => {
|
|
return `
|
|
|
|
`;
|
|
}
|
|
},
|
|
{
|
|
// Style
|
|
target: 5,
|
|
style: "style",
|
|
orderable: false,
|
|
searchable: false,
|
|
render: (data, type, row) => {
|
|
return `
|
|
<td class="size-px whitespace-nowrap align-top">
|
|
<div class="px-6 py-4 text-center">
|
|
<div class="inline-block rounded-md size-5 bg-red-500 mx-auto">
|
|
</div>
|
|
</div>
|
|
</td>
|
|
`;
|
|
}
|
|
},
|
|
{
|
|
// Created At
|
|
target: 6,
|
|
data: "created_at",
|
|
orderable: true,
|
|
searchable: true,
|
|
render: data => {
|
|
return `
|
|
<td class="size-px whitespace-nowrap align-top">
|
|
<div class="px-6 py-4">
|
|
<span class="text-sm text-gray-500 dark:text-neutral-500 text-nowrap">
|
|
${formatTimestamp(data)}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
`;
|
|
}
|
|
},
|
|
{
|
|
// Status
|
|
target: 7,
|
|
data: "active",
|
|
orderable: true,
|
|
searchable: true,
|
|
render: (data, type, row) => {
|
|
|
|
// TODO:
|
|
// fix the badge icon not showing,
|
|
// its probvably because of jquery adding a closing tag ?
|
|
|
|
wrapper = $("<div>").addClass("px-6 py-4");
|
|
badge = $("<span>").addClass("py-1 px-1.5 inline-flex items-center gap-x-1 text-xs font-medium rounded-full");
|
|
icon = $('<svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">');
|
|
label = $("<span>");
|
|
|
|
if (row.active) {
|
|
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
|
icon.append($('<svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg>'));
|
|
badge.append(icon).append(label.text("Active"));
|
|
}
|
|
else {
|
|
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
|
|
icon.append('<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>');
|
|
badge.append(icon).append(label.text("Inactive"));
|
|
}
|
|
|
|
wrapper.append(badge);
|
|
return wrapper.get(0);
|
|
|
|
// if (!row.active) {
|
|
// return `
|
|
// <div class="px-6 py-4">
|
|
// <span class="py-1 px-1.5 inline-flex items-center gap-x-1 text-xs font-medium bg-red-100 text-red-800 rounded-full dark:bg-red-500/10 dark:text-red-500">
|
|
// <svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
// <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
|
|
// </svg>
|
|
// Inactive
|
|
// </span>
|
|
// </div>
|
|
// `;
|
|
// }
|
|
// return `
|
|
// <div class="px-6 py-4">
|
|
// <span class="py-1 px-1.5 inline-flex items-center gap-x-1 text-xs font-medium bg-teal-100 text-teal-800 rounded-full dark:bg-teal-500/10 dark:text-teal-500">
|
|
// <svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
// <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
|
// </svg>
|
|
// Active
|
|
// </span>
|
|
// </div>
|
|
// `;
|
|
}
|
|
}
|
|
]
|
|
})
|
|
|
|
table.dataTable
|
|
.on("select", onTableSelectChange)
|
|
.on("deselect", onTableSelectChange)
|
|
.on("draw", onTableSelectChange);
|
|
}
|
|
|
|
//#endregion Table
|
|
|
|
// Ensure the datatable recognises when all rows are selected, otherwise rows are only visually selected
|
|
$("#selectAllBox").on("change", function() {
|
|
this.checked ? table.dataTable.rows().select() : table.dataTable.rows().deselect();
|
|
});
|
|
|
|
const onTableSelectChange = () => {
|
|
const selectedRowCount = table.dataTable.rows({ selected: true }).count();
|
|
$("#deleteRowsBtn").prop("disabled", selectedRowCount === 0);
|
|
$(".rows-selected-count-js").text(selectedRowCount);
|
|
|
|
const $elem = $(".rows-selected-count-js.zero-empty-js");
|
|
selectedRowCount === 0 ? $elem.hide() : $elem.show();
|
|
}
|
|
|
|
$("#deleteRowsBtn").on("click", async () => {
|
|
const rowIds = table.dataTable.rows({ selected: true }).data().toArray().map(row => row.id);
|
|
console.log(JSON.stringify(rowIds))
|
|
await $.ajax({
|
|
url: `/guild/${guildId}/subscriptions/api`,
|
|
method: "delete",
|
|
dataType: "json",
|
|
data: { ids: rowIds },
|
|
success: () => {
|
|
table.dataTable.draw();
|
|
table.dataTable.rows().deselect();
|
|
},
|
|
error: error => {
|
|
alert(typeof error === "object" ? JSON.stringify(error, null, 4) : error);
|
|
}
|
|
});
|
|
});
|
|
|
|
$(window).ready(() => {
|
|
setTimeout(defineTable, 500);
|
|
});
|
|
|
|
$("input[name='filterActive']").on("change", () => {
|
|
table.dataTable.draw();
|
|
});
|
|
|
|
const openSubForm = () => {
|
|
$("#subForm").removeClass("submitted");
|
|
|
|
// Clear the form values
|
|
$("#formName").val("");
|
|
$("#formUrl").val("");
|
|
$("#formPublishedThreshold").val(new Date().toISOString().slice(0, 16));
|
|
|
|
HSSelect.getInstance("#formStyle", true).element.setValue([]);
|
|
HSSelect.getInstance("#formChannels", true).element.setValue([]);
|
|
HSSelect.getInstance("#formFilters", true).element.setValue([]);
|
|
$("#formChannelsInput").css("width", "");
|
|
$("#formFiltersInput").css("width", "");
|
|
|
|
$("#formActive").prop("checked", true);
|
|
|
|
HSOverlay.open($("#subModal").get(0));
|
|
}
|
|
|
|
const closeSubForm = () => {
|
|
$("#subForm").removeClass("submitted");
|
|
HSOverlay.close($("#subModal").get(0));
|
|
}
|
|
|
|
$(document).on("click", ".openSubModal-js", openSubForm);
|
|
|
|
const submitForm = async event => {
|
|
event.preventDefault();
|
|
|
|
const form = $(event.target).get(0);
|
|
$(form).addClass("submitted");
|
|
|
|
if (!form.checkValidity()) { return; }
|
|
|
|
await $.ajax({
|
|
url: `/guild/${guildId}/subscriptions/api`,
|
|
method: "post",
|
|
dataType: "json",
|
|
data: $(event.target).serializeArray(),
|
|
success: () => {
|
|
table.dataTable.draw();
|
|
closeSubForm();
|
|
},
|
|
error: error => {
|
|
alert(JSON.stringify(error, null, 4));
|
|
}
|
|
});
|
|
}
|
|
|
|
$("#subForm").on("submit", submitForm); |