Skip to content

Commit

Permalink
Added lots of settings and markdown editor on react example
Browse files Browse the repository at this point in the history
  • Loading branch information
cesardeazevedo committed Jul 29, 2024
1 parent a975f11 commit 153dd41
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 74 deletions.
193 changes: 130 additions & 63 deletions examples/react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import ImageExtension from '@tiptap/extension-image'
import YoutubeExtension from '@tiptap/extension-youtube'
import { Markdown as MarkdownExtension } from 'tiptap-markdown'
import type { AnyExtension } from '@tiptap/react'
import { EditorContent, ReactNodeViewRenderer, useEditor } from '@tiptap/react'
import { StarterKit } from '@tiptap/starter-kit'
import { NostrMatcherExtension } from 'nostr-editor'
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import ReactJsonView from 'react-json-view'
import { Image } from './components/Image'
import { Mention } from './components/Mention'
Expand All @@ -18,82 +20,147 @@ import { NProfileExtension } from './extensions/NProfileExtension'
import { TagExtension } from './extensions/TagExtension'
import { TweetExtension } from './extensions/TweetExtension'
import { VideoExtension } from './extensions/VideoExtension'

const extensions = [
StarterKit,
NostrMatcherExtension,
LinkExtension,
TagExtension,
VideoExtension.extend({ addNodeView: () => ReactNodeViewRenderer(Video) }),
ImageExtension.extend({
addNodeView: () => ReactNodeViewRenderer(Image),
renderText: (p) => p.node.attrs.src,
}),
YoutubeExtension.extend({ renderText: (p) => p.node.attrs.src }),
TweetExtension.extend({ addNodeView: () => ReactNodeViewRenderer(Tweet) }),
NProfileExtension.extend({ addNodeView: () => ReactNodeViewRenderer(Mention) }),
NEventExtension.extend({ addNodeView: () => ReactNodeViewRenderer(NEvent) }),
NAddrExtension.extend({ addNodeView: () => ReactNodeViewRenderer(NAddr) }),
]
import { TestText } from './TestText'
import { Sidebar } from './Sidebar'
import type { EditorType, EditorExtensionSettings } from './types'

