From 4cf612758c3881945da153bfd843d998f70b954d Mon Sep 17 00:00:00 2001 From: Alex Frantz Date: Sun, 6 Sep 2026 22:41:55 -0400 Subject: [PATCH] base editor --- apps/backend/src/controllers/posts.ts | 1 + apps/backend/src/index.ts | 4 +- apps/frontend/package.json | 2 + apps/frontend/src/assets/index.css | 1 + apps/frontend/src/components/input.tsx | 31 ++ apps/frontend/src/components/layout.tsx | 29 +- apps/frontend/src/main.tsx | 9 + apps/frontend/src/pages/editor.tsx | 148 +++++ apps/frontend/src/pages/login.tsx | 23 +- apps/frontend/src/pages/post.tsx | 13 + pnpm-lock.yaml | 702 ++++++++++++++++++++++++ 11 files changed, 932 insertions(+), 31 deletions(-) create mode 100644 apps/frontend/src/components/input.tsx create mode 100644 apps/frontend/src/pages/editor.tsx diff --git a/apps/backend/src/controllers/posts.ts b/apps/backend/src/controllers/posts.ts index 26464c3..5843cfd 100644 --- a/apps/backend/src/controllers/posts.ts +++ b/apps/backend/src/controllers/posts.ts @@ -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({ diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index d444d5a..ee55f52 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -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({ diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 202ca25..37d569f 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -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" }, diff --git a/apps/frontend/src/assets/index.css b/apps/frontend/src/assets/index.css index 00b8bb2..9bd8d16 100644 --- a/apps/frontend/src/assets/index.css +++ b/apps/frontend/src/assets/index.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@plugin "@tailwindcss/typography"; body { background-color: black; diff --git a/apps/frontend/src/components/input.tsx b/apps/frontend/src/components/input.tsx new file mode 100644 index 0000000..6acc570 --- /dev/null +++ b/apps/frontend/src/components/input.tsx @@ -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 ( +
+ + 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`} + /> +
+ ); +} diff --git a/apps/frontend/src/components/layout.tsx b/apps/frontend/src/components/layout.tsx index 5474328..59fe358 100644 --- a/apps/frontend/src/components/layout.tsx +++ b/apps/frontend/src/components/layout.tsx @@ -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 ( -
-
-
{children}
-
- ); +export default function Layout({ + children, + isProtected = false, +}: { + children: ReactNode; + isProtected?: boolean; +}) { + const auth = useAuth(); + const navigate = useNavigate(); + + if (isProtected && !auth.user) { + navigate("/"); + } else { + return ( +
+
+
{children}
+
+ ); + } } diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx index 8e3d9e6..e21f152 100644 --- a/apps/frontend/src/main.tsx +++ b/apps/frontend/src/main.tsx @@ -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: , }, + { + path: "/editor", + element: , + }, + { + path: "/editor/:slug", + element: , + }, ]); createRoot(document.getElementById("root")!).render( diff --git a/apps/frontend/src/pages/editor.tsx b/apps/frontend/src/pages/editor.tsx new file mode 100644 index 0000000..d69d93a --- /dev/null +++ b/apps/frontend/src/pages/editor.tsx @@ -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(null); + const [values, setValues] = useState>({}); + const [error, setError] = useState(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 ( + +

+ {editingPost ? "Edit" : "Create"} Post +

+ +
+ + setValues((prevState) => { + return { + ...prevState, + title: v, + }; + }) + } + /> + + setValues((prevState) => { + return { + ...prevState, + description: v, + }; + }) + } + /> + +

+ setViewingMode("editing")} + className={`${viewingMode === "editing" ? "underline" : "hover:underline"} hover:cursor-pointer`} + > + Editing + + setViewingMode("preview")} + className={`${viewingMode === "preview" ? "underline" : "hover:underline"} hover:cursor-pointer`} + > + Preview + +

+ + {viewingMode === "editing" ? ( +