chore: apply prettier coding-style to the whole project (#63)
This commit is contained in:
2
.editorconfig
Normal file
2
.editorconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
[*]
|
||||
max_line_length=120
|
||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
src/EveDataProvider/esf_pb2.js
|
||||
src/EveDataProvider/protobuf.js
|
||||
@@ -9,7 +9,8 @@
|
||||
"build": "rollup -c",
|
||||
"build-storybook": "storybook build",
|
||||
"dev": "storybook dev -p 6006 --no-open",
|
||||
"lint": "eslint src --ext .js,.tsx --cache"
|
||||
"lint": "eslint src --ext .js,.tsx --cache && prettier -c src",
|
||||
"prettier": "prettier -w src"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
.entry {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
}
|
||||
.entry > span {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
.entry > span:first-child {
|
||||
display: inline-block;
|
||||
flex: unset;
|
||||
width: 20px;
|
||||
display: inline-block;
|
||||
flex: unset;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.entry > span:last-child {
|
||||
display: inline-block;
|
||||
flex: unset;
|
||||
width: 60px;
|
||||
display: inline-block;
|
||||
flex: unset;
|
||||
width: 60px;
|
||||
}
|
||||
.entry:hover {
|
||||
background-color: #cccccc;
|
||||
background-color: #cccccc;
|
||||
}
|
||||
|
||||
.header {
|
||||
cursor: inherit;
|
||||
font-weight: bold;
|
||||
cursor: inherit;
|
||||
font-weight: bold;
|
||||
}
|
||||
.header:hover {
|
||||
background-color: inherit;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.effects {
|
||||
background-color: #cccccc;
|
||||
padding: 10px 20px;
|
||||
background-color: #cccccc;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.effect {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
.effect > span:nth-child(3) {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
.effect > span:nth-child(1) {
|
||||
width: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
.effect > span:nth-child(2) {
|
||||
width: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.line:nth-child(odd) {
|
||||
background-color: #f2f2f2;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
import { fullFit } from "../../.storybook/fits";
|
||||
|
||||
import { DogmaEngineProvider } from '../DogmaEngineProvider';
|
||||
import { EsiProvider } from '../EsiProvider';
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
|
||||
import { CalculationDetail } from './';
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
import { EsiProvider } from "../EsiProvider";
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { ShipSnapshotProvider } from "../ShipSnapshotProvider";
|
||||
import { CalculationDetail } from "./";
|
||||
|
||||
const meta: Meta<typeof CalculationDetail> = {
|
||||
component: CalculationDetail,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/CalculationDetail',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/CalculationDetail",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CalculationDetail>;
|
||||
|
||||
const useShipSnapshotProvider: Decorator<{source: "Ship" | { Item: number }}> = (Story, context) => {
|
||||
const useShipSnapshotProvider: Decorator<{ source: "Ship" | { Item: number } }> = (Story, context) => {
|
||||
const [skills, setSkills] = React.useState<Record<string, number>>({});
|
||||
|
||||
return (
|
||||
@@ -32,7 +32,7 @@ const useShipSnapshotProvider: Decorator<{source: "Ship" | { Item: number }}> =
|
||||
</EsiProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
@@ -42,6 +42,6 @@ export const Default: Story = {
|
||||
parameters: {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,21 +2,25 @@ import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { EveDataContext } from "../EveDataProvider";
|
||||
import { ShipSnapshotContext, ShipSnapshotItemAttribute, ShipSnapshotItemAttributeEffect } from "../ShipSnapshotProvider";
|
||||
import {
|
||||
ShipSnapshotContext,
|
||||
ShipSnapshotItemAttribute,
|
||||
ShipSnapshotItemAttributeEffect,
|
||||
} from "../ShipSnapshotProvider";
|
||||
|
||||
import styles from "./CalculationDetail.module.css";
|
||||
import { Icon } from "../Icon";
|
||||
|
||||
const EffectOperatorOrder: Record<string, string> = {
|
||||
"PreAssign": "=",
|
||||
"PreMul": "*",
|
||||
"PreDiv": "/",
|
||||
"ModAdd": "+",
|
||||
"ModSub": "-",
|
||||
"PostMul": "*",
|
||||
"PostDiv": "/",
|
||||
"PostPercent": "%",
|
||||
"PostAssignment": "=",
|
||||
PreAssign: "=",
|
||||
PreMul: "*",
|
||||
PreDiv: "/",
|
||||
ModAdd: "+",
|
||||
ModSub: "-",
|
||||
PostMul: "*",
|
||||
PostDiv: "/",
|
||||
PostPercent: "%",
|
||||
PostAssignment: "=",
|
||||
};
|
||||
|
||||
const Effect = (props: { effect: ShipSnapshotItemAttributeEffect }) => {
|
||||
@@ -40,14 +44,21 @@ const Effect = (props: { effect: ShipSnapshotItemAttributeEffect }) => {
|
||||
}
|
||||
}
|
||||
|
||||
return <div className={styles.effect}>
|
||||
<span>{EffectOperatorOrder[props.effect.operator]}</span>
|
||||
<span>{attribute?.value || eveAttribute?.defaultValue}{props.effect.penalty ? " (penalized)" : ""}</span>
|
||||
<span>{sourceName} - {eveAttribute?.name}</span>
|
||||
</div>;
|
||||
}
|
||||
return (
|
||||
<div className={styles.effect}>
|
||||
<span>{EffectOperatorOrder[props.effect.operator]}</span>
|
||||
<span>
|
||||
{attribute?.value || eveAttribute?.defaultValue}
|
||||
{props.effect.penalty ? " (penalized)" : ""}
|
||||
</span>
|
||||
<span>
|
||||
{sourceName} - {eveAttribute?.name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CalculationDetailMeta = (props: { attributeId: number, attribute: ShipSnapshotItemAttribute }) => {
|
||||
const CalculationDetailMeta = (props: { attributeId: number; attribute: ShipSnapshotItemAttribute }) => {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const eveData = React.useContext(EveDataContext);
|
||||
|
||||
@@ -63,39 +74,41 @@ const CalculationDetailMeta = (props: { attributeId: number, attribute: ShipSnap
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
|
||||
return <div className={styles.line}>
|
||||
<div className={styles.entry} onClick={() => setExpanded(!expanded)}>
|
||||
<span>
|
||||
<Icon name={expanded ? "menu-expand" : "menu-collapse"} />
|
||||
</span>
|
||||
<span>{eveAttribute?.name}</span>
|
||||
<span>{props.attribute.value}</span>
|
||||
<span>{props.attribute.effects.length}</span>
|
||||
</div>
|
||||
<div className={clsx(styles.effects, { [styles.collapsed]: !expanded })}>
|
||||
<div className={styles.effect}>
|
||||
<span>=</span>
|
||||
<span>{props.attribute.base_value}</span>
|
||||
<span>base value {props.attributeId < 0 && <>(list of effects might be incomplete)</>}</span>
|
||||
return (
|
||||
<div className={styles.line}>
|
||||
<div className={styles.entry} onClick={() => setExpanded(!expanded)}>
|
||||
<span>
|
||||
<Icon name={expanded ? "menu-expand" : "menu-collapse"} />
|
||||
</span>
|
||||
<span>{eveAttribute?.name}</span>
|
||||
<span>{props.attribute.value}</span>
|
||||
<span>{props.attribute.effects.length}</span>
|
||||
</div>
|
||||
<div className={clsx(styles.effects, { [styles.collapsed]: !expanded })}>
|
||||
<div className={styles.effect}>
|
||||
<span>=</span>
|
||||
<span>{props.attribute.base_value}</span>
|
||||
<span>base value {props.attributeId < 0 && <>(list of effects might be incomplete)</>}</span>
|
||||
</div>
|
||||
{sortedEffects.map((effect) => {
|
||||
index += 1;
|
||||
return <Effect key={index} effect={effect} />;
|
||||
})}
|
||||
</div>
|
||||
{sortedEffects.map((effect) => {
|
||||
index += 1;
|
||||
return <Effect key={index} effect={effect} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show in detail for each attribute how the value came to be. This includes
|
||||
* the base value, all effects (and their source) and the final value.
|
||||
*/
|
||||
export const CalculationDetail = (props: {source: "Ship" | { Item: number }}) => {
|
||||
export const CalculationDetail = (props: { source: "Ship" | { Item: number } }) => {
|
||||
const shipSnapshot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
let attributes;
|
||||
if (props.source === "Ship") {
|
||||
attributes = [...shipSnapshot.hull?.attributes.entries() || []];
|
||||
attributes = [...(shipSnapshot.hull?.attributes.entries() || [])];
|
||||
} else if (props.source.Item !== undefined) {
|
||||
const item = shipSnapshot.items?.[props.source.Item];
|
||||
if (item !== undefined) {
|
||||
@@ -103,15 +116,17 @@ export const CalculationDetail = (props: {source: "Ship" | { Item: number }}) =>
|
||||
}
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div className={clsx(styles.entry, styles.header)}>
|
||||
<span></span>
|
||||
<span>Attribute</span>
|
||||
<span>Value</span>
|
||||
<span>Effects</span>
|
||||
return (
|
||||
<div>
|
||||
<div className={clsx(styles.entry, styles.header)}>
|
||||
<span></span>
|
||||
<span>Attribute</span>
|
||||
<span>Value</span>
|
||||
<span>Effects</span>
|
||||
</div>
|
||||
{attributes?.map(([attributeId, attribute]) => {
|
||||
return <CalculationDetailMeta key={attributeId} attributeId={attributeId} attribute={attribute} />;
|
||||
})}
|
||||
</div>
|
||||
{attributes?.map(([attributeId, attribute]) => {
|
||||
return <CalculationDetailMeta key={attributeId} attributeId={attributeId} attribute={attribute} />
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
import { fullFit } from "../../.storybook/fits";
|
||||
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { DogmaEngineContext, DogmaEngineProvider } from './';
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { DogmaEngineContext, DogmaEngineProvider } from "./";
|
||||
|
||||
const meta: Meta<typeof DogmaEngineProvider> = {
|
||||
component: DogmaEngineProvider,
|
||||
tags: ['autodocs'],
|
||||
title: 'Provider/DogmaEngineProvider',
|
||||
tags: ["autodocs"],
|
||||
title: "Provider/DogmaEngineProvider",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -18,9 +18,7 @@ type Story = StoryObj<typeof DogmaEngineProvider>;
|
||||
/** Convert an ES6 map to an Object, which JSON can stringify. */
|
||||
function MapToDict(_key: string, value: unknown) {
|
||||
if (value instanceof Map) {
|
||||
return Array.from(value.entries()).reduce((obj, [key, item]) => (
|
||||
Object.assign(obj, { [key]: item })
|
||||
), {});
|
||||
return Array.from(value.entries()).reduce((obj, [key, item]) => Object.assign(obj, { [key]: item }), {});
|
||||
}
|
||||
|
||||
return value;
|
||||
@@ -34,18 +32,20 @@ const TestDogmaEngine = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
DogmaEngine: loaded<br/>
|
||||
DogmaEngine: loaded
|
||||
<br />
|
||||
Stats: {JSON.stringify(stats, MapToDict)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
DogmaEngine: loading<br/>
|
||||
DogmaEngine: loading
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import React from "react";
|
||||
|
||||
import { DogmaAttribute, DogmaEffect, TypeDogmaAttribute, TypeDogmaEffect, TypeID, EveDataContext } from "../EveDataProvider";
|
||||
import {
|
||||
DogmaAttribute,
|
||||
DogmaEffect,
|
||||
TypeDogmaAttribute,
|
||||
TypeDogmaEffect,
|
||||
TypeID,
|
||||
EveDataContext,
|
||||
} from "../EveDataProvider";
|
||||
import type { init, calculate } from "@eveshipfit/dogma-engine";
|
||||
|
||||
interface EsfDogmaEngine {
|
||||
init: typeof init,
|
||||
calculate: typeof calculate,
|
||||
init: typeof init;
|
||||
calculate: typeof calculate;
|
||||
}
|
||||
|
||||
interface DogmaEngine {
|
||||
loaded?: boolean,
|
||||
loadedData?: boolean,
|
||||
engine?: EsfDogmaEngine,
|
||||
loaded?: boolean;
|
||||
loadedData?: boolean;
|
||||
engine?: EsfDogmaEngine;
|
||||
}
|
||||
|
||||
export const DogmaEngineContext = React.createContext<DogmaEngine>({});
|
||||
@@ -86,7 +93,7 @@ export const DogmaEngineProvider = (props: DogmaEngineProps) => {
|
||||
window.get_dogma_effects = undefined;
|
||||
window.get_dogma_effect = undefined;
|
||||
window.get_type_id = undefined;
|
||||
}
|
||||
};
|
||||
}, [eveData]);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -103,7 +110,5 @@ export const DogmaEngineProvider = (props: DogmaEngineProps) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <DogmaEngineContext.Provider value={dogmaEngine}>
|
||||
{props.children}
|
||||
</DogmaEngineContext.Provider>
|
||||
return <DogmaEngineContext.Provider value={dogmaEngine}>{props.children}</DogmaEngineContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
.character {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.character > select {
|
||||
background-color: #1d1d1d;
|
||||
color: #c5c5c5;
|
||||
height: 24px;
|
||||
padding-left: 5px;
|
||||
width: calc(100% - 20px);
|
||||
background-color: #1d1d1d;
|
||||
color: #c5c5c5;
|
||||
height: 24px;
|
||||
padding-left: 5px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.character > button {
|
||||
background-color: #1d1d1d;
|
||||
color: #c5c5c5;
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
background-color: #1d1d1d;
|
||||
color: #c5c5c5;
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.character > button.noCharacter {
|
||||
text-align: left;
|
||||
padding-left: 5px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding-left: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { EsiProvider } from '../EsiProvider';
|
||||
import { EsiCharacterSelection } from './';
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { EsiProvider } from "../EsiProvider";
|
||||
import { EsiCharacterSelection } from "./";
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
|
||||
const meta: Meta<typeof EsiCharacterSelection> = {
|
||||
component: EsiCharacterSelection,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/EsiCharacterSelection',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/EsiCharacterSelection",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -22,10 +22,9 @@ const withEsiProvider: Decorator<Record<string, never>> = (Story) => {
|
||||
</EsiProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
},
|
||||
args: {},
|
||||
decorators: [withEsiProvider],
|
||||
};
|
||||
|
||||
@@ -13,14 +13,22 @@ import styles from "./EsiCharacterSelection.module.css";
|
||||
export const EsiCharacterSelection = () => {
|
||||
const esi = React.useContext(EsiContext);
|
||||
|
||||
return <div className={styles.character}>
|
||||
<select onChange={e => esi.changeCharacter(e.target.value)} value={esi.currentCharacter}>
|
||||
{Object.entries(esi.characters).sort().map(([id, name]) => {
|
||||
return <option key={id} value={id}>{name.name}</option>
|
||||
})}
|
||||
</select>
|
||||
<button onClick={esi.login} title="Add another character">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.character}>
|
||||
<select onChange={(e) => esi.changeCharacter(e.target.value)} value={esi.currentCharacter}>
|
||||
{Object.entries(esi.characters)
|
||||
.sort()
|
||||
.map(([id, name]) => {
|
||||
return (
|
||||
<option key={id} value={id}>
|
||||
{name.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<button onClick={esi.login} title="Add another character">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export async function getAccessToken(refreshToken: string): Promise<{ accessToken?: string, refreshToken?: string }> {
|
||||
export async function getAccessToken(refreshToken: string): Promise<{ accessToken?: string; refreshToken?: string }> {
|
||||
let response;
|
||||
try {
|
||||
response = await fetch('https://esi.eveship.fit/', {
|
||||
method: 'POST',
|
||||
response = await fetch("https://esi.eveship.fit/", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
@@ -17,4 +17,4 @@ export async function getAccessToken(refreshToken: string): Promise<{ accessToke
|
||||
|
||||
const data = await response.json();
|
||||
return { accessToken: data.access_token, refreshToken: data.refresh_token };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export async function getCharFittings(characterId: string, accessToken: string):
|
||||
response = await fetch(`https://esi.evetech.net/v1/characters/${characterId}/fittings/`, {
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'content-type': 'application/json',
|
||||
"content-type": "application/json",
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { EsiContext, EsiProvider } from './';
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { EsiContext, EsiProvider } from "./";
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
|
||||
const meta: Meta<typeof EsiProvider> = {
|
||||
component: EsiProvider,
|
||||
tags: ['autodocs'],
|
||||
title: 'Provider/EsiProvider',
|
||||
tags: ["autodocs"],
|
||||
title: "Provider/EsiProvider",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -19,22 +19,26 @@ const TestEsi = () => {
|
||||
if (!esi.loaded) {
|
||||
return (
|
||||
<div>
|
||||
Esi: loading<br/>
|
||||
Esi: loading
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
Esi: loaded<br/>
|
||||
Esi: loaded
|
||||
<br />
|
||||
<pre>{JSON.stringify(esi, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
setSkills: (skills: Record<string, number>) => { console.log(skills); }
|
||||
setSkills: (skills: Record<string, number>) => {
|
||||
console.log(skills);
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<EveDataProvider>
|
||||
|
||||
@@ -68,54 +68,60 @@ export const EsiProvider = (props: EsiProps) => {
|
||||
accessTokens: {},
|
||||
});
|
||||
|
||||
const [characters, setCharacters] = useLocalStorage<Record<string, EsiCharacter>>('characters', {});
|
||||
const [refreshTokens, setRefreshTokens] = useLocalStorage('refreshTokens', {});
|
||||
const [currentCharacter, setCurrentCharacter] = useLocalStorage<string | undefined>('currentCharacter', undefined);
|
||||
const [characters, setCharacters] = useLocalStorage<Record<string, EsiCharacter>>("characters", {});
|
||||
const [refreshTokens, setRefreshTokens] = useLocalStorage("refreshTokens", {});
|
||||
const [currentCharacter, setCurrentCharacter] = useLocalStorage<string | undefined>("currentCharacter", undefined);
|
||||
|
||||
const changeCharacter = React.useCallback((character: string) => {
|
||||
setCurrentCharacter(character);
|
||||
const changeCharacter = React.useCallback(
|
||||
(character: string) => {
|
||||
setCurrentCharacter(character);
|
||||
|
||||
setEsi((oldEsi: Esi) => {
|
||||
return {
|
||||
...oldEsi,
|
||||
currentCharacter: character,
|
||||
};
|
||||
});
|
||||
}, [setCurrentCharacter]);
|
||||
setEsi((oldEsi: Esi) => {
|
||||
return {
|
||||
...oldEsi,
|
||||
currentCharacter: character,
|
||||
};
|
||||
});
|
||||
},
|
||||
[setCurrentCharacter],
|
||||
);
|
||||
|
||||
const login = React.useCallback(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (typeof window === "undefined") return;
|
||||
window.location.href = "https://esi.eveship.fit/";
|
||||
}, []);
|
||||
|
||||
const ensureAccessToken = React.useCallback(async (characterId: string): Promise<string | undefined> => {
|
||||
if (esiPrivate.accessTokens[characterId]) {
|
||||
return esiPrivate.accessTokens[characterId];
|
||||
}
|
||||
const ensureAccessToken = React.useCallback(
|
||||
async (characterId: string): Promise<string | undefined> => {
|
||||
if (esiPrivate.accessTokens[characterId]) {
|
||||
return esiPrivate.accessTokens[characterId];
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken } = await getAccessToken(esiPrivate.refreshTokens[characterId]);
|
||||
if (accessToken === undefined || refreshToken === undefined) {
|
||||
console.log('Failed to get access token');
|
||||
return undefined;
|
||||
}
|
||||
const { accessToken, refreshToken } = await getAccessToken(esiPrivate.refreshTokens[characterId]);
|
||||
if (accessToken === undefined || refreshToken === undefined) {
|
||||
console.log("Failed to get access token");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/* New access token; store for later use. */
|
||||
setEsiPrivate((oldEsiPrivate: EsiPrivate) => {
|
||||
return {
|
||||
...oldEsiPrivate,
|
||||
refreshTokens: {
|
||||
...oldEsiPrivate.refreshTokens,
|
||||
[characterId]: refreshToken,
|
||||
},
|
||||
accessToken: {
|
||||
...oldEsiPrivate.accessTokens,
|
||||
[characterId]: accessToken,
|
||||
},
|
||||
};
|
||||
});
|
||||
/* New access token; store for later use. */
|
||||
setEsiPrivate((oldEsiPrivate: EsiPrivate) => {
|
||||
return {
|
||||
...oldEsiPrivate,
|
||||
refreshTokens: {
|
||||
...oldEsiPrivate.refreshTokens,
|
||||
[characterId]: refreshToken,
|
||||
},
|
||||
accessToken: {
|
||||
...oldEsiPrivate.accessTokens,
|
||||
[characterId]: accessToken,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return accessToken;
|
||||
}, [esiPrivate.accessTokens, esiPrivate.refreshTokens]);
|
||||
return accessToken;
|
||||
},
|
||||
[esiPrivate.accessTokens, esiPrivate.refreshTokens],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!eveData.loaded) return;
|
||||
@@ -129,8 +135,8 @@ export const EsiProvider = (props: EsiProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (characterId === '.all-0' || characterId === '.all-5') {
|
||||
const level = characterId === '.all-0' ? 0 : 5;
|
||||
if (characterId === ".all-0" || characterId === ".all-5") {
|
||||
const level = characterId === ".all-0" ? 0 : 5;
|
||||
|
||||
const skills: Record<string, number> = {};
|
||||
for (const typeId in eveData.typeIDs) {
|
||||
@@ -208,13 +214,13 @@ export const EsiProvider = (props: EsiProps) => {
|
||||
}, [esi.currentCharacter, eveData.loaded]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
async function loginCharacter(code: string) {
|
||||
let response;
|
||||
try {
|
||||
response = await fetch('https://esi.eveship.fit/', {
|
||||
method: 'POST',
|
||||
response = await fetch("https://esi.eveship.fit/", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
code: code,
|
||||
}),
|
||||
@@ -289,11 +295,11 @@ export const EsiProvider = (props: EsiProps) => {
|
||||
|
||||
async function startup() {
|
||||
const charactersDefault = {
|
||||
'.all-0': {
|
||||
name: 'Default character - All Skills L0',
|
||||
".all-0": {
|
||||
name: "Default character - All Skills L0",
|
||||
},
|
||||
'.all-5': {
|
||||
name: 'Default character - All Skills L5',
|
||||
".all-5": {
|
||||
name: "Default character - All Skills L5",
|
||||
},
|
||||
...characters,
|
||||
};
|
||||
@@ -313,13 +319,13 @@ export const EsiProvider = (props: EsiProps) => {
|
||||
|
||||
/* Check if this was a login request. */
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const code = urlParams.get("code");
|
||||
if (code) {
|
||||
/* Remove the code from the URL. */
|
||||
window.history.replaceState(null, "", window.location.pathname + window.location.hash);
|
||||
|
||||
if (!await loginCharacter(code)) {
|
||||
console.log('Failed to login character');
|
||||
if (!(await loginCharacter(code))) {
|
||||
console.log("Failed to login character");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -330,7 +336,5 @@ export const EsiProvider = (props: EsiProps) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return <EsiContext.Provider value={esi}>
|
||||
{props.children}
|
||||
</EsiContext.Provider>
|
||||
return <EsiContext.Provider value={esi}>{props.children}</EsiContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
|
||||
export async function getSkills(characterId: string, accessToken: string): Promise<Record<string, number> | undefined> {
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(`https://esi.evetech.net/v4/characters/${characterId}/skills/`, {
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'content-type': 'application/json',
|
||||
"content-type": "application/json",
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,76 +1,75 @@
|
||||
|
||||
export interface TypeDogmaAttribute {
|
||||
attributeID: number,
|
||||
value: number,
|
||||
attributeID: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface TypeDogmaEffect {
|
||||
effectID: number,
|
||||
isDefault: boolean,
|
||||
effectID: number;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface TypeDogma {
|
||||
dogmaAttributes: TypeDogmaAttribute[],
|
||||
dogmaEffects: TypeDogmaEffect[],
|
||||
dogmaAttributes: TypeDogmaAttribute[];
|
||||
dogmaEffects: TypeDogmaEffect[];
|
||||
}
|
||||
|
||||
export interface TypeID {
|
||||
name: string,
|
||||
groupID: number,
|
||||
categoryID: number,
|
||||
published: boolean,
|
||||
factionID?: number,
|
||||
marketGroupID?: number,
|
||||
metaGroupID?: number,
|
||||
capacity?: number,
|
||||
mass?: number,
|
||||
radius?: number,
|
||||
volume?: number,
|
||||
name: string;
|
||||
groupID: number;
|
||||
categoryID: number;
|
||||
published: boolean;
|
||||
factionID?: number;
|
||||
marketGroupID?: number;
|
||||
metaGroupID?: number;
|
||||
capacity?: number;
|
||||
mass?: number;
|
||||
radius?: number;
|
||||
volume?: number;
|
||||
}
|
||||
|
||||
export interface GroupID {
|
||||
name: string,
|
||||
categoryID: number,
|
||||
published: boolean,
|
||||
name: string;
|
||||
categoryID: number;
|
||||
published: boolean;
|
||||
}
|
||||
|
||||
export interface MarketGroup {
|
||||
name: string,
|
||||
parentGroupID?: number,
|
||||
iconID?: number,
|
||||
name: string;
|
||||
parentGroupID?: number;
|
||||
iconID?: number;
|
||||
}
|
||||
|
||||
export interface DogmaAttribute {
|
||||
name: string
|
||||
published: boolean,
|
||||
defaultValue: number,
|
||||
highIsGood: boolean,
|
||||
stackable: boolean,
|
||||
name: string;
|
||||
published: boolean;
|
||||
defaultValue: number;
|
||||
highIsGood: boolean;
|
||||
stackable: boolean;
|
||||
}
|
||||
|
||||
export interface DogmaEffect {
|
||||
name: string,
|
||||
effectCategory: number,
|
||||
electronicChance: boolean,
|
||||
isAssistance: boolean,
|
||||
isOffensive: boolean,
|
||||
isWarpSafe: boolean,
|
||||
propulsionChance: boolean,
|
||||
rangeChance: boolean,
|
||||
dischargeAttributeID?: number,
|
||||
durationAttributeID?: number,
|
||||
rangeAttributeID?: number,
|
||||
falloffAttributeID?: number,
|
||||
trackingSpeedAttributeID?: number,
|
||||
fittingUsageChanceAttributeID?: number,
|
||||
resistanceAttributeID?: number,
|
||||
name: string;
|
||||
effectCategory: number;
|
||||
electronicChance: boolean;
|
||||
isAssistance: boolean;
|
||||
isOffensive: boolean;
|
||||
isWarpSafe: boolean;
|
||||
propulsionChance: boolean;
|
||||
rangeChance: boolean;
|
||||
dischargeAttributeID?: number;
|
||||
durationAttributeID?: number;
|
||||
rangeAttributeID?: number;
|
||||
falloffAttributeID?: number;
|
||||
trackingSpeedAttributeID?: number;
|
||||
fittingUsageChanceAttributeID?: number;
|
||||
resistanceAttributeID?: number;
|
||||
modifierInfo: {
|
||||
domain: number,
|
||||
func: number,
|
||||
modifiedAttributeID?: number,
|
||||
modifyingAttributeID?: number,
|
||||
operation?: number,
|
||||
groupID?: number,
|
||||
skillTypeID?: number,
|
||||
}[],
|
||||
domain: number;
|
||||
func: number;
|
||||
modifiedAttributeID?: number;
|
||||
modifyingAttributeID?: number;
|
||||
operation?: number;
|
||||
groupID?: number;
|
||||
skillTypeID?: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { EveDataContext, EveDataProvider } from './';
|
||||
import { EveDataContext, EveDataProvider } from "./";
|
||||
|
||||
const meta: Meta<typeof EveDataProvider> = {
|
||||
component: EveDataProvider,
|
||||
tags: ['autodocs'],
|
||||
title: 'Provider/EveDataProvider',
|
||||
tags: ["autodocs"],
|
||||
title: "Provider/EveDataProvider",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -17,18 +17,25 @@ const TestEveData = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
TypeIDs: {eveData.typeIDs ? Object.keys(eveData.typeIDs).length : "loading"}<br/>
|
||||
GroupIDs: {eveData.groupIDs ? Object.keys(eveData.groupIDs).length : "loading"}<br/>
|
||||
MarketGroups: {eveData.marketGroups ? Object.keys(eveData.marketGroups).length : "loading"}<br/>
|
||||
TypeDogma: {eveData.typeDogma ? Object.keys(eveData.typeDogma).length : "loading"}<br/>
|
||||
DogmaEffects: {eveData.dogmaEffects ? Object.keys(eveData.dogmaEffects).length : "loading"}<br/>
|
||||
DogmaAttributes: {eveData.dogmaAttributes ? Object.keys(eveData.dogmaAttributes).length : "loading"}<br/>
|
||||
AttributeMapper: {eveData.attributeMapping ? Object.keys(eveData.attributeMapping).length : "loading"}<br/>
|
||||
<br/>
|
||||
TypeIDs: {eveData.typeIDs ? Object.keys(eveData.typeIDs).length : "loading"}
|
||||
<br />
|
||||
GroupIDs: {eveData.groupIDs ? Object.keys(eveData.groupIDs).length : "loading"}
|
||||
<br />
|
||||
MarketGroups: {eveData.marketGroups ? Object.keys(eveData.marketGroups).length : "loading"}
|
||||
<br />
|
||||
TypeDogma: {eveData.typeDogma ? Object.keys(eveData.typeDogma).length : "loading"}
|
||||
<br />
|
||||
DogmaEffects: {eveData.dogmaEffects ? Object.keys(eveData.dogmaEffects).length : "loading"}
|
||||
<br />
|
||||
DogmaAttributes: {eveData.dogmaAttributes ? Object.keys(eveData.dogmaAttributes).length : "loading"}
|
||||
<br />
|
||||
AttributeMapper: {eveData.attributeMapping ? Object.keys(eveData.attributeMapping).length : "loading"}
|
||||
<br />
|
||||
<br />
|
||||
All loaded: {eveData.loaded ? "yes" : "no"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
|
||||
@@ -76,8 +76,8 @@ export const EveDataProvider = (props: DogmaDataProps) => {
|
||||
|
||||
newDogmaData.loaded = isLoaded(newDogmaData);
|
||||
return newDogmaData;
|
||||
}
|
||||
)});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fetchAndLoadDataFile("typeIDs", esf_pb2.esf.TypeIDs);
|
||||
@@ -102,14 +102,12 @@ export const EveDataProvider = (props: DogmaDataProps) => {
|
||||
const newDogmaData = {
|
||||
...prevDogmaData,
|
||||
attributeMapping: attributeMapping,
|
||||
}
|
||||
};
|
||||
|
||||
newDogmaData.loaded = isLoaded(newDogmaData);
|
||||
return newDogmaData;
|
||||
});
|
||||
}, [dogmaData.dogmaAttributes]);
|
||||
|
||||
return <EveDataContext.Provider value={dogmaData}>
|
||||
{props.children}
|
||||
</EveDataContext.Provider>
|
||||
return <EveDataContext.Provider value={dogmaData}>{props.children}</EveDataContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { hashFit } from '../../.storybook/fits';
|
||||
import { hashFit } from "../../.storybook/fits";
|
||||
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { EveShipFitHash } from './EveShipFitHash';
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { EveShipFitHash } from "./EveShipFitHash";
|
||||
|
||||
const meta: Meta<typeof EveShipFitHash> = {
|
||||
component: EveShipFitHash,
|
||||
tags: ['autodocs'],
|
||||
title: 'Function/EveShipFitHash',
|
||||
tags: ["autodocs"],
|
||||
title: "Function/EveShipFitHash",
|
||||
};
|
||||
|
||||
const withEveDataProvider: Decorator<{fitHash: string}> = (Story) => {
|
||||
const withEveDataProvider: Decorator<{ fitHash: string }> = (Story) => {
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<Story />
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EveShipFitHash>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
"fitHash": hashFit,
|
||||
fitHash: hashFit,
|
||||
},
|
||||
decorators: [withEveDataProvider],
|
||||
};
|
||||
|
||||
@@ -3,16 +3,16 @@ import React from "react";
|
||||
import { EsiFit } from "../ShipSnapshotProvider";
|
||||
|
||||
async function decompress(base64compressedBytes: string): Promise<string> {
|
||||
const stream = new Blob([Uint8Array.from(atob(base64compressedBytes), c => c.charCodeAt(0))]).stream();
|
||||
const stream = new Blob([Uint8Array.from(atob(base64compressedBytes), (c) => c.charCodeAt(0))]).stream();
|
||||
const decompressedStream = stream.pipeThrough(new DecompressionStream("gzip"));
|
||||
const reader = decompressedStream.getReader();
|
||||
|
||||
let result = "";
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
result += String.fromCharCode.apply(null, value);
|
||||
result += String.fromCharCode.apply(null, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -80,5 +80,5 @@ export const EveShipFitHash = (props: EveShipFitHashProps) => {
|
||||
getFit(props.fitHash);
|
||||
}, [props.fitHash]);
|
||||
|
||||
return <pre>{JSON.stringify(esiFit, null, 2)}</pre>
|
||||
return <pre>{JSON.stringify(esiFit, null, 2)}</pre>;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
import { fullFit } from "../../.storybook/fits";
|
||||
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { DogmaEngineProvider } from '../DogmaEngineProvider';
|
||||
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
|
||||
import { EveShipFitLink } from './EveShipFitLink';
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
import { ShipSnapshotProvider } from "../ShipSnapshotProvider";
|
||||
import { EveShipFitLink } from "./EveShipFitLink";
|
||||
|
||||
const meta: Meta<typeof EveShipFitLink> = {
|
||||
component: EveShipFitLink,
|
||||
tags: ['autodocs'],
|
||||
title: 'Function/EveShipFitLink',
|
||||
tags: ["autodocs"],
|
||||
title: "Function/EveShipFitLink",
|
||||
};
|
||||
|
||||
const withShipSnapshotProvider: Decorator<{ radius?: number }> = (Story, context) => {
|
||||
@@ -24,20 +24,18 @@ const withShipSnapshotProvider: Decorator<{ radius?: number }> = (Story, context
|
||||
</DogmaEngineProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EveShipFitLink>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
},
|
||||
args: {},
|
||||
decorators: [withShipSnapshotProvider],
|
||||
parameters: {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
skills: {},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,10 +9,10 @@ async function compress(str: string): Promise<string> {
|
||||
|
||||
let result = "";
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
result += String.fromCharCode.apply(null, value);
|
||||
result += String.fromCharCode.apply(null, value);
|
||||
}
|
||||
|
||||
return btoa(result);
|
||||
@@ -25,7 +25,7 @@ async function encodeEsiFit(esiFit: EsiFit): Promise<string> {
|
||||
result += `${item.flag},${item.type_id},${item.quantity}\n`;
|
||||
}
|
||||
|
||||
return "v1:" + await compress(result);
|
||||
return "v1:" + (await compress(result));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +51,7 @@ export function useEveShipFitLink() {
|
||||
}, [shipSnapshot?.loaded, shipSnapshot?.fit]);
|
||||
|
||||
return fitLink;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* useEveShipFitLink() converts the current fit into a link to https://eveship.fit.
|
||||
@@ -61,5 +61,5 @@ export function useEveShipFitLink() {
|
||||
export const EveShipFitLink = () => {
|
||||
const eveShipFitLink = useEveShipFitLink();
|
||||
|
||||
return <pre>{eveShipFitLink}</pre>
|
||||
return <pre>{eveShipFitLink}</pre>;
|
||||
};
|
||||
|
||||
@@ -51,35 +51,42 @@ export const ClipboardButton = () => {
|
||||
setIsPopupOpen(false);
|
||||
}, [eftToEsiFit, shipSnapshot]);
|
||||
|
||||
return <>
|
||||
<div className={styles.popupButton} onMouseOver={() => setIsPopupOpen(true)} onMouseOut={() => setIsPopupOpen(false)}>
|
||||
<div className={styles.button}>
|
||||
{copied ? "In Clipboard" : "Clipboard"}
|
||||
</div>
|
||||
<div className={clsx(styles.popup, {[styles.collapsed]: !isPopupOpen})}>
|
||||
<div>
|
||||
<div className={styles.button} onClick={() => setIsPasteOpen(true)}>
|
||||
Import from Clipboard
|
||||
</div>
|
||||
<div className={clsx(styles.button, styles.buttonMax)} onClick={() => copyToClipboard()}>
|
||||
Copy to Clipboard
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={styles.popupButton}
|
||||
onMouseOver={() => setIsPopupOpen(true)}
|
||||
onMouseOut={() => setIsPopupOpen(false)}
|
||||
>
|
||||
<div className={styles.button}>{copied ? "In Clipboard" : "Clipboard"}</div>
|
||||
<div className={clsx(styles.popup, { [styles.collapsed]: !isPopupOpen })}>
|
||||
<div>
|
||||
<div className={styles.button} onClick={() => setIsPasteOpen(true)}>
|
||||
Import from Clipboard
|
||||
</div>
|
||||
<div className={clsx(styles.button, styles.buttonMax)} onClick={() => copyToClipboard()}>
|
||||
Copy to Clipboard
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalDialog visible={isPasteOpen} onClose={() => setIsPasteOpen(false)} className={styles.paste} title="Import from Clipboard">
|
||||
<div>
|
||||
<ModalDialog
|
||||
visible={isPasteOpen}
|
||||
onClose={() => setIsPasteOpen(false)}
|
||||
className={styles.paste}
|
||||
title="Import from Clipboard"
|
||||
>
|
||||
<div>
|
||||
Paste your fit here
|
||||
<div>Paste your fit here</div>
|
||||
<div>
|
||||
<textarea autoFocus className={styles.pasteTextarea} ref={textAreaRef} />
|
||||
</div>
|
||||
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => importFromClipboard()}>
|
||||
Import
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<textarea autoFocus className={styles.pasteTextarea} ref={textAreaRef} />
|
||||
</div>
|
||||
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => importFromClipboard()}>
|
||||
Import
|
||||
</span>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
</>
|
||||
}
|
||||
</ModalDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,110 +1,108 @@
|
||||
|
||||
.fitButtonBar {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fitButtonBar > div {
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #321d1d;
|
||||
border: 1px solid #9f462f;
|
||||
color: #c5c5c5;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
padding: 0 20px;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
background-color: #321d1d;
|
||||
border: 1px solid #9f462f;
|
||||
color: #c5c5c5;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
padding: 0 20px;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.buttonSmall {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.buttonMax {
|
||||
text-align: center;
|
||||
width: calc(100% - 40px - 2px);
|
||||
text-align: center;
|
||||
width: calc(100% - 40px - 2px);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #864735;
|
||||
border-color: #c87d5e;
|
||||
background-color: #864735;
|
||||
border-color: #c87d5e;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popupButton {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popup {
|
||||
bottom: 40px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
bottom: 40px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
.popup:before {
|
||||
background-color: #111111;
|
||||
border-bottom: 1px solid #303030;
|
||||
border-right: 1px solid #303030;
|
||||
bottom: 5px;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 10px;
|
||||
left: 30px;
|
||||
position: absolute;
|
||||
transform: rotate(45deg);
|
||||
width: 10px;
|
||||
background-color: #111111;
|
||||
border-bottom: 1px solid #303030;
|
||||
border-right: 1px solid #303030;
|
||||
bottom: 5px;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 10px;
|
||||
left: 30px;
|
||||
position: absolute;
|
||||
transform: rotate(45deg);
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.popup > div {
|
||||
background-color: #111111;
|
||||
border: 1px solid #303030;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background-color: #111111;
|
||||
border: 1px solid #303030;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.popup > div > .button {
|
||||
margin-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.popup > div > .button:first-child {
|
||||
margin-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.renameEdit {
|
||||
margin-right: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.alreadyExists {
|
||||
max-width: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.alreadyExistsButtons {
|
||||
margin-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.alreadyExistsButtons .button {
|
||||
margin-right: 10px;
|
||||
text-align: center;
|
||||
width: calc(50% - 48px);
|
||||
margin-right: 10px;
|
||||
text-align: center;
|
||||
width: calc(50% - 48px);
|
||||
}
|
||||
.alreadyExistsButtons .button:last-child {
|
||||
margin-right: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.paste .button {
|
||||
text-align: center;
|
||||
width: calc(100% - 42px);
|
||||
text-align: center;
|
||||
width: calc(100% - 42px);
|
||||
}
|
||||
.pasteTextarea {
|
||||
height: 60px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 4px;
|
||||
width: 300px;
|
||||
height: 60px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 4px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
import { fullFit } from "../../.storybook/fits";
|
||||
|
||||
import { DogmaEngineProvider } from '../DogmaEngineProvider';
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { LocalFitProvider } from '../LocalFitProvider';
|
||||
import { ModalDialogAnchor } from '../ModalDialog/ModalDialog';
|
||||
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { LocalFitProvider } from "../LocalFitProvider";
|
||||
import { ModalDialogAnchor } from "../ModalDialog/ModalDialog";
|
||||
import { ShipSnapshotProvider } from "../ShipSnapshotProvider";
|
||||
|
||||
import { FitButtonBar } from './';
|
||||
import { FitButtonBar } from "./";
|
||||
|
||||
const meta: Meta<typeof FitButtonBar> = {
|
||||
component: FitButtonBar,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/FitButtonBar',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/FitButtonBar",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -26,7 +26,7 @@ const withEveDataProvider: Decorator<Record<string, never>> = (Story, context) =
|
||||
<DogmaEngineProvider>
|
||||
<LocalFitProvider>
|
||||
<ShipSnapshotProvider {...context.parameters.snapshot}>
|
||||
<div style={{marginTop: "100px"}}>
|
||||
<div style={{ marginTop: "100px" }}>
|
||||
<ModalDialogAnchor />
|
||||
<Story />
|
||||
</div>
|
||||
@@ -35,7 +35,7 @@ const withEveDataProvider: Decorator<Record<string, never>> = (Story, context) =
|
||||
</DogmaEngineProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
decorators: [withEveDataProvider],
|
||||
@@ -43,6 +43,6 @@ export const Default: Story = {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
skills: {},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,10 +11,12 @@ import styles from "./FitButtonBar.module.css";
|
||||
* Bar with buttons to load/save fits.
|
||||
*/
|
||||
export const FitButtonBar = () => {
|
||||
return <div className={styles.fitButtonBar}>
|
||||
<SaveButton />
|
||||
<ClipboardButton />
|
||||
<ShareButton />
|
||||
<RenameButton />
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.fitButtonBar}>
|
||||
<SaveButton />
|
||||
<ClipboardButton />
|
||||
<ShareButton />
|
||||
<RenameButton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,20 +22,22 @@ export const RenameButton = () => {
|
||||
setIsRenameOpen(true);
|
||||
}, [shipSnapshot]);
|
||||
|
||||
return <>
|
||||
<div className={styles.button} onClick={() => openRename()}>
|
||||
Rename
|
||||
</div>
|
||||
|
||||
<ModalDialog visible={isRenameOpen} onClose={() => setIsRenameOpen(false)} title="Fit Name">
|
||||
<div>
|
||||
<span className={styles.renameEdit}>
|
||||
<input type="text" autoFocus value={rename} onChange={(e) => setRename(e.target.value)} />
|
||||
</span>
|
||||
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => saveRename()}>
|
||||
Save
|
||||
</span>
|
||||
return (
|
||||
<>
|
||||
<div className={styles.button} onClick={() => openRename()}>
|
||||
Rename
|
||||
</div>
|
||||
</ModalDialog>
|
||||
</>
|
||||
}
|
||||
|
||||
<ModalDialog visible={isRenameOpen} onClose={() => setIsRenameOpen(false)} title="Fit Name">
|
||||
<div>
|
||||
<span className={styles.renameEdit}>
|
||||
<input type="text" autoFocus value={rename} onChange={(e) => setRename(e.target.value)} />
|
||||
</span>
|
||||
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => saveRename()}>
|
||||
Save
|
||||
</span>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,54 +14,64 @@ export const SaveButton = () => {
|
||||
const [isPopupOpen, setIsPopupOpen] = React.useState(false);
|
||||
const [isAlreadyExistsOpen, setIsAlreadyExistsOpen] = React.useState(false);
|
||||
|
||||
const saveBrowser = React.useCallback((force?: boolean) => {
|
||||
if (!localFit.loaded) return;
|
||||
if (!shipSnapshot.loaded || !shipSnapshot?.fit) return;
|
||||
const saveBrowser = React.useCallback(
|
||||
(force?: boolean) => {
|
||||
if (!localFit.loaded) return;
|
||||
if (!shipSnapshot.loaded || !shipSnapshot?.fit) return;
|
||||
|
||||
setIsPopupOpen(false);
|
||||
setIsPopupOpen(false);
|
||||
|
||||
if (!force) {
|
||||
for (const fit of localFit.fittings) {
|
||||
if (fit.name === shipSnapshot.fit.name) {
|
||||
setIsAlreadyExistsOpen(true);
|
||||
return;
|
||||
if (!force) {
|
||||
for (const fit of localFit.fittings) {
|
||||
if (fit.name === shipSnapshot.fit.name) {
|
||||
setIsAlreadyExistsOpen(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setIsAlreadyExistsOpen(false);
|
||||
setIsAlreadyExistsOpen(false);
|
||||
|
||||
localFit.addFit(shipSnapshot.fit);
|
||||
}, [localFit, shipSnapshot]);
|
||||
localFit.addFit(shipSnapshot.fit);
|
||||
},
|
||||
[localFit, shipSnapshot],
|
||||
);
|
||||
|
||||
return <>
|
||||
<div className={styles.popupButton} onMouseOver={() => setIsPopupOpen(true)} onMouseOut={() => setIsPopupOpen(false)}>
|
||||
<div className={styles.button}>
|
||||
Save
|
||||
</div>
|
||||
<div className={clsx(styles.popup, {[styles.collapsed]: !isPopupOpen})}>
|
||||
<div>
|
||||
<div className={styles.button} onClick={() => saveBrowser()}>
|
||||
Save in Browser
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={styles.popupButton}
|
||||
onMouseOver={() => setIsPopupOpen(true)}
|
||||
onMouseOut={() => setIsPopupOpen(false)}
|
||||
>
|
||||
<div className={styles.button}>Save</div>
|
||||
<div className={clsx(styles.popup, { [styles.collapsed]: !isPopupOpen })}>
|
||||
<div>
|
||||
<div className={styles.button} onClick={() => saveBrowser()}>
|
||||
Save in Browser
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalDialog visible={isAlreadyExistsOpen} onClose={() => setIsAlreadyExistsOpen(false)} className={styles.alreadyExists} title="Update Fitting?">
|
||||
<div>
|
||||
<ModalDialog
|
||||
visible={isAlreadyExistsOpen}
|
||||
onClose={() => setIsAlreadyExistsOpen(false)}
|
||||
className={styles.alreadyExists}
|
||||
title="Update Fitting?"
|
||||
>
|
||||
<div>
|
||||
You have a fitting with the name {shipSnapshot?.fit?.name}, do you want to update it?
|
||||
<div>You have a fitting with the name {shipSnapshot?.fit?.name}, do you want to update it?</div>
|
||||
<div className={styles.alreadyExistsButtons}>
|
||||
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => saveBrowser(true)}>
|
||||
Yes
|
||||
</span>
|
||||
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => setIsAlreadyExistsOpen(false)}>
|
||||
No
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.alreadyExistsButtons}>
|
||||
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => saveBrowser(true)}>
|
||||
Yes
|
||||
</span>
|
||||
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => setIsAlreadyExistsOpen(false)}>
|
||||
No
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
</>
|
||||
}
|
||||
</ModalDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,16 +9,21 @@ export const ShareButton = () => {
|
||||
const link = useEveShipFitLink();
|
||||
const { copy, copied } = useClipboard();
|
||||
|
||||
const onClick = React.useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
copy(link);
|
||||
}, [copy, link]);
|
||||
const onClick = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
copy(link);
|
||||
},
|
||||
[copy, link],
|
||||
);
|
||||
|
||||
return <>
|
||||
<div className={styles.popupButton}>
|
||||
<div className={styles.button} onClick={onClick}>
|
||||
{copied ? "In Clipboard" : "Share Link"}
|
||||
return (
|
||||
<>
|
||||
<div className={styles.popupButton}>
|
||||
<div className={styles.button} onClick={onClick}>
|
||||
{copied ? "In Clipboard" : "Share Link"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
import { fullFit } from "../../.storybook/fits";
|
||||
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { FormatAsEft } from './FormatAsEft';
|
||||
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
|
||||
import { DogmaEngineProvider } from '../DogmaEngineProvider';
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { FormatAsEft } from "./FormatAsEft";
|
||||
import { ShipSnapshotProvider } from "../ShipSnapshotProvider";
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
|
||||
const meta: Meta<typeof FormatAsEft> = {
|
||||
component: FormatAsEft,
|
||||
tags: ['autodocs'],
|
||||
title: 'Function/FormatAsEft',
|
||||
tags: ["autodocs"],
|
||||
title: "Function/FormatAsEft",
|
||||
};
|
||||
|
||||
const withEveDataProvider: Decorator<Record<string, never>> = (Story, context) => {
|
||||
@@ -24,7 +24,7 @@ const withEveDataProvider: Decorator<Record<string, never>> = (Story, context) =
|
||||
</DogmaEngineProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FormatAsEft>;
|
||||
@@ -35,6 +35,6 @@ export const Default: Story = {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
skills: {},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
import React from "react";
|
||||
|
||||
import { EveDataContext } from '../EveDataProvider';
|
||||
import { EveDataContext } from "../EveDataProvider";
|
||||
import { ShipSnapshotContext, ShipSnapshotSlotsType } from "../ShipSnapshotProvider";
|
||||
|
||||
/** Mapping between slot types and ESI flags (for first slot in the type). */
|
||||
const esiFlagMapping: Record<ShipSnapshotSlotsType, number[]> = {
|
||||
"lowslot": [
|
||||
11, 12, 13, 14, 15, 16, 17, 18
|
||||
],
|
||||
"medslot": [
|
||||
19, 20, 21, 22, 23, 24, 25, 26
|
||||
],
|
||||
"hislot": [
|
||||
27, 28, 29, 30, 31, 32, 33, 34
|
||||
],
|
||||
"rig": [
|
||||
92, 93, 94
|
||||
],
|
||||
"subsystem": [
|
||||
125, 126, 127, 128
|
||||
],
|
||||
lowslot: [11, 12, 13, 14, 15, 16, 17, 18],
|
||||
medslot: [19, 20, 21, 22, 23, 24, 25, 26],
|
||||
hislot: [27, 28, 29, 30, 31, 32, 33, 34],
|
||||
rig: [92, 93, 94],
|
||||
subsystem: [125, 126, 127, 128],
|
||||
};
|
||||
|
||||
/** Mapping between slot-type and the EFT string name. */
|
||||
const slotToEft: Record<ShipSnapshotSlotsType, string> = {
|
||||
"lowslot": "Low Slot",
|
||||
"medslot": "Mid Slot",
|
||||
"hislot": "High Slot",
|
||||
"rig": "Rig Slot",
|
||||
"subsystem": "Subsystem Slot",
|
||||
lowslot: "Low Slot",
|
||||
medslot: "Mid Slot",
|
||||
hislot: "High Slot",
|
||||
rig: "Rig Slot",
|
||||
subsystem: "Subsystem Slot",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -76,7 +66,7 @@ export function useFormatAsEft() {
|
||||
|
||||
return eft;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* useFormatAsEft() converts the current fit to an EFT string.
|
||||
@@ -86,5 +76,5 @@ export function useFormatAsEft() {
|
||||
export const FormatAsEft = () => {
|
||||
const toEft = useFormatAsEft();
|
||||
|
||||
return <pre>{toEft()}</pre>
|
||||
return <pre>{toEft()}</pre>;
|
||||
};
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { eftFit } from '../../.storybook/fits';
|
||||
import { eftFit } from "../../.storybook/fits";
|
||||
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { FormatEftToEsi } from './FormatEftToEsi';
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { FormatEftToEsi } from "./FormatEftToEsi";
|
||||
|
||||
const meta: Meta<typeof FormatEftToEsi> = {
|
||||
component: FormatEftToEsi,
|
||||
tags: ['autodocs'],
|
||||
title: 'Function/FormatEftToEsi',
|
||||
tags: ["autodocs"],
|
||||
title: "Function/FormatEftToEsi",
|
||||
};
|
||||
|
||||
const withEveDataProvider: Decorator<{eft: string}> = (Story) => {
|
||||
const withEveDataProvider: Decorator<{ eft: string }> = (Story) => {
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<Story />
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FormatEftToEsi>;
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import React from "react";
|
||||
|
||||
import { EveDataContext } from '../EveDataProvider';
|
||||
import { EveDataContext } from "../EveDataProvider";
|
||||
import { EsiFit } from "../ShipSnapshotProvider";
|
||||
|
||||
/** Mapping between slot types and ESI flags (for first slot in the type). */
|
||||
const esiFlagMapping: Record<string, number[]> = {
|
||||
"lowslot": [
|
||||
11, 12, 13, 14, 15, 16, 17, 18
|
||||
],
|
||||
"medslot": [
|
||||
19, 20, 21, 22, 23, 24, 25, 26
|
||||
],
|
||||
"hislot": [
|
||||
27, 28, 29, 30, 31, 32, 33, 34
|
||||
],
|
||||
"rig": [
|
||||
92, 93, 94
|
||||
],
|
||||
"subsystem": [
|
||||
125, 126, 127, 128
|
||||
],
|
||||
lowslot: [11, 12, 13, 14, 15, 16, 17, 18],
|
||||
medslot: [19, 20, 21, 22, 23, 24, 25, 26],
|
||||
hislot: [27, 28, 29, 30, 31, 32, 33, 34],
|
||||
rig: [92, 93, 94],
|
||||
subsystem: [125, 126, 127, 128],
|
||||
};
|
||||
|
||||
/** Mapping between dogma effect IDs and slot types. */
|
||||
@@ -72,11 +62,11 @@ export function useFormatEftToEsi() {
|
||||
esiFit.name = lines[0].split(",")[1].slice(0, -1).trim();
|
||||
|
||||
const slotIndex: Record<string, number> = {
|
||||
"lowslot": 0,
|
||||
"medslot": 0,
|
||||
"hislot": 0,
|
||||
"rig": 0,
|
||||
"subsystem": 0,
|
||||
lowslot: 0,
|
||||
medslot: 0,
|
||||
hislot: 0,
|
||||
rig: 0,
|
||||
subsystem: 0,
|
||||
};
|
||||
|
||||
let lastSlotType = "";
|
||||
@@ -110,13 +100,17 @@ export function useFormatEftToEsi() {
|
||||
/* Ignore items we don't care about. */
|
||||
if (!slotType) continue;
|
||||
|
||||
esiFit.items.push({"flag": esiFlagMapping[slotType][slotIndex[slotType]], "quantity": itemCount, "type_id": itemTypeId});
|
||||
esiFit.items.push({
|
||||
flag: esiFlagMapping[slotType][slotIndex[slotType]],
|
||||
quantity: itemCount,
|
||||
type_id: itemTypeId,
|
||||
});
|
||||
slotIndex[slotType]++;
|
||||
}
|
||||
|
||||
return esiFit;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface FormatEftToEsiProps {
|
||||
/** The EFT string. */
|
||||
@@ -131,5 +125,5 @@ export interface FormatEftToEsiProps {
|
||||
export const FormatEftToEsi = (props: FormatEftToEsiProps) => {
|
||||
const esiFit = useFormatEftToEsi();
|
||||
|
||||
return <pre>{JSON.stringify(esiFit(props.eft), null, 2)}</pre>
|
||||
return <pre>{JSON.stringify(esiFit(props.eft), null, 2)}</pre>;
|
||||
};
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
.listing {
|
||||
background-color: #111111;
|
||||
color: #c5c5c5;
|
||||
font-size: 15px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #111111;
|
||||
color: #c5c5c5;
|
||||
font-size: 15px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.listingContent {
|
||||
height: calc(100% - 42px - 32px - 5px);
|
||||
overflow-y: auto;
|
||||
padding-right: 20px;
|
||||
height: calc(100% - 42px - 32px - 5px);
|
||||
overflow-y: auto;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.topbar > input {
|
||||
background-color: #1d1d1d;
|
||||
color: #c5c5c5;
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
margin: 6px 0;
|
||||
padding-left: 6px;
|
||||
background-color: #1d1d1d;
|
||||
color: #c5c5c5;
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
margin: 6px 0;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.filter > span {
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
width: 32px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
width: 32px;
|
||||
}
|
||||
.filter > span:hover {
|
||||
background-color: #4f4f4f;
|
||||
background-color: #4f4f4f;
|
||||
}
|
||||
.filter > span.selected {
|
||||
background-color: #7a7a7a;
|
||||
background-color: #7a7a7a;
|
||||
}
|
||||
|
||||
.filter > span.disabled {
|
||||
color: #7a7a7a;
|
||||
cursor: default;
|
||||
color: #7a7a7a;
|
||||
cursor: default;
|
||||
}
|
||||
.filter > span.disabled:hover {
|
||||
background-color: #111111;
|
||||
background-color: #111111;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
|
||||
import { HardwareListing } from './';
|
||||
import { HardwareListing } from "./";
|
||||
|
||||
const meta: Meta<typeof HardwareListing> = {
|
||||
component: HardwareListing,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/HardwareListing',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/HardwareListing",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -17,12 +17,12 @@ type Story = StoryObj<typeof HardwareListing>;
|
||||
const withEveDataProvider: Decorator<Record<string, never>> = (Story) => {
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<div style={{height: "400px"}}>
|
||||
<div style={{ height: "400px" }}>
|
||||
<Story />
|
||||
</div>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
decorators: [withEveDataProvider],
|
||||
|
||||
@@ -24,21 +24,43 @@ interface ListingGroup {
|
||||
items: ListingItem[];
|
||||
}
|
||||
|
||||
const ModuleGroup = (props: { level: number, group: ListingGroup }) => {
|
||||
const ModuleGroup = (props: { level: number; group: ListingGroup }) => {
|
||||
const shipSnapShot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
const getChildren = React.useCallback(() => {
|
||||
return <>
|
||||
{props.group.items.sort((a, b) => a.meta - b.meta || a.name.localeCompare(b.name)).map((item) => {
|
||||
return <TreeLeaf key={item.typeId} level={2} content={item.name} onClick={() => shipSnapShot.addModule(item.typeId, item.slotType)} />;
|
||||
})}
|
||||
{Object.keys(props.group.groups).sort((a, b) => props.group.groups[a].meta - props.group.groups[b].meta || props.group.groups[a].name.localeCompare(props.group.groups[b].name)).map((groupId) => {
|
||||
return <ModuleGroup key={groupId} level={props.level + 1} group={props.group.groups[groupId]} />
|
||||
})}
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{props.group.items
|
||||
.sort((a, b) => a.meta - b.meta || a.name.localeCompare(b.name))
|
||||
.map((item) => {
|
||||
return (
|
||||
<TreeLeaf
|
||||
key={item.typeId}
|
||||
level={2}
|
||||
content={item.name}
|
||||
onClick={() => shipSnapShot.addModule(item.typeId, item.slotType)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{Object.keys(props.group.groups)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
props.group.groups[a].meta - props.group.groups[b].meta ||
|
||||
props.group.groups[a].name.localeCompare(props.group.groups[b].name),
|
||||
)
|
||||
.map((groupId) => {
|
||||
return <ModuleGroup key={groupId} level={props.level + 1} group={props.group.groups[groupId]} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}, [props, shipSnapShot]);
|
||||
|
||||
const header = <TreeHeader icon={props.group.iconID === undefined ? "" : `${defaultDataUrl}icons/${props.group.iconID}.png`} text={props.group.name} />;
|
||||
const header = (
|
||||
<TreeHeader
|
||||
icon={props.group.iconID === undefined ? "" : `${defaultDataUrl}icons/${props.group.iconID}.png`}
|
||||
text={props.group.name}
|
||||
/>
|
||||
);
|
||||
return <TreeListing level={props.level} header={header} getChildren={getChildren} />;
|
||||
};
|
||||
|
||||
@@ -76,19 +98,27 @@ export const HardwareListing = () => {
|
||||
for (const typeId in eveData.typeIDs) {
|
||||
const module = eveData.typeIDs[typeId];
|
||||
/* Modules (7), Drones (18), Subsystems (32), and Structures (66) */
|
||||
if (module.categoryID !== 7 && module.categoryID !== 18 && module.categoryID !== 32 && module.categoryID !== 66) continue;
|
||||
if (module.categoryID !== 7 && module.categoryID !== 18 && module.categoryID !== 32 && module.categoryID !== 66)
|
||||
continue;
|
||||
if (module.marketGroupID === undefined) continue;
|
||||
if (!module.published) continue;
|
||||
|
||||
let slotType: ShipSnapshotSlotsType | "dronebay" | undefined = eveData.typeDogma?.[typeId]?.dogmaEffects.map((effect) => {
|
||||
switch (effect.effectID) {
|
||||
case 11: return "lowslot";
|
||||
case 13: return "medslot";
|
||||
case 12: return "hislot";
|
||||
case 2663: return "rig";
|
||||
case 3772: return "subsystem";
|
||||
}
|
||||
}).filter((slot) => slot !== undefined)[0];
|
||||
let slotType: ShipSnapshotSlotsType | "dronebay" | undefined = eveData.typeDogma?.[typeId]?.dogmaEffects
|
||||
.map((effect) => {
|
||||
switch (effect.effectID) {
|
||||
case 11:
|
||||
return "lowslot";
|
||||
case 13:
|
||||
return "medslot";
|
||||
case 12:
|
||||
return "hislot";
|
||||
case 2663:
|
||||
return "rig";
|
||||
case 3772:
|
||||
return "subsystem";
|
||||
}
|
||||
})
|
||||
.filter((slot) => slot !== undefined)[0];
|
||||
if (module.categoryID === 18) {
|
||||
slotType = "dronebay";
|
||||
}
|
||||
@@ -191,31 +221,50 @@ export const HardwareListing = () => {
|
||||
setModuleGroups(newModuleGroups);
|
||||
}, [eveData, search, filter]);
|
||||
|
||||
return <div className={styles.listing}>
|
||||
<div className={styles.topbar}>
|
||||
<input type="text" placeholder="Search" value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
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.lowslot })}
|
||||
onClick={() => setFilter({ ...filter, lowslot: !filter.lowslot })}
|
||||
>
|
||||
<Icon name="fitting-lowslot" size={32} title="Filter: Low Slot" />
|
||||
</span>
|
||||
<span
|
||||
className={clsx({ [styles.selected]: filter.medslot })}
|
||||
onClick={() => setFilter({ ...filter, medslot: !filter.medslot })}
|
||||
>
|
||||
<Icon name="fitting-medslot" size={32} title="Filter: Mid Slot" />
|
||||
</span>
|
||||
<span
|
||||
className={clsx({ [styles.selected]: filter.hislot })}
|
||||
onClick={() => setFilter({ ...filter, hislot: !filter.hislot })}
|
||||
>
|
||||
<Icon name="fitting-hislot" size={32} title="Filter: High Slot" />
|
||||
</span>
|
||||
<span
|
||||
className={clsx({ [styles.selected]: filter.rig_subsystem })}
|
||||
onClick={() => setFilter({ ...filter, rig_subsystem: !filter.rig_subsystem })}
|
||||
>
|
||||
<Icon name="fitting-rig-subsystem" size={32} title="Filter: Rig & Subsystem Slots" />
|
||||
</span>
|
||||
<span
|
||||
className={clsx({ [styles.selected]: filter.drone })}
|
||||
onClick={() => setFilter({ ...filter, drone: !filter.drone })}
|
||||
>
|
||||
<Icon name="fitting-drones" size={32} title="Filter: Drones" />
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.listingContent}>
|
||||
{Object.keys(moduleGroups.groups)
|
||||
.sort((a, b) => moduleGroups.groups[a].name.localeCompare(moduleGroups.groups[b].name))
|
||||
.map((groupId) => {
|
||||
return <ModuleGroup key={groupId} level={1} group={moduleGroups.groups[groupId]} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.filter}>
|
||||
<span className={clsx({[styles.selected]: filter.lowslot})} onClick={() => setFilter({...filter, lowslot: !filter.lowslot})}>
|
||||
<Icon name="fitting-lowslot" size={32} title="Filter: Low Slot" />
|
||||
</span>
|
||||
<span className={clsx({[styles.selected]: filter.medslot})} onClick={() => setFilter({...filter, medslot: !filter.medslot})}>
|
||||
<Icon name="fitting-medslot" size={32} title="Filter: Mid Slot" />
|
||||
</span>
|
||||
<span className={clsx({[styles.selected]: filter.hislot})} onClick={() => setFilter({...filter, hislot: !filter.hislot})}>
|
||||
<Icon name="fitting-hislot" size={32} title="Filter: High Slot" />
|
||||
</span>
|
||||
<span className={clsx({[styles.selected]: filter.rig_subsystem})} onClick={() => setFilter({...filter, rig_subsystem: !filter.rig_subsystem})}>
|
||||
<Icon name="fitting-rig-subsystem" size={32} title="Filter: Rig & Subsystem Slots" />
|
||||
</span>
|
||||
<span className={clsx({[styles.selected]: filter.drone})} onClick={() => setFilter({...filter, drone: !filter.drone})}>
|
||||
<Icon name="fitting-drones" size={32} title="Filter: Drones" />
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.listingContent}>
|
||||
{Object.keys(moduleGroups.groups).sort((a, b) => moduleGroups.groups[a].name.localeCompare(moduleGroups.groups[b].name)).map((groupId) => {
|
||||
return <ModuleGroup key={groupId} level={1} group={moduleGroups.groups[groupId]} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,26 +2,29 @@ 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;
|
||||
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 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);
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
|
||||
if (valueToStore === undefined) {
|
||||
window.localStorage.removeItem(key);
|
||||
return;
|
||||
}
|
||||
if (valueToStore === undefined) {
|
||||
window.localStorage.removeItem(key);
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
}, [key, storedValue]);
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
},
|
||||
[key, storedValue],
|
||||
);
|
||||
|
||||
return [ storedValue, setValue ] as const;
|
||||
}
|
||||
return [storedValue, setValue] as const;
|
||||
};
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
.listing {
|
||||
background-color: #111111;
|
||||
color: #c5c5c5;
|
||||
font-size: 15px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #111111;
|
||||
color: #c5c5c5;
|
||||
font-size: 15px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.listingContent {
|
||||
height: calc(100% - 42px - 32px - 5px);
|
||||
padding-right: 20px;
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 42px - 32px - 5px);
|
||||
padding-right: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.topbar > input {
|
||||
background-color: #1d1d1d;
|
||||
color: #c5c5c5;
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
margin: 6px 0;
|
||||
padding-left: 6px;
|
||||
background-color: #1d1d1d;
|
||||
color: #c5c5c5;
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
margin: 6px 0;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.filter > span {
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
width: 32px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
width: 32px;
|
||||
}
|
||||
.filter > span:hover {
|
||||
background-color: #4f4f4f;
|
||||
background-color: #4f4f4f;
|
||||
}
|
||||
.filter > span.selected {
|
||||
background-color: #7a7a7a;
|
||||
background-color: #7a7a7a;
|
||||
}
|
||||
|
||||
.filter > span.disabled {
|
||||
color: #7a7a7a;
|
||||
cursor: default;
|
||||
color: #7a7a7a;
|
||||
cursor: default;
|
||||
}
|
||||
.filter > span.disabled:hover {
|
||||
background-color: #111111;
|
||||
background-color: #111111;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
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 { HullListing } from "./";
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
import { EsiProvider } from "../EsiProvider";
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { LocalFitProvider } from "../LocalFitProvider";
|
||||
import { ShipSnapshotProvider } from "../ShipSnapshotProvider";
|
||||
|
||||
const meta: Meta<typeof HullListing> = {
|
||||
component: HullListing,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/HullListing',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/HullListing",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -26,7 +26,7 @@ const withEsiProvider: Decorator<Record<string, never>> = (Story, context) => {
|
||||
<LocalFitProvider>
|
||||
<DogmaEngineProvider>
|
||||
<ShipSnapshotProvider {...context.parameters.snapshot}>
|
||||
<div style={{height: "400px"}}>
|
||||
<div style={{ height: "400px" }}>
|
||||
<Story />
|
||||
</div>
|
||||
</ShipSnapshotProvider>
|
||||
@@ -35,16 +35,15 @@ const withEsiProvider: Decorator<Record<string, never>> = (Story, context) => {
|
||||
</EsiProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
},
|
||||
args: {},
|
||||
decorators: [withEsiProvider],
|
||||
parameters: {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
skills: {},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ const factionIdToRace: Record<number, string> = {
|
||||
1: "Non-Empire",
|
||||
} as const;
|
||||
|
||||
const Hull = (props: { typeId: number, entry: ListingHull }) => {
|
||||
const Hull = (props: { typeId: number; entry: ListingHull }) => {
|
||||
const shipSnapShot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
const getChildren = React.useCallback(() => {
|
||||
@@ -48,61 +48,98 @@ const Hull = (props: { typeId: number, entry: ListingHull }) => {
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
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} />;
|
||||
})}</>;
|
||||
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 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} />;
|
||||
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 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} />
|
||||
})}</>;
|
||||
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}]`} />;
|
||||
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 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} />
|
||||
</>;
|
||||
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}`} />;
|
||||
@@ -143,7 +180,7 @@ export const HullListing = () => {
|
||||
|
||||
newLocalCharacterFits[fit.ship_type_id].push({
|
||||
origin: "local",
|
||||
fit
|
||||
fit,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -189,8 +226,10 @@ export const HullListing = () => {
|
||||
|
||||
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 (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;
|
||||
}
|
||||
@@ -214,38 +253,51 @@ export const HullListing = () => {
|
||||
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)} />
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Icon } from './';
|
||||
import { Icon } from "./";
|
||||
|
||||
const meta: Meta<typeof Icon> = {
|
||||
component: Icon,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/Icon',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/Icon",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -24,7 +24,7 @@ const iconMapping = {
|
||||
"hull-repair-rate": "texture/classes/fitting/statsicons/hullrepairrate.png",
|
||||
"inertia-modifier": "texture/classes/fitting/statsicons/inertiamodifier.png",
|
||||
"kinetic-resistance": "texture/classes/fitting/statsicons/kineticresistance.png",
|
||||
"mass": "texture/classes/fitting/statsicons/mass.png",
|
||||
mass: "texture/classes/fitting/statsicons/mass.png",
|
||||
"maximum-locked-targets": "texture/classes/fitting/statsicons/maximumlockedtargets.png",
|
||||
"menu-collapse": "texture/shared/triangleright.png",
|
||||
"menu-expand": "texture/shared/triangledown.png",
|
||||
@@ -34,7 +34,7 @@ const iconMapping = {
|
||||
"shield-boost-rate": "texture/classes/fitting/statsicons/shieldboostrate.png",
|
||||
"shield-hp": "texture/classes/fitting/statsicons/shieldhp.png",
|
||||
"signature-radius": "texture/classes/fitting/statsicons/signatureradius.png",
|
||||
"simulate": "texture/classes/fitting/iconsimulatorhover.png",
|
||||
simulate: "texture/classes/fitting/iconsimulatorhover.png",
|
||||
"thermal-resistance": "texture/classes/fitting/statsicons/thermalresistance.png",
|
||||
"warp-speed": "texture/classes/fitting/statsicons/warpspeed.png",
|
||||
} as const;
|
||||
@@ -58,5 +58,5 @@ export const Icon = (props: IconProps) => {
|
||||
if (icon === undefined) {
|
||||
return <span>Unknown icon: {props.name}</span>;
|
||||
}
|
||||
return <img src={`${defaultDataUrl}ui/${icon}`} width={props.size} title={props.title} />
|
||||
return <img src={`${defaultDataUrl}ui/${icon}`} width={props.size} title={props.title} />;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { LocalFitContext, LocalFitProvider } from './';
|
||||
import { LocalFitContext, LocalFitProvider } from "./";
|
||||
|
||||
const meta: Meta<typeof LocalFitProvider> = {
|
||||
component: LocalFitProvider,
|
||||
tags: ['autodocs'],
|
||||
title: 'Provider/LocalFitProvider',
|
||||
tags: ["autodocs"],
|
||||
title: "Provider/LocalFitProvider",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -18,22 +18,23 @@ const TestLocalFit = () => {
|
||||
if (!localFit.loaded) {
|
||||
return (
|
||||
<div>
|
||||
LocalFit: loading<br/>
|
||||
LocalFit: loading
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
LocalFit: loaded<br/>
|
||||
LocalFit: loaded
|
||||
<br />
|
||||
<pre>{JSON.stringify(localFit, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
},
|
||||
args: {},
|
||||
render: (args) => (
|
||||
<LocalFitProvider {...args}>
|
||||
<TestLocalFit />
|
||||
|
||||
@@ -33,13 +33,16 @@ export const LocalFitProvider = (props: LocalFitProps) => {
|
||||
|
||||
const [localFitValue, setLocalFitValue] = useLocalStorage<EsiFit[]>("fits", []);
|
||||
|
||||
const addFit = React.useCallback((fit: EsiFit) => {
|
||||
setLocalFitValue((oldFits) => {
|
||||
const newFits = oldFits.filter((oldFit) => oldFit.name !== fit.name);
|
||||
newFits.push(fit);
|
||||
return newFits;
|
||||
});
|
||||
}, [setLocalFitValue]);
|
||||
const addFit = React.useCallback(
|
||||
(fit: EsiFit) => {
|
||||
setLocalFitValue((oldFits) => {
|
||||
const newFits = oldFits.filter((oldFit) => oldFit.name !== fit.name);
|
||||
newFits.push(fit);
|
||||
return newFits;
|
||||
});
|
||||
},
|
||||
[setLocalFitValue],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLocalFit({
|
||||
@@ -49,7 +52,5 @@ export const LocalFitProvider = (props: LocalFitProps) => {
|
||||
});
|
||||
}, [localFitValue, addFit]);
|
||||
|
||||
return <LocalFitContext.Provider value={localFit}>
|
||||
{props.children}
|
||||
</LocalFitContext.Provider>
|
||||
return <LocalFitContext.Provider value={localFit}>{props.children}</LocalFitContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
.modalDialog {
|
||||
bottom: 0px;
|
||||
background-color: rgba(200, 200, 200, 0.3);
|
||||
left: 0px;
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
text-align: center;
|
||||
top: 0px;
|
||||
z-index: 100;
|
||||
bottom: 0px;
|
||||
background-color: rgba(200, 200, 200, 0.3);
|
||||
left: 0px;
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
text-align: center;
|
||||
top: 0px;
|
||||
z-index: 100;
|
||||
}
|
||||
.modalDialog:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
content: "";
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: #111111;
|
||||
border: 1px solid #303030;
|
||||
border-radius: 7px;
|
||||
color: #c5c5c5;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
background-color: #111111;
|
||||
border: 1px solid #303030;
|
||||
border-radius: 7px;
|
||||
color: #c5c5c5;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { ModalDialog } from './';
|
||||
import { ModalDialogAnchor } from './ModalDialog';
|
||||
import { ModalDialog } from "./";
|
||||
import { ModalDialogAnchor } from "./ModalDialog";
|
||||
|
||||
const meta: Meta<typeof ModalDialog> = {
|
||||
component: ModalDialog,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/ModalDialog',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/ModalDialog",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -16,23 +16,22 @@ type Story = StoryObj<typeof ModalDialog>;
|
||||
const TestModalDialog = () => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
return <>
|
||||
<input type="button" value="Open" onClick={() => setIsOpen(true)} />
|
||||
<ModalDialog visible={isOpen} onClose={() => setIsOpen(false)} title="Test Dialog">
|
||||
Test
|
||||
</ModalDialog>
|
||||
</>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<input type="button" value="Open" onClick={() => setIsOpen(true)} />
|
||||
<ModalDialog visible={isOpen} onClose={() => setIsOpen(false)} title="Test Dialog">
|
||||
Test
|
||||
</ModalDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
},
|
||||
args: {},
|
||||
render: () => (
|
||||
<div>
|
||||
<div>
|
||||
Header not covered by modal dialog
|
||||
</div>
|
||||
<div style={{position: "relative", height: "40px"}}>
|
||||
<div>Header not covered by modal dialog</div>
|
||||
<div style={{ position: "relative", height: "40px" }}>
|
||||
<ModalDialogAnchor />
|
||||
<TestModalDialog />
|
||||
</div>
|
||||
|
||||
@@ -37,12 +37,15 @@ export const ModalDialog = (props: ModalDialogProps) => {
|
||||
if (!props.visible) return null;
|
||||
if (modalDialogAnchor === null) return null;
|
||||
|
||||
return createPortal(<div className={styles.modalDialog} onClick={() => props.onClose()}>
|
||||
<div className={clsx(styles.content, props.className)} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={styles.header}>{props.title}</div>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>, modalDialogAnchor);
|
||||
return createPortal(
|
||||
<div className={styles.modalDialog} onClick={() => props.onClose()}>
|
||||
<div className={clsx(styles.content, props.className)} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={styles.header}>{props.title}</div>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>,
|
||||
modalDialogAnchor,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -52,6 +55,5 @@ export const ModalDialog = (props: ModalDialogProps) => {
|
||||
* will always use the full size of this <div> when rendering its content.
|
||||
*/
|
||||
export const ModalDialogAnchor = () => {
|
||||
return <div id="modalDialogAnchor">
|
||||
</div>
|
||||
}
|
||||
return <div id="modalDialogAnchor"></div>;
|
||||
};
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
import { fullFit } from "../../.storybook/fits";
|
||||
|
||||
import { DogmaEngineProvider } from '../DogmaEngineProvider';
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
|
||||
import { ShipAttribute } from './';
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { ShipSnapshotProvider } from "../ShipSnapshotProvider";
|
||||
import { ShipAttribute } from "./";
|
||||
|
||||
const meta: Meta<typeof ShipAttribute> = {
|
||||
component: ShipAttribute,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/ShipAttribute',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/ShipAttribute",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ShipAttribute>;
|
||||
|
||||
const withShipSnapshotProvider: Decorator<{name: string}> = (Story, context) => {
|
||||
const withShipSnapshotProvider: Decorator<{ name: string }> = (Story, context) => {
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<DogmaEngineProvider>
|
||||
@@ -27,7 +27,7 @@ const withShipSnapshotProvider: Decorator<{name: string}> = (Story, context) =>
|
||||
</DogmaEngineProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
@@ -38,6 +38,6 @@ export const Default: Story = {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
skills: {},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { EveDataContext } from '../EveDataProvider';
|
||||
import { ShipSnapshotContext } from '../ShipSnapshotProvider';
|
||||
import { EveDataContext } from "../EveDataProvider";
|
||||
import { ShipSnapshotContext } from "../ShipSnapshotProvider";
|
||||
|
||||
export interface ShipAttributeProps {
|
||||
/** Name of the attribute. */
|
||||
@@ -60,7 +60,7 @@ export function useShipAttribute(props: ShipAttributeProps) {
|
||||
maximumFractionDigits: props.fixed,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single attribute of a ship's snapshot.
|
||||
@@ -68,5 +68,5 @@ export function useShipAttribute(props: ShipAttributeProps) {
|
||||
export const ShipAttribute = (props: ShipAttributeProps) => {
|
||||
const stringValue = useShipAttribute(props);
|
||||
|
||||
return <span>{stringValue}</span>
|
||||
return <span>{stringValue}</span>;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ const useIsRemoteViewer = () => {
|
||||
}, []);
|
||||
|
||||
return remote;
|
||||
}
|
||||
};
|
||||
|
||||
export const FitLink = () => {
|
||||
const link = useEveShipFitLink();
|
||||
@@ -23,26 +23,30 @@ export const FitLink = () => {
|
||||
const { copy, copied } = useClipboard();
|
||||
|
||||
const linkText = isRemoteViewer ? "open on eveship.fit" : "share fit";
|
||||
const linkPropsClick = React.useCallback((e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
copy(link);
|
||||
}, [copy, link]);
|
||||
const linkPropsClick = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
copy(link);
|
||||
},
|
||||
[copy, link],
|
||||
);
|
||||
const linkProps = {
|
||||
onClick: isRemoteViewer ? undefined : linkPropsClick,
|
||||
};
|
||||
|
||||
return <div className={styles.fitLink}>
|
||||
<svg viewBox="0 0 730 730" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
id="fitlink"
|
||||
fill="none"
|
||||
d="M18,365 A25,25 0 0,1 712,365" />
|
||||
return (
|
||||
<div className={styles.fitLink}>
|
||||
<svg viewBox="0 0 730 730" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="fitlink" fill="none" d="M18,365 A25,25 0 0,1 712,365" />
|
||||
|
||||
<a href={link} target="_new" {...linkProps}>
|
||||
<text textAnchor="middle">
|
||||
<textPath startOffset="50%" href="#fitlink" fill="#cdcdcd">{copied ? "copied to clipboard" : linkText}</textPath>
|
||||
</text>
|
||||
</a>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
<a href={link} target="_new" {...linkProps}>
|
||||
<text textAnchor="middle">
|
||||
<textPath startOffset="50%" href="#fitlink" fill="#cdcdcd">
|
||||
{copied ? "copied to clipboard" : linkText}
|
||||
</textPath>
|
||||
</text>
|
||||
</a>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { ShipSnapshotContext } from '../ShipSnapshotProvider';
|
||||
import { ShipSnapshotContext } from "../ShipSnapshotProvider";
|
||||
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
@@ -13,10 +13,12 @@ export const Hull = () => {
|
||||
|
||||
const hull = shipSnapshot?.fit?.ship_type_id;
|
||||
if (hull === undefined) {
|
||||
return <></>
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <div className={styles.hull}>
|
||||
<img src={`https://images.evetech.net/types/${hull}/render?size=1024`} />
|
||||
</div>
|
||||
}
|
||||
return (
|
||||
<div className={styles.hull}>
|
||||
<img src={`https://images.evetech.net/types/${hull}/render?size=1024`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,23 +26,31 @@ const highlightSettings = {
|
||||
export const RadialMenu = (props: { type: "lowslot" | "medslot" | "hislot" }) => {
|
||||
const highlight = highlightSettings[props.type];
|
||||
|
||||
return <svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" className={styles.radialMenu}>
|
||||
<defs>
|
||||
<mask id={`radial-menu-${props.type}`}>
|
||||
<rect style={{ fill: "#ffffff", fillOpacity: 0.5 }} width={12} height={12} x={0} y={0} />
|
||||
return (
|
||||
<svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" className={styles.radialMenu}>
|
||||
<defs>
|
||||
<mask id={`radial-menu-${props.type}`}>
|
||||
<rect style={{ fill: "#ffffff", fillOpacity: 0.5 }} width={12} height={12} x={0} y={0} />
|
||||
|
||||
<rect style={{ fill: "#ffffff" }} width={highlight.width} height={highlight.height} x={highlight.x} y={highlight.y} />
|
||||
<rect
|
||||
style={{ fill: "#ffffff" }}
|
||||
width={highlight.width}
|
||||
height={highlight.height}
|
||||
x={highlight.x}
|
||||
y={highlight.y}
|
||||
/>
|
||||
|
||||
<circle style={{ fill: "#000000" }} cx={6} cy={6} r={5} />
|
||||
<rect style={{ fill: "#000000" }} width={3} height={3} x={0} y={0} />
|
||||
<rect style={{ fill: "#000000" }} width={4} height={3} x={9} y={0} />
|
||||
<rect style={{ fill: "#000000" }} width={3} height={4} x={0} y={9} />
|
||||
<rect style={{ fill: "#000000" }} width={4} height={4} x={9} y={9} />
|
||||
</mask>
|
||||
</defs>
|
||||
<circle style={{ fill: "#000000" }} cx={6} cy={6} r={5} />
|
||||
<rect style={{ fill: "#000000" }} width={3} height={3} x={0} y={0} />
|
||||
<rect style={{ fill: "#000000" }} width={4} height={3} x={9} y={0} />
|
||||
<rect style={{ fill: "#000000" }} width={3} height={4} x={0} y={9} />
|
||||
<rect style={{ fill: "#000000" }} width={4} height={4} x={9} y={9} />
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<g>
|
||||
<rect style={{ fill: "#ffffff" }} width={12} height={12} x={0} y={0} mask={`url(#radial-menu-${props.type})`} />
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
<g>
|
||||
<rect style={{ fill: "#ffffff" }} width={12} height={12} x={0} y={0} mask={`url(#radial-menu-${props.type})`} />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,21 +3,29 @@ import React from "react";
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
export const RingInner = () => {
|
||||
return <svg viewBox="24 24 464 464" xmlns="http://www.w3.org/2000/svg" className={styles.ringInner}>
|
||||
<defs>
|
||||
<mask id="slot-corners">
|
||||
<rect style={{ fill: "#ffffff" }} width="512" height="512" x="0" y="0" />
|
||||
return (
|
||||
<svg viewBox="24 24 464 464" xmlns="http://www.w3.org/2000/svg" className={styles.ringInner}>
|
||||
<defs>
|
||||
<mask id="slot-corners">
|
||||
<rect style={{ fill: "#ffffff" }} width="512" height="512" x="0" y="0" />
|
||||
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="133" y="126" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="366" y="129" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="366" y="366" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="132" y="369" />
|
||||
<rect style={{ fill: "#000000" }} width="12" height="12" x="230" y="44" transform="rotate(56)" />
|
||||
</mask>
|
||||
</defs>
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="133" y="126" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="366" y="129" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="366" y="366" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="132" y="369" />
|
||||
<rect style={{ fill: "#000000" }} width="12" height="12" x="230" y="44" transform="rotate(56)" />
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<g>
|
||||
<circle style={{ fill: "none", stroke: "#000000", strokeWidth: 46, strokeOpacity: 0.6 }} cx="256" cy="256" r="195" mask="url(#slot-corners)" />
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
<g>
|
||||
<circle
|
||||
style={{ fill: "none", stroke: "#000000", strokeWidth: 46, strokeOpacity: 0.6 }}
|
||||
cx="256"
|
||||
cy="256"
|
||||
r="195"
|
||||
mask="url(#slot-corners)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,15 +3,17 @@ import React from "react";
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
export const RingOuter = () => {
|
||||
return <svg viewBox="24 24 464 464" xmlns="http://www.w3.org/2000/svg" className={styles.ringOuter}>
|
||||
<g>
|
||||
<circle style={{ fill: "none", stroke: "#000000", strokeWidth: 16 }} cx="256" cy="256" r="224" />
|
||||
return (
|
||||
<svg viewBox="24 24 464 464" xmlns="http://www.w3.org/2000/svg" className={styles.ringOuter}>
|
||||
<g>
|
||||
<circle style={{ fill: "none", stroke: "#000000", strokeWidth: 16 }} cx="256" cy="256" r="224" />
|
||||
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="98" y="89" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="401" y="93" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="402" y="401" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="94" y="402" />
|
||||
<rect style={{ fill: "#000000" }} width="12" height="12" x="196" y="82" transform="rotate(56)" />
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="98" y="89" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="401" y="93" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="402" y="401" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="94" y="402" />
|
||||
<rect style={{ fill: "#000000" }} width="12" height="12" x="196" y="82" transform="rotate(56)" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,17 +3,17 @@ import React from "react";
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
export const RingTop = (props: { children: React.ReactNode }) => {
|
||||
return <div className={styles.ringTop}>
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
return <div className={styles.ringTop}>{props.children}</div>;
|
||||
};
|
||||
|
||||
export const RingTopItem = (props: { children: React.ReactNode, rotation: number }) => {
|
||||
export const RingTopItem = (props: { children: React.ReactNode; rotation: number }) => {
|
||||
const rotationStyle = {
|
||||
"--rotation": `${props.rotation}deg`,
|
||||
} as React.CSSProperties;
|
||||
|
||||
return <div className={styles.ringTopItem} style={rotationStyle}>
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
return (
|
||||
<div className={styles.ringTopItem} style={rotationStyle}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,153 +1,156 @@
|
||||
.fit {
|
||||
border-radius: 50%;
|
||||
border: 1px solid black;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
border: 1px solid black;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ringOuter {
|
||||
filter: drop-shadow(0 0 6px #000000);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
filter: drop-shadow(0 0 6px #000000);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
.ringInner {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.fitLink {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.hull {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hull > img {
|
||||
border-radius: 50%;
|
||||
height: calc(100% - 2 * 3%);
|
||||
left: 3%;
|
||||
opacity: 0.8;
|
||||
position: relative;
|
||||
top: 3%;
|
||||
width: calc(100% - 2 * 3%);
|
||||
border-radius: 50%;
|
||||
height: calc(100% - 2 * 3%);
|
||||
left: 3%;
|
||||
opacity: 0.8;
|
||||
position: relative;
|
||||
top: 3%;
|
||||
width: calc(100% - 2 * 3%);
|
||||
}
|
||||
|
||||
.ringTop {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ringTopItem {
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
transform: rotate(var(--rotation));
|
||||
width: 100%;
|
||||
z-index: 4;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
transform: rotate(var(--rotation));
|
||||
width: 100%;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.ringTopItem > div, .ringTopItem > svg {
|
||||
--reverse-rotation: calc(-1 * var(--rotation));
|
||||
.ringTopItem > div,
|
||||
.ringTopItem > svg {
|
||||
--reverse-rotation: calc(-1 * var(--rotation));
|
||||
|
||||
left: 50%;
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
top: 3.5%;
|
||||
left: 50%;
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
top: 3.5%;
|
||||
}
|
||||
|
||||
.radialMenu {
|
||||
filter: drop-shadow(0 0 2px #ffffff);
|
||||
position: absolute;
|
||||
margin-top: 3.5%;
|
||||
transform: rotate(var(--reverse-rotation));
|
||||
width: 2.5%;
|
||||
filter: drop-shadow(0 0 2px #ffffff);
|
||||
position: absolute;
|
||||
margin-top: 3.5%;
|
||||
transform: rotate(var(--reverse-rotation));
|
||||
width: 2.5%;
|
||||
}
|
||||
|
||||
.slotOuter {
|
||||
height: 18%;
|
||||
margin-left: -2.5%;
|
||||
position: absolute;
|
||||
width: 7%;
|
||||
height: 18%;
|
||||
margin-left: -2.5%;
|
||||
position: absolute;
|
||||
width: 7%;
|
||||
}
|
||||
|
||||
.slot {
|
||||
height: 50%;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slot > svg {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 4;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.slotImage {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transform: rotate(var(--reverse-rotation));
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transform: rotate(var(--reverse-rotation));
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.slotImage > img {
|
||||
border-top-left-radius: 50%;
|
||||
margin-left: -10%;
|
||||
margin-top: 5%;
|
||||
width: 120%;
|
||||
border-top-left-radius: 50%;
|
||||
margin-left: -10%;
|
||||
margin-top: 5%;
|
||||
width: 120%;
|
||||
}
|
||||
|
||||
.slot > svg {
|
||||
fill: #999999;
|
||||
stroke: #999999;
|
||||
fill: #999999;
|
||||
stroke: #999999;
|
||||
}
|
||||
.slot[data-state="Active"] > svg {
|
||||
fill: #8ae04a;
|
||||
stroke: #8ae04a;
|
||||
fill: #8ae04a;
|
||||
stroke: #8ae04a;
|
||||
}
|
||||
.slot[data-state="Overload"] > svg {
|
||||
fill: #fd2d2d;
|
||||
stroke: #fd2d2d;
|
||||
fill: #fd2d2d;
|
||||
stroke: #fd2d2d;
|
||||
}
|
||||
.slot[data-state="Offline"] > svg, .slot[data-state="Offline"] > .slotImage {
|
||||
opacity: 0.3;
|
||||
.slot[data-state="Offline"] > svg,
|
||||
.slot[data-state="Offline"] > .slotImage {
|
||||
opacity: 0.3;
|
||||
}
|
||||
.slot[data-state="Unavailable"] > svg, .slot[data-state="Unavailable"] > .slotImage {
|
||||
opacity: 0.1;
|
||||
.slot[data-state="Unavailable"] > svg,
|
||||
.slot[data-state="Unavailable"] > .slotImage {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.slotOuter .slotOptions {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
.slotOuter[data-hasitem=true]:hover .slotOptions {
|
||||
display: block;
|
||||
.slotOuter[data-hasitem="true"]:hover .slotOptions {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.slotOptions {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
.slotOptions > svg {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
filter: drop-shadow(0px 2px 1px #222222);
|
||||
margin: 6px auto;
|
||||
transform: rotate(var(--reverse-rotation));
|
||||
stroke: #ffffff;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
filter: drop-shadow(0px 2px 1px #222222);
|
||||
margin: 6px auto;
|
||||
transform: rotate(var(--reverse-rotation));
|
||||
stroke: #ffffff;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
import { fullFit } from "../../.storybook/fits";
|
||||
|
||||
import { DogmaEngineProvider } from '../DogmaEngineProvider';
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
|
||||
import { ShipFit } from './';
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { ShipSnapshotProvider } from "../ShipSnapshotProvider";
|
||||
import { ShipFit } from "./";
|
||||
|
||||
const meta: Meta<typeof ShipFit> = {
|
||||
component: ShipFit,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/ShipFit',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/ShipFit",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -29,7 +29,7 @@ const withShipSnapshotProvider: Decorator<Record<string, never>> = (Story, conte
|
||||
</DogmaEngineProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
@@ -40,6 +40,6 @@ export const Default: Story = {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
skills: {},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
|
||||
import { ShipSnapshotContext } from '../ShipSnapshotProvider';
|
||||
import { ShipSnapshotContext } from "../ShipSnapshotProvider";
|
||||
|
||||
import { FitLink } from './FitLink';
|
||||
import { Hull } from './Hull';
|
||||
import { Slot } from './Slot';
|
||||
import { FitLink } from "./FitLink";
|
||||
import { Hull } from "./Hull";
|
||||
import { Slot } from "./Slot";
|
||||
import { RadialMenu } from "./RadialMenu";
|
||||
import { RingOuter } from "./RingOuter";
|
||||
import { RingInner } from "./RingInner";
|
||||
@@ -19,55 +19,125 @@ export const ShipFit = () => {
|
||||
const shipSnapshot = React.useContext(ShipSnapshotContext);
|
||||
const slots = shipSnapshot.slots;
|
||||
|
||||
return <div className={styles.fit}>
|
||||
<RingOuter />
|
||||
<RingInner />
|
||||
return (
|
||||
<div className={styles.fit}>
|
||||
<RingOuter />
|
||||
<RingInner />
|
||||
|
||||
<Hull />
|
||||
<FitLink />
|
||||
<Hull />
|
||||
<FitLink />
|
||||
|
||||
<RingTop>
|
||||
<RingTopItem rotation={-45}><RadialMenu type="hislot" /></RingTopItem>
|
||||
<RingTop>
|
||||
<RingTopItem rotation={-45}>
|
||||
<RadialMenu type="hislot" />
|
||||
</RingTopItem>
|
||||
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 0}><Slot type="hislot" index={1} fittable={slots?.hislot >= 1} main /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 1}><Slot type="hislot" index={2} fittable={slots?.hislot >= 2} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 2}><Slot type="hislot" index={3} fittable={slots?.hislot >= 3} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 3}><Slot type="hislot" index={4} fittable={slots?.hislot >= 4} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 4}><Slot type="hislot" index={5} fittable={slots?.hislot >= 5} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 5}><Slot type="hislot" index={6} fittable={slots?.hislot >= 6} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 6}><Slot type="hislot" index={7} fittable={slots?.hislot >= 7} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 7}><Slot type="hislot" index={8} fittable={slots?.hislot >= 8} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + (71 / 7) * 0}>
|
||||
<Slot type="hislot" index={1} fittable={slots?.hislot >= 1} main />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + (71 / 7) * 1}>
|
||||
<Slot type="hislot" index={2} fittable={slots?.hislot >= 2} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + (71 / 7) * 2}>
|
||||
<Slot type="hislot" index={3} fittable={slots?.hislot >= 3} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + (71 / 7) * 3}>
|
||||
<Slot type="hislot" index={4} fittable={slots?.hislot >= 4} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + (71 / 7) * 4}>
|
||||
<Slot type="hislot" index={5} fittable={slots?.hislot >= 5} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + (71 / 7) * 5}>
|
||||
<Slot type="hislot" index={6} fittable={slots?.hislot >= 6} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + (71 / 7) * 6}>
|
||||
<Slot type="hislot" index={7} fittable={slots?.hislot >= 7} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + (71 / 7) * 7}>
|
||||
<Slot type="hislot" index={8} fittable={slots?.hislot >= 8} />
|
||||
</RingTopItem>
|
||||
|
||||
<RingTopItem rotation={43}><RadialMenu type="medslot" /></RingTopItem>
|
||||
<RingTopItem rotation={43}>
|
||||
<RadialMenu type="medslot" />
|
||||
</RingTopItem>
|
||||
|
||||
<RingTopItem rotation={53 + 72 / 7 * 0}><Slot type="medslot" index={1} fittable={slots?.medslot >= 1} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 1}><Slot type="medslot" index={2} fittable={slots?.medslot >= 2} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 2}><Slot type="medslot" index={3} fittable={slots?.medslot >= 3} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 3}><Slot type="medslot" index={4} fittable={slots?.medslot >= 4} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 4}><Slot type="medslot" index={5} fittable={slots?.medslot >= 5} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 5}><Slot type="medslot" index={6} fittable={slots?.medslot >= 6} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 6}><Slot type="medslot" index={7} fittable={slots?.medslot >= 7} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 7}><Slot type="medslot" index={8} fittable={slots?.medslot >= 8} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + (72 / 7) * 0}>
|
||||
<Slot type="medslot" index={1} fittable={slots?.medslot >= 1} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={53 + (72 / 7) * 1}>
|
||||
<Slot type="medslot" index={2} fittable={slots?.medslot >= 2} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={53 + (72 / 7) * 2}>
|
||||
<Slot type="medslot" index={3} fittable={slots?.medslot >= 3} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={53 + (72 / 7) * 3}>
|
||||
<Slot type="medslot" index={4} fittable={slots?.medslot >= 4} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={53 + (72 / 7) * 4}>
|
||||
<Slot type="medslot" index={5} fittable={slots?.medslot >= 5} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={53 + (72 / 7) * 5}>
|
||||
<Slot type="medslot" index={6} fittable={slots?.medslot >= 6} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={53 + (72 / 7) * 6}>
|
||||
<Slot type="medslot" index={7} fittable={slots?.medslot >= 7} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={53 + (72 / 7) * 7}>
|
||||
<Slot type="medslot" index={8} fittable={slots?.medslot >= 8} />
|
||||
</RingTopItem>
|
||||
|
||||
<RingTopItem rotation={133}><RadialMenu type="lowslot" /></RingTopItem>
|
||||
<RingTopItem rotation={133}>
|
||||
<RadialMenu type="lowslot" />
|
||||
</RingTopItem>
|
||||
|
||||
<RingTopItem rotation={142 + 72 / 7 * 0}><Slot type="lowslot" index={1} fittable={slots?.lowslot >= 1} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 1}><Slot type="lowslot" index={2} fittable={slots?.lowslot >= 2} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 2}><Slot type="lowslot" index={3} fittable={slots?.lowslot >= 3} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 3}><Slot type="lowslot" index={4} fittable={slots?.lowslot >= 4} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 4}><Slot type="lowslot" index={5} fittable={slots?.lowslot >= 5} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 5}><Slot type="lowslot" index={6} fittable={slots?.lowslot >= 6} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 6}><Slot type="lowslot" index={7} fittable={slots?.lowslot >= 7} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 7}><Slot type="lowslot" index={8} fittable={slots?.lowslot >= 8} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + (72 / 7) * 0}>
|
||||
<Slot type="lowslot" index={1} fittable={slots?.lowslot >= 1} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={142 + (72 / 7) * 1}>
|
||||
<Slot type="lowslot" index={2} fittable={slots?.lowslot >= 2} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={142 + (72 / 7) * 2}>
|
||||
<Slot type="lowslot" index={3} fittable={slots?.lowslot >= 3} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={142 + (72 / 7) * 3}>
|
||||
<Slot type="lowslot" index={4} fittable={slots?.lowslot >= 4} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={142 + (72 / 7) * 4}>
|
||||
<Slot type="lowslot" index={5} fittable={slots?.lowslot >= 5} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={142 + (72 / 7) * 5}>
|
||||
<Slot type="lowslot" index={6} fittable={slots?.lowslot >= 6} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={142 + (72 / 7) * 6}>
|
||||
<Slot type="lowslot" index={7} fittable={slots?.lowslot >= 7} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={142 + (72 / 7) * 7}>
|
||||
<Slot type="lowslot" index={8} fittable={slots?.lowslot >= 8} />
|
||||
</RingTopItem>
|
||||
|
||||
<RingTopItem rotation={-74 + 21 / 2 * 0}><Slot type="rig" index={1} fittable={slots?.rig >= 1} /></RingTopItem>
|
||||
<RingTopItem rotation={-74 + 21 / 2 * 1}><Slot type="rig" index={2} fittable={slots?.rig >= 2} /></RingTopItem>
|
||||
<RingTopItem rotation={-74 + 21 / 2 * 2}><Slot type="rig" index={3} fittable={slots?.rig >= 3} /></RingTopItem>
|
||||
<RingTopItem rotation={-74 + (21 / 2) * 0}>
|
||||
<Slot type="rig" index={1} fittable={slots?.rig >= 1} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-74 + (21 / 2) * 1}>
|
||||
<Slot type="rig" index={2} fittable={slots?.rig >= 2} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-74 + (21 / 2) * 2}>
|
||||
<Slot type="rig" index={3} fittable={slots?.rig >= 3} />
|
||||
</RingTopItem>
|
||||
|
||||
<RingTopItem rotation={-128 + 38 / 3 * 0}><Slot type="subsystem" index={1} fittable={slots?.subsystem >= 1} /></RingTopItem>
|
||||
<RingTopItem rotation={-128 + 38 / 3 * 1}><Slot type="subsystem" index={2} fittable={slots?.subsystem >= 2} /></RingTopItem>
|
||||
<RingTopItem rotation={-128 + 38 / 3 * 2}><Slot type="subsystem" index={3} fittable={slots?.subsystem >= 3} /></RingTopItem>
|
||||
<RingTopItem rotation={-128 + 38 / 3 * 3}><Slot type="subsystem" index={4} fittable={slots?.subsystem >= 4} /></RingTopItem>
|
||||
</RingTop>
|
||||
</div>
|
||||
<RingTopItem rotation={-128 + (38 / 3) * 0}>
|
||||
<Slot type="subsystem" index={1} fittable={slots?.subsystem >= 1} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-128 + (38 / 3) * 1}>
|
||||
<Slot type="subsystem" index={2} fittable={slots?.subsystem >= 2} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-128 + (38 / 3) * 2}>
|
||||
<Slot type="subsystem" index={3} fittable={slots?.subsystem >= 3} />
|
||||
</RingTopItem>
|
||||
<RingTopItem rotation={-128 + (38 / 3) * 3}>
|
||||
<Slot type="subsystem" index={4} fittable={slots?.subsystem >= 4} />
|
||||
</RingTopItem>
|
||||
</RingTop>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,36 +1,26 @@
|
||||
import React from "react";
|
||||
|
||||
import { EveDataContext } from '../EveDataProvider';
|
||||
import { ShipSnapshotContext } from '../ShipSnapshotProvider';
|
||||
import { EveDataContext } from "../EveDataProvider";
|
||||
import { ShipSnapshotContext } from "../ShipSnapshotProvider";
|
||||
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
const esiFlagMapping: Record<string, number[]> = {
|
||||
"lowslot": [
|
||||
11, 12, 13, 14, 15, 16, 17, 18
|
||||
],
|
||||
"medslot": [
|
||||
19, 20, 21, 22, 23, 24, 25, 26
|
||||
],
|
||||
"hislot": [
|
||||
27, 28, 29, 30, 31, 32, 33, 34
|
||||
],
|
||||
"rig": [
|
||||
92, 93, 94
|
||||
],
|
||||
"subsystem": [
|
||||
125, 126, 127, 128
|
||||
],
|
||||
lowslot: [11, 12, 13, 14, 15, 16, 17, 18],
|
||||
medslot: [19, 20, 21, 22, 23, 24, 25, 26],
|
||||
hislot: [27, 28, 29, 30, 31, 32, 33, 34],
|
||||
rig: [92, 93, 94],
|
||||
subsystem: [125, 126, 127, 128],
|
||||
};
|
||||
|
||||
const stateRotation: Record<string, string[]> = {
|
||||
"Passive": ["Passive"],
|
||||
"Online": ["Passive", "Online"],
|
||||
"Active": ["Passive", "Online", "Active"],
|
||||
"Overload": ["Passive", "Online", "Active", "Overload"],
|
||||
Passive: ["Passive"],
|
||||
Online: ["Passive", "Online"],
|
||||
Active: ["Passive", "Online", "Active"],
|
||||
Overload: ["Passive", "Online", "Active", "Overload"],
|
||||
};
|
||||
|
||||
export const Slot = (props: { type: string, index: number, fittable: boolean, main?: boolean }) => {
|
||||
export const Slot = (props: { type: string; index: number; fittable: boolean; main?: boolean }) => {
|
||||
const eveData = React.useContext(EveDataContext);
|
||||
const shipSnapshot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
@@ -43,109 +33,142 @@ export const Slot = (props: { type: string, index: number, fittable: boolean, ma
|
||||
let svg = <></>;
|
||||
|
||||
if (props.main !== undefined) {
|
||||
svg = <>
|
||||
<svg viewBox="235 40 52 50" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMin slice" style={{ display: "none" }}>
|
||||
<g id="slot">
|
||||
<path style={{ fillOpacity: 0.1, strokeWidth: 1, strokeOpacity: 0.5 }} d="M 243 46 A 210 210 0 0 1 279 46.7 L 276 84.7 A 172 172 0 0 0 246 84 L 243 46" />
|
||||
</g>
|
||||
<g id="slot-active">
|
||||
<path style={{ fillOpacity: 0.6, strokeWidth: 1 }} d="M 250 84 L 254 79 L 268 79 L 272 84" />
|
||||
</g>
|
||||
<g id="slot-passive">
|
||||
<path style={{ strokeWidth: 1 }} d="M 245 48 A 208 208 0 0 1 250 47.5 L 248 50 L 245 50" />
|
||||
<path style={{ strokeWidth: 1 }} d="M 277.5 48.5 A 208 208 0 0 0 273 48 L 275 50.5 L 277.5 50.5" />
|
||||
<path style={{ strokeWidth: 1 }} d="M 247 82 A 170 170 0 0 1 252 82 L 250 80 L 246.8 80" />
|
||||
<path style={{ strokeWidth: 1 }} d="M 275 82.5 A 170 170 0 0 0 270 82 L 272 80 L 275.2 80" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" style={{ display: "none" }}>
|
||||
<g id="unfit">
|
||||
<path style={{ fill: "none", strokeWidth: 1 }} d="M 4 6 A 8 8 0 1 1 4 14" />
|
||||
<path style={{ fill: "none", strokeWidth: 1 }} d="M 11 6 L 6 10 L 11 14" />
|
||||
<path style={{ fill: "none", strokeWidth: 1 }} d="M 6 10 L 16 10" />
|
||||
</g>
|
||||
<g id="offline">
|
||||
<path style={{ fill: "none", strokeWidth: 1 }} d="M 12 4 A 8 8 0 1 1 6 4" />
|
||||
<path style={{ fill: "none", strokeWidth: 1 }} d="M 9 2 L 9 12" />
|
||||
</g>
|
||||
</svg>
|
||||
</>;
|
||||
svg = (
|
||||
<>
|
||||
<svg
|
||||
viewBox="235 40 52 50"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMin slice"
|
||||
style={{ display: "none" }}
|
||||
>
|
||||
<g id="slot">
|
||||
<path
|
||||
style={{ fillOpacity: 0.1, strokeWidth: 1, strokeOpacity: 0.5 }}
|
||||
d="M 243 46 A 210 210 0 0 1 279 46.7 L 276 84.7 A 172 172 0 0 0 246 84 L 243 46"
|
||||
/>
|
||||
</g>
|
||||
<g id="slot-active">
|
||||
<path style={{ fillOpacity: 0.6, strokeWidth: 1 }} d="M 250 84 L 254 79 L 268 79 L 272 84" />
|
||||
</g>
|
||||
<g id="slot-passive">
|
||||
<path style={{ strokeWidth: 1 }} d="M 245 48 A 208 208 0 0 1 250 47.5 L 248 50 L 245 50" />
|
||||
<path style={{ strokeWidth: 1 }} d="M 277.5 48.5 A 208 208 0 0 0 273 48 L 275 50.5 L 277.5 50.5" />
|
||||
<path style={{ strokeWidth: 1 }} d="M 247 82 A 170 170 0 0 1 252 82 L 250 80 L 246.8 80" />
|
||||
<path style={{ strokeWidth: 1 }} d="M 275 82.5 A 170 170 0 0 0 270 82 L 272 80 L 275.2 80" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" style={{ display: "none" }}>
|
||||
<g id="unfit">
|
||||
<path style={{ fill: "none", strokeWidth: 1 }} d="M 4 6 A 8 8 0 1 1 4 14" />
|
||||
<path style={{ fill: "none", strokeWidth: 1 }} d="M 11 6 L 6 10 L 11 14" />
|
||||
<path style={{ fill: "none", strokeWidth: 1 }} d="M 6 10 L 16 10" />
|
||||
</g>
|
||||
<g id="offline">
|
||||
<path style={{ fill: "none", strokeWidth: 1 }} d="M 12 4 A 8 8 0 1 1 6 4" />
|
||||
<path style={{ fill: "none", strokeWidth: 1 }} d="M 9 2 L 9 12" />
|
||||
</g>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
svg = <>
|
||||
{svg}
|
||||
<svg viewBox="235 40 52 50" xmlns="http://www.w3.org/2000/svg" className={styles.ringInner} preserveAspectRatio="xMidYMin slice">
|
||||
<use href="#slot" />
|
||||
{props.fittable && esiItem && active && <use href="#slot-active" />}
|
||||
{props.fittable && esiItem && !active && <use href="#slot-passive" />}
|
||||
</svg>
|
||||
</>;
|
||||
svg = (
|
||||
<>
|
||||
{svg}
|
||||
<svg
|
||||
viewBox="235 40 52 50"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={styles.ringInner}
|
||||
preserveAspectRatio="xMidYMin slice"
|
||||
>
|
||||
<use href="#slot" />
|
||||
{props.fittable && esiItem && active && <use href="#slot-active" />}
|
||||
{props.fittable && esiItem && !active && <use href="#slot-passive" />}
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
|
||||
const offlineState = React.useCallback((e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
if (!shipSnapshot?.loaded || !esiItem) return;
|
||||
const offlineState = React.useCallback(
|
||||
(e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
if (!shipSnapshot?.loaded || !esiItem) return;
|
||||
|
||||
if (esiItem.state === "Passive") {
|
||||
shipSnapshot.setItemState(esiItem.flag, "Online");
|
||||
} else {
|
||||
shipSnapshot.setItemState(esiItem.flag, "Passive");
|
||||
}
|
||||
}, [shipSnapshot, esiItem]);
|
||||
if (esiItem.state === "Passive") {
|
||||
shipSnapshot.setItemState(esiItem.flag, "Online");
|
||||
} else {
|
||||
shipSnapshot.setItemState(esiItem.flag, "Passive");
|
||||
}
|
||||
},
|
||||
[shipSnapshot, esiItem],
|
||||
);
|
||||
|
||||
const cycleState = React.useCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
if (!shipSnapshot?.loaded || !esiItem) return;
|
||||
const cycleState = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
if (!shipSnapshot?.loaded || !esiItem) return;
|
||||
|
||||
const states = stateRotation[esiItem.max_state];
|
||||
const stateIndex = states.indexOf(esiItem.state);
|
||||
const states = stateRotation[esiItem.max_state];
|
||||
const stateIndex = states.indexOf(esiItem.state);
|
||||
|
||||
let newState;
|
||||
if (e.shiftKey) {
|
||||
newState = states[(stateIndex - 1 + states.length) % states.length];
|
||||
} else {
|
||||
newState = states[(stateIndex + 1) % states.length];
|
||||
}
|
||||
let newState;
|
||||
if (e.shiftKey) {
|
||||
newState = states[(stateIndex - 1 + states.length) % states.length];
|
||||
} else {
|
||||
newState = states[(stateIndex + 1) % states.length];
|
||||
}
|
||||
|
||||
shipSnapshot.setItemState(esiItem.flag, newState);
|
||||
}, [shipSnapshot, esiItem]);
|
||||
shipSnapshot.setItemState(esiItem.flag, newState);
|
||||
},
|
||||
[shipSnapshot, esiItem],
|
||||
);
|
||||
|
||||
const unfitModule = React.useCallback((e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
if (!shipSnapshot?.loaded || !esiItem) return;
|
||||
const unfitModule = React.useCallback(
|
||||
(e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
if (!shipSnapshot?.loaded || !esiItem) return;
|
||||
|
||||
shipSnapshot.removeModule(esiItem.flag);
|
||||
}, [shipSnapshot, esiItem]);
|
||||
shipSnapshot.removeModule(esiItem.flag);
|
||||
},
|
||||
[shipSnapshot, esiItem],
|
||||
);
|
||||
|
||||
/* Not fittable and nothing fitted; no need to render the slot. */
|
||||
if (esiItem === undefined && !props.fittable) {
|
||||
return <div className={styles.slotOuter} data-hasitem={false}>
|
||||
<div className={styles.slot} data-state="Unavailable">
|
||||
{svg}
|
||||
return (
|
||||
<div className={styles.slotOuter} data-hasitem={false}>
|
||||
<div className={styles.slot} data-state="Unavailable">
|
||||
{svg}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (esiItem !== undefined) {
|
||||
item = <img src={`https://images.evetech.net/types/${esiItem.type_id}/icon?size=64`} title={eveData?.typeIDs?.[esiItem.type_id].name} />
|
||||
item = (
|
||||
<img
|
||||
src={`https://images.evetech.net/types/${esiItem.type_id}/icon?size=64`}
|
||||
title={eveData?.typeIDs?.[esiItem.type_id].name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const state = (esiItem?.state === "Passive" && esiItem?.max_state !== "Passive") ? "Offline" : esiItem?.state;
|
||||
const state = esiItem?.state === "Passive" && esiItem?.max_state !== "Passive" ? "Offline" : esiItem?.state;
|
||||
|
||||
return <div className={styles.slotOuter} data-hasitem={esiItem !== undefined}>
|
||||
<div className={styles.slot} onClick={cycleState} data-state={state}>
|
||||
{svg}
|
||||
<div className={styles.slotImage}>
|
||||
{item}
|
||||
return (
|
||||
<div className={styles.slotOuter} data-hasitem={esiItem !== undefined}>
|
||||
<div className={styles.slot} onClick={cycleState} data-state={state}>
|
||||
{svg}
|
||||
<div className={styles.slotImage}>{item}</div>
|
||||
</div>
|
||||
<div className={styles.slotOptions}>
|
||||
<svg viewBox="0 0 20 20" width={20} xmlns="http://www.w3.org/2000/svg" onClick={unfitModule}>
|
||||
<use href="#unfit" />
|
||||
</svg>
|
||||
{esiItem?.max_state !== "Passive" && (
|
||||
<svg viewBox="0 0 20 20" width={20} xmlns="http://www.w3.org/2000/svg" onClick={offlineState}>
|
||||
<use href="#offline" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.slotOptions}>
|
||||
<svg viewBox="0 0 20 20" width={20} xmlns="http://www.w3.org/2000/svg" onClick={unfitModule}>
|
||||
<use href="#unfit" />
|
||||
</svg>
|
||||
{esiItem?.max_state !== "Passive" &&
|
||||
<svg viewBox="0 0 20 20" width={20} xmlns="http://www.w3.org/2000/svg" onClick={offlineState}>
|
||||
<use href="#offline" />
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
.fit {
|
||||
background-color: #111111;
|
||||
color: #c5c5c5;
|
||||
font-size: 15px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #111111;
|
||||
color: #c5c5c5;
|
||||
font-size: 15px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fitName {
|
||||
top: 35px;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 35px;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.fitNameTitle {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cpuPg {
|
||||
bottom: 0px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
text-align: right;
|
||||
bottom: 0px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cpuPgTitle {
|
||||
font-weight: bold;
|
||||
margin-top: 15px;
|
||||
font-weight: bold;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.cargoHold {
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.cargoIcon {
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
}
|
||||
.cargoText {
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
.cargoPostfix {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
import { fullFit } from "../../.storybook/fits";
|
||||
|
||||
import { DogmaEngineProvider } from '../DogmaEngineProvider';
|
||||
import { EsiProvider } from '../EsiProvider';
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
|
||||
import { ShipFitExtended } from './';
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
import { EsiProvider } from "../EsiProvider";
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { ShipSnapshotProvider } from "../ShipSnapshotProvider";
|
||||
import { ShipFitExtended } from "./";
|
||||
|
||||
const meta: Meta<typeof ShipFitExtended> = {
|
||||
component: ShipFitExtended,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/ShipFitExtended',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/ShipFitExtended",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -34,7 +34,7 @@ const useShipSnapshotProvider: Decorator<Record<string, never>> = (Story, contex
|
||||
</EsiProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
@@ -44,6 +44,6 @@ export const Default: Story = {
|
||||
parameters: {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,58 +8,58 @@ import { ShipSnapshotContext } from "../ShipSnapshotProvider";
|
||||
import styles from "./ShipFitExtended.module.css";
|
||||
|
||||
const CargoHold = () => {
|
||||
return <div>
|
||||
<div className={styles.cargoIcon}>
|
||||
<Icon name="cargo-hold" size={32} />
|
||||
</div>
|
||||
<div className={styles.cargoText}>
|
||||
<div>
|
||||
0
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.cargoIcon}>
|
||||
<Icon name="cargo-hold" size={32} />
|
||||
</div>
|
||||
<div>
|
||||
/ <ShipAttribute name="capacity" fixed={1} />
|
||||
<div className={styles.cargoText}>
|
||||
<div>0</div>
|
||||
<div>
|
||||
/ <ShipAttribute name="capacity" fixed={1} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cargoPostfix}>m3</div>
|
||||
</div>
|
||||
<div className={styles.cargoPostfix}>
|
||||
m3
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const DroneBay = () => {
|
||||
return <div>
|
||||
<div className={styles.cargoIcon}>
|
||||
<Icon name="drone-bay" size={32} />
|
||||
</div>
|
||||
<div className={styles.cargoText}>
|
||||
<div>
|
||||
0
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.cargoIcon}>
|
||||
<Icon name="drone-bay" size={32} />
|
||||
</div>
|
||||
<div>
|
||||
/ <ShipAttribute name="droneCapacity" fixed={1} />
|
||||
<div className={styles.cargoText}>
|
||||
<div>0</div>
|
||||
<div>
|
||||
/ <ShipAttribute name="droneCapacity" fixed={1} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cargoPostfix}>m3</div>
|
||||
</div>
|
||||
<div className={styles.cargoPostfix}>
|
||||
m3
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const CpuPg = (props: { title: string, children: React.ReactNode }) => {
|
||||
return <>
|
||||
<div className={styles.cpuPgTitle}>{props.title}</div>
|
||||
<div className={styles.cpuPgContent}>{props.children}</div>
|
||||
</>
|
||||
}
|
||||
const CpuPg = (props: { title: string; children: React.ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.cpuPgTitle}>{props.title}</div>
|
||||
<div className={styles.cpuPgContent}>{props.children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FitName = () => {
|
||||
const shipSnapshot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
return <>
|
||||
<div className={styles.fitNameTitle}>Name</div>
|
||||
<div className={styles.fitNameContent}>{shipSnapshot?.fit?.name}</div>
|
||||
</>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={styles.fitNameTitle}>Name</div>
|
||||
<div className={styles.fitNameContent}>{shipSnapshot?.fit?.name}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render a ship fit similar to how it is done in-game.
|
||||
@@ -69,25 +69,27 @@ const FitName = () => {
|
||||
* bottom of the fit.
|
||||
*/
|
||||
export const ShipFitExtended = () => {
|
||||
return <div className={styles.fit}>
|
||||
<ShipFit />
|
||||
return (
|
||||
<div className={styles.fit}>
|
||||
<ShipFit />
|
||||
|
||||
<div className={styles.fitName}>
|
||||
<FitName />
|
||||
</div>
|
||||
<div className={styles.fitName}>
|
||||
<FitName />
|
||||
</div>
|
||||
|
||||
<div className={styles.cargoHold}>
|
||||
<CargoHold />
|
||||
<DroneBay />
|
||||
</div>
|
||||
<div className={styles.cargoHold}>
|
||||
<CargoHold />
|
||||
<DroneBay />
|
||||
</div>
|
||||
|
||||
<div className={styles.cpuPg}>
|
||||
<CpuPg title="CPU">
|
||||
<ShipAttribute name="cpuUnused" fixed={1} />/<ShipAttribute name="cpuOutput" fixed={1} />
|
||||
</CpuPg>
|
||||
<CpuPg title="Power Grid">
|
||||
<ShipAttribute name="powerUnused" fixed={1} />/<ShipAttribute name="powerOutput" fixed={1} />
|
||||
</CpuPg>
|
||||
<div className={styles.cpuPg}>
|
||||
<CpuPg title="CPU">
|
||||
<ShipAttribute name="cpuUnused" fixed={1} />/<ShipAttribute name="cpuOutput" fixed={1} />
|
||||
</CpuPg>
|
||||
<CpuPg title="Power Grid">
|
||||
<ShipAttribute name="powerUnused" fixed={1} />/<ShipAttribute name="powerOutput" fixed={1} />
|
||||
</CpuPg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
import { fullFit } from "../../.storybook/fits";
|
||||
|
||||
import { EveDataContext, EveDataProvider } from '../EveDataProvider';
|
||||
import { DogmaEngineProvider } from '../DogmaEngineProvider';
|
||||
import { ShipSnapshotItemAttribute, ShipSnapshotContext, ShipSnapshotProvider } from './';
|
||||
import { EveDataContext, EveDataProvider } from "../EveDataProvider";
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
import { ShipSnapshotItemAttribute, ShipSnapshotContext, ShipSnapshotProvider } from "./";
|
||||
|
||||
const meta: Meta<typeof ShipSnapshotProvider> = {
|
||||
component: ShipSnapshotProvider,
|
||||
tags: ['autodocs'],
|
||||
title: 'Provider/ShipSnapshotProvider',
|
||||
tags: ["autodocs"],
|
||||
title: "Provider/ShipSnapshotProvider",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -23,21 +23,29 @@ const TestShipSnapshot = () => {
|
||||
if (shipSnapshot?.loaded) {
|
||||
return (
|
||||
<div>
|
||||
ShipSnapshot: loaded<br/>
|
||||
ShipSnapshot: loaded
|
||||
<br />
|
||||
Hull:
|
||||
<ul>
|
||||
{Array.from(shipSnapshot.hull?.attributes.entries() || []).map(([id, attribute]: [number, ShipSnapshotItemAttribute]) => <li key={id}>{eveData?.dogmaAttributes?.[id].name} ({id}): {attribute.value}</li>)}
|
||||
{Array.from(shipSnapshot.hull?.attributes.entries() || []).map(
|
||||
([id, attribute]: [number, ShipSnapshotItemAttribute]) => (
|
||||
<li key={id}>
|
||||
{eveData?.dogmaAttributes?.[id].name} ({id}): {attribute.value}
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
ShipSnapshot: loading<br/>
|
||||
ShipSnapshot: loading
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import React from "react";
|
||||
|
||||
import { DogmaEngineContext } from '../DogmaEngineProvider';
|
||||
import { DogmaEngineContext } from "../DogmaEngineProvider";
|
||||
import { EveDataContext } from "../EveDataProvider";
|
||||
|
||||
export interface ShipSnapshotItemAttributeEffect {
|
||||
operator: string,
|
||||
penalty: boolean,
|
||||
source: "Ship" | { Item: number },
|
||||
source_category: string,
|
||||
source_attribute_id: number,
|
||||
};
|
||||
operator: string;
|
||||
penalty: boolean;
|
||||
source: "Ship" | { Item: number };
|
||||
source_category: string;
|
||||
source_attribute_id: number;
|
||||
}
|
||||
|
||||
export interface ShipSnapshotItemAttribute {
|
||||
base_value: number,
|
||||
value: number,
|
||||
effects: ShipSnapshotItemAttributeEffect[],
|
||||
};
|
||||
base_value: number;
|
||||
value: number;
|
||||
effects: ShipSnapshotItemAttributeEffect[];
|
||||
}
|
||||
|
||||
export interface ShipSnapshotItem {
|
||||
type_id: number,
|
||||
quantity: number,
|
||||
flag: number,
|
||||
state: "Passive" | "Online" | "Active" | "Overload",
|
||||
max_state: "Passive" | "Online" | "Active" | "Overload",
|
||||
attributes: Map<number, ShipSnapshotItemAttribute>,
|
||||
effects: number[],
|
||||
type_id: number;
|
||||
quantity: number;
|
||||
flag: number;
|
||||
state: "Passive" | "Online" | "Active" | "Overload";
|
||||
max_state: "Passive" | "Online" | "Active" | "Overload";
|
||||
attributes: Map<number, ShipSnapshotItemAttribute>;
|
||||
effects: number[];
|
||||
}
|
||||
|
||||
export interface EsiFit {
|
||||
@@ -45,7 +45,7 @@ interface ShipSnapshotSlots {
|
||||
lowslot: number;
|
||||
subsystem: number;
|
||||
rig: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type ShipSnapshotSlotsType = keyof ShipSnapshotSlots;
|
||||
|
||||
@@ -68,11 +68,11 @@ interface ShipSnapshot {
|
||||
export const ShipSnapshotContext = React.createContext<ShipSnapshot>({
|
||||
loaded: undefined,
|
||||
slots: {
|
||||
"hislot": 0,
|
||||
"medslot": 0,
|
||||
"lowslot": 0,
|
||||
"subsystem": 0,
|
||||
"rig": 0,
|
||||
hislot: 0,
|
||||
medslot: 0,
|
||||
lowslot: 0,
|
||||
subsystem: 0,
|
||||
rig: 0,
|
||||
},
|
||||
addModule: () => {},
|
||||
removeModule: () => {},
|
||||
@@ -83,11 +83,11 @@ export const ShipSnapshotContext = React.createContext<ShipSnapshot>({
|
||||
});
|
||||
|
||||
const slotStart: Record<ShipSnapshotSlotsType, number> = {
|
||||
"hislot": 27,
|
||||
"medslot": 19,
|
||||
"lowslot": 11,
|
||||
"subsystem": 125,
|
||||
"rig": 92,
|
||||
hislot: 27,
|
||||
medslot: 19,
|
||||
lowslot: 11,
|
||||
subsystem: 125,
|
||||
rig: 92,
|
||||
};
|
||||
|
||||
export interface ShipSnapshotProps {
|
||||
@@ -107,11 +107,11 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
|
||||
const [shipSnapshot, setShipSnapshot] = React.useState<ShipSnapshot>({
|
||||
loaded: undefined,
|
||||
slots: {
|
||||
"hislot": 0,
|
||||
"medslot": 0,
|
||||
"lowslot": 0,
|
||||
"subsystem": 0,
|
||||
"rig": 0,
|
||||
hislot: 0,
|
||||
medslot: 0,
|
||||
lowslot: 0,
|
||||
subsystem: 0,
|
||||
rig: 0,
|
||||
},
|
||||
addModule: () => {},
|
||||
removeModule: () => {},
|
||||
@@ -140,7 +140,7 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
|
||||
return item;
|
||||
}),
|
||||
};
|
||||
})
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setName = React.useCallback((name: string) => {
|
||||
@@ -151,43 +151,46 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
|
||||
...oldFit,
|
||||
name: name,
|
||||
};
|
||||
})
|
||||
});
|
||||
}, []);
|
||||
|
||||
const addModule = React.useCallback((typeId: number, slot: ShipSnapshotSlotsType | "dronebay") => {
|
||||
setCurrentFit((oldFit: EsiFit | undefined) => {
|
||||
if (oldFit === undefined) return undefined;
|
||||
const addModule = React.useCallback(
|
||||
(typeId: number, slot: ShipSnapshotSlotsType | "dronebay") => {
|
||||
setCurrentFit((oldFit: EsiFit | undefined) => {
|
||||
if (oldFit === undefined) return undefined;
|
||||
|
||||
let flag = 0;
|
||||
let flag = 0;
|
||||
|
||||
/* Find the first free slot for that slot-type. */
|
||||
if (slot !== "dronebay") {
|
||||
for (let i = slotStart[slot]; i < slotStart[slot] + shipSnapshot.slots[slot]; i++) {
|
||||
if (oldFit.items.find((item) => item.flag === i) !== undefined) continue;
|
||||
/* Find the first free slot for that slot-type. */
|
||||
if (slot !== "dronebay") {
|
||||
for (let i = slotStart[slot]; i < slotStart[slot] + shipSnapshot.slots[slot]; i++) {
|
||||
if (oldFit.items.find((item) => item.flag === i) !== undefined) continue;
|
||||
|
||||
flag = i;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
flag = 87;
|
||||
}
|
||||
|
||||
/* Couldn't find a free slot. */
|
||||
if (flag === 0) return oldFit;
|
||||
|
||||
return {
|
||||
...oldFit,
|
||||
items: [
|
||||
...oldFit.items,
|
||||
{
|
||||
flag: flag,
|
||||
type_id: typeId,
|
||||
quantity: 1,
|
||||
flag = i;
|
||||
break;
|
||||
}
|
||||
],
|
||||
};
|
||||
})
|
||||
}, [shipSnapshot.slots]);
|
||||
} else {
|
||||
flag = 87;
|
||||
}
|
||||
|
||||
/* Couldn't find a free slot. */
|
||||
if (flag === 0) return oldFit;
|
||||
|
||||
return {
|
||||
...oldFit,
|
||||
items: [
|
||||
...oldFit.items,
|
||||
{
|
||||
flag: flag,
|
||||
type_id: typeId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
[shipSnapshot.slots],
|
||||
);
|
||||
|
||||
const removeModule = React.useCallback((flag: number) => {
|
||||
setCurrentFit((oldFit: EsiFit | undefined) => {
|
||||
@@ -197,19 +200,22 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
|
||||
...oldFit,
|
||||
items: oldFit.items.filter((item) => item.flag !== flag),
|
||||
};
|
||||
})
|
||||
});
|
||||
}, []);
|
||||
|
||||
const changeHull = React.useCallback((typeId: number) => {
|
||||
const hullName = eveData?.typeIDs?.[typeId].name;
|
||||
const changeHull = React.useCallback(
|
||||
(typeId: number) => {
|
||||
const hullName = eveData?.typeIDs?.[typeId].name;
|
||||
|
||||
setCurrentFit({
|
||||
"name": `New ${hullName}`,
|
||||
"description": "",
|
||||
"ship_type_id": typeId,
|
||||
"items": []
|
||||
})
|
||||
}, [eveData]);
|
||||
setCurrentFit({
|
||||
name: `New ${hullName}`,
|
||||
description: "",
|
||||
ship_type_id: typeId,
|
||||
items: [],
|
||||
});
|
||||
},
|
||||
[eveData],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setShipSnapshot((oldSnapshot) => ({
|
||||
@@ -230,11 +236,11 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
|
||||
const snapshot = dogmaEngine.engine?.calculate(currentFit, props.skills);
|
||||
|
||||
const slots = {
|
||||
"hislot": 0,
|
||||
"medslot": 0,
|
||||
"lowslot": 0,
|
||||
"subsystem": 0,
|
||||
"rig": 0,
|
||||
hislot: 0,
|
||||
medslot: 0,
|
||||
lowslot: 0,
|
||||
subsystem: 0,
|
||||
rig: 0,
|
||||
};
|
||||
|
||||
slots.hislot = snapshot.hull.attributes.get(eveData?.attributeMapping?.hiSlots || 0)?.value || 0;
|
||||
@@ -266,7 +272,5 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
|
||||
setCurrentFit(props.fit);
|
||||
}, [props.fit]);
|
||||
|
||||
return <ShipSnapshotContext.Provider value={shipSnapshot}>
|
||||
{props.children}
|
||||
</ShipSnapshotContext.Provider>
|
||||
return <ShipSnapshotContext.Provider value={shipSnapshot}>{props.children}</ShipSnapshotContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
export { ShipSnapshotContext, ShipSnapshotProvider } from "./ShipSnapshotProvider";
|
||||
export type { EsiFit, ShipSnapshotItem, ShipSnapshotItemAttribute, ShipSnapshotItemAttributeEffect, ShipSnapshotSlotsType } from "./ShipSnapshotProvider";
|
||||
export type {
|
||||
EsiFit,
|
||||
ShipSnapshotItem,
|
||||
ShipSnapshotItemAttribute,
|
||||
ShipSnapshotItemAttributeEffect,
|
||||
ShipSnapshotSlotsType,
|
||||
} from "./ShipSnapshotProvider";
|
||||
|
||||
@@ -3,23 +3,21 @@ import React from "react";
|
||||
|
||||
import styles from "./ShipStatistics.module.css";
|
||||
|
||||
export const Category = (props: {headerLabel: string, headerContent: React.ReactNode, children: React.ReactNode}) => {
|
||||
export const Category = (props: { headerLabel: string; headerContent: React.ReactNode; children: React.ReactNode }) => {
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
|
||||
return <div className={styles.panel}>
|
||||
<div onClick={() => setExpanded((current) => !current)} className={styles.header}>
|
||||
<div>{props.headerLabel}</div>
|
||||
<div style={{textAlign: "right"}}>{props.headerContent}</div>
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div onClick={() => setExpanded((current) => !current)} className={styles.header}>
|
||||
<div>{props.headerLabel}</div>
|
||||
<div style={{ textAlign: "right" }}>{props.headerContent}</div>
|
||||
</div>
|
||||
|
||||
<div className={expanded ? styles.expanded : styles.collapsed}>
|
||||
{props.children}
|
||||
<div className={expanded ? styles.expanded : styles.collapsed}>{props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const CategoryLine = (props: {className?: string, children: React.ReactNode}) => {
|
||||
return <div className={ctlx(styles.line, props.className )}>
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
export const CategoryLine = (props: { className?: string; children: React.ReactNode }) => {
|
||||
return <div className={ctlx(styles.line, props.className)}>{props.children}</div>;
|
||||
};
|
||||
|
||||
@@ -1,57 +1,95 @@
|
||||
import React from "react";
|
||||
|
||||
import { useShipAttribute } from '../ShipAttribute';
|
||||
import { useShipAttribute } from "../ShipAttribute";
|
||||
|
||||
import styles from "./ShipStatistics.module.css";
|
||||
import clsx from "clsx";
|
||||
import { IconName, Icon } from "../Icon";
|
||||
|
||||
export const RechargeRateItem = (props: {name: string, icon: IconName}) => {
|
||||
export const RechargeRateItem = (props: { name: string; icon: IconName }) => {
|
||||
const stringValue = useShipAttribute({
|
||||
name: props.name,
|
||||
fixed: 1,
|
||||
});
|
||||
|
||||
if (stringValue == "0.0") {
|
||||
return <span className={styles.statistic}>
|
||||
return (
|
||||
<span className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name={props.icon} size={24} />
|
||||
</span>
|
||||
<span>No Module</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name={props.icon} size={24} />
|
||||
</span>
|
||||
<span>
|
||||
No Module
|
||||
</span>
|
||||
<span>{stringValue} hp/s</span>
|
||||
</span>
|
||||
}
|
||||
|
||||
return <span className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name={props.icon} size={24} />
|
||||
</span>
|
||||
<span>
|
||||
{stringValue} hp/s
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const RechargeRate = () => {
|
||||
const [moduleType, setModuleType] = React.useState("passiveShieldRecharge");
|
||||
const [showDropdown, setShowDropdown] = React.useState(false);
|
||||
|
||||
return <span className={styles.rechargeRate}>
|
||||
<div className={styles.rechargeRateDropdown} onClick={() => setShowDropdown((current) => !current)}>
|
||||
^
|
||||
</div>
|
||||
{ showDropdown && <div className={styles.rechargeRateDropdownContent}>
|
||||
<div onClick={() => { setModuleType("armorRepairRate"); setShowDropdown(false); }} className={clsx({[styles.rechargeRateDropdownContentSelected]: moduleType == "armorRepairRate"})}>Armor repair rate</div>
|
||||
<div onClick={() => { setModuleType("hullRepairRate"); setShowDropdown(false); }} className={clsx({[styles.rechargeRateDropdownContentSelected]: moduleType == "hullRepairRate"})}>Hull repair rate</div>
|
||||
<div onClick={() => { setModuleType("passiveShieldRecharge"); setShowDropdown(false); }} className={clsx({[styles.rechargeRateDropdownContentSelected]: moduleType == "passiveShieldRecharge"})}>Passive shield recharge</div>
|
||||
<div onClick={() => { setModuleType("shieldBoostRate"); setShowDropdown(false); }} className={clsx({[styles.rechargeRateDropdownContentSelected]: moduleType == "shieldBoostRate"})}>Shield boost rate</div>
|
||||
</div> }
|
||||
<div onClick={() => setShowDropdown((current) => !current)}>
|
||||
{ moduleType == "armorRepairRate" && <RechargeRateItem name="armorRepairRate" icon="armor-repair-rate" /> }
|
||||
{ moduleType == "hullRepairRate" && <RechargeRateItem name="hullRepairRate" icon="hull-repair-rate" /> }
|
||||
{ moduleType == "passiveShieldRecharge" && <RechargeRateItem name="passiveShieldRecharge" icon="passive-shield-recharge" /> }
|
||||
{ moduleType == "shieldBoostRate" && <RechargeRateItem name="shieldBoostRate" icon="shield-boost-rate" /> }
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
return (
|
||||
<span className={styles.rechargeRate}>
|
||||
<div className={styles.rechargeRateDropdown} onClick={() => setShowDropdown((current) => !current)}>
|
||||
^
|
||||
</div>
|
||||
{showDropdown && (
|
||||
<div className={styles.rechargeRateDropdownContent}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setModuleType("armorRepairRate");
|
||||
setShowDropdown(false);
|
||||
}}
|
||||
className={clsx({ [styles.rechargeRateDropdownContentSelected]: moduleType == "armorRepairRate" })}
|
||||
>
|
||||
Armor repair rate
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setModuleType("hullRepairRate");
|
||||
setShowDropdown(false);
|
||||
}}
|
||||
className={clsx({ [styles.rechargeRateDropdownContentSelected]: moduleType == "hullRepairRate" })}
|
||||
>
|
||||
Hull repair rate
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setModuleType("passiveShieldRecharge");
|
||||
setShowDropdown(false);
|
||||
}}
|
||||
className={clsx({ [styles.rechargeRateDropdownContentSelected]: moduleType == "passiveShieldRecharge" })}
|
||||
>
|
||||
Passive shield recharge
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setModuleType("shieldBoostRate");
|
||||
setShowDropdown(false);
|
||||
}}
|
||||
className={clsx({ [styles.rechargeRateDropdownContentSelected]: moduleType == "shieldBoostRate" })}
|
||||
>
|
||||
Shield boost rate
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div onClick={() => setShowDropdown((current) => !current)}>
|
||||
{moduleType == "armorRepairRate" && <RechargeRateItem name="armorRepairRate" icon="armor-repair-rate" />}
|
||||
{moduleType == "hullRepairRate" && <RechargeRateItem name="hullRepairRate" icon="hull-repair-rate" />}
|
||||
{moduleType == "passiveShieldRecharge" && (
|
||||
<RechargeRateItem name="passiveShieldRecharge" icon="passive-shield-recharge" />
|
||||
)}
|
||||
{moduleType == "shieldBoostRate" && <RechargeRateItem name="shieldBoostRate" icon="shield-boost-rate" />}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
|
||||
import { useShipAttribute } from '../ShipAttribute';
|
||||
import { useShipAttribute } from "../ShipAttribute";
|
||||
|
||||
import styles from "./ShipStatistics.module.css";
|
||||
|
||||
export const Resistance = (props: {name: string}) => {
|
||||
export const Resistance = (props: { name: string }) => {
|
||||
const stringValue = useShipAttribute({
|
||||
name: props.name,
|
||||
fixed: 0,
|
||||
@@ -23,9 +23,10 @@ export const Resistance = (props: {name: string}) => {
|
||||
type = "explosive";
|
||||
}
|
||||
|
||||
return <span className={styles.resistance}>
|
||||
<span className={styles.resistanceProgress} data-type={type} style={{width: `${stringValue}%`}}>
|
||||
return (
|
||||
<span className={styles.resistance}>
|
||||
<span className={styles.resistanceProgress} data-type={type} style={{ width: `${stringValue}%` }}></span>
|
||||
<span>{stringValue} %</span>
|
||||
</span>
|
||||
<span>{stringValue} %</span>
|
||||
</span>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,154 +1,153 @@
|
||||
.panel {
|
||||
background-color: #111111;
|
||||
color: #c5c5c5;
|
||||
font-size: 15px;
|
||||
width: 350px;
|
||||
background-color: #111111;
|
||||
color: #c5c5c5;
|
||||
font-size: 15px;
|
||||
width: 350px;
|
||||
}
|
||||
.panel > div {
|
||||
overflow-y: hidden;
|
||||
transition: max-height 0.5s ease-in-out;
|
||||
overflow-y: hidden;
|
||||
transition: max-height 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #1d1d1d;
|
||||
display: flex;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
justify-content: space-between;
|
||||
background-color: #1d1d1d;
|
||||
display: flex;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header > div {
|
||||
flex: 1;
|
||||
margin: 0px 10px;
|
||||
flex: 1;
|
||||
margin: 0px 10px;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
max-height: 0px;
|
||||
max-height: 0px;
|
||||
}
|
||||
.expanded {
|
||||
max-height: 180px;
|
||||
max-height: 180px;
|
||||
}
|
||||
|
||||
|
||||
.line {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
justify-content: space-between;
|
||||
line-height: 20px;
|
||||
margin: 10px 0px;
|
||||
display: flex;
|
||||
height: 20px;
|
||||
justify-content: space-between;
|
||||
line-height: 20px;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
.line > span {
|
||||
flex: 1;
|
||||
margin: 0px 5px;
|
||||
flex: 1;
|
||||
margin: 0px 5px;
|
||||
}
|
||||
|
||||
.statistic {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
.statistic > span:last-child {
|
||||
line-height: 24px;
|
||||
margin-left: 4px;
|
||||
line-height: 24px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.defense {
|
||||
margin: 20px 0px;
|
||||
margin: 20px 0px;
|
||||
}
|
||||
.defense:last-child {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.defenseShield {
|
||||
position: relative;
|
||||
top: -5px;
|
||||
position: relative;
|
||||
top: -5px;
|
||||
}
|
||||
.defenseShield > span:first-child {
|
||||
line-height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.defenseShield > span:first-child > img {
|
||||
padding-top: 4px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.defenseShield > span:last-child {
|
||||
line-height: 15px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.resistanceHeader {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
width: 50px;
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.resistance {
|
||||
background-color: #252124;
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
margin-left: 5px;
|
||||
position: relative;
|
||||
width: 50px;
|
||||
background-color: #252124;
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
margin-left: 5px;
|
||||
position: relative;
|
||||
width: 50px;
|
||||
}
|
||||
.resistance > span {
|
||||
display: inline-block;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
display: inline-block;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.resistance > .resistanceProgress {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.resistance > .resistanceProgress[data-type="em"] {
|
||||
background-color: #195e8c;
|
||||
background-color: #195e8c;
|
||||
}
|
||||
.resistance > .resistanceProgress[data-type="thermal"] {
|
||||
background-color: #8c1919;
|
||||
background-color: #8c1919;
|
||||
}
|
||||
.resistance > .resistanceProgress[data-type="kinetic"] {
|
||||
background-color: #727272;
|
||||
background-color: #727272;
|
||||
}
|
||||
.resistance > .resistanceProgress[data-type="explosive"] {
|
||||
background-color: #8c5e19;
|
||||
background-color: #8c5e19;
|
||||
}
|
||||
|
||||
.rechargeRate {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rechargeRateDropdown {
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
.rechargeRateDropdownContent {
|
||||
background-color: #111111;
|
||||
line-height: 25px;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
width: 150px;
|
||||
z-index: 10;
|
||||
background-color: #111111;
|
||||
line-height: 25px;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
width: 150px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.rechargeRateDropdownContent > div {
|
||||
cursor: pointer;
|
||||
padding: 0px 5px;
|
||||
cursor: pointer;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
.rechargeRateDropdownContentSelected {
|
||||
background-color: #727272;
|
||||
background-color: #727272;
|
||||
}
|
||||
|
||||
.rechargeRateDropdownContent > div:hover {
|
||||
background-color: #4e4e4e;
|
||||
background-color: #4e4e4e;
|
||||
}
|
||||
|
||||
.capacitorStable {
|
||||
color: #8dc169;
|
||||
color: #8dc169;
|
||||
}
|
||||
.capacitorUnstable {
|
||||
color: #ff454b;
|
||||
color: #ff454b;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
import { fullFit } from "../../.storybook/fits";
|
||||
|
||||
import { DogmaEngineProvider } from '../DogmaEngineProvider';
|
||||
import { EsiProvider } from '../EsiProvider';
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
|
||||
import { ShipStatistics } from './';
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
import { EsiProvider } from "../EsiProvider";
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { ShipSnapshotProvider } from "../ShipSnapshotProvider";
|
||||
import { ShipStatistics } from "./";
|
||||
|
||||
const meta: Meta<typeof ShipStatistics> = {
|
||||
component: ShipStatistics,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/ShipStatistics',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/ShipStatistics",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -32,13 +32,13 @@ const useShipSnapshotProvider: Decorator<Record<string, never>> = (Story, contex
|
||||
</EsiProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
decorators: [useShipSnapshotProvider],
|
||||
parameters: {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { ShipAttribute } from '../ShipAttribute';
|
||||
import { ShipAttribute } from "../ShipAttribute";
|
||||
import { EveDataContext } from "../EveDataProvider";
|
||||
import { ShipSnapshotContext } from "../ShipSnapshotProvider";
|
||||
|
||||
@@ -29,184 +29,219 @@ export const ShipStatistics = () => {
|
||||
const hours = Math.floor(capacitorDepletesIn / 3600);
|
||||
const minutes = Math.floor((capacitorDepletesIn % 3600) / 60);
|
||||
const seconds = Math.floor(capacitorDepletesIn % 60);
|
||||
capacitorState = `Depletes in ${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||
capacitorState = `Depletes in ${hours.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||
} else {
|
||||
capacitorState = "Stable";
|
||||
}
|
||||
}
|
||||
|
||||
return <div>
|
||||
<Category headerLabel="Capacitor" headerContent={
|
||||
<span className={clsx({[styles.capacitorStable]: capacitorState === "Stable", [styles.capacitorUnstable]: capacitorState !== "Stable"})}>{capacitorState}</span>
|
||||
}>
|
||||
<CategoryLine>
|
||||
<span>
|
||||
<ShipAttribute name="capacitorCapacity" fixed={1} /> GJ / <ShipAttribute name="rechargeRate" fixed={2} divideBy={1000} /> s
|
||||
</span>
|
||||
</CategoryLine>
|
||||
<CategoryLine>
|
||||
<span>
|
||||
Δ <ShipAttribute name="capacitorPeakDelta" fixed={1} /> GJ/s (<ShipAttribute name="capacitorPeakDeltaPercentage" fixed={1} />%)
|
||||
</span>
|
||||
</CategoryLine>
|
||||
</Category>
|
||||
return (
|
||||
<div>
|
||||
<Category
|
||||
headerLabel="Capacitor"
|
||||
headerContent={
|
||||
<span
|
||||
className={clsx({
|
||||
[styles.capacitorStable]: capacitorState === "Stable",
|
||||
[styles.capacitorUnstable]: capacitorState !== "Stable",
|
||||
})}
|
||||
>
|
||||
{capacitorState}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<CategoryLine>
|
||||
<span>
|
||||
<ShipAttribute name="capacitorCapacity" fixed={1} /> GJ /{" "}
|
||||
<ShipAttribute name="rechargeRate" fixed={2} divideBy={1000} /> s
|
||||
</span>
|
||||
</CategoryLine>
|
||||
<CategoryLine>
|
||||
<span>
|
||||
Δ <ShipAttribute name="capacitorPeakDelta" fixed={1} /> GJ/s (
|
||||
<ShipAttribute name="capacitorPeakDeltaPercentage" fixed={1} />
|
||||
%)
|
||||
</span>
|
||||
</CategoryLine>
|
||||
</Category>
|
||||
|
||||
<Category headerLabel="Offense" headerContent={
|
||||
<span>? dps</span>
|
||||
}>
|
||||
<CategoryLine>
|
||||
<span>
|
||||
? dps
|
||||
</span>
|
||||
<span>
|
||||
? HP
|
||||
</span>
|
||||
</CategoryLine>
|
||||
</Category>
|
||||
<Category headerLabel="Offense" headerContent={<span>? dps</span>}>
|
||||
<CategoryLine>
|
||||
<span>? dps</span>
|
||||
<span>? HP</span>
|
||||
</CategoryLine>
|
||||
</Category>
|
||||
|
||||
<Category headerLabel="Defense" headerContent={
|
||||
<span><ShipAttribute name="ehp" fixed={0} /> ehp</span>
|
||||
}>
|
||||
<CategoryLine>
|
||||
<RechargeRate />
|
||||
<span style={{flex: 2}}>
|
||||
<span className={styles.resistanceHeader}><Icon name="em-resistance" size={28} /></span>
|
||||
<span className={styles.resistanceHeader}><Icon name="thermal-resistance" size={28} /></span>
|
||||
<span className={styles.resistanceHeader}><Icon name="kinetic-resistance" size={28} /></span>
|
||||
<span className={styles.resistanceHeader}><Icon name="explosive-resistance" size={28} /></span>
|
||||
</span>
|
||||
</CategoryLine>
|
||||
<CategoryLine className={styles.defense}>
|
||||
<span className={clsx(styles.statistic, styles.defenseShield)}>
|
||||
<Category
|
||||
headerLabel="Defense"
|
||||
headerContent={
|
||||
<span>
|
||||
<Icon name="shield-hp" size={24} />
|
||||
<ShipAttribute name="ehp" fixed={0} /> ehp
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="shieldCapacity" fixed={0} /> hp<br/>
|
||||
<ShipAttribute name="shieldRechargeRate" fixed={0} divideBy={1000} /> s<br/>
|
||||
}
|
||||
>
|
||||
<CategoryLine>
|
||||
<RechargeRate />
|
||||
<span style={{ flex: 2 }}>
|
||||
<span className={styles.resistanceHeader}>
|
||||
<Icon name="em-resistance" size={28} />
|
||||
</span>
|
||||
<span className={styles.resistanceHeader}>
|
||||
<Icon name="thermal-resistance" size={28} />
|
||||
</span>
|
||||
<span className={styles.resistanceHeader}>
|
||||
<Icon name="kinetic-resistance" size={28} />
|
||||
</span>
|
||||
<span className={styles.resistanceHeader}>
|
||||
<Icon name="explosive-resistance" size={28} />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span style={{flex: 2}}>
|
||||
<Resistance name="shieldEmDamageResonance" />
|
||||
<Resistance name="shieldThermalDamageResonance" />
|
||||
<Resistance name="shieldKineticDamageResonance" />
|
||||
<Resistance name="shieldExplosiveDamageResonance" />
|
||||
</span>
|
||||
</CategoryLine>
|
||||
<CategoryLine className={styles.defense}>
|
||||
<span className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="armor-hp" size={24} />
|
||||
</CategoryLine>
|
||||
<CategoryLine className={styles.defense}>
|
||||
<span className={clsx(styles.statistic, styles.defenseShield)}>
|
||||
<span>
|
||||
<Icon name="shield-hp" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="shieldCapacity" fixed={0} /> hp
|
||||
<br />
|
||||
<ShipAttribute name="shieldRechargeRate" fixed={0} divideBy={1000} /> s<br />
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="armorHP" fixed={0} /> hp
|
||||
<span style={{ flex: 2 }}>
|
||||
<Resistance name="shieldEmDamageResonance" />
|
||||
<Resistance name="shieldThermalDamageResonance" />
|
||||
<Resistance name="shieldKineticDamageResonance" />
|
||||
<Resistance name="shieldExplosiveDamageResonance" />
|
||||
</span>
|
||||
</span>
|
||||
<span style={{flex: 2}}>
|
||||
<Resistance name="armorEmDamageResonance" />
|
||||
<Resistance name="armorThermalDamageResonance" />
|
||||
<Resistance name="armorKineticDamageResonance" />
|
||||
<Resistance name="armorExplosiveDamageResonance" />
|
||||
</span>
|
||||
</CategoryLine>
|
||||
<CategoryLine className={styles.defense}>
|
||||
<span className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="hull-hp" size={24} />
|
||||
</CategoryLine>
|
||||
<CategoryLine className={styles.defense}>
|
||||
<span className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="armor-hp" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="armorHP" fixed={0} /> hp
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="hp" fixed={0} /> hp
|
||||
<span style={{ flex: 2 }}>
|
||||
<Resistance name="armorEmDamageResonance" />
|
||||
<Resistance name="armorThermalDamageResonance" />
|
||||
<Resistance name="armorKineticDamageResonance" />
|
||||
<Resistance name="armorExplosiveDamageResonance" />
|
||||
</span>
|
||||
</span>
|
||||
<span style={{flex: 2}}>
|
||||
<Resistance name="emDamageResonance" />
|
||||
<Resistance name="thermalDamageResonance" />
|
||||
<Resistance name="kineticDamageResonance" />
|
||||
<Resistance name="explosiveDamageResonance" />
|
||||
</span>
|
||||
</CategoryLine>
|
||||
</Category>
|
||||
</CategoryLine>
|
||||
<CategoryLine className={styles.defense}>
|
||||
<span className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="hull-hp" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="hp" fixed={0} /> hp
|
||||
</span>
|
||||
</span>
|
||||
<span style={{ flex: 2 }}>
|
||||
<Resistance name="emDamageResonance" />
|
||||
<Resistance name="thermalDamageResonance" />
|
||||
<Resistance name="kineticDamageResonance" />
|
||||
<Resistance name="explosiveDamageResonance" />
|
||||
</span>
|
||||
</CategoryLine>
|
||||
</Category>
|
||||
|
||||
<Category headerLabel="Targeting" headerContent={
|
||||
<span><ShipAttribute name="maxTargetRange" fixed={2} divideBy={1000} /> km</span>
|
||||
}>
|
||||
<CategoryLine>
|
||||
<span title="Scan Strength" className={styles.statistic}>
|
||||
<Category
|
||||
headerLabel="Targeting"
|
||||
headerContent={
|
||||
<span>
|
||||
<Icon name="sensor-strength" size={24} />
|
||||
<ShipAttribute name="maxTargetRange" fixed={2} divideBy={1000} /> km
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="scanStrength" fixed={2} /> points
|
||||
}
|
||||
>
|
||||
<CategoryLine>
|
||||
<span title="Scan Strength" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="sensor-strength" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="scanStrength" fixed={2} /> points
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span title="Scan Resolution" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="scan-resolution" size={24} />
|
||||
<span title="Scan Resolution" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="scan-resolution" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="scanResolution" fixed={0} /> mm
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="scanResolution" fixed={0} /> mm
|
||||
</CategoryLine>
|
||||
<CategoryLine>
|
||||
<span title="Signature Radius" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="signature-radius" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="signatureRadius" fixed={0} /> m
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</CategoryLine>
|
||||
<CategoryLine>
|
||||
<span title="Signature Radius" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="signature-radius" size={24} />
|
||||
<span title="Maximum Locked Targets" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="maximum-locked-targets" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="maxLockedTargets" fixed={0} />x
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="signatureRadius" fixed={0} /> m
|
||||
</span>
|
||||
</span>
|
||||
<span title="Maximum Locked Targets" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="maximum-locked-targets" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="maxLockedTargets" fixed={0} />x
|
||||
</span>
|
||||
</span>
|
||||
</CategoryLine>
|
||||
</Category>
|
||||
</CategoryLine>
|
||||
</Category>
|
||||
|
||||
<Category headerLabel="Navigation" headerContent={
|
||||
<span><ShipAttribute name="maxVelocity" fixed={1} /> m/s</span>
|
||||
}>
|
||||
<CategoryLine>
|
||||
<span title="Mass" className={styles.statistic}>
|
||||
<Category
|
||||
headerLabel="Navigation"
|
||||
headerContent={
|
||||
<span>
|
||||
<Icon name="mass" size={24} />
|
||||
<ShipAttribute name="maxVelocity" fixed={1} /> m/s
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="mass" fixed={2} divideBy={1000} /> t
|
||||
}
|
||||
>
|
||||
<CategoryLine>
|
||||
<span title="Mass" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="mass" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="mass" fixed={2} divideBy={1000} /> t
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span title="Inertia Modifier" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="inertia-modifier" size={24} />
|
||||
<span title="Inertia Modifier" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="inertia-modifier" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="agility" fixed={4} />x
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="agility" fixed={4} />x
|
||||
</CategoryLine>
|
||||
<CategoryLine>
|
||||
<span title="Ship Warp Speed" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="warp-speed" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="warpSpeedMultiplier" fixed={2} /> AU/s
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</CategoryLine>
|
||||
<CategoryLine>
|
||||
<span title="Ship Warp Speed" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="warp-speed" size={24} />
|
||||
<span title="Align Time" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="align-time" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="alignTime" fixed={2} />s
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="warpSpeedMultiplier" fixed={2} /> AU/s
|
||||
</span>
|
||||
</span>
|
||||
<span title="Align Time" className={styles.statistic}>
|
||||
<span>
|
||||
<Icon name="align-time" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="alignTime" fixed={2} />s
|
||||
</span>
|
||||
</span>
|
||||
</CategoryLine>
|
||||
</Category>
|
||||
</div>
|
||||
</CategoryLine>
|
||||
</Category>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
.header {
|
||||
display: flex;
|
||||
height: var(--height);
|
||||
line-height: var(--height);
|
||||
padding: 2px 0;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
height: var(--height);
|
||||
line-height: var(--height);
|
||||
padding: 2px 0;
|
||||
user-select: none;
|
||||
}
|
||||
.header > span {
|
||||
margin-left: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.headerHover:hover {
|
||||
background-color: #4f4f4f;
|
||||
background-color: #4f4f4f;
|
||||
}
|
||||
|
||||
.header1 {
|
||||
background-color: #1d1d1d;
|
||||
background-color: #1d1d1d;
|
||||
}
|
||||
|
||||
.headerText {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.headerAction {
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.headerAction:hover {
|
||||
opacity: 1.0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: 20px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.leaf {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.leafIcon {
|
||||
left: -16px;
|
||||
margin-top: 2px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
margin-top: 2px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
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';
|
||||
import { TreeListing } from "./";
|
||||
import { EsiProvider } from "../EsiProvider";
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { EsiFit, ShipSnapshotProvider } from "../ShipSnapshotProvider";
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
|
||||
const meta: Meta<typeof TreeListing> = {
|
||||
component: TreeListing,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/TreeListing',
|
||||
tags: ["autodocs"],
|
||||
title: "Component/TreeListing",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TreeListing>;
|
||||
|
||||
const withEsiProvider: Decorator<{ changeHull: (typeId: number) => void, changeFit: (fit: EsiFit) => void }> = (Story, context) => {
|
||||
const withEsiProvider: Decorator<{ changeHull: (typeId: number) => void; changeFit: (fit: EsiFit) => void }> = (
|
||||
Story,
|
||||
context,
|
||||
) => {
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<EsiProvider setSkills={console.log}>
|
||||
<DogmaEngineProvider>
|
||||
<ShipSnapshotProvider {...context.parameters.snapshot}>
|
||||
<div style={{height: "400px"}}>
|
||||
<div style={{ height: "400px" }}>
|
||||
<Story />
|
||||
</div>
|
||||
</ShipSnapshotProvider>
|
||||
@@ -32,7 +35,7 @@ const withEsiProvider: Decorator<{ changeHull: (typeId: number) => void, changeF
|
||||
</EsiProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
@@ -44,6 +47,6 @@ export const Default: Story = {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
skills: {},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,62 +9,88 @@ interface Tree {
|
||||
size: number;
|
||||
}
|
||||
|
||||
export const TreeContext = React.createContext<Tree>({size: 24});
|
||||
export const TreeContext = React.createContext<Tree>({ 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<HTMLDivElement, MouseEvent>) => void }) => {
|
||||
export const TreeHeaderAction = (props: {
|
||||
icon: IconName;
|
||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
}) => {
|
||||
const tree = React.useContext(TreeContext);
|
||||
|
||||
return <div className={styles.headerAction} onClick={props.onClick}>
|
||||
<Icon name={props.icon} size={tree.size} />
|
||||
</div>
|
||||
}
|
||||
return (
|
||||
<div className={styles.headerAction} onClick={props.onClick}>
|
||||
<Icon name={props.icon} size={tree.size} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Header for a listing.
|
||||
*/
|
||||
export const TreeHeader = (props: { icon?: string, text: string, action?: React.ReactNode }) => {
|
||||
export const TreeHeader = (props: { icon?: string; text: string; action?: React.ReactNode }) => {
|
||||
const tree = React.useContext(TreeContext);
|
||||
|
||||
return <>
|
||||
{props.icon !== undefined && <span>
|
||||
<img src={props.icon} height={tree.size} width={tree.size} alt="" />
|
||||
</span>}
|
||||
<span className={styles.headerText}>
|
||||
{props.text}
|
||||
</span>
|
||||
{props.action && <span>
|
||||
{props.action}
|
||||
</span>}
|
||||
</>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{props.icon !== undefined && (
|
||||
<span>
|
||||
<img src={props.icon} height={tree.size} width={tree.size} alt="" />
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.headerText}>{props.text}</span>
|
||||
{props.action && <span>{props.action}</span>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TreeLeaf = (props: { level: number, height?: number, icon?: IconName, iconTitle?: string, content: string, onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void }) => {
|
||||
export const TreeLeaf = (props: {
|
||||
level: number;
|
||||
height?: number;
|
||||
icon?: IconName;
|
||||
iconTitle?: string;
|
||||
content: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
}) => {
|
||||
const stylesHeader = styles[`header${props.level}`];
|
||||
|
||||
const height = props.height ?? 20;
|
||||
const style = { "--height": `${height}px` } as React.CSSProperties;
|
||||
|
||||
return <div>
|
||||
<TreeContext.Provider value={{size: height}}>
|
||||
<div style={style} className={clsx(styles.header, stylesHeader, {[styles.headerHover]: props.onClick !== undefined, [styles.leaf]: props.onClick !== undefined})} onClick={props.onClick}>
|
||||
{props.icon !== undefined && <span className={styles.leafIcon}>
|
||||
<Icon name={props.icon} size={12} title={props.iconTitle} />
|
||||
</span>}
|
||||
<span className={styles.headerText}>
|
||||
{props.content}
|
||||
</span>
|
||||
return (
|
||||
<div>
|
||||
<TreeContext.Provider value={{ size: height }}>
|
||||
<div
|
||||
style={style}
|
||||
className={clsx(styles.header, stylesHeader, {
|
||||
[styles.headerHover]: props.onClick !== undefined,
|
||||
[styles.leaf]: props.onClick !== undefined,
|
||||
})}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.icon !== undefined && (
|
||||
<span className={styles.leafIcon}>
|
||||
<Icon name={props.icon} size={12} title={props.iconTitle} />
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.headerText}>{props.content}</span>
|
||||
</div>
|
||||
</TreeContext.Provider>
|
||||
</div>;
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tree listing for hulls, modules, and charges.
|
||||
*/
|
||||
export const TreeListing = (props: { level: number, header: React.ReactNode, height?: number, getChildren: () => React.ReactNode }) => {
|
||||
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}`];
|
||||
@@ -79,17 +105,21 @@ export const TreeListing = (props: { level: number, header: React.ReactNode, hei
|
||||
children = props.getChildren();
|
||||
}
|
||||
|
||||
return <div>
|
||||
<TreeContext.Provider value={{size: height}}>
|
||||
<div style={style} className={clsx(styles.header, styles.headerHover, stylesHeader)} onClick={() => setExpanded((current) => !current)}>
|
||||
<span>
|
||||
<Icon name={expanded ? "menu-expand" : "menu-collapse"} size={12} />
|
||||
</span>
|
||||
{props.header}
|
||||
</div>
|
||||
<div className={clsx(styles.content, stylesContent)}>
|
||||
{children}
|
||||
</div>
|
||||
</TreeContext.Provider>
|
||||
</div>
|
||||
return (
|
||||
<div>
|
||||
<TreeContext.Provider value={{ size: height }}>
|
||||
<div
|
||||
style={style}
|
||||
className={clsx(styles.header, styles.headerHover, stylesHeader)}
|
||||
onClick={() => setExpanded((current) => !current)}
|
||||
>
|
||||
<span>
|
||||
<Icon name={expanded ? "menu-expand" : "menu-collapse"} size={12} />
|
||||
</span>
|
||||
{props.header}
|
||||
</div>
|
||||
<div className={clsx(styles.content, stylesContent)}>{children}</div>
|
||||
</TreeContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
40
src/index.ts
40
src/index.ts
@@ -1,20 +1,20 @@
|
||||
export * from './CalculationDetail';
|
||||
export * from './DogmaEngineProvider';
|
||||
export * from './EsiCharacterSelection';
|
||||
export * from './EsiProvider';
|
||||
export * from './EveDataProvider';
|
||||
export * from './EveShipFitHash';
|
||||
export * from './EveShipFitLink';
|
||||
export * from './FitButtonBar';
|
||||
export * from './FormatEftToEsi';
|
||||
export * from './FormatAsEft';
|
||||
export * from './HardwareListing';
|
||||
export * from './HullListing';
|
||||
export * from './Icon';
|
||||
export * from './LocalFitProvider';
|
||||
export * from './ModalDialog';
|
||||
export * from './ShipAttribute';
|
||||
export * from './ShipFit';
|
||||
export * from './ShipFitExtended';
|
||||
export * from './ShipSnapshotProvider';
|
||||
export * from './ShipStatistics';
|
||||
export * from "./CalculationDetail";
|
||||
export * from "./DogmaEngineProvider";
|
||||
export * from "./EsiCharacterSelection";
|
||||
export * from "./EsiProvider";
|
||||
export * from "./EveDataProvider";
|
||||
export * from "./EveShipFitHash";
|
||||
export * from "./EveShipFitLink";
|
||||
export * from "./FitButtonBar";
|
||||
export * from "./FormatEftToEsi";
|
||||
export * from "./FormatAsEft";
|
||||
export * from "./HardwareListing";
|
||||
export * from "./HullListing";
|
||||
export * from "./Icon";
|
||||
export * from "./LocalFitProvider";
|
||||
export * from "./ModalDialog";
|
||||
export * from "./ShipAttribute";
|
||||
export * from "./ShipFit";
|
||||
export * from "./ShipFitExtended";
|
||||
export * from "./ShipSnapshotProvider";
|
||||
export * from "./ShipStatistics";
|
||||
|
||||
2
src/types.d.ts
vendored
2
src/types.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
declare module '*.module.css' {
|
||||
declare module "*.module.css" {
|
||||
const css: any;
|
||||
export default css;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user