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.
|
* 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]: [],
|
||||||
|
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;
|
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 {
|
||||||
|
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 React from "react";
|
||||||
|
|
||||||
import Card from "@components/widgets/Card";
|
import Card from "@widgets/Card";
|
||||||
|
|
||||||
import { listCommands } from "@backend/data";
|
import { listCommands } from "@backend/data";
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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";
|
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"}>
|
||||||
<h1 className={"ItemsPage_Title"}>Items</h1>
|
<div className={"ItemsPage_Header"}>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{
|
||||||
|
items.length > 0 ? (
|
||||||
|
<VirtualizedGrid
|
||||||
|
list={items} itemHeight={64}
|
||||||
|
itemsPerRow={20} gap={5} itemGap={5}
|
||||||
|
render={(item) => (
|
||||||
|
<Item key={item.id} data={item} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
</div>
|
</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