pyrss-ng/src/client/public/js/guild/subscriptions.js

339 lines
14 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()}`;
}
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 `
`;
}
},
{
// 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) => {
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);
}
// 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");
$("#formPublishedThreshold").val(new Date().toISOString().slice(0, 16));
$("#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 = $("#subForm").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(error);
}
});
}
$("#subForm").on("submit", submitForm);