imageboard/app/routes/threads/$threadId.jsx

314 lines
9.6 KiB
React
Raw Normal View History

import { redirect } from "@remix-run/node";
2023-01-16 13:35:41 +01:00
import {
unstable_createFileUploadHandler,
unstable_parseMultipartFormData,
json,
} from "@remix-run/node";
import { useLoaderData, Form, useActionData, Link } from "@remix-run/react";
import { useState } from "react";
import Overlay from "~/components/Overlay";
2023-01-12 16:03:38 +01:00
import prisma from "~/utils/db.server";
export async function action({ request, params }) {
const threadId = params.threadId;
2023-01-16 13:35:41 +01:00
if (!parseInt(threadId)) throw new Error("Bad route parameter");
2023-01-12 16:03:38 +01:00
const clonedData = request.clone();
const formData = await clonedData.formData();
const post = formData.get("post");
const replying = formData.get("replying");
2023-01-12 16:03:38 +01:00
const fileUploadHandler = unstable_createFileUploadHandler({
2023-01-16 13:35:41 +01:00
directory: "./public/uploads/",
2023-01-12 16:03:38 +01:00
maxPartSize: 500000,
2023-01-16 14:43:52 +01:00
file: ({ filename }) => {
if (formData.get("anonymize"))
return `${require("crypto").randomBytes(16).toString("hex")}.${
filename.split(".").slice(-1)[0]
}`;
return filename;
},
2023-01-16 13:35:41 +01:00
filter: (data) => {
const fileTypes = ["jpeg", "jpg", "png", "gif"];
// if sent file is not an image, don't handle it
2023-01-16 13:35:41 +01:00
if (!fileTypes.includes(data.contentType.split("/")[1])) return false;
return true;
},
2023-01-12 16:03:38 +01:00
});
const errors = {};
2023-01-12 16:03:38 +01:00
let imageName;
let multiPartformdata;
try {
2023-01-16 13:35:41 +01:00
multiPartformdata = await unstable_parseMultipartFormData(
request,
fileUploadHandler
);
multiPartformdata.get("image") !== null
? (imageName = multiPartformdata.get("image").name)
: (imageName = null);
} catch (err) {
errors.image = "Image size too big";
2023-01-16 13:35:41 +01:00
}
if (typeof post !== "string" || post.length > 50 || post.length < 3) {
errors.post = "Post too long or short";
2023-01-16 13:35:41 +01:00
}
if (replying !== "") {
let currentThreadreplyids = [];
const currentThread = await prisma.thread.findUnique({
where: {
id: parseInt(threadId),
},
include: {
2023-01-16 13:35:41 +01:00
posts: true,
},
});
2023-01-16 13:35:41 +01:00
currentThread.posts.map((post) => currentThreadreplyids.push(post.id));
2023-01-16 13:35:41 +01:00
if (
typeof replying !== "string" ||
!parseInt(replying) ||
!currentThreadreplyids.includes(parseInt(replying))
) {
errors.replying = "bad reply id";
}
}
2023-01-12 18:26:01 +01:00
if (Object.keys(errors).length) {
return json(errors, { status: 422 });
2023-01-16 13:35:41 +01:00
}
2023-01-12 18:26:01 +01:00
2023-01-12 16:03:38 +01:00
const createPost = await prisma.post.create({
data: {
comment: post,
imageName: imageName,
2023-01-12 21:58:35 +01:00
replyingTo: replying ? parseInt(replying) : null,
2023-01-12 16:03:38 +01:00
postId: parseInt(threadId),
},
});
return redirect(`/threads/${threadId}`);
2023-01-16 13:35:41 +01:00
}
2023-01-12 16:03:38 +01:00
export async function loader({ params }) {
const threadId = params.threadId;
2023-01-16 13:35:41 +01:00
if (!parseInt(threadId)) throw new Error("Bad route parameter");
2023-01-12 16:03:38 +01:00
const thread = await prisma.thread.findUnique({
where: {
2023-01-12 21:58:35 +01:00
id: parseInt(threadId),
2023-01-12 16:03:38 +01:00
},
include: {
posts: {
include: {
replies: true,
},
},
2023-01-12 16:03:38 +01:00
},
});
if (!thread) {
2023-01-16 13:35:41 +01:00
throw new Error("Thread not found");
}
2023-01-12 16:03:38 +01:00
return thread;
}
export default function Thread() {
const data = useLoaderData();
2023-01-12 18:26:01 +01:00
const actionData = useActionData();
const [replying, setReplying] = useState();
2023-01-12 16:03:38 +01:00
return (
<Overlay>
2023-01-15 13:41:50 +01:00
<div className="flex flex-col">
2023-01-16 13:35:41 +01:00
<div
id="OP"
className="bg-ctp-crust w-fit flex flex-col shadow-lg border-4 rounded p-4"
>
<span>
<strong>{data.title}</strong> Post id: <strong>{data.id}</strong>{" "}
Created at:{" "}
<strong>{new Date(data.createdAt).toLocaleString()}</strong> Reply
count: <strong>{data.posts.length}</strong>
</span>
<br />
<div className="flex flex-col">
2023-01-16 13:35:41 +01:00
<img
className="w-60 max-h-96"
src={`/uploads/${data.imageName}`}
alt="post image"
/>
<Link
className=" text-xs text-ctp-surface0"
to={`/uploads/${data.imageName}`}
target="_blank"
referrerPolicy="no-referrer"
>
Click to show full image
</Link>
</div>
2023-01-15 13:41:50 +01:00
<p className="">{data.post}</p>
</div>
2023-01-15 13:41:50 +01:00
<ul className="m-8 ">
2023-01-16 13:35:41 +01:00
{data.posts.map((post) => (
<li
id={`${post.id}`}
className="rounded w-fit shadow p-4 m-4 odd:bg-ctp-mantle border even:bg-ctp-crust"
key={post.id}
>
<div className="flex flex-wrap">
2023-01-16 13:35:41 +01:00
<span>
Reply id: <strong>{post.id}</strong> Replied at:{" "}
<strong>{new Date(post.createdAt).toLocaleString()} </strong>
</span>
<Link
className="mx-2 text-ctp-rosewater hover:text-ctp-maroon hover:underline"
onClick={(event) => {
event.preventDefault();
document.getElementById(`bottom`).scrollIntoView(true);
setReplying(post.id);
}}
to={`#bottom`}
>
Reply
</Link>
<ul className="flex flex-wrap space-x-1">
2023-01-16 13:35:41 +01:00
{post?.replies?.map((reply) => (
<li key={reply.id}>
2023-01-16 13:35:41 +01:00
<Link
onClick={(event) => {
event.preventDefault();
document
.getElementById(`${reply.id}`)
.scrollIntoView(true);
}}
className="text-ctp-teal hover:text-ctp-sky hover:underline"
to={`#${reply.id}`}
>
#{reply.id}
</Link>
</li>
))}
</ul>
</div>
2023-01-16 13:35:41 +01:00
{post.replyingTo ? (
<Link
onClick={(event) => {
event.preventDefault();
document
.getElementById(`${post.replyingTo}`)
.scrollIntoView(true);
}}
className="font-semibold text-sm hover:underline"
to={`#${post.replyingTo}`}
>
Replying to id: {post.replyingTo}
</Link>
) : (
""
)}
{post.imageName !== null ? (
<div className="flex flex-col">
<img
className="w-60 max-h-96"
src={`/uploads/${post.imageName}`}
alt="post image"
/>
<Link
className=" text-xs text-ctp-surface0"
to={`/uploads/${post.imageName}`}
target="_blank"
referrerPolicy="no-referrer"
>
Click to show full image
</Link>
</div>
) : (
""
)}
2023-01-12 16:03:38 +01:00
<p>{post.comment}</p>
</li>
2023-01-16 13:35:41 +01:00
))}
2023-01-12 16:03:38 +01:00
</ul>
</div>
2023-01-16 13:35:41 +01:00
<Form
id="bottom"
className="space-y-4 flex flex-col items-center"
method="post"
encType="multipart/form-data"
>
2023-01-15 23:20:24 +01:00
<label className="flex flex-col" htmlFor="text-input">
2023-01-16 13:35:41 +01:00
{replying ? (
<p className="text-center font-semibold text-ctp-black">
Replying to reply id: {replying}{" "}
<button
className="text-ctp-sky hover:text-ctp-teal hover:underline"
onClick={() => setReplying("")}
>
Reset
</button>
</p>
) : (
""
)}
<span className="font-semibold text-center text-ctp-subtext0">
Post
</span>{" "}
<textarea
className=" bg-ctp-surface0 m-1 p-1 rounded shadow shadow-ctp-overlay0"
required
minLength={3}
name="post"
type="text"
/>
{actionData ? <p>{actionData.post}</p> : ""}
2023-01-12 16:03:38 +01:00
</label>
2023-01-16 15:18:38 +01:00
<label className="flex flex-col items-center" htmlFor="image-upload">
2023-01-16 13:35:41 +01:00
<span className="font-semibold text-center text-ctp-subtext0">
Image (500kbps max)
</span>
<input
className="block w-full px-3 py-1.5 text-base font-semibold text-ctp-text bg-ctp-surface0 bg-clip-padding rounded transition ease-in-out m-0"
accept="image/*"
type="file"
id="image-upload"
name="image"
/>
2023-01-16 14:43:52 +01:00
<label htmlFor="anon">
<span>Anonymize filename </span>
<input type="checkbox" name="anonymize" id="anon-check" />
</label>
2023-01-16 15:18:38 +01:00
<p className="text-ctp-red">{actionData?.image}</p>
2023-01-12 16:03:38 +01:00
</label>
<details class="flex items-center justify-center duration-300">
2023-01-16 13:35:41 +01:00
<summary className="text-center text-ctp-subtext0 font-semibold cursor-pointer">
Manual replying (needed when JS is disabled)
</summary>
<div class="flex flex-col items-center justify-center">
<input
className="bg-ctp-surface0 m-1 p-1 rounded shadow shadow-ctp-overlay0"
placeholder="Enter a Reply ID"
type="number"
name="replying"
value={replying}
/>
</div>
</details>
2023-01-15 23:20:24 +01:00
<p className="text-ctp-red">{actionData?.replying}</p>
2023-01-16 13:35:41 +01:00
<button
className="bg-ctp-crust px-4 py-2 shadow rounded-full"
type="submit"
>
Submit
</button>
2023-01-12 16:03:38 +01:00
</Form>
</Overlay>
2023-01-12 16:03:38 +01:00
);
2023-01-16 13:35:41 +01:00
}