initial commit
This commit is contained in:
commit
c3cca4e80a
|
@ -0,0 +1,53 @@
|
||||||
|
# Welcome to Remix!
|
||||||
|
|
||||||
|
- [Remix Docs](https://remix.run/docs)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
From your terminal:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts your app in development mode, rebuilding assets on file changes.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
First, build your app for production:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the app in production mode:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you'll need to pick a host to deploy it to.
|
||||||
|
|
||||||
|
### DIY
|
||||||
|
|
||||||
|
If you're familiar with deploying node applications, the built-in Remix app server is production-ready.
|
||||||
|
|
||||||
|
Make sure to deploy the output of `remix build`
|
||||||
|
|
||||||
|
- `build/`
|
||||||
|
- `public/build/`
|
||||||
|
|
||||||
|
### Using a Template
|
||||||
|
|
||||||
|
When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd ..
|
||||||
|
# create a new project, and pick a pre-configured host
|
||||||
|
npx create-remix@latest
|
||||||
|
cd my-new-remix-app
|
||||||
|
# remove the new project's app (not the old one!)
|
||||||
|
rm -rf app
|
||||||
|
# copy your app over
|
||||||
|
cp -R ../my-old-remix-app/app app
|
||||||
|
```
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { RemixBrowser } from "@remix-run/react";
|
||||||
|
import { startTransition, StrictMode } from "react";
|
||||||
|
import { hydrateRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
function hydrate() {
|
||||||
|
startTransition(() => {
|
||||||
|
hydrateRoot(
|
||||||
|
document,
|
||||||
|
<StrictMode>
|
||||||
|
<RemixBrowser />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof requestIdleCallback === "function") {
|
||||||
|
requestIdleCallback(hydrate);
|
||||||
|
} else {
|
||||||
|
// Safari doesn't support requestIdleCallback
|
||||||
|
// https://caniuse.com/requestidlecallback
|
||||||
|
setTimeout(hydrate, 1);
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { PassThrough } from "stream";
|
||||||
|
|
||||||
|
import { Response } from "@remix-run/node";
|
||||||
|
import { RemixServer } from "@remix-run/react";
|
||||||
|
import isbot from "isbot";
|
||||||
|
import { renderToPipeableStream } from "react-dom/server";
|
||||||
|
|
||||||
|
const ABORT_DELAY = 5000;
|
||||||
|
|
||||||
|
export default function handleRequest(
|
||||||
|
request,
|
||||||
|
responseStatusCode,
|
||||||
|
responseHeaders,
|
||||||
|
remixContext
|
||||||
|
) {
|
||||||
|
return isbot(request.headers.get("user-agent"))
|
||||||
|
? handleBotRequest(
|
||||||
|
request,
|
||||||
|
responseStatusCode,
|
||||||
|
responseHeaders,
|
||||||
|
remixContext
|
||||||
|
)
|
||||||
|
: handleBrowserRequest(
|
||||||
|
request,
|
||||||
|
responseStatusCode,
|
||||||
|
responseHeaders,
|
||||||
|
remixContext
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBotRequest(
|
||||||
|
request,
|
||||||
|
responseStatusCode,
|
||||||
|
responseHeaders,
|
||||||
|
remixContext
|
||||||
|
) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let didError = false;
|
||||||
|
|
||||||
|
const { pipe, abort } = renderToPipeableStream(
|
||||||
|
<RemixServer context={remixContext} url={request.url} />,
|
||||||
|
{
|
||||||
|
onAllReady() {
|
||||||
|
const body = new PassThrough();
|
||||||
|
|
||||||
|
responseHeaders.set("Content-Type", "text/html");
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
new Response(body, {
|
||||||
|
headers: responseHeaders,
|
||||||
|
status: didError ? 500 : responseStatusCode,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
pipe(body);
|
||||||
|
},
|
||||||
|
onShellError(error) {
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
didError = true;
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(abort, ABORT_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBrowserRequest(
|
||||||
|
request,
|
||||||
|
responseStatusCode,
|
||||||
|
responseHeaders,
|
||||||
|
remixContext
|
||||||
|
) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let didError = false;
|
||||||
|
|
||||||
|
const { pipe, abort } = renderToPipeableStream(
|
||||||
|
<RemixServer context={remixContext} url={request.url} />,
|
||||||
|
{
|
||||||
|
onShellReady() {
|
||||||
|
const body = new PassThrough();
|
||||||
|
|
||||||
|
responseHeaders.set("Content-Type", "text/html");
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
new Response(body, {
|
||||||
|
headers: responseHeaders,
|
||||||
|
status: didError ? 500 : responseStatusCode,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
pipe(body);
|
||||||
|
},
|
||||||
|
onShellError(err) {
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
didError = true;
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(abort, ABORT_DELAY);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import {
|
||||||
|
Links,
|
||||||
|
LiveReload,
|
||||||
|
Meta,
|
||||||
|
Outlet,
|
||||||
|
Scripts,
|
||||||
|
ScrollRestoration,
|
||||||
|
} from "@remix-run/react";
|
||||||
|
|
||||||
|
export const meta = () => ({
|
||||||
|
charset: "utf-8",
|
||||||
|
title: "New Remix App",
|
||||||
|
viewport: "width=device-width,initial-scale=1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<Meta />
|
||||||
|
<Links />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Outlet />
|
||||||
|
<ScrollRestoration />
|
||||||
|
<Scripts />
|
||||||
|
<LiveReload />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { useLoaderData, Form } from "@remix-run/react";
|
||||||
|
import { unstable_createFileUploadHandler, unstable_parseMultipartFormData } from "@remix-run/node";
|
||||||
|
import prisma from "~/utils/db.server";
|
||||||
|
import { redirect } from "@remix-run/node";
|
||||||
|
import { Link } from "@remix-run/react";
|
||||||
|
|
||||||
|
export async function action({ request }) {
|
||||||
|
const clonedData = request.clone();
|
||||||
|
const formData = await clonedData.formData();
|
||||||
|
const title = formData.get("title");
|
||||||
|
const post = formData.get("post");
|
||||||
|
|
||||||
|
const fileUploadHandler = unstable_createFileUploadHandler({
|
||||||
|
directory: './public/uploads/',
|
||||||
|
maxPartSize: 500000,
|
||||||
|
file: ({ filename }) => filename,
|
||||||
|
});
|
||||||
|
|
||||||
|
const multiPartformdata = await unstable_parseMultipartFormData(request, fileUploadHandler);
|
||||||
|
const imageName = multiPartformdata.get("image").name;
|
||||||
|
|
||||||
|
const newThread = await prisma.thread.create({
|
||||||
|
data: {
|
||||||
|
title: title,
|
||||||
|
post: post,
|
||||||
|
imageName: imageName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return redirect(`threads/${newThread.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader() {
|
||||||
|
const data = await prisma.thread.findMany({
|
||||||
|
include: {
|
||||||
|
posts: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const data = useLoaderData();
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form method="post" encType="multipart/form-data">
|
||||||
|
<label htmlFor="">Title: <input required type="text" name="title" /></label>
|
||||||
|
<br />
|
||||||
|
<label htmlFor="">Post: <input required type="text" name="post" /></label>
|
||||||
|
<br />
|
||||||
|
<label htmlFor="">Image: <input required accept="image/*" type="file" id="image-upload" name="image" /></label>
|
||||||
|
<br />
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</Form>
|
||||||
|
<ul>
|
||||||
|
{data.map(thread =>
|
||||||
|
<li key={thread.id}>
|
||||||
|
<Link to={`threads/${thread.id}`}>{thread.title}</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { redirect, unstable_createFileUploadHandler, unstable_parseMultipartFormData } from "@remix-run/node";
|
||||||
|
import { useLoaderData, Form } from "@remix-run/react";
|
||||||
|
import prisma from "~/utils/db.server";
|
||||||
|
|
||||||
|
export async function action({ request, params }) {
|
||||||
|
const threadId = params.threadId;
|
||||||
|
|
||||||
|
const clonedData = request.clone();
|
||||||
|
const formData = await clonedData.formData();
|
||||||
|
const post = formData.get("post");
|
||||||
|
|
||||||
|
const fileUploadHandler = unstable_createFileUploadHandler({
|
||||||
|
directory: './public/uploads/',
|
||||||
|
maxPartSize: 500000,
|
||||||
|
file: ({ filename }) => filename,
|
||||||
|
});
|
||||||
|
|
||||||
|
const multiPartformdata = await unstable_parseMultipartFormData(request, fileUploadHandler);
|
||||||
|
let imageName;
|
||||||
|
multiPartformdata.get("image") !== null ? imageName = multiPartformdata.get("image").name : imageName = null;
|
||||||
|
|
||||||
|
const createPost = await prisma.post.create({
|
||||||
|
data: {
|
||||||
|
comment: post,
|
||||||
|
imageName: imageName,
|
||||||
|
postId: parseInt(threadId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return redirect(`/threads/${threadId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loader({ params }) {
|
||||||
|
const threadId = params.threadId;
|
||||||
|
const thread = await prisma.thread.findUnique({
|
||||||
|
where: {
|
||||||
|
id: Number(threadId),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
posts: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Thread() {
|
||||||
|
const data = useLoaderData();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<span><strong>{data.title}</strong> Post id: <strong>{data.id}</strong> Created at: <strong>{data.createdAt}</strong> Reply count: <strong>{data.posts.length}</strong></span>
|
||||||
|
<br />
|
||||||
|
<a href={`/${data.imageName}`} target="_blank"><img src={`/uploads/${data.imageName}`} alt="post image" width={240} /></a>
|
||||||
|
<p>{data.post}</p>
|
||||||
|
<hr />
|
||||||
|
<ul>
|
||||||
|
{data.posts.map(post =>
|
||||||
|
<li key={post.id}>
|
||||||
|
<span>Reply id: <strong>{post.id}</strong> Replied at: <strong>{post.createdAt}</strong></span>
|
||||||
|
<br />
|
||||||
|
{post.imageName !== 'null' ? <img width={240} src={`/uploads/${post.imageName}`} alt="post image" /> : ''}
|
||||||
|
<p>{post.comment}</p>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<Form method="post" encType="multipart/form-data">
|
||||||
|
<label htmlFor="text-input">
|
||||||
|
Post: <input required minLength={3} name="post" type="text" />
|
||||||
|
</label>
|
||||||
|
<label htmlFor="image-upload">
|
||||||
|
Image: <input type="file" name="image" id="image-upload" />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
let prisma;
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
prisma = new PrismaClient();
|
||||||
|
} else {
|
||||||
|
if (!global.prisma) {
|
||||||
|
global.prisma = new PrismaClient();
|
||||||
|
}
|
||||||
|
prisma = global.prisma;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default prisma;
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"include": ["**/*.js", "**/*.jsx"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"target": "ES2019",
|
||||||
|
"strict": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./app/*"]
|
||||||
|
},
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"build": "remix build",
|
||||||
|
"dev": "remix dev",
|
||||||
|
"start": "remix-serve build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/client": "^4.8.1",
|
||||||
|
"@remix-run/node": "^1.10.0",
|
||||||
|
"@remix-run/react": "^1.10.0",
|
||||||
|
"@remix-run/serve": "^1.10.0",
|
||||||
|
"isbot": "^3.6.5",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@remix-run/dev": "^1.10.0",
|
||||||
|
"@remix-run/eslint-config": "^1.10.0",
|
||||||
|
"eslint": "^8.27.0",
|
||||||
|
"prisma": "^4.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,37 @@
|
||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Thread {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
|
||||||
|
title String
|
||||||
|
post String
|
||||||
|
imageName String
|
||||||
|
|
||||||
|
posts Post[]
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Post {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
|
||||||
|
comment String
|
||||||
|
imageName String?
|
||||||
|
|
||||||
|
post Thread @relation(fields: [postId], references: [id])
|
||||||
|
postId Int
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -0,0 +1,8 @@
|
||||||
|
/** @type {import('@remix-run/dev').AppConfig} */
|
||||||
|
module.exports = {
|
||||||
|
ignoredRouteFiles: ["**/.*"],
|
||||||
|
// appDirectory: "app",
|
||||||
|
// assetsBuildDirectory: "public/build",
|
||||||
|
// serverBuildPath: "build/index.js",
|
||||||
|
// publicPath: "/build/",
|
||||||
|
};
|
Loading…
Reference in New Issue