2023-01-14 18:20:29 +01:00
|
|
|
import { redirect } from "@remix-run/node";
|
2023-01-16 13:35:41 +01:00
|
|
|
import {
|
|
|
|
unstable_createFileUploadHandler,
|
|
|
|
unstable_parseMultipartFormData,
|
|
|
|
json,
|
|
|
|
} from "@remix-run/node";
|
2023-01-17 09:20:48 +01:00
|
|
|
import {
|
|
|
|
useLoaderData,
|
|
|
|
Form,
|
|
|
|
useActionData,
|
|
|
|
Link,
|
|
|
|
useTransition,
|
|
|
|
} from "@remix-run/react";
|
2023-01-17 07:59:00 +01:00
|
|
|
import { useEffect, useState } from "react";
|
2023-01-12 17:48:00 +01:00
|
|
|
import Overlay from "~/components/Overlay";
|
2023-01-16 19:03:09 +01:00
|
|
|
import { ThreadReply } from "~/components/ThreadReply";
|
2023-01-12 16:03:38 +01:00
|
|
|
import prisma from "~/utils/db.server";
|
2023-01-17 07:59:00 +01:00
|
|
|
import { useNavigate } from "@remix-run/react";
|
2023-01-12 16:03:38 +01:00
|
|
|
|
|
|
|
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");
|
2023-01-14 19:03:30 +01:00
|
|
|
const replying = formData.get("replying");
|
2023-01-12 21:18:11 +01:00
|
|
|
|
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"];
|
2023-01-14 15:36:39 +01:00
|
|
|
// 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;
|
2023-01-14 15:36:39 +01:00
|
|
|
|
|
|
|
return true;
|
|
|
|
},
|
2023-01-12 16:03:38 +01:00
|
|
|
});
|
|
|
|
|
2023-01-12 21:18:11 +01:00
|
|
|
const errors = {};
|
2023-01-14 18:20:29 +01:00
|
|
|
|
2023-01-12 16:03:38 +01:00
|
|
|
let imageName;
|
2023-01-12 21:18:11 +01:00
|
|
|
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);
|
2023-01-12 21:18:11 +01:00
|
|
|
} catch (err) {
|
2023-01-14 00:51:52 +01:00
|
|
|
errors.image = "Image size too big";
|
2023-01-16 13:35:41 +01:00
|
|
|
}
|
2023-01-12 21:18:11 +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
|
|
|
}
|
2023-01-12 21:18:11 +01:00
|
|
|
|
2023-01-14 19:03:30 +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-14 19:03:30 +01:00
|
|
|
},
|
|
|
|
});
|
2023-01-16 13:35:41 +01:00
|
|
|
currentThread.posts.map((post) => currentThreadreplyids.push(post.id));
|
2023-01-14 18:20:29 +01:00
|
|
|
|
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-14 19:03:30 +01:00
|
|
|
|
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),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-01-14 19:03:30 +01:00
|
|
|
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-14 18:20:29 +01:00
|
|
|
|
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: {
|
2023-01-14 12:23:40 +01:00
|
|
|
posts: {
|
|
|
|
include: {
|
|
|
|
replies: true,
|
|
|
|
},
|
|
|
|
},
|
2023-01-12 16:03:38 +01:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-01-14 15:05:47 +01:00
|
|
|
if (!thread) {
|
2023-01-16 13:35:41 +01:00
|
|
|
throw new Error("Thread not found");
|
2023-01-14 15:05:47 +01:00
|
|
|
}
|
|
|
|
|
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();
|
2023-01-17 09:20:48 +01:00
|
|
|
const transition = useTransition();
|
2023-01-12 21:18:11 +01:00
|
|
|
const [replying, setReplying] = useState();
|
2023-01-17 07:59:00 +01:00
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
|
|
const refresh = () => {
|
|
|
|
navigate(".", { replace: true });
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const timer = setInterval(() => {
|
|
|
|
refresh();
|
|
|
|
}, 5000);
|
|
|
|
|
|
|
|
return () => clearInterval(timer);
|
|
|
|
}, []);
|
2023-01-12 16:03:38 +01:00
|
|
|
|
|
|
|
return (
|
2023-01-12 17:48:00 +01:00
|
|
|
<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"
|
2023-01-16 15:42:10 +01:00
|
|
|
className="flex w-fit flex-col rounded border-4 bg-ctp-crust p-4 shadow-lg"
|
2023-01-16 13:35:41 +01:00
|
|
|
>
|
|
|
|
<span>
|
|
|
|
<strong>{data.title}</strong> Post id: <strong>{data.id}</strong>{" "}
|
|
|
|
Created at:{" "}
|
2023-01-17 07:59:20 +01:00
|
|
|
<strong>{new Date(data.createdAt).toLocaleTimeString()}</strong>{" "}
|
|
|
|
Reply count: <strong>{data.posts.length}</strong>
|
2023-01-16 13:35:41 +01:00
|
|
|
</span>
|
2023-01-12 17:48:00 +01:00
|
|
|
<br />
|
2023-01-14 14:16:45 +01:00
|
|
|
<div className="flex flex-col">
|
2023-01-16 13:35:41 +01:00
|
|
|
<img
|
2023-01-16 15:42:10 +01:00
|
|
|
className="max-h-96 w-60"
|
2023-01-16 13:35:41 +01:00
|
|
|
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>
|
2023-01-14 14:16:45 +01:00
|
|
|
</div>
|
2023-01-15 13:41:50 +01:00
|
|
|
<p className="">{data.post}</p>
|
2023-01-12 17:48:00 +01:00
|
|
|
</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) => (
|
2023-01-17 07:58:19 +01:00
|
|
|
<ThreadReply key={post.id} setReplying={setReplying} post={post} />
|
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"
|
2023-01-16 15:42:10 +01:00
|
|
|
className="flex flex-col items-center space-y-4"
|
2023-01-16 13:35:41 +01:00
|
|
|
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 ? (
|
2023-01-16 15:42:10 +01:00
|
|
|
<p className="text-ctp-black text-center font-semibold">
|
2023-01-16 13:35:41 +01:00
|
|
|
Replying to reply id: {replying}{" "}
|
|
|
|
<button
|
|
|
|
className="text-ctp-sky hover:text-ctp-teal hover:underline"
|
|
|
|
onClick={() => setReplying("")}
|
|
|
|
>
|
|
|
|
Reset
|
|
|
|
</button>
|
|
|
|
</p>
|
|
|
|
) : (
|
|
|
|
""
|
|
|
|
)}
|
2023-01-16 15:42:10 +01:00
|
|
|
<span className="text-center font-semibold text-ctp-subtext0">
|
2023-01-16 13:35:41 +01:00
|
|
|
Post
|
|
|
|
</span>{" "}
|
|
|
|
<textarea
|
2023-01-16 15:42:10 +01:00
|
|
|
className=" m-1 rounded bg-ctp-surface0 p-1 shadow shadow-ctp-overlay0"
|
2023-01-16 13:35:41 +01:00
|
|
|
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 15:42:10 +01:00
|
|
|
<span className="text-center font-semibold text-ctp-subtext0">
|
2023-01-16 13:35:41 +01:00
|
|
|
Image (500kbps max)
|
|
|
|
</span>
|
|
|
|
<input
|
2023-01-16 15:42:10 +01:00
|
|
|
className="m-0 block w-full rounded bg-ctp-surface0 bg-clip-padding px-3 py-1.5 text-base font-semibold text-ctp-text transition ease-in-out"
|
2023-01-16 13:35:41 +01:00
|
|
|
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>
|
2023-01-17 09:20:48 +01:00
|
|
|
<details className="flex items-center justify-center duration-300">
|
2023-01-16 15:42:10 +01:00
|
|
|
<summary className="cursor-pointer text-center font-semibold text-ctp-subtext0">
|
2023-01-16 13:35:41 +01:00
|
|
|
Manual replying (needed when JS is disabled)
|
|
|
|
</summary>
|
|
|
|
<div class="flex flex-col items-center justify-center">
|
|
|
|
<input
|
2023-01-16 15:42:10 +01:00
|
|
|
className="m-1 rounded bg-ctp-surface0 p-1 shadow shadow-ctp-overlay0"
|
2023-01-16 13:35:41 +01:00
|
|
|
placeholder="Enter a Reply ID"
|
|
|
|
type="number"
|
|
|
|
name="replying"
|
|
|
|
value={replying}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-01-14 14:16:45 +01:00
|
|
|
</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
|
2023-01-16 15:42:10 +01:00
|
|
|
className="rounded-full bg-ctp-crust px-4 py-2 shadow"
|
2023-01-16 13:35:41 +01:00
|
|
|
type="submit"
|
|
|
|
>
|
2023-01-17 09:20:48 +01:00
|
|
|
{transition.state === "submitting" ? "Submitting..." : "Submit"}
|
2023-01-16 13:35:41 +01:00
|
|
|
</button>
|
2023-01-12 16:03:38 +01:00
|
|
|
</Form>
|
2023-01-12 17:48:00 +01:00
|
|
|
</Overlay>
|
2023-01-12 16:03:38 +01:00
|
|
|
);
|
2023-01-16 13:35:41 +01:00
|
|
|
}
|
2023-01-16 16:18:57 +01:00
|
|
|
|
|
|
|
export function ErrorBoundary({ error }) {
|
|
|
|
return (
|
|
|
|
<Overlay>
|
|
|
|
<div className="w-full rounded bg-ctp-red p-4 text-white">
|
|
|
|
<h1 className="text-2xl">{error.message}</h1>
|
|
|
|
<code>{error.stack}</code>
|
|
|
|
</div>
|
|
|
|
</Overlay>
|
|
|
|
);
|
|
|
|
}
|