Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into fetching-data
Browse files Browse the repository at this point in the history
  • Loading branch information
in-mai-space committed Jun 11, 2024
2 parents b7a5dc6 + 254df35 commit ead7afe
Show file tree
Hide file tree
Showing 12 changed files with 1,368 additions and 0 deletions.
38 changes: 38 additions & 0 deletions frontend/dashboard/src/components/dashboard/bubbleMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {BubbleMenu} from '@tiptap/react';
import { Editor } from '@tiptap/react'
import { Bold, Heading1, Heading2, Italic, ListIcon } from 'lucide-react';

interface BubbleMenuProps {
editor: Editor | null;
}

export const EditorBubbleMenu: React.FC<BubbleMenuProps> = ({ editor }) => {
if (!editor) {
return null
}

return (
<BubbleMenu className="flex items-center flex-row bg-gray-300 rounded-md space-x-1 p-2" editor={editor} tippyOptions={{ duration: 100 }}>
<div className={editor.isActive('bold') ? 'bg-gray-400 rounded-md p-1.5' : 'p-1.5'}
onClick={() => editor.chain().focus().toggleBold().run()}>
<Bold size={20} />
</div>
<div className={editor.isActive('italic') ? 'bg-gray-400 rounded-md p-1.5' : 'p-1.5'}
onClick={() => editor.chain().focus().toggleItalic().run()}>
<Italic size={20}/>
</div>
<div className={editor.isActive('heading', { level: 1 }) ? 'bg-gray-400 rounded-md p-1.5' : 'p-1.5'}
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}>
<Heading1 size={20} />
</div>
<div className={editor.isActive('heading', { level: 2 }) ? 'bg-gray-400 rounded-md p-1.5' : 'p-1.5'}
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}>
<Heading2 size={20}/>
</div>
<div className={editor.isActive('bulletList') ? 'bg-gray-400 rounded-md p-1.5' : 'p-1.5'}
onClick={() => editor.chain().focus().toggleBulletList().run()}>
<ListIcon size={20}/>
</div>
</BubbleMenu>
)
}
5 changes: 5 additions & 0 deletions frontend/dashboard/src/components/dashboard/divider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const Divider = () => {
return (
<div className="bg-black h-5 w-0.5 mx-5" />
)
}
36 changes: 36 additions & 0 deletions frontend/dashboard/src/components/dashboard/editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client"
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { Toolbar } from './toolbar'
import Underline from '@tiptap/extension-underline';
import { Link } from '@tiptap/extension-link';
import { BubbleMenu } from '@tiptap/extension-bubble-menu';
import { EditorBubbleMenu } from './bubbleMenu';
import React from 'react';
import './styles.css';

const Tiptap = () => {
const editor = useEditor({
autofocus: true,
editable: false,
extensions: [
StarterKit, Underline,
BubbleMenu,
Link.configure({
openOnClick: true,
autolink: true,
}),
],
content: {"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Hello World"}]},{"type":"heading","attrs":{"level":2},"content":[{"type":"text","marks":[{"type":"bold"},{"type":"italic"},{"type":"strike"},{"type":"underline"}],"text":"This is generate"}]},{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Task 1"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Task 2"}]}]}]},{"type":"orderedList","attrs":{"start":1},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Task 3"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Task 4"}]}]}]},{"type":"paragraph","content":[{"type":"text","marks":[{"type":"link","attrs":{"href":"https://generatenu.com","target":"_blank","rel":"noopener noreferrer nofollow","class":null}}],"text":"link"}]}]},
})

return (
<>
<Toolbar editor={editor}/>
{editor && <EditorBubbleMenu editor={editor}/>}
<EditorContent className="h-15 pl-10 p-5 rounded-md bg-slate-200" editor={editor} />
</>
)
}

export default Tiptap;
104 changes: 104 additions & 0 deletions frontend/dashboard/src/components/dashboard/hyperlink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { Editor } from "@tiptap/react";
import { Link } from "lucide-react";
import { useEffect } from "react";
import { z } from "zod";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

interface HyperLinkProps {
editor: Editor | null;
disabled: boolean;
}

const linkSchema = z.object({
link: z.string().url("Invalid URL").optional(),
});

type LinkData = z.infer<typeof linkSchema>;

