1
0
mirror of https://github.com/barthuijgen/factorio-sites.git synced 2025-03-17 21:17:57 +02:00

Feat: improve blueprint creation, add comments to blueprints

This commit is contained in:
Bart 2021-04-22 22:26:43 +02:00
parent 2a415f0c6e
commit e18cd37245
13 changed files with 259 additions and 16 deletions

2
.gitignore vendored
View File

@ -6,7 +6,7 @@
/out-tsc
# dependencies
/node_modules
node_modules
# IDEs and editors
/.idea

View File

@ -0,0 +1,21 @@
-- CreateTable
CREATE TABLE "comment" (
"id" UUID NOT NULL,
"blueprint_page_id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"body" TEXT NOT NULL,
"reply_comment_id" UUID,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "comment" ADD FOREIGN KEY ("blueprint_page_id") REFERENCES "blueprint_page"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "comment" ADD FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "comment" ADD FOREIGN KEY ("reply_comment_id") REFERENCES "comment"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -16,7 +16,7 @@ enum enum_user_role {
model blueprint {
id String @id @default(uuid()) @db.Uuid
label String? @db.VarChar(255)
description String?
description String? @db.Text
game_version String? @db.VarChar(255)
blueprint_hash String @unique @db.VarChar(40)
image_hash String @db.VarChar(40)
@ -32,7 +32,7 @@ model blueprint {
model blueprint_book {
id String @id @default(uuid()) @db.Uuid
label String? @db.VarChar(255)
description String?
description String? @db.Text
child_tree Json @db.JsonB
blueprint_hash String @unique @db.VarChar(40)
is_modded Boolean
@ -51,7 +51,7 @@ model blueprint_page {
blueprint_book_id String? @unique @db.Uuid
blueprint_ids String[] @db.Uuid
title String @db.VarChar(255)
description_markdown String?
description_markdown String? @db.Text
tags String[] @db.VarChar(255)
image_hash String @db.VarChar(40)
factorioprints_id String? @unique @db.VarChar(255)
@ -61,6 +61,7 @@ model blueprint_page {
blueprint blueprint? @relation(fields: [blueprint_id], references: [id])
user_favorites user_favorites[]
user user? @relation(fields: [user_id], references: [id])
comments comment[]
}
model session {
@ -94,6 +95,7 @@ model user {
session session[]
user_favorites user_favorites[]
blueprint_pages blueprint_page[]
comment comment[]
}
model user_favorites {
@ -107,6 +109,20 @@ model user_favorites {
@@id([user_id, blueprint_page_id])
}
model comment {
id String @id @default(uuid()) @db.Uuid
blueprint_page_id String @db.Uuid
user_id String @db.Uuid
body String @db.Text
reply_comment_id String? @db.Uuid
created_at DateTime @default(now())
updated_at DateTime @updatedAt
blueprint_page blueprint_page @relation(fields: [blueprint_page_id], references: [id])
user user @relation(fields: [user_id], references: [id])
reply_to comment? @relation("commentToComment", fields: [reply_comment_id], references: [id])
replies comment[] @relation("commentToComment")
}
model blueprint_entities {
entity String @unique
}

View File

@ -0,0 +1,114 @@
import { FormEventHandler, useEffect, useState } from "react";
import { CommentWithUsername } from "@factorio-sites/types";
import { useAuth } from "../providers/auth";
import { Button } from "./Button";
import styled from "@emotion/styled";
import { format } from "date-fns";
import { getLocaleDateFormat } from "@factorio-sites/web-utils";
interface CommentsProps {
blueprint_page_id: string;
}
const AddCommentDiv = styled.div`
border-bottom: 1px solid #ccc;
padding-bottom: 1rem;
margin-bottom: 1rem;
textarea {
color: #fff;
display: block;
margin-top: 0.5rem;
background: #414040;
border: 1px solid #ddd;
border-radius: 4px;
width: 400px;
min-height: 80px;
}
button {
margin-top: 0.5rem;
}
.close {
margin-left: 1rem;
}
`;
const CommentDiv = styled.div`
background: #4e4c4c;
margin: 0.5rem 0;
`;
export const Comments: React.FC<CommentsProps> = ({ blueprint_page_id }) => {
const auth = useAuth();
const [addCommentOpen, setAddCommentOpen] = useState(false);
const [comments, setComments] = useState<CommentWithUsername[]>([]);
const [commentBody, setCommentBody] = useState("");
const fetchTopLevelComments = async () => {
const result = await fetch(`/api/blueprint/comments?blueprint_page_id=${blueprint_page_id}`);
const body = await result.json();
setComments(body.comments);
};
useEffect(() => {
fetchTopLevelComments();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
console.log({ comments });
const onSubmitComment: FormEventHandler<HTMLFormElement> = async (event) => {
event.preventDefault();
const result = await fetch("/api/blueprint/comment", {
method: "POST",
body: JSON.stringify({ blueprint_page_id, body: commentBody }),
headers: {
"Content-Type": "application/json",
},
});
setCommentBody("");
setAddCommentOpen(false);
console.log("result", await result.json());
fetchTopLevelComments();
};
return (
<div>
{auth && (
<AddCommentDiv>
{addCommentOpen ? (
<form onSubmit={onSubmitComment}>
<div>Add a new comment</div>
<textarea
value={commentBody}
onChange={(event) => setCommentBody(event.target.value)}
/>
<Button primary type="submit">
Send
</Button>
<Button type="button" className="close" onClick={() => setAddCommentOpen(false)}>
close
</Button>
</form>
) : (
<Button onClick={() => setAddCommentOpen(true)}>Add comment</Button>
)}
</AddCommentDiv>
)}
{comments?.length ? (
comments.map((comment) => (
<CommentDiv key={comment.id}>
<div>
{comment.user.username} at{" "}
{format(new Date(comment.created_at), getLocaleDateFormat() + " HH:mm")}
</div>
<div>{comment.body}</div>
</CommentDiv>
))
) : (
<div>There are no comments yet</div>
)}
</div>
);
};

View File

@ -6,6 +6,15 @@ const StyledTooltip = styled.span`
display: inline-block;
cursor: pointer;
vertical-align: middle;
&:active:after {
content: attr(title);
padding: 5px;
border: 1px solid #ccc;
top: 5px;
right: 10%;
background: #bada55;
}
`;
export interface TooltipProps extends React.HTMLAttributes<HTMLSpanElement> {

View File

@ -18,6 +18,7 @@ import { BlueprintImage, RENDERERS } from "./BlueprintImage";
import { useAuth } from "../../providers/auth";
import { Button } from "../Button";
import { PUBLIC_URL } from "../../utils/env";
import { Comments } from "../Comments";
const StyledBlueptintPage = styled(Grid)`
grid-gap: 16px;
@ -133,7 +134,7 @@ export const BlueprintSubPage: React.FC<BlueprintProps> = ({
{string && (
<BlueprintImage
string={string}
label={blueprint.label}
label={blueprint.label || ""}
blueprint_hash={blueprint.blueprint_hash}
onSetRenderer={setRenderer}
/>
@ -184,6 +185,10 @@ export const BlueprintSubPage: React.FC<BlueprintProps> = ({
{data && <BlueprintEntities data={data} />}
</Panel>
<Panel title="Comments" gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 4" })}>
<Comments blueprint_page_id={blueprint_page.id} />
</Panel>
<Panel
title="Blueprint data"
className="bp-strings"

View File

@ -30,6 +30,7 @@ import { BlueprintImage, RENDERERS } from "./BlueprintImage";
import { Button } from "../Button";
import { useAuth } from "../../providers/auth";
import { PUBLIC_URL } from "../../utils/env";
import { Comments } from "../Comments";
const StyledBlueptintPage = styled(Grid)`
grid-gap: 16px;
@ -206,7 +207,7 @@ export const BlueprintBookSubPage: React.FC<BlueprintBookSubPageProps> = ({
{selectedBlueprintString && (
<BlueprintImage
string={selectedBlueprintString}
label={selected.data.label}
label={selected.data.label || ""}
blueprint_hash={selected.data.blueprint_hash}
onSetRenderer={setRenderer}
/>
@ -261,6 +262,10 @@ export const BlueprintBookSubPage: React.FC<BlueprintBookSubPageProps> = ({
</Panel>
)}
<Panel title="Comments" gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 4" })}>
<Comments blueprint_page_id={blueprint_page.id} />
</Panel>
<Panel
className="bp-strings"
gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 4" })}

View File

@ -0,0 +1,20 @@
import { apiHandler } from "../../../utils/api-handler";
import { createComment } from "@factorio-sites/database";
const handler = apiHandler(async (req, res, { session }) => {
if (req.method !== "POST") return res.status(400).json({ error: "method must be POST" });
if (!session) {
return res.status(401).json({ status: "Not authenticated" });
}
const { body, blueprint_page_id } = req.body;
console.log({ body, blueprint_page_id });
const result = await createComment(blueprint_page_id, session.user, body);
console.log(result);
res.status(200).json({ status: "Comment submitted" });
});
export default handler;

View File

@ -0,0 +1,17 @@
import { apiHandler } from "../../../utils/api-handler";
import { getComments } from "@factorio-sites/database";
const handler = apiHandler(async (req, res, { session }) => {
if (req.method !== "GET") return res.status(400).json({ error: "method must be GET" });
const blueprint_page_id = req.query.blueprint_page_id as string;
if (!blueprint_page_id) return res.status(400).json({ status: "blueprint_page_id required" });
const comments = await getComments(blueprint_page_id);
console.log(comments);
res.status(200).json({ comments });
});
export default handler;

View File

@ -3,7 +3,7 @@ import { blueprintDataToDbData, encodeBlueprint, hashString } from "@factorio-si
import { blueprint as BlueprintModel } from "@prisma/client";
import { saveBlueprintString } from "../gcp-storage";
import { prisma } from "../postgres/database";
import { Blueprint, BlueprintData } from "@factorio-sites/types";
import { Blueprint, BlueprintData, DbBlueprintData } from "@factorio-sites/types";
// const blueprintImageRequestTopic = getBlueprintImageRequestTopic();
@ -11,12 +11,14 @@ const mapBlueprintInstanceToEntry = (entity: BlueprintModel): Blueprint => ({
id: entity.id,
blueprint_hash: entity.blueprint_hash,
image_hash: entity.image_hash,
image_version: entity.image_version,
label: entity.label || "",
description: entity.description || "",
tags: entity.tags,
created_at: entity.created_at && entity.created_at.getTime() / 1000,
updated_at: entity.updated_at && entity.updated_at.getTime() / 1000,
game_version: entity.game_version || null,
data: (entity.data as unknown) as DbBlueprintData,
});
export async function getBlueprintById(id: string): Promise<Blueprint | null> {

View File

@ -0,0 +1,34 @@
import { user } from "@prisma/client";
import { prisma } from "../postgres/database";
export async function createComment(blueprint_page_id: string, user: user, body: string) {
const result = await prisma.comment.create({
data: {
body,
blueprint_page_id,
user_id: user.id,
},
});
return result;
}
export async function getComments(blueprint_page_id: string) {
const result = await prisma.comment.findMany({
where: {
blueprint_page_id,
reply_comment_id: null,
},
include: {
user: {
select: {
username: true,
},
},
},
orderBy: {
created_at: "desc",
},
});
return result;
}

View File

@ -2,4 +2,5 @@ export * from "./user";
export * from "./blueprint";
export * from "./blueprint_book";
export * from "./blueprint_page";
export * from "./comment";
export * from "./factorioprints";

View File

@ -1,4 +1,5 @@
import { Signal } from "./blueprint-string";
import { comment, blueprint, user } from "@prisma/client";
import { Icon, Signal } from "./blueprint-string";
export interface ChildTreeBlueprint {
type: "blueprint";
@ -15,16 +16,10 @@ export interface ChildTreeBlueprintBook {
export type ChildTree = Array<ChildTreeBlueprint | ChildTreeBlueprintBook>;
export interface Blueprint {
id: string;
label: string; // from source
description: string | null; // from source
game_version: string | null; // from source
blueprint_hash: string;
image_hash: string;
export interface Blueprint extends Omit<blueprint, "data" | "created_at" | "updated_at"> {
created_at: number;
updated_at: number;
tags: string[];
data: DbBlueprintData | null;
}
export interface BlueprintBook {
@ -72,3 +67,7 @@ export interface DbBlueprintData {
recipes: Record<string, number>;
tiles: Record<string, number>;
}
export type Comment = comment;
export type CommentWithUsername = comment & { user: Pick<user, "username"> };