Skip to content

Commit

Permalink
fix: link input for dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
in-mai-space committed May 24, 2024
1 parent ac2d230 commit 13c02b9
Show file tree
Hide file tree
Showing 27 changed files with 2,449 additions and 8,521 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ node_modules
backend/tests/api/__debug_*
.env.prod

frontend/mobile/ios/
# frontend/mobile/ios/
frontend/mobile/android/
tmp/
ios
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,26 @@ export const EditorBubbleMenu: React.FC<BubbleMenuProps> = ({ editor }) => {
}

return (
<BubbleMenu className="flex items-center flex-row bg-slate-300 rounded-md space-x-3 p-2" editor={editor} tippyOptions={{ duration: 100 }}>
<div className={editor.isActive('bold') ? 'bg-slate-400 rounded-md p-1.5' : 'p-1.5'}
<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-slate-400 rounded-md p-1.5' : 'p-1.5'}
<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-slate-400 rounded-md p-1.5' : 'p-1.5'}
<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 />
<Heading1 size={20} />
</div>
<div className={editor.isActive('heading', { level: 2 }) ? 'bg-slate-400 rounded-md p-1.5' : 'p-1.5'}
<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 />
<Heading2 size={20}/>
</div>
<div className={editor.isActive('bulletList') ? 'bg-slate-400 rounded-md p-1.5' : 'p-1.5'}
<div className={editor.isActive('bulletList') ? 'bg-gray-400 rounded-md p-1.5' : 'p-1.5'}
onClick={() => editor.chain().focus().toggleBulletList().run()}>
<ListIcon />
<ListIcon size={20}/>
</div>
</BubbleMenu>
)
Expand Down
5 changes: 5 additions & 0 deletions frontend/dashboard/components/editor/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/components/editor/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;
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,64 @@ import {
} from "@/components/ui/popover";
import { Editor } from "@tiptap/react";
import { Link } from "lucide-react";
import { useCallback } from "react";
import { useCallback, useEffect, useState } 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"),
link: z.string().url("Invalid URL").optional(),
});

type LinkData = z.infer<typeof linkSchema>;

export const HyperlinkButton: React.FC<HyperLinkProps> = ({ editor }) => {
export const HyperlinkButton: React.FC<HyperLinkProps> = ({ editor, disabled }) => {
if (!editor) {
return null;
}

const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<LinkData>({
resolver: zodResolver(linkSchema),
defaultValues: {
link: editor.getAttributes("link").href || "",
link: editor.getAttributes("link").href,
},
});

const setLinkInEditor = useCallback(
(data: LinkData) => {
const url = data.link;
if (!url) {
editor.chain().focus().extendMarkRange("link").unsetLink().run();
return;
// set hyperlink
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();
};

// update the link input when users unlink or link
useEffect(() => {
const updateLink = () => {
const currentLink = editor.getAttributes("link").href;
if (currentLink) {
setValue("link", currentLink);
} else {
setValue("link", "");
}
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
},
[editor]
);
};
editor.on("transaction", updateLink);
return () => {
editor.off("transaction", updateLink);
};
}, [editor, setValue]);

const onSubmit = handleSubmit((data) => {
setLinkInEditor(data);
Expand All @@ -59,10 +75,11 @@ export const HyperlinkButton: React.FC<HyperLinkProps> = ({ editor }) => {
<PopoverTrigger asChild>
<Button
variant="link"
className={editor.isActive("link") ? "bg-slate-400 rounded-md p-1.5" : "p-1.5"}
className={editor.isActive("link") && editor.isEditable ? "bg-slate-300 rounded-md p-1.5" : "p-1.5"}
size="link"
disabled={disabled}
>
<Link size={20} />
<Link />
</Button>
</PopoverTrigger>
<PopoverContent className="w-76">
Expand Down Expand Up @@ -90,9 +107,9 @@ export const HyperlinkButton: React.FC<HyperLinkProps> = ({ editor }) => {
)}
</div>
<Button type="submit">Submit</Button>
</div>
</form>
</div>
</form>
</PopoverContent>
</Popover>
)
}
);
};
20 changes: 20 additions & 0 deletions frontend/dashboard/components/editor/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/components/editor/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 "@/components/ui/button";

interface ToolbarProps {
editor: Editor | null;
}

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

const [json, setJSON] = useState("")

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="link"
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="link"
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="link" 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="link" 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="link" 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="link" 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="link" 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="link" 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="link"
onClick={() => {
editor.chain().focus().unsetLink().run();
}}
disabled={!editor.isActive('link') || !editor.isEditable}
className="p-1.5"
>
<Unlink />
</Button>
<Divider />
<Button variant="link"
size="link" disabled={!editor.isEditable} className="p-1.5" onClick={() => editor.chain().focus().undo().run()}>
<Undo />
</Button>
<Button variant="link"
size="link" 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 13c02b9

Please sign in to comment.