base editor
This commit is contained in:
@@ -8,6 +8,7 @@ export async function createPost(
|
|||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
console.log(req.body);
|
||||||
const body = schemas.posts.create.parse(req.body);
|
const body = schemas.posts.create.parse(req.body);
|
||||||
|
|
||||||
const post = await prisma.post.create({
|
const post = await prisma.post.create({
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import usersRoute from "./routers/users.js";
|
|||||||
import postsRoute from "./routers/posts.js";
|
import postsRoute from "./routers/posts.js";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
app.use(express.urlencoded({ extended: false }));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
// passport strategy
|
// passport strategy
|
||||||
passport.use(
|
passport.use(
|
||||||
@@ -54,7 +56,6 @@ passport.deserializeUser(async (id: number, done) => {
|
|||||||
done(null, user ?? false);
|
done(null, user ?? false);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(express.urlencoded({ extended: false }));
|
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
secret: env.SESSION_SECRET,
|
secret: env.SESSION_SECRET,
|
||||||
@@ -63,7 +64,6 @@ app.use(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use(passport.session());
|
app.use(passport.session());
|
||||||
app.use(express.json());
|
|
||||||
if (env.NODE_ENV === "production") {
|
if (env.NODE_ENV === "production") {
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
|
|||||||
@@ -10,9 +10,11 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.18.3",
|
"react-router-dom": "^7.18.3",
|
||||||
"tailwindcss": "^4.3.3"
|
"tailwindcss": "^4.3.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: black;
|
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 type { ReactNode } from "react";
|
||||||
import Header from "./header";
|
import Header from "./header";
|
||||||
|
import { useAuth } from "../providers/AuthProvider";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: ReactNode }) {
|
export default function Layout({
|
||||||
return (
|
children,
|
||||||
<div className="bg-black text-white min-h-screen w-full max-w-[42rem] 2xl:max-w-[48rem] mx-auto select-none px-5">
|
isProtected = false,
|
||||||
<Header />
|
}: {
|
||||||
<div className="relative">{children}</div>
|
children: ReactNode;
|
||||||
</div>
|
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 Home from "./pages/home";
|
||||||
import LogIn from "./pages/login";
|
import LogIn from "./pages/login";
|
||||||
import Post from "./pages/post";
|
import Post from "./pages/post";
|
||||||
|
import Editor from "./pages/editor";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -21,6 +22,14 @@ const router = createBrowserRouter([
|
|||||||
path: "/post/:slug",
|
path: "/post/:slug",
|
||||||
element: <Post />,
|
element: <Post />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/editor",
|
||||||
|
element: <Editor />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/editor/:slug",
|
||||||
|
element: <Editor />,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
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 { useAuth } from "../providers/AuthProvider";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import Input from "../components/input";
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LogIn() {
|
export default function LogIn() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Layout from "../components/layout";
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import type { Post } from "../utils/types";
|
import type { Post } from "../utils/types";
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
|
||||||
export default function Post() {
|
export default function Post() {
|
||||||
const [post, setPost] = useState<Post | null>(null);
|
const [post, setPost] = useState<Post | null>(null);
|
||||||
@@ -33,6 +34,18 @@ export default function Post() {
|
|||||||
<div className="my-2">
|
<div className="my-2">
|
||||||
<h1 className="text-2xl font-bold">{post.title}</h1>
|
<h1 className="text-2xl font-bold">{post.title}</h1>
|
||||||
<p className="text-sm opacity-50">{post.description}</p>
|
<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>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
Generated
+702
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user