diff --git a/.firebase/hosting.YnVpbGQ.cache b/.firebase/hosting.YnVpbGQ.cache new file mode 100644 index 0000000..51df47d --- /dev/null +++ b/.firebase/hosting.YnVpbGQ.cache @@ -0,0 +1,18 @@ +favicon.ico,499162500000,eae62e993eb980ec8a25058c39d5a51feab118bd2100c4deebb2a9c158ec11f9 +manifest.json,499162500000,aff3449bdc238776f5d6d967f19ec491b36aed5fb7f23ccff6500736fd58494a +robots.txt,499162500000,bfe106a3fb878dc83461c86818bf74fc1bdc7f28538ba613cd3e775516ce8b49 +asset-manifest.json,1638706700478,eb7ec3814a623a8505b0c23598eeedd98555dbb322e2338058f425fcddc5f647 +index.html,1638706700478,6f81e7693be702d362f54816d45b92b20814993e97580ce9c16dcea45d4599a7 +static/css/main.622cf653.chunk.css,1638706700499,da9d1cd64cac094f79ee2d9ad943cddbd4ec9fdb234890cedd51dc85af8abbf6 +static/js/2.3ebbb465.chunk.js.LICENSE.txt,1638706700504,778549c2e183347c9a3e4ec088f4ee92fe68ee391b2593ae4642eafc59aea763 +static/css/main.622cf653.chunk.css.map,1638706700502,000c08c2c7d95e7fb51daadc7c80235f8ff7abd5052564b001ade20bb56002a6 +static/js/main.60c4126d.chunk.js,1638706700500,4772b4eefa70829282876b9b8766febf28df46bd25e1b5bf7467522816872732 +static/js/runtime-main.c78050a0.js,1638706700502,ae6a81da94288cb9dcbc73b48edd6a0850a6e9ee4c5618b5aef2804a6ffb4d28 +static/js/runtime-main.c78050a0.js.map,1638706700504,25bc469baa6b13a87732cd282070e39fbf9590662fde81509502d649523d4632 +static/media/add.0a53e6f3.svg,1638706700502,683f6f4a31e2204308448ddb626c23630d0900d649149c7cc52e272b6d90006d +static/media/book.600ff157.svg,1638706700501,f3d8abdf23b8973caa30998dcfec039dde9edb8b537e99870130ec9f045bc72e +static/media/close.7704b6d5.svg,1638706700499,ee6b0b0cac9bf89c73a44c14376a4766b9709199d302973344291cf92abc6845 +static/media/log-out.316dd325.svg,1638706700500,0e090b6705dddb34364f9f09d229d055622b7971a3db14298bc7025510eba25e +static/js/main.60c4126d.chunk.js.map,1638706700502,8ad5f89fb798d942f1a8bb3df622a6ecf67c505c05a8b9d6628b1f302ef23add +static/js/2.3ebbb465.chunk.js,1638706700500,4bf057d302fdf47adc653639983ea70d9334f5c9db403fdfe9681617899e1287 +static/js/2.3ebbb465.chunk.js.map,1638706700504,ec3d364b776397be250ca6ab5b9c321cc651f7bd7eab643720a3f6ef9565d367 diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..f5fe2ed --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "notr-6524b" + } +} diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml new file mode 100644 index 0000000..e7a598f --- /dev/null +++ b/.github/workflows/firebase-hosting-merge.yml @@ -0,0 +1,20 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on merge +'on': + push: + branches: + - main +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm ci && npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_NOTR_6524B }}' + channelId: live + projectId: notr-6524b diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml new file mode 100644 index 0000000..cb29a1b --- /dev/null +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -0,0 +1,17 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on PR +'on': pull_request +jobs: + build_and_preview: + if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm ci && npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_NOTR_6524B }}' + projectId: notr-6524b diff --git a/.ionide/symbolCache.db b/.ionide/symbolCache.db new file mode 100644 index 0000000..8a73c4f Binary files /dev/null and b/.ionide/symbolCache.db differ diff --git a/README.md b/README.md index fe155ee..ee0a3ff 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# NoTR \ No newline at end of file +# Простий нотатник + +[Прев'ю](https://notr-6524b.web.app/notes) diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..cfbc74c --- /dev/null +++ b/firebase.json @@ -0,0 +1,12 @@ +{ + "hosting": { + "public": "build", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/package.json b/package.json index c82e72c..337712b 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "deploy": "npm run build && gh-pages -d build" + "deploy": "npm run build && firebase deploy" }, "eslintConfig": { "extends": [ @@ -46,5 +46,7 @@ "last 1 safari version" ] }, - "devDependencies": {} + "devDependencies": { + "eslint-plugin-react-hooks": "^4.2.0" + } } diff --git a/src/assets/icons/add.svg b/src/assets/icons/add.svg new file mode 100644 index 0000000..a627142 --- /dev/null +++ b/src/assets/icons/add.svg @@ -0,0 +1 @@ +Add Circle \ No newline at end of file diff --git a/src/assets/icons/book.svg b/src/assets/icons/book.svg new file mode 100644 index 0000000..9f6133c --- /dev/null +++ b/src/assets/icons/book.svg @@ -0,0 +1 @@ +Book \ No newline at end of file diff --git a/src/assets/icons/close.svg b/src/assets/icons/close.svg new file mode 100644 index 0000000..5b0112c --- /dev/null +++ b/src/assets/icons/close.svg @@ -0,0 +1 @@ +Close Circle \ No newline at end of file diff --git a/src/assets/icons/mark.svg b/src/assets/icons/mark.svg new file mode 100644 index 0000000..c6cdee7 --- /dev/null +++ b/src/assets/icons/mark.svg @@ -0,0 +1 @@ +Bookmark \ No newline at end of file diff --git a/src/components/BurgerMenu/style.scss b/src/components/BurgerMenu/style.scss index 6374709..91c01ee 100644 --- a/src/components/BurgerMenu/style.scss +++ b/src/components/BurgerMenu/style.scss @@ -6,6 +6,7 @@ padding: 5px; background: none; border: none; + cursor: pointer; display: flex; flex-direction: column; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 822f5bc..82171b1 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -16,6 +16,7 @@ export const buttonWeaveAnimation = 700; interface Props extends ButtonHTMLAttributes { upperCase?: boolean; + active?: boolean | { className: string }; } interface ClickAnimation { @@ -30,6 +31,7 @@ export const Button: FC = ({ className, onClick, children, + active, ...props }) => { const [clickAnimation, setClickAnimation] = useState( @@ -68,7 +70,9 @@ export const Button: FC = ({ className={clsx( className, css.Button, - upperCase && css.Button__upperCase + upperCase && css.Button__upperCase, + active && css.Button__active, + typeof active === "string" && active )} onClick={handleOnClick} > diff --git a/src/components/Button/style.module.scss b/src/components/Button/style.module.scss index 77903ca..ec16eb9 100644 --- a/src/components/Button/style.module.scss +++ b/src/components/Button/style.module.scss @@ -48,6 +48,9 @@ &:focus { box-shadow: 0 0 0 0.4rem rgba(13, 110, 253, 0.5); } + &__active { + border-bottom: 2px solid $main-functional-color; + } &__upperCase { text-transform: uppercase; diff --git a/src/components/Card/index.tsx b/src/components/Card/index.tsx new file mode 100644 index 0000000..2de2161 --- /dev/null +++ b/src/components/Card/index.tsx @@ -0,0 +1,61 @@ +import React, { FC } from "react"; +import clsx from "clsx"; + +import closeURL from "assets/icons/close.svg"; + +import css from "./style.module.scss"; + +export enum DefaultCardColors { + blue = "#007bff", + green = "#28a745", + red = "#dc3545", + yellow = "#ffc107", + teal = "#17a2b8", + dark = "#343a40", + gray = "#6c757d", +} + +interface Props { + name: string; + title: string; + text: string; + color?: DefaultCardColors | string; + + classes?: { + root?: string; + }; + + deleteCard?: () => void; +} + +export const Card: FC = ({ + name, + title, + text, + color = DefaultCardColors.blue, + classes, + deleteCard, +}) => { + return ( +
+
+ {name} + {!!deleteCard && ( + Delete Card + )} +
+
+
{title}
+