export const HyperlinkButton: React.FC<HyperLinkProps> = ({ editor, disabled }) => {
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<LinkData>({
resolver: zodResolver(linkSchema),
defaultValues: {
link: editor?.getAttributes("link").href || "", // Use optional chaining here
},
});

useEffect(() => {
if (!editor) return;

const updateLink = () => {
const currentLink = editor.getAttributes("link").href;
setValue("link", currentLink || "");
};

editor.on("transaction", updateLink);
return () => {
editor.off("transaction", updateLink);
};
}, [editor, setValue]);

const setLinkInEditor = (data: LinkData) => {
const url = data.link;
if (!url) {
editor?.chain().focus().unsetLink().run();
return;
}
editor?.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
};

const onSubmit = handleSubmit((data) => {
setLinkInEditor(data);
});

return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="link"
className={editor?.isActive("link") && editor?.isEditable ? "bg-slate-300 rounded-md p-1.5" : "p-1.5"}
size="icon"
disabled={disabled}
>
<Link />
</Button>
</PopoverTrigger>
<PopoverContent className="w-76">
<form onSubmit={onSubmit} className="flex flex-row items-center space-x-5">
<div className="flex flex-row space-x-2">
<div className="flex flex-col items-top">
<Controller
name="link"
control={control}
render={({ field }) => (
<Input
id="link"
placeholder="Enter a link"
{...field}
onKeyDown={(e) => {
if (e.key === "Enter") {
onSubmit();
}
}}
/>
)}
/>
{errors.link && (
<p className="text-sm pt-1 text-red-400">{errors.link.message}</p>
)}
</div>
<Button type="submit">Submit</Button>
</div>
</form>
</PopoverContent>
</Popover>
);
};
20 changes: 20 additions & 0 deletions frontend/dashboard/src/components/dashboard/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.tiptap {
> * + * {
margin-top: 0.75em;
}

a {
color: revert;
text-decoration: revert;
}

ul, ol {
list-style: revert;
padding-left: revert;
}

h1, h2 {
font-size: revert;
font-weight: revert;
}
}
101 changes: 101 additions & 0 deletions frontend/dashboard/src/components/dashboard/toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Editor } from '@tiptap/react'
import {Bold, Italic, Strikethrough,
Heading1, Heading2, ListOrdered,
Undo, Redo, Unlink,
ListIcon, UnderlineIcon} from 'lucide-react';
import { useState } from 'react';
import { Divider } from './divider';
import { HyperlinkButton } from './hyperlink';
import { Button } from "../ui/button";

interface ToolbarProps {
editor: Editor | null;
}

export const Toolbar: React.FC<ToolbarProps> = ({ editor }) => {
const [, setJSON] = useState("")

if (!editor) {
return null
}

return (
<div className="flex flex-row bg-slate-200 p-2 rounded-md items-center mb-2 justify-between flex-wrap">
<div className="ml-1 flex flex-row items-center flex-wrap space-x-1">
<Button onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.isEditable}
variant="link"
size="icon"
className={editor.isActive('bold') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}>
<Bold />
</Button>
<Button disabled={!editor.isEditable}
variant="link"
size="icon"
onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive('italic') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}>
<Italic />
</Button>
<Button variant="link"
size="icon" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleStrike().run()}
className={editor.isActive('strike') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}>
<Strikethrough />
</Button>
<Button variant="link"
size="icon" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleUnderline().run()}
className={editor.isActive('underline') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}>
<UnderlineIcon />
</Button>
<Divider />
<Button variant="link"
size="icon" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={editor.isActive('heading', { level: 1 }) && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}>
<Heading1 />
</Button>
<Button variant="link"
size="icon" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={editor.isActive('heading', { level: 2 }) && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}>
<Heading2 />
</Button>
<Divider />
<Button variant="link"
size="icon" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive('bulletList') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}>
<ListIcon />
</Button>
<Button variant="link"
size="icon" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={editor.isActive('orderedList') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}>
<ListOrdered />
</Button>
<HyperlinkButton disabled={!editor.isEditable} editor={editor}/>
<Button
variant="link"
size="icon"
onClick={() => {
editor.chain().focus().unsetLink().run();
}}
disabled={!editor.isActive('link') || !editor.isEditable}
className="p-1.5"
>
<Unlink />
</Button>
<Divider />
<Button variant="link"
size="icon" disabled={!editor.isEditable} className="p-1.5" onClick={() => editor.chain().focus().undo().run()}>
<Undo />
</Button>
<Button variant="link"
size="icon" disabled={!editor.isEditable} className="p-1.5" onClick={() => editor.chain().focus().redo().run()}>
<Redo />
</Button>
</div>

<Button onClick={() => {
setJSON(JSON.stringify(editor.getJSON()));
editor.setEditable(!editor.isEditable);
}}
className="px-4 py-1 bg-black rounded-md text-white text-sm">{editor.isEditable ? "Save" : "Edit"}</Button>
</div>
)
}
Loading

0 comments on commit ead7afe

Please sign in to comment.