Files
eveship.fit.react/src/HullListing/HullListing.tsx

252 lines
9.1 KiB
TypeScript

import clsx from "clsx";
import React from "react";
import { EsiContext } from "../EsiProvider";
import { EsiFit, ShipSnapshotContext } from "../ShipSnapshotProvider";
import { EveDataContext } from "../EveDataProvider";
import { Icon, IconName } from "../Icon";
import { TreeListing, TreeHeader, TreeHeaderAction, TreeLeaf } from "../TreeListing";
import { LocalFitContext } from "../LocalFitProvider";
import styles from "./HullListing.module.css";
interface ListingFit {
origin: "local" | "esi-character";
fit: EsiFit;
}
interface ListingHull {
name: string;
fits: ListingFit[];
}
interface ListingHulls {
[typeId: string]: ListingHull;
}
interface ListingGroup {
[raceName: string]: ListingHulls;
}
interface ListingGroups {
[groupName: string]: ListingGroup;
}
const factionIdToRace: Record<number, string> = {
500001: "Caldari",
500002: "Minmatar",
500003: "Amarr",
500004: "Gallente",
1: "Non-Empire",
} as const;
const Hull = (props: { typeId: number, entry: ListingHull }) => {
const shipSnapShot = React.useContext(ShipSnapshotContext);
const getChildren = React.useCallback(() => {
if (props.entry.fits.length === 0) {
return <TreeLeaf level={4} content={"No Item"} />;
} else {
let index = 0;
return <>{props.entry.fits.sort((a, b) => a.fit.name.localeCompare(b.fit.name)).map((fit) => {
index += 1;
let icon: IconName | undefined;
let iconTitle: string | undefined;
switch (fit.origin) {
case "local":
icon = "fitting-local";
iconTitle = "Browser-stored fitting";
break;
case "esi-character":
icon = "fitting-character";
iconTitle = "In-game personal fitting";
break;
}
return <TreeLeaf key={`${fit.fit.ship_type_id}-${index}`} level={4} content={fit.fit.name} onClick={() => shipSnapShot.changeFit(fit.fit)} icon={icon} iconTitle={iconTitle} />;
})}</>;
}
}, [props, shipSnapShot]);
const onClick = React.useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
shipSnapShot.changeHull(props.typeId);
}, [props, shipSnapShot]);
const headerAction = <TreeHeaderAction icon="simulate" onClick={onClick} />;
const header = <TreeHeader icon={`https://images.evetech.net/types/${props.typeId}/icon?size=32`} text={props.entry.name} action={headerAction} />;
return <TreeListing level={3} header={header} height={32} getChildren={getChildren} />;
}
const HullRace = (props: { raceId: number, entries: ListingHulls }) => {
const getChildren = React.useCallback(() => {
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 <Hull key={typeId} typeId={parseInt(typeId)} entry={entry} />
})}</>;
}, [props]);
if (props.entries === undefined) return null;
const header = <TreeHeader icon={`https://images.evetech.net/corporations/${props.raceId}/logo?size=32`} text={`${factionIdToRace[props.raceId]} [${Object.keys(props.entries).length}]`} />;
return <TreeListing level={2} header={header} getChildren={getChildren} />;
}
const HullGroup = (props: { name: string, entries: ListingGroup }) => {
const getChildren = React.useCallback(() => {
return <>
<HullRace raceId={500003} entries={props.entries.Amarr} />
<HullRace raceId={500001} entries={props.entries.Caldari} />
<HullRace raceId={500004} entries={props.entries.Gallente} />
<HullRace raceId={500002} entries={props.entries.Minmatar} />
<HullRace raceId={1} entries={props.entries.NonEmpire} />
</>;
}, [props]);
const header = <TreeHeader text={`${props.name}`} />;
return <TreeListing level={1} header={header} getChildren={getChildren} />;
};
/**
* Show all the fittings for the current ESI character.
*/
export const HullListing = () => {
const esi = React.useContext(EsiContext);
const localFit = React.useContext(LocalFitContext);
const eveData = React.useContext(EveDataContext);
const shipSnapShot = React.useContext(ShipSnapshotContext);
const [hullGroups, setHullGroups] = React.useState<ListingGroups>({});
const [search, setSearch] = React.useState<string>("");
const [filter, setFilter] = React.useState({
localCharacter: false,
esiCharacter: false,
currentHull: false,
});
const [localCharacterFits, setLocalCharacterFits] = React.useState<Record<string, ListingFit[]>>({});
const [esiCharacterFits, setEsiCharacterFits] = React.useState<Record<string, ListingFit[]>>({});
React.useEffect(() => {
if (!localFit.loaded) return;
if (!localFit.fittings) return;
const newLocalCharacterFits: Record<string, ListingFit[]> = {};
for (const fit of localFit.fittings) {
if (fit.ship_type_id === undefined) continue;
if (newLocalCharacterFits[fit.ship_type_id] === undefined) {
newLocalCharacterFits[fit.ship_type_id] = [];
}
newLocalCharacterFits[fit.ship_type_id].push({
origin: "local",
fit
});
}
setLocalCharacterFits(newLocalCharacterFits);
}, [localFit]);
React.useEffect(() => {
if (!esi.loaded) return;
if (!esi.currentCharacter) return;
const charFittings = esi.characters[esi.currentCharacter].charFittings || [];
const newEsiCharacterFits: Record<string, ListingFit[]> = {};
for (const fit of charFittings) {
if (fit.ship_type_id === undefined) continue;
if (newEsiCharacterFits[fit.ship_type_id] === undefined) {
newEsiCharacterFits[fit.ship_type_id] = [];
}
newEsiCharacterFits[fit.ship_type_id].push({
origin: "esi-character",
fit,
});
}
setEsiCharacterFits(newEsiCharacterFits);
}, [esi]);
React.useEffect(() => {
if (!eveData.loaded) return;
const anyFilter = filter.localCharacter || filter.esiCharacter;
const newHullGroups: ListingGroups = {};
for (const typeId in eveData.typeIDs) {
const hull = eveData.typeIDs[typeId];
if (hull.categoryID !== 6) continue;
if (hull.marketGroupID === undefined) continue;
if (!hull.published) continue;
if (filter.currentHull && shipSnapShot.fit?.ship_type_id !== parseInt(typeId)) continue;
const fits: ListingFit[] = [];
if (anyFilter) {
if (filter.localCharacter && Object.keys(localCharacterFits).includes(typeId)) fits.push(...localCharacterFits[typeId]);
if (filter.esiCharacter && Object.keys(esiCharacterFits).includes(typeId)) fits.push(...esiCharacterFits[typeId]);
if (fits.length == 0) {
if (!filter.currentHull || shipSnapShot.fit?.ship_type_id !== parseInt(typeId)) continue;
}
} else {
if (Object.keys(localCharacterFits).includes(typeId)) fits.push(...localCharacterFits[typeId]);
if (Object.keys(esiCharacterFits).includes(typeId)) fits.push(...esiCharacterFits[typeId]);
}
if (search !== "" && !hull.name.toLowerCase().includes(search.toLowerCase())) continue;
const group = eveData.groupIDs?.[hull.groupID]?.name ?? "Unknown Group";
const race = factionIdToRace[hull.factionID || 0] ?? "NonEmpire";
if (newHullGroups[group] === undefined) {
newHullGroups[group] = {};
}
if (newHullGroups[group][race] === undefined) {
newHullGroups[group][race] = {};
}
newHullGroups[group][race][typeId] = {
name: hull.name,
fits,
}
}
setHullGroups(newHullGroups);
}, [eveData, search, filter, localCharacterFits, esiCharacterFits, shipSnapShot.fit?.ship_type_id]);
return <div className={styles.listing}>
<div className={styles.topbar}>
<input type="text" placeholder="Search" value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
<div className={styles.filter}>
<span className={clsx({[styles.selected]: filter.localCharacter})} onClick={() => setFilter({...filter, localCharacter: !filter.localCharacter})}>
<Icon name="fitting-local" size={32} title="Filter: Browser-stored fittings" />
</span>
<span className={clsx({[styles.selected]: filter.esiCharacter})} onClick={() => setFilter({...filter, esiCharacter: !filter.esiCharacter})}>
<Icon name="fitting-character" size={32} title="Filter: in-game personal fittings" />
</span>
<span className={styles.disabled}>
<Icon name="fitting-corporation" size={32} title="CCP didn't implement this ESI endpoint (yet?)" />
</span>
<span className={styles.disabled}>
<Icon name="fitting-alliance" size={32} title="CCP didn't implement this ESI endpoint (yet?)" />
</span>
<span className={clsx({[styles.selected]: filter.currentHull})} onClick={() => setFilter({...filter, currentHull: !filter.currentHull})}>
<Icon name="fitting-hull" size={32} title="Filter: current hull" />
</span>
</div>
<div className={styles.listingContent}>
{Object.keys(hullGroups).sort().map((groupName) => {
const groupData = hullGroups[groupName];
return <HullGroup key={groupName} name={groupName} entries={groupData} />
})}
</div>
</div>
};