Skip to content

Commit

Permalink
feat: link embed (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
QingWei-Li authored Jul 4, 2021
1 parent 93130b6 commit 0add791
Show file tree
Hide file tree
Showing 19 changed files with 625 additions and 171 deletions.
72 changes: 72 additions & 0 deletions components/editor/dictionary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import useI18n from 'libs/web/hooks/use-i18n'
import { useMemo } from 'react'

export const useDictionary = () => {
const { t } = useI18n()

const dictionary = useMemo(
() => ({
addColumnAfter: t('Insert column after'),
addColumnBefore: t('Insert column before'),
addRowAfter: t('Insert row after'),
addRowBefore: t('Insert row before'),
alignCenter: t('Align center'),
alignLeft: t('Align left'),
alignRight: t('Align right'),
bulletList: t('Bulleted list'),
checkboxList: t('Todo list'),
codeBlock: t('Code block'),
codeCopied: t('Copied to clipboard'),
codeInline: t('Code'),
createLink: t('Create link'),
createLinkError: t('Sorry, an error occurred creating the link'),
createNewDoc: t('Create a new note'),
deleteColumn: t('Delete column'),
deleteRow: t('Delete row'),
deleteTable: t('Delete table'),
deleteImage: t('Delete image'),
alignImageLeft: t('Float left half width'),
alignImageRight: t('Float right half width'),
alignImageDefault: t('Center large'),
em: t('Italic'),
embedInvalidLink: t('Sorry, that link won’t work for this embed type'),
findOrCreateDoc: t('Find or create a note…'),
h1: t('Big heading'),
h2: t('Medium heading'),
h3: t('Small heading'),
heading: t('Heading'),
hr: t('Divider'),
image: t('Image'),
imageUploadError: t('Sorry, an error occurred uploading the image'),
info: t('Info'),
infoNotice: t('Info notice'),
link: t('Link'),
linkCopied: t('Link copied to clipboard'),
mark: t('Highlight'),
newLineEmpty: t("Type '/' to insert…"),
newLineWithSlash: t('Keep typing to filter…'),
noResults: t('No results'),
openLink: t('Open link'),
orderedList: t('Ordered list'),
pageBreak: t('Page break'),
pasteLink: t('Paste a link…'),
pasteLinkWithTitle: (title: string): string =>
t(`Paste a {{title}} link…`, { title }),
placeholder: t('Placeholder'),
quote: t('Quote'),
removeLink: t('Remove link'),
searchOrPasteLink: t('Search or paste a link…'),
strikethrough: t('Strikethrough'),
strong: t('Bold'),
subheading: t('Subheading'),
table: t('Table'),
tip: t('Tip'),
tipNotice: t('Tip notice'),
warning: t('Warning'),
warningNotice: t('Warning notice'),
}),
[t]
)

return dictionary
}
84 changes: 8 additions & 76 deletions components/editor/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { FC, useEffect, useState } from 'react'
import { use100vh } from 'react-div-100vh'
import MarkdownEditor, { Props } from 'rich-markdown-editor'
import { useEditorTheme } from './theme'
import useMounted from 'libs/web/hooks/use-mounted'
import useI18n from 'libs/web/hooks/use-i18n'
import Tooltip from './tooltip'
import extensions from './extensions'
import EditorState from 'libs/web/state/editor'
import { useToast } from 'libs/web/hooks/use-toast'
import { useDictionary } from './dictionary'
import { useEmbeds } from './embeds'

export type EditorProps = Pick<Props, 'readOnly'>

Expand All @@ -18,88 +19,18 @@ const Editor: FC<EditorProps> = ({ readOnly }) => {
onClickLink,
onUploadImage,
onHoverLink,
onNoteChange,
onEditorChange,
backlinks,
editorEl,
note,
} = EditorState.useContainer()
const height = use100vh()
const mounted = useMounted()
const { t } = useI18n()
const editorTheme = useEditorTheme()
const [hasMinHeight, setHasMinHeight] = useState(true)
const toast = useToast()

const dictionary = useMemo(
() => ({
addColumnAfter: t('Insert column after'),
addColumnBefore: t('Insert column before'),
addRowAfter: t('Insert row after'),
addRowBefore: t('Insert row before'),
alignCenter: t('Align center'),
alignLeft: t('Align left'),
alignRight: t('Align right'),
bulletList: t('Bulleted list'),
checkboxList: t('Todo list'),
codeBlock: t('Code block'),
codeCopied: t('Copied to clipboard'),
codeInline: t('Code'),
createLink: t('Create link'),
createLinkError: t('Sorry, an error occurred creating the link'),
createNewDoc: t('Create a new note'),
deleteColumn: t('Delete column'),
deleteRow: t('Delete row'),
deleteTable: t('Delete table'),
deleteImage: t('Delete image'),
alignImageLeft: t('Float left half width'),
alignImageRight: t('Float right half width'),
alignImageDefault: t('Center large'),
em: t('Italic'),
embedInvalidLink: t('Sorry, that link won’t work for this embed type'),
findOrCreateDoc: t('Find or create a note…'),
h1: t('Big heading'),
h2: t('Medium heading'),
h3: t('Small heading'),
heading: t('Heading'),
hr: t('Divider'),
image: t('Image'),
imageUploadError: t('Sorry, an error occurred uploading the image'),
info: t('Info'),
infoNotice: t('Info notice'),
link: t('Link'),
linkCopied: t('Link copied to clipboard'),
mark: t('Highlight'),
newLineEmpty: t("Type '/' to insert…"),
newLineWithSlash: t('Keep typing to filter…'),
noResults: t('No results'),
openLink: t('Open link'),
orderedList: t('Ordered list'),
pageBreak: t('Page break'),
pasteLink: t('Paste a link…'),
pasteLinkWithTitle: (title: string): string =>
t(`Paste a {{title}} link…`, { title }),
placeholder: t('Placeholder'),
quote: t('Quote'),
removeLink: t('Remove link'),
searchOrPasteLink: t('Search or paste a link…'),
strikethrough: t('Strikethrough'),
strong: t('Bold'),
subheading: t('Subheading'),
table: t('Table'),
tip: t('Tip'),
tipNotice: t('Tip notice'),
warning: t('Warning'),
warningNotice: t('Warning notice'),
}),
[t]
)

const onEditorChange = useCallback(
(value: () => string): void => {
onNoteChange.callback({ content: value() })
},
[onNoteChange]
)
const dictionary = useDictionary()
const embeds = useEmbeds()

useEffect(() => {
setHasMinHeight((backlinks?.length ?? 0) <= 0)
Expand All @@ -124,6 +55,7 @@ const Editor: FC<EditorProps> = ({ readOnly }) => {
tooltip={Tooltip}
extensions={extensions}
className="px-4 md:px-0"
embeds={embeds}
/>
<style jsx global>{`
.ProseMirror ul {
Expand All @@ -150,7 +82,7 @@ const Editor: FC<EditorProps> = ({ readOnly }) => {
.ProseMirror h3 {
font-size: 1.5em;
}
.ProseMirror a {
.ProseMirror a:not(.bookmark) {
text-decoration: underline;
}
`}</style>
Expand Down
57 changes: 57 additions & 0 deletions components/editor/embeds/bookmark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Skeleton } from '@material-ui/lab'
import useFetcher from 'libs/web/api/fetcher'
import { decode } from 'qss'
import { FC, useEffect, useState } from 'react'
import { Metadata } from 'unfurl.js/dist/types'
import { EmbedProps } from '.'

export const Bookmark: FC<EmbedProps> = ({ attrs: { href } }) => {
const { request } = useFetcher()
const [data, setData] = useState<Metadata>()

useEffect(() => {
request<undefined, Metadata>({
url: href,
method: 'GET',
}).then((data) => {
setData(data)
})
}, [href, request])

const image = data?.open_graph?.images?.[0].url ?? data?.favicon
const title = data?.open_graph?.title ?? data?.title
const description = data?.open_graph?.description ?? data?.description
const url =
data?.open_graph?.url ??
decode<{ url: string }>(href.replace(/.*\?/, '')).url

if (!data) {
return <Skeleton variant="rect" height={128}></Skeleton>
}

return (
<a
className="bookmark overflow-hidden border-gray-200 border rounded flex h-32 !no-underline hover:bg-blue-50"
href={url}
target="_blank"
rel="noreferrer"
>
<div className="flex-1 p-2 overflow-hidden">
<div className="mb-2 block text-gray-800 overflow-ellipsis overflow-hidden h-6">
{title}
</div>
<div className="text-sm overflow-ellipsis overflow-hidden h-10 text-gray-400 mb-2">
{description}
</div>
<div className="text-sm overflow-ellipsis overflow-hidden h-5">
{url}
</div>
</div>
{!!image && (
<div className="md:w-48 flex w-0">
<img className="m-auto object-cover h-full" src={image} alt={title} />
</div>
)}
</a>
)
}
37 changes: 37 additions & 0 deletions components/editor/embeds/embed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Skeleton } from '@material-ui/lab'
import useFetcher from 'libs/web/api/fetcher'
import { FC, useEffect, useState } from 'react'
import { Metadata } from 'unfurl.js/dist/types'
import { EmbedProps } from '.'
import InnerHTML from 'dangerously-set-html-content'
import { decode } from 'qss'

export const Embed: FC<EmbedProps> = ({ attrs: { href } }) => {
const { request } = useFetcher()
const [data, setData] = useState<Metadata>()

useEffect(() => {
request<undefined, Metadata>({
url: href,
method: 'GET',
}).then((data) => {
setData(data)
})
}, [href, request])

if (!data) {
return <Skeleton variant="rect" height={128}></Skeleton>
}

const html = (data?.oEmbed as any)?.html

if (html) {
return <InnerHTML html={html} />
}

const url =
data?.open_graph?.url ??
decode<{ url: string }>(href.replace(/.*\?/, '')).url

return <iframe className="w-full h-96" src={url} allowFullScreen />
}
42 changes: 42 additions & 0 deletions components/editor/embeds/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import CsrfTokenState from 'libs/web/state/csrf-token'
import { useCallback } from 'react'
import { Props } from 'rich-markdown-editor'
import { Bookmark } from './bookmark'
import { Embed } from './embed'

export type EmbedProps = {
attrs: {
href: string
matches: string[]
}
}

export const useEmbeds = () => {
const csrfToken = CsrfTokenState.useContainer()

const createEmbedComponent = useCallback(
(Component) => {
return (props: EmbedProps) => {
return (
<CsrfTokenState.Provider initialState={csrfToken}>
<Component {...props} />
</CsrfTokenState.Provider>
)
}
},
[csrfToken]
)

return [
{
title: 'Bookmark',
matcher: (url) => url.match(/^\/api\/extract\?type=bookmark/),
component: createEmbedComponent(Bookmark),
},
{
title: 'Embed',
matcher: (url) => url.match(/^\/api\/extract\?type=embed/),
component: createEmbedComponent(Embed),
},
] as Props['embeds']
}
6 changes: 6 additions & 0 deletions components/icon-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
LinkIcon,
ArrowSmLeftIcon,
ArrowSmRightIcon,
ExternalLinkIcon,
BookmarkAltIcon,
PuzzleIcon,
} from '@heroicons/react/outline'

export const ICONS = {
Expand All @@ -32,6 +35,9 @@ export const ICONS = {
Link: LinkIcon,
ArrowSmLeft: ArrowSmLeftIcon,
ArrowSmRight: ArrowSmRightIcon,
ExternalLink: ExternalLinkIcon,
BookmarkAlt: BookmarkAltIcon,
Puzzle: PuzzleIcon,
}

const IconButton = forwardRef<
Expand Down
2 changes: 2 additions & 0 deletions components/layout/layout-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { SwipeableDrawer } from '@material-ui/core'
import SidebarMenu from 'components/portal/sidebar-menu/sidebar-menu'
import { NoteModel } from 'libs/shared/note'
import PreviewModal from 'components/portal/preview-modal'
import LinkToolbar from 'components/portal/link-toolbar/link-toolbar'

const MainWrapper: FC = ({ children }) => {
const {
Expand Down Expand Up @@ -103,6 +104,7 @@ const LayoutMain: FC<{
</SearchState.Provider>
<ShareModal />
<PreviewModal />
<LinkToolbar />
<SidebarMenu />
</NoteState.Provider>
</NoteTreeState.Provider>
Expand Down
Loading

0 comments on commit 0add791

Please sign in to comment.