From be03788cfcd6148ae03537425581aefecbc67734 Mon Sep 17 00:00:00 2001 From: Corban-Lee Jones Date: Mon, 5 May 2025 18:51:28 +0100 Subject: [PATCH] feat: added styles page table and edit modal --- src/client/src/ts/guild/styles.ts | 513 ++++++++++++++++++ src/client/views/guild/styles.ejs | 300 +++++++++- .../controllers/guild/style.controller.ts | 27 +- 3 files changed, 838 insertions(+), 2 deletions(-) create mode 100644 src/client/src/ts/guild/styles.ts diff --git a/src/client/src/ts/guild/styles.ts b/src/client/src/ts/guild/styles.ts new file mode 100644 index 0000000..10986e0 --- /dev/null +++ b/src/client/src/ts/guild/styles.ts @@ -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 = ` +
+
+ +
+

+ No results found +

+

+ Refine your search or create a new style. + Alternatively, use a template to deploy a ready-made style. +

+ +
+ + +
+
+`; + + +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 ` +
+ +
+ `} + }, + { + target: 1, + data: "name", + orderable: true, + searchable: true, + className: "size-px whitespace-nowrap", + render: (data: string, _type: string, row: prisma.Feed) => { return ` + + ${data} + + `} + }, + { + target: 2, + data: "colour", + orderable: true, + searchable: true, + className: "size-px whitespace-nowrap", + render: (data: string) => { return ` +
+ + ${data} + +
+ `} + }, + { + 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 = $("
").addClass("px-6 py-4"); + const badge = $("").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 = $("
").addClass("px-6 py-4"); + const badge = $("").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 = $("
").addClass("px-6 py-4"); + const badge = $("").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 = $("
").addClass("px-6 py-4"); + const badge = $("").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 = $("
").addClass("px-6 py-4"); + const badge = $("").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 = $("
").addClass("px-6 py-4"); + const badge = $("").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 = $("
").addClass("px-6 py-4"); + const badge = $("").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 ` +
+ + ${formatTimestamp(data)} + +
+ `} + }, +]; + +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: '', + optionTemplate: ` +
+ + +
`, + 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: '', + optionTemplate: ` +
+ + +
`, + 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 \ No newline at end of file diff --git a/src/client/views/guild/styles.ejs b/src/client/views/guild/styles.ejs index 379c9d7..4183829 100644 --- a/src/client/views/guild/styles.ejs +++ b/src/client/views/guild/styles.ejs @@ -2,4 +2,302 @@ <%- include("header") -%> -Styles page placeholder \ No newline at end of file +
+
+
+
+
+ + +
+ + + +
+ + +
+ +
+ + +
+ + + + + + + + + + + + + + + + +
+ + +
+ Name + + + +
+
+
+ Colour + + + +
+
+
+ Title Mutator + + + +
+
+
+ Description Mutator + + + +
+
+
+ Show Author + + + +
+
+
+ Show Image + + + +
+
+
+ Show Thumbnail + + + +
+
+
+ Show Footer + + + +
+
+
+ Show Timestamp + + + +
+
+
+ Created at + + + +
+
+
+ + +
+
+
+
+
+ + + + +<% block("scripts").append(''); %> \ No newline at end of file diff --git a/src/server/controllers/guild/style.controller.ts b/src/server/controllers/guild/style.controller.ts index f0758f1..f52069b 100644 --- a/src/server/controllers/guild/style.controller.ts +++ b/src/server/controllers/guild/style.controller.ts @@ -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 = { + 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 }); };