diff --git a/package-lock.json b/package-lock.json index 1cbb2fb223..07a6432528 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2929,7 +2929,7 @@ }, "util": { "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -6015,9 +6015,9 @@ } }, "eslint-plugin-react-hooks": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.0.1.tgz", - "integrity": "sha512-xir+3KHKo86AasxlCV8AHRtIZPHljqCRRUYgASkbatmt0fad4+5GgC7zkT7o/06hdKM6MIwp8giHVXqBPaarHQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.3.0.tgz", + "integrity": "sha512-gLKCa52G4ee7uXzdLiorca7JIQZPPXRAQDXV83J4bUEeUuc5pIEyZYAZ45Xnxe5IuupxEqHS+hUhSLIimK1EMw==", "dev": true }, "eslint-scope": { @@ -6966,7 +6966,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7144,7 +7143,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7161,7 +7159,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7178,7 +7175,6 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7199,7 +7195,6 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -7303,7 +7298,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -7442,7 +7436,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7464,7 +7457,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -9292,7 +9284,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9470,7 +9461,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9487,7 +9477,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -9504,7 +9493,6 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9525,7 +9513,6 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -9629,7 +9616,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -9768,7 +9754,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9790,7 +9775,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -11078,7 +11062,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minipass": { @@ -15208,7 +15192,7 @@ }, "readable-stream": { "version": "1.0.33", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz", "integrity": "sha1-OjYN1mwbHX/UcFOJhg7aHQ9hEmw=", "requires": { "core-util-is": "~1.0.0", diff --git a/package.json b/package.json index 7123427d77..d9b795f1c3 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "eslint-config-prettier": "^6.3.0", "eslint-plugin-prettier": "^3.1.1", "eslint-plugin-react": "^7.14.3", - "eslint-plugin-react-hooks": "^2.0.1", + "eslint-plugin-react-hooks": "^2.3.0", "html-webpack-plugin": "^3.2.0", "jest": "^24.9.0", "json-loader": "^0.5.7", diff --git a/resources/Desktop.svg b/resources/Desktop.svg new file mode 100644 index 0000000000..33462d45be --- /dev/null +++ b/resources/Desktop.svg @@ -0,0 +1,44 @@ + + + + Macbook Pro + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/Mobile.svg b/resources/Mobile.svg new file mode 100644 index 0000000000..5aed769403 --- /dev/null +++ b/resources/Mobile.svg @@ -0,0 +1,79 @@ + + + + Group 8 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/App.tsx b/src/components/App.tsx index 3a61c1de81..5aedd5fd21 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -16,6 +16,7 @@ import '../lib/i18n' import '../lib/analytics' import CodeMirrorStyle from './CodeMirrorStyle' import { useGeneralStatus } from '../lib/generalStatus' +import Modal from './Modal' const App = () => { const { initialize, initialized } = useDb() @@ -67,6 +68,7 @@ const App = () => { + diff --git a/src/components/Modal/contents/DownloadOurAppModal.tsx b/src/components/Modal/contents/DownloadOurAppModal.tsx new file mode 100644 index 0000000000..cb6f105545 --- /dev/null +++ b/src/components/Modal/contents/DownloadOurAppModal.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { + ModalContainer, + ModalHeader, + ModalSubtitle, + ModalBody, + ModalFlex +} from './styled' + +const DownloadOurAppModal = () => { + return ( + + Download our apps + + Use Boostnote on your local and focus on your work! + + + +
+ + +
+
+ + + + We are planning for a launch by the end of the year. + +
+
+
+
+ ) +} + +export default DownloadOurAppModal diff --git a/src/components/Modal/contents/styled.ts b/src/components/Modal/contents/styled.ts new file mode 100644 index 0000000000..207bd4ed13 --- /dev/null +++ b/src/components/Modal/contents/styled.ts @@ -0,0 +1,79 @@ +import styled from '../../../lib/styled' +import { textColor } from '../../../lib/styled/styleFunctions' + +export const ModalContainer = styled.div` + width: 100%; + ${textColor} + padding: 3px; +` + +export const ModalHeader = styled.h1` + text-align: center; + margin-top: 10px; + margin-bottom: 10px; +` + +export const ModalSubtitle = styled.h4` + text-align: center; + margin-bottom: 8vh; +` + +export const ModalBody = styled.div` + margin-top: 20px; + padding: 0 2%; + + .button { + display: block; + background-color: rgb(3, 197, 136); + font-size: 16px; + line-height: 1; + text-transform: uppercase; + color: rgb(255, 255, 255); + padding: 16px 32px; + border-width: initial; + border-style: none; + border-color: initial; + border-image: initial; + border-radius: 4px; + margin: auto; + margin-bottom: 10px; + + &:not(:disabled):hover { + cursor: pointer; + opacity: 0.8; + } + + &.darker { + background-color: #d7d7d7; + color: #000; + } + + .subtext { + font-size: 12px; + } + } +` + +export const ModalFlex = styled.div` + width: 100%; + display: flex; + flex-wrap: no-wrap; + justify-content: space-between; + align-items: top; + + div { + flex: 1 1 0; + } + + img { + max-width: 100%; + width: auto; + height: 30vh; + display: block; + margin: auto; + } + + .center { + text-align: center; + } +` diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx new file mode 100644 index 0000000000..f75822a21e --- /dev/null +++ b/src/components/Modal/index.tsx @@ -0,0 +1,90 @@ +import React, { useMemo, useCallback } from 'react' +import { useGlobalKeyDownHandler } from '../../lib/keyboard' +import { useModal } from '../../lib/modal/store' +import { + StyledModalsBackground, + StyledModalsContainer, + StyledModalsSkipButton +} from './styled' +import Icon from '../atoms/Icon' +import { mdiChevronRightCircleOutline } from '@mdi/js' +import { usePreferences } from '../../lib/preferences' +import DownloadOurAppModal from './contents/DownloadOurAppModal' + +interface ModalsRenderingOptions { + closable: boolean + body: JSX.Element + onSkip?: () => void +} + +export default () => { + const { modalContent, closeModal } = useModal() + const { setPreferences } = usePreferences() + + const content = useMemo((): ModalsRenderingOptions => { + const basicModal: ModalsRenderingOptions = { + closable: true, + body: <> + } + + switch (modalContent) { + case 'download-app': + basicModal.body = + basicModal.onSkip = () => { + setPreferences({ + 'general.enableDownloadAppModal': false + }) + closeModal() + } + break + default: + break + } + + return basicModal + }, [modalContent, setPreferences, closeModal]) + + const closeHandler = useCallback(() => { + if (content.onSkip != null) { + return content.onSkip() + } + return closeModal() + }, [closeModal, content]) + + const keydownHandler = useMemo(() => { + return (event: KeyboardEvent) => { + if (event.key === 'Escape') { + closeHandler() + } + } + }, [closeHandler]) + useGlobalKeyDownHandler(keydownHandler) + + const backgroundClickHandler = useMemo(() => { + return (event: MouseEvent) => { + event.preventDefault() + closeHandler() + } + }, [closeHandler]) + + if (modalContent == null) return null + + return ( + <> + + + {content.body} + + {content.closable && ( + + + Skip + + + )} + + + ) +} diff --git a/src/components/Modal/styled.ts b/src/components/Modal/styled.ts new file mode 100644 index 0000000000..9bba07db77 --- /dev/null +++ b/src/components/Modal/styled.ts @@ -0,0 +1,68 @@ +import styled from '../../lib/styled' +import { + border, + backgroundColor, + contextMenuShadow, + iconColor +} from '../../lib/styled/styleFunctions' + +const zIndexModalsBackground = 8001 + +export const StyledModalsBackground = styled.div` + z-index: ${zIndexModalsBackground}; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + ${backgroundColor}; + opacity: 0.8; + display: flex; + overflow: hidden; +` + +export const StyledModalsContainer = styled.div` + z-index: ${zIndexModalsBackground + 1}; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: 50vw; + height: 70vh; + ${border} + ${backgroundColor} + ${contextMenuShadow} + border-radius: 4px; + display: flex; + overflow: hidden; +` + +export const StyledModalsHeader = styled.h1` + margin: 0; + padding: 1em 0; +` + +export const StyledModalsSkipButton = styled.button` + position: absolute; + bottom: 0; + right: 2%; + width: auto; + height: 40px; + background-color: transparent; + border: none; + font-size: 16px; + white-space: nowrap; + cursor: pointer; + ${iconColor} + + span { + line-height: 20px; + vertical-align: middle; + } + + .icon { + vertical-align: middle; + } +` diff --git a/src/components/Router.tsx b/src/components/Router.tsx index af8bb88cfd..374f622076 100644 --- a/src/components/Router.tsx +++ b/src/components/Router.tsx @@ -11,6 +11,7 @@ import useRedirectHandler from '../lib/router/redirect' export default () => { const routeParams = useRouteParams() const db = useDb() + useRedirectHandler() switch (routeParams.name) { diff --git a/src/components/SideNavigator/SideNavigator.tsx b/src/components/SideNavigator/SideNavigator.tsx index fbc1d575ca..87a4fb0248 100644 --- a/src/components/SideNavigator/SideNavigator.tsx +++ b/src/components/SideNavigator/SideNavigator.tsx @@ -19,7 +19,8 @@ import { usePreferences } from '../../lib/preferences' import { backgroundColor, iconColor, - textColor + textColor, + uiTextColor } from '../../lib/styled/styleFunctions' import SideNavigatorItem from './SideNavigatorItem' import { useGeneralStatus } from '../../lib/generalStatus' @@ -67,8 +68,12 @@ const StyledSideNavContainer = styled.nav` display: flex; flex-direction: column; } + .empty { padding: 4px; + padding-left: 26px; + margin-bottom: 4px; + ${uiTextColor} user-select: none; } diff --git a/src/components/Tutorials/TutorialsNavigator.tsx b/src/components/Tutorials/TutorialsNavigator.tsx index cf7143903b..0f4adadce4 100644 --- a/src/components/Tutorials/TutorialsNavigator.tsx +++ b/src/components/Tutorials/TutorialsNavigator.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React, { useMemo, useCallback } from 'react' import { tutorialsTree, TutorialsNavigatorTreeItem } from '../../lib/tutorials' import SideNavigatorItem from '../SideNavigator/SideNavigatorItem' import { @@ -6,12 +6,11 @@ import { mdiFolderOutline, mdiHelpCircleOutline } from '@mdi/js' -import { - useRouteParams, - useRouter, - currentTutorialPathname -} from '../../lib/router' +import { useRouter, useCurrentTutorialPathname } from '../../lib/router' import { useGeneralStatus } from '../../lib/generalStatus' +import { useContextMenu, MenuTypes } from '../../lib/contextMenu' +import { useDialog, DialogIconTypes } from '../../lib/dialog' +import { usePreferences } from '../../lib/preferences' interface NavigatorNode { id: string @@ -23,75 +22,106 @@ interface NavigatorNode { children: NavigatorNode[] active?: boolean } -interface TutorialsNavigatorProps {} -const TutorialsNavigator = ({ }: TutorialsNavigatorProps) => { - const routeParams = useRouteParams() +const TutorialsNavigator = ({}) => { const { push } = useRouter() - const currentHref = currentTutorialPathname() + const currentHref = useCurrentTutorialPathname() + const { popup } = useContextMenu() + const { messageBox } = useDialog() const { toggleSideNavOpenedItem, sideNavOpenedItemSet } = useGeneralStatus() + const { setPreferences } = usePreferences() const tutorials = tutorialsTree - function getNavigatorNodesFromTreeItem( - tree: TutorialsNavigatorTreeItem, - currentDepth: number, - parentNode?: NavigatorNode, - parentComponentPathname?: string - ): NavigatorNode | undefined { - if (tree.type === 'note') { - return - } + const getNavigatorNodesFromTreeItem = useCallback( + ( + tree: TutorialsNavigatorTreeItem, + currentDepth: number, + parentNode?: NavigatorNode, + parentComponentPathname?: string + ): NavigatorNode | undefined => { + if (tree.type === 'note') { + return + } - const componentPathname = `${parentComponentPathname != null && - parentComponentPathname}/${tree.absolutePath}` - const nodeHref = `${parentNode != null ? parentNode.href : '/app'}/${ - tree.slug - }` + const componentPathname = `${parentComponentPathname != null && + parentComponentPathname}/${tree.absolutePath}` + const nodeHref = `${parentNode != null ? parentNode.href : '/app'}/${ + tree.slug + }` - const folderIsActive = currentHref.split('/notes/note:')[0] === nodeHref + const folderIsActive = currentHref.split('/notes/note:')[0] === nodeHref - const notesUnderCurrentNode = tree.children.filter( - child => child.type === 'note' - ) + const notesUnderCurrentNode = tree.children.filter( + child => child.type === 'note' + ) - const nodeId = `TF-${nodeHref.split('/app')[1]}` - const currentNode = { - id: nodeId, - name: `${tree.label} ${ - notesUnderCurrentNode.length > 0 - ? `(${notesUnderCurrentNode.length})` - : '' - }`, - iconPath: - tree.type === 'folder' - ? folderIsActive - ? mdiFolderOpenOutline - : mdiFolderOutline - : mdiHelpCircleOutline, - href: nodeHref, - active: folderIsActive, - depth: currentDepth, - opened: sideNavOpenedItemSet.has(nodeId), - children: [] - } + const nodeId = `TF-${nodeHref.split('/app')[1]}` + const currentNode = { + id: nodeId, + name: `${tree.label} ${ + notesUnderCurrentNode.length > 0 + ? `(${notesUnderCurrentNode.length})` + : '' + }`, + iconPath: + tree.type === 'folder' + ? folderIsActive + ? mdiFolderOpenOutline + : mdiFolderOutline + : mdiHelpCircleOutline, + href: nodeHref, + active: folderIsActive, + depth: currentDepth, + opened: sideNavOpenedItemSet.has(nodeId), + children: [] + } - const childrenNodes = - tree.children.length === 0 - ? [] - : (tree.children - .map(childrenTree => - getNavigatorNodesFromTreeItem( - childrenTree, - currentDepth + 1, - currentNode, - componentPathname + const childrenNodes = + tree.children.length === 0 + ? [] + : (tree.children + .map(childrenTree => + getNavigatorNodesFromTreeItem( + childrenTree, + currentDepth + 1, + currentNode, + componentPathname + ) ) - ) - .filter(node => node != null) as NavigatorNode[]) - return { - ...currentNode, - children: childrenNodes + .filter(node => node != null) as NavigatorNode[]) + return { + ...currentNode, + children: childrenNodes + } + }, + [currentHref, sideNavOpenedItemSet] + ) + + const createOnContextMenuHandler = (depth: number) => { + return (event: React.MouseEvent) => { + event.preventDefault() + popup(event, [ + { + type: MenuTypes.Normal, + label: 'Remove folder', + enabled: depth === 0, + onClick: () => { + messageBox({ + title: `Hide tutorials`, + message: + 'You can choose to display them again in your preferences.', + iconType: DialogIconTypes.Warning, + buttons: ['Hide', 'Cancel'], + defaultButtonIndex: 0, + cancelButtonIndex: 1, + onClose: () => { + setPreferences({ 'general.tutorials': 'hide' }) + } + }) + } + } + ]) } } @@ -99,7 +129,7 @@ const TutorialsNavigator = ({ }: TutorialsNavigatorProps) => { return tutorials .map(tutorial => getNavigatorNodesFromTreeItem(tutorial, 0)) .filter(node => node != null) as NavigatorNode[] - }, [routeParams, tutorials, toggleSideNavOpenedItem]) + }, [tutorials, getNavigatorNodesFromTreeItem]) const redirectToTutorialNode = (node: NavigatorNode) => { push(node.href) @@ -122,6 +152,7 @@ const TutorialsNavigator = ({ }: TutorialsNavigatorProps) => { onClick={() => redirectToTutorialNode(node)} onFoldButtonClick={() => toggleSideNavOpenedItem(node.id)} folded={node.children.length === 0 ? undefined : !node.opened} + onContextMenu={createOnContextMenuHandler(node.depth)} /> {node.children.map(child => renderNode(child, node.opened))} diff --git a/src/components/Tutorials/TutorialsPage.tsx b/src/components/Tutorials/TutorialsPage.tsx index 067283ea83..4543c6fe16 100644 --- a/src/components/Tutorials/TutorialsPage.tsx +++ b/src/components/Tutorials/TutorialsPage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useCallback, useState } from 'react' +import React, { useCallback, useMemo } from 'react' import { tutorialsTree, TutorialsNavigatorTreeItem } from '../../lib/tutorials' import TwoPaneLayout from '../atoms/TwoPaneLayout' import { useGeneralStatus } from '../../lib/generalStatus' @@ -16,20 +16,89 @@ type TutoriasPagePicker = { } export default ({ pathname }: TutorialsPageProps) => { - const [ - currentTutorialBranch, - setCurrentTutorialBranch - ] = useState(getCurrentNodeFromTutorialTrees) + const { generalStatus, setGeneralStatus } = useGeneralStatus() + const router = useRouter() + + const searchThroughTreeForIdenticalNode = useCallback( + ( + pathToSearch: string, + parentDepthPath: string, + parentAbsolutePath: string, + tree: TutorialsNavigatorTreeItem, + parentTree?: TutorialsNavigatorTreeItem + ): TutoriasPagePicker | null => { + let match = null + const currentDepthPath = `${parentDepthPath}/${ + tree.type === 'note' ? 'notes/note:' : '' + }${tree.slug}` + + const currentAbsolutePath = parentAbsolutePath + '/' + tree.absolutePath + if (currentDepthPath === pathToSearch) { + const currentTreeWithDepthAbsolutePath = { + ...tree, + absolutePath: currentAbsolutePath, + children: Object.entries(tree.children).map(obj => { + return { + ...obj[1], + absolutePath: currentAbsolutePath + '/' + obj[1].absolutePath + } + }) as TutorialsNavigatorTreeItem[] + } + + const parentTreeWithDepthAbsolutePath = + parentTree != null + ? { + ...parentTree, + absolutePath: parentAbsolutePath, + children: Object.entries(parentTree.children).map(obj => { + return { + ...obj[1], + absolutePath: parentAbsolutePath + '/' + obj[1].absolutePath + } + }) + } + : undefined + + return { + currentTree: currentTreeWithDepthAbsolutePath, + parentTree: parentTreeWithDepthAbsolutePath + } + } + + for (let i = 0; i < tree.children.length; i++) { + match = searchThroughTreeForIdenticalNode( + pathToSearch, + currentDepthPath, + currentAbsolutePath, + tree.children[i], + tree + ) + if (match != null) { + break + } + } - const [currentFolderPathname, setCurrentFolderPathname] = useState( - getCurrentFolderPathname + return match + }, + [] ) - const [selectedNote, setSelectedNote] = useState< - TutorialsNavigatorTreeItem | undefined - >() - const { generalStatus, setGeneralStatus } = useGeneralStatus() - const router = useRouter() + const currentTutorialBranch = useMemo(() => { + let match = null + for (let i = 0; i < tutorialsTree.length; i++) { + match = searchThroughTreeForIdenticalNode( + pathname, + '/app', + '', + tutorialsTree[i] + ) + if (match != null) { + break + } + } + return match + }, [pathname, searchThroughTreeForIdenticalNode]) + const updateNoteListWidth = useCallback( (leftWidth: number) => { setGeneralStatus({ @@ -51,16 +120,7 @@ export default ({ pathname }: TutorialsPageProps) => { })) }, [setGeneralStatus]) - useEffect(() => { - setCurrentTutorialBranch(getCurrentNodeFromTutorialTrees) - setCurrentFolderPathname(getCurrentFolderPathname) - }, [pathname, tutorialsTree]) - - useEffect(() => { - setSelectedNote(getCurrentNote) - }, [currentTutorialBranch]) - - function getCurrentNote(): TutorialsNavigatorTreeItem | undefined { + const selectedNote = useMemo((): TutorialsNavigatorTreeItem | undefined => { if (currentTutorialBranch == null) { return undefined } @@ -76,88 +136,11 @@ export default ({ pathname }: TutorialsPageProps) => { return notesChildren[0] } return undefined - } + }, [currentTutorialBranch]) - function getCurrentFolderPathname() { + const currentFolderPathname = useMemo(() => { return pathname.split('/notes')[0] - } - - function getCurrentNodeFromTutorialTrees() { - let match = null - for (let i = 0; i < tutorialsTree.length; i++) { - match = searchThroughTreeForIdenticalNode( - pathname, - '/app', - '', - tutorialsTree[i] - ) - if (match != null) { - break - } - } - return match - } - - function searchThroughTreeForIdenticalNode( - pathToSearch: string, - parentDepthPath: string, - parentAbsolutePath: string, - tree: TutorialsNavigatorTreeItem, - parentTree?: TutorialsNavigatorTreeItem - ): TutoriasPagePicker | null { - let match = null - const currentDepthPath = `${parentDepthPath}/${ - tree.type === 'note' ? 'notes/note:' : '' - }${tree.slug}` - - const currentAbsolutePath = parentAbsolutePath + '/' + tree.absolutePath - if (currentDepthPath === pathToSearch) { - const currentTreeWithDepthAbsolutePath = { - ...tree, - absolutePath: currentAbsolutePath, - children: Object.entries(tree.children).map(obj => { - return { - ...obj[1], - absolutePath: currentAbsolutePath + '/' + obj[1].absolutePath - } - }) as TutorialsNavigatorTreeItem[] - } - - const parentTreeWithDepthAbsolutePath = - parentTree != null - ? { - ...parentTree, - absolutePath: parentAbsolutePath, - children: Object.entries(parentTree.children).map(obj => { - return { - ...obj[1], - absolutePath: parentAbsolutePath + '/' + obj[1].absolutePath - } - }) - } - : undefined - - return { - currentTree: currentTreeWithDepthAbsolutePath, - parentTree: parentTreeWithDepthAbsolutePath - } - } - - for (let i = 0; i < tree.children.length; i++) { - match = searchThroughTreeForIdenticalNode( - pathToSearch, - currentDepthPath, - currentAbsolutePath, - tree.children[i], - tree - ) - if (match != null) { - break - } - } - - return match - } + }, [pathname]) const navigateUp = useCallback(() => { if (currentTutorialBranch == null) { @@ -199,7 +182,7 @@ export default ({ pathname }: TutorialsPageProps) => { ) } return - }, [selectedNote]) + }, [selectedNote, currentTutorialBranch, router, currentFolderPathname]) const navigateDown = useCallback(() => { if (currentTutorialBranch == null) { @@ -241,7 +224,7 @@ export default ({ pathname }: TutorialsPageProps) => { ) } return - }, [selectedNote]) + }, [selectedNote, currentFolderPathname, currentTutorialBranch, router]) return ( ( + null + ) + + const openModal = useCallback((content: ModalsContentOptions) => { + setModalContent(content) + }, []) + + const closeModal = useCallback(() => { + setModalContent(null) + }, []) + + return { + modalContent, + closeModal, + openModal + } +} + +export const { + StoreProvider: ModalProvider, + useStore: useModal +} = createStoreContext(useModalStore, 'modal') diff --git a/src/lib/modal/types.ts b/src/lib/modal/types.ts new file mode 100644 index 0000000000..2f74b279ca --- /dev/null +++ b/src/lib/modal/types.ts @@ -0,0 +1,14 @@ +export type ModalsContentOptions = 'download-app' + +export interface ModalsRenderingOptions { + closable: boolean + skippable: boolean + onClose?(): void + body: JSX.Element +} + +export interface ModalsContext { + modalContent: ModalsContentOptions | null + openModal: (options: ModalsContentOptions) => void + closeModal: () => void +} diff --git a/src/lib/preferences/store.ts b/src/lib/preferences/store.ts index 463a8de626..99295f36d4 100644 --- a/src/lib/preferences/store.ts +++ b/src/lib/preferences/store.ts @@ -30,6 +30,7 @@ const basePreferences: Preferences = { 'general.theme': 'dark', 'general.noteSorting': 'date-updated', 'general.enableAnalytics': true, + 'general.enableDownloadAppModal': true, 'general.tutorials': 'display', // Editor diff --git a/src/lib/preferences/types.ts b/src/lib/preferences/types.ts index ab6a660eb1..156b2db66c 100644 --- a/src/lib/preferences/types.ts +++ b/src/lib/preferences/types.ts @@ -19,6 +19,7 @@ export interface Preferences { 'general.theme': GeneralThemeOptions 'general.noteSorting': GeneralNoteSortingOptions 'general.enableAnalytics': boolean + 'general.enableDownloadAppModal': boolean 'general.tutorials': GeneralTutorialsOptions // Editor diff --git a/src/lib/router/redirect.ts b/src/lib/router/redirect.ts index 51e0828ff9..d6d69c32a9 100644 --- a/src/lib/router/redirect.ts +++ b/src/lib/router/redirect.ts @@ -1,24 +1,29 @@ import { useDb } from '../db' import { useRouter } from './store' -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' +import { entries } from '../db/utils' +import { useModal } from '../modal' import { usePreferences } from '../preferences' export default function useRedirectHandler() { const db = useDb() + const { replace, pathname } = useRouter() + const { openModal } = useModal() const { preferences } = usePreferences() - const { push, pathname } = useRouter() + const storageMapRef = useRef(db.storageMap) useEffect(() => { - if (!db.initialized || db.storageMap == null) { - return - } - if ( - pathname === '/app' && - preferences['general.tutorials'] === 'display' && - Object.keys(db.storageMap).length === 0 - ) { - push('/app/tutorials/welcome-pack/guides/notes/note:storage-guide') + const storageEntries = entries(storageMapRef.current) + + if (pathname !== '/app') return + + if (storageEntries.length === 0) { + if (preferences['general.enableDownloadAppModal']) { + openModal('download-app') + } + replace('/app/storages') + } else { + replace(`/app/storages/${storageEntries[0][1].id}`) } - return - }, [pathname, db.initialized, preferences['general.displayTutorials']]) + }, [pathname, replace]) } diff --git a/src/lib/router/utils.ts b/src/lib/router/utils.ts index 1c785555aa..d7b51375d6 100644 --- a/src/lib/router/utils.ts +++ b/src/lib/router/utils.ts @@ -215,7 +215,7 @@ export const usePathnameWithoutNoteId = () => { }, [routeParams, pathname]) } -export const currentTutorialPathname = () => { +export const useCurrentTutorialPathname = () => { const routeParams = useRouteParams() return useMemo(() => { switch (routeParams.name) { diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Guides/AboutOurCommunity.md b/src/lib/tutorials/files/tutorials/AboutOurCommunity.md similarity index 100% rename from src/lib/tutorials/files/tutorials/WelcomePack/Guides/AboutOurCommunity.md rename to src/lib/tutorials/files/tutorials/AboutOurCommunity.md diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Templates/Brainstorm.md b/src/lib/tutorials/files/tutorials/Brainstorm.md similarity index 100% rename from src/lib/tutorials/files/tutorials/WelcomePack/Templates/Brainstorm.md rename to src/lib/tutorials/files/tutorials/Brainstorm.md diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Templates/BugfixReport.md b/src/lib/tutorials/files/tutorials/BugfixReport.md similarity index 100% rename from src/lib/tutorials/files/tutorials/WelcomePack/Templates/BugfixReport.md rename to src/lib/tutorials/files/tutorials/BugfixReport.md diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Playground/GetStarted.md b/src/lib/tutorials/files/tutorials/GetStarted.md similarity index 100% rename from src/lib/tutorials/files/tutorials/WelcomePack/Playground/GetStarted.md rename to src/lib/tutorials/files/tutorials/GetStarted.md diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Guides/KeyboardShortcuts.md b/src/lib/tutorials/files/tutorials/KeyboardShortcuts.md similarity index 100% rename from src/lib/tutorials/files/tutorials/WelcomePack/Guides/KeyboardShortcuts.md rename to src/lib/tutorials/files/tutorials/KeyboardShortcuts.md diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Templates/MeetingNotes.md b/src/lib/tutorials/files/tutorials/MeetingNotes.md similarity index 100% rename from src/lib/tutorials/files/tutorials/WelcomePack/Templates/MeetingNotes.md rename to src/lib/tutorials/files/tutorials/MeetingNotes.md diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Guides/StorageGuide.md b/src/lib/tutorials/files/tutorials/StorageGuide.md similarity index 100% rename from src/lib/tutorials/files/tutorials/WelcomePack/Guides/StorageGuide.md rename to src/lib/tutorials/files/tutorials/StorageGuide.md diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Playground/TodaysTask.md b/src/lib/tutorials/files/tutorials/TodaysTask.md similarity index 100% rename from src/lib/tutorials/files/tutorials/WelcomePack/Playground/TodaysTask.md rename to src/lib/tutorials/files/tutorials/TodaysTask.md diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Templates/WeeklyPlanner.md b/src/lib/tutorials/files/tutorials/WeeklyPlanner.md similarity index 100% rename from src/lib/tutorials/files/tutorials/WelcomePack/Templates/WeeklyPlanner.md rename to src/lib/tutorials/files/tutorials/WeeklyPlanner.md diff --git a/src/lib/tutorials/tree.ts b/src/lib/tutorials/tree.ts index 03d64c94ae..9ee9dcac74 100644 --- a/src/lib/tutorials/tree.ts +++ b/src/lib/tutorials/tree.ts @@ -14,99 +14,67 @@ export const tutorialsTree: TutorialsNavigatorTreeItem[] = [ type: 'storage', children: [ { - label: 'Welcome Pack', - absolutePath: 'WelcomePack', - slug: 'welcome-pack', - type: 'folder', - children: [ - { - label: 'Playground', - absolutePath: 'Playground', - slug: 'playground', - type: 'folder', - children: [ - { - label: 'Get Started!', - absolutePath: 'GetStarted.md', - slug: 'get-started', - type: 'note', - children: [] - }, - { - label: "Today's Task", - absolutePath: 'TodaysTask.md', - slug: 'daily-task', - type: 'note', - children: [] - } - ] - }, - { - label: 'Guides', - absolutePath: 'Guides', - slug: 'guides', - type: 'folder', - children: [ - { - label: 'About our community', - absolutePath: 'AboutOurCommunity.md', - slug: 'community', - type: 'note', - children: [] - }, - { - label: 'Keyboard shortcuts', - absolutePath: 'KeyboardShortcuts.md', - slug: 'keyboard-shortcuts', - type: 'note', - children: [] - }, - { - label: 'Storage guide', - absolutePath: 'StorageGuide.md', - slug: 'storage-guide', - type: 'note', - children: [] - } - ] - }, - { - label: 'Templates', - absolutePath: 'Templates', - slug: 'templates', - type: 'folder', - children: [ - { - label: 'Brainstorm', - absolutePath: 'Brainstorm.md', - slug: 'brainstorm', - type: 'note', - children: [] - }, - { - label: 'Bugfix Report', - absolutePath: 'BugfixReport.md', - slug: 'bugfix-report', - type: 'note', - children: [] - }, - { - label: 'Meeting Notes', - absolutePath: 'MeetingNotes.md', - slug: 'meeting-notes', - type: 'note', - children: [] - }, - { - label: 'Weekly Planner', - absolutePath: 'WeeklyPlanner.md', - slug: 'weekly-planner', - type: 'note', - children: [] - } - ] - } - ] + label: 'Get Started!', + absolutePath: 'GetStarted.md', + slug: 'get-started', + type: 'note', + children: [] + }, + { + label: "Today's Task", + absolutePath: 'TodaysTask.md', + slug: 'daily-task', + type: 'note', + children: [] + }, + { + label: 'About our community', + absolutePath: 'AboutOurCommunity.md', + slug: 'community', + type: 'note', + children: [] + }, + { + label: 'Keyboard shortcuts', + absolutePath: 'KeyboardShortcuts.md', + slug: 'keyboard-shortcuts', + type: 'note', + children: [] + }, + { + label: 'Storage guide', + absolutePath: 'StorageGuide.md', + slug: 'storage-guide', + type: 'note', + children: [] + }, + { + label: 'Template - Brainstorm', + absolutePath: 'Brainstorm.md', + slug: 'brainstorm', + type: 'note', + children: [] + }, + { + label: 'Template - Bugfix Report', + absolutePath: 'BugfixReport.md', + slug: 'bugfix-report', + type: 'note', + children: [] + }, + { + label: 'Template - Meeting Notes', + absolutePath: 'MeetingNotes.md', + slug: 'meeting-notes', + type: 'note', + children: [] + }, + { + label: 'Template - Weekly Planner', + absolutePath: 'WeeklyPlanner.md', + slug: 'weekly-planner', + type: 'note', + children: [] } ] }