add filters
This commit is contained in:
parent
7434cd1d09
commit
b66c6f173d
@ -1383,6 +1383,10 @@ video {
|
|||||||
resize: both;
|
resize: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-cols-1 {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.flex-row {
|
.flex-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
@ -1806,11 +1810,6 @@ video {
|
|||||||
padding-bottom: 0.375rem;
|
padding-bottom: 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.py-10 {
|
|
||||||
padding-top: 2.5rem;
|
|
||||||
padding-bottom: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.py-2 {
|
.py-2 {
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
@ -2813,11 +2812,6 @@ hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 te
|
|||||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover\:bg-gray-200:hover {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover\:bg-gray-300:hover {
|
.hover\:bg-gray-300:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
|
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
|
||||||
@ -2858,11 +2852,6 @@ hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 te
|
|||||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.focus\:bg-gray-200:focus {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.focus\:bg-gray-50:focus {
|
.focus\:bg-gray-50:focus {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
|
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
|
||||||
@ -3449,11 +3438,6 @@ hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 te
|
|||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lg\:py-14 {
|
|
||||||
padding-top: 3.5rem;
|
|
||||||
padding-bottom: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lg\:ps-64 {
|
.lg\:ps-64 {
|
||||||
padding-inline-start: 16rem;
|
padding-inline-start: 16rem;
|
||||||
}
|
}
|
||||||
@ -3656,11 +3640,6 @@ hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 te
|
|||||||
background-color: rgb(191 219 254 / var(--tw-bg-opacity, 1));
|
background-color: rgb(191 219 254 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-neutral-600:hover:where(.dark, .dark *) {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(82 82 82 / var(--tw-bg-opacity, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark\:hover\:bg-neutral-700:hover:where(.dark, .dark *) {
|
.dark\:hover\:bg-neutral-700:hover:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(64 64 64 / var(--tw-bg-opacity, 1));
|
background-color: rgb(64 64 64 / var(--tw-bg-opacity, 1));
|
||||||
@ -3696,11 +3675,6 @@ hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 te
|
|||||||
color: rgb(115 115 115 / var(--tw-text-opacity, 1));
|
color: rgb(115 115 115 / var(--tw-text-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:focus\:bg-neutral-600:focus:where(.dark, .dark *) {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(82 82 82 / var(--tw-bg-opacity, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark\:focus\:bg-neutral-700:focus:where(.dark, .dark *) {
|
.dark\:focus\:bg-neutral-700:focus:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(64 64 64 / var(--tw-bg-opacity, 1));
|
background-color: rgb(64 64 64 / var(--tw-bg-opacity, 1));
|
||||||
|
266
src/client/public/js/guild/filters.js
Normal file
266
src/client/public/js/guild/filters.js
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
|
||||||
|
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 filter and it will appear here.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-5 flex flex-col sm:flex-row gap-2">
|
||||||
|
<button type="button" class="openfilterModal-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 filter
|
||||||
|
</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}/filters/api/datatable`,
|
||||||
|
dataSrc: "data",
|
||||||
|
data: (d) => {
|
||||||
|
if (d === undefined) { return ;}
|
||||||
|
|
||||||
|
d.filters = {};
|
||||||
|
const is_whitelist = $("input[name='filterType']:checked").val();
|
||||||
|
d.filters.is_whitelist = is_whitelist;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Match
|
||||||
|
targets: 2,
|
||||||
|
data: "match",
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Algorithm
|
||||||
|
target: 3,
|
||||||
|
data: "algorithm",
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
render: (data, type, row) => {
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Filters
|
||||||
|
target: 4,
|
||||||
|
data: "is_insensitive",
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
render: (data, type, row) => {
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Whitelist
|
||||||
|
target: 5,
|
||||||
|
data: "is_whitelist",
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
render: (data, type, row) => {
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
$(window).ready(() => {
|
||||||
|
setTimeout(defineTable, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("input[name='filterType']").on("change", () => {
|
||||||
|
table.dataTable.draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
const openFilterForm = () => {
|
||||||
|
$("#filterForm").removeClass("submitted");
|
||||||
|
HSOverlay.open($("#filterModal").get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeFilterForm = () => {
|
||||||
|
$("#filterForm").removeClass("submitted");
|
||||||
|
HSOverlay.close($("#filterModal").get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on("click", ".openfilterModal-js", openFilterForm);
|
||||||
|
|
||||||
|
const submitForm = async event => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const form = $(event.target).get(0);
|
||||||
|
$(form).addClass("submitted");
|
||||||
|
|
||||||
|
if (!form.checkValidity()) { return; }
|
||||||
|
|
||||||
|
await $.ajax({
|
||||||
|
url: `/guild/${guildId}/filters/api`,
|
||||||
|
method: "post",
|
||||||
|
dataType: "json",
|
||||||
|
data: $(event.target).serializeArray(),
|
||||||
|
success: () => {
|
||||||
|
table.dataTable.draw();
|
||||||
|
closeFilterForm();
|
||||||
|
},
|
||||||
|
error: error => {
|
||||||
|
alert(JSON.stringify(error, null, 4));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#filterForm").on("submit", submitForm);
|
@ -217,8 +217,7 @@ const defineTable = () => {
|
|||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
,
|
|
||||||
{
|
{
|
||||||
// Status
|
// Status
|
||||||
target: 7,
|
target: 7,
|
||||||
@ -297,13 +296,13 @@ $(window).ready(() => {
|
|||||||
|
|
||||||
$("input[name='filterActive']").on("change", () => {
|
$("input[name='filterActive']").on("change", () => {
|
||||||
table.dataTable.draw();
|
table.dataTable.draw();
|
||||||
})
|
});
|
||||||
|
|
||||||
const openSubForm = () => {
|
const openSubForm = () => {
|
||||||
$("#subForm").removeClass("submitted");
|
$("#subForm").removeClass("submitted");
|
||||||
$("#formPublishedThreshold").val(new Date().toISOString().slice(0, 16));
|
$("#formPublishedThreshold").val(new Date().toISOString().slice(0, 16));
|
||||||
$("#formActive").prop("checked", true);
|
$("#formActive").prop("checked", true);
|
||||||
HSOverlay.open($("#subModal").get(0))
|
HSOverlay.open($("#subModal").get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeSubForm = () => {
|
const closeSubForm = () => {
|
||||||
@ -316,7 +315,7 @@ $(document).on("click", ".openSubModal-js", openSubForm);
|
|||||||
const submitForm = async event => {
|
const submitForm = async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const form = $("#subForm").get(0);
|
const form = $(event.target).get(0);
|
||||||
$(form).addClass("submitted");
|
$(form).addClass("submitted");
|
||||||
|
|
||||||
if (!form.checkValidity()) { return; }
|
if (!form.checkValidity()) { return; }
|
||||||
@ -331,7 +330,7 @@ const submitForm = async event => {
|
|||||||
closeSubForm();
|
closeSubForm();
|
||||||
},
|
},
|
||||||
error: error => {
|
error: error => {
|
||||||
alert(error);
|
alert(JSON.stringify(error, null, 4));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,310 @@
|
|||||||
|
|
||||||
<%- include("guildHeader") -%>
|
<%- include("guildHeader") -%>
|
||||||
|
|
||||||
<div class="p-4">
|
<!-- Table Section -->
|
||||||
Filters
|
<div id="table" class="--prevent-on-load-init max-w-full overflow-hidden px-4 sm:px-6">
|
||||||
</div>
|
<!-- Card -->
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="-m-1.5">
|
||||||
|
<div class="max-w-full p-1.5 min-w-full inline-block align-middle">
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden dark:bg-neutral-900 dark:border-neutral-800">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="px-6 py-4 gap-3 flex flex-nowrap justify-between items-center border-b border-gray-200 dark:border-neutral-700">
|
||||||
|
<!-- Input -->
|
||||||
|
<div class="hidden sm:block sm:col-span-1">
|
||||||
|
<label for="search" class="sr-only">Search</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input type="text" id="search" name="search" class="form-input px-3 ps-11 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600" placeholder="Search" data-hs-datatable-search="">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4">
|
||||||
|
<svg class="shrink-0 size-4 text-gray-400 dark:text-neutral-500" 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"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- End Input -->
|
||||||
|
|
||||||
|
<div class="sm:col-span-2 md:grow">
|
||||||
|
<div class="flex justify-end gap-x-2">
|
||||||
|
|
||||||
|
<button type="button" id="deleteRowsBtn" disabled 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-red-500 shadow-sm hover:bg-gray-50 focus:outline-none focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800" href="#">
|
||||||
|
<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="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
||||||
|
<span>
|
||||||
|
<span class="hidden sm:inline">Delete</span>
|
||||||
|
<span class="rows-selected-count-js zero-empty-js before:content-['('] after:content-[')'] empty:hidden"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="hs-dropdown [--placement:bottom-right] relative inline-block h-full" data-hs-dropdown-auto-close="inside">
|
||||||
|
<button id="hs-as-table-table-filter-dropdown" type="button" 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" aria-haspopup="menu" aria-expanded="false" aria-label="Dropdown">
|
||||||
|
<svg class="shrink-0 size-3.5" 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="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg>
|
||||||
|
Filter
|
||||||
|
</button>
|
||||||
|
<div class="hs-dropdown-menu transition-[opacity,margin] duration hs-dropdown-open:opacity-100 opacity-0 hidden divide-y divide-gray-200 min-w-48 z-10 bg-white shadow-md rounded-lg mt-2 dark:divide-neutral-700 dark:bg-neutral-800 dark:border dark:border-neutral-700" role="menu" aria-orientation="vertical" aria-labelledby="hs-as-table-table-filter-dropdown">
|
||||||
|
<div class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||||
|
<label for="hs-as-filters-dropdown-all" class="flex py-2.5 px-3">
|
||||||
|
<input type="radio" name="filterType" class="form-radio shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-as-filters-dropdown-all" checked value="">
|
||||||
|
<span class="ms-3 text-sm text-gray-800 dark:text-neutral-200">All</span>
|
||||||
|
</label>
|
||||||
|
<label for="hs-as-filters-dropdown-published" class="flex py-2.5 px-3">
|
||||||
|
<input type="radio" name="filterType" class="form-radio shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-as-filters-dropdown-published" value="1">
|
||||||
|
<span class="ms-3 text-sm text-gray-800 dark:text-neutral-200">Whitelist</span>
|
||||||
|
</label>
|
||||||
|
<label for="hs-as-filters-dropdown-pending" class="flex py-2.5 px-3">
|
||||||
|
<input type="radio" name="filterType" class="form-radio shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-as-filters-dropdown-pending" value="0">
|
||||||
|
<span class="ms-3 text-sm text-gray-800 dark:text-neutral-200">Blacklist</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="openfilterModal-js py-2 px-3 inline-flex 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">
|
||||||
|
<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>
|
||||||
|
<span>
|
||||||
|
Add
|
||||||
|
<span class="hidden sm:inline">filter</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- End Header -->
|
||||||
|
|
||||||
|
<div class="min-w-full overflow-x-auto">
|
||||||
|
<!-- Table -->
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
|
||||||
|
<thead class="bg-gray-50 dark:bg-neutral-800 border-none">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="ps-6 py-3 text-start --exclude-from-ordering">
|
||||||
|
<label for="hs-at-with-checkboxes-main" class="flex ml-[1px]">
|
||||||
|
<input type="checkbox" id="selectAllBox" 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">
|
||||||
|
<span class="sr-only">Checkbox</span>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th scope="col" data-dt-column="name" class="px-6 py-3 text-start">
|
||||||
|
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||||
|
Name
|
||||||
|
</span>
|
||||||
|
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" 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 class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||||
|
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th scope="col" class="px-6 py-3 text-start">
|
||||||
|
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||||
|
Match
|
||||||
|
</span>
|
||||||
|
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" 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 class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||||
|
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th scope="col" class="px-6 py-3 text-start">
|
||||||
|
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||||
|
Algorithm
|
||||||
|
</span>
|
||||||
|
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" 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 class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||||
|
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th scope="col" class="px-6 py-3 text-start">
|
||||||
|
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200 text-nowrap">
|
||||||
|
Case-Sensitive
|
||||||
|
</span>
|
||||||
|
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" 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 class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||||
|
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th scope="col" class="px-6 py-3 text-start">
|
||||||
|
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||||
|
Type
|
||||||
|
</span>
|
||||||
|
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" 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 class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||||
|
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th scope="col" class="px-6 py-3 text-start">
|
||||||
|
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200 text-nowrap">
|
||||||
|
Created at
|
||||||
|
</span>
|
||||||
|
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" 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 class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||||
|
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!-- End Table -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="px-6 py-4 gap-3 flex justify-between items-center border-t border-gray-200 dark:border-neutral-700">
|
||||||
|
<div class="max-w-sm space-y-3">
|
||||||
|
<select data-hs-select='{
|
||||||
|
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
|
||||||
|
"toggleClasses": "form-select hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-2 px-3 pe-9 flex text-nowrap w-full cursor-pointer bg-white border border-gray-200 rounded-lg text-start text-sm text-gray-800 shadow-sm hover:bg-gray-50 focus:outline-none focus:bg-gray-50 before:absolute before:inset-0 before:z-[1] dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800",
|
||||||
|
"dropdownClasses": "mt-2 z-50 w-20 max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg shadow-md overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700",
|
||||||
|
"dropdownScope": "window",
|
||||||
|
"optionClasses": "py-2 px-3 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-none focus:bg-gray-100 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
|
||||||
|
"optionTemplate": "<div class=\"flex justify-between items-center w-full\"><span data-title></span><span class=\"hidden hs-selected:block\"><svg class=\"shrink-0 size-3.5 text-blue-600 dark:text-blue-500\" xmlns=\"http:.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\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>"
|
||||||
|
}' class="hidden" data-hs-datatable-page-entities="">
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="5">5</option>
|
||||||
|
<option value="10" selected>10</option>
|
||||||
|
<option value="15">15</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden sm:inline-flex items-center gap-x-2" data-hs-datatable-info="">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
||||||
|
Showing
|
||||||
|
<span data-hs-datatable-info-from=""></span>
|
||||||
|
to
|
||||||
|
<span data-hs-datatable-info-to=""></span>
|
||||||
|
of
|
||||||
|
<span data-hs-datatable-info-length=""></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline-flex gap-x-2" data-hs-datatable-paging="">
|
||||||
|
<button type="button" 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" data-hs-datatable-paging-prev="">
|
||||||
|
<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="m15 18-6-6 6-6"/></svg>
|
||||||
|
Prev
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center space-x-1 " data-hs-datatable-paging-pages=""></div>
|
||||||
|
<button type="button" 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" data-hs-datatable-paging-next="">
|
||||||
|
Next
|
||||||
|
<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="m9 18 6-6-6-6"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- End Footer -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- End Card -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- End Table Section -->
|
||||||
|
|
||||||
|
<!-- Popup -->
|
||||||
|
<div id="filterModal" class="hs-overlay hidden size-full fixed top-0 start-0 z-[80] overflow-x-hidden overflow-y-auto pointer-events-none" role="dialog" tabindex="-1" aria-labelledby="hs-scale-animation-modal-label">
|
||||||
|
<div class="hs-overlay-animation-target hs-overlay-open:scale-100 hs-overlay-open:opacity-100 scale-95 opacity-0 ease-in-out transition-all duration-200 lg:max-w-4xl lg:w-full m-3 lg:mx-auto min-h-[calc(100%-3.5rem)] flex items-center">
|
||||||
|
<div class="w-full p-4 sm:p-7 flex flex-col bg-white border shadow-sm rounded-lg pointer-events-auto dark:bg-neutral-900 dark:border-neutral-800 dark:shadow-neutral-700/70">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 dark:text-neutral-200">Filter</h2>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
||||||
|
Filter out unwanted content from your feeds.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form id="filterForm" novalidate class="group grid grid-cols-1 gap-y-4 sm:gap-y-6 md:gap-y-8 gap-x-6 sm:gap-x-8 md:gap-x-10">
|
||||||
|
<div>
|
||||||
|
<label for="formName" class="text-input-label">Name</label>
|
||||||
|
<input type="text" id="formName" name="name" class="form-input text-input peer group-[.submitted]:invalid:border-red-500 group-[.submitted]:invalid:ring-red-500" required>
|
||||||
|
<p class="text-input-help block group-[.submitted]:peer-[:invalid]:hidden">
|
||||||
|
Human-readable name for this entry.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm text-red-500 hidden group-[.submitted]:peer-[:invalid]:block">
|
||||||
|
Please enter a name.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="formMatch" class="text-input-label">Match</label>
|
||||||
|
<input type="text" id="formMatch" name="match" class="form-input text-input peer group-[.submitted]:invalid:border-red-500 group-[.submitted]:invalid:ring-red-500" required>
|
||||||
|
<p class="text-input-help block group-[.submitted]:peer-[:invalid]:hidden">
|
||||||
|
The
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm text-red-500 hidden group-[.submitted]:peer-[:invalid]:block">
|
||||||
|
Please enter a match.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<label for="formAlgorithm" class="text-input-label">Algorithm</label>
|
||||||
|
<select id="formAlgorithm" name="algorithm" class="peer" data-hs-select='{
|
||||||
|
"placeholder": "Select option...",
|
||||||
|
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
|
||||||
|
"toggleClasses": "form-select hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 select-input group-[.submitted]:peer-[:invalid]:border-red-500 group-[.submitted]:peer-[:invalid]:ring-red-500",
|
||||||
|
"dropdownScope": "window",
|
||||||
|
"wrapperClasses": "peer",
|
||||||
|
"dropdownClasses": "z-[80] w-full max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700",
|
||||||
|
"optionClasses": "py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-none focus:bg-gray-100 hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
|
||||||
|
"optionTemplate": "<div class=\"flex justify-between items-center w-full\"><span data-title></span><span class=\"hidden hs-selected:block\"><svg class=\"shrink-0 size-3.5 text-blue-600 dark:text-blue-500 \" xmlns=\"http:.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\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>"
|
||||||
|
}' class="hidden" required>
|
||||||
|
<option value="">Choose</option>
|
||||||
|
<option value="any">Match Any Word</option>
|
||||||
|
<option value="all">Match All Words</option>
|
||||||
|
<option value="exact">Match Exact Expression</option>
|
||||||
|
<option value="regex">Match Regex</option>
|
||||||
|
<option value="fuzzy">Match Roughly</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-input-help block group-[.submitted]:peer-has-[:invalid]:hidden">
|
||||||
|
The algorithm used to filter out content.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm text-red-500 hidden group-[.submitted]:peer-has-[:invalid]:block">
|
||||||
|
Please select an option.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label for="formWhitelist" class="flex gap-4">
|
||||||
|
<input type="checkbox" id="formWhitelist" name="is_whitelist" class="form-radio relative w-[3.25rem] h-7 p-px bg-gray-100 border-transparent text-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:ring-blue-600 disabled:opacity-50 disabled:pointer-events-none checked:bg-none checked:text-blue-600 checked:border-blue-600 focus:checked:border-blue-600 dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-600 before:inline-block before:size-6 before:bg-white checked:before:bg-blue-200 before:translate-x-0 checked:before:translate-x-full before:rounded-full before:shadow before:transform before:ring-0 before:transition before:ease-in-out before:duration-200 dark:before:bg-neutral-400 dark:checked:before:bg-blue-200">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span class="block text-sm dark:text-neutral-400">Whitelist</span>
|
||||||
|
<span class="block text-sm text-gray-500 dark:text-neutral-500">Inactive entries will not be processed.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label for="formInsensitive" class="flex gap-4">
|
||||||
|
<input type="checkbox" id="formInsensitive" name="is_insensitive" class="form-radio relative w-[3.25rem] h-7 p-px bg-gray-100 border-transparent text-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:ring-blue-600 disabled:opacity-50 disabled:pointer-events-none checked:bg-none checked:text-blue-600 checked:border-blue-600 focus:checked:border-blue-600 dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-600 before:inline-block before:size-6 before:bg-white checked:before:bg-blue-200 before:translate-x-0 checked:before:translate-x-full before:rounded-full before:shadow before:transform before:ring-0 before:transition before:ease-in-out before:duration-200 dark:before:bg-neutral-400 dark:checked:before:bg-blue-200">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span class="block text-sm dark:text-neutral-400">Insensitive</span>
|
||||||
|
<span class="block text-sm text-gray-500 dark:text-neutral-500">Inactive entries will not be processed.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
<div class="flex items-center gap-x-2 mt-8">
|
||||||
|
<button type="button" class="me-auto 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">
|
||||||
|
Templates
|
||||||
|
</button>
|
||||||
|
<button type="button" 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" data-hs-overlay="#filterModal">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button type="submit" form="filterForm" class="group-invalid:pointer-events-none group-invalid:opacity-30 py-2 px-3 inline-flex 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">
|
||||||
|
Save changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- End Popup -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var guildId = "<%- guild.id %>"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<% block("scripts").append('<script defer src="/static/js/guild/filters.js"></script>'); %>
|
@ -3,7 +3,7 @@
|
|||||||
<%- include("guildHeader") -%>
|
<%- include("guildHeader") -%>
|
||||||
|
|
||||||
<!-- Table Section -->
|
<!-- Table Section -->
|
||||||
<div id="table" class="--prevent-on-load-init max-w-full overflow-hidden px-4 sm:px-6"> <!-- px-4 py-10 sm:px-6 lg:px-8 lg:py-14 -->
|
<div id="table" class="--prevent-on-load-init max-w-full overflow-hidden px-4 sm:px-6">
|
||||||
<!-- Card -->
|
<!-- Card -->
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="-m-1.5">
|
<div class="-m-1.5">
|
||||||
@ -238,18 +238,6 @@
|
|||||||
Manage your RSS feeds with filters and channel targets.
|
Manage your RSS feeds with filters and channel targets.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="flex justify-between items-center py-3 px-4 border-b dark:border-neutral-700">
|
|
||||||
<h3 id="hs-scale-animation-modal-label" class="font-bold text-gray-800 dark:text-white">
|
|
||||||
Add Subscription
|
|
||||||
</h3>
|
|
||||||
<button type="button" class="size-8 inline-flex justify-center items-center gap-x-2 rounded-full border border-transparent bg-gray-100 text-gray-800 hover:bg-gray-200 focus:outline-none focus:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-400 dark:focus:bg-neutral-600" aria-label="Close" data-hs-overlay="#hs-scale-animation-modal">
|
|
||||||
<span class="sr-only">Close</span>
|
|
||||||
<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="M18 6 6 18"></path>
|
|
||||||
<path d="m6 6 12 12"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div> -->
|
|
||||||
<form id="subForm" novalidate class="group grid sm:grid-cols-2 gap-y-4 sm:gap-y-6 md:gap-y-8 gap-x-6 sm:gap-x-8 md:gap-x-10">
|
<form id="subForm" novalidate class="group grid sm:grid-cols-2 gap-y-4 sm:gap-y-6 md:gap-y-8 gap-x-6 sm:gap-x-8 md:gap-x-10">
|
||||||
<div>
|
<div>
|
||||||
<label for="formName" class="text-input-label">Name</label>
|
<label for="formName" class="text-input-label">Name</label>
|
||||||
|
25
src/db/migrations/20250213221612_create_filters.ts
Normal file
25
src/db/migrations/20250213221612_create_filters.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
const TABLE = "filters";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.createTable(TABLE, table => {
|
||||||
|
table.increments("id").primary();
|
||||||
|
table.string("guild_id").notNullable();
|
||||||
|
table.string("name").notNullable();
|
||||||
|
table.string("match").notNullable();
|
||||||
|
table.enum("algorithm", [
|
||||||
|
"any", "all", "exact",
|
||||||
|
"regex", "fuzzy"
|
||||||
|
]).notNullable();
|
||||||
|
table.boolean("is_insensitive").notNullable();
|
||||||
|
table.boolean("is_whitelist").notNullable();
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TABLE);
|
||||||
|
}
|
||||||
|
|
12
src/db/models/filters.model.ts
Normal file
12
src/db/models/filters.model.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
export interface Filter {
|
||||||
|
id: number;
|
||||||
|
guild_id: string;
|
||||||
|
name: string;
|
||||||
|
match: string;
|
||||||
|
algorithm: string;
|
||||||
|
is_insensitive: boolean;
|
||||||
|
is_whitelist: boolean;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
63
src/server/controllers/guild/filter.api.controller.ts
Normal file
63
src/server/controllers/guild/filter.api.controller.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { buildDatatableQuery } from "@utils/datatable";
|
||||||
|
import { db } from "@db/db";
|
||||||
|
import { Filter } from "@db/models/filters.model";
|
||||||
|
|
||||||
|
const isPostgres = db.client.config.client === "pg";
|
||||||
|
const TABLE = "filters";
|
||||||
|
|
||||||
|
export const datatable = async (request: Request, response: Response) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
let query = db(TABLE).where({ guild_id: request.params.guildId });
|
||||||
|
|
||||||
|
const datatableQuery = await buildDatatableQuery(request as any, query, TABLE);
|
||||||
|
const { recordsTotal, recordsFiltered } = datatableQuery;
|
||||||
|
const data = await datatableQuery.query;
|
||||||
|
|
||||||
|
console.debug(`total: ${recordsTotal} filtered: ${recordsFiltered} filtered+paged: ${data.length}`);
|
||||||
|
|
||||||
|
response.json({
|
||||||
|
data,
|
||||||
|
recordsFiltered,
|
||||||
|
recordsTotal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
response.status(500).json({ error: "Failed to fetch datatable for filters" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const post = async (request: Request, response: Response) => {
|
||||||
|
try {
|
||||||
|
console.debug(JSON.stringify(request.body, null, 4));
|
||||||
|
|
||||||
|
const guild_id = request.params.guildId;
|
||||||
|
const { name, match, algorithm, is_insensitive, is_whitelist } = request.body;
|
||||||
|
|
||||||
|
if (!name || !match || !algorithm) {
|
||||||
|
response.status(400).json({ error: "Missing required fields" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [filter] = await db<Filter>(TABLE)
|
||||||
|
.insert({
|
||||||
|
guild_id,
|
||||||
|
name,
|
||||||
|
match,
|
||||||
|
algorithm,
|
||||||
|
is_insensitive: is_insensitive == "on",
|
||||||
|
is_whitelist: is_whitelist == "on"
|
||||||
|
})
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
response.status(201).json(filter);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
response.status(500).json({ error: "Failed to create Filter" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { datatable, post }
|
@ -1,11 +1,18 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
|
||||||
import subApiController from "@server/controllers/guild/sub.api.controller";
|
import subApiController from "@server/controllers/guild/sub.api.controller";
|
||||||
|
import filterApiController from "@server/controllers/guild/filter.api.controller";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get("/:guildId/subscriptions/api/datatable", subApiController.datatable);
|
router.get("/:guildId/subscriptions/api/datatable", subApiController.datatable);
|
||||||
router.get("/:guildId/subscriptions/api", subApiController.get);
|
router.get("/:guildId/subscriptions/api", subApiController.get);
|
||||||
router.post("/:guildId/subscriptions/api", subApiController.post);
|
router.post("/:guildId/subscriptions/api", subApiController.post);
|
||||||
router.delete("/:guildId/subscriptions/api", subApiController.del);
|
router.delete("/:guildId/subscriptions/api", subApiController.del);
|
||||||
|
|
||||||
|
router.get("/:guildId/filters/api/datatable", filterApiController.datatable);
|
||||||
|
// router.get("/:guildId/filters/api", filterApiController.get);
|
||||||
|
router.post("/:guildId/filters/api", filterApiController.post);
|
||||||
|
// router.delete("/:guildId/filters/api", filterApiController.del);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
Loading…
x
Reference in New Issue
Block a user