diff --git a/src/handbook/src/backend/types.ts b/src/handbook/src/backend/types.ts index f44c4ddb5..4b9776b26 100644 --- a/src/handbook/src/backend/types.ts +++ b/src/handbook/src/backend/types.ts @@ -1,4 +1,6 @@ export type Page = "Home" | "Commands" | "Avatars" | "Items"; +export type Days = "Sunday" | "Monday" | "Tuesday" + | "Wednesday" | "Thursday" | "Friday" | "Saturday"; export type Command = { name: string[]; @@ -22,6 +24,26 @@ export type Item = { icon: string; }; +// Exported from Project Amber. +export type ItemInfo = { + response: number | 200 | 404; + data: { + name: string; + description: string; + type: string; + recipe: boolean; + mapMark: boolean; + source: { + name: string; + type: string | "domain"; + days: Days; + }[]; + icon: string; + rank: 1 | 2 | 3 | 4 | 5; + route: string; + }; +}; + export enum Target { None = "NONE", Offline = "OFFLINE", @@ -66,3 +88,21 @@ export enum ItemCategory { export function isPage(page: string): page is Page { return ["Home", "Commands"].includes(page); } + +/** + * Converts an item type to a string. + * + * @param type The item type to convert. + */ +export function itemTypeToString(type: ItemType): string { + switch (type) { + default: return "Unknown"; + case ItemType.None: return "None"; + case ItemType.Virtual: return "Virtual"; + case ItemType.Material: return "Material"; + case ItemType.Reliquary: return "Reliquary"; + case ItemType.Weapon: return "Weapon"; + case ItemType.Display: return "Display"; + case ItemType.Furniture: return "Furniture"; + } +} diff --git a/src/handbook/src/css/pages/ItemsPage.scss b/src/handbook/src/css/pages/ItemsPage.scss index 576e13088..3ef443681 100644 --- a/src/handbook/src/css/pages/ItemsPage.scss +++ b/src/handbook/src/css/pages/ItemsPage.scss @@ -3,12 +3,20 @@ height: 100%; width: 100%; + flex-direction: row; + justify-content: space-between; background-color: var(--background-color); - flex-direction: column; padding: 24px; } +.ItemsPage_Content { + display: flex; + flex-direction: column; + + width: 80%; +} + .ItemsPage_Header { display: flex; flex-direction: row; @@ -72,3 +80,14 @@ margin-bottom: 28px; overflow-y: scroll; } + +.ItemsPage_Card { + display: flex; + + width: 100%; + max-width: 300px; + min-height: 300px; + max-height: 700px; + + align-self: center; +} diff --git a/src/handbook/src/css/widgets/ItemCard.scss b/src/handbook/src/css/widgets/ItemCard.scss new file mode 100644 index 000000000..8e3c88a8f --- /dev/null +++ b/src/handbook/src/css/widgets/ItemCard.scss @@ -0,0 +1,71 @@ +.ItemCard { + display: flex; + flex-direction: column; + justify-content: space-between; + + width: 100%; + height: 100%; + max-width: 300px; + min-height: 300px; + max-height: 700px; + + padding: 20px; + box-sizing: border-box; + + border-radius: 10px; + background-color: var(--accent-color); +} + +.ItemCard_Content { + display: flex; + gap: 10px; + + flex-direction: column; +} + +.ItemCard_Header { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.ItemCard_Info { + display: flex; + flex-direction: column; + gap: 10px; + + :nth-child(1) { + font-weight: bold; + font-size: 20px; + + max-width: 170px; + max-height: 40px; + + color: var(--text-primary-color); + } + + :nth-child(2) { + font-size: 16px; + + color: var(--text-primary-color); + } +} + +.ItemCard_Icon { + width: 64px; + height: 64px +} + +.ItemCard_Description { + display: flex; + flex-direction: column; + + max-width: 250px; + max-height: 460px; + + p { + font-size: 14px; + + color: var(--text-primary-color); + } +} diff --git a/src/handbook/src/ui/pages/ItemsPage.tsx b/src/handbook/src/ui/pages/ItemsPage.tsx index 1c9381819..f8cdd3e47 100644 --- a/src/handbook/src/ui/pages/ItemsPage.tsx +++ b/src/handbook/src/ui/pages/ItemsPage.tsx @@ -1,17 +1,22 @@ import React, { ChangeEvent } from "react"; import Item from "@widgets/Item"; +import ItemCard from "@widgets/ItemCard"; import VirtualizedGrid from "@components/VirtualizedGrid"; import { ItemCategory } from "@backend/types"; -import type { Item as ItemType } from "@backend/types"; +import type { Item as ItemType, ItemInfo } from "@backend/types"; import { getItems, sortedItems } from "@backend/data"; +import { fetchItemData } from "@app/utils"; import "@css/pages/ItemsPage.scss"; interface IState { filters: ItemCategory[]; search: string; + + selected: ItemType | null; + selectedInfo: ItemInfo | null; } class ItemsPage extends React.Component<{}, IState> { @@ -20,7 +25,10 @@ class ItemsPage extends React.Component<{}, IState> { this.state = { filters: [], - search: "" + search: "", + + selected: null, + selectedInfo: null }; } @@ -82,34 +90,63 @@ class ItemsPage extends React.Component<{}, IState> { return item.id > 0; } + /** + * Sets the selected item. + * + * @param item The item. + * @private + */ + private async setSelectedItem(item: ItemType): Promise { + let data: ItemInfo | null = null; try { + data = await fetchItemData(item); + } catch { } + + this.setState({ + selected: item, + selectedInfo: data + }); + } + render() { const items = this.getItems(); return (
-
-

Items

+
+
+

Items

-
- +
+ +
+ + {items.length > 0 ? ( + this.showItem(item))} + itemHeight={64} + itemsPerRow={18} + gap={5} + itemGap={5} + render={(item) => this.setSelectedItem(item)} + />} + /> + ) : undefined}
- {items.length > 0 ? ( - this.showItem(item))} - itemHeight={64} - itemsPerRow={20} - gap={5} - itemGap={5} - render={(item) => } +
+ - ) : undefined} +
); } diff --git a/src/handbook/src/ui/widgets/Item.tsx b/src/handbook/src/ui/widgets/Item.tsx index b0e1f2532..81ebbea05 100644 --- a/src/handbook/src/ui/widgets/Item.tsx +++ b/src/handbook/src/ui/widgets/Item.tsx @@ -7,6 +7,7 @@ import "@css/widgets/Item.scss"; interface IProps { data: ItemData; + onClick?: () => void; } interface IState { @@ -51,7 +52,9 @@ class Item extends React.Component { render() { return ( -
+
{this.state.icon && ( { + return

{line}

; + }); +} + +interface IProps { + item: ItemType | null; + info: ItemInfo | null; +} + +interface IState { + icon: boolean; +} + +class ItemCard extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + icon: true + }; + } + + render() { + const { item, info } = this.props; + const data = info?.data; + + return item ? ( +
+
+
+
+

{data?.name ?? item.name}

+

{data?.type ?? itemTypeToString(item.type)}

+
+ + { this.state.icon && {item.name} this.setState({ icon: false })} + /> } +
+ +
+ {toDescription(data?.description)} +
+
+ +
+ +
+
+ ) : undefined; + } +} + +export default ItemCard; diff --git a/src/handbook/src/utils.ts b/src/handbook/src/utils.ts index 49fe950f3..74c46d66c 100644 --- a/src/handbook/src/utils.ts +++ b/src/handbook/src/utils.ts @@ -1,5 +1,5 @@ -import { ItemType, Quality } from "@backend/types"; import type { Item } from "@backend/types"; +import { ItemInfo, ItemType, Quality } from "@backend/types"; /** * Fetches the name of the CSS variable for the quality. @@ -73,3 +73,41 @@ const refSwitch: { [key: number]: string } = { 10000005: "traveler_anemo", 10000007: "traveler_geo" }; + +/** + * Gets the route for an item type. + * + * @param type The type of the item. + */ +export function typeToRoute(type: ItemType): string { + switch (type) { + default: + return "material"; + case ItemType.Furniture: + return "furniture"; + case ItemType.Reliquary: + return "reliquary"; + case ItemType.Weapon: + return "weapon"; + } +} + +/** + * Fetches the data for an item. + * Uses the Project Amber API to get the data. + * + * @route GET https://api.ambr.top/v2/EN/{type}/{id} + * @param item The item to fetch the data for. + */ +export async function fetchItemData(item: Item): Promise { + let url = `https://api.ambr.top/v2/EN/(type)/(id)`; + + // Replace the type and ID in the URL. + url = url.replace("(type)", typeToRoute(item.type)); + url = url.replace("(id)", item.id.toString()); + + // Fetch the data. + return fetch(url) + .then((res) => res.json()) + .catch(() => {}); +}