Stylings / Restructuring

This commit is contained in:
Sebastian Seedorf
2022-08-14 10:49:11 +02:00
parent 7682aeaea1
commit 76f508a847
17 changed files with 333 additions and 137 deletions

View File

@@ -1,32 +0,0 @@
.plane {
position: relative;
overflow: scroll;
overflow-scrolling: touch;
background-color: lightsalmon;
padding: 2em;
height: 80vh;
}
.tiny {
font-size: 0.5em;
margin-top: -1.6em;
}
.small {
font-size: 0.8em;
}
.node {
display: inline-block;
position: absolute;
width: 200px;
height: fit-content;
padding: 0.2em;
background-color: red;
}
.linkOut {
position: absolute;
top: 0.5em;
right: 0.5em;
}

View File

@@ -1,85 +0,0 @@
import {FC, useMemo} from "react";
import {EnrichedEntity, Recipe} from "../../src/types";
import styles from './ProducingGraph.module.css'
import {EntityIcon} from "../home/EntityIcon/EntityIcon";
import {sortByProperty} from "../../src/utils";
import Link from "next/link";
import {RecipeSpan} from "../home/Recipe/Recipe";
export interface ProducingNode {
inputs: string[]
outputs: string[]
name: string
icons?: (EnrichedEntity|string)[]
linkOut?: string
recipe?: Recipe
}
interface Props {
nodes: ProducingNode[]
inputs: string[]
}
export const ProducingGraph: FC<Props> = ({nodes, inputs}) => {
const rows: ProducingNode[][] = useMemo(() => {
const available = new Set(inputs)
let todo = [...nodes]
const result: ProducingNode[][] = []
while (todo.length) {
const amount = todo.length
const thisRow: string[] = []
result.push([])
todo = todo.filter((node) => {
if (node.inputs.every(input => available.has(input))) {
result[result.length - 1].push(node)
thisRow.push(...node.outputs)
return false
}
return true
})
thisRow.map(uid => available.add(uid))
result[result.length - 1].sort(sortByProperty(val => -val.outputs.length * 1000 + -val.inputs.length))
if (amount === todo.length) {
console.warn("Loop detected! Left over:", todo)
result.pop()
break
}
}
return result
}, [inputs, nodes])
return <div className={styles.plane}>
{inputs.map((input, idx) => <EntityIcon className={styles.input} style={{left: 75 * idx}} key={input} value={input} />)}
{rows.map((row, colIdx) => row.map((node, idx) => (
<div
className={styles.node}
key={node.name}
style={{left: 220*idx, top: 320*colIdx+100}}
>
{ node.linkOut && <Link className={styles.linkOut} href={node.linkOut}>🔗</Link> }
<h3>{node.name}</h3>
{ node.icons?.length ? <div className={styles.tiny}>
{node.icons.map((input) => <EntityIcon key={typeof input === "string" ? input : input.href} value={input} />)}
</div> : null }
{
node.recipe
? <RecipeSpan recipe={node.recipe}/>
: <>
<h4>Inputs</h4>
<div className={styles.small}>
{node.inputs.map((input) => <EntityIcon key={input} value={input} />)}
</div>
{node.outputs.length ? <>
<h4>Outputs</h4>
<div className={styles.small}>
{node.outputs.map((input) => <EntityIcon key={input} value={input} />)}
</div>
</>: null}
</>
}
</div>
)))}
</div>
}

View File

@@ -0,0 +1,20 @@
.plane {
border: 1px solid red;
padding: 2em;
width: fit-content;
display: flex;
flex-direction: column;
gap: 5em;
}
.row {
display: flex;
gap: 2em;
}
.node {
padding: 0.5em;
border-radius: 4px;
border: 1px solid #DDDDDD;
background-color: #EEE;
}

View File

