feat: added styles page table and edit modal
Some checks failed
Build / build (push) Failing after 38s
Some checks failed
Build / build (push) Failing after 38s
This commit is contained in:
parent
2d8a26f392
commit
be03788cfc
513
src/client/src/ts/guild/styles.ts
Normal file
513
src/client/src/ts/guild/styles.ts
Normal file
@ -0,0 +1,513 @@
|
||||
import $ from "jquery";
|
||||
import "datatables.net-select-dt";
|
||||
import HSDropdown from "@preline/dropdown";
|
||||
import HSOverlay, { IOverlayOptions } from "@preline/overlay";
|
||||
import HSSelect, { ISelectOptions, ISingleOption } from "@preline/select";
|
||||
import HSDataTable, { IDataTableOptions } from "@preline/datatable";
|
||||
import DataTable, { Api, ConfigColumnDefs, AjaxSettings } from "datatables.net-dt";
|
||||
import { autoUpdate, computePosition, offset } from "@floating-ui/dom";
|
||||
import { formatTimestamp } from "../../../src/ts/main";
|
||||
import prisma from "../../../../../generated/prisma";
|
||||
|
||||
declare let guildId: string;
|
||||
declare const textMutators: { [key: string]: string };
|
||||
|
||||
// #region DataTable
|
||||
//
|
||||
|
||||
// Fix dependency bugs with preline
|
||||
(window as any).DataTable = DataTable;
|
||||
(window as any).$hsDataTableCollection = [];
|
||||
|
||||
const emptyTableHtml: string = `
|
||||
<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">
|
||||
Refine your search or create a new style.
|
||||
Alternatively, use a template to deploy a ready-made style.
|
||||
</p>
|
||||
|
||||
<div class="mt-5 flex flex-col sm:flex-row gap-2">
|
||||
<button type="button" class="open-edit-modal-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-hidden 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>
|
||||
Create a style
|
||||
</button>
|
||||
<button type="button" onclick="alert('not implemented');" class="py-2 px-3 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden 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>
|
||||
`;
|
||||
|
||||
|
||||
const columnDefs: ConfigColumnDefs[] = [
|
||||
// Select checkbox column
|
||||
{
|
||||
target: 0,
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
className: "size-px whitespace-nowrap",
|
||||
render: (_data: unknown, _type: unknown, row: prisma.MessageStyle) => { return `
|
||||
<div class="ps-6 py-4">
|
||||
<label class="rowSelect${row.id}-js" class="flex">
|
||||
<input type="checkbox" id="rowSelect${row.id}-js" class="cj-table-checkbox" data-hs-datatable-row-selecting-individual="">
|
||||
<span class="sr-only">Select Row</span>
|
||||
</label>
|
||||
</div>
|
||||
`}
|
||||
},
|
||||
{
|
||||
target: 1,
|
||||
data: "name",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
className: "size-px whitespace-nowrap",
|
||||
render: (data: string, _type: string, row: prisma.Feed) => { return `
|
||||
<span class="cj-table-link open-edit-modal-js max-w-[250px] truncate" data-id="${row.id}">
|
||||
${data}
|
||||
</span>
|
||||
`}
|
||||
},
|
||||
{
|
||||
target: 2,
|
||||
data: "colour",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
className: "size-px whitespace-nowrap",
|
||||
render: (data: string) => { return `
|
||||
<div class="px-6 py-4">
|
||||
<span class="cj-table-text">
|
||||
${data}
|
||||
</span>
|
||||
</div>
|
||||
`}
|
||||
},
|
||||
{
|
||||
target: 3,
|
||||
data: "title_mutator",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
className: "size-px whitespace-nowrap",
|
||||
render: (data: string, type: string) => {
|
||||
if (type !== "display") return data;
|
||||
|
||||
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||
const badge = $("<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");
|
||||
|
||||
badge.text(data);
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
target: 4,
|
||||
data: "description_mutator",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
className: "size-px whitespace-nowrap",
|
||||
render: (data: string, type: string) => {
|
||||
if (type !== "display") return data;
|
||||
|
||||
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||
const badge = $("<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");
|
||||
|
||||
badge.text(data);
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
target: 5,
|
||||
data: "show_author",
|
||||
orderable: true,
|
||||
searchable: false,
|
||||
className: "size-px whitespace-nowrap",
|
||||
render: (data: boolean) => {
|
||||
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||
const badge = $("<span>").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 text-xs rounded-md");
|
||||
|
||||
if (data) {
|
||||
badge.text("Show");
|
||||
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||
} else {
|
||||
badge.text("Hide");
|
||||
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
|
||||
}
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
target: 6,
|
||||
data: "show_image",
|
||||
orderable: true,
|
||||
searchable: false,
|
||||
className: "size-px whitespace-nowrap",
|
||||
render: (data: boolean) => {
|
||||
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||
const badge = $("<span>").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 text-xs rounded-md");
|
||||
|
||||
if (data) {
|
||||
badge.text("Show");
|
||||
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||
} else {
|
||||
badge.text("Hide");
|
||||
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
|
||||
}
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
target: 7,
|
||||
data: "show_thumbnail",
|
||||
orderable: true,
|
||||
searchable: false,
|
||||
className: "size-px whitespace-nowrap",
|
||||
render: (data: boolean) => {
|
||||
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||
const badge = $("<span>").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 text-xs rounded-md");
|
||||
|
||||
if (data) {
|
||||
badge.text("Show");
|
||||
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||
} else {
|
||||
badge.text("Hide");
|
||||
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
|
||||
}
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
target: 8,
|
||||
data: "show_footer",
|
||||
orderable: true,
|
||||
searchable: false,
|
||||
className: "size-px whitespace-nowrap",
|
||||
render: (data: boolean) => {
|
||||
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||
const badge = $("<span>").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 text-xs rounded-md");
|
||||
|
||||
if (data) {
|
||||
badge.text("Show");
|
||||
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||
} else {
|
||||
badge.text("Hide");
|
||||
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
|
||||
}
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
target: 9,
|
||||
data: "show_timestamp",
|
||||
orderable: true,
|
||||
searchable: false,
|
||||
className: "size-px whitespace-nowrap",
|
||||
render: (data: boolean) => {
|
||||
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||
const badge = $("<span>").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 text-xs rounded-md");
|
||||
|
||||
if (data) {
|
||||
badge.text("Show");
|
||||
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||
} else {
|
||||
badge.text("Hide");
|
||||
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
|
||||
}
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
target: 10,
|
||||
data: "created_at",
|
||||
orderable: true,
|
||||
searchable: false,
|
||||
className: "size-px whitespace-nowrap",
|
||||
render: (data: string) => { return `
|
||||
<div class="px-6 py-4">
|
||||
<span class="cj-table-text">
|
||||
${formatTimestamp(data)}
|
||||
</span>
|
||||
</div>
|
||||
`}
|
||||
},
|
||||
];
|
||||
|
||||
const ajaxSettings: AjaxSettings = {
|
||||
url: `/guild/${guildId}/styles/api/datatable`,
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
dataSrc: "data",
|
||||
data: (data: unknown) => {
|
||||
if (data === undefined) return;
|
||||
// TODO,
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
};
|
||||
|
||||
const tableOptions: IDataTableOptions = {
|
||||
ajax: ajaxSettings,
|
||||
serverSide: true,
|
||||
processing: true,
|
||||
select: {
|
||||
style: "multi",
|
||||
selector: "td:first-child input[type='checkbox']"
|
||||
},
|
||||
columnDefs: columnDefs,
|
||||
pagingOptions: { pageBtnClasses: "hidden" },
|
||||
rowSelectingOptions: { selectAllSelector: "#selectAllBox" },
|
||||
language: {
|
||||
zeroRecords: emptyTableHtml,
|
||||
emptyTable: emptyTableHtml,
|
||||
loadingRecords: "Placeholder loading message..."
|
||||
},
|
||||
drawCallback: () => HSDropdown.autoInit(),
|
||||
rowCallback: (row: HTMLTableRowElement) => {
|
||||
$(row).addClass("bg-white dark:bg-neutral-900");
|
||||
}
|
||||
};
|
||||
|
||||
const table: HSDataTable = new HSDataTable(
|
||||
$("#table").get(0) as HTMLElement,
|
||||
tableOptions
|
||||
);
|
||||
|
||||
const onTableSelectChange = () => {
|
||||
const selectedRowsCount = (table as any).dataTable.rows({ selected: true }).count();
|
||||
$("#deleteRowsBtn").prop("disabled", selectedRowsCount === 0);
|
||||
$(".rows-selected-count-js").text(selectedRowsCount);
|
||||
|
||||
const $elem = $(".rows-selected-count-js.zero-empty-js");
|
||||
selectedRowsCount === 0 ? $elem.hide() : $elem.show();
|
||||
};
|
||||
|
||||
(table as any).dataTable
|
||||
.on("select", onTableSelectChange)
|
||||
.on("deselect", onTableSelectChange)
|
||||
.on("draw", onTableSelectChange);
|
||||
|
||||
$("#selectAllBox").on("change", function() {
|
||||
(this as HTMLInputElement).checked
|
||||
? (table as any).dataTable.rows().select()
|
||||
: (table as any).dataTable.rows().deselect();
|
||||
});
|
||||
|
||||
$("#deleteRowsBtn").on("click", async () => {
|
||||
const dt: Api = (table as any).dataTable;
|
||||
const rowsData = dt.rows({ selected: true }).data().toArray();
|
||||
const rowIds = rowsData.map((row: prisma.MessageStyle) => row.id);
|
||||
|
||||
await $.ajax({
|
||||
url: `/guild/${guildId}/styles/api`,
|
||||
method: "delete",
|
||||
dataType: "json",
|
||||
data: { ids: rowIds },
|
||||
success: () => {
|
||||
dt.draw();
|
||||
dt.rows().deselect();
|
||||
},
|
||||
error: error => {
|
||||
alert(typeof error === "object" ? JSON.stringify(error, null, 4) : error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
|
||||
// #region Page Size Select
|
||||
// https://preline.co/plugins/html/advanced-select.html
|
||||
|
||||
(window as any).$hsSelectCollection = [];
|
||||
(window as any)["FloatingUIDOM"] = {
|
||||
computePosition: computePosition,
|
||||
autoUpdate: autoUpdate,
|
||||
offset: offset
|
||||
};
|
||||
|
||||
// Close on click.
|
||||
window.addEventListener('click', (evt) => {
|
||||
const evtTarget = evt.target;
|
||||
|
||||
HSSelect.closeCurrentlyOpened(evtTarget as HTMLElement);
|
||||
});
|
||||
|
||||
const pageSelectOptions: ISelectOptions = {
|
||||
toggleTag: '<button type="button" aria-expanded="false"></button>',
|
||||
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>`,
|
||||
toggleClasses: "cj-table-paging-select-toggle",
|
||||
optionClasses: "cj-table-paging-select-option",
|
||||
dropdownClasses: "cj-table-paging-select-dropdown",
|
||||
dropdownSpace: 10,
|
||||
dropdownScope: "parent",
|
||||
dropdownPlacement: "top",
|
||||
dropdownVerticalFixedPlacement: null
|
||||
};
|
||||
|
||||
const pageSizeSelect: HSSelect = new HSSelect(
|
||||
$("#selectPageSize-js").get(0) as HTMLElement,
|
||||
pageSelectOptions
|
||||
);
|
||||
|
||||
// #endregion
|
||||
|
||||
|
||||
// #region Edit Modal
|
||||
|
||||
(window as any).$hsOverlayCollection = [];
|
||||
|
||||
const editModalOptions: IOverlayOptions = {};
|
||||
|
||||
const editModal: HSOverlay = new HSOverlay(
|
||||
$("#editModal").get(0) as HTMLElement,
|
||||
editModalOptions
|
||||
);
|
||||
|
||||
$(document).on("click", ".open-edit-modal-js", async event => {
|
||||
await openEditModal($(event.target).data("id"));
|
||||
});
|
||||
|
||||
const clearEditModalData = () => {
|
||||
$(editModal.el).removeData("id");
|
||||
|
||||
// TODO: clear values
|
||||
$("#formName").val("");
|
||||
$("#formColour").val("");
|
||||
|
||||
titleMutatorSelect.setValue("");
|
||||
descriptionMutatorSelect.setValue("");
|
||||
|
||||
$("#formShowAuthor").prop("checked", true);
|
||||
$("#formShowImage").prop("checked", true);
|
||||
$("#formShowThumbnail").prop("checked", true);
|
||||
$("#formShowFooter").prop("checked", true);
|
||||
$("#formShowTimestamp").prop("checked", true);
|
||||
};
|
||||
|
||||
const loadEditModalData = async (id: number) => {
|
||||
const style: prisma.MessageStyle = await $.ajax({
|
||||
url: `/guild/${guildId}/styles/api?id=${id}`,
|
||||
method: "get"
|
||||
});
|
||||
|
||||
$(editModal.el).data("id", style.id);
|
||||
|
||||
// TODO: set values
|
||||
}
|
||||
|
||||
const openEditModal = async (id: number | undefined) => {
|
||||
$("#editForm").removeClass("submitted");
|
||||
editModal.open();
|
||||
|
||||
id === undefined
|
||||
? clearEditModalData()
|
||||
: loadEditModalData(id);
|
||||
};
|
||||
|
||||
const closeEditModal = () => {
|
||||
editModal.close();
|
||||
};
|
||||
|
||||
const mutatorSelectOptions: ISelectOptions = {
|
||||
toggleTag: '<button type="button" aria-expanded="false"><span data-title></span></button>',
|
||||
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>`,
|
||||
toggleClasses: "cj-select-toggle select-input",
|
||||
optionClasses: "cj-select-option",
|
||||
dropdownClasses: "cj-select-dropdown",
|
||||
wrapperClasses: "peer",
|
||||
dropdownSpace: 10,
|
||||
dropdownScope: "parent",
|
||||
dropdownPlacement: "top",
|
||||
dropdownVerticalFixedPlacement: null
|
||||
};
|
||||
|
||||
const titleMutatorSelect = new HSSelect(
|
||||
$("#formTitleMutator").get(0),
|
||||
mutatorSelectOptions
|
||||
);
|
||||
|
||||
// Add options to title mutator select
|
||||
titleMutatorSelect.addOption({ title: "None", val: "" });
|
||||
Object.entries(textMutators).forEach(([key, description]) => {
|
||||
titleMutatorSelect.addOption({
|
||||
title: description,
|
||||
val: key
|
||||
} as ISingleOption)
|
||||
})
|
||||
|
||||
const descriptionMutatorSelect = new HSSelect(
|
||||
$("#formDescriptionMutator").get(0),
|
||||
mutatorSelectOptions
|
||||
);
|
||||
|
||||
// Add options to description mutator select
|
||||
descriptionMutatorSelect.addOption({ title: "None", val: "" });
|
||||
Object.entries(textMutators).forEach(([key, description]) => {
|
||||
descriptionMutatorSelect.addOption({
|
||||
title: description,
|
||||
val: key
|
||||
} as ISingleOption)
|
||||
})
|
||||
|
||||
$("#editForm").on("submit", async event => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = $(event.target).get(0) as HTMLFormElement;
|
||||
$(form).addClass("submitted");
|
||||
|
||||
if (!form.checkValidity()) return;
|
||||
|
||||
let method = "post";
|
||||
const data = $(event.target).serializeArray();
|
||||
|
||||
// If 'id' has a value, we are patching an existing entry
|
||||
const id: number | undefined = $(editModal.el).data("id");
|
||||
if (id !== undefined) {
|
||||
data.push({ name: "id", value: `${id}` });
|
||||
method = "patch";
|
||||
}
|
||||
|
||||
await $.ajax({
|
||||
url: `/guild/${guildId}/styles/api`,
|
||||
dataType: "json",
|
||||
method: method,
|
||||
data: data,
|
||||
success: () => {
|
||||
(table as any).dataTable.draw()
|
||||
closeEditModal();
|
||||
},
|
||||
error: error => {
|
||||
alert(JSON.stringify(error, null, 4));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// #endregion
|
@ -2,4 +2,302 @@
|
||||
|
||||
<%- include("header") -%>
|
||||
|
||||
Styles page placeholder
|
||||
<div id="table" class="--prevent-on-load-init max-w-full px-4 sm:px-6">
|
||||
<div class="flex flex-col">
|
||||
<div class="-m-1.5">
|
||||
<div class="max-w-full min-w-full p-1.5 inline-block align-middle">
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-xs dark:bg-neutral-900 dark:border-neutral-700">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 gap-3 flex flex-nowrap justify-between items-center">
|
||||
|
||||
<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" 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>
|
||||
|
||||
<div class="sm:col-span-2">
|
||||
<button type="button" id="deleteRowsBtn" disabled class="py-2 px-3 inline--lex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-red-500 shadow-xs hover:bg-gray-50 focus:outline-hidden 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>
|
||||
<button type="button" class="open-edit-modal-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-hidden 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>
|
||||
Create
|
||||
<span class="hidden sm:inline">a style</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="min-w-full overflow-x-auto">
|
||||
<table class="cj-table">
|
||||
<thead class="cj-thead">
|
||||
<tr>
|
||||
<th scope="col" class="cj-table-header --exclude-from-ordering">
|
||||
<label for="selectAllBox" class="flex">
|
||||
<input type="checkbox" id="selectAllBox" class="cj-table-checkbox">
|
||||
<span class="sr-only">Checkbox</span>
|
||||
</label>
|
||||
</th>
|
||||
<th scope="col" data-dt-column="name" class="cj-table-header">
|
||||
<div class="cj-table-header-content cursor-pointer">
|
||||
<span>Name</span>
|
||||
<svg 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="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" data-dt-column="colour" class="cj-table-header">
|
||||
<div class="cj-table-header-content cursor-pointer">
|
||||
<span>Colour</span>
|
||||
<svg 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="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" data-dt-column="title_mutator" class="cj-table-header">
|
||||
<div class="cj-table-header-content cursor-pointer">
|
||||
<span>Title Mutator</span>
|
||||
<svg 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="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" data-dt-column="description_mutator" class="cj-table-header">
|
||||
<div class="cj-table-header-content cursor-pointer">
|
||||
<span>Description Mutator</span>
|
||||
<svg 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="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" data-dt-column="show_author" class="cj-table-header">
|
||||
<div class="cj-table-header-content cursor-pointer">
|
||||
<span>Show Author</span>
|
||||
<svg 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="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" data-dt-column="show_image" class="cj-table-header">
|
||||
<div class="cj-table-header-content cursor-pointer">
|
||||
<span>Show Image</span>
|
||||
<svg 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="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" data-dt-column="show_thumbnail" class="cj-table-header">
|
||||
<div class="cj-table-header-content cursor-pointer">
|
||||
<span>Show Thumbnail</span>
|
||||
<svg 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="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" data-dt-column="show_footer" class="cj-table-header">
|
||||
<div class="cj-table-header-content cursor-pointer">
|
||||
<span>Show Footer</span>
|
||||
<svg 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="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" data-dt-column="show_timestamp" class="cj-table-header">
|
||||
<div class="cj-table-header-content cursor-pointer">
|
||||
<span>Show Timestamp</span>
|
||||
<svg 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="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" data-dt-column="created_at" class="cj-table-header">
|
||||
<div class="cj-table-header-content cursor-pointer">
|
||||
<span>Created at</span>
|
||||
<svg 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="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="cj-table-footer">
|
||||
<div class="max-w-sm space-y-3">
|
||||
<select id="selectPageSize-js" data-hs-datatable-page-entities="">
|
||||
<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>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm:inline-flex items-center gap-x-2" data-hs-datatable-info="">
|
||||
<p class="text-sm text-gray dark:text-neutral-400">
|
||||
<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="cj-table-paging-btn" 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="cj-table-paging-btn" 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editModal" 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">
|
||||
<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-xs 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">Message Style</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
||||
message style placeholder description.
|
||||
</p>
|
||||
</div>
|
||||
<form id="editForm" 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>
|
||||
<label for="formName" class="text-input-label">Name</label>
|
||||
<input type="text" id="formName" name="name" class="form-input text-input peer invalid:group-[.submitted]:border-red-500 invalid:group-[.submitted]:ring-red-500" required>
|
||||
<p class="text-input-help block peer-invalid:group-[.submitted]:hidden">
|
||||
Human-readable name for this entry.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-red-500 hidden peer-invalid:group-[.submitted]:block">
|
||||
Please enter a name.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="formColour" class="text-input-label">Colour</label>
|
||||
<input type="text" id="formColour" name="colour" class="form-input text-input peer invalid:group-[.submitted]:border-red-500 invalid:group-[.submitted]:ring-red-500" required>
|
||||
<p class="text-input-help block peer-invalid:group-[.submitted]:hidden">
|
||||
hex colour placeholder
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-red-500 hidden peer-invalid:group-[.submitted]:block">
|
||||
Please enter a colour.
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label for="formTitleMutator" class="text-input-label">Title Mutator</label>
|
||||
<select name="title_mutator" id="formTitleMutator" class="peer --prevent-on-load-init">
|
||||
<option value="">Choose</option>
|
||||
</select>
|
||||
<p class="text-input-help block peer-has-invalid:group-[.submitted]:hidden">
|
||||
title mutator placeholder.
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label for="formDescriptionMutator" class="text-input-label">Description Mutator</label>
|
||||
<select name="description_mutator" id="formDescriptionMutator" class="peer --prevent-on-load-init">
|
||||
<option value="">Choose</option>
|
||||
</select>
|
||||
<p class="text-input-help block peer-has-invalid:group-[.submitted]:hidden">
|
||||
title mutator placeholder.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="formShowAuthor" class="flex gap-4">
|
||||
<input type="checkbox" id="formShowAuthor" name="show_author" 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-sm 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">Show author</span>
|
||||
<span class="block text-sm text-gray-500 dark:text-neutral-500">
|
||||
Show author placeholder
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="formShowImage" class="flex gap-4">
|
||||
<input type="checkbox" id="formShowImage" name="show_image" 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-sm 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">Show image</span>
|
||||
<span class="block text-sm text-gray-500 dark:text-neutral-500">
|
||||
Show image placeholder
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="formShowThumbnail" class="flex gap-4">
|
||||
<input type="checkbox" id="formShowThumbnail" name="show_thumbnail" 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-sm 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">Show thumbnail</span>
|
||||
<span class="block text-sm text-gray-500 dark:text-neutral-500">
|
||||
Show thumbnail placeholder
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="formShowFooter" class="flex gap-4">
|
||||
<input type="checkbox" id="formShowFooter" name="show_footer" 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-sm 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">Show footer</span>
|
||||
<span class="block text-sm text-gray-500 dark:text-neutral-500">
|
||||
Show footer placeholder
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="formShowTimestamp" class="flex gap-4">
|
||||
<input type="checkbox" id="formShowTimestamp" name="show_timestamp" 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-sm 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">Show timestamp</span>
|
||||
<span class="block text-sm text-gray-500 dark:text-neutral-500">
|
||||
Show timestamp placeholder
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</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-xs hover:bg-gray-50 focus:outline-hidden 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-xs hover:bg-gray-50 focus:outline-hidden 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="#editModal">
|
||||
Close
|
||||
</button>
|
||||
<button type="submit" form="editForm" 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-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var guildId = "<%- guild.id %>";
|
||||
var textMutators = JSON.parse(`<%- JSON.stringify( textMutators ) %> `);
|
||||
</script>
|
||||
<% block("scripts").append('<script src="/public/generated/js/guild/styles.js"></script>'); %>
|
@ -1,5 +1,6 @@
|
||||
import { Request, Response } from "express";
|
||||
import { client as bot } from "@bot/bot";
|
||||
import prisma from "generated/prisma";
|
||||
|
||||
export const get = async (request: Request, response: Response) => {
|
||||
const guildId = request.params.guildId;
|
||||
@ -10,9 +11,33 @@ export const get = async (request: Request, response: Response) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const textMutators: Record<keyof typeof prisma.TextMutator, string> = {
|
||||
UWUIFY: "UWUify",
|
||||
UWUIFY_SFW: "UWUify (Safe)",
|
||||
GOTHIC_SCRIPT: "Gothic Script",
|
||||
EMOJI_SUBSTITUTE: "Emoji Substitute",
|
||||
ZALGO: "Zalgo",
|
||||
MORSE_CODE: "Morse Code",
|
||||
BINARY: "Binary",
|
||||
HEXADECIMAL: "Hexadecimal",
|
||||
REMOVE_VOWELS: "Remove Vowels",
|
||||
DOUBLE_CHARACTERS: "Double Characters",
|
||||
SMALL_CASE: "Small Case",
|
||||
LEET_SPEAK: "L33t Sp34k",
|
||||
PIG_LATIN: "Pig Latin",
|
||||
UPSIDE_DOWN: "Upside Down",
|
||||
ALL_REVERSED: "All Reversed",
|
||||
REVERSED_WORDS: "Reversed Words",
|
||||
SHUFFLE_WORDS: "Shuffle Words",
|
||||
RANDOM_CASE: "Random Case",
|
||||
GIBBERISH: "Gibberish",
|
||||
SHAKESPEAREAN: "Shakespearean"
|
||||
};
|
||||
|
||||
response.render("guild/styles", {
|
||||
title: `${guild.name} - Relay`,
|
||||
guild: guild
|
||||
guild: guild,
|
||||
textMutators: textMutators
|
||||
});
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user