refactor: move skills storage inside ShipSnapshotProvider (#82)

This makes EsiProvider dependant on ShipSnapshotProvider, but also
avoids pages having to store skills themselves.
This commit is contained in:
Patric Stout
2024-05-10 21:16:23 +02:00
committed by GitHub
parent 67543579c8
commit 235d28cfa5
24 changed files with 109 additions and 142 deletions

View File

@@ -21,17 +21,15 @@ type Story = StoryObj<typeof CalculationDetail>;
const useShipSnapshotProvider: Decorator<{
source: "Ship" | "Char" | "Structure" | "Target" | { Item?: number; Cargo?: number };
}> = (Story, context) => {
const [skills, setSkills] = React.useState<Record<string, number>>({});
return (
<EveDataProvider>
<EsiProvider setSkills={setSkills}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot} skills={skills}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot}>
<EsiProvider>
<Story {...context.args} />
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EsiProvider>
</EsiProvider>
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EveDataProvider>
);
};
@@ -43,7 +41,7 @@ export const Default: Story = {
decorators: [useShipSnapshotProvider],
parameters: {
snapshot: {
fit: fullFit,
initialFit: fullFit,
},
},
};

View File

@@ -19,19 +19,17 @@ export default meta;
type Story = StoryObj<typeof DroneBay>;
const useShipSnapshotProvider: Decorator<Record<string, never>> = (Story, context) => {
const [skills, setSkills] = React.useState<Record<string, number>>({});
return (
<EveDataProvider>
<EsiProvider setSkills={setSkills}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot} skills={skills}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot}>
<EsiProvider>
<div style={{ width: context.args.width, height: context.args.width }}>
<Story />
</div>
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EsiProvider>
</EsiProvider>
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EveDataProvider>
);
};

View File

@@ -17,7 +17,7 @@ type Story = StoryObj<typeof EsiCharacterSelection>;
const withEsiProvider: Decorator<Record<string, never>> = (Story) => {
return (
<EveDataProvider>
<EsiProvider setSkills={console.log}>
<EsiProvider>
<Story />
</EsiProvider>
</EveDataProvider>

View File

@@ -1,8 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { fullFit } from "../../.storybook/fits";
import { EsiContext, EsiProvider } from "./";
import { EveDataProvider } from "../EveDataProvider";
import { ShipSnapshotProvider } from "../ShipSnapshotProvider";
const meta: Meta<typeof EsiProvider> = {
component: EsiProvider,
@@ -35,16 +38,13 @@ const TestEsi = () => {
};
export const Default: Story = {
args: {
setSkills: (skills: Record<string, number>) => {
console.log(skills);
},
},
render: (args) => (
render: () => (
<EveDataProvider>
<EsiProvider {...args}>
<TestEsi />
</EsiProvider>
<ShipSnapshotProvider initialFit={fullFit}>
<EsiProvider>
<TestEsi />
</EsiProvider>
</ShipSnapshotProvider>
</EveDataProvider>
),
};

View File

@@ -1,7 +1,7 @@
import { jwtDecode } from "jwt-decode";
import React from "react";
import { EsiFit } from "../ShipSnapshotProvider";
import { EsiFit, ShipSnapshotContext } from "../ShipSnapshotProvider";
import { getAccessToken } from "./EsiAccessToken";
import { getSkills } from "./EsiSkills";
@@ -47,8 +47,6 @@ export const EsiContext = React.createContext<Esi>({
});
export interface EsiProps {
/** Callback to call when skills are changed. */
setSkills: (skills: Record<string, number>) => void;
/** Children that can use this provider. */
children: React.ReactNode;
}
@@ -58,6 +56,7 @@ export interface EsiProps {
*/
export const EsiProvider = (props: EsiProps) => {
const eveData = React.useContext(EveDataContext);
const snapshot = React.useContext(ShipSnapshotContext);
const [esi, setEsi] = React.useState<Esi>({
loaded: undefined,
@@ -151,7 +150,7 @@ export const EsiProvider = (props: EsiProps) => {
/* Skills already fetched? We won't do it again till the user reloads. */
const currentSkills = esi.characters[characterId]?.skills;
if (currentSkills !== undefined) {
props.setSkills(currentSkills);
snapshot.changeSkills(currentSkills);
return;
}
@@ -178,7 +177,7 @@ export const EsiProvider = (props: EsiProps) => {
};
});
props.setSkills(skills);
snapshot.changeSkills(skills);
return;
}
@@ -208,7 +207,7 @@ export const EsiProvider = (props: EsiProps) => {
};
});
props.setSkills(skills);
snapshot.changeSkills(skills);
});
getCharFittings(characterId, accessToken).then((charFittings) => {

View File

@@ -34,8 +34,7 @@ export const Default: Story = {
decorators: [withShipSnapshotProvider],
parameters: {
snapshot: {
fit: fullFit,
skills: {},
initialFit: fullFit,
},
},
};

View File

@@ -39,16 +39,16 @@ export function useEveShipFitLink() {
if (!shipSnapshot?.loaded) return;
async function doCreateLink() {
if (!shipSnapshot?.fit) {
if (!shipSnapshot?.currentFit) {
setFitLink("");
return;
}
const fitHash = await encodeEsiFit(shipSnapshot.fit);
const fitHash = await encodeEsiFit(shipSnapshot.currentFit);
setFitLink(`https://eveship.fit/#fit:${fitHash}`);
}
doCreateLink();
}, [shipSnapshot?.loaded, shipSnapshot?.fit]);
}, [shipSnapshot?.loaded, shipSnapshot?.currentFit]);
return fitLink;
}

View File

@@ -41,8 +41,7 @@ export const Default: Story = {
decorators: [withEveDataProvider],
parameters: {
snapshot: {
fit: fullFit,
skills: {},
initialFit: fullFit,
},
},
};

View File

@@ -18,7 +18,7 @@ export const RenameButton = () => {
}, [rename, shipSnapshot]);
const openRename = React.useCallback(() => {
setRename(shipSnapshot?.fit?.name ?? "");
setRename(shipSnapshot?.currentFit?.name ?? "");
setIsRenameOpen(true);
}, [shipSnapshot]);