@@ -0,0 +1,71 @@
import {FC, HTMLProps, PropsWithChildren, useMemo} from "react";
import styles from './ProducingGraph.module.css'
import {EntityIcon} from "../../home/EntityIcon/EntityIcon";
import {sortByProperty} from "../../../src/utils";
import {EnrichedEntity, Recipe} from "../../../src/types";
interface GraphNodeBase {
inputs: string[]
outputs: string[]
name: string
}
export type GraphNode<T extends Record<string, unknown>> = GraphNodeBase & T
interface Props<T extends {}> {
nodes: GraphNode<T>[]
inputs: string[]
outputs?: string[]
childType: FC<HTMLProps<HTMLDivElement> & {node: GraphNode<T>}>
}
export const ProducingGraph = <T extends Record<string, unknown>,>({nodes, inputs, outputs, childType: ChildType}: PropsWithChildren<Props<T>>) => {
const rows: GraphNode<T>[][] = useMemo(() => {
const available = new Set(inputs)
let todo = [...nodes]
const result: GraphNode<T>[][] = []
while (todo.length) {
const amount = todo.length
const thisRow: string[] = []
result.push([])
todo = todo.filter((node) => {
if (node.inputs.every(input => available.has(input))) {
result[result.length - 1].push(node)
thisRow.push(...node.outputs)
return false
}
return true
})
thisRow.map(uid => available.add(uid))
result[result.length - 1].sort(sortByProperty(val => -val.outputs.length * 1000 + -val.inputs.length))
if (amount === todo.length) {
console.warn("Loop detected! Left over:", todo)
result.pop()
break
}
}
return result
}, [inputs, nodes])
return <div className={styles.plane}>
<div className={styles.row}>
{inputs.map((input, idx) => <EntityIcon className={styles.input} key={input} value={input} />)}
</div>
{rows.map((row, colIdx) => {
return <div className={styles.row} key={colIdx}>
{
row.map((node) => <ChildType
className={styles.node}
key={node.name}
node={node}
/>)
}
</div>
})}
{outputs ? <div className={styles.row}>
{outputs.map((input, idx) => <EntityIcon className={styles.input} key={input} value={input} />)}
</div> : null }
</div>
}

View File

@@ -0,0 +1,5 @@
.root {
height: fit-content;
width: fit-content;
position: relative;
}

View File

@@ -0,0 +1,21 @@
import {FC, HTMLProps} from "react";
import {GraphNode} from "../../shared/ProducingGraph/ProducingGraph";
import {Recipe} from "../../../src/types";
import cx from "classnames";
import styles from "./NodeDetails.module.css";
import {RecipeSpan} from "../../home/Recipe/Recipe";
export type DetailGraphNode = GraphNode<{
recipes: Recipe[]
}>
interface Props extends HTMLProps<HTMLDivElement> {
node: DetailGraphNode
}
export const NodeDetails: FC<Props> = ({node, className, ...props}) => {
return <div {...props} className={cx(className, styles.root)}>
<h3>{node.name}</h3>
{node.recipes.map((recipe, idx) => <RecipeSpan key={idx} recipe={recipe}/>)}
</div>
}

View File

@@ -0,0 +1,18 @@
.root {
height: fit-content;
width: fit-content;
}
.tiny {
font-size: 0.5em;
margin-top: -1.6em;
}
.small {
font-size: 0.8em;
}
.linkOut {
margin-inline-end: 0.5em;
}

View File

@@ -0,0 +1,36 @@
import {FC, HTMLProps} from "react";
import {GraphNode} from "../../shared/ProducingGraph/ProducingGraph";
import {EnrichedEntity, Recipe} from "../../../src/types";
import cx from "classnames";
import styles from "./NodeOverview.module.css";
import {RecipeSpan} from "../../home/Recipe/Recipe";
import Link from "next/link";
import {EntityIcon} from "../../home/EntityIcon/EntityIcon";
export type OverviewGraphNode = GraphNode<{
icons: (EnrichedEntity|string)[]
linkOut: string
}>
interface Props extends HTMLProps<HTMLDivElement> {
node: OverviewGraphNode
}
export const NodeOverview: FC<Props> = ({node, className, ...props}) => {
return <div {...props} className={cx(className, styles.root)}>
<h3><span className={styles.linkOut}><Link href={node.linkOut}>🔗</Link></span>{node.name}</h3>
{ node.icons?.length ? <div className={styles.tiny}>
{node.icons.map((input) => <EntityIcon key={typeof input === "string" ? input : input.href} value={input} />)}
</div> : null }
<h4>Inputs</h4>
<div className={styles.small}>
{node.inputs.map((input) => <EntityIcon key={input} value={input} />)}
</div>
{node.outputs.length ? <>
<h4>Outputs</h4>
<div className={styles.small}>
{node.outputs.map((input) => <EntityIcon key={input} value={input} />)}
</div>
</>: null}
</div>
}

