This commit is contained in:
Alexander Gorshkov 2024-03-22 15:30:37 +00:00
parent 02fc3ca102
commit acc2ba4a48
4 changed files with 310 additions and 111 deletions

97
package-lock.json generated
View File

@ -13,9 +13,16 @@
"@chakra-ui/react": "^2.8.2", "@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@xeger/quill-image-actions": "^0.7.2",
"@xeger/quill-image-formats": "^0.7.2",
"framer-motion": "^11.0.8", "framer-motion": "^11.0.8",
"next": "14.1.3", "next": "14.1.3",
"quill": "^1.3.7", "quill": "^1.3.7",
"quill-image-drop-module": "^1.0.3",
"quill-image-edit-module": "^0.1.6",
"quill-image-resize-module": "^3.0.0",
"quill-image-resize-module-react": "^3.0.0",
"quill-resize-module": "^1.2.4",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-icons": "^5.0.1", "react-icons": "^5.0.1",
@ -1678,6 +1685,29 @@
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"devOptional": true "devOptional": true
}, },
"node_modules/@xeger/quill-image-actions": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@xeger/quill-image-actions/-/quill-image-actions-0.7.2.tgz",
"integrity": "sha512-OhaZnYCie9J1ZLx2+Nsznj+qkdHn9Yx7q3iOCCKFs6kAzcZUVBPv2fMjK+nzURySLyCVQOP3Ur2mZx1yYqgppw==",
"dependencies": {
"core-js": "^3.0",
"deepmerge": "^4.2"
},
"peerDependencies": {
"quill": "^1.3.6"
}
},
"node_modules/@xeger/quill-image-formats": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@xeger/quill-image-formats/-/quill-image-formats-0.7.2.tgz",
"integrity": "sha512-L9beiJKWzjHxBJEZB21I8YjFgOHYROsrfFMvIqP+kjc/u/MecHY1mXzR9VySLr+Qvj55t+vkKKuJ0ZPKvQSaPA==",
"dependencies": {
"core-js": "^3.0"
},
"peerDependencies": {
"quill": "^1.3.6"
}
},
"node_modules/@zag-js/dom-query": { "node_modules/@zag-js/dom-query": {
"version": "0.16.0", "version": "0.16.0",
"resolved": "https://registry.npmjs.org/@zag-js/dom-query/-/dom-query-0.16.0.tgz", "resolved": "https://registry.npmjs.org/@zag-js/dom-query/-/dom-query-0.16.0.tgz",
@ -1858,6 +1888,16 @@
"toggle-selection": "^1.0.6" "toggle-selection": "^1.0.6"
} }
}, },
"node_modules/core-js": {
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz",
"integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@ -1905,6 +1945,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/define-data-property": { "node_modules/define-data-property": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@ -2520,6 +2568,55 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/quill-image-drop-module": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/quill-image-drop-module/-/quill-image-drop-module-1.0.3.tgz",
"integrity": "sha512-HP0Y2kb3nQk1QbRKZzEe1j3mArRQerN5B/U/MlXrOnxmhy3m/xYmVv0YoE13vWnGnBOIcoXGJ/9fi7l6AwsP8Q==",
"dependencies": {
"quill": "^1.2.2"
}
},
"node_modules/quill-image-edit-module": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/quill-image-edit-module/-/quill-image-edit-module-0.1.6.tgz",
"integrity": "sha512-yB5YiXqX8Scxxk9AaqKRAekqUB/WHFqrFHWDq6E1+Q/KjIFAmYTB8az1XuXiz8M7M/LL5689p5ZdqyYppS8R/Q==",
"dependencies": {
"deepmerge": "^4.2.2"
}
},
"node_modules/quill-image-resize-module": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/quill-image-resize-module/-/quill-image-resize-module-3.0.0.tgz",
"integrity": "sha512-1TZBnUxU/WIx5dPyVjQ9yN7C6mLZSp04HyWBEMqT320DIq4MW4JgzlOPDZX5ZpBM3bU6sacU4kTLUc8VgYQZYw==",
"dependencies": {
"lodash": "^4.17.4",
"quill": "^1.2.2",
"raw-loader": "^0.5.1"
}
},
"node_modules/quill-image-resize-module-react": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/quill-image-resize-module-react/-/quill-image-resize-module-react-3.0.0.tgz",
"integrity": "sha512-3jVChLoXh+fwEELx3OswOEEuF+1KU3r/B9RAqZ//s+d+UMduVZzUepU1g/XoxjKoBJvWD2lJwBIFBRUNb8ebCw==",
"dependencies": {
"lodash": "^4.17.4",
"quill": "^1.2.2",
"raw-loader": "^0.5.1"
}
},
"node_modules/quill-resize-module": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/quill-resize-module/-/quill-resize-module-1.2.4.tgz",
"integrity": "sha512-toBGslzfzxXlokN7d7naL6iq2Da5mDS4zzvyES2YpqwBqO/yrxHR1rylz0D1zLHAZcF+aeDWBNwktrYQMN5vGw==",
"dependencies": {
"extend": "^3.0.2"
}
},
"node_modules/raw-loader": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz",
"integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q=="
},
"node_modules/react": { "node_modules/react": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",

View File

@ -14,9 +14,16 @@
"@chakra-ui/react": "^2.8.2", "@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@xeger/quill-image-actions": "^0.7.2",
"@xeger/quill-image-formats": "^0.7.2",
"framer-motion": "^11.0.8", "framer-motion": "^11.0.8",
"next": "14.1.3", "next": "14.1.3",
"quill": "^1.3.7", "quill": "^1.3.7",
"quill-image-drop-module": "^1.0.3",
"quill-image-edit-module": "^0.1.6",
"quill-image-resize-module": "^3.0.0",
"quill-image-resize-module-react": "^3.0.0",
"quill-resize-module": "^1.2.4",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-icons": "^5.0.1", "react-icons": "^5.0.1",

View File

@ -91,8 +91,7 @@ body {
to bottom, to bottom,
transparent, transparent,
rgb(var(--background-end-rgb)) rgb(var(--background-end-rgb))
) ) rgb(var(--background-start-rgb));
rgb(var(--background-start-rgb));
} }
a { a {
@ -101,16 +100,57 @@ a {
} }
.ql-editor::before { .ql-editor::before {
left: 0!important; left: 0 !important;
right: 0!important; right: 0 !important;
font-size: 16px; font-size: 16px;
} }
.ql-editor { .ql-editor {
padding: 12px 0!important; padding: 12px 0 !important;
font-size: 16px; font-size: 16px;
} }
.quill {
container-type: inline-size;
}
.quill .ql-editor::-webkit-scrollbar {
width: var(--chakra-sizes-2);
}
.quill .ql-editor::-webkit-scrollbar-track {
background: var(--chakra-colors-gray-50)
}
.quill .ql-editor::-webkit-scrollbar-thumb {
background: var(--chakra-colors-gray-400);
border-radius: var(--chakra-sizes-4)
}
.quill .ql-toolbar {
border-radius: var(--border-radius);
}
.quill .ql-container {
margin-top: .5rem;
height: calc(100% - 43px - .5rem);
border: none !important;
}
.quill .ql-container img {
max-width: 1100px;
}
.quill .ql-container p {
margin-bottom: 1rem;
}
@container (width < 600px) {
.ql-container {
height: calc(100% - 68px) !important;
}
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html { html {
color-scheme: dark; color-scheme: dark;

View File

@ -7,28 +7,50 @@ import {
Divider, Divider,
Flex, Flex,
Heading, Heading,
HStack, HStack, Icon, IconButton,
Input, Input,
Link, Link, Menu, MenuButton, MenuDivider, MenuItem, MenuList,
Text, Text,
VStack, VStack,
Wrap Wrap
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import {Logo} from "@/components/shared/Logo"; import {Logo} from "@/components/shared/Logo";
import {BsPlus} from "react-icons/bs"; import {BsCopy, BsGlobe, BsPlus, BsThreeDotsVertical, BsTrash} from "react-icons/bs";
import 'react-quill/dist/quill.snow.css'; import 'react-quill/dist/quill.snow.css';
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import {useState} from "react"; import {useEffect, useState} from "react";
import Quill from "quill";
import {ImageDrop} from 'quill-image-drop-module';
const QuillEditor = dynamic(() => import('react-quill'), {ssr: false}); const QuillEditor = dynamic(() => import('react-quill'), {ssr: false});
Quill.register('modules/imageDrop', ImageDrop);
let titleTimeout: NodeJS.Timeout | null = null;
let contentTimeout: NodeJS.Timeout | null = null;
export default function NotesChakraPage() { export default function NotesChakraPage() {
const [title, setTitle] = useState('')
const [content, setContent] = useState(''); const [content, setContent] = useState('');
useEffect(() => {
if (typeof window !== "undefined" && window.localStorage) {
setTitle(window.localStorage.getItem("qnote:current-note-title") || '')
setContent(window.localStorage.getItem("qnote:current-note") || '');
}
return () => {
if (contentTimeout !== null) {
clearTimeout(contentTimeout);
}
if (titleTimeout !== null) {
clearTimeout(titleTimeout);
}
}
}, []);
const quillModules = { const quillModules = {
imageDrop: true,
toolbar: [ toolbar: [
[{header: [1, 2, 3, false]}], [{header: [1, 2, 3, false]}],
['bold', 'italic', 'underline', 'strike', 'blockquote'], ['bold', 'italic', 'underline', 'strike', 'blockquote'],
@ -41,7 +63,6 @@ export default function NotesChakraPage() {
], ],
}; };
const quillFormats = [ const quillFormats = [
'header', 'header',
'bold', 'bold',
@ -58,9 +79,30 @@ export default function NotesChakraPage() {
'code-block', 'code-block',
]; ];
const handleTitleChange = (newTitle: string) => {
setTitle(newTitle);
const handleEditorChange = (newContent) => { if (titleTimeout !== null) {
clearTimeout(titleTimeout);
}
titleTimeout = setTimeout(() => {
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem("qnote:current-note-title", newTitle);
}
}, 750)
}
const handleEditorChange = (newContent: string) => {
setContent(newContent); setContent(newContent);
if (contentTimeout !== null) {
clearTimeout(contentTimeout);
}
contentTimeout = setTimeout(() => {
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem("qnote:current-note", newContent);
}
}, 750)
}; };
return ( return (
@ -71,16 +113,17 @@ export default function NotesChakraPage() {
display="flex" display="flex"
flexDirection="column" flexDirection="column"
height="100%" height="100%"
overflowY="hidden" overflow="hidden"
> >
<Flex <Flex
px={{base: '4', sm: '10'}} px={{base: '4', sm: '10'}}
justifyContent="center" flexDirection={{base: "column", xl: "row"}}
justifyContent={{base: "start", xl: "center"}}
alignItems="start" alignItems="start"
h="auto" h="auto"
minH="100%" minH="100%"
gap={5}> gap={5}>
<VStack w="100%" maxW="sm" h="100%" minH="100%"> <VStack w="100%" maxW="sm" h={{base: "96px", xl: "100%"}} minH={{base: "96px", xl: "100%"}}>
<Link href="/" w="100%" border="none" outline="none" _before={{display: "none"}}> <Link href="/" w="100%" border="none" outline="none" _before={{display: "none"}}>
<HStack justifyContent="start" alignItems="center" w="100%"> <HStack justifyContent="start" alignItems="center" w="100%">
<Logo w={24}/> <Logo w={24}/>
@ -88,7 +131,7 @@ export default function NotesChakraPage() {
</HStack> </HStack>
</Link> </Link>
<Container <Container
display="flex" display={{base: "none", xl: "flex"}}
flex={1} flex={1}
flexDirection="column" flexDirection="column"
w="100%" w="100%"
@ -98,7 +141,7 @@ export default function NotesChakraPage() {
bg={{base: 'transparent', sm: 'bg.surface'}} bg={{base: 'transparent', sm: 'bg.surface'}}
boxShadow={{base: 'none', sm: 'xl'}} boxShadow={{base: 'none', sm: 'xl'}}
borderRadius={{base: 'none', sm: 'xl'}} borderRadius={{base: 'none', sm: 'xl'}}
overflowY="hidden" overflow="hidden"
> >
<VStack spacing={3} w="100%" h="100%" minH="100%"> <VStack spacing={3} w="100%" h="100%" minH="100%">
<Flex w="100%" justifyContent="space-between" alignItems="center" px={4}> <Flex w="100%" justifyContent="space-between" alignItems="center" px={4}>
@ -124,12 +167,12 @@ export default function NotesChakraPage() {
}}> }}>
<VStack spacing={1} w="100%" px={1} py={1}> <VStack spacing={1} w="100%" px={1} py={1}>
{Array.from(Array(50).keys()).map(i => ( {Array.from(Array(50).keys()).map(i => (
<Wrap key={i} w="100%"> <Wrap key={i} w="100%" _hover={{bg: 'gray.50'}}>
<Box w="100%" px={4} py={2}> <Box w="100%" px={4} py={4}>
<Text fontWeight="black">Note name ${i + 1}</Text> <Text fontWeight="black">Note name ${i + 1}</Text>
<Text>Note text preview...</Text> <Text>Note text preview...</Text>
</Box> </Box>
<Divider my={2}/> <Divider my={0}/>
</Wrap> </Wrap>
))} ))}
</VStack> </VStack>
@ -139,18 +182,17 @@ export default function NotesChakraPage() {
</VStack> </VStack>
<Container <Container
display="flex"
flex={1} flex={1}
flexDirection="column"
w="100%" w="100%"
h="auto" h="100%"
minH="100%" maxHeight={{base: 'calc(100% - 130px)', xl: '100%'}}
mx={0} mx={0}
py={{base: '0', sm: '8'}} py={{base: '0', sm: '8'}}
bg={{base: 'transparent', sm: 'bg.surface'}} bg={{base: 'transparent', sm: 'bg.surface'}}
boxShadow={{base: 'none', sm: 'xl'}} boxShadow={{base: 'none', sm: 'xl'}}
borderRadius={{base: 'none', sm: 'xl'}} borderRadius={{base: 'none', sm: 'xl'}}
> >
<HStack w="100%" alignItems="start" justifyContent="space-between" spacing={3}>
<Input size="xl" <Input size="xl"
placeholder="Unnamed note" placeholder="Unnamed note"
px={0} px={0}
@ -164,10 +206,23 @@ export default function NotesChakraPage() {
outline: "none", outline: "none",
borderBottomColor: "brand.500" borderBottomColor: "brand.500"
}} }}
value={title}
onChange={(e) => handleTitleChange(e.target.value)}
mb={3} mb={3}
/> />
<Box flex={1} w="100%" overflowY="auto" w="100%"> <Menu>
<MenuButton as={IconButton} aria-label="Note menu" icon={<BsThreeDotsVertical/>} variant="ghost"/>
<MenuList>
<MenuItem icon={<Icon as={BsGlobe} boxSize={3}/>}>Publish</MenuItem>
<MenuItem icon={<Icon as={BsCopy} boxSize={3}/>}>Duplicate</MenuItem>
<MenuDivider/>
<MenuItem icon={<Icon as={BsTrash} boxSize={3}/>} color="red">Delete</MenuItem>
</MenuList>
</Menu>
</HStack>
<Box w="100%" overflowY="auto" h="calc(100% - var(--chakra-sizes-12))">
<QuillEditor <QuillEditor
theme="snow" theme="snow"
placeholder="Write down all your thoughts..." placeholder="Write down all your thoughts..."
@ -175,7 +230,7 @@ export default function NotesChakraPage() {
onChange={handleEditorChange} onChange={handleEditorChange}
modules={quillModules} modules={quillModules}
formats={quillFormats} formats={quillFormats}
style={{width: '100%', height: '100%', overflowY: 'auto' }} style={{width: '100%', height: '100%', overflowY: 'auto'}}
/> />
</Box> </Box>
</Container> </Container>