mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
added comment editor with fixed menu
This commit is contained in:
parent
99618e93a7
commit
95439fbbef
@ -14,3 +14,7 @@ export { EditorContentWrapper } from "./ui/components/editor-content";
|
|||||||
// hooks
|
// hooks
|
||||||
export { useEditor } from "./ui/hooks/useEditor";
|
export { useEditor } from "./ui/hooks/useEditor";
|
||||||
export { useReadOnlyEditor } from "./ui/hooks/useReadOnlyEditor";
|
export { useReadOnlyEditor } from "./ui/hooks/useReadOnlyEditor";
|
||||||
|
|
||||||
|
// helper items
|
||||||
|
export * from "./ui/menus/menu-items";
|
||||||
|
export * from "./lib/editor-commands";
|
||||||
|
36
packages/editor/core/src/lib/editor-commands.ts
Normal file
36
packages/editor/core/src/lib/editor-commands.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Editor } from "@tiptap/react";
|
||||||
|
import { UploadImage } from "../types/upload-image";
|
||||||
|
import { startImageUpload } from "../ui/plugins/upload-image";
|
||||||
|
|
||||||
|
export const toggleBold = (editor: Editor) => editor?.chain().focus().toggleBold().run();
|
||||||
|
|
||||||
|
export const toggleItalic = (editor: Editor) => editor?.chain().focus().toggleItalic().run();
|
||||||
|
|
||||||
|
export const toggleUnderline = (editor: Editor) => editor?.chain().focus().toggleUnderline().run();
|
||||||
|
|
||||||
|
export const toggleStrike = (editor: Editor) => editor?.chain().focus().toggleStrike().run();
|
||||||
|
|
||||||
|
export const toggleCode = (editor: Editor) => editor?.chain().focus().toggleCode().run();
|
||||||
|
|
||||||
|
export const toggleBulletList = (editor: Editor) => editor?.chain().focus().toggleBulletList().run();
|
||||||
|
|
||||||
|
export const toggleOrderedList = (editor: Editor) => editor?.chain().focus().toggleOrderedList().run();
|
||||||
|
|
||||||
|
export const toggleBlockquote = (editor: Editor) => editor?.chain().focus().toggleBlockquote().run();
|
||||||
|
|
||||||
|
export const insertTable = (editor: Editor) => editor?.chain().focus().insertTable().run();
|
||||||
|
|
||||||
|
export const insertImage = (editor: Editor, uploadFile: UploadImage, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.accept = "image/*";
|
||||||
|
input.onchange = async () => {
|
||||||
|
if (input.files?.length) {
|
||||||
|
const file = input.files[0];
|
||||||
|
const pos = editor.view.state.selection.from;
|
||||||
|
startImageUpload(file, editor.view, pos, uploadFile, setIsSubmitting);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
@ -1,13 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
iconName: string;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
|
|
||||||
<span className={`material-symbols-rounded text-sm leading-5 font-light ${className}`}>
|
|
||||||
{iconName}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
|||||||
import { Editor } from "@tiptap/react";
|
|
||||||
import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "../../../lib/utils";
|
|
||||||
import { Tooltip } from "../table-menu/tooltip";
|
|
||||||
import { Icon } from "./icon";
|
|
||||||
|
|
||||||
export interface BubbleMenuItem {
|
|
||||||
name: string;
|
|
||||||
isActive: () => boolean;
|
|
||||||
command: () => void;
|
|
||||||
icon: typeof BoldIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
type EditorBubbleMenuProps = {
|
|
||||||
editor: Editor;
|
|
||||||
accessValue: string;
|
|
||||||
onAccessChange: (accessKey: string) => void;
|
|
||||||
commentAccess: {
|
|
||||||
icon: string;
|
|
||||||
key: string;
|
|
||||||
label: "Private" | "Public";
|
|
||||||
}[] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
|
||||||
const items: BubbleMenuItem[] = [
|
|
||||||
{
|
|
||||||
name: "bold",
|
|
||||||
isActive: () => props.editor?.isActive("bold"),
|
|
||||||
command: () => props.editor?.chain().focus().toggleBold().run(),
|
|
||||||
icon: BoldIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "italic",
|
|
||||||
isActive: () => props.editor?.isActive("italic"),
|
|
||||||
command: () => props.editor?.chain().focus().toggleItalic().run(),
|
|
||||||
icon: ItalicIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "underline",
|
|
||||||
isActive: () => props.editor?.isActive("underline"),
|
|
||||||
command: () => props.editor?.chain().focus().toggleUnderline().run(),
|
|
||||||
icon: UnderlineIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "strike",
|
|
||||||
isActive: () => props.editor?.isActive("strike"),
|
|
||||||
command: () => props.editor?.chain().focus().toggleStrike().run(),
|
|
||||||
icon: StrikethroughIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "code",
|
|
||||||
isActive: () => props.editor?.isActive("code"),
|
|
||||||
command: () => props.editor?.chain().focus().toggleCode().run(),
|
|
||||||
icon: CodeIcon,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleAccessChange = (accessKey: string) => {
|
|
||||||
props.onAccessChange(accessKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
|
||||||
>
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex border border-custom-border-300 divide-x divide-custom-border-300 rounded overflow-hidden">
|
|
||||||
{props?.commentAccess?.map((access) => (
|
|
||||||
<Tooltip key={access.key} tooltipContent={access.label}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleAccessChange(access.key)}
|
|
||||||
className={`grid place-items-center p-1 hover:bg-custom-background-80 ${props.accessValue === access.key ? "bg-custom-background-80" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
iconName={access.icon}
|
|
||||||
className={`w-4 h-4 -mt-1 ${props.accessValue === access.key
|
|
||||||
? "!text-custom-text-100"
|
|
||||||
: "!text-custom-text-400"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
type="button"
|
|
||||||
onClick={item.command}
|
|
||||||
className={cn(
|
|
||||||
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
|
|
||||||
{
|
|
||||||
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<item.icon
|
|
||||||
className={cn("h-4 w-4", {
|
|
||||||
"text-custom-text-100": item.isActive(),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
81
packages/editor/core/src/ui/menus/menu-items/index.tsx
Normal file
81
packages/editor/core/src/ui/menus/menu-items/index.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { BoldIcon, QuoteIcon, ImageIcon, TableIcon, ListIcon, ListOrderedIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react";
|
||||||
|
import { Editor } from "@tiptap/react";
|
||||||
|
import { UploadImage } from "../../../types/upload-image";
|
||||||
|
import { insertImage, insertTable, toggleBlockquote, toggleBold, toggleBulletList, toggleCode, toggleItalic, toggleOrderedList, toggleStrike } from "../../../lib/editor-commands";
|
||||||
|
|
||||||
|
export interface EditorMenuItem {
|
||||||
|
name: string;
|
||||||
|
isActive: () => boolean;
|
||||||
|
command: () => void;
|
||||||
|
icon: typeof BoldIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BoldItem = (editor: Editor): EditorMenuItem => ({
|
||||||
|
name: "bold",
|
||||||
|
isActive: () => editor?.isActive("bold"),
|
||||||
|
command: () => toggleBold(editor),
|
||||||
|
icon: BoldIcon,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ItalicItem = (editor: Editor): EditorMenuItem => ({
|
||||||
|
name: "italic",
|
||||||
|
isActive: () => editor?.isActive("italic"),
|
||||||
|
command: () => toggleItalic(editor),
|
||||||
|
icon: ItalicIcon,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const UnderLineItem = (editor: Editor): EditorMenuItem => ({
|
||||||
|
name: "underline",
|
||||||
|
isActive: () => editor?.isActive("underline"),
|
||||||
|
command: () => UnderLineItem(editor),
|
||||||
|
icon: UnderlineIcon,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({
|
||||||
|
name: "strike",
|
||||||
|
isActive: () => editor?.isActive("strike"),
|
||||||
|
command: () => toggleStrike(editor),
|
||||||
|
icon: StrikethroughIcon,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CodeItem = (editor: Editor): EditorMenuItem => ({
|
||||||
|
name: "code",
|
||||||
|
isActive: () => editor?.isActive("code"),
|
||||||
|
command: () => toggleCode(editor),
|
||||||
|
icon: CodeIcon,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const BulletListItem = (editor: Editor): EditorMenuItem => ({
|
||||||
|
name: "bullet-list",
|
||||||
|
isActive: () => editor?.isActive("bulletList"),
|
||||||
|
command: () => toggleBulletList(editor),
|
||||||
|
icon: ListIcon,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const NumberedListItem = (editor: Editor): EditorMenuItem => ({
|
||||||
|
name: "ordered-list",
|
||||||
|
isActive: () => editor?.isActive("orderedList"),
|
||||||
|
command: () => toggleOrderedList(editor),
|
||||||
|
icon: ListOrderedIcon
|
||||||
|
})
|
||||||
|
|
||||||
|
export const QuoteItem = (editor: Editor): EditorMenuItem => ({
|
||||||
|
name: "quote",
|
||||||
|
isActive: () => editor?.isActive("quote"),
|
||||||
|
command: () => toggleBlockquote(editor),
|
||||||
|
icon: QuoteIcon
|
||||||
|
})
|
||||||
|
|
||||||
|
export const TableItem = (editor: Editor): EditorMenuItem => ({
|
||||||
|
name: "quote",
|
||||||
|
isActive: () => editor?.isActive("table"),
|
||||||
|
command: () => insertTable(editor),
|
||||||
|
icon: TableIcon
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ImageItem = (editor: Editor, uploadFile: UploadImage, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorMenuItem => ({
|
||||||
|
name: "image",
|
||||||
|
isActive: () => editor?.isActive("image"),
|
||||||
|
command: () => insertImage(editor, uploadFile, setIsSubmitting),
|
||||||
|
icon: ImageIcon,
|
||||||
|
})
|
@ -83,7 +83,7 @@ const LiteTextEditor = ({
|
|||||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||||
{(editable !== false) &&
|
{(editable !== false) &&
|
||||||
(<div className="w-full mt-4">
|
(<div className="w-full mt-4">
|
||||||
<FixedMenu editor={editor} commentAccessSpecifier={commentAccessSpecifier} />
|
<FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} commentAccessSpecifier={commentAccessSpecifier} />
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Editor } from "@tiptap/react";
|
import { Editor } from "@tiptap/react";
|
||||||
import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react";
|
import { BoldIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@plane/editor-core";
|
import { BoldItem, BulletListItem, cn, CodeItem, ImageItem, ItalicItem, NumberedListItem, QuoteItem, StrikeThroughItem, TableItem, UnderLineItem } from "@plane/editor-core";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { Tooltip } from "../../tooltip";
|
import { Tooltip } from "../../tooltip";
|
||||||
|
import { UploadImage } from "../..";
|
||||||
|
|
||||||
export interface BubbleMenuItem {
|
export interface BubbleMenuItem {
|
||||||
name: string;
|
name: string;
|
||||||
@ -24,40 +25,31 @@ type EditorBubbleMenuProps = {
|
|||||||
label: "Private" | "Public";
|
label: "Private" | "Public";
|
||||||
}[] | undefined;
|
}[] | undefined;
|
||||||
}
|
}
|
||||||
|
uploadFile: UploadImage;
|
||||||
|
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||||
const items: BubbleMenuItem[] = [
|
const basicMarkItems: BubbleMenuItem[] = [
|
||||||
{
|
BoldItem(props.editor),
|
||||||
name: "bold",
|
ItalicItem(props.editor),
|
||||||
isActive: () => props.editor?.isActive("bold"),
|
UnderLineItem(props.editor),
|
||||||
command: () => props.editor?.chain().focus().toggleBold().run(),
|
StrikeThroughItem(props.editor),
|
||||||
icon: BoldIcon,
|
];
|
||||||
},
|
|
||||||
{
|
const listItems: BubbleMenuItem[] = [
|
||||||
name: "italic",
|
BulletListItem(props.editor),
|
||||||
isActive: () => props.editor?.isActive("italic"),
|
NumberedListItem(props.editor),
|
||||||
command: () => props.editor?.chain().focus().toggleItalic().run(),
|
];
|
||||||
icon: ItalicIcon,
|
|
||||||
},
|
const userActionItems: BubbleMenuItem[] = [
|
||||||
{
|
QuoteItem(props.editor),
|
||||||
name: "underline",
|
CodeItem(props.editor),
|
||||||
isActive: () => props.editor?.isActive("underline"),
|
];
|
||||||
command: () => props.editor?.chain().focus().toggleUnderline().run(),
|
|
||||||
icon: UnderlineIcon,
|
const complexItems: BubbleMenuItem[] = [
|
||||||
},
|
TableItem(props.editor),
|
||||||
{
|
ImageItem(props.editor, props.uploadFile, props.setIsSubmitting),
|
||||||
name: "strike",
|
|
||||||
isActive: () => props.editor?.isActive("strike"),
|
|
||||||
command: () => props.editor?.chain().focus().toggleStrike().run(),
|
|
||||||
icon: StrikethroughIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "code",
|
|
||||||
isActive: () => props.editor?.isActive("code"),
|
|
||||||
command: () => props.editor?.chain().focus().toggleCode().run(),
|
|
||||||
icon: CodeIcon,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleAccessChange = (accessKey: string) => {
|
const handleAccessChange = (accessKey: string) => {
|
||||||
@ -69,28 +61,91 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
|||||||
<div
|
<div
|
||||||
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
||||||
>
|
>
|
||||||
<div className="flex">
|
{props.commentAccessSpecifier && (<div className="flex border border-custom-border-300 mt-0 divide-x divide-custom-border-300 rounded overflow-hidden">
|
||||||
{props.commentAccessSpecifier && (<div className="flex border border-custom-border-300 divide-x divide-custom-border-300 rounded overflow-hidden">
|
{props?.commentAccessSpecifier.commentAccess?.map((access) => (
|
||||||
{props?.commentAccessSpecifier.commentAccess?.map((access) => (
|
<Tooltip key={access.key} tooltipContent={access.label}>
|
||||||
<Tooltip key={access.key} tooltipContent={access.label}>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => handleAccessChange(access.key)}
|
||||||
onClick={() => handleAccessChange(access.key)}
|
className={`grid place-basicMarkItems-center p-1 hover:bg-custom-background-80 ${props.commentAccessSpecifier?.accessValue === access.key ? "bg-custom-background-80" : ""
|
||||||
className={`grid place-items-center p-1 hover:bg-custom-background-80 ${props.commentAccessSpecifier?.accessValue === access.key ? "bg-custom-background-80" : ""
|
}`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName={access.icon}
|
||||||
|
className={`w-4 h-4 -mt-1 ${props.commentAccessSpecifier?.accessValue === access.key
|
||||||
|
? "!text-custom-text-100"
|
||||||
|
: "!text-custom-text-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
/>
|
||||||
<Icon
|
</button>
|
||||||
iconName={access.icon}
|
</Tooltip>
|
||||||
className={`w-4 h-4 -mt-1 ${props.commentAccessSpecifier?.accessValue === access.key
|
))}
|
||||||
? "!text-custom-text-100"
|
</div>)}
|
||||||
: "!text-custom-text-400"
|
<div className="flex">
|
||||||
}`}
|
{basicMarkItems.map((item, index) => (
|
||||||
/>
|
<button
|
||||||
</button>
|
key={index}
|
||||||
</Tooltip>
|
type="button"
|
||||||
))}
|
onClick={item.command}
|
||||||
</div>)}
|
className={cn(
|
||||||
{items.map((item, index) => (
|
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
|
||||||
|
{
|
||||||
|
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon
|
||||||
|
className={cn("h-4 w-4", {
|
||||||
|
"text-custom-text-100": item.isActive(),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
{listItems.map((item, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
type="button"
|
||||||
|
onClick={item.command}
|
||||||
|
className={cn(
|
||||||
|
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
|
||||||
|
{
|
||||||
|
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon
|
||||||
|
className={cn("h-4 w-4", {
|
||||||
|
"text-custom-text-100": item.isActive(),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
{userActionItems.map((item, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
type="button"
|
||||||
|
onClick={item.command}
|
||||||
|
className={cn(
|
||||||
|
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
|
||||||
|
{
|
||||||
|
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon
|
||||||
|
className={cn("h-4 w-4", {
|
||||||
|
"text-custom-text-100": item.isActive(),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
{complexItems.map((item, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -309,6 +309,7 @@ const renderItems = () => {
|
|||||||
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
|
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
|
||||||
component = new ReactRenderer(CommandList, {
|
component = new ReactRenderer(CommandList, {
|
||||||
props,
|
props,
|
||||||
|
// @ts-ignore
|
||||||
editor: props.editor,
|
editor: props.editor,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -85,10 +85,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
|||||||
}, [issue, reset]);
|
}, [issue, reset]);
|
||||||
|
|
||||||
const debouncedTitleSave = useDebouncedCallback(async () => {
|
const debouncedTitleSave = useDebouncedCallback(async () => {
|
||||||
setTimeout(async () => {
|
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
||||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
}, 1500);
|
||||||
}, 500);
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
Loading…
Reference in New Issue
Block a user