From c0ecc3f1839ffd0794cebb596f63929cfdf27b0c Mon Sep 17 00:00:00 2001 From: Patric Stout Date: Sun, 3 Dec 2023 15:59:05 +0100 Subject: [PATCH] chore: refactor hull-listing to use tree-listing (#35) This simplifies the code, and makes adding module-listing a lot easier. --- src/HullListing/HullListing.module.css | 64 -------------- src/HullListing/HullListing.tsx | 110 ++++++++---------------- src/TreeListing/TreeListing.module.css | 38 ++++++++ src/TreeListing/TreeListing.stories.tsx | 49 +++++++++++ src/TreeListing/TreeListing.tsx | 92 ++++++++++++++++++++ src/TreeListing/index.ts | 1 + 6 files changed, 214 insertions(+), 140 deletions(-) create mode 100644 src/TreeListing/TreeListing.module.css create mode 100644 src/TreeListing/TreeListing.stories.tsx create mode 100644 src/TreeListing/TreeListing.tsx create mode 100644 src/TreeListing/index.ts diff --git a/src/HullListing/HullListing.module.css b/src/HullListing/HullListing.module.css index ffe7ef1..6384a6b 100644 --- a/src/HullListing/HullListing.module.css +++ b/src/HullListing/HullListing.module.css @@ -13,70 +13,6 @@ overflow-y: auto; } -.header1, .header2, .header3, .header4 { - display: flex; - user-select: none; -} -.header1, .header2, .header4 { - height: 25px; - line-height: 25px; -} -.header1 > span, .header2 > span, .header3 > span, .header4 > span { - margin: 0 4px; -} - -.header1 { - background-color: #1d1d1d; -} - -.header1:hover, .header2:hover, .header3:hover, .header4:hover { - background-color: #4f4f4f; -} -.header4.noitem:hover { - background-color: #111111; -} - -.collapsed { - display: none; -} - -.level1, .level2, .level3 { - margin-left: 24px; -} - -.hull { - display: flex; -} -.hull > span { - display: inline-block; - height: 32px; - line-height: 32px; -} -.hull > span:nth-child(3) { - flex: 1; -} -.hull > span:last-child { - text-align: right; -} -.hull > span:last-child > img { - opacity: 0.5; -} -.hull > span:last-child > img:hover { - opacity: 1.0; -} - -.hullSimulate { - cursor: pointer; -} - -.fit { - cursor: pointer; - padding-left: 6px; -} -.fit.noitem { - cursor: default; -} - .topbar { display: flex; } diff --git a/src/HullListing/HullListing.tsx b/src/HullListing/HullListing.tsx index f7a6896..0dfa7a2 100644 --- a/src/HullListing/HullListing.tsx +++ b/src/HullListing/HullListing.tsx @@ -5,6 +5,7 @@ import { EsiContext } from "../EsiProvider"; import { EsiFit, ShipSnapshotContext } from "../ShipSnapshotProvider"; import { EveDataContext } from "../EveDataProvider"; import { Icon } from "../Icon"; +import { TreeListing, TreeHeader, TreeHeaderAction, TreeLeaf } from "../TreeListing"; import styles from "./HullListing.module.css"; @@ -30,112 +31,69 @@ const factionIdToRace: Record = { 500002: "Minmatar", 500003: "Amarr", 500004: "Gallente", + 1: "Non-Empire", } as const; const Hull = (props: { typeId: number, entry: ListingFit, changeHull: (typeId: number) => void, changeFit: (fit: EsiFit) => void }) => { - const [expanded, setExpanded] = React.useState(false); - - let children = <>; - if (expanded) { + const getChildren = React.useCallback(() => { if (props.entry.fits.length === 0) { - children = <> -
- No Item -
- ; + return ; } else { let index = 0; - children = <>{props.entry.fits.map((fit) => { + return <>{props.entry.fits.map((fit) => { index += 1; - return
props.changeFit(fit)}> - {fit.name} -
+ return props.changeFit(fit)} />; })}; } + }, [props]); + + function onClick(e: React.MouseEvent) { + e.stopPropagation(); + props.changeHull(props.typeId); } - return
-
setExpanded((current) => !current)}> - - - - - - - - {props.entry.name} - - props.changeHull(props.typeId)}> - - -
-
- {children} -
-
+ const headerAction = ; + const header = ; + return ; } -const HullRace = (props: { name: string, entries: ListingHulls, changeHull: (typeId: number) => void, changeFit: (fit: EsiFit) => void }) => { - const [expanded, setExpanded] = React.useState(false); - - if (props.entries === undefined) return null; - - let children = <>; - if (expanded) { +const HullRace = (props: { raceId: number, entries: ListingHulls, changeHull: (typeId: number) => void, changeFit: (fit: EsiFit) => void }) => { + const getChildren = React.useCallback(() => { const changeProps = { changeHull: props.changeHull, changeFit: props.changeFit, }; - children = <>{Object.keys(props.entries).sort((a, b) => props.entries[a].name.localeCompare(props.entries[b].name)).map((typeId) => { + return <>{Object.keys(props.entries).sort((a, b) => props.entries[a].name.localeCompare(props.entries[b].name)).map((typeId) => { const entry = props.entries[typeId]; return })}; - } + }, [props]); - return
-
setExpanded((current) => !current)}> - - - - {props.name} [{Object.keys(props.entries).length}] -
-
- {children} -
-
+ if (props.entries === undefined) return null; + + const header = ; + return ; } const HullGroup = (props: { name: string, entries: ListingGroup, changeHull: (typeId: number) => void, changeFit: (fit: EsiFit) => void }) => { - const [expanded, setExpanded] = React.useState(false); - - let children = <>; - if (expanded) { + const getChildren = React.useCallback(() => { const changeProps = { changeHull: props.changeHull, changeFit: props.changeFit, }; - children = <> - - - - - + return <> + + + + + ; - } + }, [props]); - return
-
setExpanded((current) => !current)}> - - - - {props.name} -
-
- {children} -
-
+ const header = ; + return ; }; /** @@ -229,7 +187,7 @@ export const HullListing = (props: { changeHull: (typeId: number) => void, chang setFilter({...filter, esiCharacter: !filter.esiCharacter})}> - + @@ -238,7 +196,7 @@ export const HullListing = (props: { changeHull: (typeId: number) => void, chang setFilter({...filter, currentHull: !filter.currentHull})}> - +
diff --git a/src/TreeListing/TreeListing.module.css b/src/TreeListing/TreeListing.module.css new file mode 100644 index 0000000..d099852 --- /dev/null +++ b/src/TreeListing/TreeListing.module.css @@ -0,0 +1,38 @@ +.header { + display: flex; + height: var(--height); + padding: 2px 0; + line-height: var(--height); + user-select: none; +} +.header > span { + margin-left: 4px; +} +.headerHover:hover { + background-color: #4f4f4f; +} + +.header1 { + background-color: #1d1d1d; +} + +.headerText { + flex: 1; +} + +.headerAction { + cursor: pointer; + opacity: 0.5; + margin-right: 4px; +} +.headerAction:hover { + opacity: 1.0; +} + +.content { + margin-left: 20px; +} + +.leaf { + cursor: pointer; +} diff --git a/src/TreeListing/TreeListing.stories.tsx b/src/TreeListing/TreeListing.stories.tsx new file mode 100644 index 0000000..c85cd76 --- /dev/null +++ b/src/TreeListing/TreeListing.stories.tsx @@ -0,0 +1,49 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { fullFit } from '../../.storybook/fits'; + +import { TreeListing } from './'; +import { EsiProvider } from '../EsiProvider'; +import { EveDataProvider } from '../EveDataProvider'; +import { EsiFit, ShipSnapshotProvider } from '../ShipSnapshotProvider'; +import { DogmaEngineProvider } from '../DogmaEngineProvider'; + +const meta: Meta = { + component: TreeListing, + tags: ['autodocs'], + title: 'Component/TreeListing', +}; + +export default meta; +type Story = StoryObj; + +const withEsiProvider: Decorator<{ changeHull: (typeId: number) => void, changeFit: (fit: EsiFit) => void }> = (Story, context) => { + return ( + + + + +
+ +
+
+
+
+
+ ); +} + +export const Default: Story = { + args: { + changeHull: (typeId: number) => console.log(`changeHull(${typeId})`), + changeFit: (fit: EsiFit) => console.log(`changeFit(${fit})`), + }, + decorators: [withEsiProvider], + parameters: { + snapshot: { + fit: fullFit, + skills: {}, + } + }, +}; diff --git a/src/TreeListing/TreeListing.tsx b/src/TreeListing/TreeListing.tsx new file mode 100644 index 0000000..7557419 --- /dev/null +++ b/src/TreeListing/TreeListing.tsx @@ -0,0 +1,92 @@ +import clsx from "clsx"; +import React from "react"; + +import { Icon, IconName } from "../Icon"; + +import styles from "./TreeListing.module.css"; + +interface Tree { + size: number; +} + +export const TreeContext = React.createContext({size: 24}); + +/** + * Action (the icon on the right side of the header) for a header. + */ +export const TreeHeaderAction = (props: { icon: IconName, onClick: (e: React.MouseEvent) => void }) => { + const tree = React.useContext(TreeContext); + + return
+ +
+} + +/** + * Header for a listing. + */ +export const TreeHeader = (props: { icon?: string, text: string, action?: React.ReactNode }) => { + const tree = React.useContext(TreeContext); + + return <> + {props.icon !== undefined && + + } + + {props.text} + + {props.action && + {props.action} + } + +} + +export const TreeLeaf = (props: { level: number, height?: number, content: string, onClick?: (e: React.MouseEvent) => void }) => { + const stylesHeader = styles[`header${props.level}`]; + + const height = props.height ?? 20; + const style = { "--height": `${height}px` } as React.CSSProperties; + + return
+ +
+ + {props.content} + +
+
+
; +} + +/** + * Tree listing for hulls, modules, and charges. + */ +export const TreeListing = (props: { level: number, header: React.ReactNode, height?: number, getChildren: () => React.ReactNode }) => { + const [expanded, setExpanded] = React.useState(false); + + const stylesHeader = styles[`header${props.level}`]; + const stylesContent = styles[`content${props.level}`]; + + const height = props.height ?? 20; + const style = { "--height": `${height}px` } as React.CSSProperties; + + let children: React.ReactNode = <>; + /* Speed up rendering by not rendering children if we are collapsed. */ + if (expanded) { + children = props.getChildren(); + } + + return
+ +
setExpanded((current) => !current)}> + + + + {props.header} +
+
+ {children} +
+
+
+}; diff --git a/src/TreeListing/index.ts b/src/TreeListing/index.ts new file mode 100644 index 0000000..718e1de --- /dev/null +++ b/src/TreeListing/index.ts @@ -0,0 +1 @@ +export { TreeListing, TreeHeader, TreeHeaderAction, TreeLeaf } from "./TreeListing";