feat: loading fits from a Browser's LocalStorage (#43)

You cannot save yet; but when you can, it loads fine.
This commit is contained in:
Patric Stout
2023-12-22 13:31:51 +01:00
committed by GitHub
parent fc97ab72f5
commit 44137e0235
7 changed files with 156 additions and 38 deletions

View File

@@ -8,6 +8,8 @@ import { getSkills } from "./EsiSkills";
import { getCharFittings } from "./EsiFittings";
import { EveDataContext } from "../EveDataProvider";
import { useLocalStorage } from "../Helpers/LocalStorage";
export interface EsiCharacter {
name: string;
skills?: Record<string, number>;
@@ -48,32 +50,6 @@ export interface EsiProps {
children: React.ReactNode;
}
const useLocalStorage = function <T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = React.useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
});
const setValue = React.useCallback((value: T | ((val: T) => T)) => {
if (typeof window === 'undefined') return;
if (storedValue == value) return;
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (valueToStore === undefined) {
window.localStorage.removeItem(key);
return;
}
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}, [key, storedValue]);
return [ storedValue, setValue ] as const;
}
/**
* Keeps track (in local storage) of ESI characters and their refresh token.
*/

View File

@@ -0,0 +1,27 @@
import React from "react";
export const useLocalStorage = function <T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = React.useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
});
const setValue = React.useCallback((value: T | ((val: T) => T)) => {
if (typeof window === 'undefined') return;
if (storedValue == value) return;
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (valueToStore === undefined) {
window.localStorage.removeItem(key);
return;
}
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}, [key, storedValue]);
return [ storedValue, setValue ] as const;
}

View File

@@ -4,10 +4,11 @@ import React from "react";
import { fullFit } from '../../.storybook/fits';
import { HullListing } from './';
import { DogmaEngineProvider } from '../DogmaEngineProvider';
import { EsiProvider } from '../EsiProvider';
import { EveDataProvider } from '../EveDataProvider';
import { LocalFitProvider } from '../LocalFitProvider';
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
import { DogmaEngineProvider } from '../DogmaEngineProvider';
const meta: Meta<typeof HullListing> = {
component: HullListing,
@@ -22,13 +23,15 @@ const withEsiProvider: Decorator<Record<string, never>> = (Story, context) => {
return (
<EveDataProvider>
<EsiProvider setSkills={console.log}>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot}>
<div style={{height: "400px"}}>
<Story />
</div>
</ShipSnapshotProvider>
</DogmaEngineProvider>
<LocalFitProvider>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot}>
<div style={{height: "400px"}}>
<Story />
</div>
</ShipSnapshotProvider>
</DogmaEngineProvider>
</LocalFitProvider>
</EsiProvider>
</EveDataProvider>
);

View File

@@ -6,6 +6,7 @@ import { EsiFit, ShipSnapshotContext } from "../ShipSnapshotProvider";
import { EveDataContext } from "../EveDataProvider";
import { Icon } from "../Icon";
import { TreeListing, TreeHeader, TreeHeaderAction, TreeLeaf } from "../TreeListing";
import { LocalFitContext } from "../LocalFitProvider";
import styles from "./HullListing.module.css";
@@ -93,18 +94,39 @@ const HullGroup = (props: { name: string, entries: ListingGroup }) => {
*/
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, EsiFit[]>>({});
const [esiCharacterFits, setEsiCharacterFits] = React.useState<Record<string, EsiFit[]>>({});
React.useEffect(() => {
if (!localFit.loaded) return;
if (!localFit.fittings) return;
const newLocalCharacterFits: Record<string, EsiFit[]> = {};
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(fit);
}
setLocalCharacterFits(newLocalCharacterFits);
}, [localFit]);
React.useEffect(() => {
if (!esi.loaded) return;
if (!esi.currentCharacter) return;
@@ -127,7 +149,7 @@ export const HullListing = () => {
React.useEffect(() => {
if (!eveData.loaded) return;
const anyFilter = filter.esiCharacter;
const anyFilter = filter.localCharacter || filter.esiCharacter;
const newHullGroups: ListingGroups = {};
@@ -141,11 +163,13 @@ export const HullListing = () => {
const fits: EsiFit[] = [];
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]);
}
@@ -168,15 +192,15 @@ export const HullListing = () => {
}
setHullGroups(newHullGroups);
}, [eveData, search, filter, esiCharacterFits, shipSnapShot.fit?.ship_type_id]);
}, [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={styles.disabled}>
<Icon name="fitting-local" size={32} title="Not yet implemented" />
<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" />

View File

@@ -0,0 +1,42 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from "react";
import { LocalFitContext, LocalFitProvider } from './';
const meta: Meta<typeof LocalFitProvider> = {
component: LocalFitProvider,
tags: ['autodocs'],
title: 'Provider/LocalFitProvider',
};
export default meta;
type Story = StoryObj<typeof LocalFitProvider>;
const TestLocalFit = () => {
const localFit = React.useContext(LocalFitContext);
if (!localFit.loaded) {
return (
<div>
LocalFit: loading<br/>
</div>
);
}
return (
<div>
LocalFit: loaded<br/>
<pre>{JSON.stringify(localFit, null, 2)}</pre>
</div>
);
}
export const Default: Story = {
args: {
},
render: (args) => (
<LocalFitProvider {...args}>
<TestLocalFit />
</LocalFitProvider>
),
};

View File

@@ -0,0 +1,44 @@
import React from "react";
import { EsiFit } from "../ShipSnapshotProvider";
import { useLocalStorage } from "../Helpers/LocalStorage";
export interface LocalFit {
loaded?: boolean;
fittings: EsiFit[];
}
export const LocalFitContext = React.createContext<LocalFit>({
loaded: undefined,
fittings: [],
});
export interface LocalFitProps {
/** Children that can use this provider. */
children: React.ReactNode;
}
/**
* Keeps track (in local storage) of fits.
*/
export const LocalFitProvider = (props: LocalFitProps) => {
const [localFit, setLocalFit] = React.useState<LocalFit>({
loaded: undefined,
fittings: [],
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [localFitValue, setLocalFitValue] = useLocalStorage<EsiFit[]>("fits", []);
React.useEffect(() => {
setLocalFit({
loaded: true,
fittings: localFitValue,
});
}, [localFitValue]);
return <LocalFitContext.Provider value={localFit}>
{props.children}
</LocalFitContext.Provider>
};

View File

@@ -0,0 +1,2 @@
export { LocalFitContext, LocalFitProvider } from "./LocalFitProvider";
export type { LocalFit } from "./LocalFitProvider";