Implement part of the items page

This commit is contained in:
KingRainbow44 2023-04-08 01:19:35 -04:00
parent b2f15066be
commit a27f7e0373
No known key found for this signature in database
GPG Key ID: FC2CB64B00D257BE
9 changed files with 291 additions and 13 deletions

View File

@ -16,7 +16,7 @@ type TaggedItems = { [key: number]: Item[] }
* TODO: Figure out what suffix is for which artifact type. * TODO: Figure out what suffix is for which artifact type.
*/ */
const sortedItems: TaggedItems = { export const sortedItems: TaggedItems = {
[ItemCategory.Constellation]: [], // Range: 1102 - 11xx [ItemCategory.Constellation]: [], // Range: 1102 - 11xx
[ItemCategory.Weapon]: [], [ItemCategory.Weapon]: [],
[ItemCategory.Artifact]: [], [ItemCategory.Artifact]: [],

View File

@ -0,0 +1,4 @@
.GridRow {
display: flex;
flex-direction: row;
}

View File

@ -9,15 +9,58 @@
padding: 24px; padding: 24px;
} }
.ItemsPage_Header {
display: flex;
flex-direction: row;
gap: 30px;
align-content: center;
margin-bottom: 30px;
}
.ItemsPage_Title { .ItemsPage_Title {
max-width: 275px; max-width: 130px;
max-height: 60px; max-height: 60px;
font-size: 48px; font-size: 48px;
font-weight: bold; font-weight: bold;
text-align: center; 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 { .ItemsPage_List {

View File

@ -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;
}

View File

@ -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<ListProps>;
const AutoSizer = _AutoSizer as unknown as React.FC<AutoSizerProps>;
import "@css/components/VirtualizedGrid.scss";
interface IProps<T> {
list: T[];
render: (item: T) => React.ReactNode;
itemHeight: number;
itemsPerRow?: number;
gap?: number;
itemGap?: number;
}
interface IState {
scrollTop: number;
}
class VirtualizedGrid<T> extends React.Component<IProps<T>, IState> {
constructor(props: IProps<T>) {
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 (
<div key={props.key} style={{
...props.style,
gap: this.props.itemGap ?? 0
}} className={"GridRow"}>
{items.map((item, index) =>
<div key={index}>{item}</div>)}
<div style={{ height: this.props.gap ?? 0 }} />
</div>
);
}
render() {
const { list, itemHeight, itemsPerRow } = this.props;
return (
<AutoSizer>
{({ height, width }) => (
<List height={height - 150} width={width}
rowHeight={itemHeight + (this.props.gap ?? 0)}
rowCount={Math.ceil(list.length / (itemsPerRow ?? 10))}
rowRenderer={this.rowRender.bind(this)}
scrollTop={this.state.scrollTop}
onScroll={(e) => this.setState({ scrollTop: e.scrollTop })}
/>
)}
</AutoSizer>
);
}
}
export default VirtualizedGrid;

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import Card from "@components/widgets/Card"; import Card from "@widgets/Card";
import { listCommands } from "@backend/data"; import { listCommands } from "@backend/data";

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import HomeButton from "@components/widgets/HomeButton"; import HomeButton from "@widgets/HomeButton";
import { ReactComponent as DiscordLogo } from "@icons/discord.svg"; import { ReactComponent as DiscordLogo } from "@icons/discord.svg";

View File

@ -1,21 +1,101 @@
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"; 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<HTMLInputElement>): void {
this.setState({ search: event.target.value });
}
render() { render() {
const items = this.getItems();
return ( return (
<div className={"ItemsPage"}> <div className={"ItemsPage"}>
<div className={"ItemsPage_Header"}>
<h1 className={"ItemsPage_Title"}>Items</h1> <h1 className={"ItemsPage_Title"}>Items</h1>
<div className={"ItemsPage_List"}> <div className={"ItemsPage_Search"}>
{getItems().map((item) => ( <input type={"text"}
<p key={item.id}>{item.name}</p> className={"ItemsPage_Input"}
))} placeholder={"Search..."}
onChange={this.onChange.bind(this)}
/>
</div> </div>
</div> </div>
{
items.length > 0 ? (
<VirtualizedGrid
list={items} itemHeight={64}
itemsPerRow={20} gap={5} itemGap={5}
render={(item) => (
<Item key={item.id} data={item} />
)}
/>
) : undefined
}
</div>
); );
} }
} }

View File

@ -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<IProps, IState> {
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 (
<div className={"Item"}>
<img
className={"Item_Icon"}
alt={this.props.data.name}
src={this.getIcon()}
/>
<div className={"Item_Info"}>
</div>
</div>
);
}
}
export default Item;