base editor

This commit is contained in:
2026-09-06 22:41:55 -04:00
parent 04e66527ca
commit 4cf612758c
11 changed files with 932 additions and 31 deletions
+1
View File
@@ -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({
+2 -2
View File
@@ -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({
+2
View File
@@ -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
View File
@@ -1,4 +1,5 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
body {
background-color: black;
+31
View File
@@ -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>
);
}
+16 -1
View File
@@ -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 }) {
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>
);
}
}
+9
View File
@@ -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(
+148
View File
@@ -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 -22
View File
@@ -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();
+13
View File
@@ -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>
);
+702
View File
File diff suppressed because it is too large Load Diff