{text}

+
+
+ ); +}; diff --git a/src/components/Card/style.module.scss b/src/components/Card/style.module.scss new file mode 100644 index 0000000..48add56 --- /dev/null +++ b/src/components/Card/style.module.scss @@ -0,0 +1,61 @@ +@import "sass/index"; + +$iconSize: 25px; +$iconMargin: 10px; + +.root { + background-clip: border-box; + position: relative; + word-wrap: break-word; + + display: flex; + flex-direction: column; + + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 4px; + width: 290px; + min-height: 200px; + margin: 5px; + + text-align: left; + color: white; +} +.name { + padding: 12px 20px; + background: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); + font-size: 1.6rem; + min-height: 45px; + + position: relative; + + &:first-child { + border-radius: 3px 3px 0 0; + } +} +.body { + flex: 1 1 auto; + padding: 20px; +} +.title { + font-weight: 500; + line-height: 1.2; + font-size: 2rem; + margin: 0; + margin-bottom: 12px; +} +.text { + margin: 0; + font-size: 1.6rem; +} + +.deleteIcon { + width: $iconSize; + height: $iconSize; + + position: absolute; + right: 10px; + top: 10px; + + cursor: pointer; +} diff --git a/src/components/Color/index.tsx b/src/components/Color/index.tsx new file mode 100644 index 0000000..8d45367 --- /dev/null +++ b/src/components/Color/index.tsx @@ -0,0 +1,32 @@ +import clsx from "clsx"; +import React, { FC, useState } from "react"; +import css from "./style.module.scss"; + +interface Props { + defaultColor?: string; + className?: string; + onChange?: (value: string) => void; +} + +export const Color: FC = ({ + defaultColor = "#ffffff", + className, + onChange, +}) => { + const [color, setColor] = useState(defaultColor); + + return ( +
+ { + const color = target.value; + setColor(color); + onChange && onChange(color); + }} + /> +
+ ); +}; diff --git a/src/components/Color/style.module.scss b/src/components/Color/style.module.scss new file mode 100644 index 0000000..e33995f --- /dev/null +++ b/src/components/Color/style.module.scss @@ -0,0 +1,16 @@ +$size: 20px; + +.root { + width: $size; + height: $size; + border-radius: 50%; + margin: 2px; + position: relative; +} + +.input { + opacity: 0; + position: absolute; + width: 100%; + height: 100%; +} diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx new file mode 100644 index 0000000..3fff594 --- /dev/null +++ b/src/components/Input/index.tsx @@ -0,0 +1,41 @@ +import React, { DetailedHTMLProps, FC, InputHTMLAttributes } from "react"; +import clsx from "clsx"; + +import css from "./style.module.scss"; + +interface Props + extends DetailedHTMLProps< + InputHTMLAttributes, + HTMLInputElement + > { + type?: "text" | "number"; + label?: string; + + classes?: { + label?: string; + input?: string; + }; +} + +export const Input: FC = ({ + label, + className, + type = "text", + classes, + ...props +}) => { + return ( + + ); +}; diff --git a/src/components/Input/style.module.scss b/src/components/Input/style.module.scss new file mode 100644 index 0000000..c3ac46d --- /dev/null +++ b/src/components/Input/style.module.scss @@ -0,0 +1,39 @@ +@import "sass/index"; + +.wrapper { + display: block; +} + +.input { + display: block; + + width: 100%; + padding: 6px 12px; + + font-size: 16px; + line-height: 1.5; + color: #495057; + + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: 4px; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + + margin: 0; + overflow: visible; + + &:focus { + color: #495057; + background-color: #fff; + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 3.5px RGB(0 123 255 / 25%); + } +} + +.label { + color: $main-functional-color; + font-size: 1.4rem; + margin: 0 0 5px; +} diff --git a/src/components/SideBar/index.tsx b/src/components/SideBar/index.tsx new file mode 100644 index 0000000..2ac3858 --- /dev/null +++ b/src/components/SideBar/index.tsx @@ -0,0 +1,15 @@ +import React, { FC } from "react"; +import clsx from "clsx"; +import css from "./style.module.scss"; + +interface Props { + disabled: boolean; +} + +export const SideBar: FC = ({ disabled, children }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/src/components/SideBar/style.module.scss b/src/components/SideBar/style.module.scss new file mode 100644 index 0000000..a80ef9a --- /dev/null +++ b/src/components/SideBar/style.module.scss @@ -0,0 +1,19 @@ +@import "sass/index"; + +.root { + padding: 5px; + + max-width: $minimum-screen-width; + min-width: calc(#{$minimum-screen-width} - 100px); + height: 100%; + overflow: hidden; + transition: max-width $long-transition-time, min-width $long-transition-time, + padding $long-transition-time; + + &__disabled { + max-width: 0; + min-width: 0; + padding-left: 0; + padding-right: 0; + } +} diff --git a/src/components/SideBarItem/index.tsx b/src/components/SideBarItem/index.tsx new file mode 100644 index 0000000..be750d5 --- /dev/null +++ b/src/components/SideBarItem/index.tsx @@ -0,0 +1,26 @@ +import React, { FC } from "react"; +import { Link } from "react-router-dom"; +import { useLocation } from "react-router"; +import clsx from "clsx"; + +import { Img } from "components"; +import css from "./style.module.scss"; + +interface Props { + name: string; + path: string; + img?: string; +} + +export const SideBarItem: FC = ({ name, path, img }) => { + const { pathname } = useLocation(); + return ( + + +

{name}

+ + ); +}; diff --git a/src/components/SideBarItem/style.module.scss b/src/components/SideBarItem/style.module.scss new file mode 100644 index 0000000..5c3b374 --- /dev/null +++ b/src/components/SideBarItem/style.module.scss @@ -0,0 +1,31 @@ +@import "sass/index"; + +$navImgSize: 20px; + +.root { + padding: 5px; + margin: 2px 0; + display: flex; + align-items: center; + color: $main-functional-color; + text-decoration: none; + border-radius: 4px; + + transition: background $transition-time, color $transition-time; + + &_img { + width: $navImgSize; + height: $navImgSize; + margin-right: 5px; + } + &_name { + margin: 0; + font-weight: bold; + font-size: 1.2rem; + } + + &__selected { + background: $main-functional-color; + color: $second-functional-color; + } +} diff --git a/src/components/UserName/index.tsx b/src/components/UserName/index.tsx new file mode 100644 index 0000000..366933a --- /dev/null +++ b/src/components/UserName/index.tsx @@ -0,0 +1,32 @@ +import React, { FC } from "react"; +import { Img } from "components"; +import { useUser } from "providers"; +import css from "./style.module.scss"; +import clsx from "clsx"; + +interface Props { + withoutName?: boolean; + classes?: { + wrapper?: string; + img?: string; + text?: string; + }; +} + +export const UserName: FC = ({ withoutName, classes }) => { + const { user } = useUser(); + return ( +
+ user + {!withoutName && ( +

+ {user?.displayName} +

+ )} +
+ ); +}; diff --git a/src/components/UserName/style.module.scss b/src/components/UserName/style.module.scss new file mode 100644 index 0000000..5419f11 --- /dev/null +++ b/src/components/UserName/style.module.scss @@ -0,0 +1,17 @@ +@import "sass/index"; +$avatarSize: 30px; + +.user { + display: flex; + align-items: center; + + &_avatar { + width: $avatarSize; + height: $avatarSize; + border-radius: 50%; + } + &_name { + white-space: nowrap; + margin-left: 10px; + } +} diff --git a/src/components/index.tsx b/src/components/index.tsx index 919223b..bb78a82 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -1,3 +1,10 @@ export { BurgerMenu } from "./BurgerMenu"; +export { UserName } from "./UserName"; export { Button } from "./Button"; +export { Input } from "./Input"; +export { Color } from "./Color"; +export { Card } from "./Card"; export { Img } from "./Img"; + +export { SideBar } from "./SideBar"; +export { SideBarItem } from "./SideBarItem"; diff --git a/src/configs/general.ts b/src/configs/general.ts new file mode 100644 index 0000000..0782c16 --- /dev/null +++ b/src/configs/general.ts @@ -0,0 +1,4 @@ +import css from "./style.module.scss"; + +export const minimumScreenWidth = parseInt(css.minimumScreenWidth), + mobileScreenWidth = parseInt(css.mobileScreenWidth); diff --git a/src/configs/pages/index.ts b/src/configs/pages/index.ts index cefbca2..6fb4b38 100644 --- a/src/configs/pages/index.ts +++ b/src/configs/pages/index.ts @@ -10,10 +10,10 @@ export const appPageList: PageList[] = [ path: "/", redirect: "/notes", }, - { - path: "/main", - component: Main, - }, + // { + // path: "/main", + // component: Main, + // }, { path: "*", component: ErrorPage, diff --git a/src/configs/pages/types.ts b/src/configs/pages/types.ts index 309de06..e42f275 100644 --- a/src/configs/pages/types.ts +++ b/src/configs/pages/types.ts @@ -2,6 +2,6 @@ import { FC } from "react"; export interface PageList { path: string; - component?: (() => JSX.Element) | FC | (() => FC); + component?: FC; redirect?: string; } diff --git a/src/configs/style.module.scss b/src/configs/style.module.scss new file mode 100644 index 0000000..5aa96c0 --- /dev/null +++ b/src/configs/style.module.scss @@ -0,0 +1 @@ +@import "sass/index"; diff --git a/src/containers/App/index.tsx b/src/containers/App/index.tsx index bf5c7f6..0a185d2 100644 --- a/src/containers/App/index.tsx +++ b/src/containers/App/index.tsx @@ -1,6 +1,6 @@ import { Layout, Providers } from ".."; -import "./style.css"; +import "./style.scss"; function App() { return ( diff --git a/src/containers/App/style.css b/src/containers/App/style.css deleted file mode 100644 index 4931613..0000000 --- a/src/containers/App/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.App { - text-align: center; -} diff --git a/src/containers/App/style.scss b/src/containers/App/style.scss new file mode 100644 index 0000000..6272068 --- /dev/null +++ b/src/containers/App/style.scss @@ -0,0 +1,15 @@ +@import "sass/index"; + +body { + color: $main-functional-color; +} + +.App { + text-align: center; +} + +p { + font-size: 1.4rem; + margin: 0; + color: inherit; +} diff --git a/src/containers/Layout/index.tsx b/src/containers/Layout/index.tsx index f593fd9..8e69e9a 100644 --- a/src/containers/Layout/index.tsx +++ b/src/containers/Layout/index.tsx @@ -1,19 +1,22 @@ import { FC, useState } from "react"; +import clsx from "clsx"; + import { Header, SideBar } from "sections"; import { Page } from ".."; +import css from "./style.module.scss"; export const Layout: FC = () => { const [isSideBarOpen, setIsSideBarOpen] = useState(false); return ( -
+
-
- +
+
diff --git a/src/containers/Layout/style.module.scss b/src/containers/Layout/style.module.scss new file mode 100644 index 0000000..73abeb7 --- /dev/null +++ b/src/containers/Layout/style.module.scss @@ -0,0 +1,8 @@ +@import "sass/index"; + +.root { +} + +.page { + display: flex; +} diff --git a/src/containers/Page/index.tsx b/src/containers/Page/index.tsx index f0c6de7..935e208 100644 --- a/src/containers/Page/index.tsx +++ b/src/containers/Page/index.tsx @@ -1,29 +1,24 @@ -import { - BrowserRouter as Router, - Switch, - Route, - Redirect, -} from "react-router-dom"; +import React, { FC } from "react"; +import { Switch, Route, Redirect } from "react-router-dom"; import { appPageList } from "configs/pages"; +import css from "./style.module.scss"; -export const Page = () => { +export const Page: FC = () => { return ( <> -
- - - {appPageList.map(({ component, path, redirect }) => { - if (component) { - return ; - } - if (redirect) { - return ; - } +
+ + {appPageList.map(({ component, path, redirect }) => { + if (component) { + return ; + } + if (redirect) { + return ; + } - return null; - })} - - + return null; + })} +
); diff --git a/src/containers/Page/style.module.scss b/src/containers/Page/style.module.scss new file mode 100644 index 0000000..88de80d --- /dev/null +++ b/src/containers/Page/style.module.scss @@ -0,0 +1,3 @@ +.root { + width: 100%; +} diff --git a/src/containers/Providers/index.tsx b/src/containers/Providers/index.tsx index 408bea1..66239a7 100644 --- a/src/containers/Providers/index.tsx +++ b/src/containers/Providers/index.tsx @@ -1,6 +1,15 @@ import React, { FC } from "react"; +import { BrowserRouter } from "react-router-dom"; + import { UserContextProvider } from "providers"; +import { NotesContextProvider } from "providers/NoteProvider"; export const Providers: FC = ({ children }) => { - return {children}; + return ( + + + {children} + + + ); }; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index d7a5d70..887cad6 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,2 @@ export { useListeners } from "./useListeners"; +export { useWindowSize } from "./useWindowSize"; diff --git a/src/hooks/useWindowSize.ts b/src/hooks/useWindowSize.ts new file mode 100644 index 0000000..92a0abf --- /dev/null +++ b/src/hooks/useWindowSize.ts @@ -0,0 +1,24 @@ +import React, { useEffect, useState } from "react"; +import { mobileScreenWidth } from "configs/general"; + +const getWindowSizes = () => ({ + width: window.innerWidth, + height: window.innerHeight, + isMobile: window.innerWidth < mobileScreenWidth, +}); + +export const useWindowSize = () => { + const [sizes, setSizes] = useState(getWindowSizes()); + + useEffect(() => { + const setWindowSizes = () => { + setSizes(getWindowSizes); + }; + window.addEventListener("resize", setWindowSizes); + return () => { + window.removeEventListener("resize", setWindowSizes); + }; + }, []); + + return sizes; +}; diff --git a/src/modules/firebase/controllers/dataBase.ts b/src/modules/firebase/controllers/dataBase.ts new file mode 100644 index 0000000..7f6e707 --- /dev/null +++ b/src/modules/firebase/controllers/dataBase.ts @@ -0,0 +1,27 @@ +import { collection, getDocs, doc, setDoc } from "@firebase/firestore"; +import { dataBase, FireBaseListProps } from "modules"; +import { NoteElement } from "providers"; +import { createGuard } from "utils"; + +const noteElementGuardian = createGuard("list"); + +export const getNotesFromCloud = async ( + userId: string, + middleware: (data: NoteElement[]) => void +) => { + const firebaseNoteData = await getDocs(collection(dataBase, userId)); + + firebaseNoteData.forEach((v) => { + const notes = v.data(); + + if (noteElementGuardian(notes)) { + middleware(notes.list); + } + }); +}; + +export const addNotesToCloud = async (userId: string, notes: NoteElement[]) => { + setDoc(doc(dataBase, userId, "notes"), { + list: notes, + }); +}; diff --git a/src/modules/firebase/index.ts b/src/modules/firebase/index.ts index e312d35..6b74190 100644 --- a/src/modules/firebase/index.ts +++ b/src/modules/firebase/index.ts @@ -1,6 +1,7 @@ import { initializeApp } from "firebase/app"; import { getAnalytics } from "firebase/analytics"; import { getAuth, GoogleAuthProvider } from "firebase/auth"; +import { getFirestore } from "@firebase/firestore"; const firebaseConfig = { apiKey: "AIzaSyDqN77ygdYm-9SuwTLC0xddMCuh8dI1EPs", @@ -17,3 +18,4 @@ export const firebaseAnalytics = getAnalytics(firebaseApp); export const googleAuthProvider = new GoogleAuthProvider(); export const firebaseAuth = getAuth(firebaseApp); +export const dataBase = getFirestore(); diff --git a/src/modules/firebase/types.ts b/src/modules/firebase/types.ts index 1b069d8..cc3e62a 100644 --- a/src/modules/firebase/types.ts +++ b/src/modules/firebase/types.ts @@ -1 +1,6 @@ +import { NoteElement } from "providers"; + export type AuthStatus = "success" | "error"; +export interface FireBaseListProps { + list: NoteElement[]; +} diff --git a/src/modules/index.ts b/src/modules/index.ts new file mode 100644 index 0000000..5625ee0 --- /dev/null +++ b/src/modules/index.ts @@ -0,0 +1,4 @@ +export * from "./firebase"; +export * from "./firebase/types"; +export * from "./firebase/controllers/auth"; +export * from "./firebase/controllers/dataBase"; diff --git a/src/pages/ErrorPage/index.tsx b/src/pages/ErrorPage/index.tsx index cdc9c8a..ecb7ddb 100644 --- a/src/pages/ErrorPage/index.tsx +++ b/src/pages/ErrorPage/index.tsx @@ -1,3 +1,5 @@ -export const ErrorPage = () => { +import { FC } from "react"; + +export const ErrorPage: FC = () => { return
404 page
; }; diff --git a/src/pages/Main/index.tsx b/src/pages/Main/index.tsx index 089270f..526ca20 100644 --- a/src/pages/Main/index.tsx +++ b/src/pages/Main/index.tsx @@ -1,3 +1,5 @@ -export const Main = () => { +import { FC } from "react"; + +export const Main: FC = () => { return
Main page
; }; diff --git a/src/pages/Notes/components/Cards/components/Creator/components/ColorSelect/index.tsx b/src/pages/Notes/components/Cards/components/Creator/components/ColorSelect/index.tsx new file mode 100644 index 0000000..0f3d33b --- /dev/null +++ b/src/pages/Notes/components/Cards/components/Creator/components/ColorSelect/index.tsx @@ -0,0 +1,48 @@ +import React, { DetailedHTMLProps, FC } from "react"; + +import { Color } from "components"; +import { DefaultCardColors } from "components/Card"; + +import css from "./style.module.scss"; + +interface Props + extends Omit< + DetailedHTMLProps, HTMLDivElement>, + "onChange" + > { + onChange: (v: string) => void; +} +interface ColorProps + extends DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + > { + color: string; +} + +const DefaultColor: FC = ({ color, ...props }) => ( +
+); + +const ColorSelect: FC = ({ onChange, ...props }) => { + return ( +
+
+

Select Color:

+
+ {Object.values(DefaultCardColors).map((color) => { + return ( + onChange(color)} /> + ); + })} +
+
+
+

Or create custom:

+ +
+
+ ); +}; + +export default ColorSelect; diff --git a/src/pages/Notes/components/Cards/components/Creator/components/ColorSelect/style.module.scss b/src/pages/Notes/components/Cards/components/Creator/components/ColorSelect/style.module.scss new file mode 100644 index 0000000..29eda19 --- /dev/null +++ b/src/pages/Notes/components/Cards/components/Creator/components/ColorSelect/style.module.scss @@ -0,0 +1,22 @@ +.root { +} + +.box { + margin-top: 10px; +} + +.text { + margin-bottom: 3px; +} + +.defaultColorsList { + display: flex; +} + +.defaultColor { + width: 20px; + height: 20px; + cursor: pointer; + border-radius: 50%; + margin: 2px; +} diff --git a/src/pages/Notes/components/Cards/components/Creator/index.tsx b/src/pages/Notes/components/Cards/components/Creator/index.tsx new file mode 100644 index 0000000..8c68e12 --- /dev/null +++ b/src/pages/Notes/components/Cards/components/Creator/index.tsx @@ -0,0 +1,108 @@ +import React, { ChangeEvent, FC, useState } from "react"; + +import { NoteElement, Notes, TabValue, useNotes } from "providers"; +import { Button, Card, Input } from "components"; +import { useWindowSize } from "hooks"; + +import closeURL from "assets/icons/close.svg"; + +import ColorSelect from "./components/ColorSelect"; +import css from "./style.module.scss"; + +interface Props { + onClose: () => void; + activeTab: TabValue; +} + +export const Creator: FC = ({ onClose, activeTab }) => { + const { addNote } = useNotes(); + const [cardData, setCardData] = useState({ + name: "", + title: "", + text: "", + }); + + const handleChangeName = (ev: ChangeEvent) => { + setCardData({ + ...cardData, + name: ev.target.value, + }); + }; + + const handleChangeTitle = (ev: ChangeEvent) => { + setCardData({ + ...cardData, + title: ev.target.value, + }); + }; + + const handleChangeText = (ev: ChangeEvent) => { + setCardData({ + ...cardData, + text: ev.target.value, + }); + }; + + const handleChangeColor = (value: string) => { + setCardData({ + ...cardData, + color: value, + }); + }; + + const handleCreateNote = (event: React.FormEvent) => { + event.preventDefault(); + addNote(cardData, activeTab); + setTimeout(() => { + onClose(); + }, 0); + }; + + const { isMobile } = useWindowSize(); + + return ( +
+ Close +
+ + + + + + + + +
+ ); +}; diff --git a/src/pages/Notes/components/Cards/components/Creator/style.module.scss b/src/pages/Notes/components/Cards/components/Creator/style.module.scss new file mode 100644 index 0000000..ac5ad8c --- /dev/null +++ b/src/pages/Notes/components/Cards/components/Creator/style.module.scss @@ -0,0 +1,87 @@ +@import "sass/index"; + +$padding: 5px; +$closeIconSize: 25px; + +.root { + background: white; + width: $mobile-screen-width; + padding: $padding; + + display: flex; + justify-content: space-between; + align-items: flex-start; + + min-width: 300px; + + & > :last-child { + min-width: 290px; + margin: 0; + } +} + +.control { + width: 100%; + margin-right: $padding; + padding-top: $padding + $closeIconSize; + + & > :nth-child(n) { + margin-top: 10px; + } +} + +.textLabel { +} + +.colorSelect { +} + +.card { + margin-top: $padding + $closeIconSize !important; +} + +.closeIcon { + width: $closeIconSize; + height: $closeIconSize; + + position: absolute; + right: 0; + z-index: 2; +} + +@media (max-width: $mobile-screen-width) { + .card { + position: absolute; + max-height: 200px; + + color: transparent; + } + + .colorSelect { + margin-top: 50px !important; + } + + .textLabel { + display: none; + } + + .control { + width: 300px; + margin: 0 auto; + padding: 10px; + padding-top: $padding * 2 + $closeIconSize; + z-index: 1; + + & > :first-child { + margin-top: 0; + margin-bottom: 40px; + } + } + + .root { + height: 100%; + width: 100%; + + justify-content: center; + } +} diff --git a/src/pages/Notes/components/Cards/components/index.ts b/src/pages/Notes/components/Cards/components/index.ts new file mode 100644 index 0000000..369db48 --- /dev/null +++ b/src/pages/Notes/components/Cards/components/index.ts @@ -0,0 +1 @@ +export { Creator } from "./Creator"; diff --git a/src/pages/Notes/components/Cards/index.tsx b/src/pages/Notes/components/Cards/index.tsx new file mode 100644 index 0000000..f9e434d --- /dev/null +++ b/src/pages/Notes/components/Cards/index.tsx @@ -0,0 +1,52 @@ +import React, { FC, useRef, useState } from "react"; + +import { TabValue, useNotes } from "providers"; +import { Card } from "components"; +import { Modal } from "pop-ups"; + +import addIconUrl from "assets/icons/add.svg"; +import { Creator } from "./components"; +import css from "./style.module.scss"; + +interface Props { + activeTab: TabValue; +} + +export const Cards: FC = ({ activeTab }) => { + const { notes, setNotes } = useNotes(); + const [creatorIsOpen, setCreatorIsOpen] = useState(false); + const addButtonRef = useRef(null); + + const closeCreator = () => setCreatorIsOpen(false); + const openCreator = () => setCreatorIsOpen(true); + + const deleteCard = (iterator: number) => { + if (iterator <= notes[activeTab].length) { + notes[activeTab].splice(iterator, 1); + } + + setNotes({ ...notes }); + }; + + return ( +
+ + + + + + +
+ {notes[activeTab].map((note, i) => { + return deleteCard(i)} />; + })} +
+
+ ); +}; diff --git a/src/pages/Notes/components/Cards/style.module.scss b/src/pages/Notes/components/Cards/style.module.scss new file mode 100644 index 0000000..4131350 --- /dev/null +++ b/src/pages/Notes/components/Cards/style.module.scss @@ -0,0 +1,20 @@ +$img-size: 30px; + +.root { +} +.addButton { + background: transparent; + border: none; + display: block; + cursor: pointer; + + &__img { + width: $img-size; + height: $img-size; + } +} + +.cardWrapper { + display: flex; + flex-wrap: wrap; +} diff --git a/src/pages/Notes/components/Tabs/index.tsx b/src/pages/Notes/components/Tabs/index.tsx new file mode 100644 index 0000000..93b87f2 --- /dev/null +++ b/src/pages/Notes/components/Tabs/index.tsx @@ -0,0 +1,36 @@ +import React, { FC } from "react"; +import clsx from "clsx"; + +import { Button } from "components"; +import { useUser } from "providers"; + +import css from "./style.module.scss"; + +interface Props { + tabList: string[]; + activeTab: string; + changeActiveTab: (value: string) => void; +} + +export const Tabs: FC = ({ tabList, activeTab, changeActiveTab }) => { + const { user } = useUser(); + return ( +
+ {tabList.map((item, i) => { + if (!user && i === 1) { + return null; + } + + return ( + + ); + })} +
+ ); +}; diff --git a/src/pages/Notes/components/Tabs/style.module.scss b/src/pages/Notes/components/Tabs/style.module.scss new file mode 100644 index 0000000..de48bd0 --- /dev/null +++ b/src/pages/Notes/components/Tabs/style.module.scss @@ -0,0 +1,11 @@ +.root { + width: 100%; + padding: 5px; + display: flex; + justify-content: flex-end; + flex-wrap: wrap; +} + +.tab { + margin: 0 5px; +} diff --git a/src/pages/Notes/components/index.ts b/src/pages/Notes/components/index.ts new file mode 100644 index 0000000..434706b --- /dev/null +++ b/src/pages/Notes/components/index.ts @@ -0,0 +1,2 @@ +export { Tabs } from "./Tabs"; +export { Cards } from "./Cards"; diff --git a/src/pages/Notes/index.tsx b/src/pages/Notes/index.tsx index 670cbd3..3f6460e 100644 --- a/src/pages/Notes/index.tsx +++ b/src/pages/Notes/index.tsx @@ -1,5 +1,22 @@ -import React from "react"; +import { useUser } from "providers"; +import React, { FC, useState } from "react"; -export const Notes = () => { - return
Notes PAge
; +import { Cards, Tabs } from "./components"; + +export const tabList = ["Local", "Cloud"]; + +export const Notes: FC = () => { + const [activeTab, setActiveTab] = useState(tabList[0]); + const { user } = useUser(); + const changeActiveTab = (value: string) => { + const activeValue = user ? value : "Local"; + setActiveTab(activeValue); + }; + + return ( +
+ + +
+ ); }; diff --git a/src/pop-ups/Modal/index.tsx b/src/pop-ups/Modal/index.tsx new file mode 100644 index 0000000..1a9e2c6 --- /dev/null +++ b/src/pop-ups/Modal/index.tsx @@ -0,0 +1,31 @@ +import React, { FC } from "react"; + +import { PopUpFrameBackground, PopUpWrapper } from "pop-ups"; + +import css from "./style.module.scss"; + +interface Props { + isOpen: boolean; + onClose: () => void; + background: PopUpFrameBackground; +} + +export const Modal: FC = ({ children, background, isOpen, onClose }) => { + if (!isOpen) { + return null; + } + return ( + + {children} + + ); +}; diff --git a/src/pop-ups/Modal/style.module.scss b/src/pop-ups/Modal/style.module.scss new file mode 100644 index 0000000..f73da87 --- /dev/null +++ b/src/pop-ups/Modal/style.module.scss @@ -0,0 +1,18 @@ +@import "sass/index"; + +.root { + display: flex; + justify-content: center; + align-items: center; + + position: fixed; + top: 0; + left: 0; +} + +.paper { + @media (max-width: $mobile-screen-width) { + width: 100vw; + height: 100vh; + } +} diff --git a/src/pop-ups/PopUpMenu/index.tsx b/src/pop-ups/PopUpMenu/index.tsx index d06f0d5..5fc8b4b 100644 --- a/src/pop-ups/PopUpMenu/index.tsx +++ b/src/pop-ups/PopUpMenu/index.tsx @@ -1,21 +1,30 @@ import clsx from "clsx"; -import React, { FC, useEffect, useMemo, useRef, useState } from "react"; -import { PopUpWrapper } from "pop-ups"; +import React, { FC, useEffect, useMemo, useState } from "react"; +import { PopUpFrameBackground, PopUpWrapper } from "pop-ups"; import { createGuard } from "utils"; + import css from "./style.module.scss"; interface Props { anchor?: HTMLElement | Element | null; isOpen: boolean; onClose: () => void; + background?: PopUpFrameBackground; } -export const PopUpMenu: FC = ({ children, anchor, isOpen, onClose }) => { +export const PopUpMenu: FC = ({ + children, + anchor, + isOpen, + onClose, + background = "transparent", +}) => { const [rootElement, setRootElement] = useState(null); const [isClosed, setIsClosed] = useState(true); const menuPosition = useMemo(() => { const elementGuard = createGuard("offsetLeft"); + if (anchor && elementGuard(anchor) && rootElement) { let top = anchor.offsetTop + anchor.offsetHeight; let left = anchor.offsetLeft + anchor.offsetWidth / 2; @@ -67,7 +76,7 @@ export const PopUpMenu: FC = ({ children, anchor, isOpen, onClose }) => { } return ( - +
setRootElement(node)} diff --git a/src/pop-ups/PopUpWrapper/index.tsx b/src/pop-ups/PopUpWrapper/index.tsx index d327799..816f61f 100644 --- a/src/pop-ups/PopUpWrapper/index.tsx +++ b/src/pop-ups/PopUpWrapper/index.tsx @@ -1,36 +1,37 @@ import React, { FC, MouseEvent, useCallback } from "react"; import { createPortal } from "react-dom"; import clsx from "clsx"; + +import { PopUpWrapperProps } from "pop-ups"; + import css from "./style.module.scss"; -interface Props { - background?: "transparent" | "dark"; - frameOnClick?: () => void; -} +const rootDom = document.getElementById("pop-up"); -export const PopUpWrapper: FC = ({ +export const PopUpWrapper: FC = ({ children, background = "transparent", frameOnClick, + classes, }) => { - const rootDom = document.querySelector("#pop-up"); const stopPropagation = useCallback((event: MouseEvent) => { event.stopPropagation(); }, []); if (rootDom) { return createPortal( -
+
-
{children}
+
{children}
, rootDom ); diff --git a/src/pop-ups/index.ts b/src/pop-ups/index.ts index 768c2ee..52442fd 100644 --- a/src/pop-ups/index.ts +++ b/src/pop-ups/index.ts @@ -1,2 +1,5 @@ export { PopUpWrapper } from "./PopUpWrapper"; export { PopUpMenu } from "./PopUpMenu"; +export { Modal } from "./Modal"; + +export * from "./types"; diff --git a/src/pop-ups/types.ts b/src/pop-ups/types.ts new file mode 100644 index 0000000..c74b8df --- /dev/null +++ b/src/pop-ups/types.ts @@ -0,0 +1,12 @@ +export type PopUpFrameBackground = "transparent" | "dark"; + +export interface PopUpWrapperProps { + background?: PopUpFrameBackground; + frameOnClick?: () => void; + + classes?: { + root?: string; + frame?: string; + paper?: string; + }; +} diff --git a/src/providers/NoteProvider/consts.ts b/src/providers/NoteProvider/consts.ts new file mode 100644 index 0000000..d856a56 --- /dev/null +++ b/src/providers/NoteProvider/consts.ts @@ -0,0 +1 @@ +export const LOCAL_NOTES_NAME = "localNotes"; diff --git a/src/providers/NoteProvider/context.ts b/src/providers/NoteProvider/context.ts new file mode 100644 index 0000000..f78bd6b --- /dev/null +++ b/src/providers/NoteProvider/context.ts @@ -0,0 +1,13 @@ +import { createContext } from "react"; +import { NoteContext } from "./types"; + +const defaultContext: NoteContext = { + notes: { + cloud: [], + local: [], + isDataReceived: false, + }, + setNotes: () => null, + addNote: () => null, +}; +export const NotesContext = createContext(defaultContext); diff --git a/src/providers/NoteProvider/index.tsx b/src/providers/NoteProvider/index.tsx new file mode 100644 index 0000000..840ed88 --- /dev/null +++ b/src/providers/NoteProvider/index.tsx @@ -0,0 +1,68 @@ +import { FC, useContext, useEffect, useState } from "react"; + +import { TabValue, useUser } from "providers"; +import { addNotesToCloud, getNotesFromCloud } from "modules"; + +import { NotesContext } from "./context"; +import { Notes, NoteElement } from "./types"; +import { LOCAL_NOTES_NAME } from "./consts"; + +export const NotesContextProvider: FC = ({ children }) => { + const { user } = useUser(); + const [notes, setNotes] = useState({ + cloud: [], + local: JSON.parse( + localStorage.getItem(LOCAL_NOTES_NAME) || "[]" + ) as NoteElement[], + + isDataReceived: false, + }); + + const addNote = (value: NoteElement, place: TabValue) => { + setNotes({ + ...notes, + [place]: [...notes[place], value], + }); + }; + + useEffect(() => { + if (user) { + getNotesFromCloud(user.uid, (cloud) => { + setNotes({ + ...notes, + cloud, + isDataReceived: true, + }); + }); + } else { + setNotes({ + ...notes, + cloud: [], + isDataReceived: false, + }); + } + }, [user]); + + useEffect(() => { + const { cloud, local, isDataReceived } = notes; + localStorage.setItem(LOCAL_NOTES_NAME, JSON.stringify(local)); + + if (user && isDataReceived) { + addNotesToCloud(user.uid, cloud); + } + }, [notes]); + + return ( + + {children} + + ); +}; + +export const useNotes = () => useContext(NotesContext); diff --git a/src/providers/NoteProvider/types.ts b/src/providers/NoteProvider/types.ts new file mode 100644 index 0000000..c4eb39d --- /dev/null +++ b/src/providers/NoteProvider/types.ts @@ -0,0 +1,19 @@ +export type TabValue = "local" | "cloud"; +export interface NoteContext { + notes: Notes; + setNotes: React.Dispatch>; + addNote: (value: NoteElement, place: TabValue) => void; +} + +export interface NoteElement { + name: string; + title: string; + text: string; + color?: string; +} + +export interface Notes { + local: NoteElement[]; + cloud: NoteElement[]; + isDataReceived: boolean; +} diff --git a/src/providers/UserProvider/index.tsx b/src/providers/UserProvider/index.tsx index e63d1af..a6c9383 100644 --- a/src/providers/UserProvider/index.tsx +++ b/src/providers/UserProvider/index.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback, useContext, useEffect, useState } from "react"; +import { FC, useContext, useEffect, useState } from "react"; import { User } from "@firebase/auth"; import { @@ -12,13 +12,14 @@ import { UserContext } from "./context"; export const UserContextProvider: FC = ({ children }) => { const [user, setUser] = useState(null); - const checkAuth = useCallback(() => { + console.log("🚀 ~ file: index.tsx ~ line 15 ~ user", user); + const checkAuth = () => { getAuthResult((user) => setUser(user)); - }, []); + }; const logIn = () => { fireBaseAuth((user) => setUser(user)); }; - const logOut = useCallback(() => { + const logOut = () => { const deleteUser = () => setUser(null); const getMessage = (type: AuthStatus) => { console.log("log out status:", type); @@ -28,11 +29,12 @@ export const UserContextProvider: FC = ({ children }) => { }; getLogOut(deleteUser, getMessage); - }, []); + }; useEffect(() => { checkAuth(); }, []); + return ( = ({ anchor, isOpen, onClose }) => { > {user && (
-
- user -

{user?.displayName}

-
+
= ({ isSideBarOpen, setIsSideBarOpen }) => { return ( -
+
{ - return
; +import { FC } from "react"; + +import { SideBar as SideBarWrapper, SideBarItem, UserName } from "components"; + +import bookUrl from "assets/icons/book.svg"; +import markUrl from "assets/icons/mark.svg"; + +import css from "./style.module.scss"; + +const navigation = [ + // { + // path: "/main", + // name: "Main", + // img: markUrl, + // }, + { + path: "/notes", + name: "Notes", + img: bookUrl, + }, +]; + +interface Props { + disabled: boolean; +} + +export const SideBar: FC = ({ disabled }) => { + return ( + + + {navigation.map(({ name, path, img }) => ( + + ))} + + ); }; diff --git a/src/sections/SideBar/style.module.scss b/src/sections/SideBar/style.module.scss new file mode 100644 index 0000000..c7b8726 --- /dev/null +++ b/src/sections/SideBar/style.module.scss @@ -0,0 +1,6 @@ +@import "sass/index"; + +.user { + border-bottom: $line; + padding: 5px; +} diff --git a/src/utils/createGuard.ts b/src/utils/createGuard.ts index 62eece1..42ed50a 100644 --- a/src/utils/createGuard.ts +++ b/src/utils/createGuard.ts @@ -5,6 +5,8 @@ export function createGuard(checkedKey: string) { } //@ts-ignore - return !!(value as Type)[checkedKey]; + const result = (value as Type)[checkedKey]; + + return !!result || result === 0; }; } diff --git a/tsconfig.json b/tsconfig.json index e7eb2f5..bf42a23 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,11 +2,7 @@ "compilerOptions": { "target": "es5", "baseUrl": "./src", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -21,7 +17,5 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": [ - "./src" - ] + "include": ["./src"] }