1
0
mirror of https://github.com/barthuijgen/factorio-sites.git synced 2025-01-23 11:45:21 +02:00

Upgrade packages, added tags

This commit is contained in:
Bart 2021-03-11 20:37:17 +01:00
parent 364b849310
commit 7df5776fab
13 changed files with 501 additions and 333 deletions

View File

@ -6,15 +6,15 @@ interface Tag {
}
interface SelectProps {
options: string[];
options: Array<string | { value: string; label: string }>;
value: string[];
onChange: (tags: string[]) => void;
className?: string;
}
export const Select: React.FC<SelectProps> = ({ options, value, onChange, className }) => {
const TAGS = options.map((key) => {
return { value: key, label: key.replace(/[_-]/g, " ") };
const TAGS = options.map((tag) => {
return typeof tag === "string" ? { value: tag, label: tag.replace(/[_-]/g, " ") } : tag;
});
return (

View File

@ -15,7 +15,7 @@ const handler = apiHandler(async (req, res, { session }) => {
return res.status(401).json({ status: "Not authenticated" });
}
const { title, description, string } = req.body;
const { title, description, string, tags } = req.body;
const parsed = await parseBlueprintString(string).catch(() => null);
@ -35,7 +35,7 @@ const handler = apiHandler(async (req, res, { session }) => {
const info = {
user_id: session.user.id,
title,
tags: [],
tags: Array.isArray(tags) ? tags : [],
description_markdown: description,
};

View File

@ -20,8 +20,10 @@ import {
RadioGroup,
Stack,
Radio,
Checkbox,
} from "@chakra-ui/react";
import { MdSearch } from "react-icons/md";
import { TAGS } from "@factorio-sites/common-utils";
interface IndexProps {
totalItems: number;
@ -46,12 +48,17 @@ export const Index: NextPage<IndexProps> = ({
}, [router.query.q]);
if (!data) return null;
console.log(JSON.stringify(router.query, null, 2));
const entityOptions = Object.keys(data.entities).filter(
(key) => !key.startsWith("factorio-logo") && !key.startsWith("crash-site")
);
const recipeOptions = Object.keys(data.recipes);
const itemOptions = Object.keys(data.items).filter((key) => key.includes("module"));
const tagsOptions = TAGS.map((tag) => ({
label: `${tag.category}: ${tag.label}`,
value: tag.value,
}));
return (
<SimpleGrid columns={1} margin="0.7rem">
@ -100,6 +107,29 @@ export const Index: NextPage<IndexProps> = ({
</RadioGroup>
</Box>
</Box>
<Box css={{ marginTop: "1rem" }}>
<Text css={{ marginRight: "1rem" }}>Search mode</Text>
<Box css={{ marginRight: "1rem" }}>
<RadioGroup
onChange={(value: string) => router.push(routerQueryToHref({ mode: value }))}
value={(router.query.mode as string) || "and"}
>
<Stack>
<Radio value="and">AND</Radio>
<Radio value="or">OR</Radio>
</Stack>
</RadioGroup>
</Box>
</Box>
<Box css={{ marginTop: "1rem" }}>
<Text css={{ marginRight: "1rem" }}>Tags</Text>
<Select
options={tagsOptions}
value={queryValueAsArray(router.query.tags)}
onChange={(tags) => router.push(routerQueryToHref({ tags }))}
css={{ width: "200px", marginRight: "1rem" }}
/>
</Box>
<Box css={{ marginTop: "1rem" }}>
<Text css={{ marginRight: "1rem" }}>Entities</Text>
<Select
@ -127,6 +157,16 @@ export const Index: NextPage<IndexProps> = ({
css={{ width: "200px", marginRight: "1rem" }}
/>
</Box>
<Box css={{ marginTop: "1rem", display: "flex", alignItems: "center" }}>
<Text css={{ marginRight: "1rem", display: "inline-block" }}>Snaps to grid</Text>
<Checkbox
value="true"
onChange={(ev) =>
router.push(routerQueryToHref({ absolute_snapping: String(ev.target.checked) }))
}
isChecked={router.query.absolute_snapping === "true"}
/>
</Box>
</Box>
<Box>
<Box css={{ display: "flex", flexWrap: "wrap", minHeight: "400px" }}>
@ -154,17 +194,23 @@ export async function getServerSideProps({ query }: NextPageContext) {
const items = query.items ? String(query.items).split(",") : undefined;
const recipes = query.recipes ? String(query.recipes).split(",") : undefined;
const user = query.user ? String(query.user) : undefined;
const absolute_snapping = query.absolute_snapping
? String(query.absolute_snapping) === "true"
: false;
const mode = String(query.mode).toUpperCase() === "OR" ? "OR" : "AND";
const { count, rows } = await searchBlueprintPages({
page,
perPage,
query: query.q as string,
order,
mode,
tags,
entities,
items,
recipes,
user,
absolute_snapping,
});
return {

View File

@ -15,6 +15,7 @@ import {
import { Formik, Field, FieldProps } from "formik";
import { css } from "@emotion/react";
import { chakraResponsive } from "@factorio-sites/web-utils";
import { TAGS } from "@factorio-sites/common-utils";
import { Panel } from "../../components/Panel";
import { validateCreateBlueprintForm } from "../../utils/validate";
import { ImageEditor } from "../../components/ImageEditor";
@ -28,6 +29,11 @@ const FieldStyle = css`
export const UserBlueprintCreate: NextPage = () => {
const router = useRouter();
const tagsOptions = TAGS.map((tag) => ({
label: `${tag.category}: ${tag.label}`,
value: tag.value,
}));
return (
<div css={{ margin: "0.7rem" }}>
<Formik
@ -98,9 +104,9 @@ export const UserBlueprintCreate: NextPage = () => {
isInvalid={meta.touched && !!meta.error}
css={FieldStyle}
>
<FormLabel>Tags (WIP)</FormLabel>
<FormLabel>Tags</FormLabel>
<Select
options={[]}
options={tagsOptions}
value={field.value}
onChange={(tags) => setFieldValue("tags", tags)}
/>

View File

@ -27,6 +27,7 @@ import { Panel } from "../../../components/Panel";
import { validateCreateBlueprintForm } from "../../../utils/validate";
import { ImageEditor } from "../../../components/ImageEditor";
import { Select } from "../../../components/Select";
import { TAGS } from "@factorio-sites/common-utils";
const FieldStyle = css`
margin-bottom: 1rem;
@ -45,6 +46,11 @@ export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, sel
if (!blueprintPage) return null;
const tagsOptions = TAGS.map((tag) => ({
label: `${tag.category}: ${tag.label}`,
value: tag.value,
}));
return (
<div css={{ margin: "0.7rem" }}>
<Formik
@ -52,7 +58,7 @@ export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, sel
title: blueprintPage.title,
description: blueprintPage.description_markdown,
string: selected.string,
tags: [] as string[],
tags: blueprintPage.tags || ([] as string[]),
}}
validate={validateCreateBlueprintForm}
onSubmit={async (values, { setSubmitting, setErrors, setStatus }) => {
@ -123,9 +129,9 @@ export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, sel
isInvalid={meta.touched && !!meta.error}
css={FieldStyle}
>
<FormLabel>Tags (WIP)</FormLabel>
<FormLabel>Tags</FormLabel>
<Select
options={[]}
options={tagsOptions}
value={field.value}
onChange={(tags) => setFieldValue("tags", tags)}
/>

View File

@ -86,6 +86,10 @@ export const validateCreateBlueprintForm = (values: CreateBlueprintValues) => {
errors.description = validateRequired(values.description);
errors.string = validateRequired(values.string);
if (values.string) {
console.log(parseBlueprintStringClient(values.string));
}
// If string is set also validate it to be parsable
if (!errors.string && !parseBlueprintStringClient(values.string)) {
errors.string = "Not recognised as a blueprint string";

View File

@ -1 +1,2 @@
export * from "./lib/common-utils";
export * from "./lib/tags";

View File

@ -0,0 +1,42 @@
type Tag = { value: string; label: string; category: string };
export const TAGS: Tag[] = [
{ value: "belt/balancer", label: "Balancer", category: "Belt" },
{ value: "belt/prioritizer", label: "Prioritizer", category: "Belt" },
{ value: "belt/tap", label: "Tap", category: "Belt" },
{ value: "circuit/indicator", label: "circuit indicator", category: "Circuit" },
{ value: "circuit/counter", label: "circuit counter", category: "Circuit" },
{ value: "state/early_game", label: "Early game", category: "State" },
{ value: "state/mid_game", label: "Mid game", category: "State" },
{ value: "state/late_game", label: "Late game", category: "State" },
// { value: "meta/beaconized", label: "Beaconized", category: "Meta" },
{ value: "meta/compact", label: "Compact", category: "Meta" },
{ value: "meta/marathon", label: "Marathon", category: "Meta" },
{ value: "meta/inputs_are_marked", label: "Inputs are marked", category: "Meta" },
{ value: "mods/modded", label: "Modded", category: "Mods" },
// { value: "power/nuclear", label: "power nuclear", category: "power" },
// { value: "power/solar", label: "power solar", category: "power" },
// { value: "power/steam", label: "power steam", category: "power" },
// { value: "power/accumulator", label: "power accumulator", category: "power" },
// { value: "oil processing", label: "oil processing", category: "" },
// { value: "coal liquification", label: "coal liquification", category: "" },
{ value: "train/loading_station", label: "Loading station", category: "Train" },
{ value: "train/unloading_station", label: "Unloading station", category: "Train" },
{ value: "train/pax", label: "Pax", category: "Train" },
{ value: "train/intersection", label: "Intersection", category: "Train" },
{ value: "train/stacker", label: "Stacker", category: "Train" },
{ value: "train/track", label: "Track", category: "Train" },
{ value: "train/LHD", label: "Left hand drive", category: "Train" },
{ value: "train/RHD", label: "Right hand drive", category: "Train" },
];
export const TAGS_BY_KEY = TAGS.reduce((tags, tag) => {
tags[tag.value] = tag;
return tags;
}, {} as Record<string, Tag>);

View File

@ -60,7 +60,7 @@ export async function createBlueprintBook(
child_tree.push({
type: "blueprint",
id: result.id,
name: blueprint.blueprint.label,
name: blueprint.blueprint.label || "",
});
blueprint_ids.push(result.id);
} else if (blueprint.blueprint_book) {

View File

@ -1,5 +1,5 @@
import { blueprint_page, user } from "@prisma/client";
import { join, raw, sqltag } from "@prisma/client/runtime";
import { join, raw, Sql, sqltag } from "@prisma/client/runtime";
import { getBlueprintImageRequestTopic } from "../gcp-pubsub";
import { prisma } from "../postgres/database";
import { BlueprintPage, ChildTree } from "@factorio-sites/types";
@ -53,57 +53,78 @@ export async function searchBlueprintPages({
perPage = 10,
query,
order,
mode = "AND",
tags,
entities,
items,
recipes,
user,
absolute_snapping,
}: {
page: number;
perPage: number;
query?: string;
order: "date" | "favorites" | string;
mode: "AND" | "OR";
tags?: string[];
entities?: string[];
items?: string[];
recipes?: string[];
user?: string;
absolute_snapping?: boolean;
}): Promise<{ count: number; rows: BlueprintPage[] }> {
const orderMap: Record<string, string> = {
date: "blueprint_page.updated_at",
favorites: "favorite_count",
};
const blueprintDataSearch =
entities || items || recipes
? sqltag`LEFT JOIN blueprint ON blueprint.id = ANY(blueprint_page.blueprint_ids) OR blueprint.id = blueprint_page.blueprint_id`
: sqltag``;
const entitiesFragment = entities
? sqltag`AND blueprint.data -> 'entities' ?& array[${join(entities)}::text]`
const conditionals: Sql[] = [];
const joins: Sql[] = [];
let requires_blueprint_join = false;
if (query) {
conditionals.push(sqltag`blueprint_page.title ILIKE ${`%${query}%`}`);
}
if (entities) {
conditionals.push(sqltag`blueprint.data -> 'entities' ?& array[${join(entities)}::text]`);
requires_blueprint_join = true;
}
if (items) {
conditionals.push(sqltag`blueprint.data -> 'items' ?& array[${join(items)}::text]`);
requires_blueprint_join = true;
}
if (recipes) {
conditionals.push(sqltag`blueprint.data -> 'recipes' ?& array[${join(recipes)}::text]`);
requires_blueprint_join = true;
}
if (tags) {
conditionals.push(sqltag`blueprint_page.tags @> array[${join(tags)}::varchar]`);
}
if (user) {
conditionals.push(sqltag`blueprint_page.user_id = ${user}`);
}
if (absolute_snapping) {
conditionals.push(sqltag`(blueprint.data -> 'absolute_snapping')::boolean = true`);
requires_blueprint_join = true;
}
if (requires_blueprint_join) {
joins.push(
sqltag`LEFT JOIN blueprint ON blueprint.id = ANY(blueprint_page.blueprint_ids) OR blueprint.id = blueprint_page.blueprint_id`
);
}
const joinsFragment = joins.length ? sqltag`${join(joins)}` : sqltag``;
const conditionalFragment = conditionals.length
? sqltag`WHERE ${join(conditionals, ` ${mode} `)}`
: sqltag``;
const itemsFragment = items
? sqltag`AND blueprint.data -> 'items' ?& array[${join(items)}::text]`
: sqltag``;
const recipesFragment = recipes
? sqltag`AND blueprint.data -> 'recipes' ?& array[${join(recipes)}::text]`
: sqltag``;
const tagsFragment = tags
? sqltag`AND blueprint_page.tags @> array[${join(tags)}::varchar]`
: sqltag``;
const userFragment = user ? sqltag`AND blueprint_page.user_id = ${user}` : sqltag``;
try {
const result = (
await prisma.$queryRaw<(blueprint_page & { favorite_count: number })[]>`
SELECT DISTINCT blueprint_page.*, (SELECT COUNT(*) FROM user_favorites where user_favorites.blueprint_page_id = blueprint_page.id) AS favorite_count
FROM public.blueprint_page
${blueprintDataSearch}
WHERE blueprint_page.title ILIKE ${query ? `%${query}%` : "%"}
${entitiesFragment}
${itemsFragment}
${recipesFragment}
${tagsFragment}
${userFragment}
${joinsFragment}
${conditionalFragment}
ORDER BY ${raw(orderMap[order] || orderMap.date)} DESC
LIMIT ${perPage} OFFSET ${(page - 1) * perPage}`
).map((blueprintPage) => ({
@ -115,13 +136,8 @@ export async function searchBlueprintPages({
const countResult = await prisma.$queryRaw<{ count: number }[]>`
SELECT COUNT(DISTINCT blueprint_page.id)
FROM public.blueprint_page
${blueprintDataSearch}
WHERE blueprint_page.title ILIKE ${query ? `%${query}%` : "%"}
${entitiesFragment}
${itemsFragment}
${recipesFragment}
${tagsFragment}
${userFragment}`;
${joinsFragment}
${conditionalFragment}`;
return {
count: countResult[0].count,

View File

@ -29,10 +29,14 @@ export interface BlueprintData {
tiles?: Tile[];
icons: Icon[];
item: "blueprint";
label: string;
label?: string;
description?: string;
"snap-to-grid": { x: number; y: number };
"absolute-snapping": boolean;
/** Mode 'absolute' if 'absolute-snapping' is also present, otherwise 'relative */
"snap-to-grid"?: { x: number; y: number };
/** if true 'snap-to-grid' is also present */
"absolute-snapping"?: boolean;
/** can only exist in 'absolute' mode. changes offset to the game grid */
"position-relative-to-grid"?: { x: number; y: number };
version: number;
}

View File

@ -36,15 +36,15 @@
"@google-cloud/datastore": "6.3.1",
"@google-cloud/pubsub": "2.10.0",
"@google-cloud/secret-manager": "3.5.0",
"@google-cloud/storage": "5.7.4",
"@hookstate/core": "3.0.5",
"@google-cloud/storage": "5.8.1",
"@hookstate/core": "3.0.6",
"@prisma/client": "2.18.0",
"bbcode-to-react": "0.2.9",
"bcrypt": "5.0.1",
"cookie": "0.4.1",
"document-register-element": "1.14.10",
"formik": "2.2.6",
"framer-motion": "3.3.0",
"framer-motion": "3.10.2",
"next": "10.0.7",
"nprogress": "0.2.0",
"openid": "2.0.7",
@ -59,11 +59,11 @@
"react-markdown": "5.0.3",
"react-multi-select-component": "3.1.5",
"sharp": "0.27.2",
"ws": "7.4.3"
"ws": "7.4.4"
},
"devDependencies": {
"@babel/core": "7.13.10",
"@babel/preset-env": "7.12.16",
"@babel/preset-env": "7.13.10",
"@babel/preset-react": "7.12.13",
"@babel/preset-typescript": "7.13.0",
"@nrwl/cli": "11.5.1",
@ -80,13 +80,13 @@
"@types/bcrypt": "3.0.0",
"@types/cookie": "0.4.0",
"@types/jest": "26.0.20",
"@types/node": "14.14.28",
"@types/node": "14.14.33",
"@types/nprogress": "0.2.0",
"@types/openid": "2.0.1",
"@types/pako": "1.0.1",
"@types/puppeteer": "5.4.3",
"@types/react": "17.0.2",
"@types/react-dom": "17.0.1",
"@types/react": "17.0.3",
"@types/react-dom": "17.0.2",
"@types/sharp": "0.27.1",
"@types/ws": "7.4.0",
"@typescript-eslint/eslint-plugin": "4.17.0",

603
yarn.lock

File diff suppressed because it is too large Load Diff