mirror of
https://github.com/barthuijgen/factorio-sites.git
synced 2024-11-21 18:16:33 +02:00
Fix edit blueprint, db improvements, tag search
This commit is contained in:
parent
7e77230fac
commit
00358228dc
@ -27,7 +27,7 @@ const boxShadow = `inset 0 0 3px 0 #000, 0 -2px 2px -1px #000, -2px 0 2px -2px #
|
||||
0 3px 3px -3px #8f8c8b, 0 2px 2px -2px #8f8c8b, 0 1px 1px -1px #8f8c8b`;
|
||||
|
||||
export const Panel: React.FC<
|
||||
BoxProps & { title?: string | ReactNode | null; css?: SerializedStyles }
|
||||
Omit<BoxProps, "title"> & { title?: ReactNode; css?: SerializedStyles }
|
||||
> = ({ children, title, css: prop_css, ...props }) => (
|
||||
<Box css={prop_css ? [panelStyles, prop_css] : [panelStyles]} {...props}>
|
||||
{title ? <h2>{title}</h2> : null}
|
||||
|
38
apps/blueprints/src/components/TagsSelect.tsx
Normal file
38
apps/blueprints/src/components/TagsSelect.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import MultiSelect from "react-multi-select-component";
|
||||
import { useFbeData } from "../hooks/fbe.hook";
|
||||
|
||||
interface Tag {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TagsSelectProps {
|
||||
value: string[];
|
||||
onChange: (tags: string[]) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TagsSelect: React.FC<TagsSelectProps> = ({ value, onChange, className }) => {
|
||||
const { data } = useFbeData();
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const TAGS = Object.keys(data.entities)
|
||||
.filter((key) => !key.startsWith("factorio_logo") && !key.startsWith("crash_site"))
|
||||
.map((key) => {
|
||||
const item = data.entities[key];
|
||||
return { value: item.name, label: item.name.replace(/_/g, " ") };
|
||||
});
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
css={{ color: "black" }}
|
||||
className={className}
|
||||
options={TAGS}
|
||||
value={value.map((value) => ({ value, label: value.replace(/_/g, " ") }))}
|
||||
onChange={(tags: Tag[]) => onChange(tags.map((tag) => tag.value))}
|
||||
labelledBy="Select"
|
||||
hasSelectAll={false}
|
||||
/>
|
||||
);
|
||||
};
|
48
apps/blueprints/src/hooks/fbe.hook.ts
Normal file
48
apps/blueprints/src/hooks/fbe.hook.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { useEffect, useState, RefObject } from "react";
|
||||
|
||||
export type FBE = typeof import("@fbe/editor");
|
||||
export type Editor = InstanceType<FBE["Editor"]>;
|
||||
|
||||
export const useFBE = (canvasRef?: RefObject<HTMLCanvasElement>) => {
|
||||
const [FBE, setFBE] = useState<FBE | null>(null);
|
||||
const [editor, setEditor] = useState<Editor | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const _FBE = await import("@fbe/editor");
|
||||
setFBE(_FBE);
|
||||
if (canvasRef) {
|
||||
const _editor = new _FBE.Editor();
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
await _editor.init(canvas);
|
||||
canvas.style.width = "100%";
|
||||
canvas.style.height = "auto";
|
||||
_editor.setRendererSize(canvas.offsetWidth, canvas.offsetHeight);
|
||||
setEditor(_editor);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return { FBE, editor };
|
||||
};
|
||||
|
||||
export const useFbeData = () => {
|
||||
const [FBE, setFBE] = useState<FBE | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const _FBE = await import("@fbe/editor");
|
||||
if (!_FBE.FD.items) {
|
||||
await fetch("/api/fbe-proxy/data.json")
|
||||
.then((res) => res.text())
|
||||
.then((modules) => _FBE.FD.loadData(modules));
|
||||
}
|
||||
setFBE(_FBE);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return { data: FBE?.FD };
|
||||
};
|
@ -4,13 +4,16 @@ import { useCallback } from "react";
|
||||
export const useRouterQueryToHref = () => {
|
||||
const router = useRouter();
|
||||
return useCallback(
|
||||
(override: Record<string, string>) => {
|
||||
(override: Record<string, string | string[] | null>) => {
|
||||
const query = { ...router.query, ...override };
|
||||
const href =
|
||||
"/?" +
|
||||
Object.keys(query)
|
||||
.map((key) => `${key}=${query[key]}`)
|
||||
.join("&");
|
||||
const keys = Object.keys(query).filter((key) => query[key] !== null);
|
||||
const href = keys.length
|
||||
? "/?" +
|
||||
Object.keys(query)
|
||||
.filter((key) => query[key] !== null)
|
||||
.map((key) => `${key}=${query[key]}`)
|
||||
.join("&")
|
||||
: "/";
|
||||
return href;
|
||||
},
|
||||
[router]
|
||||
|
58
apps/blueprints/src/pages/api/blueprint/edit.ts
Normal file
58
apps/blueprints/src/pages/api/blueprint/edit.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { createBlueprint, editBlueprintPage, createBlueprintBook } from "@factorio-sites/database";
|
||||
import { parseBlueprintString } from "@factorio-sites/node-utils";
|
||||
import { parseDatabaseError } from "../../../utils/api.utils";
|
||||
import { apiHandler } from "../../../utils/api-handler";
|
||||
|
||||
const handler = apiHandler(async (req, res, { session }) => {
|
||||
if (!session) {
|
||||
return res.status(401).json({ status: "Not authenticated" });
|
||||
}
|
||||
|
||||
const { id, title, description, string, tags } = req.body;
|
||||
|
||||
const parsed = await parseBlueprintString(string).catch(() => null);
|
||||
|
||||
// Validation
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!title) errors.title = "Required";
|
||||
if (!description) errors.description = "Required";
|
||||
if (!string) errors.string = "Required";
|
||||
if (!parsed) errors.string = "Not recognised as a blueprint string";
|
||||
|
||||
if (Object.keys(errors).length) {
|
||||
return res.status(400).json({ errors });
|
||||
}
|
||||
|
||||
try {
|
||||
const info = {
|
||||
user_id: session.user.id,
|
||||
title,
|
||||
tags: Array.isArray(tags) ? tags : [],
|
||||
description_markdown: description,
|
||||
};
|
||||
console.log(info);
|
||||
|
||||
if (parsed?.data.blueprint) {
|
||||
const { insertedId } = await createBlueprint(parsed.data.blueprint, info);
|
||||
const page = await editBlueprintPage(id, "blueprint", insertedId, info);
|
||||
return res.status(200).json({ success: true, id: page.id });
|
||||
} else if (parsed?.data.blueprint_book) {
|
||||
const { insertedId } = await createBlueprintBook(parsed.data.blueprint_book, info);
|
||||
const page = await editBlueprintPage(id, "blueprint_book", insertedId, info);
|
||||
return res.status(200).json({ success: true, id: page.id });
|
||||
}
|
||||
} catch (reason) {
|
||||
const insert_errors = parseDatabaseError(reason);
|
||||
if (insert_errors) {
|
||||
if (insert_errors.blueprint_id || insert_errors.blueprint_book_id) {
|
||||
insert_errors.string = "This string already exists";
|
||||
}
|
||||
return res.status(400).json({ errors: insert_errors });
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({ status: "Failed to create blueprint" });
|
||||
});
|
||||
|
||||
export default handler;
|
@ -17,9 +17,10 @@ const handler = apiHandler(async (req, res, { session }) => {
|
||||
update.username = username;
|
||||
}
|
||||
if (email) {
|
||||
update.email = email;
|
||||
} else if (user.email && user.steam_id) {
|
||||
// User currently has email but wants to delete it, allow if steam_id exists
|
||||
update.email = email.toLowerCase();
|
||||
}
|
||||
// User currently has email but wants to delete it, allow if steam_id exists
|
||||
else if (user.email && user.steam_id) {
|
||||
update.email = null;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import BBCode from "bbcode-to-react";
|
||||
import { Button, Grid, Image, Box } from "@chakra-ui/react";
|
||||
import { Button, Grid, Image } from "@chakra-ui/react";
|
||||
import {
|
||||
BlueprintBook,
|
||||
Blueprint,
|
||||
@ -20,6 +20,8 @@ import { CopyButton } from "../../components/CopyButton";
|
||||
import { ImageEditor } from "../../components/ImageEditor";
|
||||
import { useAuth } from "../../providers/auth";
|
||||
import { pageHandler } from "../../utils/page-handler";
|
||||
import styled from "@emotion/styled";
|
||||
import { AiOutlineHeart, AiFillHeart } from "react-icons/ai";
|
||||
|
||||
type Selected =
|
||||
| { type: "blueprint"; data: Pick<Blueprint, "id" | "blueprint_hash" | "image_hash"> }
|
||||
@ -34,6 +36,15 @@ interface IndexProps {
|
||||
favorite: boolean;
|
||||
}
|
||||
|
||||
const StyledTable = styled.table`
|
||||
td {
|
||||
border: 1px solid #909090;
|
||||
}
|
||||
td:not(.no-padding) {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Index: NextPage<IndexProps> = ({
|
||||
image_exists,
|
||||
selected,
|
||||
@ -120,53 +131,88 @@ export const Index: NextPage<IndexProps> = ({
|
||||
templateColumns={chakraResponsive({ mobile: "1fr", desktop: "1fr 1fr" })}
|
||||
gap={6}
|
||||
>
|
||||
<Panel title={blueprint_page.title} gridColumn="1">
|
||||
{auth && (
|
||||
<Box>
|
||||
<Button colorScheme="green" onClick={onClickFavorite}>
|
||||
Favorite ({isFavorite ? "yes" : "no"})
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Panel
|
||||
title={
|
||||
<div css={{ position: "relative" }}>
|
||||
<span>{blueprint_page.title}</span>
|
||||
{auth && (
|
||||
<Button
|
||||
colorScheme="green"
|
||||
onClick={onClickFavorite}
|
||||
css={{ position: "absolute", right: "10px", top: "-7px", height: "35px" }}
|
||||
>
|
||||
Favorite {isFavorite ? <AiFillHeart /> : <AiOutlineHeart />}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
gridColumn="1"
|
||||
>
|
||||
{blueprint_book ? (
|
||||
<>
|
||||
<div>This string contains a blueprint book </div>
|
||||
<br />
|
||||
<BookChildTree
|
||||
child_tree={[
|
||||
{
|
||||
id: blueprint_book.id,
|
||||
name: blueprint_book.label,
|
||||
type: "blueprint_book",
|
||||
children: blueprint_book.child_tree,
|
||||
},
|
||||
]}
|
||||
base_url={`/blueprint/${blueprint_page.id}`}
|
||||
selected_id={selected.data.id}
|
||||
/>
|
||||
<div css={{ maxHeight: "400px", overflow: "auto" }}>
|
||||
<BookChildTree
|
||||
child_tree={[
|
||||
{
|
||||
id: blueprint_book.id,
|
||||
name: blueprint_book.label,
|
||||
type: "blueprint_book",
|
||||
children: blueprint_book.child_tree,
|
||||
},
|
||||
]}
|
||||
base_url={`/blueprint/${blueprint_page.id}`}
|
||||
selected_id={selected.data.id}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : blueprint ? (
|
||||
<>
|
||||
<div>This string contains one blueprint</div>
|
||||
<div>tags: {blueprint.tags.join(", ")}</div>
|
||||
</>
|
||||
<Markdown>{blueprint_page.description_markdown}</Markdown>
|
||||
) : null}
|
||||
</Panel>
|
||||
<Panel title={"Info"}>
|
||||
<StyledTable>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>User</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tags</td>
|
||||
<td>{blueprint_page.tags.join(", ")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last updated</td>
|
||||
<td>{new Date(blueprint_page.updated_at * 1000).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
<td>{new Date(blueprint_page.created_at * 1000).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Favorites</td>
|
||||
<td>{blueprint_page.favorite_count || "0"}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
</Panel>
|
||||
<Panel
|
||||
title={"Image"}
|
||||
gridColumn={chakraResponsive({ mobile: "1", desktop: "2" })}
|
||||
gridRow={chakraResponsive({ mobile: "1", desktop: null })}
|
||||
gridRow={chakraResponsive({ mobile: "1", desktop: "1 / span 2" })}
|
||||
>
|
||||
{/* {renderImage()} */}
|
||||
{blueprintString && <ImageEditor string={blueprintString}></ImageEditor>}
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
title="Description"
|
||||
gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 2" })}
|
||||
>
|
||||
<Markdown>{blueprint_page.description_markdown}</Markdown>
|
||||
</Panel>
|
||||
{blueprint_book && (
|
||||
<Panel
|
||||
title="Description"
|
||||
gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 2" })}
|
||||
>
|
||||
<Markdown>{blueprint_page.description_markdown}</Markdown>
|
||||
</Panel>
|
||||
)}
|
||||
{selected.type === "blueprint" && data?.blueprint && (
|
||||
<Panel
|
||||
title={
|
||||
@ -179,7 +225,7 @@ export const Index: NextPage<IndexProps> = ({
|
||||
}
|
||||
gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 2" })}
|
||||
>
|
||||
<table>
|
||||
<StyledTable>
|
||||
<tbody>
|
||||
{Object.entries(
|
||||
data.blueprint.entities.reduce<Record<string, number>>((entities, entity) => {
|
||||
@ -194,7 +240,7 @@ export const Index: NextPage<IndexProps> = ({
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([entry_name, entry]) => (
|
||||
<tr key={entry_name} css={{}}>
|
||||
<td css={{ border: "1px solid #909090" }}>
|
||||
<td className="no-padding">
|
||||
<Image
|
||||
alt={entry_name.replace(/-/g, " ")}
|
||||
src={`https://factorioprints.com/icons/${entry_name}.png`}
|
||||
@ -203,12 +249,12 @@ export const Index: NextPage<IndexProps> = ({
|
||||
height="32px"
|
||||
/>
|
||||
</td>
|
||||
<td css={{ padding: "5px 10px", border: "1px solid #909090" }}>{entry_name}</td>
|
||||
<td css={{ padding: "5px 10px", border: "1px solid #909090" }}>{entry}</td>
|
||||
<td>{entry_name}</td>
|
||||
<td>{entry}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledTable>
|
||||
</Panel>
|
||||
)}
|
||||
<Panel title="string" gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 2" })}>
|
||||
|
@ -1,12 +1,15 @@
|
||||
import React from "react";
|
||||
import { NextPage, NextPageContext } from "next";
|
||||
import { BlueprintPage, getMostRecentBlueprintPages, init } from "@factorio-sites/database";
|
||||
import Link from "next/link";
|
||||
import { BlueprintPage, searchBlueprintPages, init } from "@factorio-sites/database";
|
||||
import { SimpleGrid, Box, RadioGroup, Stack, Radio } from "@chakra-ui/react";
|
||||
import { Panel } from "../components/Panel";
|
||||
import { Pagination } from "../components/Pagination";
|
||||
import { useRouterQueryToHref } from "../hooks/query.hook";
|
||||
import { useRouter } from "next/router";
|
||||
import { BlueprintLink } from "../components/BlueprintLink";
|
||||
import { TagsSelect } from "../components/TagsSelect";
|
||||
import { queryValueAsArray } from "../utils/query.utils";
|
||||
|
||||
interface IndexProps {
|
||||
totalItems: number;
|
||||
@ -23,18 +26,20 @@ export const Index: NextPage<IndexProps> = ({
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const routerQueryToHref = useRouterQueryToHref();
|
||||
|
||||
return (
|
||||
<SimpleGrid columns={1} margin="0.7rem">
|
||||
<Panel title="Blueprints">
|
||||
<Box
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderBottom: "1px solid #b7b7b7",
|
||||
paddingBottom: "0.3rem",
|
||||
}}
|
||||
>
|
||||
<Box css={{ marginRight: "1rem" }}>Sort order</Box>
|
||||
<Box>
|
||||
<Box css={{ marginRight: "1rem" }}>
|
||||
<RadioGroup
|
||||
onChange={(value: string) => router.push(routerQueryToHref({ order: value }))}
|
||||
value={(router.query.order as string) || "date"}
|
||||
@ -45,6 +50,22 @@ export const Index: NextPage<IndexProps> = ({
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
<TagsSelect
|
||||
value={queryValueAsArray(router.query.tags)}
|
||||
onChange={(tags) => router.push(routerQueryToHref({ tags }))}
|
||||
css={{ width: "200px", marginRight: "1rem" }}
|
||||
/>
|
||||
{router.query.q && (
|
||||
<Box>
|
||||
<span>Search term:</span>
|
||||
<span
|
||||
css={{ margin: "0 5px 0 0.5rem", border: "1px solid #ababab", padding: "0px 10px" }}
|
||||
>
|
||||
{router.query.q}
|
||||
</span>
|
||||
<Link href={routerQueryToHref({ q: null })}>x</Link>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
{blueprints.map((bp) => (
|
||||
@ -62,11 +83,14 @@ export async function getServerSideProps({ query }: NextPageContext) {
|
||||
const page = Number(query.page || "1");
|
||||
const perPage = Number(query["per-page"] || "10");
|
||||
const order = (query["order"] as string) || "date";
|
||||
const { count, rows } = await getMostRecentBlueprintPages({
|
||||
const tags = query.tags ? String(query.tags).split(",") : undefined;
|
||||
console.log(tags);
|
||||
const { count, rows } = await searchBlueprintPages({
|
||||
page,
|
||||
perPage,
|
||||
query: query.q as string,
|
||||
order,
|
||||
tags,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -30,17 +30,18 @@ import { Panel } from "../../../components/Panel";
|
||||
import { validateCreateBlueprintForm } from "../../../utils/validate";
|
||||
import { useAuth } from "../../../providers/auth";
|
||||
import { ImageEditor } from "../../../components/ImageEditor";
|
||||
import { useFbeData } from "../../../hooks/fbe.hook";
|
||||
|
||||
const FieldStyle = css`
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const TAGS = [
|
||||
{ value: "foo", label: "foo" },
|
||||
{ value: "bar", label: "bar" },
|
||||
{ value: "x", label: "x" },
|
||||
{ value: "y", label: "y" },
|
||||
];
|
||||
// const TAGS = [
|
||||
// { value: "foo", label: "foo" },
|
||||
// { value: "bar", label: "bar" },
|
||||
// { value: "x", label: "x" },
|
||||
// { value: "y", label: "y" },
|
||||
// ];
|
||||
|
||||
type Selected =
|
||||
| { type: "blueprint"; data: Pick<Blueprint, "id" | "blueprint_hash">; string: string }
|
||||
@ -53,6 +54,7 @@ interface UserBlueprintProps {
|
||||
export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, selected }) => {
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
const { data } = useFbeData();
|
||||
|
||||
if (!auth) {
|
||||
router.push("/");
|
||||
@ -60,6 +62,15 @@ export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, sel
|
||||
|
||||
if (!blueprintPage) return null;
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const TAGS = Object.keys(data.entities)
|
||||
.filter((key) => !key.startsWith("factorio_logo") && !key.startsWith("crash_site"))
|
||||
.map((key) => {
|
||||
const item = data.entities[key];
|
||||
return { value: item.name, label: item.name.replace(/_/g, " ") };
|
||||
});
|
||||
|
||||
return (
|
||||
<div css={{ margin: "0.7rem" }}>
|
||||
<Formik
|
||||
@ -67,16 +78,20 @@ export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, sel
|
||||
title: blueprintPage.title,
|
||||
description: blueprintPage.description_markdown,
|
||||
string: selected.string,
|
||||
tags: [],
|
||||
tags: [] as { value: string; label: string }[],
|
||||
}}
|
||||
validate={validateCreateBlueprintForm}
|
||||
onSubmit={async (values, { setSubmitting, setErrors, setStatus }) => {
|
||||
setStatus("");
|
||||
|
||||
const result = await fetch("/api/blueprint/create", {
|
||||
const result = await fetch("/api/blueprint/edit", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(values),
|
||||
body: JSON.stringify({
|
||||
...values,
|
||||
id: blueprintPage.id,
|
||||
tags: values.tags.map((tag) => tag.value),
|
||||
}),
|
||||
}).then((res) => res.json());
|
||||
|
||||
if (result.status) {
|
||||
|
@ -12,6 +12,8 @@ interface UserBlueprintsProps {
|
||||
}
|
||||
|
||||
export const UserBlueprints: NextPage<UserBlueprintsProps> = ({ blueprints }) => {
|
||||
if (!blueprints) return null;
|
||||
|
||||
return (
|
||||
<div css={{ margin: "0.7rem" }}>
|
||||
<SimpleGrid columns={1} margin="0 auto" maxWidth="800px">
|
||||
@ -42,6 +44,10 @@ export const UserBlueprints: NextPage<UserBlueprintsProps> = ({ blueprints }) =>
|
||||
|
||||
export const getServerSideProps = pageHandler(async (context, { session }) => {
|
||||
if (!session) {
|
||||
if (context.res) {
|
||||
context.res.statusCode = 302;
|
||||
context.res.setHeader("Location", "/");
|
||||
}
|
||||
return { props: {} };
|
||||
}
|
||||
|
||||
|
9
apps/blueprints/src/utils/query.utils.ts
Normal file
9
apps/blueprints/src/utils/query.utils.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const queryValueAsArray = (value: string | string[] | undefined) => {
|
||||
if (value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return value.split(",");
|
||||
}
|
||||
return [];
|
||||
};
|
@ -1,4 +1,6 @@
|
||||
import { blueprint_page } from "@prisma/client";
|
||||
import { join, raw, sqltag } from "@prisma/client/runtime";
|
||||
|
||||
import { prisma } from "../postgres/database";
|
||||
import { BlueprintPage } from "../types";
|
||||
|
||||
@ -8,6 +10,7 @@ const mapBlueprintPageEntityToObject = (entity: blueprint_page): BlueprintPage =
|
||||
blueprint_book_id: entity.blueprint_book_id ?? null,
|
||||
title: entity.title,
|
||||
description_markdown: entity.description_markdown || "",
|
||||
tags: entity.tags,
|
||||
created_at: entity.created_at && entity.created_at.getTime() / 1000,
|
||||
updated_at: entity.updated_at && entity.updated_at.getTime() / 1000,
|
||||
factorioprints_id: entity.factorioprints_id ?? null,
|
||||
@ -31,32 +34,35 @@ export async function getBlueprintPageByFactorioprintsId(
|
||||
return result ? mapBlueprintPageEntityToObject(result) : null;
|
||||
}
|
||||
|
||||
export async function getMostRecentBlueprintPages({
|
||||
export async function searchBlueprintPages({
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
query,
|
||||
order,
|
||||
tags,
|
||||
}: {
|
||||
page: number;
|
||||
perPage: number;
|
||||
query?: string;
|
||||
order: "date" | "favorites" | string;
|
||||
tags?: string[];
|
||||
}): Promise<{ count: number; rows: BlueprintPage[] }> {
|
||||
const orderMap: Record<string, string> = {
|
||||
date: "updated_at",
|
||||
favorites: "favorite_count",
|
||||
};
|
||||
|
||||
const tagsFragment = tags
|
||||
? sqltag`AND blueprint_page.tags @> array[${join(tags)}::varchar]`
|
||||
: sqltag``;
|
||||
const result = (
|
||||
await prisma.$queryRaw<(blueprint_page & { favorite_count: number })[]>(
|
||||
`SELECT *, (SELECT COUNT(*) FROM user_favorites where user_favorites.blueprint_page_id = blueprint_page.id) AS favorite_count
|
||||
FROM public.blueprint_page
|
||||
WHERE blueprint_page.title ILIKE $1
|
||||
ORDER BY ${orderMap[order] || orderMap.date} DESC
|
||||
LIMIT $2 OFFSET $3`,
|
||||
query ? `%${query}%` : "%",
|
||||
perPage,
|
||||
(page - 1) * perPage
|
||||
)
|
||||
await prisma.$queryRaw<(blueprint_page & { favorite_count: number })[]>`
|
||||
SELECT *, (SELECT COUNT(*) FROM user_favorites where user_favorites.blueprint_page_id = blueprint_page.id) AS favorite_count
|
||||
FROM public.blueprint_page
|
||||
WHERE blueprint_page.title ILIKE ${query ? `%${query}%` : "%"}
|
||||
${tagsFragment}
|
||||
ORDER BY ${raw(orderMap[order] || orderMap.date)} DESC
|
||||
LIMIT ${perPage} OFFSET ${(page - 1) * perPage}`
|
||||
).map((blueprintPage) => ({
|
||||
...blueprintPage,
|
||||
created_at: new Date(blueprintPage.created_at),
|
||||
@ -99,3 +105,36 @@ export async function createBlueprintPage(
|
||||
console.log(`Created Blueprint Page`);
|
||||
return page;
|
||||
}
|
||||
|
||||
export async function editBlueprintPage(
|
||||
blueprintPageId: string,
|
||||
type: "blueprint" | "blueprint_book",
|
||||
targetId: string,
|
||||
extraInfo: {
|
||||
title: string;
|
||||
user_id: string | null;
|
||||
description_markdown: string;
|
||||
tags?: string[];
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
factorioprints_id?: string;
|
||||
}
|
||||
) {
|
||||
const page = await prisma.blueprint_page.update({
|
||||
where: { id: blueprintPageId },
|
||||
data: {
|
||||
user_id: extraInfo.user_id,
|
||||
title: extraInfo.title,
|
||||
description_markdown: extraInfo.description_markdown,
|
||||
factorioprints_id: extraInfo.factorioprints_id,
|
||||
blueprint_id: type === "blueprint" ? targetId : undefined,
|
||||
blueprint_book_id: type === "blueprint_book" ? targetId : undefined,
|
||||
tags: extraInfo.tags ? extraInfo.tags : [],
|
||||
updated_at: extraInfo.updated_at ? new Date(extraInfo.updated_at * 1000) : new Date(),
|
||||
created_at: extraInfo.created_at ? new Date(extraInfo.created_at * 1000) : new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Updated Blueprint Page`);
|
||||
return page;
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ export const createUserWithEmail = async (
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
email: email.toLowerCase(),
|
||||
username,
|
||||
password: hash,
|
||||
last_login_ip: ip,
|
||||
@ -46,7 +46,10 @@ export const loginUserWithEmail = async ({
|
||||
useragent: string;
|
||||
ip: string;
|
||||
}): Promise<(user & { session: session }) | null> => {
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: email.toLowerCase() },
|
||||
});
|
||||
|
||||
if (!user || !user.password) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { getEnvOrThrow, getSecretOrThrow } from "../env.util";
|
||||
|
||||
const globalWithPrisma = globalThis as typeof globalThis & { prisma: PrismaClient };
|
||||
|
||||
export let prisma: PrismaClient;
|
||||
|
||||
const _init = async () => {
|
||||
if (prisma) return prisma;
|
||||
if (globalWithPrisma.prisma) return globalWithPrisma.prisma;
|
||||
|
||||
const databasePassword = getEnvOrThrow("POSTGRES_PASSWORD");
|
||||
|
||||
@ -19,8 +21,6 @@ const _init = async () => {
|
||||
},
|
||||
});
|
||||
|
||||
// const x = await prismaClient.user.create({ data: { username: 1 } });
|
||||
|
||||
await prismaClient.$connect();
|
||||
|
||||
return prismaClient;
|
||||
@ -29,6 +29,9 @@ const _init = async () => {
|
||||
const promise = _init()
|
||||
.then((result) => {
|
||||
prisma = result;
|
||||
if (!globalWithPrisma.prisma) {
|
||||
globalWithPrisma.prisma = prisma;
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.catch((reason) => {
|
||||
|
@ -46,6 +46,7 @@ export interface BlueprintPage {
|
||||
blueprint_book_id: string | null;
|
||||
title: string;
|
||||
description_markdown: string;
|
||||
tags: string[];
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
factorioprints_id: string | null;
|
||||
|
Loading…
Reference in New Issue
Block a user