mirror of
https://github.com/Melledy/Grasscutter.git
synced 2024-11-23 10:21:42 +00:00
Implement part of the items page
This commit is contained in:
parent
b2f15066be
commit
a27f7e0373
@ -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]: [],
|
||||
|
4
src/handbook/src/css/components/VirtualizedGrid.scss
Normal file
4
src/handbook/src/css/components/VirtualizedGrid.scss
Normal file
@ -0,0 +1,4 @@
|
||||
.GridRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
@ -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 {
|
||||
|
21
src/handbook/src/css/widgets/Item.scss
Normal file
21
src/handbook/src/css/widgets/Item.scss
Normal 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;
|
||||
}
|
81
src/handbook/src/ui/components/VirtualizedGrid.tsx
Normal file
81
src/handbook/src/ui/components/VirtualizedGrid.tsx
Normal 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;
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import Card from "@components/widgets/Card";
|
||||
import Card from "@widgets/Card";
|
||||
|
||||
import { listCommands } from "@backend/data";
|
||||
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
||||
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() {
|
||||
const items = this.getItems();
|
||||
|
||||
return (
|
||||
<div className={"ItemsPage"}>
|
||||
<div className={"ItemsPage_Header"}>
|
||||
<h1 className={"ItemsPage_Title"}>Items</h1>
|
||||
|
||||
<div className={"ItemsPage_List"}>
|
||||
{getItems().map((item) => (
|
||||
<p key={item.id}>{item.name}</p>
|
||||
))}
|
||||
<div className={"ItemsPage_Search"}>
|
||||
<input type={"text"}
|
||||
className={"ItemsPage_Input"}
|
||||
placeholder={"Search..."}
|
||||
onChange={this.onChange.bind(this)}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
49
src/handbook/src/ui/widgets/Item.tsx
Normal file
49
src/handbook/src/ui/widgets/Item.tsx
Normal 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;
|
Loading…
Reference in New Issue
Block a user