View File

@@ -1,8 +1,9 @@
import '../styles/globals.css' import '../styles/globals.css'
import type { AppProps } from 'next/app' import type { AppProps } from 'next/app'
import {GroupProvider} from "../components/contexts/GroupProvider"; import {GroupProvider} from "../components/contexts/GroupProvider";
import {FC} from "react";
function MyApp({ Component, pageProps }: AppProps) { const MyApp: FC<AppProps> = ({ Component, pageProps }) => {
return <GroupProvider> return <GroupProvider>
<Component {...pageProps} /> <Component {...pageProps} />
</GroupProvider> </GroupProvider>

17
pages/_document.tsx Normal file
View File

@@ -0,0 +1,17 @@
import {Html, Main, NextScript, DocumentProps, Head} from 'next/document';
import {FC} from "react";
const MyDocument: FC<DocumentProps> = ({ __NEXT_DATA__ }) => {
const pageProps: Record<string, unknown>|undefined = __NEXT_DATA__?.props?.pageProps;
return (
<Html>
<Head />
<body className={pageProps?.bodyClassName as string}>
<Main />
<NextScript />
</body>
</Html>
);
}
export default MyDocument

View File

@@ -1,17 +1,22 @@
import type { NextPage } from 'next' import type { NextPage } from 'next'
import Head from 'next/head' import Head from 'next/head'
import {Home} from "../components/home/Home"; import {Home} from "../components/home/Home";
import {useEffect} from "react";
const Page: NextPage = () => { const Page: NextPage = () => {
useEffect(() =>
{
document.body.classList.add("scroll");
});
return ( return (
<div> <>
<Head> <Head>
<title>Factorio Microservices</title> <title>Factorio Microservices</title>
<meta name="description" content="Create Factorio microservices" /> <meta name="description" content="Create Factorio microservices" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<Home/> <Home/>
</div> </>
) )
} }

View File

@@ -2,12 +2,15 @@ import type { NextPage } from 'next'
import Head from 'next/head' import Head from 'next/head'
import {useGroups} from "../../components/contexts/GroupProvider"; import {useGroups} from "../../components/contexts/GroupProvider";
import {useFactories} from "../../src/hooks/useFactories"; import {useFactories} from "../../src/hooks/useFactories";
import {ProducingGraph, ProducingNode} from "../../components/shared/ProducingGraph"; import {GraphNode, ProducingGraph} from "../../components/shared/ProducingGraph/ProducingGraph";
import {useMemo} from "react"; import {useMemo} from "react";
import {calculateInputs} from "../../src/calculateInputs"; import {calculateInputs} from "../../src/calculateInputs";
import {useRouter} from "next/router"; import {useRouter} from "next/router";
import {isNonNullable} from "../../src/utils"; import {groupBy, isNonNullable} from "../../src/utils";
import {EnrichedEntity} from "../../src/types"; import {EnrichedEntity, Recipe} from "../../src/types";
import {DetailGraphNode, NodeDetails} from "../../components/visualize/NodeDetails/NodeDetails";
const Page: NextPage = () => { const Page: NextPage = () => {
const {query: {name}} = useRouter() const {query: {name}} = useRouter()
@@ -34,21 +37,29 @@ const Page: NextPage = () => {
) )
}, [baseFactories, exportedFactories, findFactory, group, ignoredFactories]) }, [baseFactories, exportedFactories, findFactory, group, ignoredFactories])
const producingNodes: ProducingNode[] = useMemo(() => { const producingNodes: DetailGraphNode[] = useMemo(() => {
if (!group) return [] if (!group) return []
return Array.from(new Set([...intermediateFactories, ...group.exports, ...group.malls])) const nodes = Array.from(new Set([...intermediateFactories, ...group.exports, ...group.malls]))
.map(findFactory) .map(findFactory)
.filter(isNonNullable) .filter(isNonNullable)
.map((factory: EnrichedEntity) => ({ .map((factory: EnrichedEntity) => ({
inputs: Object.keys(factory.recipe?.prerequisites ?? {}).sort((a, b) => a.localeCompare(b)), inputs: Object.keys(factory.recipe?.prerequisites ?? {}).sort((a, b) => a.localeCompare(b)),
outputs: Object.keys(factory.recipe?.output ?? {}).sort((a, b) => a.localeCompare(b)), outputs: Object.keys(factory.recipe?.output ?? {}).sort((a, b) => a.localeCompare(b)),
name: factory.name, name: factory.name,
recipe: factory.recipe recipes: [factory.recipe]
})) } as DetailGraphNode))
return Object
.values(groupBy(nodes, node => node.inputs.join()))
.map(nodesOfInput => ({
inputs: nodesOfInput[0].inputs,
outputs: Array.from(new Set(nodesOfInput.flatMap(node => node.outputs))),
name: nodesOfInput.map(node => node.name).join(', '),
recipes: nodesOfInput.flatMap(node => node.recipes)
} as DetailGraphNode))
}, [findFactory, group, intermediateFactories]) }, [findFactory, group, intermediateFactories])
return ( return (
<div> <>
<Head> <Head>
<title>Factorio Microservices</title> <title>Factorio Microservices</title>
<meta name="description" content="Create Factorio microservices" /> <meta name="description" content="Create Factorio microservices" />
@@ -56,10 +67,19 @@ const Page: NextPage = () => {
</Head> </Head>
<main> <main>
<h1>Factorio Microservices</h1> <h1>Factorio Microservices</h1>
<ProducingGraph nodes={producingNodes} inputs={inputFactories}></ProducingGraph> <ProducingGraph
nodes={producingNodes}
inputs={inputFactories}
outputs={group?.exports}
childType={NodeDetails}
/>
</main> </main>
</div> </>
) )
} }
export async function getServerSideProps() {
return { props: { bodyClassName: 'scroll' } };
}
export default Page export default Page

