From 7a97961fef6ed81502f0192a80e93e95f8910675 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 21 Feb 2025 17:49:42 +0100 Subject: [PATCH] Started working on the blogs --- backend/api/blogs/new.go | 9 --- .../migrations/20250221153208_update_blog.go | 25 +++++++ backend/core/models/blogs.go | 3 +- .../app/(auth)/dashboard/blogs/new/editor.tsx | 54 ------------- .../app/(auth)/dashboard/blogs/new/page.tsx | 75 +++++++++++++++++-- frontend/components/editable-text.tsx | 40 ++++++++++ frontend/components/editor.tsx | 32 +++++++- frontend/lib/sluggify.ts | 11 +++ frontend/types/types.tsx | 8 +- 9 files changed, 178 insertions(+), 79 deletions(-) create mode 100644 backend/cmd/migrate/migrations/20250221153208_update_blog.go delete mode 100644 frontend/app/(auth)/dashboard/blogs/new/editor.tsx create mode 100644 frontend/components/editable-text.tsx create mode 100644 frontend/lib/sluggify.ts diff --git a/backend/api/blogs/new.go b/backend/api/blogs/new.go index fe69e3b..761618a 100644 --- a/backend/api/blogs/new.go +++ b/backend/api/blogs/new.go @@ -3,7 +3,6 @@ package blogs import ( "context" "encoding/json" - "io" "net/http" core "fr.latosa-escrima/core" @@ -11,14 +10,6 @@ import ( ) func HandleNew(w http.ResponseWriter, r *http.Request) { - _, err := io.ReadAll(r.Body) - if err != nil { - core.JSONError{ - Status: core.Error, - Message: err.Error(), - }.Respond(w, http.StatusNoContent) - return - } var blog models.Blog if err := json.NewDecoder(r.Body).Decode(&blog); err != nil { core.JSONError{ diff --git a/backend/cmd/migrate/migrations/20250221153208_update_blog.go b/backend/cmd/migrate/migrations/20250221153208_update_blog.go new file mode 100644 index 0000000..5932357 --- /dev/null +++ b/backend/cmd/migrate/migrations/20250221153208_update_blog.go @@ -0,0 +1,25 @@ +package migrations + +import ( + "context" + "fmt" + + "fr.latosa-escrima/core/models" + "github.com/uptrace/bun" +) + +func init() { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [up migration] ") + _, err := db.NewDropColumn(). + Model((*models.Blog)(nil)). + Column("href"). + Exec(ctx) + _, err = db.Exec(`ALTER TABLE blogs RENAME COLUMN label TO title; + ALTER TABLE blogs ALTER COLUMN title SET NOT NULL;`) + return err + }, func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [down migration] ") + return nil + }) +} diff --git a/backend/core/models/blogs.go b/backend/core/models/blogs.go index 9943256..cec999e 100644 --- a/backend/core/models/blogs.go +++ b/backend/core/models/blogs.go @@ -13,12 +13,11 @@ type Blog struct { BlogID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"blogID"` Slug string `bun:"slug,unique,notnull" json:"slug"` Content string `bun:"content,notnull" json:"content"` - Label string `bun:"label" json:"label"` + Title string `bun:"title,notnull" json:"title"` AuthorID uuid.UUID `bun:"author_id,type:uuid,notnull" json:"authorID"` Published time.Time `bun:"published,default:current_timestamp" json:"published"` Summary string `bun:"summary" json:"summary"` Image string `bun:"image" json:"image"` - Href string `bun:"href" json:"href"` Author User `bun:"rel:belongs-to,join:author_id=user_id" json:"author"` } diff --git a/frontend/app/(auth)/dashboard/blogs/new/editor.tsx b/frontend/app/(auth)/dashboard/blogs/new/editor.tsx deleted file mode 100644 index 1aed351..0000000 --- a/frontend/app/(auth)/dashboard/blogs/new/editor.tsx +++ /dev/null @@ -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 ( -
-
-
- -
-
- - -
- ); -} diff --git a/frontend/app/(auth)/dashboard/blogs/new/page.tsx b/frontend/app/(auth)/dashboard/blogs/new/page.tsx index 6ff5278..1780b8a 100644 --- a/frontend/app/(auth)/dashboard/blogs/new/page.tsx +++ b/frontend/app/(auth)/dashboard/blogs/new/page.tsx @@ -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( + "/blog/new", + undefined, + "POST", + true, + false, + ); + + const content = localStorage.getItem("blog_draft") ?? ""; -export default async function NewBlog() { return ( - <> - - +
+ {/* This div should have a proper layout, ONLY PART TO BE MODIFIED*/} +
+ {/*Should have an image or a placeholder that opens a dialog when clicked on to set the url of an image.*/} + +

{title}

+
+

{slug}

+ {/*Should have a summary input box or textarea*/} +
+
+
+ +
+
+ + +
); } diff --git a/frontend/components/editable-text.tsx b/frontend/components/editable-text.tsx new file mode 100644 index 0000000..20b4c24 --- /dev/null +++ b/frontend/components/editable-text.tsx @@ -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) => { + onChange(e.target.value); + }; + + return ( +
setIsEditing(true)}> + {isEditing ? ( + + ) : ( + cloneElement(child, {}, text) + )} +
+ ); +} diff --git a/frontend/components/editor.tsx b/frontend/components/editor.tsx index a052da7..9019764 100644 --- a/frontend/components/editor.tsx +++ b/frontend/components/editor.tsx @@ -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>; } -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 ?? ""); }, }); diff --git a/frontend/lib/sluggify.ts b/frontend/lib/sluggify.ts new file mode 100644 index 0000000..4e47499 --- /dev/null +++ b/frontend/lib/sluggify.ts @@ -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 +} diff --git a/frontend/types/types.tsx b/frontend/types/types.tsx index a33ba6c..df7ab4e 100644 --- a/frontend/types/types.tsx +++ b/frontend/types/types.tsx @@ -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