View File

@@ -17,13 +17,13 @@ export const SaveButton = () => {
const saveBrowser = React.useCallback(
(force?: boolean) => {
if (!localFit.loaded) return;
if (!shipSnapshot.loaded || !shipSnapshot?.fit) return;
if (!shipSnapshot.loaded || !shipSnapshot?.currentFit) return;
setIsPopupOpen(false);
if (!force) {
for (const fit of localFit.fittings) {
if (fit.name === shipSnapshot.fit.name) {
if (fit.name === shipSnapshot.currentFit.name) {
setIsAlreadyExistsOpen(true);
return;
}
@@ -32,7 +32,7 @@ export const SaveButton = () => {
setIsAlreadyExistsOpen(false);
localFit.addFit(shipSnapshot.fit);
localFit.addFit(shipSnapshot.currentFit);
},
[localFit, shipSnapshot],
);
@@ -61,7 +61,7 @@ export const SaveButton = () => {
title="Update Fitting?"
>
<div>
<div>You have a fitting with the name {shipSnapshot?.fit?.name}, do you want to update it?</div>
<div>You have a fitting with the name {shipSnapshot?.currentFit?.name}, do you want to update it?</div>
<div className={styles.alreadyExistsButtons}>
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => saveBrowser(true)}>
Yes

View File

@@ -33,8 +33,7 @@ export const Default: Story = {
decorators: [withEveDataProvider],
parameters: {
snapshot: {
fit: fullFit,
skills: {},
initialFit: fullFit,
},
},
};

View File

@@ -30,14 +30,14 @@ export function useFormatAsEft() {
return (): string | undefined => {
if (!eveData?.loaded) return undefined;
if (!shipSnapshot?.loaded || shipSnapshot.fit == undefined) return undefined;
if (!shipSnapshot?.loaded || shipSnapshot.currentFit == undefined) return undefined;
let eft = "";
const shipType = eveData.typeIDs?.[shipSnapshot.fit.ship_type_id];
const shipType = eveData.typeIDs?.[shipSnapshot.currentFit.ship_type_id];
if (!shipType) return undefined;
eft += `[${shipType.name}, ${shipSnapshot.fit.name}]\n`;
eft += `[${shipType.name}, ${shipSnapshot.currentFit.name}]\n`;
for (const slotType of Object.keys(esiFlagMapping) as ShipSnapshotSlotsType[]) {
let index = 1;
@@ -46,7 +46,7 @@ export function useFormatAsEft() {
if (index > shipSnapshot.slots[slotType]) break;
index += 1;
const module = shipSnapshot.fit.items.find((item) => item.flag === flag);
const module = shipSnapshot.currentFit.items.find((item) => item.flag === flag);
if (module === undefined) {
eft += "[Empty " + slotToEft[slotType] + "]\n";
continue;

View File

@@ -19,19 +19,17 @@ export default meta;
type Story = StoryObj<typeof HardwareListing>;
const useShipSnapshotProvider: Decorator<Record<string, never>> = (Story, context) => {
const [skills, setSkills] = React.useState<Record<string, number>>({});
return (
<EveDataProvider>
<EsiProvider setSkills={setSkills}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot} skills={skills}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot}>
<EsiProvider>
<div style={{ width: context.args.width, height: context.args.width }}>
<Story />
</div>
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EsiProvider>
</EsiProvider>
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EveDataProvider>
);
};
@@ -40,7 +38,7 @@ export const Default: Story = {
decorators: [useShipSnapshotProvider],
parameters: {
snapshot: {
fit: fullFit,
initialFit: fullFit,
},
},
};

View File

@@ -22,17 +22,17 @@ type Story = StoryObj<typeof HullListing>;
const withEsiProvider: Decorator<Record<string, never>> = (Story, context) => {
return (
<EveDataProvider>
<EsiProvider setSkills={console.log}>
<LocalFitProvider>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot}>
<LocalFitProvider>
<EsiProvider>
<div style={{ height: "400px" }}>
<Story />
</div>
</ShipSnapshotProvider>
</DogmaEngineProvider>
</LocalFitProvider>
</EsiProvider>
</EsiProvider>
</LocalFitProvider>
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EveDataProvider>
);
};
@@ -42,8 +42,7 @@ export const Default: Story = {
decorators: [withEsiProvider],
parameters: {
snapshot: {
fit: fullFit,
skills: {},
initialFit: fullFit,
},
},
};

View File

@@ -222,7 +222,7 @@ export const HullListing = () => {
if (hull.marketGroupID === undefined) continue;
if (!hull.published) continue;
if (filter.currentHull && shipSnapShot.fit?.ship_type_id !== parseInt(typeId)) continue;
if (filter.currentHull && shipSnapShot.currentFit?.ship_type_id !== parseInt(typeId)) continue;
const fits: ListingFit[] = [];
if (anyFilter) {
@@ -231,7 +231,7 @@ export const HullListing = () => {
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;
if (!filter.currentHull || shipSnapShot.currentFit?.ship_type_id !== parseInt(typeId)) continue;
}
} else {
if (Object.keys(localCharacterFits).includes(typeId)) fits.push(...localCharacterFits[typeId]);
@@ -257,7 +257,7 @@ export const HullListing = () => {
}
setHullGroups(newHullGroups);
}, [eveData, search, filter, localCharacterFits, esiCharacterFits, shipSnapShot.fit?.ship_type_id]);
}, [eveData, search, filter, localCharacterFits, esiCharacterFits, shipSnapShot.currentFit?.ship_type_id]);
return (
<div className={styles.listing}>

View File

@@ -36,8 +36,7 @@ export const Default: Story = {
decorators: [withShipSnapshotProvider],
parameters: {
snapshot: {
fit: fullFit,
skills: {},
initialFit: fullFit,
},
},
};

View File

@@ -11,7 +11,7 @@ export interface ShipFitProps {
export const Hull = () => {
const shipSnapshot = React.useContext(ShipSnapshotContext);
const hull = shipSnapshot?.fit?.ship_type_id;
const hull = shipSnapshot?.currentFit?.ship_type_id;
if (hull === undefined) {
return <></>;
}

View File

@@ -38,8 +38,7 @@ export const Default: Story = {
decorators: [withShipSnapshotProvider],
parameters: {
snapshot: {
fit: fullFit,
skills: {},
initialFit: fullFit,
},
},
};

View File

@@ -19,19 +19,17 @@ export default meta;
type Story = StoryObj<typeof ShipFitExtended>;
const useShipSnapshotProvider: Decorator<Record<string, never>> = (Story, context) => {
const [skills, setSkills] = React.useState<Record<string, number>>({});
return (
<EveDataProvider>
<EsiProvider setSkills={setSkills}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot} skills={skills}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot}>
<EsiProvider>
<div style={{ width: context.args.width, height: context.args.width }}>
<Story />
</div>
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EsiProvider>
</EsiProvider>
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EveDataProvider>
);
};
@@ -43,7 +41,7 @@ export const Default: Story = {
decorators: [useShipSnapshotProvider],
parameters: {
snapshot: {
fit: fullFit,
initialFit: fullFit,
},
},
};

View File

@@ -74,7 +74,7 @@ const FitName = () => {
return (
<>
<div className={styles.fitNameTitle}>Name</div>
<div className={styles.fitNameContent}>{shipSnapshot?.fit?.name}</div>
<div className={styles.fitNameContent}>{shipSnapshot?.currentFit?.name}</div>
</>
);
};

View File

@@ -49,8 +49,7 @@ const TestShipSnapshot = () => {
export const Default: Story = {
args: {
fit: fullFit,
skills: {},
initialFit: fullFit,
},
render: (args) => (
<EveDataProvider>

View File

@@ -65,7 +65,8 @@ interface ShipSnapshot {
slots: ShipSnapshotSlots;
fit?: EsiFit;
currentFit?: EsiFit;
currentSkills?: Record<string, number>;
addModule: (typeId: number, slot: ShipSnapshotSlotsType | "dronebay") => void;
removeModule: (flag: number) => void;
@@ -77,6 +78,7 @@ interface ShipSnapshot {
changeFit: (fit: EsiFit) => void;
setItemState: (flag: number, state: string) => void;
setName: (name: string) => void;
changeSkills: (skills: Record<string, number>) => void;
}
export const ShipSnapshotContext = React.createContext<ShipSnapshot>({
@@ -98,6 +100,7 @@ export const ShipSnapshotContext = React.createContext<ShipSnapshot>({
changeFit: () => {},
setItemState: () => {},
setName: () => {},
changeSkills: () => {},
});
const slotStart: Record<ShipSnapshotSlotsType, number> = {
@@ -111,10 +114,10 @@ const slotStart: Record<ShipSnapshotSlotsType, number> = {
export interface ShipSnapshotProps {
/** Children that can use this provider. */
children: React.ReactNode;
/** A ship fit in ESI representation. */
fit: EsiFit;
/** A list of skills to apply to the fit: {skill_id: skill_level}. */
skills: Record<string, number>;
/** The initial fit to use. */
initialFit: EsiFit;
/** The initial skills to use. */
initialSkills?: Record<string, number>;
}
/**
@@ -122,6 +125,10 @@ export interface ShipSnapshotProps {
*/
export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
const eveData = React.useContext(EveDataContext);
const dogmaEngine = React.useContext(DogmaEngineContext);
const [currentFit, setCurrentFit] = React.useState<EsiFit>(props.initialFit);
const [currentSkills, setCurrentSkills] = React.useState<Record<string, number>>(props.initialSkills ?? {});
const [shipSnapshot, setShipSnapshot] = React.useState<ShipSnapshot>({
loaded: undefined,
slots: {
@@ -138,17 +145,14 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
toggleDrones: () => {},
removeDrones: () => {},
changeHull: () => {},
changeFit: () => {},
setItemState: () => {},
setName: () => {},
changeFit: setCurrentFit,
changeSkills: setCurrentSkills,
});
const [currentFit, setCurrentFit] = React.useState<EsiFit | undefined>(undefined);
const dogmaEngine = React.useContext(DogmaEngineContext);
const setItemState = React.useCallback((flag: number, state: string) => {
setCurrentFit((oldFit: EsiFit | undefined) => {
if (oldFit === undefined) return undefined;
setCurrentFit((oldFit: EsiFit) => {
return {
...oldFit,
items: oldFit?.items?.map((item) => {
@@ -166,9 +170,7 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
}, []);
const setName = React.useCallback((name: string) => {
setCurrentFit((oldFit: EsiFit | undefined) => {
if (oldFit === undefined) return undefined;
setCurrentFit((oldFit: EsiFit) => {
return {
...oldFit,
name: name,
@@ -178,9 +180,7 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
const addModule = React.useCallback(
(typeId: number, slot: ShipSnapshotSlotsType | "dronebay") => {
setCurrentFit((oldFit: EsiFit | undefined) => {
if (oldFit === undefined) return undefined;
setCurrentFit((oldFit: EsiFit) => {
let flag = 0;
/* Find the first free slot for that slot-type. */
@@ -215,9 +215,7 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
);
const removeModule = React.useCallback((flag: number) => {
setCurrentFit((oldFit: EsiFit | undefined) => {
if (oldFit === undefined) return undefined;
setCurrentFit((oldFit: EsiFit) => {
return {
...oldFit,
items: oldFit.items.filter((item) => item.flag !== flag),
@@ -233,9 +231,7 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
)?.value ?? -1;
const groupID = eveData.typeIDs?.[chargeTypeId]?.groupID ?? -1;
setCurrentFit((oldFit: EsiFit | undefined) => {
if (oldFit === undefined) return undefined;
setCurrentFit((oldFit: EsiFit) => {
const newItems = [];
for (let item of oldFit.items) {
@@ -281,9 +277,7 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
);
const removeCharge = React.useCallback((flag: number) => {
setCurrentFit((oldFit: EsiFit | undefined) => {
if (oldFit === undefined) return undefined;
setCurrentFit((oldFit: EsiFit) => {
return {
...oldFit,
items: oldFit.items.map((item) => {
@@ -301,9 +295,7 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
}, []);
const toggleDrones = React.useCallback((typeId: number, active: number) => {
setCurrentFit((oldFit: EsiFit | undefined) => {
if (oldFit === undefined) return undefined;
setCurrentFit((oldFit: EsiFit) => {
/* Find the amount of drones in the current fit. */
const count = oldFit.items
.filter((item) => item.flag === 87 && item.type_id === typeId)
@@ -352,9 +344,7 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
}, []);
const removeDrones = React.useCallback((typeId: number) => {
setCurrentFit((oldFit: EsiFit | undefined) => {
if (oldFit === undefined) return undefined;
setCurrentFit((oldFit: EsiFit) => {
return {
...oldFit,
items: oldFit.items.filter((item) => item.flag !== 87 || item.type_id !== typeId),
@@ -386,7 +376,6 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
toggleDrones,
removeDrones,
changeHull,
changeFit: setCurrentFit,
setItemState,
setName,
}));
@@ -424,10 +413,10 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
React.useEffect(() => {
if (!dogmaEngine.loaded || !eveData.loaded) return;
if (currentFit === undefined || props.skills === undefined) return;
if (currentFit === undefined || currentSkills === undefined) return;
const fit = fixupCharge(currentFit);
const snapshot = dogmaEngine.engine?.calculate(fit, props.skills);
const snapshot = dogmaEngine.engine?.calculate(fit, currentSkills);
const slots = {
hislot: 0,
@@ -461,14 +450,11 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
structure: snapshot.structure,
target: snapshot.target,
slots,
fit: currentFit,
currentFit: currentFit,
currentSkills: currentSkills,
};
});
}, [eveData, dogmaEngine, currentFit, fixupCharge, props.skills]);
React.useEffect(() => {
setCurrentFit(props.fit);
}, [props.fit]);
}, [eveData, dogmaEngine, currentFit, fixupCharge, currentSkills]);
return <ShipSnapshotContext.Provider value={shipSnapshot}>{props.children}</ShipSnapshotContext.Provider>;
};

View File

@@ -19,17 +19,15 @@ export default meta;
type Story = StoryObj<typeof ShipStatistics>;
const useShipSnapshotProvider: Decorator<Record<string, never>> = (Story, context) => {
const [skills, setSkills] = React.useState<Record<string, number>>({});
return (
<EveDataProvider>
<EsiProvider setSkills={setSkills}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot} skills={skills}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot}>
<EsiProvider>
<Story />
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EsiProvider>
</EsiProvider>
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EveDataProvider>
);
};
@@ -38,7 +36,7 @@ export const Default: Story = {
decorators: [useShipSnapshotProvider],
parameters: {
snapshot: {
fit: fullFit,
initialFit: fullFit,
},
},
};

View File

@@ -22,8 +22,7 @@ export const Default: Story = {
},
parameters: {
snapshot: {
fit: fullFit,
skills: {},
initialFit: fullFit,
},
},
};