View File

@@ -2,9 +2,10 @@ import type { NextPage } from 'next'
import Head from 'next/head' import Head from 'next/head'
import {useGroups} from "../../components/contexts/GroupProvider"; import {useGroups} from "../../components/contexts/GroupProvider";
import {useFactories} from "../../src/hooks/useFactories"; import {useFactories} from "../../src/hooks/useFactories";
import {ProducingGraph, ProducingNode} from "../../components/shared/ProducingGraph"; import {GraphNode, ProducingGraph} from "../../components/shared/ProducingGraph/ProducingGraph";
import {useMemo} from "react"; import {useMemo} from "react";
import {calculateInputs} from "../../src/calculateInputs"; import {calculateInputs} from "../../src/calculateInputs";
import {NodeOverview, OverviewGraphNode} from "../../components/visualize/NodeOverview/NodeOverview";
const Page: NextPage = () => { const Page: NextPage = () => {
const { const {
@@ -17,7 +18,7 @@ const Page: NextPage = () => {
findFactory findFactory
} = useFactories() } = useFactories()
const producingNodes: ProducingNode[] = useMemo(() => { const producingNodes: OverviewGraphNode[] = useMemo(() => {
return Object.values(groups).map(group => ({ return Object.values(groups).map(group => ({
inputs: calculateInputs( inputs: calculateInputs(
[...group.exports, ...group.malls], [...group.exports, ...group.malls],
@@ -34,7 +35,7 @@ const Page: NextPage = () => {
}, [baseFactories, exportedFactories, findFactory, groups, ignoredFactories]) }, [baseFactories, exportedFactories, findFactory, groups, ignoredFactories])
return ( return (
<div> <>
<Head> <Head>
<title>Factorio Microservices</title> <title>Factorio Microservices</title>
<meta name="description" content="Create Factorio microservices" /> <meta name="description" content="Create Factorio microservices" />
@@ -42,12 +43,16 @@ const Page: NextPage = () => {
</Head> </Head>
<main> <main>
<h1>Factorio Microservices</h1> <h1>Factorio Microservices</h1>
<ProducingGraph nodes={producingNodes} inputs={baseFactories}></ProducingGraph> <ProducingGraph nodes={producingNodes} inputs={baseFactories} childType={NodeOverview}></ProducingGraph>
</main> </main>
</div> </>
) )
} }
export async function getStaticProps() {
return { props: { bodyClassName: 'scroll' } };
}
function fixedEncodeURIComponent(str: string): string { function fixedEncodeURIComponent(str: string): string {
return encodeURIComponent(str).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16)); return encodeURIComponent(str).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16));
} }

75
res/manual.json Normal file
View File

@@ -0,0 +1,75 @@
[
{
"name": "Empty barrel",
"href": "/Empty_barrel",
"image": "/images/thumb/Empty_barrel.png/32px-Empty_barrel.png",
"recipe": {
"prerequisites": {
"/Steel_plate": 1
},
"time": 1,
"output": {
"/Empty_barrel": 1
}
}
},
{
"name": "Light oil",
"href": "/Light_oil",
"image": "/images/thumb/Light_oil.png/32px-Light_oil.png",
"recipe": {
"prerequisites": {
"/Heavy_oil": 40,
"/Water": 30
},
"time": 2,
"output": {
"/Light_oil": 30
}
}
},
{
"name": "Heavy oil",
"href": "/Heavy_oil",
"image": "/images/thumb/Heavy_oil.png/32px-Heavy_oil.png",
"recipe": {
"prerequisites": {
"/Crude_oil": 100,
"/Water": 50
},
"time": 5,
"output": {
"/Heavy_oil": 30
}
}
},
{
"name": "Petroleum gas",
"href": "/Petroleum_gas",
"image": "/images/thumb/Petroleum_gas.png/32px-Petroleum_gas.png",
"recipe": {
"prerequisites": {
"/Light_oil": 30,
"/Water": 50
},
"time": 2,
"output": {
"/Petroleum_gas": 20
}
}
},
{
"name": "Solid fuel",
"href": "/Solid_fuel",
"image": "/images/thumb/Solid_fuel.png/32px-Solid_fuel.png",
"recipe": {
"prerequisites": {
"/Light_oil": 10
},
"time": 2,
"output": {
"/Solid_fuel": 1
}
}
}
]

View File

@@ -1,8 +1,11 @@
import {EnrichedEntity, Entity} from "../types"; import {EnrichedEntity, Entity} from "../types";
import details from "../../res/details.json"; import details from "../../res/details.json";
import manual from "../../res/manual.json";
const factories = (details as Entity[]).map((detail: EnrichedEntity) => { const joined = [...details, ...manual] as Entity[]
detail.usedBy = (details as Entity[])
const factories = joined.map((detail: EnrichedEntity) => {
detail.usedBy = joined
.filter(f => Object .filter(f => Object
.keys(f.recipe?.prerequisites ?? {}) .keys(f.recipe?.prerequisites ?? {})
.includes(detail.href) .includes(detail.href)

View File

@@ -1,3 +1,5 @@
import {FC, PropsWithChildren} from "react";
export function isNonNullable<T>(any: T): any is NonNullable<T> { export function isNonNullable<T>(any: T): any is NonNullable<T> {
return any !== undefined && any !== null return any !== undefined && any !== null
} }
@@ -10,3 +12,12 @@ export function sortByProperty<T>(transform: (val: T) => number | string): (a: T
return a2 === b2 ? 0 : -1 return a2 === b2 ? 0 : -1
} }
} }
export function groupBy<T>(arr: T[], transform: (val: T) => string): Record<string, T[]> {
const result: Record<string, T[]> = {}
for (const elem of arr) {
const key = transform(elem)
result[key] = [...(result[key] ?? []), elem]
}
return result
}

View File

@@ -12,6 +12,11 @@ body {
padding: 2em; padding: 2em;
} }
body.scroll {
overflow: auto;
overflow-scrolling: touch;
}
a { a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;