function App() {
const editor = useEditor({
extensions: extensions,
onUpdate: () => {
handleSnapshot()
},
const [raw, setRaw] = useState('')
const [type, setType] = useState<EditorType>('text')
const [snapshot, setSnapshot] = useState({})
const [settings, setSettings] = useState<EditorExtensionSettings>({
nevent1: true,
nprofile1: true,
naddr1: true,
links: true,
images: true,
tags: true,
videos: true,
youtube: true,
tweet: true,
})
const extensions = useMemo(() => {
const baseExtensions: AnyExtension[] = []

const [raw, setRaw] = useState('raw_')
const [snapshot, setSnapshot] = useState({})
if (type === 'text') {
// Disabled markdown elements
baseExtensions.push(
StarterKit.configure({
heading: false,
bold: false,
italic: false,
strike: false,
listItem: false,
bulletList: false,
orderedList: false,
code: false,
codeBlock: false,
blockquote: false,
}),
)
} else {
// Markdown
// StarterKit already bundles markdown elements
baseExtensions.push(MarkdownExtension)
baseExtensions.push(StarterKit)
}

baseExtensions.push(NostrMatcherExtension)

if (settings.links) baseExtensions.push(LinkExtension)
if (settings.tags) baseExtensions.push(TagExtension)
if (settings.videos) baseExtensions.push(VideoExtension.extend({ addNodeView: () => ReactNodeViewRenderer(Video) }))
if (settings.images)
baseExtensions.push(
ImageExtension.extend({
addNodeView: () => ReactNodeViewRenderer(Image),
renderText: (p) => p.node.attrs.src,
}),
)
if (settings.youtube) baseExtensions.push(YoutubeExtension.extend({ renderText: (p) => p.node.attrs.src }))
if (settings.tweet) baseExtensions.push(TweetExtension.extend({ addNodeView: () => ReactNodeViewRenderer(Tweet) }))
if (settings.nprofile1)
baseExtensions.push(NProfileExtension.extend({ addNodeView: () => ReactNodeViewRenderer(Mention) }))
if (settings.nevent1)
baseExtensions.push(NEventExtension.extend({ addNodeView: () => ReactNodeViewRenderer(NEvent) }))
if (settings.naddr1) baseExtensions.push(NAddrExtension.extend({ addNodeView: () => ReactNodeViewRenderer(NAddr) }))

return baseExtensions
}, [type, settings])

const prevContent = useRef('')

const editor = useEditor(
{
extensions,
onUpdate: () => {
handleSnapshot()
},
},
[extensions],
)
useEffect(() => {
// Preserve the content when settings change
editor?.commands.setContent(prevContent.current, true)
}, [editor, prevContent])

const handleSnapshot = useCallback(() => {
if (editor) {
setRaw(editor.getText())
prevContent.current = editor.getText()
if (type === 'text') {
setRaw(editor.getText())
} else {
setRaw(editor.storage.markdown.getMarkdown())
}
// ReactJsonView is buggy so we need to stringify and parse
setSnapshot(JSON.parse(JSON.stringify(editor?.state.toJSON() || {})).doc.content)
}
}, [editor])
}, [type, editor])

const handleChangeEditor = useCallback(
(type: EditorType) => {
setType(type)
},
[type],
)

const handleChangeExtensions = useCallback(
(name: string, value: boolean) => {
setSettings({
...settings,
[name]: value,
})
},
[settings],
)

return (
<div className='w-3/4 relative'>
<h1 className='text-xl'>nostr-editor</h1>
<br />
<h6>testing text</h6>
<span id='raw' className='text-xs break-all text-wrap z-20 relative'>
Hello
nostr:nprofile1qy88wumn8ghj7mn0wvhxcmmv9uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcprfmhxue69uhhyetvv9ujuem9w3skccne9e3k7mf0wccsqgxxvqas78x0a339m8qgkaf7fam5atmarne8dy3rzfd4l4x6w2qpncmfs8zh
and
nostr:nprofile1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hsz9thwden5te0wfjkccte9ejxzmt4wvhxjme0qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gfnma0u
and
nostr:nprofile1qyfhwumn8ghj7ur4wfcxcetsv9njuetn9uqsuamnwvaz7tmwdaejumr0dshsz9mhwden5te0wfjkccte9ec8y6tdv9kzumn9wshsqgyzxs0cs2mw40xjhfl3a7g24ktpeur54u2mnm6y5z0e6250h7lx5gflu83m
<br />
nostr:nevent1qvzqqqqqqypzplnld0r0wvutw6alsrd5q2k7vk2nug9j7glxd6ycyp9k8nzz2wdrqyg8wumn8ghj7mn0wd68ytnhd9hx2qg5waehxw309aex2mrp0yhxgctdw4eju6t0qyxhwumn8ghj7mn0wvhxcmmvqqs9gg4thq8ng87z8377jxksjwhk9dl0f8su9c4kq335ydzp0ykmv5gqt3csa
<div className='flex'>
<main className='relative width-auto p-10' style={{ width: 'calc(100% - 400px)' }}>
<h1>nostr-editor</h1>
<br />
image: https://image.nostr.build/87dbc55a6391d15bddda206561d53867a5679dd95e84fe8ed62bfe2e3adcadf3.jpg
</span>
<div className='mt-4 z-20 relative'>
<EditorContent editor={editor} id='editor' />
<small>
Don't forget the{' '}
<code>
<b>nostr:</b>
</code>{' '}
prefix
</small>
</div>
{raw && (
<>
<h1>raw text</h1>
{raw}
</>
)}
{snapshot && (
<div className='text-left pl-5 mt-5'>
<h1 className='mb-2'>Schema</h1>
<ReactJsonView src={snapshot} />
<TestText />
<div className='mt-2 z-20 relative'>
<EditorContent editor={editor} id='editor' />
<small>
Don't forget the <code>nostr:</code>
prefix
</small>
</div>
)}
{raw && (
<>
<h3>raw text</h3>
<pre className='break-all text-wrap'>{raw}</pre>
</>
)}
{snapshot && (
<div className='text-left pl-5 mt-5'>
<h3 className='mb-2'>Schema</h3>
<ReactJsonView src={snapshot} />
</div>
)}
</main>
<Sidebar type={type} onChangeEditor={handleChangeEditor} onChangeExtensions={handleChangeExtensions} />
</div>
)
}
Expand Down
59 changes: 59 additions & 0 deletions examples/react/src/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
IconAt,
IconBrandX,
IconBrandYoutube,
IconExternalLink,
IconHash,
IconLink,
IconMovie,
IconPhoto,
IconQuote,
} from '@tabler/icons-react'

import { Switch } from './components/settings/Switch'
import type { EditorType } from './types'

type Props = {
type: EditorType
onChangeEditor: (type: 'text' | 'markdown') => void
onChangeExtensions: (name: string, value: boolean) => void
}

