diff --git a/package.json b/package.json index 5713067..7ca1fd8 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "devDependencies": { "@tailwindcss/cli": "^4.1.4", "@tailwindcss/postcss": "^4.1.4", + "@types/dropzone": "^5.7.9", "@types/ejs": "^3.1.5", "@types/express": "^5.0.1", "@types/jquery": "^3.5.32", diff --git a/src/client/src/css/main.css b/src/client/src/css/main.css index edd65f7..9fde15c 100644 --- a/src/client/src/css/main.css +++ b/src/client/src/css/main.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "./preline"; +@import "../../../../node_modules/preline/src/plugins/datepicker/styles.css"; @config "../../../../tailwind.config.js"; @@ -370,4 +371,8 @@ dark:focus:outline-hidden dark:focus:ring-1 dark:focus:ring-neutral-600; -} \ No newline at end of file +} + +/* Vanilla Calendar z-index */ + +.vc { z-index: 80; } \ No newline at end of file diff --git a/src/client/src/ts/guild/feeds.ts b/src/client/src/ts/guild/feeds.ts index 25570a2..4fd66fb 100644 --- a/src/client/src/ts/guild/feeds.ts +++ b/src/client/src/ts/guild/feeds.ts @@ -9,6 +9,31 @@ import { formatTimestamp, verifyChannels } from "../../../src/ts/main"; import prisma from "../../../../../generated/prisma"; import HSDatepicker, { ICustomDatepickerOptions } from "@preline/datepicker"; + + +// import "preline"; +// import _ from "lodash"; +// import noUiSlider from "nouislider"; +// import "datatables.net"; +// import "dropzone/dist/dropzone-min.js"; +// import * as VanillaCalendarPro from "vanilla-calendar-pro"; + +// // Preline requirements +// window._ = _; +// window.$ = $; +// window.jQuery = $; +// window.DataTable = $.fn.dataTable; +// window.noUiSlider = noUiSlider; +// window.VanillaCalendarPro = VanillaCalendarPro; + +// window.HSStaticMethods.autoInit(); +// // document.addEventListener("DOMContentLoaded", () => { +// // if (window.HSStaticMethods && typeof window.HSStaticMethods.autoInit === "function") { +// // } else { +// // console.warn("Preline is not available on window.HSStaticMethods."); +// // } +// // }); + declare let guildId: string; declare let channels: Array; @@ -582,11 +607,6 @@ const publishedThresholdOptions: ICustomDatepickerOptions = { } }; -// const publishedThresholdInput = new HSDatepicker( -// $("#formPublishedThreshold").get(0), -// publishedThresholdOptions -// ); - $("#editForm").on("submit", async event => { event.preventDefault(); diff --git a/src/client/src/ts/guild/test.ts b/src/client/src/ts/guild/test.ts new file mode 100644 index 0000000..5e734eb --- /dev/null +++ b/src/client/src/ts/guild/test.ts @@ -0,0 +1,593 @@ +import { formatTimestamp, verifyChannels } from "../main"; +import HSDropdown from "preline/dist/dropdown"; +import HSDataTable, { IDataTableOptions } from "preline/dist/datatable"; +import HSSelect, { ISelectOptions } from "preline/dist/select"; +import HSOverlay, { IOverlayOptions } from "preline/dist/overlay"; +import HSDatepicker, { ICustomDatepickerOptions } from "preline/dist/datepicker"; +import { AjaxSettings, ConfigColumnDefs } from "datatables.net-dt"; +import prisma from "../../../../../generated/prisma"; + +declare let guildId: string; +declare let channels: Array; + +// #region DataTable + +const emptyTableHtml: string = ` +
+
+ +
+

+ No results found +

+

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

