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:
@@ -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.
|
||||
*/
|
||||
|
||||
27
src/Helpers/LocalStorage.tsx
Normal file
27
src/Helpers/LocalStorage.tsx
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
42
src/LocalFitProvider/LocalFitProvider.stories.tsx
Normal file
42
src/LocalFitProvider/LocalFitProvider.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
44
src/LocalFitProvider/LocalFitProvider.tsx
Normal file
44
src/LocalFitProvider/LocalFitProvider.tsx
Normal 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>
|
||||
};
|
||||
2
src/LocalFitProvider/index.ts
Normal file
2
src/LocalFitProvider/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { LocalFitContext, LocalFitProvider } from "./LocalFitProvider";
|
||||
export type { LocalFit } from "./LocalFitProvider";
|
||||
Reference in New Issue
Block a user