diff --git a/src/handbook/src/backend/data.ts b/src/handbook/src/backend/data.ts index de48d6d2b..7ebf0d483 100644 --- a/src/handbook/src/backend/data.ts +++ b/src/handbook/src/backend/data.ts @@ -16,7 +16,7 @@ type TaggedItems = { [key: number]: Item[] } * TODO: Figure out what suffix is for which artifact type. */ -const sortedItems: TaggedItems = { +export const sortedItems: TaggedItems = { [ItemCategory.Constellation]: [], // Range: 1102 - 11xx [ItemCategory.Weapon]: [], [ItemCategory.Artifact]: [], diff --git a/src/handbook/src/css/components/VirtualizedGrid.scss b/src/handbook/src/css/components/VirtualizedGrid.scss new file mode 100644 index 000000000..44b569499 --- /dev/null +++ b/src/handbook/src/css/components/VirtualizedGrid.scss @@ -0,0 +1,4 @@ +.GridRow { + display: flex; + flex-direction: row; +} diff --git a/src/handbook/src/css/pages/ItemsPage.scss b/src/handbook/src/css/pages/ItemsPage.scss index 6c5be5b65..576e13088 100644 --- a/src/handbook/src/css/pages/ItemsPage.scss +++ b/src/handbook/src/css/pages/ItemsPage.scss @@ -9,15 +9,58 @@ padding: 24px; } +.ItemsPage_Header { + display: flex; + flex-direction: row; + + gap: 30px; + align-content: center; + + margin-bottom: 30px; +} + .ItemsPage_Title { - max-width: 275px; + max-width: 130px; max-height: 60px; font-size: 48px; font-weight: bold; text-align: center; + justify-content: center; +} - margin-bottom: 30px; +.ItemsPage_Search { + display: flex; + + width: 100%; + height: 100%; + max-width: 465px; + max-height: 60px; + + box-sizing: border-box; + align-items: center; + border-radius: 10px; + + background-color: var(--secondary-color); +} + +.ItemsPage_Input { + background-color: transparent; + border: none; + + color: var(--text-primary-color); + font-size: 20px; + width: 100%; + padding: 11px; + + &:focus, &:active { + outline: none; + } +} + +.ItemsPage_Input::placeholder { + color: var(--text-secondary-color); + opacity: 1; } .ItemsPage_List { diff --git a/src/handbook/src/css/widgets/Item.scss b/src/handbook/src/css/widgets/Item.scss new file mode 100644 index 000000000..2e6611575 --- /dev/null +++ b/src/handbook/src/css/widgets/Item.scss @@ -0,0 +1,21 @@ +.Item { + display: flex; + + width: 100%; + height: 100%; + max-width: 64px; + max-height: 64px; + + border-radius: 10px; + background-color: var(--secondary-color); +} + +.Item_Icon { + width: 64px; + height: 64px; +} + +.Item_Info { + position: absolute; + display: flex; +} diff --git a/src/handbook/src/ui/components/VirtualizedGrid.tsx b/src/handbook/src/ui/components/VirtualizedGrid.tsx new file mode 100644 index 000000000..4a99bef8b --- /dev/null +++ b/src/handbook/src/ui/components/VirtualizedGrid.tsx @@ -0,0 +1,81 @@ +import React from "react"; + +import { List as _List, ListProps, ListRowProps } from "react-virtualized/dist/es/List"; +import { AutoSizer as _AutoSizer, AutoSizerProps } from "react-virtualized/dist/es/AutoSizer"; + +const List = _List as unknown as React.FC; +const AutoSizer = _AutoSizer as unknown as React.FC; + +import "@css/components/VirtualizedGrid.scss"; + +interface IProps { + list: T[]; + render: (item: T) => React.ReactNode; + + itemHeight: number; + itemsPerRow?: number; + + gap?: number; + itemGap?: number; +} + +interface IState { + scrollTop: number; +} + +class VirtualizedGrid extends React.Component, IState> { + constructor(props: IProps) { + super(props); + + this.state = { + scrollTop: 0 + }; + } + + /** + * Renders a row of items. + */ + private rowRender(props: ListRowProps): React.ReactNode { + const items: React.ReactNode[] = []; + + // Calculate the items to render. + const perRow = this.props.itemsPerRow ?? 10; + for (let i = 0; i < perRow; i++) { + const itemIndex = props.index * perRow + i; + if (itemIndex < this.props.list.length) { + items.push(this.props.render(this.props.list[itemIndex])); + } + } + + return ( +
+ {items.map((item, index) => +
{item}
)} +
+
+ ); + } + + render() { + const { list, itemHeight, itemsPerRow } = this.props; + + return ( + + {({ height, width }) => ( + this.setState({ scrollTop: e.scrollTop })} + /> + )} + + ); + } +} + +export default VirtualizedGrid; diff --git a/src/handbook/src/ui/pages/CommandsPage.tsx b/src/handbook/src/ui/pages/CommandsPage.tsx index 165955be7..6c5416ace 100644 --- a/src/handbook/src/ui/pages/CommandsPage.tsx +++ b/src/handbook/src/ui/pages/CommandsPage.tsx @@ -1,6 +1,6 @@ import React from "react"; -import Card from "@components/widgets/Card"; +import Card from "@widgets/Card"; import { listCommands } from "@backend/data"; diff --git a/src/handbook/src/ui/pages/HomePage.tsx b/src/handbook/src/ui/pages/HomePage.tsx index 463238980..97dc11527 100644 --- a/src/handbook/src/ui/pages/HomePage.tsx +++ b/src/handbook/src/ui/pages/HomePage.tsx @@ -1,6 +1,6 @@ import React from "react"; -import HomeButton from "@components/widgets/HomeButton"; +import HomeButton from "@widgets/HomeButton"; import { ReactComponent as DiscordLogo } from "@icons/discord.svg"; diff --git a/src/handbook/src/ui/pages/ItemsPage.tsx b/src/handbook/src/ui/pages/ItemsPage.tsx index 6994100dc..49ce19205 100644 --- a/src/handbook/src/ui/pages/ItemsPage.tsx +++ b/src/handbook/src/ui/pages/ItemsPage.tsx @@ -1,20 +1,100 @@ -import React from "react"; +import React, { ChangeEvent } from "react"; -import { getItems } from "@backend/data"; +import Item from "@widgets/Item"; +import VirtualizedGrid from "@components/VirtualizedGrid"; + +import { ItemCategory } from "@backend/types"; +import type { Item as ItemType } from "@backend/types"; +import { getItems, sortedItems } from "@backend/data"; import "@css/pages/ItemsPage.scss"; -class ItemsPage extends React.PureComponent { +interface IState { + filters: ItemCategory[]; + search: string; +} + +class ItemsPage extends React.Component<{}, IState> { + constructor(props: {}) { + super(props); + + this.state = { + filters: [], + search: "" + }; + } + + /** + * Gets the items to render. + * @private + */ + private getItems(): ItemType[] { + let items: ItemType[] = []; + + // Add items based on filters. + const filters = this.state.filters; + if (filters.length == 0) { + items = getItems(); + } else { + for (const filter of filters) { + // Add items from the category. + items = items.concat(sortedItems[filter]); + // Remove duplicate items. + items = items.filter((item, index) => { + return items.indexOf(item) == index; + }); + } + } + + // Filter out items that don't match the search. + const search = this.state.search.toLowerCase(); + if (search != "") { + items = items.filter((item) => { + return item.name.toLowerCase().includes(search); + }); + } + + return items; + } + + /** + * Invoked when the search input changes. + * + * @param event The event. + * @private + */ + private onChange(event: ChangeEvent): void { + this.setState({ search: event.target.value }); + } + render() { + const items = this.getItems(); + return (
-

Items

+
+

Items

-
- {getItems().map((item) => ( -

{item.name}

- ))} +
+ +
+ + { + items.length > 0 ? ( + ( + + )} + /> + ) : undefined + }
); } diff --git a/src/handbook/src/ui/widgets/Item.tsx b/src/handbook/src/ui/widgets/Item.tsx new file mode 100644 index 000000000..28b1244d5 --- /dev/null +++ b/src/handbook/src/ui/widgets/Item.tsx @@ -0,0 +1,49 @@ +import React from "react"; + +import type { Item as ItemData } from "@backend/types"; + +import "@css/widgets/Item.scss"; + +interface IProps { + data: ItemData; +} + +interface IState { + popout: boolean; +} + +class Item extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + popout: false + }; + } + + /** + * Fetches the icon for the item. + * @private + */ + private getIcon(): string { + return `https://paimon.moe/images/items/teachings_of_freedom.png`; + } + + render() { + return ( +
+ {this.props.data.name} + +
+ +
+
+ ); + } +} + +export default Item;