From b0b7bf5dccb678459b32d8705eaef74ddaee83b9 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 22 Jun 2023 17:40:29 +0800 Subject: [PATCH] Update notebook styling --- .../web/components/patterns/ArticleNotes.tsx | 283 ++++++++++++++++++ .../patterns/MDEditorSavePlugin.tsx | 33 ++ .../components/patterns/RcEditorStyles.tsx | 34 +++ .../components/templates/article/Notebook.tsx | 4 +- 4 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 packages/web/components/patterns/ArticleNotes.tsx create mode 100644 packages/web/components/patterns/MDEditorSavePlugin.tsx create mode 100644 packages/web/components/patterns/RcEditorStyles.tsx diff --git a/packages/web/components/patterns/ArticleNotes.tsx b/packages/web/components/patterns/ArticleNotes.tsx new file mode 100644 index 000000000..b8da7c9ef --- /dev/null +++ b/packages/web/components/patterns/ArticleNotes.tsx @@ -0,0 +1,283 @@ +/* eslint-disable react/no-children-prop */ +import { + ChangeEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { formattedShortTime } from '../../lib/dateFormatting' +import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives' + +import MarkdownIt from 'markdown-it' +import MdEditor, { Plugins } from 'react-markdown-editor-lite' +import 'react-markdown-editor-lite/lib/index.css' +import ReactMarkdown from 'react-markdown' +import throttle from 'lodash/throttle' +import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation' +import { Highlight } from '../../lib/networking/fragments/highlightFragment' +import { Button } from '../elements/Button' +import { + ModalContent, + ModalOverlay, + ModalRoot, +} from '../elements/ModalPrimitives' +import { CloseButton } from '../elements/CloseButton' +import { StyledText } from '../elements/StyledText' +import remarkGfm from 'remark-gfm' +import MDEditorSavePlugin from './MDEditorSavePlugin' +import HandleFullScreen from './MDEditorSavePlugin' +import Counter from './MDEditorSavePlugin' +import { isDarkTheme } from '../../lib/themeUpdater' +import { RcEditorStyles } from './RcEditorStyles' + +const mdParser = new MarkdownIt() + +MdEditor.use(Plugins.TabInsert, { + tabMapValue: 1, // note that 1 means a '\t' instead of ' '. +}) + +console.log() +MdEditor.use(Counter) + +type NoteSectionProps = { + targetId: string + + placeHolder: string + mode: 'edit' | 'preview' + + sizeMode: 'normal' | 'maximized' + setEditMode: (set: 'edit' | 'preview') => void + + text: string | undefined + saveText: (text: string, completed: (success: boolean) => void) => void +} + +export function ArticleNotes(props: NoteSectionProps): JSX.Element { + const [lastSaved, setLastSaved] = useState(undefined) + + const saveText = useCallback( + (text, updateTime) => { + props.saveText(text, (success) => { + if (success) { + setLastSaved(updateTime) + } + }) + }, + [props] + ) + + return ( + + ) +} + +type HighlightViewNoteProps = { + targetId: string + + placeHolder: string + mode: 'edit' | 'preview' + + highlight: Highlight + + sizeMode: 'normal' | 'maximized' + setEditMode: (set: 'edit' | 'preview') => void + + text: string | undefined + updateHighlight: (highlight: Highlight) => void +} + +export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element { + const [lastSaved, setLastSaved] = useState(undefined) + + const saveText = useCallback( + (text, updateTime) => { + ;(async () => { + const success = await updateHighlightMutation({ + annotation: text, + highlightId: props.highlight?.id, + }) + if (success) { + setLastSaved(updateTime) + props.highlight.annotation = text + props.updateHighlight(props.highlight) + } + })() + }, + [props] + ) + + return ( + + ) +} + +type MarkdownNote = { + targetId: string + + placeHolder: string + + sizeMode: 'normal' | 'maximized' + + text: string | undefined + fillBackground: boolean | undefined + + lastSaved: Date | undefined + saveText: (text: string, updateTime: Date) => void +} + +export function MarkdownNote(props: MarkdownNote): JSX.Element { + const editorRef = useRef(null) + const [lastChanged, setLastChanged] = useState(undefined) + const [errorSaving, setErrorSaving] = useState(undefined) + const isDark = isDarkTheme() + + const saveRef = useRef(props.saveText) + + useEffect(() => { + saveRef.current = props.saveText + }, [props.lastSaved, lastChanged]) + + const debouncedSave = useMemo< + (text: string, updateTime: Date) => void + >(() => { + const func = (text: string, updateTime: Date) => { + saveRef.current?.(text, updateTime) + } + return throttle(func, 3000) + }, []) + + const handleEditorChange = useCallback( + ( + data: { text: string; html: string }, + event?: ChangeEvent | undefined + ) => { + if (event) { + event.preventDefault() + } + + const updateTime = new Date() + setLastChanged(updateTime) + localStorage.setItem(`note-${props.targetId}`, JSON.stringify(data)) + debouncedSave(data.text, updateTime) + }, + [props.lastSaved, lastChanged] + ) + + useEffect(() => { + const saveMarkdownNote = () => { + const md = editorRef.current?.getMdValue() + if (md) { + props.saveText(md, new Date()) + } + } + document.addEventListener('saveMarkdownNote', saveMarkdownNote) + return () => { + document.removeEventListener('saveMarkdownNote', saveMarkdownNote) + } + }, [props, editorRef]) + + console.log('isDark: ', isDark) + + return ( + ) => { + if (event.code.toLowerCase() === 'escape') { + event.preventDefault() + event.stopPropagation() + } + }} + > + mdParser.render(text)} + onChange={handleEditorChange} + /> + + {errorSaving && ( + + {errorSaving} + + )} + {props.lastSaved !== undefined ? ( + <> + {lastChanged === props.lastSaved + ? 'Saved' + : `Last saved ${formattedShortTime( + props.lastSaved.toISOString() + )}`} + + ) : null} + + + ) +} diff --git a/packages/web/components/patterns/MDEditorSavePlugin.tsx b/packages/web/components/patterns/MDEditorSavePlugin.tsx new file mode 100644 index 000000000..7d9605542 --- /dev/null +++ b/packages/web/components/patterns/MDEditorSavePlugin.tsx @@ -0,0 +1,33 @@ +import { Disc, FloppyDisk } from 'phosphor-react' +import { PluginComponent, PluginProps } from 'react-markdown-editor-lite' +import { SpanBox } from '../elements/LayoutPrimitives' +import { Button } from '../elements/Button' + +export default class MDEditorSavePlugin extends PluginComponent { + static pluginName = 'save' + + static align = 'right' + + constructor(props: any) { + super(props) + + this.handleClick = this.handleClick.bind(this) + } + + private handleClick() {} + + render() { + return ( + + ) + } +} diff --git a/packages/web/components/patterns/RcEditorStyles.tsx b/packages/web/components/patterns/RcEditorStyles.tsx new file mode 100644 index 000000000..058cc6bbe --- /dev/null +++ b/packages/web/components/patterns/RcEditorStyles.tsx @@ -0,0 +1,34 @@ +export const RcEditorStyles = (isDark: boolean, shadow: boolean) => { + return { + '.rc-md-editor .rc-md-navigation': { + background: 'var(--colors-grayBg)', + borderBottom: '1px solid $thBorderSubtle', + }, + '.rc-md-editor': { + borderRadius: '5px', + backgroundColor: isDark ? '#2A2A2A' : 'white', + border: '1px solid $thBorderSubtle', + boxShadow: shadow ? '0px 4px 4px rgba(33, 33, 33, 0.1)' : 'unset', + }, + '.rc-md-navigation': { + borderRadius: '5px', + borderBottomLeftRadius: '0px', + borderBottomRightRadius: '0px', + background: 'var(--colors-grayBg)', + }, + '.rc-md-editor .editor-container >.section': { + borderRight: 'unset', + }, + '.rc-md-editor .editor-container .sec-md .input': { + padding: '10px', + borderRadius: '5px', + fontSize: '16px', + color: isDark ? '#EBEBEB' : 'black', + backgroundColor: isDark ? '#2A2A2A' : 'white', + }, + '.rc-md-editor .drop-wrap': { + border: '1px solid $thBorderSubtle', + backgroundColor: isDark ? '#2A2A2A' : 'white', + }, + } +} diff --git a/packages/web/components/templates/article/Notebook.tsx b/packages/web/components/templates/article/Notebook.tsx index 1c2add16f..cde685879 100644 --- a/packages/web/components/templates/article/Notebook.tsx +++ b/packages/web/components/templates/article/Notebook.tsx @@ -29,7 +29,7 @@ import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItems import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter' import { Button } from '../../elements/Button' import { Dropdown, DropdownOption } from '../../elements/DropdownElements' -import { ArticleNoteBox } from '../../patterns/ArticleNotes' +import { ArticleNotes } from '../../patterns/ArticleNotes' type NotebookProps = { viewer: UserBasicData @@ -328,7 +328,7 @@ export function Notebook(props: NotebookProps): JSX.Element { distribution="start" css={{ width: '100%', mt: '10px', gap: '10px' }} > -