base editor
This commit is contained in:
@@ -8,6 +8,7 @@ export async function createPost(
|
||||
next: NextFunction,
|
||||
) {
|
||||
try {
|
||||
console.log(req.body);
|
||||
const body = schemas.posts.create.parse(req.body);
|
||||
|
||||
const post = await prisma.post.create({
|
||||
|
||||
@@ -17,6 +17,8 @@ import usersRoute from "./routers/users.js";
|
||||
import postsRoute from "./routers/posts.js";
|
||||
|
||||
const app = express();
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(express.json());
|
||||
|
||||
// passport strategy
|
||||
passport.use(
|
||||
@@ -54,7 +56,6 @@ passport.deserializeUser(async (id: number, done) => {
|
||||
done(null, user ?? false);
|
||||
});
|
||||
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(
|
||||
session({
|
||||
secret: env.SESSION_SECRET,
|
||||
@@ -63,7 +64,6 @@ app.use(
|
||||
}),
|
||||
);
|
||||
app.use(passport.session());
|
||||
app.use(express.json());
|
||||
if (env.NODE_ENV === "production") {
|
||||
app.use(
|
||||
cors({
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.18.3",
|
||||
"tailwindcss": "^4.3.3"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
body {
|
||||
background-color: black;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
type InputType = {
|
||||
label: string;
|
||||
forLabel: string;
|
||||
type: "password" | "email" | "text";
|
||||
dark?: boolean;
|
||||
onChange?: (v: string) => void;
|
||||
};
|
||||
|
||||
export default function Input({
|
||||
label,
|
||||
forLabel,
|
||||
type,
|
||||
dark = true,
|
||||
onChange,
|
||||
}: InputType) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<label htmlFor={forLabel} className="text-sm block">
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
id={forLabel}
|
||||
onChange={onChange ? (e) => onChange(e.target.value) : undefined}
|
||||
type={type}
|
||||
required
|
||||
name={forLabel}
|
||||
className={`${dark ? "bg-black text-white" : "bg-white text-black"} rounded-lg border-white block`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,26 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Header from "./header";
|
||||
import { useAuth } from "../providers/AuthProvider";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="bg-black text-white min-h-screen w-full max-w-[42rem] 2xl:max-w-[48rem] mx-auto select-none px-5">
|
||||
<Header />
|
||||
<div className="relative">{children}</div>
|
||||
</div>
|
||||
);
|
||||
export default function Layout({
|
||||
children,
|
||||
isProtected = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
isProtected?: boolean;
|
||||
}) {
|
||||
const auth = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (isProtected && !auth.user) {
|
||||
navigate("/");
|
||||
} else {
|
||||
return (
|
||||
<div className="bg-black text-white min-h-screen w-full max-w-[42rem] 2xl:max-w-[48rem] mx-auto select-none px-5">
|
||||
<Header />
|
||||
<div className="relative">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import "./assets/index.css";
|
||||
import Home from "./pages/home";
|
||||
import LogIn from "./pages/login";
|
||||
import Post from "./pages/post";
|
||||
import Editor from "./pages/editor";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -21,6 +22,14 @@ const router = createBrowserRouter([
|
||||
path: "/post/:slug",
|
||||
element: <Post />,
|
||||
},
|
||||
{
|
||||
path: "/editor",
|
||||
element: <Editor />,
|
||||
},
|
||||
{
|
||||
path: "/editor/:slug",
|
||||
element: <Editor />,
|
||||
},
|
||||
]);
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import Layout from "../components/layout";
|
||||
import Input from "../components/input";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import type { Post } from "../utils/types";
|
||||
|
||||
export default function Editor() {
|
||||
const [editingPost, setEditingPost] = useState<Post | null>(null);
|
||||
const [values, setValues] = useState<Partial<Post>>({});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [viewingMode, setViewingMode] = useState<"editing" | "preview">(
|
||||
"editing",
|
||||
);
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (params.slug) {
|
||||
fetch("/api/posts/" + params.slug, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(async (res) => {
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setEditingPost(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function createPost() {
|
||||
console.log(values);
|
||||
|
||||
if (!values?.title || !values.content || !values.description) {
|
||||
setError("Missing required value");
|
||||
return;
|
||||
}
|
||||
|
||||
const title = values?.title;
|
||||
const slug = title
|
||||
.replace(/[^a-zA-Z0-9 ]/g, "")
|
||||
.replaceAll(" ", "-")
|
||||
.replaceAll(" ", "-")
|
||||
.toLowerCase();
|
||||
|
||||
const res = await fetch("/api/posts/create", {
|
||||
method: "post",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...values,
|
||||
slug,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
navigate("/post/" + slug);
|
||||
} else {
|
||||
setError("Failed to create post");
|
||||
console.error("Failed to create post! " + res.status);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout isProtected={true}>
|
||||
<h1 className="text-2xl font-bold">
|
||||
{editingPost ? "Edit" : "Create"} Post
|
||||
</h1>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
forLabel="title"
|
||||
label="Title"
|
||||
type="text"
|
||||
dark={false}
|
||||
onChange={(v) =>
|
||||
setValues((prevState) => {
|
||||
return {
|
||||
...prevState,
|
||||
title: v,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
forLabel="description"
|
||||
label="Description"
|
||||
type="text"
|
||||
dark={false}
|
||||
onChange={(v) =>
|
||||
setValues((prevState) => {
|
||||
return {
|
||||
...prevState,
|
||||
description: v,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<h1 className="space-x-2">
|
||||
<span
|
||||
onClick={() => setViewingMode("editing")}
|
||||
className={`${viewingMode === "editing" ? "underline" : "hover:underline"} hover:cursor-pointer`}
|
||||
>
|
||||
Editing
|
||||
</span>
|
||||
<span
|
||||
onClick={() => setViewingMode("preview")}
|
||||
className={`${viewingMode === "preview" ? "underline" : "hover:underline"} hover:cursor-pointer`}
|
||||
>
|
||||
Preview
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{viewingMode === "editing" ? (
|
||||
<textarea
|
||||
defaultValue={editingPost?.content || undefined}
|
||||
value={values.content}
|
||||
onChange={(v) => {
|
||||
setValues((prevState) => {
|
||||
return {
|
||||
...prevState,
|
||||
content: v.target.value,
|
||||
};
|
||||
});
|
||||
}}
|
||||
className="p-2 bg-white text-black w-full h-120"
|
||||
/>
|
||||
) : (
|
||||
<div className="prose prose-invert">
|
||||
<Markdown>{values.content}</Markdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white p-2 rounded-lg w-full text-black font-bold"
|
||||
onClick={createPost}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +1,7 @@
|
||||
import { useAuth } from "../providers/AuthProvider";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
type InputType = {
|
||||
label: string;
|
||||
forLabel: string;
|
||||
type: "password" | "email";
|
||||
};
|
||||
const Input = ({ label, forLabel, type }: InputType) => {
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={forLabel} className="text-sm">
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
id={forLabel}
|
||||
type={type}
|
||||
required
|
||||
name={forLabel}
|
||||
className="bg-black text-white rounded-lg border-white block"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import Input from "../components/input";
|
||||
|
||||
export default function LogIn() {
|
||||
const auth = useAuth();
|
||||
|
||||
@@ -2,6 +2,7 @@ import Layout from "../components/layout";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import type { Post } from "../utils/types";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
export default function Post() {
|
||||
const [post, setPost] = useState<Post | null>(null);
|
||||
@@ -33,6 +34,18 @@ export default function Post() {
|
||||
<div className="my-2">
|
||||
<h1 className="text-2xl font-bold">{post.title}</h1>
|
||||
<p className="text-sm opacity-50">{post.description}</p>
|
||||
|
||||
<div className="my-2">
|
||||
<div className="prose prose-invert">
|
||||
<Markdown>{post.content}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm opacity-50">
|
||||
Posted {new Date(post.createdAt).toLocaleString()}{" "}
|
||||
{post.updatedAt !== post.createdAt &&
|
||||
"- updated on " + new Date(post.updatedAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
Generated
+702
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user