chore: apply prettier coding-style to the whole project (#63)

This commit is contained in:
Patric Stout
2024-03-03 09:59:30 +01:00
committed by GitHub
parent 64d41f2f4d
commit 9260537e3f
77 changed files with 2166 additions and 1768 deletions

2
.editorconfig Normal file
View File

@@ -0,0 +1,2 @@
[*]
max_line_length=120

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
src/EveDataProvider/esf_pb2.js
src/EveDataProvider/protobuf.js

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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,
}
},
},
};

View File

@@ -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>
);
};

View File

@@ -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: () => (

View File

@@ -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>;
};

View File

@@ -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%;
}

View File

@@ -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],
};

View File

@@ -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>
);
};

View File

@@ -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 };
};
}

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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>;
};

View File

@@ -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) {

View File

@@ -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;
}[];
}

View File

@@ -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: () => (

View File

@@ -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>;
};

View File

@@ -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],
};

View File

@@ -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>;
};

View File

@@ -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: {},
}
},
},
};

View File

@@ -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>;
};

View File

@@ -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>
</>
);
};

View File

@@ -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;
}

View File

@@ -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: {},
}
},
},
};

View File

@@ -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>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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>
</>
}
</>
);
};

View File

@@ -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: {},
}
},
},
};

View File

@@ -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>;
};

View File

@@ -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>;

View File

@@ -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>;
};

View File

@@ -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;
}

View File

@@ -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],

View File

@@ -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>
);
};

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -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: {},
}
},
},
};

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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} />;
};

View File

@@ -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 />

View File

@@ -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>;
};

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>;
};

View File

@@ -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: {},
}
},
},
};

View File

@@ -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>;
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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: {},
}
},
},
};

View File

@@ -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>
);
};

View File

@@ -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>
}
);
};

View File

@@ -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;
}

View File

@@ -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,
}
},
},
};

View File

@@ -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>
);
};

View File

@@ -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: {

View File

@@ -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>;
};

View File

@@ -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";

View File

@@ -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>;
};

View File

@@ -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>
);
};

View File

@@ -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>
}
);
};

View File

@@ -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;
}

View File

@@ -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,
}
},
},
};

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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: {},
}
},
},
};

View File

@@ -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>
);
};

View File

@@ -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
View File

@@ -1,4 +1,4 @@
declare module '*.module.css' {
declare module "*.module.css" {
const css: any;
export default css;
}