import { zodResolver } from "@hookform/resolvers/zod"; import { ArrowClockwise, ArrowCounterClockwise, Code, CodeBlock, HighlighterCircle, Image as ImageIcon, KeyReturn, LinkSimple, ListBullets, ListNumbers, Minus, Paragraph, TextAlignCenter, TextAlignJustify, TextAlignLeft, TextAlignRight, TextAUnderline, TextB, TextHOne, TextHThree, TextHTwo, TextIndent, TextItalic, TextOutdent, TextStrikethrough, } from "@phosphor-icons/react"; import { PopoverTrigger } from "@radix-ui/react-popover"; import { cn } from "@reactive-resume/utils"; import { Highlight } from "@tiptap/extension-highlight"; import { Image } from "@tiptap/extension-image"; import { Link } from "@tiptap/extension-link"; import { TextAlign } from "@tiptap/extension-text-align"; import { Underline } from "@tiptap/extension-underline"; import { Editor, EditorContent, EditorContentProps, useEditor } from "@tiptap/react"; import { StarterKit } from "@tiptap/starter-kit"; import { forwardRef, useCallback } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { Button } from "./button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "./form"; import { Input } from "./input"; import { Popover, PopoverContent } from "./popover"; import { Skeleton } from "./skeleton"; import { Toggle } from "./toggle"; import { Tooltip } from "./tooltip"; const InsertImageFormSchema = z.object({ src: z.string(), alt: z.string().optional(), }); type InsertImageFormValues = z.infer; type InsertImageProps = { onInsert: (value: InsertImageFormValues) => void; }; const InsertImageForm = ({ onInsert }: InsertImageProps) => { const form = useForm({ resolver: zodResolver(InsertImageFormSchema), defaultValues: { src: "", alt: "" }, }); const onSubmit = (values: InsertImageFormValues) => { onInsert(values); form.reset(); }; return (

Insert an image from an external URL and use it on your resume.

( URL )} /> ( Description )} />
); }; const Toolbar = ({ editor }: { editor: Editor }) => { const setLink = useCallback(() => { const previousUrl = editor.getAttributes("link").href; const url = window.prompt("URL", previousUrl); // cancelled if (url === null) { return; } // empty if (url === "") { editor.chain().focus().extendMarkRange("link").unsetLink().run(); return; } // update link editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run(); }, [editor]); return (
editor.chain().focus().toggleBold().run()} > editor.chain().focus().toggleItalic().run()} > editor.chain().focus().toggleStrike().run()} > editor.chain().focus().toggleUnderline().run()} > editor.chain().focus().toggleHighlight().run()} > editor.chain().focus().toggleCode().run()} > editor.chain().focus().toggleCodeBlock().run()} > editor.chain().focus().toggleHeading({ level: 1 }).run()} > editor.chain().focus().toggleHeading({ level: 2 }).run()} > editor.chain().focus().toggleHeading({ level: 3 }).run()} > editor.chain().focus().setParagraph().run()} > editor.chain().focus().setTextAlign("left").run()} > editor.chain().focus().setTextAlign("center").run()} > editor.chain().focus().setTextAlign("right").run()} > editor.chain().focus().setTextAlign("justify").run()} > editor.chain().focus().toggleBulletList().run()} > editor.chain().focus().toggleOrderedList().run()} > editor.chain().focus().setImage(props).run()} />
); }; interface RichInputProps extends Omit< EditorContentProps, "ref" | "editor" | "content" | "value" | "onChange" | "className" > { content?: string; onChange?: (value: string) => void; hideToolbar?: boolean; className?: string; editorClassName?: string; footer?: (editor: Editor) => React.ReactNode; } export const RichInput = forwardRef( ( { content, onChange, footer, hideToolbar = false, className, editorClassName, ...props }, _ref, ) => { const editor = useEditor({ extensions: [ StarterKit, Image, Underline, Highlight, Link.extend({ inclusive: false }).configure({ openOnClick: false }), TextAlign.configure({ types: ["heading", "paragraph"] }), ], editorProps: { attributes: { class: cn( "prose prose-sm prose-zinc max-h-[200px] max-w-none overflow-y-scroll dark:prose-invert focus:outline-none [&_*]:my-2", editorClassName, ), }, }, content, parseOptions: { preserveWhitespace: "full" }, onUpdate: ({ editor }) => onChange?.(editor.getHTML()), }); if (!editor) { return (
); } return (
{!hideToolbar && } {footer?.(editor)}
); }, ); RichInput.displayName = "RichInput";