+ +
+ + +
+
+`; + +const columnDefs: ConfigColumnDefs[] = [ + // Select checkbox column + { + target: 0, + className: "size-px whitespace-nowrap", + render: (_data: unknown, _type: unknown, row: prisma.Feed) => { 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: "url", + orderable: true, + searchable: true, + className: "size-px whitespace-nowrap", + render: (data: string) => { return ` + + ${data} + + `} + }, + { + target: 3, + data: "channels", + className: "size-px", + render: (data: prisma.Channel[], type: string, row: prisma.Feed) => { + if (type !== "display") { return data; } + if (!data.length) { return ""; } + + const wrapper = $("
").addClass("flex flex-nowrap gap-1 px-6 py-4"); + const tag = $("").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"); + + if (!verifyChannels(data, channels)) { + wrapper.text("invalid channels").addClass("whitespace-nowrap"); + return wrapper.get(0); + } + + const firstChannelName = "# " + channels.find(c => c.id === data[0].channel_id).name; + wrapper.append(tag.clone().text(firstChannelName)); + + // No need to run the dropdown code if there's no more to show + if (data.length === 1) { + return wrapper.get(0); + } + + data.shift(); + + if (data.length <= 1) { + const secondChannelName = "# " + channels.find(c => c.id === data[0].channel_id).name; + wrapper.append(tag.clone().text(secondChannelName)); + return wrapper.get(0); + } + + const dropdown = $("
").addClass("hs-dropdown inline-block"); + const dropdownBtn = $("', + 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 +}; + +window.addEventListener("preline:ready", () => { + const selectEl = $("#selectPageSize-js").get(0); + if (!HSSelect.getInstance(selectEl, true)) { + new HSSelect(selectEl, pageSelectOptions); + } +}); + +// #endregion + +// #region Edit Modal + +const closeEditModal = () => { editModal.close() }; + +const openEditModal = async (id: number | undefined) => { + $("#editForm").removeClass("submitted"); + editModal.open(); + + id === undefined + ? clearEditModalData() + : loadEditModalData(id); +}; + +$(document).on("click", ".open-edit-modal-js", async event => { + await openEditModal($(event.target).data("id")); +}); + +const editModalOptions: IOverlayOptions = {}; + +let editModal: HSOverlay; + +window.addEventListener("preline:ready", () => { + const modalEl = $("#editModal").get(0); + if (!HSOverlay.getInstance(modalEl, true)) { + editModal = new HSOverlay(modalEl, editModalOptions); + } +}); + +// #endregion + +// #region Edit Form + +interface ExpandedFeed extends prisma.Feed { + channels: prisma.Channel[], + filters: prisma.Feed[] +} + +const clearEditModalData = () => { + $(editModal.el).removeData("id"); + + $("#formName").val(""); + $("#formUrl").val(""); + $("#formActive").prop("checked", true); + channelSelect.setValue([]); + filterSelect.setValue([]); + styleSelect.setValue(""); +}; + +const loadEditModalData = async (id: number) => { + const feed: ExpandedFeed = await $.ajax({ + url: `/guild/${guildId}/feeds/api?id=${id}`, + method: "get" + }); + + $(editModal.el).data("id", feed.id); + + $("#formName").val(feed.name); + $("#formUrl").val(feed.url); + $("#formActive").prop("checked", feed.active); + + channelSelect.setValue(feed.channels.map(channel => channel.channel_id)); + filterSelect.setValue(feed.filters.map(filter => `${filter.id}`)); + styleSelect.setValue(`${feed.message_style_id}`); +} + +$("#editForm").on("submit", async event => { + event.preventDefault(); + + const form = $(event.target).get(0) as HTMLFormElement; + $(form).addClass("submitted"); + + const validity = form.checkValidity(); + if (!validity) { + console.debug(`Submit form invalid: ${validity}`); + 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}/feeds/api`, + dataType: "json", + method: method, + data: data, + success: () => { + (table as any).dataTable.draw() + closeEditModal(); + }, + error: error => { + alert(JSON.stringify(error, null, 4)); + } + }); +}); + +const channelSelectOptions: ISelectOptions = { + placeholder: "Select option....", + mode: "tags", + + tagsItemTemplate: ` +
+
+
+
+ +
+
+ `, + optionTemplate: ` +
+
+
+
+
+
+
+ +
+
+ `, + tagsInputId: "formChannelsInput", + wrapperClasses: "cj-tag-select-wrapper", + dropdownClasses: "cj-tag-select-dropdown w-full", + tagsInputClasses: "cj-tag-select-input", + + dropdownScope: "window", + dropdownSpace: 10, + dropdownPlacement: "bottom", + dropdownVerticalFixedPlacement: null, + + hasSearch: false, + searchNoResultClasses: "cj-tag-select-search-no-results", +}; + +const filterSelectOptions: ISelectOptions = { + placeholder: "Select option....", + mode: "tags", + + tagsItemTemplate: ` +
+
+ +
+
+
+ +
+
+ `, + optionTemplate: ` +
+
+ +
+
+
+
+
+
+ +
+
+ `, + tagsInputId: "formFiltersInput", + wrapperClasses: "cj-tag-select-wrapper", + dropdownClasses: "cj-tag-select-dropdown w-full", + tagsInputClasses: "cj-tag-select-input", + + dropdownScope: "window", + dropdownSpace: 10, + dropdownPlacement: "bottom", + dropdownVerticalFixedPlacement: null, + + // API + apiUrl: `/guild/${guildId}/filters/api/select`, + apiQuery: "limit=15", + apiFieldsMap: { + id: "id", + val: "id", + title: "name", + description: "value", + name: "title" + }, + apiSearchQueryKey: "search", + hasSearch: false, + searchNoResultClasses: "cj-tag-select-search-no-results", + +}; + +const styleSelectOptions: ISelectOptions = { + placeholder: "Select option...", + + 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, + + apiUrl: `/guild/${guildId}/styles/api/select`, + // apiQuery: "limit=15", + apiFieldsMap: { + id: "id", + val: "id", + title: "name", + description: "value", + name: "title" + }, + apiSearchQueryKey: "search", + hasSearch: false, + optionAllowEmptyOption: true +}; + +let channelSelect: HSSelect; +let filterSelect: HSSelect; +let styleSelect: HSSelect; + +window.addEventListener("preline:ready", () => { + const exists = (element: HTMLElement) => HSSelect.getInstance(element, true); + + const channelEl = $("#formChannels").get(0); + const filterEl = $("#formFilters").get(0); + const styleEl = $("#formMessageStyle").get(0); + + if (!exists(channelEl) && !exists(filterEl) && !exists(styleEl)) { + channelSelect = new HSSelect(channelEl, channelSelectOptions); + filterSelect = new HSSelect(filterEl, filterSelectOptions); + styleSelect = new HSSelect(styleEl, styleSelectOptions); + } +}); + +const publishedThresholdOptions: ICustomDatepickerOptions = { + type: "default", + dateMax: "2050-00-00", + mode: "custom-select", + layouts: { + default: ` +
+
+
+ <#CustomArrowPrev /> +
+
+ <#CustomMonth /> + / + <#CustomYear /> +
+
+ <#CustomArrowNext /> +
+
+
+
+ <#Week /><#Dates /> +
+
+
+ <#CustomTime />` + }, + inputModeOptions: { itemsSeparator: " / " }, + templates: { + arrowPrev: '', + arrowNext: '' + }, + +}; + +let publishedThresholdInput: HSDatepicker; + +window.addEventListener("preline:ready", () => { + const publishedEl = $("#formPublishedThreshold").get(0); + if (!HSDatepicker.getInstance(publishedEl, true)) { + publishedThresholdInput = new HSDatepicker(publishedEl, publishedThresholdOptions); + } +}); + +// #endregion diff --git a/src/client/src/ts/main.ts b/src/client/src/ts/main.ts index 9630fc0..567de1f 100644 --- a/src/client/src/ts/main.ts +++ b/src/client/src/ts/main.ts @@ -1,22 +1,31 @@ -import { Channel } from "discord.js"; -import prisma from "../../../../generated/prisma"; - +import "preline"; import $ from "jquery"; import _ from "lodash"; import noUiSlider from "nouislider"; import "datatables.net"; import "dropzone/dist/dropzone-min.js"; import * as VanillaCalendarPro from "vanilla-calendar-pro"; +import * as FloatingUIDOM from "@floating-ui/dom"; -import "../types/client.d.ts"; +import { Channel } from "discord.js"; +import prisma from "../../../../generated/prisma"; -// Preline requirements +// Preline: requirements window._ = _; window.$ = $; window.jQuery = $; window.DataTable = $.fn.dataTable; window.noUiSlider = noUiSlider; window.VanillaCalendarPro = VanillaCalendarPro; +window.FloatingUIDOM = FloatingUIDOM + +document.addEventListener("DOMContentLoaded", () => { + window.HSStaticMethods.autoInit("all"); + + setTimeout(() => { + window.dispatchEvent(new Event("preline:ready")); + }, 100); +}); // Preline: necessary for header events. window.addEventListener("load", () => { diff --git a/src/client/src/types/client.d.ts b/src/client/src/types/client.d.ts index 3325a84..399ba4a 100644 --- a/src/client/src/types/client.d.ts +++ b/src/client/src/types/client.d.ts @@ -1,15 +1,23 @@ +import type noUiSlider from "nouislider"; import type { IStaticMethods } from "preline/dist"; -interface Window { - // Optional third-party libraries - _: any; - $: typeof import("jquery"); - jQuery: typeof import("jquery"); - DataTable: any; - Dropzone: any; - VanillaCalendarPro: any; - noUiSlider: any; +declare global { + interface Window { + // Optional third-party libraries + _: typeof import("lodash"); + $: typeof import("jquery"); + jQuery: typeof import("jquery"); + DataTable: typeof $.fn.dataTable; + Dropzone: typeof import("dropzone"); + noUiSlider: typeof noUiSlider; + VanillaCalendarPro: typeof import("vanilla-calendar-pro"); + + // Preline UI + HSStaticMethods: IStaticMethods; - // Preline UI - HSStaticMethods: IStaticMethods; -} \ No newline at end of file + // Floating UI + FloatingUIDOM: typeof import("@floating-ui/dom"); + } +} + +export {}; \ No newline at end of file diff --git a/src/client/views/guild/feeds.ejs b/src/client/views/guild/feeds.ejs index dfdc0b4..b7ee820 100644 --- a/src/client/views/guild/feeds.ejs +++ b/src/client/views/guild/feeds.ejs @@ -141,7 +141,7 @@
-