Started working on the blogs
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
40
frontend/components/editable-text.tsx
Normal file
40
frontend/components/editable-text.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
11
frontend/lib/sluggify.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user