Started working on the blogs

This commit is contained in:
cdricms
2025-02-21 17:49:42 +01:00
parent de828d4c13
commit 7a97961fef
9 changed files with 178 additions and 79 deletions

View File

@@ -1,54 +0,0 @@
"use client";
import { Editor } from "@/components/editor";
import { Button } from "@/components/ui/button";
import useApiMutation from "@/hooks/use-api";
import { useState } from "react";
export default function BlogEditor() {
const {
trigger,
isMutating: loading,
isSuccess,
} = useApiMutation("/blog/new", undefined, "POST", false, true);
const content = localStorage.getItem("blog_draft") ?? "";
return (
<section className="m-10">
<div className="flex">
<div className="flex-1">
<Editor content={content} />
</div>
</div>
<Button
className="text-black bg-white"
onClick={async () => {
try {
// const blogContent = localStorage.getItem("blog_draft");
const res = await trigger({
label: "This is my label",
summary: "A summary",
image: "none",
href: "none",
blogID: "id",
slug: "myslug",
// content: blogContent,
published: "",
});
if (!res)
throw new Error("The server hasn't responded.");
if (res.status === "Error")
throw new Error(res.message);
if (res.data) console.log(res.data);
return res;
} catch (error: any) {
throw new Error(error.message);
}
}}
>
Sauvegarder
</Button>
</section>
);
}

View File

@@ -1,14 +1,73 @@
"use client";
import dynamic from "next/dynamic";
import EditableText from "@/components/editable-text";
import { LocalEditor } from "@/components/editor";
import { Button } from "@/components/ui/button";
import useApiMutation from "@/hooks/use-api";
import sluggify from "@/lib/sluggify";
import { NewBlog } from "@/types/types";
import { useMemo, useState } from "react";
const Editor = dynamic(() => import("./editor").then((mod) => mod.default), {
ssr: false,
});
export default function BlogEditor() {
const [title, setTitle] = useState("");
const slug = useMemo(() => sluggify(title), [title]);
const {
trigger: newBlog,
isMutating: loading,
isSuccess,
} = useApiMutation<undefined, NewBlog>(
"/blog/new",
undefined,
"POST",
true,
false,
);
const content = localStorage.getItem("blog_draft") ?? "";
export default async function NewBlog() {
return (
<>
<Editor />
</>
<section className="m-10">
{/* This div should have a proper layout, ONLY PART TO BE MODIFIED*/}
<div>
{/*Should have an image or a placeholder that opens a dialog when clicked on to set the url of an image.*/}
<EditableText onChange={setTitle}>
<h1>{title}</h1>
</EditableText>
<p className="text-muted">{slug}</p>
{/*Should have a summary input box or textarea*/}
</div>
<div className="flex">
<div className="flex-1">
<LocalEditor content={content} setTitle={setTitle} />
</div>
</div>
<Button
className="text-black bg-white"
onClick={async () => {
try {
const blogContent = localStorage.getItem("blog_draft");
if (!blogContent) return;
if (title.length < 1) return;
const res = await newBlog({
title,
summary: "A summary",
image: "none",
slug,
content: blogContent,
});
if (!res)
throw new Error("The server hasn't responded.");
if (res.status === "Error")
throw new Error(res.message);
if (res.data) console.log(res.data);
return res;
} catch (error: any) {
throw new Error(error.message);
}
}}
>
Sauvegarder
</Button>
</section>
);
}

View File

@@ -0,0 +1,40 @@
import { useState, ReactNode, cloneElement } from "react";
import { Input } from "@/components/ui/input";
interface EditableTextProps {
children: ReactNode;
onChange: (newText: string) => void;
}
export default function EditableText({
children,
onChange,
}: EditableTextProps) {
const [isEditing, setIsEditing] = useState(false);
const child = Array.isArray(children) ? children[0] : children;
const text = child?.props.children || "";
const handleBlur = () => {
setIsEditing(false);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
return (
<div onClick={() => setIsEditing(true)}>
{isEditing ? (
<Input
autoFocus
value={text}
onChange={handleChange}
onBlur={handleBlur}
/>
) : (
cloneElement(child, {}, text)
)}
</div>
);
}

View File

@@ -4,7 +4,7 @@ import * as React from "react";
import { cn } from "@/lib/utils";
import { Card } from "@/components/ui/card";
import { EditorContent, useEditor } from "@tiptap/react";
import { Editor, EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline";
import Link from "@tiptap/extension-link";
@@ -16,11 +16,28 @@ import { EditorMenu } from "./editor-menu";
interface EditorProps {
content: string;
onChange?: (markdown: string) => void;
onChange?: (content: string) => void;
className?: string;
setTitle?: React.Dispatch<React.SetStateAction<string>>;
}
export function Editor({ content, onChange, className }: EditorProps) {
export function LocalEditor({
content,
onChange,
className,
setTitle,
}: EditorProps) {
const getTitle = (editor: Editor) => {
const h1s: string[] = [];
editor.state.doc.descendants((node, pos) => {
if (node.type.name === "heading" && node.attrs.level === 1) {
h1s.push(node.textContent);
}
});
return h1s.length > 0 ? h1s[0] : null;
};
const editor = useEditor({
extensions: [
StarterKit,
@@ -46,9 +63,16 @@ export function Editor({ content, onChange, className }: EditorProps) {
class: "prose prose-sm sm:prose-base lg:prose-lg xl:prose-2xl m-5 focus:outline-none dark:prose-invert",
},
},
onCreate: ({ editor }) => {
const title = getTitle(editor);
setTitle?.(title ?? "");
},
onUpdate: ({ editor }) => {
console.log("Update");
localStorage.setItem("blog_draft", editor.getHTML());
// Set the first H1 if it exists
const title = getTitle(editor);
setTitle?.(title ?? "");
},
});

11
frontend/lib/sluggify.ts Normal file
View File

@@ -0,0 +1,11 @@
export default function sluggify(text: string): string {
return text
.toLowerCase() // Convert to lowercase
.trim() // Remove leading/trailing whitespace
.normalize("NFD") // Normalize special characters
.replace(/[\u0300-\u036f]/g, "") // Remove diacritics
.replace(/[^a-z0-9\s-]/g, "") // Remove special characters except spaces and hyphens
.replace(/\s+/g, "-") // Replace spaces with hyphens
.replace(/-+/g, "-") // Replace multiple hyphens with single hyphen
.slice(0, 100); // Limit length to 100 characters
}

View File

@@ -24,16 +24,20 @@ export interface Blog {
blogID: string;
slug: string;
content: string;
label?: string;
title: string;
authorID: string;
published: string;
summary?: string;
image?: string;
href?: string;
author: User; // Relation to User
}
export type NewBlog = Omit<
Blog,
"blogID" | "authorID" | "author" | "published"
>;
// User type definition
export interface User {
userId: string; // UUID represented as a string