Added entity span
This commit is contained in:
27
components/EntityIcon.module.css
Normal file
27
components/EntityIcon.module.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
.span {
|
||||||
|
background: #DDD;
|
||||||
|
font-size: 2em;
|
||||||
|
border: 1px solid white;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-end: 0.2em;
|
||||||
|
inset-block-end: 0;
|
||||||
|
font-size: 50%;
|
||||||
|
color: white;
|
||||||
|
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
margin-inline-end: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
29
components/EntityIcon.tsx
Normal file
29
components/EntityIcon.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {FC, HTMLProps, useMemo} from "react"
|
||||||
|
import {Entity} from "../src/types"
|
||||||
|
import {useDetails} from "../src/hooks/useDetails"
|
||||||
|
import styles from './EntityIcon.module.css'
|
||||||
|
|
||||||
|
interface Props extends Omit<HTMLProps<HTMLSpanElement>, 'value'> {
|
||||||
|
value: Entity|string
|
||||||
|
amount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EntityIcon: FC<Props> = ({value, amount, ...rest}) => {
|
||||||
|
const details = useDetails()
|
||||||
|
const entity = useMemo<Entity>(() => {
|
||||||
|
return typeof value === "object"
|
||||||
|
? value
|
||||||
|
: details.find(detail => detail.href === value) ?? {
|
||||||
|
href: value,
|
||||||
|
name: value,
|
||||||
|
image: value,
|
||||||
|
recipe: undefined
|
||||||
|
}
|
||||||
|
}, [details, value])
|
||||||
|
|
||||||
|
return <span className={styles.span} {...rest}>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img className={styles.img} src={`https://wiki.factorio.com${entity.image}`} alt={entity.name}/>
|
||||||
|
{amount !== undefined && <span className={styles.amount}>{amount}</span>}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
73
components/EntitySpan.module.css
Normal file
73
components/EntitySpan.module.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
.span {
|
||||||
|
background: #DDD;
|
||||||
|
font-size: 1em;
|
||||||
|
border: 1px solid white;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
--background: lightsalmon;
|
||||||
|
--arrow-width: 0.6em;
|
||||||
|
--arrow-height: 0.4em;
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: calc(100% + var(--arrow-width));
|
||||||
|
top: -500%;
|
||||||
|
bottom: -500%;
|
||||||
|
margin: auto 0;
|
||||||
|
width: max-content;
|
||||||
|
height: max-content;
|
||||||
|
background: var(--background);
|
||||||
|
padding: 0.5em;
|
||||||
|
border-radius: 0.7em;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.span:hover > .tooltip {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip::before {
|
||||||
|
content: "";
|
||||||
|
border-style: solid;
|
||||||
|
top: -500%;
|
||||||
|
bottom: -500%;
|
||||||
|
margin: auto 0;
|
||||||
|
height: max-content;
|
||||||
|
border-width: var(--arrow-height) var(--arrow-width) var(--arrow-height) 0;
|
||||||
|
border-color: transparent var(--background) transparent transparent;
|
||||||
|
position: absolute;
|
||||||
|
left: calc(var(--arrow-width) * -1 + 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
margin-inline-end: 0.2em;
|
||||||
|
transform: translateY(0.1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-block: 1em 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong:first-child {
|
||||||
|
margin-block-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftClick {
|
||||||
|
height: 1em;
|
||||||
|
transform: translateY(0.1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightClick {
|
||||||
|
height: 1em;
|
||||||
|
transform: scaleX(-1) translateY(0.1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickBtn {
|
||||||
|
fill: red;
|
||||||
|
}
|
||||||
47
components/EntitySpan.tsx
Normal file
47
components/EntitySpan.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import {FC, HTMLProps, useMemo} from "react"
|
||||||
|
import {Entity} from "../src/types"
|
||||||
|
import {useDetails} from "../src/hooks/useDetails"
|
||||||
|
import styles from './EntitySpan.module.css'
|
||||||
|
import {RecipeSpan} from "./Recipe";
|
||||||
|
import {LeftClickIcon} from "./LeftClickIcon";
|
||||||
|
|
||||||
|
interface Props extends Omit<HTMLProps<HTMLSpanElement>, 'value'> {
|
||||||
|
value: Entity|string
|
||||||
|
leftClickText?: string
|
||||||
|
rightClickText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EntitySpan: FC<Props> = ({value, leftClickText, rightClickText, ...rest}) => {
|
||||||
|
const details = useDetails()
|
||||||
|
const entity = useMemo<Entity>(() => {
|
||||||
|
return typeof value === "object"
|
||||||
|
? value
|
||||||
|
: details.find(detail => detail.href === value) ?? {
|
||||||
|
href: value,
|
||||||
|
name: value,
|
||||||
|
image: value,
|
||||||
|
recipe: undefined
|
||||||
|
}
|
||||||
|
}, [details, value])
|
||||||
|
|
||||||
|
return <span className={styles.span} {...rest}>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img className={styles.img} src={`https://wiki.factorio.com${entity.image}`} alt={entity.name}/>
|
||||||
|
{entity.name}
|
||||||
|
<div className={styles.tooltip}>
|
||||||
|
{entity.recipe && (
|
||||||
|
<>
|
||||||
|
<div className={styles.strong}>Recipe</div>
|
||||||
|
<RecipeSpan recipe={entity.recipe}/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(leftClickText || rightClickText) && (
|
||||||
|
<>
|
||||||
|
<div className={styles.strong}>Actions</div>
|
||||||
|
{leftClickText && <div><LeftClickIcon className={styles.leftClick} classClick={styles.clickBtn}/> {leftClickText}</div>}
|
||||||
|
{rightClickText && <div><LeftClickIcon className={styles.rightClick} classClick={styles.clickBtn}/> {rightClickText}</div>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import {FactorySelect} from "./FactorySelect";
|
|||||||
import {useDetails} from "../src/hooks/useDetails";
|
import {useDetails} from "../src/hooks/useDetails";
|
||||||
import {Entity} from "../src/types";
|
import {Entity} from "../src/types";
|
||||||
import styles from "./Group.module.css"
|
import styles from "./Group.module.css"
|
||||||
|
import {EntitySpan} from "./EntitySpan";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onRemove: () => void
|
onRemove: () => void
|
||||||
@@ -109,13 +110,12 @@ export const Group: FC<Props> = ({
|
|||||||
<h4>Inputs</h4>
|
<h4>Inputs</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
inputs.map(input => <li key={input}><a
|
inputs.map(input => <li key={input}><EntitySpan
|
||||||
href={'javascript:void(0)'}
|
value={input}
|
||||||
onClick={() => addIntermediateFactory(input)}
|
onClick={() => addIntermediateFactory(input)}
|
||||||
style={{color: basic.has(input) ? 'darkgreen' : exported.has(input) ? 'orange' : undefined}}
|
style={{color: basic.has(input) ? 'darkgreen' : exported.has(input) ? 'orange' : undefined}}
|
||||||
>
|
leftClickText={"Add to intermediate factories"}
|
||||||
{input}
|
/></li>)
|
||||||
</a></li>)
|
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,17 +123,16 @@ export const Group: FC<Props> = ({
|
|||||||
<h4>Suggestions</h4>
|
<h4>Suggestions</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{suggestions.map(suggestion => <li key={suggestion.href}>
|
{suggestions.map(suggestion => <li key={suggestion.href}>
|
||||||
<a
|
<EntitySpan
|
||||||
href={'javascript:void(0)'}
|
value={suggestion}
|
||||||
onClick={() => addOutputFactory(suggestion.href)}
|
onClick={() => addOutputFactory(suggestion.href)}
|
||||||
onContextMenu={event => {
|
onContextMenu={event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
onDoIgnore(suggestion.href)
|
onDoIgnore(suggestion.href)
|
||||||
}}
|
}}
|
||||||
style={{color: basic.has(suggestion.href) ? 'darkgreen' : exported.has(suggestion.href) ? 'orange' : undefined}}
|
leftClickText={"Add to output factories"}
|
||||||
>
|
rightClickText={"Exclude this recipe from suggestions"}
|
||||||
{suggestion.name}
|
/>
|
||||||
</a>
|
|
||||||
</li>)}
|
</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,3 +2,13 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(500px, max-content));
|
grid-template-columns: repeat(auto-fit, minmax(500px, max-content));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.missingFactories {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missingFactories > * {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {sortByProperty} from "../src/utils";
|
|||||||
import {useDetails} from "../src/hooks/useDetails";
|
import {useDetails} from "../src/hooks/useDetails";
|
||||||
import {Entity} from "../src/types";
|
import {Entity} from "../src/types";
|
||||||
import pako from 'pako';
|
import pako from 'pako';
|
||||||
|
import {EntitySpan} from "./EntitySpan";
|
||||||
|
|
||||||
interface Group {
|
interface Group {
|
||||||
name: string
|
name: string
|
||||||
@@ -108,15 +109,19 @@ export const HomeComponent: FC = () => {
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Missing factories</legend>
|
<legend>Missing factories</legend>
|
||||||
<ul>
|
<div className={styles.missingFactories}>
|
||||||
{
|
{ missingFactories.map(missing => (
|
||||||
missingFactories.map(missing => <li key={missing.href}
|
<EntitySpan
|
||||||
|
key={missing.href}
|
||||||
|
value={missing}
|
||||||
onContextMenu={event => {
|
onContextMenu={event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setExcludedSuggestions([...excludedSuggestions, missing.href])
|
setExcludedSuggestions([...excludedSuggestions, missing.href])
|
||||||
}}>{missing.name}</li>)
|
}}
|
||||||
}
|
rightClickText={"Exclude this recipe from suggestions"}
|
||||||
</ul>
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{
|
{
|
||||||
|
|||||||
16
components/LeftClickIcon.tsx
Normal file
16
components/LeftClickIcon.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import {FC} from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string
|
||||||
|
classBody?: string
|
||||||
|
classClick?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LeftClickIcon: FC<Props> = ({className, classBody, classClick}) => {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" className={className}>
|
||||||
|
<path className={classBody} d="M51.552,8.117v32.554l-3.095,0.009c-9.203,0.027-17.447,0.297-24.509,0.803l-0.291,0.021 c-0.026,1.733-0.036,3.521-0.036,5.378c0,24.854,1.527,45,26.379,45c24.854,0,26.379-20.146,26.379-45 C76.379,22.563,74.903,8.701,51.552,8.117z" />
|
||||||
|
<path className={classClick} id="click" d="M48.448,37.577V8.117C27.971,8.629,24.313,19.354,23.727,38.388C29.914,37.945,38.002,37.607,48.448,37.577z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
components/Recipe.module.css
Normal file
5
components/Recipe.module.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.recipe {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2em;
|
||||||
|
}
|
||||||
23
components/Recipe.tsx
Normal file
23
components/Recipe.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {FC} from "react"
|
||||||
|
import {Recipe} from "../src/types"
|
||||||
|
import {EntityIcon} from "./EntityIcon";
|
||||||
|
import styles from './Recipe.module.css'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
recipe: Recipe
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RecipeSpan: FC<Props> = ({recipe}) => {
|
||||||
|
const joinByPlus = (elems: JSX.Element[]) => elems.reduce((acc, curr) => {
|
||||||
|
if (acc.length) {
|
||||||
|
return [...acc, '+', curr]
|
||||||
|
} else {
|
||||||
|
return [curr]
|
||||||
|
}
|
||||||
|
}, [] as (JSX.Element|string)[])
|
||||||
|
const before = Object.entries({...recipe.prerequisites}).map(([key, amount]) => <EntityIcon key={key} value={key} amount={amount} />)
|
||||||
|
const after = Object.entries({...recipe.output}).map(([key, amount]) => <EntityIcon key={key} value={key} amount={amount} />)
|
||||||
|
return <span className={styles.recipe}>
|
||||||
|
{joinByPlus(before)} → {joinByPlus(after)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-select": "^5.4.0"
|
"react-select": "^5.4.0",
|
||||||
|
"react-tooltip": "^4.2.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "18.6.4",
|
"@types/node": "18.6.4",
|
||||||
|
|||||||
15
yarn.lock
15
yarn.lock
@@ -1727,7 +1727,7 @@ prelude-ls@^1.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||||
|
|
||||||
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.8.1:
|
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
@@ -1772,6 +1772,14 @@ react-select@^5.4.0:
|
|||||||
prop-types "^15.6.0"
|
prop-types "^15.6.0"
|
||||||
react-transition-group "^4.3.0"
|
react-transition-group "^4.3.0"
|
||||||
|
|
||||||
|
react-tooltip@^4.2.21:
|
||||||
|
version "4.2.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.21.tgz#840123ed86cf33d50ddde8ec8813b2960bfded7f"
|
||||||
|
integrity sha512-zSLprMymBDowknr0KVDiJ05IjZn9mQhhg4PRsqln0OZtURAJ1snt1xi5daZfagsh6vfsziZrc9pErPTDY1ACig==
|
||||||
|
dependencies:
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
uuid "^7.0.3"
|
||||||
|
|
||||||
react-transition-group@^4.3.0:
|
react-transition-group@^4.3.0:
|
||||||
version "4.4.5"
|
version "4.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
|
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
|
||||||
@@ -2071,6 +2079,11 @@ use-sync-external-store@1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
|
||||||
|
uuid@^7.0.3:
|
||||||
|
version "7.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
|
||||||
|
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
|
||||||
|
|
||||||
v8-compile-cache@^2.0.3:
|
v8-compile-cache@^2.0.3:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||||
|
|||||||
Reference in New Issue
Block a user