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