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

@ -1,118 +1,158 @@
:root { :root {
--max-width: 1100px; --max-width: 1100px;
--border-radius: 12px; --border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
"Fira Mono", "Droid Sans Mono", "Courier New", monospace; "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
--foreground-rgb: 0, 0, 0; --foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220; --background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255; --background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient( --primary-glow: conic-gradient(
from 180deg at 50% 50%, from 180deg at 50% 50%,
#16abff33 0deg, #16abff33 0deg,
#0885ff33 55deg, #0885ff33 55deg,
#54d6ff33 120deg, #54d6ff33 120deg,
#0071ff33 160deg, #0071ff33 160deg,
transparent 360deg transparent 360deg
); );
--secondary-glow: radial-gradient( --secondary-glow: radial-gradient(
rgba(255, 255, 255, 1), rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0) rgba(255, 255, 255, 0)
); );
--tile-start-rgb: 239, 245, 249; --tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233; --tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient( --tile-border: conic-gradient(
#00000080, #00000080,
#00000040, #00000040,
#00000030, #00000030,
#00000020, #00000020,
#00000010, #00000010,
#00000010, #00000010,
#00000080 #00000080
); );
--callout-rgb: 238, 240, 241; --callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176; --callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188; --card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135; --card-border-rgb: 131, 134, 135;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--foreground-rgb: 255, 255, 255; --foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0; --background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0; --background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient( --secondary-glow: linear-gradient(
to bottom right, to bottom right,
rgba(1, 65, 255, 0), rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0), rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3) rgba(1, 65, 255, 0.3)
); );
--tile-start-rgb: 2, 13, 46; --tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19; --tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient( --tile-border: conic-gradient(
#ffffff80, #ffffff80,
#ffffff40, #ffffff40,
#ffffff30, #ffffff30,
#ffffff20, #ffffff20,
#ffffff10, #ffffff10,
#ffffff10, #ffffff10,
#ffffff80 #ffffff80
); );
--callout-rgb: 20, 20, 20; --callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108; --callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100; --card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200; --card-border-rgb: 200, 200, 200;
} }
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
html, html,
body { body {
max-width: 100vw; max-width: 100vw;
overflow-x: hidden; overflow-x: hidden;
} }
body { body {
color: rgb(var(--foreground-rgb)); color: rgb(var(--foreground-rgb));
background: linear-gradient( background: linear-gradient(
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 {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
} }
.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,20 +206,33 @@ 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>
<QuillEditor <MenuButton as={IconButton} aria-label="Note menu" icon={<BsThreeDotsVertical/>} variant="ghost"/>
theme="snow" <MenuList>
placeholder="Write down all your thoughts..." <MenuItem icon={<Icon as={BsGlobe} boxSize={3}/>}>Publish</MenuItem>
value={content} <MenuItem icon={<Icon as={BsCopy} boxSize={3}/>}>Duplicate</MenuItem>
onChange={handleEditorChange} <MenuDivider/>
modules={quillModules} <MenuItem icon={<Icon as={BsTrash} boxSize={3}/>} color="red">Delete</MenuItem>
formats={quillFormats} </MenuList>
style={{width: '100%', height: '100%', overflowY: 'auto' }} </Menu>
/> </HStack>
</Box>
<Box w="100%" overflowY="auto" h="calc(100% - var(--chakra-sizes-12))">
<QuillEditor
theme="snow"
placeholder="Write down all your thoughts..."
value={content}
onChange={handleEditorChange}
modules={quillModules}
formats={quillFormats}
style={{width: '100%', height: '100%', overflowY: 'auto'}}
/>
</Box>
</Container> </Container>
</Flex> </Flex>
</Box> </Box>