'use client' import { ComponentProps, useCallback, useEffect } from 'react' import { useEditor, EditorContent } from '@tiptap/react' import { getCookie } from 'cookies-next' import { useTranslations } from 'next-intl' import StarterKit from '@tiptap/starter-kit' import Link from '@tiptap/extension-link' import Highlight from '@tiptap/extension-highlight' import Placeholder from '@tiptap/extension-placeholder' import Typography from '@tiptap/extension-typography' import Youtube from '@tiptap/extension-youtube' import CustomMention from '~extensions/CustomMention' import classNames from 'classnames' import { mentionSuggestionOptions } from '~utils/mentionSuggestions' import type { JSONContent } from '@tiptap/core' import ToolbarButton from '~components/common/ToolbarButton' import BoldIcon from 'remixicon-react/BoldIcon' import ItalicIcon from 'remixicon-react/ItalicIcon' import StrikethroughIcon from 'remixicon-react/StrikethroughIcon' import UnorderedListIcon from 'remixicon-react/ListUnorderedIcon' import OrderedListIcon from '~public/icons/remix/list-ordered-2.svg' import PaintbrushIcon from 'remixicon-react/PaintBrushLineIcon' import H1Icon from 'remixicon-react/H1Icon' import H2Icon from 'remixicon-react/H2Icon' import H3Icon from 'remixicon-react/H3Icon' import LinkIcon from 'remixicon-react/LinkIcon' import YoutubeIcon from 'remixicon-react/YoutubeLineIcon' import styles from './index.module.scss' interface Props extends ComponentProps<'div'> { bound: boolean editable?: boolean content?: string onUpdate?: (content: JSONContent) => void } const Editor = ({ bound, className, content, editable, onUpdate, ...props }: Props) => { // Hooks: Locale const locale = (getCookie('NEXT_LOCALE') as string) || 'en' const t = useTranslations('common') useEffect(() => { editor?.commands.setContent(formatContent(content)) }, [content]) // Setup: Editor const editor = useEditor({ content: formatContent(content), editable: editable, editorProps: { attributes: { class: classNames( { [styles.editor]: true, [styles.bound]: bound, }, className?.split(' ').map((c) => styles[c]) ), }, }, extensions: [ StarterKit.configure({ heading: { levels: [1, 2, 3], }, }), Link, Highlight, Placeholder.configure({ emptyEditorClass: styles.empty, placeholder: t('modals.edit_team.placeholders.description'), }), Typography, CustomMention.configure({ renderLabel({ options, node }) { return `${node.attrs.id.name[locale] ?? node.attrs.id.granblue_en}` }, suggestion: mentionSuggestionOptions, HTMLAttributes: { class: classNames({ [styles.mention]: true, }), }, }), Youtube.configure({ inline: false, modestBranding: true, interfaceLanguage: locale, }), ], onUpdate: ({ editor }) => { const json = editor.getJSON() if (onUpdate) onUpdate(json) }, }) // Methods: Convenience function isJSON(content?: string) { if (!content) return false try { JSON.parse(content) } catch (e) { return false } return true } function formatContent(content?: string) { if (!content) return '' if (isJSON(content)) return JSON.parse(content) else { // Otherwise, create a new
tag after each double newline.
// Add < br /> tags for single newlines.
// Add a < br /> after each paragraph.
const paragraphs = content.split('\n\n')
const formatted = paragraphs
.map((p) => {
const lines = p.split('\n')
return lines.join('
')
})
.join('
')
return formatted
}
}
// Methods: Actions
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])
const addYoutubeVideo = () => {
const url = prompt('Enter YouTube URL')
if (editor && url) {
editor.commands.setYoutubeVideo({
src: url,
width: 320,
height: 180,
})
}
}
// Methods: Rendering
if (!editor) {
return null
}
return (