mirror of
https://github.com/barthuijgen/factorio-sites.git
synced 2025-03-17 21:17:57 +02:00
Blueprint page refactor (#51)
* blueprint page refactor * Deduplicate code on blueprint page Co-authored-by: Bart <barthuijgen@users.noreply.github.com>
This commit is contained in:
parent
653a1001e4
commit
8dc22086f9
apps/blueprints/src
components/blueprint
Blueprint.tsxBlueprintBook.tsxBlueprintData.tsxBlueprintEntities.tsxBlueprintInfo.tsxBlueprintTags.tsxFavoriteButton.tsx
pages/blueprint
174
apps/blueprints/src/components/blueprint/Blueprint.tsx
Normal file
174
apps/blueprints/src/components/blueprint/Blueprint.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import BBCode from "bbcode-to-react";
|
||||
import { Grid, Image, Box } from "@chakra-ui/react";
|
||||
import { Blueprint as IBlueprint, BlueprintPage, BlueprintStringData } from "@factorio-sites/types";
|
||||
import { chakraResponsive, parseBlueprintStringClient } from "@factorio-sites/web-utils";
|
||||
import { Panel } from "../../components/Panel";
|
||||
import { Markdown } from "../../components/Markdown";
|
||||
import { CopyButton } from "../../components/CopyButton";
|
||||
import { ImageEditor } from "../../components/ImageEditor";
|
||||
import styled from "@emotion/styled";
|
||||
import { FavoriteButton } from "./FavoriteButton";
|
||||
import { useUrl } from "../../hooks/url.hook";
|
||||
import { BlueprintData } from "./BlueprintData";
|
||||
import { BlueprintInfo } from "./BlueprintInfo";
|
||||
import { BlueprintTags } from "./BlueprintTags";
|
||||
import { BlueprintEntities } from "./BlueprintEntities";
|
||||
|
||||
const StyledBlueptintPage = styled(Grid)`
|
||||
grid-gap: 16px;
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
width: calc(100% - 120px);
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
&.image {
|
||||
height: 579px;
|
||||
}
|
||||
&.child-tree {
|
||||
overflow: hidden;
|
||||
height: 579px;
|
||||
position: relative;
|
||||
.child-tree-wrapper {
|
||||
height: 483px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.tags {
|
||||
text-align: left;
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
margin: 3px;
|
||||
padding: 0 3px;
|
||||
background: #313131;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
max-height: 600px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMarkdown = styled(Markdown)`
|
||||
max-height: 600px;
|
||||
`;
|
||||
|
||||
interface BlueprintProps {
|
||||
blueprint: IBlueprint;
|
||||
blueprint_page: BlueprintPage;
|
||||
favorite: boolean;
|
||||
}
|
||||
|
||||
export const BlueprintSubPage: React.FC<BlueprintProps> = ({
|
||||
blueprint,
|
||||
blueprint_page,
|
||||
favorite,
|
||||
}) => {
|
||||
const url = useUrl();
|
||||
const [string, setString] = useState<string | null>(null);
|
||||
const [data, setData] = useState<BlueprintStringData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/string/${blueprint.blueprint_hash}`)
|
||||
.then((res) => res.text())
|
||||
.then((string) => {
|
||||
setString(string);
|
||||
const data = parseBlueprintStringClient(string);
|
||||
setData(data);
|
||||
})
|
||||
.catch((reason) => console.error(reason));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledBlueptintPage templateColumns={chakraResponsive({ mobile: "1fr", desktop: "1fr 1fr" })}>
|
||||
<Panel
|
||||
className="image"
|
||||
gridColumn="2"
|
||||
title={
|
||||
<div className="title">
|
||||
<span>Image</span>
|
||||
<img
|
||||
src="/fbe.svg"
|
||||
alt="Factorio blueprint editor"
|
||||
css={{ display: "inline-block", height: "24px", marginLeft: "10px" }}
|
||||
/>
|
||||
<Box css={{ display: "inline-block", flexGrow: 1, textAlign: "right" }}>
|
||||
{string && (
|
||||
<CopyButton
|
||||
primary
|
||||
css={{ marginRight: "1rem" }}
|
||||
label="Copy Blueprint"
|
||||
content={string}
|
||||
/>
|
||||
)}
|
||||
{blueprint.blueprint_hash && url && (
|
||||
<CopyButton
|
||||
label="Copy URL"
|
||||
content={`${url.origin}/api/string/${blueprint.blueprint_hash}`}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{string && <ImageEditor string={string}></ImageEditor>}
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
className="description"
|
||||
gridColumn="1"
|
||||
gridRow="1"
|
||||
title={
|
||||
<div className="title">
|
||||
<span className="text">{blueprint_page.title}</span>
|
||||
<FavoriteButton is_favorite={favorite} blueprint_page_id={blueprint_page.id} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<StyledMarkdown>{blueprint_page.description_markdown}</StyledMarkdown>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Info" gridColumn="1" gridRow="2">
|
||||
<BlueprintInfo blueprint_page={blueprint_page} />
|
||||
</Panel>
|
||||
|
||||
<Panel gridColumn="2" gridRow="2" title="Tags">
|
||||
<BlueprintTags blueprint_page={blueprint_page} />
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
className="entities"
|
||||
gridColumn="1 / span 2"
|
||||
title={
|
||||
<span>
|
||||
Entities for{" "}
|
||||
{data?.blueprint?.label ? BBCode.toReact(data.blueprint.label) : "blueprint"}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{data && <BlueprintEntities data={data} />}
|
||||
</Panel>
|
||||
|
||||
<Panel className="bp-strings" gridColumn="1 / span 2" title="Blueprint data">
|
||||
{string && data && <BlueprintData string={string} data={data} />}
|
||||
</Panel>
|
||||
</StyledBlueptintPage>
|
||||
);
|
||||
};
|
256
apps/blueprints/src/components/blueprint/BlueprintBook.tsx
Normal file
256
apps/blueprints/src/components/blueprint/BlueprintBook.tsx
Normal file
@ -0,0 +1,256 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import BBCode from "bbcode-to-react";
|
||||
import { Image, Box, Grid } from "@chakra-ui/react";
|
||||
import styled from "@emotion/styled";
|
||||
import {
|
||||
BlueprintBook,
|
||||
Blueprint,
|
||||
BlueprintPage,
|
||||
BlueprintStringData,
|
||||
} from "@factorio-sites/types";
|
||||
import {
|
||||
chakraResponsive,
|
||||
ChildTreeBlueprintBookEnriched,
|
||||
mergeBlueprintDataAndChildTree,
|
||||
parseBlueprintStringClient,
|
||||
} from "@factorio-sites/web-utils";
|
||||
import { Panel } from "../../components/Panel";
|
||||
import { Markdown } from "../../components/Markdown";
|
||||
import { BookChildTree } from "../../components/BookChildTree";
|
||||
import { CopyButton } from "../../components/CopyButton";
|
||||
import { ImageEditor } from "../../components/ImageEditor";
|
||||
import { useUrl } from "../../hooks/url.hook";
|
||||
import { FavoriteButton } from "./FavoriteButton";
|
||||
import { BlueprintData } from "./BlueprintData";
|
||||
import { BlueprintInfo } from "./BlueprintInfo";
|
||||
import { BlueprintTags } from "./BlueprintTags";
|
||||
import { BlueprintEntities } from "./BlueprintEntities";
|
||||
|
||||
const StyledBlueptintPage = styled(Grid)`
|
||||
grid-gap: 16px;
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
width: calc(100% - 120px);
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
&.image {
|
||||
height: 579px;
|
||||
}
|
||||
&.child-tree {
|
||||
overflow: hidden;
|
||||
height: 579px;
|
||||
position: relative;
|
||||
.child-tree-wrapper {
|
||||
height: 483px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
max-height: 600px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMarkdown = styled(Markdown)`
|
||||
max-height: 600px;
|
||||
`;
|
||||
|
||||
type Selected =
|
||||
| { type: "blueprint"; data: Pick<Blueprint, "id" | "blueprint_hash" | "image_hash" | "label"> }
|
||||
| { type: "blueprint_book"; data: Pick<BlueprintBook, "id" | "blueprint_hash" | "label"> };
|
||||
|
||||
interface BlueprintBookSubPageProps {
|
||||
selected: Selected;
|
||||
blueprint_book: BlueprintBook;
|
||||
blueprint_page: BlueprintPage;
|
||||
favorite: boolean;
|
||||
}
|
||||
|
||||
export const BlueprintBookSubPage: React.FC<BlueprintBookSubPageProps> = ({
|
||||
selected,
|
||||
blueprint_book,
|
||||
blueprint_page,
|
||||
favorite,
|
||||
}) => {
|
||||
const url = useUrl();
|
||||
const [selectedBlueprintString, setSelectedBlueprintString] = useState<string | null>(null);
|
||||
const [bookChildTreeData, setBookChildTreeData] = useState<ChildTreeBlueprintBookEnriched | null>(
|
||||
null
|
||||
);
|
||||
const [selectedData, setSelectedData] = useState<BlueprintStringData | null>(null);
|
||||
const selectedHash = selected.data.blueprint_hash;
|
||||
const showEntities = selected.type === "blueprint" && selectedData?.blueprint;
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/string/${blueprint_book.blueprint_hash}`)
|
||||
.then((res) => res.text())
|
||||
.then((string) => {
|
||||
const data = parseBlueprintStringClient(string);
|
||||
if (data) {
|
||||
setBookChildTreeData(
|
||||
mergeBlueprintDataAndChildTree(data, {
|
||||
id: blueprint_book.id,
|
||||
name: blueprint_book.label,
|
||||
type: "blueprint_book",
|
||||
children: blueprint_book.child_tree,
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((reason) => console.error(reason));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/string/${selectedHash}`)
|
||||
.then((res) => res.text())
|
||||
.then((string) => {
|
||||
setSelectedBlueprintString(string);
|
||||
if (selected.type === "blueprint") {
|
||||
const data = parseBlueprintStringClient(string);
|
||||
setSelectedData(data);
|
||||
} else {
|
||||
setSelectedData(null);
|
||||
}
|
||||
})
|
||||
.catch((reason) => console.error(reason));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedHash]);
|
||||
|
||||
const onRequestData = () => {
|
||||
fetch(`/api/string/${selectedHash}`)
|
||||
.then((res) => res.text())
|
||||
.then((string) => {
|
||||
const data = parseBlueprintStringClient(string);
|
||||
setSelectedData(data);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledBlueptintPage
|
||||
className="bp-book"
|
||||
templateColumns={chakraResponsive({ mobile: "1fr", desktop: "1fr 1fr" })}
|
||||
>
|
||||
<Panel
|
||||
className="child-tree"
|
||||
title={
|
||||
<div className="title">
|
||||
<span className="text">{blueprint_page.title}</span>
|
||||
<FavoriteButton is_favorite={favorite} blueprint_page_id={blueprint_page.id} />
|
||||
</div>
|
||||
}
|
||||
gridColumn="1"
|
||||
gridRow="1"
|
||||
>
|
||||
{bookChildTreeData && (
|
||||
<div className="child-tree-wrapper">
|
||||
<BookChildTree
|
||||
blueprint_book={bookChildTreeData}
|
||||
base_url={`/blueprint/${blueprint_page.id}`}
|
||||
selected_id={selected.data.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
className="image"
|
||||
gridColumn="2"
|
||||
title={
|
||||
<div className="title">
|
||||
<span>Image</span>
|
||||
<img
|
||||
src="/fbe.svg"
|
||||
alt="Factorio blueprint editor"
|
||||
css={{ display: "inline-block", height: "24px", marginLeft: "10px" }}
|
||||
/>
|
||||
<Box css={{ display: "inline-block", flexGrow: 1, textAlign: "right" }}>
|
||||
{selectedBlueprintString && (
|
||||
<CopyButton
|
||||
primary
|
||||
css={{ marginRight: "1rem" }}
|
||||
label="Copy Blueprint"
|
||||
content={selectedBlueprintString}
|
||||
/>
|
||||
)}
|
||||
{selected.data.blueprint_hash && url && (
|
||||
<CopyButton
|
||||
label="Copy URL"
|
||||
content={`${url.origin}/api/string/${selected.data.blueprint_hash}`}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{selectedBlueprintString && <ImageEditor string={selectedBlueprintString}></ImageEditor>}
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
className="description"
|
||||
gridColumn="1"
|
||||
gridRow={"2 / span 2"}
|
||||
title={
|
||||
<div className="title">
|
||||
<span className="text">Description </span>
|
||||
<FavoriteButton is_favorite={favorite} blueprint_page_id={blueprint_page.id} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<StyledMarkdown>{blueprint_page.description_markdown}</StyledMarkdown>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Info" gridColumn="2" gridRow="2">
|
||||
<BlueprintInfo blueprint_page={blueprint_page} />
|
||||
</Panel>
|
||||
|
||||
<Panel title="Tags" gridColumn="2" gridRow={"3"}>
|
||||
<BlueprintTags blueprint_page={blueprint_page} />
|
||||
</Panel>
|
||||
|
||||
{showEntities && (
|
||||
<Panel
|
||||
className="entities"
|
||||
gridColumn="1 / span 2"
|
||||
title={
|
||||
<span>
|
||||
Entities for{" "}
|
||||
{selectedData?.blueprint?.label
|
||||
? BBCode.toReact(selectedData.blueprint.label)
|
||||
: "blueprint"}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{selectedData && <BlueprintEntities data={selectedData} />}
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel
|
||||
className="bp-strings"
|
||||
gridColumn="1 / span 2"
|
||||
title={`data for ${selected.type.replace("_", " ")} "${selected.data.label}"`}
|
||||
>
|
||||
{selectedBlueprintString && (
|
||||
<BlueprintData
|
||||
string={selectedBlueprintString}
|
||||
data={selectedData}
|
||||
onRequestData={onRequestData}
|
||||
/>
|
||||
)}
|
||||
</Panel>
|
||||
</StyledBlueptintPage>
|
||||
);
|
||||
};
|
63
apps/blueprints/src/components/blueprint/BlueprintData.tsx
Normal file
63
apps/blueprints/src/components/blueprint/BlueprintData.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { Button } from "../Button";
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BlueprintStringData } from "@factorio-sites/types";
|
||||
|
||||
interface BlueprintDataProps {
|
||||
string: string;
|
||||
data: BlueprintStringData | null;
|
||||
onRequestData?: () => void;
|
||||
}
|
||||
|
||||
export const BlueprintData: React.FC<BlueprintDataProps> = ({ string, data, onRequestData }) => {
|
||||
const [showDetails, setShowDetails] = useState<"string" | "json" | "none">("none");
|
||||
|
||||
useEffect(() => {
|
||||
setShowDetails("none");
|
||||
}, [string, data]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowDetails(showDetails === "string" ? "none" : "string");
|
||||
}}
|
||||
>
|
||||
{showDetails === "string" ? "Hide" : "Show"} string
|
||||
</Button>
|
||||
<Button
|
||||
css={{ marginLeft: "1rem" }}
|
||||
onClick={() => {
|
||||
setShowDetails(showDetails === "json" ? "none" : "json");
|
||||
if (!data && onRequestData) {
|
||||
onRequestData();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{showDetails === "json" ? "Hide" : "Show"} json
|
||||
</Button>
|
||||
</Box>
|
||||
<Box css={{ marginTop: "1rem" }}>
|
||||
{showDetails === "string" && (
|
||||
<textarea
|
||||
value={string || "Loading..."}
|
||||
readOnly
|
||||
css={{
|
||||
width: "100%",
|
||||
height: "100px",
|
||||
resize: "none",
|
||||
color: "#fff",
|
||||
backgroundColor: "#414040",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showDetails === "json" && (
|
||||
<pre css={{ maxHeight: "500px", overflowY: "scroll" }}>
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
import { Box, Image } from "@chakra-ui/react";
|
||||
import { BlueprintStringData } from "@factorio-sites/types";
|
||||
import styled from "@emotion/styled";
|
||||
|
||||
const StyledBox = styled(Box)`
|
||||
td {
|
||||
border: 1px solid #909090;
|
||||
}
|
||||
td:not(.no-padding) {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface BlueprintEntitiesProps {
|
||||
data: BlueprintStringData;
|
||||
}
|
||||
|
||||
export const BlueprintEntities: React.FC<BlueprintEntitiesProps> = ({ data }) => {
|
||||
return (
|
||||
<StyledBox>
|
||||
<table>
|
||||
<tbody>
|
||||
{data?.blueprint?.entities &&
|
||||
Object.entries(
|
||||
data.blueprint.entities.reduce<Record<string, number>>((entities, entity) => {
|
||||
if (entities[entity.name]) {
|
||||
entities[entity.name]++;
|
||||
} else {
|
||||
entities[entity.name] = 1;
|
||||
}
|
||||
return entities;
|
||||
}, {})
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([entry_name, entry]) => (
|
||||
<tr key={entry_name}>
|
||||
<td className="no-padding">
|
||||
<Image
|
||||
alt={entry_name.replace(/-/g, " ")}
|
||||
src={`https://factorioprints.com/icons/${entry_name}.png`}
|
||||
fallbackSrc="https://storage.googleapis.com/factorio-blueprints-assets/error-icon.png"
|
||||
width="32px"
|
||||
height="32px"
|
||||
/>
|
||||
</td>
|
||||
<td>{entry_name}</td>
|
||||
<td>{entry}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledBox>
|
||||
);
|
||||
};
|
66
apps/blueprints/src/components/blueprint/BlueprintInfo.tsx
Normal file
66
apps/blueprints/src/components/blueprint/BlueprintInfo.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { format } from "date-fns";
|
||||
import Link from "next/link";
|
||||
import { BlueprintPage } from "@factorio-sites/types";
|
||||
import styled from "@emotion/styled";
|
||||
|
||||
const StyledBox = styled(Box)`
|
||||
dl {
|
||||
display: flex;
|
||||
dt {
|
||||
width: 65%;
|
||||
font-weight: 600;
|
||||
}
|
||||
dd {
|
||||
width: 35%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-left: -64px;
|
||||
margin-right: -64px;
|
||||
border: none;
|
||||
height: 2px;
|
||||
margin: 12px auto;
|
||||
box-shadow: inset 0 1px 1px 0 #131313, inset 0 -1px 1px 0 #838383, 0 0 4px 0 #392f2e;
|
||||
}
|
||||
`;
|
||||
|
||||
interface BlueprintInfoProps {
|
||||
blueprint_page: BlueprintPage;
|
||||
}
|
||||
|
||||
export const BlueprintInfo: React.FC<BlueprintInfoProps> = ({ blueprint_page }) => {
|
||||
return (
|
||||
<StyledBox>
|
||||
<dl>
|
||||
<dt>User:</dt>
|
||||
<dd>
|
||||
{blueprint_page.user ? (
|
||||
<Link href={`/?user=${blueprint_page.user?.id}`}>
|
||||
<a>{blueprint_page.user?.username}</a>
|
||||
</Link>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
<hr />
|
||||
<dl>
|
||||
<dt>Last updated:</dt>
|
||||
<dd>{format(new Date(blueprint_page.updated_at * 1000), "DD/mm/YYYY")}</dd>
|
||||
</dl>
|
||||
<hr />
|
||||
<dl>
|
||||
<dt>Created:</dt>
|
||||
<dd>{format(new Date(blueprint_page.created_at * 1000), "DD/mm/YYYY")}</dd>
|
||||
</dl>
|
||||
<hr />
|
||||
<dl>
|
||||
<dt>Favorites:</dt>
|
||||
<dd>{blueprint_page.favorite_count || "0"}</dd>
|
||||
</dl>
|
||||
</StyledBox>
|
||||
);
|
||||
};
|
36
apps/blueprints/src/components/blueprint/BlueprintTags.tsx
Normal file
36
apps/blueprints/src/components/blueprint/BlueprintTags.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { BlueprintPage } from "@factorio-sites/types";
|
||||
import styled from "@emotion/styled";
|
||||
import { TAGS_BY_KEY } from "@factorio-sites/common-utils";
|
||||
|
||||
const StyledBox = styled(Box)`
|
||||
text-align: left;
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
margin: 3px;
|
||||
padding: 0 3px;
|
||||
background: #313131;
|
||||
border-radius: 3px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface BlueprintTagsProps {
|
||||
blueprint_page: BlueprintPage;
|
||||
}
|
||||
|
||||
export const BlueprintTags: React.FC<BlueprintTagsProps> = ({ blueprint_page }) => {
|
||||
return (
|
||||
<StyledBox>
|
||||
{blueprint_page.tags.length ? (
|
||||
blueprint_page.tags.map((tag) => (
|
||||
<span key={tag} className="tag">
|
||||
{TAGS_BY_KEY[tag].category}: {TAGS_BY_KEY[tag].label}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<div>No tags have been added yet</div>
|
||||
)}
|
||||
</StyledBox>
|
||||
);
|
||||
};
|
40
apps/blueprints/src/components/blueprint/FavoriteButton.tsx
Normal file
40
apps/blueprints/src/components/blueprint/FavoriteButton.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { useState } from "react";
|
||||
import { AiFillHeart, AiOutlineHeart } from "react-icons/ai";
|
||||
import { useAuth } from "../../providers/auth";
|
||||
import { Button } from "../Button";
|
||||
|
||||
interface FavoriteButtonProps {
|
||||
is_favorite: boolean;
|
||||
blueprint_page_id: string;
|
||||
}
|
||||
|
||||
export const FavoriteButton: React.FC<FavoriteButtonProps> = ({
|
||||
is_favorite: isFavoriteDefault,
|
||||
blueprint_page_id,
|
||||
}) => {
|
||||
const auth = useAuth();
|
||||
const [isFavorite, setIsFavorite] = useState(isFavoriteDefault);
|
||||
|
||||
const onClickFavorite = async () => {
|
||||
const result = await fetch("/api/user/favorite", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ blueprint_page_id }),
|
||||
}).then((res) => res.json());
|
||||
setIsFavorite(result.favorite);
|
||||
};
|
||||
|
||||
if (!auth) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={onClickFavorite}
|
||||
css={{ display: "inline-flex", float: "right", fontSize: "initial" }}
|
||||
>
|
||||
Favorite
|
||||
<span className="icon" css={{ marginLeft: "5px" }}>
|
||||
{isFavorite ? <AiFillHeart /> : <AiOutlineHeart />}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
@ -1,122 +1,16 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import Link from "next/link";
|
||||
import BBCode from "bbcode-to-react";
|
||||
import { Image, Box, Grid } from "@chakra-ui/react";
|
||||
import styled from "@emotion/styled";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
getBlueprintBookById,
|
||||
getBlueprintById,
|
||||
getBlueprintPageWithUserById,
|
||||
isBlueprintPageUserFavorite,
|
||||
} from "@factorio-sites/database";
|
||||
import {
|
||||
BlueprintBook,
|
||||
Blueprint,
|
||||
BlueprintPage,
|
||||
BlueprintStringData,
|
||||
} from "@factorio-sites/types";
|
||||
import { TAGS_BY_KEY, timeLogger } from "@factorio-sites/common-utils";
|
||||
import {
|
||||
chakraResponsive,
|
||||
ChildTreeBlueprintBookEnriched,
|
||||
mergeBlueprintDataAndChildTree,
|
||||
parseBlueprintStringClient,
|
||||
} from "@factorio-sites/web-utils";
|
||||
import { Panel } from "../../components/Panel";
|
||||
import { Markdown } from "../../components/Markdown";
|
||||
import { BookChildTree } from "../../components/BookChildTree";
|
||||
import { CopyButton } from "../../components/CopyButton";
|
||||
import { ImageEditor } from "../../components/ImageEditor";
|
||||
import { useAuth } from "../../providers/auth";
|
||||
import { BlueprintBook, Blueprint, BlueprintPage } from "@factorio-sites/types";
|
||||
import { timeLogger } from "@factorio-sites/common-utils";
|
||||
import { pageHandler } from "../../utils/page-handler";
|
||||
import { AiOutlineHeart, AiFillHeart } from "react-icons/ai";
|
||||
import { Button } from "../../components/Button";
|
||||
import { useUrl } from "../../hooks/url.hook";
|
||||
|
||||
const StyledBlueptintPage = styled(Grid)`
|
||||
grid-gap: 16px;
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
width: calc(100% - 120px);
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
&.image {
|
||||
height: 579px;
|
||||
}
|
||||
&.child-tree {
|
||||
overflow: hidden;
|
||||
height: 579px;
|
||||
position: relative;
|
||||
.child-tree-wrapper {
|
||||
height: 483px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
&.info {
|
||||
dl {
|
||||
display: flex;
|
||||
dt {
|
||||
width: 65%;
|
||||
font-weight: 600;
|
||||
}
|
||||
dd {
|
||||
width: 35%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-left: -64px;
|
||||
margin-right: -64px;
|
||||
border: none;
|
||||
height: 2px;
|
||||
margin: 12px auto;
|
||||
box-shadow: inset 0 1px 1px 0 #131313, inset 0 -1px 1px 0 #838383, 0 0 4px 0 #392f2e;
|
||||
}
|
||||
}
|
||||
|
||||
&.tags {
|
||||
text-align: left;
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
margin: 3px;
|
||||
padding: 0 3px;
|
||||
background: #313131;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.entities table {
|
||||
td {
|
||||
border: 1px solid #909090;
|
||||
}
|
||||
td:not(.no-padding) {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
max-height: 600px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMarkdown = styled(Markdown)`
|
||||
max-height: 600px;
|
||||
`;
|
||||
import { BlueprintSubPage } from "../../components/blueprint/Blueprint";
|
||||
import { BlueprintBookSubPage } from "../../components/blueprint/BlueprintBook";
|
||||
|
||||
type Selected =
|
||||
| { type: "blueprint"; data: Pick<Blueprint, "id" | "blueprint_hash" | "image_hash" | "label"> }
|
||||
@ -137,58 +31,6 @@ export const Index: NextPage<IndexProps> = ({
|
||||
blueprint_page,
|
||||
favorite,
|
||||
}) => {
|
||||
const auth = useAuth();
|
||||
const url = useUrl();
|
||||
const [selectedBlueprintString, setSelectedBlueprintString] = useState<string | null>(null);
|
||||
const [bookChildTreeData, setBookChildTreeData] = useState<ChildTreeBlueprintBookEnriched | null>(
|
||||
null
|
||||
);
|
||||
const [selectedData, setSelectedData] = useState<BlueprintStringData | null>(null);
|
||||
const [showDetails, setShowDetails] = useState<"string" | "json" | "none">("none");
|
||||
const [isFavorite, setIsFavorite] = useState(favorite);
|
||||
const selectedHash = selected.data.blueprint_hash;
|
||||
const isBlueprintBook = Boolean(blueprint_book);
|
||||
// const isBlueprintBookChild = isBlueprintBook && selected.type === "blueprint";
|
||||
const showEntities = selected.type === "blueprint" && selectedData?.blueprint;
|
||||
|
||||
useEffect(() => {
|
||||
const hash = blueprint_book ? blueprint_book.blueprint_hash : blueprint?.blueprint_hash;
|
||||
fetch(`/api/string/${hash}`)
|
||||
.then((res) => res.text())
|
||||
.then((string) => {
|
||||
const data = parseBlueprintStringClient(string);
|
||||
if (data && blueprint_book) {
|
||||
setBookChildTreeData(
|
||||
mergeBlueprintDataAndChildTree(data, {
|
||||
id: blueprint_book.id,
|
||||
name: blueprint_book.label,
|
||||
type: "blueprint_book",
|
||||
children: blueprint_book.child_tree,
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((reason) => console.error(reason));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/string/${selectedHash}`)
|
||||
.then((res) => res.text())
|
||||
.then((string) => {
|
||||
setShowDetails("none");
|
||||
setSelectedBlueprintString(string);
|
||||
if (selected.type === "blueprint") {
|
||||
const data = parseBlueprintStringClient(string);
|
||||
setSelectedData(data);
|
||||
} else {
|
||||
setSelectedData(null);
|
||||
}
|
||||
})
|
||||
.catch((reason) => console.error(reason));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedHash]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log({
|
||||
selected,
|
||||
@ -199,275 +41,22 @@ export const Index: NextPage<IndexProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onClickFavorite = async () => {
|
||||
const result = await fetch("/api/user/favorite", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ blueprint_page_id: blueprint_page.id }),
|
||||
}).then((res) => res.json());
|
||||
setIsFavorite(result.favorite);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledBlueptintPage
|
||||
className={clsx({ "bp-book": isBlueprintBook })}
|
||||
templateColumns={chakraResponsive({ mobile: "1fr", desktop: "1fr 1fr" })}
|
||||
>
|
||||
{isBlueprintBook && (
|
||||
<Panel
|
||||
className="child-tree"
|
||||
title={
|
||||
<div className="title">
|
||||
<span className="text">{blueprint_page.title}</span>
|
||||
{auth && (
|
||||
<Button
|
||||
onClick={onClickFavorite}
|
||||
css={{ display: "inline-flex", float: "right", fontSize: "initial" }}
|
||||
>
|
||||
Favorite
|
||||
<span className="icon" css={{ marginLeft: "5px" }}>
|
||||
{isFavorite ? <AiFillHeart /> : <AiOutlineHeart />}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
gridColumn="1"
|
||||
gridRow="1"
|
||||
>
|
||||
{bookChildTreeData && (
|
||||
<div className="child-tree-wrapper">
|
||||
<BookChildTree
|
||||
blueprint_book={bookChildTreeData}
|
||||
base_url={`/blueprint/${blueprint_page.id}`}
|
||||
selected_id={selected.data.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel
|
||||
className="image"
|
||||
gridColumn="2"
|
||||
title={
|
||||
<div className="title">
|
||||
<span>Image</span>
|
||||
<img
|
||||
src="/fbe.svg"
|
||||
alt="Factorio blueprint editor"
|
||||
css={{ display: "inline-block", height: "24px", marginLeft: "10px" }}
|
||||
/>
|
||||
<Box css={{ display: "inline-block", float: "right" }}>
|
||||
{selectedBlueprintString && (
|
||||
<CopyButton
|
||||
primary
|
||||
css={{ marginRight: "1rem" }}
|
||||
label="Copy Blueprint"
|
||||
content={selectedBlueprintString}
|
||||
/>
|
||||
)}
|
||||
{selected.data.blueprint_hash && url && (
|
||||
<CopyButton
|
||||
label="Copy URL"
|
||||
content={`${url.origin}/api/string/${selected.data.blueprint_hash}`}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{selectedBlueprintString && <ImageEditor string={selectedBlueprintString}></ImageEditor>}
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
className="description"
|
||||
gridColumn="1"
|
||||
gridRow={isBlueprintBook ? "2 / span 2" : "1"}
|
||||
title={
|
||||
<div className="title">
|
||||
<span className="text">{isBlueprintBook ? "Description" : blueprint_page.title}</span>
|
||||
{auth && !isBlueprintBook && (
|
||||
<Button
|
||||
onClick={onClickFavorite}
|
||||
css={{ display: "inline-flex", float: "right", fontSize: "initial" }}
|
||||
>
|
||||
Favorite
|
||||
<span className="icon" css={{ marginLeft: "5px" }}>
|
||||
{isFavorite ? <AiFillHeart /> : <AiOutlineHeart />}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
// bottom={
|
||||
// <div css={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
// {selectedBlueprintString && (
|
||||
// <CopyButton
|
||||
// css={{ marginRight: 16 }}
|
||||
// label="Copy Blueprint"
|
||||
// content={selectedBlueprintString}
|
||||
// />
|
||||
// )}
|
||||
// {selected.data.blueprint_hash && typeof window !== "undefined" && (
|
||||
// <CopyButton
|
||||
// label="Copy URL"
|
||||
// content={`${window.location.origin}/api/string/${selected.data.blueprint_hash}`}
|
||||
// />
|
||||
// )}
|
||||
// </div>
|
||||
// }
|
||||
>
|
||||
<StyledMarkdown>{blueprint_page.description_markdown}</StyledMarkdown>
|
||||
</Panel>
|
||||
|
||||
<Panel className="info" gridColumn={isBlueprintBook ? "2" : "1"} gridRow="2" title={"Info"}>
|
||||
<Box>
|
||||
<dl>
|
||||
<dt>User:</dt>
|
||||
<dd>
|
||||
{blueprint_page.user ? (
|
||||
<Link href={`/?user=${blueprint_page.user?.id}`}>
|
||||
<a>{blueprint_page.user?.username}</a>
|
||||
</Link>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
<hr />
|
||||
<dl>
|
||||
<dt>Last updated:</dt>
|
||||
<dd>{new Date(blueprint_page.updated_at * 1000).toLocaleDateString()}</dd>
|
||||
</dl>
|
||||
<hr />
|
||||
<dl>
|
||||
<dt>Created:</dt>
|
||||
<dd>{new Date(blueprint_page.created_at * 1000).toLocaleDateString()}</dd>
|
||||
</dl>
|
||||
<hr />
|
||||
<dl>
|
||||
<dt>Favorites:</dt>
|
||||
<dd>{blueprint_page.favorite_count || "0"}</dd>
|
||||
</dl>
|
||||
</Box>
|
||||
</Panel>
|
||||
|
||||
<Panel className="tags" gridColumn="2" gridRow={isBlueprintBook ? "3" : "2"} title={"Tags"}>
|
||||
{blueprint_page.tags.length ? (
|
||||
blueprint_page.tags.map((tag) => (
|
||||
<span key={tag} className="tag">
|
||||
{TAGS_BY_KEY[tag].category}: {TAGS_BY_KEY[tag].label}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<div>No tags have been added yet</div>
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
{showEntities && (
|
||||
<Panel
|
||||
className="entities"
|
||||
gridColumn="1 / span 2"
|
||||
title={
|
||||
<span>
|
||||
Entities for{" "}
|
||||
{selectedData?.blueprint?.label
|
||||
? BBCode.toReact(selectedData.blueprint.label)
|
||||
: "blueprint"}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
{selectedData?.blueprint?.entities &&
|
||||
Object.entries(
|
||||
selectedData.blueprint.entities.reduce<Record<string, number>>(
|
||||
(entities, entity) => {
|
||||
if (entities[entity.name]) {
|
||||
entities[entity.name]++;
|
||||
} else {
|
||||
entities[entity.name] = 1;
|
||||
}
|
||||
return entities;
|
||||
},
|
||||
{}
|
||||
)
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([entry_name, entry]) => (
|
||||
<tr key={entry_name}>
|
||||
<td className="no-padding">
|
||||
<Image
|
||||
alt={entry_name.replace(/-/g, " ")}
|
||||
src={`https://factorioprints.com/icons/${entry_name}.png`}
|
||||
fallbackSrc="https://storage.googleapis.com/factorio-blueprints-assets/error-icon.png"
|
||||
width="32px"
|
||||
height="32px"
|
||||
/>
|
||||
</td>
|
||||
<td>{entry_name}</td>
|
||||
<td>{entry}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel
|
||||
className="bp-strings"
|
||||
gridColumn="1 / span 2"
|
||||
title={`data for ${selected.type.replace("_", " ")} "${selected.data.label}"`}
|
||||
>
|
||||
<Box>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowDetails(showDetails === "string" ? "none" : "string");
|
||||
}}
|
||||
>
|
||||
{showDetails === "string" ? "Hide" : "Show"} string
|
||||
</Button>
|
||||
<Button
|
||||
css={{ marginLeft: "1rem" }}
|
||||
onClick={() => {
|
||||
setShowDetails(showDetails === "json" ? "none" : "json");
|
||||
if (!selectedData) {
|
||||
fetch(`/api/string/${selectedHash}`)
|
||||
.then((res) => res.text())
|
||||
.then((string) => {
|
||||
const data = parseBlueprintStringClient(string);
|
||||
setSelectedData(data);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{showDetails === "json" ? "Hide" : "Show"} json
|
||||
</Button>
|
||||
</Box>
|
||||
<Box css={{ marginTop: "1rem" }}>
|
||||
{showDetails === "string" && (
|
||||
<textarea
|
||||
value={selectedBlueprintString || "Loading..."}
|
||||
readOnly
|
||||
css={{
|
||||
width: "100%",
|
||||
height: "100px",
|
||||
resize: "none",
|
||||
color: "#fff",
|
||||
backgroundColor: "#414040",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showDetails === "json" && (
|
||||
<pre css={{ maxHeight: "500px", overflowY: "scroll" }}>
|
||||
{JSON.stringify(selectedData, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</Box>
|
||||
</Panel>
|
||||
</StyledBlueptintPage>
|
||||
);
|
||||
if (blueprint) {
|
||||
return (
|
||||
<BlueprintSubPage blueprint={blueprint} blueprint_page={blueprint_page} favorite={favorite} />
|
||||
);
|
||||
} else if (blueprint_book) {
|
||||
return (
|
||||
<BlueprintBookSubPage
|
||||
selected={selected}
|
||||
blueprint_book={blueprint_book}
|
||||
blueprint_page={blueprint_page}
|
||||
favorite={favorite}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <div>404</div>;
|
||||
}
|
||||
};
|
||||
|
||||
export const getServerSideProps = pageHandler(async (context, { session }) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user