diff --git a/code/modules/admin/force_event.dm b/code/modules/admin/force_event.dm index b4a099e4ff5..f8d8ceccf0a 100644 --- a/code/modules/admin/force_event.dm +++ b/code/modules/admin/force_event.dm @@ -29,19 +29,46 @@ return GLOB.fun_state /datum/force_event/ui_static_data(mob/user) + var/static/list/category_to_icons + if(!category_to_icons) + category_to_icons = list( + EVENT_CATEGORY_AI = "robot", + EVENT_CATEGORY_ANOMALIES = "cloud-bolt", + EVENT_CATEGORY_BUREAUCRATIC = "print", + EVENT_CATEGORY_ENGINEERING = "wrench", + EVENT_CATEGORY_ENTITIES = "ghost", + EVENT_CATEGORY_FRIENDLY = "face-smile", + EVENT_CATEGORY_HEALTH = "brain", + EVENT_CATEGORY_HOLIDAY = "calendar", + EVENT_CATEGORY_INVASION = "user-group", + EVENT_CATEGORY_JANITORIAL = "bath", + EVENT_CATEGORY_SPACE = "meteor", + EVENT_CATEGORY_WIZARD = "hat-wizard", + ) var/list/data = list() - data["categories"] = list() - for(var/datum/round_event_control/event_control in SSevents.control) - if(!data["categories"][event_control.category]) - data["categories"][event_control.category] = list( + + var/list/categories_seen = list() + var/list/categories = list() + + var/list/events = list() + + for(var/datum/round_event_control/event_control as anything in SSevents.control) + //add category + if(!categories_seen[event_control.category]) + categories_seen[event_control.category] = TRUE + UNTYPED_LIST_ADD(categories, list( "name" = event_control.category, - "events" = list() - ) - data["categories"][event_control.category]["events"] += list(list( + "icon" = category_to_icons[event_control.category], + )) + //add event, with one value matching up the category + UNTYPED_LIST_ADD(events, list( "name" = event_control.name, "description" = event_control.description, - "type" = event_control.type + "type" = event_control.type, + "category" = event_control.category, )) + data["categories"] = categories + data["events"] = events return data /datum/force_event/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) diff --git a/tgui/packages/common/collections.ts b/tgui/packages/common/collections.ts index 3902feec5cd..baf02242372 100644 --- a/tgui/packages/common/collections.ts +++ b/tgui/packages/common/collections.ts @@ -301,6 +301,27 @@ export const binaryInsertWith = return copy; }; +/** + * This method takes a collection of items and a number, returning a collection + * of collections, where the maximum amount of items in each is that second arg + */ +export const paginate = (collection: T[], maxPerPage: number): T[][] => { + const pages: T[][] = []; + let page: T[] = []; + let itemsToAdd = maxPerPage; + + for (const item of collection) { + page.push(item); + itemsToAdd--; + if (!itemsToAdd) { + itemsToAdd = maxPerPage; + pages.push(page); + page = []; + } + } + return pages; +}; + const isObject = (obj: unknown) => typeof obj === 'object' && obj !== null; // Does a deep merge of two objects. DO NOT FEED CIRCULAR OBJECTS!! diff --git a/tgui/packages/tgui/interfaces/ForceEvent.js b/tgui/packages/tgui/interfaces/ForceEvent.js deleted file mode 100644 index 36da2962042..00000000000 --- a/tgui/packages/tgui/interfaces/ForceEvent.js +++ /dev/null @@ -1,116 +0,0 @@ -import { useBackend, useLocalState } from '../backend'; -import { Stack, Button, Icon, Input, Section, Table } from '../components'; -import { Window } from '../layouts'; -import { flow } from 'common/fp'; -import { filter, sortBy } from 'common/collections'; - -export const ForceEvent = (props, context) => { - return ( - - - - - - - - ); -}; - -export const EventSearch = (props, context) => { - const [searchQuery, setSearchQuery] = useLocalState( - context, - 'searchQuery', - '' - ); - - return ( -
- - - - - - setSearchQuery(e.target.value)} - placeholder="Search..." - value={searchQuery} - /> - - -
- ); -}; - -export const EventOptionsPanel = (props, context) => { - const { data } = useBackend(context); - - const [announce, setAnnounce] = useLocalState(context, 'announce', true); - - return ( - setAnnounce(!announce)}> - Announce event to the crew - - ); -}; - -export const EventContent = (props, context) => { - const { data } = useBackend(context); - - const categories = Object.values(data.categories); - const sortCategories = sortBy((category) => category.name); - - return sortCategories(categories).map((category) => ( - - )); -}; - -export const EventList = (props, context) => { - const { act } = useBackend(context); - const { category } = props; - const [searchQuery] = useLocalState(context, 'searchQuery', ''); - const [announce] = useLocalState(context, 'announce', true); - - const filtered_events = flow([ - filter((event) => - event.name?.toLowerCase().includes(searchQuery.toLowerCase()) - ), - sortBy((event) => event.name), - ])(category.events || []); - - if (!filtered_events.length) { - return null; - } - - return ( -
- - {filtered_events.map((event) => { - return ( - - {event.name} - -
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/ForceEvent.tsx b/tgui/packages/tgui/interfaces/ForceEvent.tsx new file mode 100644 index 00000000000..8dfe62912cc --- /dev/null +++ b/tgui/packages/tgui/interfaces/ForceEvent.tsx @@ -0,0 +1,200 @@ +import { paginate } from 'common/collections'; +import { useBackend, useLocalState } from '../backend'; +import { Stack, Button, Icon, Input, Section, Tabs } from '../components'; +import { Window } from '../layouts'; + +const CATEGORY_PAGE_ITEMS = 4; +const EVENT_PAGE_ITEMS = 2; +const EVENT_PAGE_MAXCHARS = 48; + +/** + * Same as paginate, but respecting event names with a character max length + * that will also create a new page if created + */ +const paginateEvents = (events: Event[], maxPerPage: number): Event[][] => { + const pages: Event[][] = []; + let page: Event[] = []; + // conditions that make a new page + let itemsToAdd = maxPerPage; + let maxChars = EVENT_PAGE_MAXCHARS; + + for (const event of events) { + maxChars -= event.name.length; + if (maxChars <= 0) { + // would overflow the next line over + itemsToAdd = maxPerPage; + maxChars = EVENT_PAGE_MAXCHARS - event.name.length; + pages.push(page); + page = []; + } + page.push(event); + itemsToAdd--; + if (!itemsToAdd) { + // max amount of items we allow + itemsToAdd = maxPerPage; + maxChars = EVENT_PAGE_MAXCHARS; + pages.push(page); + page = []; + } + } + if (page.length) { + pages.push(page); + } + return pages; +}; + +type Event = { + name: string; + description: string; + type: string; + category: string; +}; + +type Category = { + name: string; + icon: string; +}; + +type ForceEventData = { + categories: Category[]; + events: Event[]; +}; + +export const ForceEvent = (props, context) => { + return ( + + + + + + + + + + + + + ); +}; + +export const PanelOptions = (props, context) => { + const [searchQuery, setSearchQuery] = useLocalState( + context, + 'searchQuery', + '' + ); + + const [announce, setAnnounce] = useLocalState(context, 'announce', true); + + return ( + + + + + + setSearchQuery(e.target.value)} + placeholder="Search..." + value={searchQuery} + /> + + + setAnnounce(!announce)}> + Announce + + + + ); +}; + +export const EventSection = (props, context) => { + const { data, act } = useBackend(context); + const { categories, events } = data; + + const [category] = useLocalState(context, 'category', categories[0]); + const [searchQuery] = useLocalState(context, 'searchQuery', ''); + const [announce] = useLocalState(context, 'announce', true); + + const preparedEvents = paginateEvents( + events.filter((event) => { + // remove events not in the category you're looking at + if (event.category !== category.name) { + return false; + } + // remove events not being searched for, if a search is active + if (searchQuery && !event.name.toLowerCase().includes(searchQuery)) { + return false; + } + return true; + }), + EVENT_PAGE_ITEMS + ); + + return ( +
}> + + {preparedEvents.map((eventPage, i) => ( + + + {eventPage.map((event) => ( + + + + ))} + + + ))} + +
+ ); +}; + +export const EventTabs = (props, context) => { + const { data } = useBackend(context); + const { categories } = data; + + const [category, setCategory] = useLocalState( + context, + 'category', + categories[0] + ); + + const layerCats = paginate(categories, CATEGORY_PAGE_ITEMS); + + return ( +
+ {layerCats.map((page, i) => ( + + {page.map((cat) => ( + setCategory(cat)}> + {cat.name} + + ))} + + ))} +
+ ); +};