export function Sidebar(props: Props) {
const handleChange = (name: string) => (value: boolean) => {
props.onChangeExtensions(name, value)
}

const activeColor = (value: EditorType) => (props.type === value ? 'bg-blue-500 text-white' : 'bg-gray-200')

return (
<nav className='fixed h-full right-0 w-5 bg-gray-100 px-8 py-14' style={{ width: 400 }}>
<h3>Settings</h3>
<div className='my-4'>
<button
className={`rounded-full px-3 mr-1 ${activeColor('text')}`}
onClick={() => props.onChangeEditor('text')}>
Text
</button>
<button
className={`rounded-full px-3 mr-1 ${activeColor('markdown')}`}
onClick={() => props.onChangeEditor('markdown')}>
Markdown
</button>
<button className='rounded-full px-3 mr-1 bg-gray-100'>Asciidoc (soon)</button>
</div>
<h3>Extensions</h3>
<div className='mt-4'>
<Switch icon={IconQuote} label='nevent1' onChange={handleChange('nevent1')} />
<Switch icon={IconAt} label='nprofile1' onChange={handleChange('nprofile1')} />
<Switch icon={IconExternalLink} label='naddr1' onChange={handleChange('naddr1')} />
<Switch icon={IconLink} label='Links' onChange={handleChange('links')} />
<Switch icon={IconPhoto} label='Images' onChange={handleChange('images')} />
<Switch icon={IconHash} label='Tags' onChange={handleChange('tags')} />
<Switch icon={IconMovie} label='Videos' onChange={handleChange('videos')} />
<Switch icon={IconBrandYoutube} label='Youtube' onChange={handleChange('youtube')} />
<Switch icon={IconBrandX} label='Tweets' onChange={handleChange('tweet')} />
</div>
</nav>
)
}
17 changes: 17 additions & 0 deletions examples/react/src/TestText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const TestText = () => (
<>
<h6>testing text</h6>
<span id='raw' className='text-xs break-all text-wrap z-20 relative'>
Hello
nostr:nprofile1qy88wumn8ghj7mn0wvhxcmmv9uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcprfmhxue69uhhyetvv9ujuem9w3skccne9e3k7mf0wccsqgxxvqas78x0a339m8qgkaf7fam5atmarne8dy3rzfd4l4x6w2qpncmfs8zh
and
nostr:nprofile1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hsz9thwden5te0wfjkccte9ejxzmt4wvhxjme0qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gfnma0u
and
nostr:nprofile1qyfhwumn8ghj7ur4wfcxcetsv9njuetn9uqsuamnwvaz7tmwdaejumr0dshsz9mhwden5te0wfjkccte9ec8y6tdv9kzumn9wshsqgyzxs0cs2mw40xjhfl3a7g24ktpeur54u2mnm6y5z0e6250h7lx5gflu83m
<br />
nostr:nevent1qvzqqqqqqypzplnld0r0wvutw6alsrd5q2k7vk2nug9j7glxd6ycyp9k8nzz2wdrqyg8wumn8ghj7mn0wd68ytnhd9hx2qg5waehxw309aex2mrp0yhxgctdw4eju6t0qyxhwumn8ghj7mn0wvhxcmmvqqs9gg4thq8ng87z8377jxksjwhk9dl0f8su9c4kq335ydzp0ykmv5gqt3csa
<br />
image: https://image.nostr.build/87dbc55a6391d15bddda206561d53867a5679dd95e84fe8ed62bfe2e3adcadf3.jpg
</span>
</>
)
27 changes: 27 additions & 0 deletions examples/react/src/components/settings/Switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Icon, IconProps } from '@tabler/icons-react'

type Props = {
label: string
defaultChecked?: boolean
icon?: React.ForwardRefExoticComponent<IconProps & React.RefAttributes<Icon>>
onChange: (value: boolean) => void
}

export function Switch(props: Props) {
const { defaultChecked = true } = props
const Icon = props.icon
return (
<label className='w-full mb-4 flex items-center cursor-pointer justify-between'>
<span className='text-sm font-medium text-gray-900 flex flex-row align-center justify-center'>
{Icon && <Icon strokeWidth='1.5' size={28} />} <span className='ml-4 leading-6'>{props.label}</span>
</span>
<input
type='checkbox'
defaultChecked={defaultChecked}
onChange={(event) => props.onChange(event.target.checked)}
className='sr-only peer'
/>
<div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none dark:peer-focus:ring-blue-600 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
)
}
Loading

0 comments on commit 153dd41

Please sign in to comment.