├── .firebase
└── hosting.YnVpbGQ.cache
├── .firebaserc
├── .gitignore
├── README.md
├── firebase.json
├── index.html
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── App.js
├── App.test.js
├── UI
└── theme.js
├── components
├── AuthForm
│ ├── AuthForm.js
│ ├── LogInForm.js
│ ├── RegisterForm.js
│ ├── SocialsAuth.js
│ └── common-elements.js
├── ColorPicker
│ ├── ColorPicker.js
│ └── ColorPickerElements.js
├── EditNoteModal
│ └── EditNoteModal.js
├── Footer
│ └── Footer.js
├── FormNoteList
│ └── FormNoteList.js
├── Navigation
│ ├── Navigation.js
│ └── navigation-elements.js
├── NoteForm
│ ├── NoteForm.js
│ ├── NoteFormElements.js
│ └── NotesFormFooter.js
├── NotesList
│ ├── Checkbox.js
│ ├── Column.js
│ ├── Note.js
│ ├── NoteDraggable.js
│ ├── NotesList.js
│ ├── NotesListMobile.js
│ └── notes-list-elements.js
├── TagList
│ └── TagList.js
├── TagWidget
│ ├── AvaiableTags.js
│ ├── TagWidget.js
│ ├── scrollbar.css
│ └── widget-elements.js
└── index.js
├── containers
├── About
│ └── About.js
├── Home
│ └── Home.js
├── Notes
│ └── Notes.js
└── index.js
├── firebase
├── firebaseAPI.js
├── firebaseAuth.js
└── firebaseConfig.js
├── images
├── font
│ ├── fontello.eot
│ ├── fontello.svg
│ ├── fontello.ttf
│ ├── fontello.woff
│ └── fontello.woff2
├── fontello.css
├── pin-in-diagonal-position.png
├── pin-outline.png
└── pin1.png
├── index.css
├── index.js
├── redux
├── auth.js
├── notes.js
└── storeConfig.js
└── utils.js
/.firebase/hosting.YnVpbGQ.cache:
--------------------------------------------------------------------------------
1 | asset-manifest.json,1580922057256,00b0bc8d88e309b531fd9135fa9357b6e55fb6157fa77849c84575fa6540e52a
2 | favicon.ico,1580922032103,c599b7a91ab3627e3538125d9f40adc2d4bf949046984262670545dc7738af06
3 | manifest.json,1580922032103,ed7b98046be74c7d1476b73ebb8cd1f00c2b34443af1525af60b9dd20be9fb9a
4 | index.html,1580922057256,c9556727fa7adfabae822b5f1fce123909621f7b3fcdc282a7c57ea4ffe46260
5 | precache-manifest.ae074338b0131aa7491db94e42af2232.js,1580922057256,43cf2d71eac32bcc0ade2e179cd6c1d6fc7111a0cb2bad2678e96c53fb7be6ff
6 | service-worker.js,1580922057256,5d247069f58027a702d78ea9349bdb19ce6ad114afc654abcc566d7c65e63877
7 | static/css/2.764ccc25.chunk.css,1580922057256,96e30f5cf73f33d636fc1cec624c3fe11e4ad23124b6f13a48d5542cc5f57b01
8 | static/css/main.df013226.chunk.css,1580922057223,8cc8ae397a78b0c18b86be203c061347c929916575f82551032ec2f730d237d8
9 | static/css/main.df013226.chunk.css.map,1580922057256,10e71639dce394863a8010200ede70ba5c215e48d44427eef48932cd3116942a
10 | static/css/2.764ccc25.chunk.css.map,1580922057256,a0a06ff18c092ffe84ad302460cb4de9ad30a51f617f0449b9a108b8a49b8b47
11 | static/js/main.403dbbfb.chunk.js,1580922057256,c86629d0c7d3958ef14545399ae2b99b532203d9a010350d27580e0b15aaa500
12 | static/js/runtime~main.a8a9905a.js,1580922057227,5b0313db8c475761662a933e703f2a6bd16847cdfc34b81915f5dd56862e4e77
13 | static/js/runtime~main.a8a9905a.js.map,1580922057256,2510643041ce395196dfc3f9ae31cd72d7127dbd8457479959c6e22dd1b1eaeb
14 | static/media/fontello.7f49b01e.ttf,1580922057256,6332c330e23d12bf3882036b26b48e194fcc4f0d8713725b2fadbbb9b0320815
15 | static/media/fontello.48370e24.woff2,1580922057252,f8e2331421403a673560e48b7dae9fd1a412d2c9c88f0e54d103ee00ba64ce94
16 | static/media/fontello.806abce4.eot,1580922057223,b85552f23c8af5ee8c9af428fa6fa1133e252271487c588eacb8db46fa073e9b
17 | static/media/fontello.e7c6ec07.svg,1580922057256,af9d78dff27f634a3f88d3757f843739c2bec99aec0652a2eca7fa92eee581a9
18 | static/media/fontello.f31b8162.woff,1580922057256,053a9b3a9acbff062db2fed9b39334e418bbaf5c24d86dbdd2f4ddfd7f202870
19 | static/js/main.403dbbfb.chunk.js.map,1580922057256,d44d870c83bb511975d53e8eb2f5f1ff53783557d27453684bc5f629ec63072c
20 | static/js/2.286c880b.chunk.js,1580922057256,55481ab00e5c666abcbeec4a9420bc5735df694b8c135ac85aa4170588342b86
21 | static/js/2.286c880b.chunk.js.map,1580922057256,7646bfd2d0393bb36c31b85197974e5c64626fa5961c0082ce17fb5b8cd9553e
22 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "keep-clone-app"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | .env
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.codacy.com/app/simon125/google-keep-clone?utm_source=github.com&utm_medium=referral&utm_content=simon125/google-keep-clone&utm_campaign=Badge_Grade)
2 | 
3 |
4 | # Google keep clone
5 |
6 |
7 | ### Google keep clone is a clone of well known service from google for notes/tasks and much more.
8 |
9 | By doing it, I was trying to clone the UI almost in 100%(from. list of notes, panel for searching) without bunch of features,
10 | Project for me is kind of challange, sandbox for new getting know new features/packages/compoennts/libraries, and playground for getting know good practices and approaches in react which I'm looking for in docs/medium/tutorials.
11 |
12 | ### Live demo
13 |
14 | You can easily check the current version [here](https://keep-clone-app.firebaseapp.com/)
15 |
16 |
17 | ## Tech
18 |
19 | - React (with hooks^^)
20 | - Redux
21 | - Formik for validation form
22 | - Firebase (firestore, auth)
23 | - Styled Components
24 | - npm, bunch of components, drag and drop library (react-beautiful-dnd)
25 |
26 | - trello for tracking progress and keep order
27 |
28 |
29 | ## TODO
30 | - set rules in firebase
31 | - filtering/searching
32 | - Refactor if new side project won't consume my whole attention
33 | When I'm using this app things which need improvment appear,
34 | - one more time !!!filtering/searching!!!
35 | - possible to delete the tag from DB
36 | - improve edit mode in desktop
37 | - improve edit mode in mobile
38 | - displaying in center tools to note in mobile version
39 |
40 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "rewrites": [
10 | {
11 | "source": "**",
12 | "destination": "/index.html"
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Welcome to Firebase Hosting
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
32 |
33 |
34 |
35 |
Welcome
36 |
Firebase Hosting Setup Complete
37 |
You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!
38 |
Open Hosting Documentation
39 |
40 | Firebase SDK Loading…
41 |
42 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "note-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "clone-deep": "^4.0.1",
7 | "deep-equal": "^1.1.0",
8 | "firebase": "^6.1.1",
9 | "formik": "^1.5.7",
10 | "normalize.css": "^8.0.1",
11 | "prop-types": "^15.7.2",
12 | "react": "^16.8.6",
13 | "react-autosize-textarea": "^7.0.0",
14 | "react-beautiful-dnd": "^11.0.4",
15 | "react-dom": "^16.8.6",
16 | "react-grid-layout": "^0.16.6",
17 | "react-loader-spinner": "^3.1.5",
18 | "react-masonry-component": "^6.2.1",
19 | "react-redux": "^7.0.3",
20 | "react-router-dom": "^5.0.1",
21 | "react-scripts": "3.0.1",
22 | "react-scroll-up-button": "^1.6.4",
23 | "redux": "^4.0.1",
24 | "redux-thunk": "^2.3.0",
25 | "styled-components": "^4.3.1",
26 | "uuid": "^3.3.2",
27 | "yup": "^0.27.0"
28 | },
29 | "scripts": {
30 | "start": "react-scripts start",
31 | "build": "react-scripts build",
32 | "test": "react-scripts test",
33 | "eject": "react-scripts eject"
34 | },
35 | "eslintConfig": {
36 | "extends": "react-app"
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simon125/google-keep-clone/744b52c5b2a839a8612a0d6248f77881c66c4ec4/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 |
27 |
28 |
29 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Navigation, Footer } from './components';
3 | import { Home, Notes, About } from './containers';
4 | import { AppContainer } from './UI/theme';
5 | import { Provider } from 'react-redux';
6 | import { store } from './redux/storeConfig';
7 | import { Route, BrowserRouter as Router, Switch } from 'react-router-dom';
8 | function App() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/UI/theme.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | export const AppContainer = styled.div`
3 | min-height: 100%;
4 | background-color: #fcfcfc;
5 | `;
6 | export const Card = styled.div`
7 | padding: 15px;
8 | margin: 5px;
9 | border-radius: 3px;
10 | box-sizing: border-box;
11 | box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2),
12 | 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 2px 1px -1px rgba(0, 0, 0, 0.12);
13 | background-color: #fff;
14 | @media (max-width: 500px) {
15 | margin: 5px 0;
16 | }
17 | `;
18 | export const Icon = styled.span`
19 | color: ${(props) => (props.color ? props.color : '#fff')};
20 | `;
21 |
--------------------------------------------------------------------------------
/src/components/AuthForm/AuthForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import LogInForm from "./LogInForm";
3 | import RegisterForm from "./RegisterForm";
4 | import SocialsAuth from "./SocialsAuth";
5 | import { AuthContainer } from "./common-elements";
6 |
7 | function AuthForm() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | export default AuthForm;
18 |
--------------------------------------------------------------------------------
/src/components/AuthForm/LogInForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { withRouter } from 'react-router-dom';
3 | import { connect } from 'react-redux';
4 | import styled from 'styled-components';
5 | import { Formik, Form } from 'formik';
6 | import * as Yup from 'yup';
7 | import {
8 | InputField,
9 | SubmitButton,
10 | FormHeader,
11 | FormContainer,
12 | Label,
13 | RememberMeLabel,
14 | InputErrMsg
15 | } from './common-elements';
16 | import { Card } from '../../UI/theme';
17 | import { signInWithEmailAndPassword } from '../../firebase/firebaseAuth';
18 |
19 | const RememberMeSection = styled.div`
20 | margin-top: 10px;
21 | display: flex;
22 | align-items: center;
23 | `;
24 | const Checkbox = styled.input`
25 | margin: 0 3px;
26 | background: transparent;
27 | `;
28 | function LogInForm(props) {
29 | const [signInState] = useState({
30 | loginEmail: '',
31 | loginPassword: ''
32 | });
33 |
34 | return (
35 |
36 | Login by email
37 | {
51 | signInWithEmailAndPassword(loginEmail, loginPassword)
52 | .then(() => {
53 | resetForm();
54 | setTimeout(() => {
55 | props.history.push('/notes');
56 | }, 500);
57 | //TODO SHOW TOAST
58 | })
59 | .catch((error) => {
60 | //TODO check if there is possible to get multiply of errors message
61 | const errorCode = error.code;
62 | if (errorCode === 'auth/invalid-email') {
63 | setErrors({ loginEmail: 'Invalid Email!' });
64 | } else if (errorCode === 'auth/user-disabled') {
65 | setErrors({ loginEmail: 'User is disabled!' });
66 | } else if (errorCode === 'auth/user-not-found') {
67 | setErrors({ loginEmail: 'User is not found!' });
68 | } else if (errorCode === 'auth/wrong-password') {
69 | setErrors({
70 | loginPassword: 'Wrong password'
71 | });
72 | } else {
73 | setErrors({
74 | loginEmail: 'Invalid Email',
75 | loginPassword: 'Invalid password'
76 | });
77 | }
78 | });
79 | }}
80 | render={({ handleChange, errors, values, touched, handleBlur }) => (
81 |
123 | )}
124 | />
125 |
126 | );
127 | }
128 |
129 | const mapStateToProps = (state) => {
130 | return {
131 | isLoggedIn: state.auth.isLoggedIn
132 | };
133 | };
134 |
135 | export default withRouter(connect(mapStateToProps, {})(LogInForm));
136 |
--------------------------------------------------------------------------------
/src/components/AuthForm/RegisterForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Formik, Form } from 'formik';
3 | import { connect } from 'react-redux';
4 | import * as Yup from 'yup';
5 | import {
6 | InputField,
7 | SubmitButton,
8 | FormHeader,
9 | FormContainer,
10 | Label,
11 | InputErrMsg
12 | } from './common-elements';
13 | import { Card } from '../../UI/theme';
14 | import { createUserWithEmailAndPassword } from '../../firebase/firebaseAuth';
15 | import { withRouter } from 'react-router-dom';
16 |
17 | function RegisterForm(props) {
18 | const [signUpState] = useState({
19 | email: '',
20 | password: '',
21 | name: '',
22 | repeatedPassword: ''
23 | });
24 |
25 | return (
26 |
27 | Register by email
28 | {
47 | createUserWithEmailAndPassword(email, password)
48 | .then(() => {
49 | resetForm();
50 | //TODO SHOW TOAST
51 | setTimeout(() => {
52 | props.history.push('/notes');
53 | }, 500);
54 | })
55 | .catch((error) => {
56 | //TODO check if there is possible to get multiply of errors message
57 | const errorCode = error.code;
58 | if (errorCode === 'auth/weak-password') {
59 | setErrors({ password: 'Password is to weak!' });
60 | } else if (errorCode === 'auth/invalid-email') {
61 | setErrors({ email: 'Invalid Email' });
62 | } else if (errorCode === 'auth/email-already-in-use') {
63 | setErrors({ email: 'Email already in use!' });
64 | } else if (errorCode === 'auth/operation-not-allowed') {
65 | setErrors({
66 | email: 'Invalid Email',
67 | password: 'Invalid password'
68 | });
69 | } else {
70 | setErrors({
71 | email: 'Invalid Email',
72 | password: 'Invalid password'
73 | });
74 | }
75 | });
76 | }}
77 | render={({ handleChange, values, errors, touched, handleBlur }) => (
78 |
141 | )}
142 | />
143 |
144 | );
145 | }
146 |
147 | const mapStateToProps = (state) => {
148 | return {
149 | isLoggedIn: state.auth.isLoggedIn
150 | };
151 | };
152 |
153 | export default withRouter(connect(mapStateToProps, {})(RegisterForm));
154 |
--------------------------------------------------------------------------------
/src/components/AuthForm/SocialsAuth.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, Icon } from '../../UI/theme';
3 | import {
4 | SocialsContainer,
5 | FacebookBtn,
6 | GoogleBtn,
7 | FormHeader
8 | } from './common-elements';
9 | import {
10 | signInWithGoogle,
11 | signInWithFacebook
12 | } from '../../firebase/firebaseAuth';
13 |
14 | function SocialsAuth() {
15 | return (
16 |
17 | Continue with google account
18 |
19 | {/*
20 | Sing in with
21 | */}
22 |
23 | Sing in with
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export default SocialsAuth;
31 |
--------------------------------------------------------------------------------
/src/components/AuthForm/common-elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const InputField = styled.input`
4 | margin: 4px 5px 8px 5px;
5 | outline: none;
6 | border: ${({ isInvalid }) =>
7 | isInvalid ? 'solid 1px red' : '1px solid #bbb'};
8 | border-radius: 5px;
9 | padding-left: 15px;
10 | width: 200px;
11 | height: 30px;
12 | @media (max-width: 500px) {
13 | width: 90vw;
14 | margin: 5px 0;
15 | }
16 | `;
17 | export const InputErrMsg = styled.p`
18 | position: relative;
19 | margin-top: -7px;
20 | margin-bottom: -4px;
21 | padding-left: 5px;
22 | color: red;
23 | font-size: 10px;
24 | display: ${({ isInvalid }) => (isInvalid ? 'block' : 'none')};
25 | `;
26 |
27 | export const SubmitButton = styled.button`
28 | letter-spacing: 0.3px;
29 | margin: 5px;
30 | outline: none;
31 | width: 200px;
32 | height: 35px;
33 | border: none;
34 | background: #f6d622;
35 | border-radius: 5px;
36 | color: #fff;
37 | cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
38 | transition: all 0.3s;
39 | &:hover {
40 | background: ${(props) => (props.disabled ? '' : '#ffa500')};
41 | }
42 | @media (max-width: 500px) {
43 | width: 90vw;
44 | margin: 5px 0;
45 | }
46 | `;
47 |
48 | export const SocialsContainer = styled.div`
49 | margin: 5px;
50 | width: 450px;
51 | display: flex;
52 | justify-content: space-between;
53 | @media (max-width: 500px) {
54 | width: auto;
55 | flex-direction: column;
56 | margin: 5px 0;
57 | }
58 | `;
59 |
60 | export const FacebookBtn = styled.button`
61 | letter-spacing: 0.3px;
62 | background: #3b5998;
63 | border: none;
64 | outline: none;
65 | cursor: pointer;
66 | color: #fff;
67 | width: 48%;
68 | padding: 7px;
69 | border-radius: 5px;
70 | @media (max-width: 500px) {
71 | width: 90vw;
72 | margin-top: 5px;
73 | }
74 | `;
75 |
76 | export const GoogleBtn = styled.button`
77 | letter-spacing: 0.3px;
78 | background: #db3236;
79 | border: none;
80 | outline: none;
81 | cursor: pointer;
82 | color: #fff;
83 | width: 90%;
84 | margin: 0 auto;
85 | padding: 7px;
86 | border-radius: 5px;
87 | @media (max-width: 500px) {
88 | width: 90vw;
89 | margin-top: 5px;
90 | }
91 | `;
92 |
93 | export const AuthContainer = styled.div`
94 | width: 500px;
95 | display: flex;
96 | flex-wrap: wrap;
97 | justify-content: center;
98 | align-items: flex-start;
99 | @media (max-width: 500px) {
100 | flex-direction: column;
101 | align-items: center;
102 | width: 100%;
103 | }
104 | `;
105 | export const FormHeader = styled.h3`
106 | letter-spacing: 0.3px;
107 | text-align: center;
108 | margin: 3px 0 8px 0;
109 | color: #555;
110 | `;
111 | export const FormContainer = styled.div`
112 | display: flex;
113 | flex-direction: column;
114 | @media (max-width: 500px) {
115 | width: 100%;
116 | }
117 | `;
118 | export const Label = styled.label`
119 | letter-spacing: 0.3px;
120 | margin-top: 5px;
121 | color: #555;
122 | `;
123 | export const RememberMeLabel = styled(Label)`
124 | margin-top: 0;
125 | `;
126 |
--------------------------------------------------------------------------------
/src/components/ColorPicker/ColorPicker.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | ColorPickerContainer,
4 | Button,
5 | Icon,
6 | Pallette,
7 | Color
8 | } from './ColorPickerElements';
9 |
10 | function ColorPicker({ setBgColor, chosenColor = 'rgba(255,255,255,0.8)' }) {
11 | const [isPalleteOpen, setIsPalleteOpen] = useState(false);
12 | const [hideTime, setHideTime] = useState(null);
13 | const colors = [
14 | 'rgba(255,255,255,0.8)',
15 | '#f28b82',
16 | '#fbbc04',
17 | '#fff475',
18 | '#ccff90',
19 | '#a7ffeb',
20 | '#cbf0f8',
21 | '#aecbfa',
22 | '#d7aefb',
23 | '#fdcfe8',
24 | '#e6c9a8',
25 | '#e8eaed'
26 | ];
27 | return (
28 |
29 |
41 | {isPalleteOpen && (
42 | clearTimeout(hideTime)}
44 | onMouseLeave={() => setIsPalleteOpen(false)}
45 | >
46 | {colors.map((color, index) => (
47 | setBgColor(color)} key={index} color={color}>
48 | {chosenColor === color ? (
49 |
50 | ) : (
51 | ''
52 | )}
53 |
54 | ))}
55 |
56 | )}
57 |
58 | );
59 | }
60 |
61 | export default ColorPicker;
62 |
--------------------------------------------------------------------------------
/src/components/ColorPicker/ColorPickerElements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Pallette = styled.div`
4 | position: absolute;
5 | top: -115px;
6 | left: -50px;
7 | z-index: 10;
8 | background: #fff;
9 | box-shadow: 0px 0px 2px 1px rgba(122, 122, 122, 0.5);
10 | width: 125px;
11 | display: flex;
12 | justify-content: center;
13 | flex-wrap: wrap;
14 | `;
15 | export const Icon = styled.span`
16 | color: ${(props) => (props.color ? props.color : 'auto')};
17 | `;
18 | export const Button = styled.button`
19 | color: #666;
20 | border: none;
21 | background: transparent;
22 | outline: none;
23 | cursor: pointer;
24 | &:hover {
25 | color: #333;
26 | }
27 | `;
28 | export const Color = styled.button`
29 | box-shadow: 0px 0px 2px 1px rgba(122, 122, 122, 0.5);
30 | opacity: 0.8;
31 | margin: 5px;
32 | outline: none;
33 | background: ${(props) => props.color};
34 | width: 26px;
35 | height: 26px;
36 | border-radius: 50%;
37 | border: none;
38 | &:hover {
39 | border: 2px solid #333;
40 | box-shadow: none;
41 | }
42 | `;
43 | export const ColorPickerContainer = styled.div`
44 | position: relative;
45 | `;
46 |
--------------------------------------------------------------------------------
/src/components/EditNoteModal/EditNoteModal.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import ReactDom from 'react-dom';
3 | import { connect } from 'react-redux';
4 | import styled from 'styled-components';
5 | import NoteForm from '../NoteForm/NoteForm';
6 |
7 | const Overlay = styled.div`
8 | position: fixed;
9 | z-index: 10;
10 | top: 0;
11 | left: 0;
12 | width: 100vw;
13 | height: 100vh;
14 | background: rgba(155, 155, 155, 0.8);
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | visibility: ${(props) => (props.hiddenProp ? 'hidden' : 'visible')};
19 | opacity: ${(props) => (props.hiddenProp ? '0' : '1')};
20 | `;
21 |
22 | const Modal = styled.div`
23 | transform: ${(props) => (props.editModeOn ? 'scale(1)' : 'scale(0.4)')};
24 | transition: all 0.1s;
25 | `;
26 |
27 | const modalRoot = document.getElementById('modal-root');
28 |
29 | const EditNoteModal = ({ editedNote }) => {
30 | const [temp, setTemp] = useState(false);
31 | const [temp1, setTemp1] = useState(false);
32 |
33 | useEffect(() => {
34 | if (editedNote.hasOwnProperty('title')) {
35 | setTemp1(true);
36 | setTimeout(() => setTemp(true), 0);
37 | } else {
38 | setTemp(false);
39 | setTimeout(() => setTemp1(false), 100);
40 | }
41 | }, [editedNote]);
42 |
43 | return ReactDom.createPortal(
44 |
45 | {temp1 && }
46 | ,
47 | modalRoot
48 | );
49 | };
50 |
51 | EditNoteModal.propTypes = {};
52 |
53 | const mapStateToProps = (state) => {
54 | return {
55 | editedNote: state.notes.editedNote
56 | };
57 | };
58 |
59 | export default connect(mapStateToProps, {})(EditNoteModal);
60 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const FooterContainer = styled.footer`
5 | height: 8vh;
6 | text-align: center;
7 | background: #f4b400;
8 | line-height: 70px;
9 | color: #fff;
10 | font-size: 20px;
11 | letter-spacing: 0.4px;
12 | @media (max-width: 959px) {
13 | height: 50px;
14 | line-height: 50px;
15 | }
16 | `;
17 |
18 | function Footer() {
19 | return Google Keep Clone;
20 | }
21 |
22 | export default Footer;
23 |
--------------------------------------------------------------------------------
/src/components/FormNoteList/FormNoteList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | IconButton,
4 | Icon,
5 | ListContainer,
6 | ListItem,
7 | ListItemForm,
8 | // Checkbox,
9 | ListItemFormInput
10 | } from '../NoteForm/NoteFormElements';
11 | import Checkbox from '../NotesList/Checkbox';
12 | import uuid from 'uuid';
13 |
14 | import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
15 |
16 | function FormNoteList({
17 | checkList,
18 | setCheckList,
19 | deleteListItem,
20 | editMode = false
21 | }) {
22 | const [listItem, setListItem] = useState('');
23 | const handleSubmit = (e) => {
24 | const uid = uuid();
25 | const newCheckList = {
26 | ...checkList,
27 | [uid]: {
28 | listItem: e.target.value,
29 | status: false,
30 | uid
31 | }
32 | };
33 | setCheckList(newCheckList);
34 | setListItem('');
35 | };
36 | const handleChange = (e, item) => {
37 | setCheckList({
38 | ...checkList,
39 | [item.uid]: {
40 | ...checkList[item.uid],
41 | listItem: e.target.value
42 | }
43 | });
44 | };
45 | const handleKeyUp = (e) => {
46 | if (e.key === 'Enter') {
47 | document.getElementById('listItemFormInput').focus();
48 | }
49 | };
50 | //TODO: refactor result function
51 | const onDragEnd = (result) => {
52 | const sourceIndex = result.source.index;
53 | const destinationIndex = result.destination.index;
54 |
55 | if (!destinationIndex || destinationIndex === sourceIndex) {
56 | return;
57 | }
58 | let movedItemId;
59 | let newCheckList = Object.values(checkList)
60 | .filter((listItem, index) => {
61 | if (index !== sourceIndex) return true;
62 | movedItemId = listItem.uid;
63 | return false;
64 | })
65 | .reduce((newCheckList, listItem, index) => {
66 | if (index === destinationIndex) {
67 | newCheckList[movedItemId] = {
68 | uid: movedItemId,
69 | listItem: checkList[movedItemId].listItem
70 | };
71 | }
72 | newCheckList[listItem.uid] = {
73 | uid: listItem.uid,
74 | listItem: listItem.listItem
75 | };
76 | return newCheckList;
77 | }, {});
78 | if (Object.values(newCheckList).length !== destinationIndex + 1) {
79 | newCheckList[movedItemId] = {
80 | uid: movedItemId,
81 | listItem: checkList[movedItemId].listItem
82 | };
83 | }
84 | setCheckList(newCheckList);
85 | };
86 | return (
87 |
88 |
89 | {(provided, snapshot) => (
90 |
91 | {Object.values(checkList).map((item, i, arr) => (
92 |
98 | {(provided, snapshot) => (
99 |
103 |
104 |
108 | {
110 | setCheckList({
111 | ...checkList,
112 | [item.uid]: {
113 | ...checkList[item.uid],
114 | status: !checkList[item.uid].status
115 | }
116 | });
117 | }}
118 | listItem={checkList[item.uid]}
119 | />
120 | handleChange(e, item)}
124 | onKeyUp={handleKeyUp}
125 | />
126 |
127 |
132 |
133 | )}
134 |
135 | ))}
136 | {provided.placeholder}
137 |
138 |
139 |
146 |
147 |
148 | )}
149 |
150 |
151 | );
152 | }
153 |
154 | export default FormNoteList;
155 |
--------------------------------------------------------------------------------
/src/components/Navigation/Navigation.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { connect } from 'react-redux';
3 | import {
4 | DesktopLinksGroup,
5 | NavElement,
6 | AuthButton,
7 | Title,
8 | Nav,
9 | Hamburger,
10 | MobileLinksGroup,
11 | CloseNavBtn,
12 | NavMobileElement
13 | } from './navigation-elements';
14 | import { Link } from 'react-router-dom';
15 | import { Icon } from '../../UI/theme';
16 | import { signOut } from '../../firebase/firebaseAuth';
17 |
18 | function DesktopNav({ isLoggedIn }) {
19 | return (
20 |
21 |
22 |
23 | Home
24 |
25 |
26 | {isLoggedIn && (
27 |
28 |
29 | Notes
30 |
31 |
32 | )}
33 |
34 |
35 | About
36 |
37 |
38 | {isLoggedIn && (
39 |
40 | Log out
41 |
42 | )}
43 |
44 | );
45 | }
46 | function MobileNav({ isLoggedIn }) {
47 | const [isMobileNavOpen, toggleMobileNav] = useState(false);
48 | return (
49 | <>
50 | toggleMobileNav(!isMobileNavOpen)}>
51 |
52 |
53 |
54 |
55 | toggleMobileNav(!isMobileNavOpen)}
57 | style={{ textDecoration: 'none', color: '#fff' }}
58 | to="/"
59 | >
60 | Home
61 |
62 |
63 | {isLoggedIn && (
64 |
65 | toggleMobileNav(!isMobileNavOpen)}
67 | style={{ textDecoration: 'none', color: '#fff' }}
68 | to="/notes"
69 | >
70 | Notes
71 |
72 |
73 | )}
74 |
75 | toggleMobileNav(!isMobileNavOpen)}
77 | style={{ textDecoration: 'none', color: '#fff' }}
78 | to="/about"
79 | >
80 | About
81 |
82 |
83 | {isLoggedIn && (
84 |
85 | {
87 | signOut();
88 | toggleMobileNav(!isMobileNavOpen);
89 | }}
90 | >
91 | Log out
92 |
93 |
94 | )}
95 | toggleMobileNav(!isMobileNavOpen)}>
96 | Close
97 |
98 |
99 | >
100 | );
101 | }
102 |
103 | function Navigation({ isLoggedIn }) {
104 | return (
105 |
113 | );
114 | }
115 |
116 | const mapStateToProps = (state) => {
117 | return {
118 | isLoggedIn: state.auth.isLoggedIn
119 | };
120 | };
121 |
122 | export default connect(mapStateToProps, {})(Navigation);
123 |
--------------------------------------------------------------------------------
/src/components/Navigation/navigation-elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Nav = styled.nav`
4 | height: 8vh;
5 | background-color: #f4b400;
6 | display: flex;
7 | justify-content: space-between;
8 | color: #fff;
9 | padding: 0 30px;
10 | align-items: center;
11 | @media (max-width: 959px) {
12 | height: 50px;
13 | }
14 | `;
15 | export const DesktopLinksGroup = styled.ul`
16 | margin-right: 100px;
17 | display: flex;
18 | list-style: none;
19 | `;
20 | export const NavElement = styled.li`
21 | margin: 0 15px;
22 | transition: all 0.5s;
23 | cursor: pointer;
24 | &:hover {
25 | transform: scale(1.1);
26 | }
27 | `;
28 | export const AuthButton = styled.a`
29 | text-decoration: none;
30 | color: #fff;
31 | cursor: pointer;
32 | `;
33 |
34 | export const Title = styled.h1`
35 | @media (max-width: 959px) {
36 | font-size: calc(1.1rem + 16 * (100vw - 320px) / (960 - 320));
37 | line-height: calc(110% + 3.2 * (100vw - 960px) / (320 - 960));
38 | }
39 | `;
40 |
41 | export const Hamburger = styled.button`
42 | border: none;
43 | outline: none;
44 | background: transparent;
45 | cursor: pointer;
46 | `;
47 |
48 | export const MobileLinksGroup = styled.ul`
49 | margin: 0;
50 | padding: 0;
51 | border: none;
52 | transition: all 0.3s;
53 | overflow: hidden;
54 | height ${(props) => (props.isOpen ? '100vh' : '0')};
55 | position: fixed;
56 | top: 0;
57 | left: 0;
58 | width: 100vw;
59 | display: flex;
60 | flex-direction: column;
61 | justify-content: center;
62 | align-items: center;
63 | list-style: none;
64 | background: rgba(0,0,0,0.8);
65 | `;
66 |
67 | export const CloseNavBtn = styled.button`
68 | color: #fff;
69 | outline: none;
70 | border: none;
71 | background: transparent;
72 | margin-top: 20px;
73 | font-size: 30px;
74 | transition: all 0.5s;
75 | &:hover {
76 | transform: scale(1.1);
77 | }
78 | `;
79 | export const NavMobileElement = styled(NavElement)`
80 | margin-top: 20px;
81 | font-size: 30px;
82 | `;
83 |
--------------------------------------------------------------------------------
/src/components/NoteForm/NoteForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import uuid from 'uuid';
3 | import {
4 | FormContainer,
5 | FormGroup,
6 | TitleField,
7 | NoteField,
8 | IconButton
9 | } from './NoteFormElements';
10 | import FormNoteList from '../FormNoteList/FormNoteList';
11 | import TagList from '../TagList/TagList';
12 | import NotesFormFooter from './NotesFormFooter';
13 | import TextareaAutosize from 'react-autosize-textarea';
14 | import { connect } from 'react-redux';
15 | import {
16 | addNote,
17 | updateStructureLocally,
18 | clearEditNote
19 | } from '../../redux/notes';
20 | import { updateNote, updateStructure } from '../../firebase/firebaseAPI';
21 | import {
22 | getListBasedOnLineTextBreak,
23 | getSingleNoteBasedOnList,
24 | checkIfTargetIsForm
25 | } from '../../utils';
26 |
27 | const hasCheckListItems = (checkList) => {
28 | if (checkList) {
29 | return Object.keys(checkList).length > 0;
30 | }
31 | return false;
32 | };
33 |
34 | function NoteForm({
35 | addNote,
36 | structure,
37 | editMode = false,
38 | editedNote,
39 | clearEditNote,
40 | updateStructureLocally
41 | }) {
42 | const [editedNoteCopy] = useState({ ...editedNote });
43 | const [title, setTitle] = useState(editedNote.title ? editedNote.title : '');
44 | const [note, setNote] = useState(editedNote.note ? editedNote.note : '');
45 | const [checkList, setCheckList] = useState(
46 | editedNote.checkList ? editedNote.checkList : {}
47 | );
48 | const [isPinned, setIsPinned] = useState(
49 | editedNote.isPinned ? editedNote.isPinned : false
50 | );
51 | const [tags, setTags] = useState(editedNote.tags ? editedNote.tags : []);
52 | const [bgColor, setBgColor] = useState(
53 | editedNote.bgColor ? editedNote.bgColor : 'rgba(255,255,255,0.8)'
54 | );
55 | const [isInputOpen, setInputOpen] = useState(editMode);
56 | // here has a problem with converting undefined or null to object keys
57 | const [noteEditorMode, setNoteEditorMode] = useState(
58 | editMode && hasCheckListItems(editedNote.checkList) ? true : false
59 | );
60 |
61 | const titleInput = useRef();
62 |
63 | const toggleNoteEditorMode = () => {
64 | let newNote = '';
65 | let newNoteCheckList = {};
66 |
67 | if (noteEditorMode) {
68 | newNote = getSingleNoteBasedOnList(checkList);
69 | } else {
70 | newNoteCheckList = getListBasedOnLineTextBreak(note);
71 | }
72 | setNote(newNote);
73 | setCheckList(newNoteCheckList);
74 | setNoteEditorMode(!noteEditorMode);
75 | };
76 |
77 | const resetForm = () => {
78 | setTitle('');
79 | setNote('');
80 | setTags([]);
81 | setCheckList({});
82 | setIsPinned(false);
83 | setBgColor('rgba(255,255,255,0.8)');
84 | setNoteEditorMode(false);
85 | setInputOpen(false);
86 | if (editMode && clearEditNote) {
87 | clearEditNote();
88 | }
89 | };
90 |
91 | const validateFields = () => {
92 | return (note + title).trim() !== '' || Object.values(checkList).length > 0;
93 | };
94 | const checkIfNoteHasChanged = (changedFields) => {
95 | for (let prop in changedFields) {
96 | if (editedNoteCopy[prop] !== changedFields[prop]) {
97 | return true;
98 | }
99 | }
100 | return false;
101 | };
102 | const getChangedFields = (fields) => {
103 | const changedFields = {};
104 |
105 | for (let prop in fields) {
106 | if (editedNoteCopy[prop] !== fields[prop]) {
107 | changedFields[prop] = fields[prop];
108 | }
109 | }
110 | return changedFields;
111 | };
112 |
113 | // eslint-disable-next-line react-hooks/exhaustive-deps
114 | const handleBodyClick = (e) => {
115 | const targetIsForm = checkIfTargetIsForm(e.target);
116 |
117 | if (targetIsForm) {
118 | return;
119 | } else if (!targetIsForm && validateFields()) {
120 | if (editMode) {
121 | const fields = {
122 | title,
123 | note,
124 | checkList,
125 | isPinned,
126 | tags,
127 | bgColor
128 | };
129 | if (checkIfNoteHasChanged(fields)) {
130 | const changedFields = getChangedFields(fields);
131 | updateNote(changedFields, editedNote.id).then(() => {
132 | if (changedFields.hasOwnProperty('isPinned')) {
133 | let noteStructure;
134 | if (!changedFields.isPinned) {
135 | noteStructure = { ...structure };
136 | for (let prop in noteStructure) {
137 | noteStructure[prop].tasksIds = noteStructure[
138 | prop
139 | ].tasksIds.filter((taskId) => taskId !== editedNote.uuid);
140 | }
141 | noteStructure['column-1'].tasksIds.push(editedNote.uuid);
142 | } else {
143 | noteStructure = { ...structure };
144 |
145 | for (let prop in noteStructure) {
146 | noteStructure[prop].tasksIds = noteStructure[
147 | prop
148 | ].tasksIds.filter((taskId) => taskId !== editedNote.uuid);
149 | }
150 | noteStructure['column-5'].tasksIds.push(editedNote.uuid);
151 | }
152 | updateStructure(noteStructure);
153 | updateStructureLocally(noteStructure);
154 | }
155 | });
156 | }
157 | } else {
158 | const newUuid = uuid();
159 | const newNote = {
160 | title,
161 | note,
162 | checkList,
163 | isPinned,
164 | tags,
165 | bgColor,
166 | column: isPinned ? 5 : 1, // These numbers are starting indexes of columns in note lists
167 | uuid: newUuid
168 | };
169 | addNote(newNote);
170 | }
171 | }
172 | setInputOpen(false);
173 | resetForm();
174 | };
175 |
176 | const deleteListItem = (e) => {
177 | const newCheckListItems = {
178 | ...checkList
179 | };
180 | delete newCheckListItems[e.target.name];
181 | setCheckList(newCheckListItems);
182 | };
183 |
184 | useEffect(() => {
185 | document.body.addEventListener('mousedown', handleBodyClick);
186 |
187 | return () => {
188 | document.body.removeEventListener('mousedown', handleBodyClick);
189 | };
190 | }, [editMode, handleBodyClick, isInputOpen, tags]);
191 |
192 | return (
193 |
194 | {' '}
195 | {(isInputOpen || editMode) && (
196 |
197 | setTitle(e.target.value)}
202 | placeholder="Tytuł"
203 | />
204 | setIsPinned(!isPinned)}
207 | />{' '}
208 |
209 | )}{' '}
210 |
211 | {' '}
212 | {noteEditorMode ? (
213 |
219 | ) : (
220 | setNote(e.target.value)}
228 | onClick={() => setInputOpen(true)}
229 | placeholder="Utwórz notatkę..."
230 | />
231 | )}{' '}
232 | {!isInputOpen && !editMode && (
233 | {
236 | setNoteEditorMode(true);
237 | setInputOpen(true);
238 | }}
239 | />
240 | )}{' '}
241 | {' '}
242 |
243 | {(isInputOpen || editMode) && (
244 |
253 | )}{' '}
254 |
255 | );
256 | }
257 | const mapStateToProps = (state) => {
258 | return {
259 | structure: {
260 | ...state.notes.noteStructure
261 | },
262 | editedNote: { ...state.notes.editedNote }
263 | };
264 | };
265 |
266 | const mapDispatchToProps = {
267 | addNote,
268 | updateStructureLocally,
269 | clearEditNote
270 | };
271 |
272 | export default connect(mapStateToProps, mapDispatchToProps)(NoteForm);
273 |
--------------------------------------------------------------------------------
/src/components/NoteForm/NoteFormElements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const NotesContainer = styled.section`
4 | box-sizing: border-box;
5 | padding-top: 50px;
6 | min-height: 84vh;
7 | @media (max-width: 959px) {
8 | min-height: calc(100vh - 100px);
9 | }
10 | `;
11 | export const FormContainer = styled.div`
12 | margin: 0 auto;
13 | max-width: 600px;
14 | min-height: 60px;
15 | transition: background 0.2s;
16 | background: ${(props) =>
17 | props.bgColor === 'transparent' ? '#fefefe' : props.bgColor};
18 | border-radius: 5px;
19 | box-shadow: 1px 1px 8px 1px rgba(89, 89, 89, 0.53);
20 | padding: 20px 0px 6px 0px;
21 | @media (max-width: 650px) {
22 | margin: 0 25px;
23 | }
24 | @media (max-width: 600px) {
25 | margin: 0 10px;
26 | }
27 | `;
28 |
29 | export const FormGroup = styled.div`
30 | display: flex;
31 | justify-content: space-between;
32 | `;
33 | export const FormToolsGroup = styled(FormGroup)`
34 | margin: 10px 20px 0 20px;
35 | align-items: center;
36 | transition: opacity 200ms;
37 | opacity: ${(props) => (props.isHovered ? 1 : 0)};
38 | `;
39 | // export const Icon = styled.span`
40 | // width: 27px;
41 | // `;
42 | export const Icon = styled.span`
43 | margin-right: 15px;
44 | color: #999;
45 | cursor: pointer;
46 | `;
47 | export const IconButton = styled.button`
48 | opacity: ${(props) => (props.opacity ? props.opacity : '1')}
49 | color: #666;
50 | border: none;
51 | margin-right: ${(props) => (props.margin ? props.margin : '15px')};
52 | background: transparent;
53 | outline: none;
54 | cursor: pointer;
55 | &:hover {
56 | color: #333;
57 | cursor: pointer;
58 | }
59 | `;
60 | export const TitleField = styled.input`
61 | background: transparent;
62 | border: none;
63 | color: #666;
64 | outline: none;
65 | font-size: 16px;
66 | font-weight: 400;
67 | margin: 0 20px 18px 20px;
68 | width: 100%;
69 | `;
70 | // export const NoteField = styled.textarea`
71 | // background: transparent;
72 | // border: none;
73 | // color: #666;
74 | // outline: none;
75 | // letter-spacing: 0.7px;
76 | // font-size: 15px;
77 | // width: 100%;
78 | // margin: 0 20px 0 20px;
79 | // height: auto;
80 | // `;
81 | export const NoteField = {
82 | background: 'transparent',
83 | border: 'none',
84 | color: '#666',
85 | outline: 'none',
86 | letterSpacing: '0.7px',
87 | fontSize: '15px',
88 | width: '100%',
89 | margin: ' 0 20px 0 20px',
90 | height: 'auto'
91 | };
92 | export const CloseBtn = styled.button`
93 | background: transparent;
94 | border: none;
95 | color: #444;
96 | outline: none;
97 | height: 35px;
98 | padding: 0 20px;
99 | cursor: pointer;
100 | margin-left: 40px;
101 | &:hover {
102 | background: rgba(240, 240, 240, 0.5);
103 | }
104 | `;
105 |
106 | export const ListContainer = styled.ul`
107 | width: 100%;
108 | list-style: none;
109 | max-height: 500px;
110 | overflow-y: auto;
111 | `;
112 | export const ListItem = styled.li`
113 | display: flex;
114 | justify-content: space-between;
115 | padding: 10px 10px;
116 | background: inherit;
117 | ${Icon} {
118 | transition: opacity 0.1s;
119 | opacity: 0;
120 | }
121 | &:hover ${Icon} {
122 | opacity: 1;
123 | }
124 | ${IconButton} {
125 | transition: opacity 0.1s;
126 | opacity: 0;
127 | }
128 | &:hover ${IconButton} {
129 | opacity: 1;
130 | }
131 | `;
132 | export const ListItemForm = styled.li`
133 | border-top: 1px solid rgba(200, 200, 200, 0.9);
134 | border-bottom: 1px solid rgba(200, 200, 200, 0.9);
135 | padding: 10px 35px;
136 | width: 100%;
137 | display: flex;
138 | background: transparent;
139 | margin-top: ${(props) => (props.marginPlaceholder ? '38px' : 0)};
140 | `;
141 | export const Checkbox = styled.input`
142 | margin-right: 10px;
143 | `;
144 | export const ListItemFormInput = styled.input`
145 | border: none;
146 | outline: none;
147 | flex-grow: 1;
148 | background: transparent;
149 | `;
150 |
--------------------------------------------------------------------------------
/src/components/NoteForm/NotesFormFooter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TagWidget from '../TagWidget/TagWidget';
3 | import ColorPicker from '../ColorPicker/ColorPicker';
4 | import { FormToolsGroup, CloseBtn, IconButton } from './NoteFormElements';
5 |
6 | function NotesFormFooter({
7 | chosenTags,
8 | setTags,
9 | bgColor,
10 | setBgColor,
11 | handleToggleClick,
12 | handleCloseClick,
13 | noteEditorMode,
14 | closeOption = true,
15 | isHovered = true,
16 | children
17 | }) {
18 | return (
19 |
20 |
24 |
29 |
30 | {closeOption && Zamknij}
31 | {children}
32 |
33 | );
34 | }
35 |
36 | export default NotesFormFooter;
37 |
--------------------------------------------------------------------------------
/src/components/NotesList/Checkbox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const CheckBoxContainer = styled.label`
5 | margin-right: 8px;
6 | cursor: pointer;
7 | `;
8 |
9 | export default function Checkbox({ handleCheck, listItem }) {
10 | return (
11 |
12 |
16 | handleCheck(listItem)}
18 | style={{ display: 'none' }}
19 | type="checkbox"
20 | />
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/NotesList/Column.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Task from './NoteDraggable';
3 | import { Droppable } from 'react-beautiful-dnd';
4 | import { ColumnContainer, TaskList } from './notes-list-elements';
5 |
6 | export default class Column extends React.Component {
7 | render() {
8 | const { column, tasks } = this.props;
9 | return (
10 |
11 |
12 | {(provided, snapshot) => (
13 |
18 | {tasks
19 | .sort((a, b) => a.row - b.row)
20 | .map((task, index) => {
21 | return (
22 |
28 | );
29 | })}
30 | {provided.placeholder}
31 |
32 | )}
33 |
34 |
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/NotesList/Note.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { IconButton } from '../NoteForm/NoteFormElements';
4 | import NotesFormFooter from '../NoteForm/NotesFormFooter';
5 | import {
6 | updateNote,
7 | removeNoteFromDB,
8 | updateStructure
9 | } from '../../firebase/firebaseAPI';
10 | import {
11 | updateStructureLocally,
12 | deleteNote,
13 | editNote
14 | } from '../../redux/notes';
15 | import TagList from '../TagList/TagList';
16 | import cloneDeep from 'clone-deep';
17 | import Checkbox from './Checkbox';
18 | import {
19 | NoteContainer,
20 | Title,
21 | NoteContent,
22 | TextNote,
23 | CheckListItem
24 | } from './notes-list-elements';
25 | import uuid from 'uuid';
26 |
27 | class Task extends React.Component {
28 | state = {
29 | isHovered: false,
30 | isContextMenuOpen: false,
31 | editMode: false
32 | };
33 |
34 | handleMouseOver = () => {
35 | this.setState({ isHovered: true });
36 | };
37 |
38 | handleMouseLeave = () => {
39 | this.setState({ isHovered: false });
40 | };
41 |
42 | handleDeleteClick = () => {
43 | const {
44 | noteStructure,
45 | // column,
46 | task,
47 | deleteNote,
48 | updateStructureLocally
49 | } = this.props;
50 | let column;
51 | for (let prop in noteStructure) {
52 | if (noteStructure[prop].hasOwnProperty('tasksIds')) {
53 | const isLookingCol = noteStructure[prop].tasksIds.find(
54 | (uid) => uid === task.uuid
55 | );
56 | if (isLookingCol) {
57 | column = prop;
58 | }
59 | }
60 | }
61 | const newStructure = cloneDeep(noteStructure);
62 | newStructure[column].tasksIds = newStructure[column].tasksIds.filter(
63 | (taskId) => taskId !== task.uuid
64 | );
65 |
66 | deleteNote(task);
67 | updateStructureLocally(newStructure);
68 | removeNoteFromDB(task, column);
69 | };
70 |
71 | handleCheck = (listItem) => {
72 | const {
73 | task: { checkList, id }
74 | } = this.props;
75 | const newCheckList = { ...checkList };
76 | newCheckList[listItem.uid].status = !newCheckList[listItem.uid].status;
77 | updateNote({ checkList: newCheckList }, id);
78 | };
79 |
80 | handleToggleClick = () => {
81 | const { task } = this.props;
82 | if (Object.values(task.checkList).length === 0) {
83 | const newCheckList = {};
84 | task.note.split(/\n/).forEach((phrase) => {
85 | const newUid = uuid();
86 | newCheckList[newUid] = {
87 | uid: newUid,
88 | status: false,
89 | listItem: phrase
90 | };
91 | });
92 | updateNote({ checkList: newCheckList, note: '' }, task.id);
93 | } else {
94 | const newNote = Object.values(task.checkList)
95 | .map((note) => note.listItem)
96 | .join('\n');
97 | updateNote({ checkList: {}, note: newNote }, task.id);
98 | }
99 | };
100 |
101 | handlePinClick = (isPinned, id) => {
102 | updateNote(
103 | { isPinned: !this.props.task.isPinned },
104 | this.props.task.id
105 | ).then(() => {
106 | let noteStructure;
107 | if (!this.props.task.isPinned) {
108 | noteStructure = { ...this.props.noteStructure };
109 | for (let prop in noteStructure) {
110 | if (noteStructure[prop].hasOwnProperty('tasksIds')) {
111 | noteStructure[prop].tasksIds = noteStructure[prop].tasksIds.filter(
112 | (taskId) => taskId !== this.props.task.uuid
113 | );
114 | }
115 | }
116 | noteStructure['column-1'].tasksIds.push(this.props.task.uuid);
117 | } else {
118 | noteStructure = { ...this.props.noteStructure };
119 |
120 | for (let prop in noteStructure) {
121 | if (noteStructure[prop].hasOwnProperty('tasksIds')) {
122 | noteStructure[prop].tasksIds = noteStructure[prop].tasksIds.filter(
123 | (taskId) => taskId !== this.props.task.uuid
124 | );
125 | }
126 | }
127 | noteStructure['column-5'].tasksIds.push(this.props.task.uuid);
128 | }
129 | updateStructure(noteStructure);
130 | this.props.updateStructureLocally(noteStructure);
131 | });
132 | };
133 |
134 | render() {
135 | const {
136 | task: { title, note, bgColor, tags, checkList, isPinned, id }
137 | } = this.props;
138 |
139 | const { isHovered } = this.state;
140 | // TODO: try to refactor this place, create function which returns content
141 | let content =
142 | Object.values(checkList).length === 0 ? (
143 | {note}
144 | ) : (
145 | Object.values(checkList).reduce(
146 | (listsToDisplay, listItem) => {
147 | const itemToDisplay = (
148 |
149 |
150 |
155 | {listItem.listItem}
156 |
157 |
158 | );
159 |
160 | if (!listItem.status) {
161 | return [
162 | [...listsToDisplay[0], itemToDisplay],
163 | [...listsToDisplay[1]]
164 | ];
165 | } else {
166 | return [
167 | [...listsToDisplay[0]],
168 | [...listsToDisplay[1], itemToDisplay]
169 | ];
170 | }
171 | },
172 | [[], []]
173 | )
174 | );
175 |
176 | if (content.length > 1 && content[0].length > 0 && content[1].length > 0) {
177 | content = [
178 | ...content[0],
179 | ,
186 | ...content[1]
187 | ];
188 | }
189 |
190 | return (
191 | this.props.editNote(this.props.task)}
197 | isHovered={this.state.isHovered}
198 | bgColor={this.props.task.bgColor}
199 | >
200 |
201 | {title !== '' && (
202 |
203 | {title}
204 |
213 |
214 | )}
215 | {title === '' && (
216 |
225 | )}
226 | {content}
227 |
228 | updateNote({ tags: [...newTags] }, id)}
232 | />
233 |
234 | {
238 | updateNote({ tags: [...newTags] }, id);
239 | }}
240 | bgColor={bgColor}
241 | setBgColor={(color) => {
242 | updateNote({ bgColor: color }, id);
243 | }}
244 | noteEditorMode={note.trim() === ''}
245 | handleToggleClick={this.handleToggleClick}
246 | closeOption={false}
247 | >
248 |
255 |
256 |
257 | );
258 | }
259 | }
260 | const mapStateToProps = (state) => {
261 | return {
262 | noteStructure: state.notes.noteStructure
263 | };
264 | };
265 |
266 | export default connect(mapStateToProps, {
267 | updateStructureLocally,
268 | deleteNote,
269 | editNote
270 | })(Task);
271 |
--------------------------------------------------------------------------------
/src/components/NotesList/NoteDraggable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Draggable } from 'react-beautiful-dnd';
4 | import { IconButton } from '../NoteForm/NoteFormElements';
5 | import NotesFormFooter from '../NoteForm/NotesFormFooter';
6 | import {
7 | updateNote,
8 | removeNoteFromDB,
9 | updateStructure
10 | } from '../../firebase/firebaseAPI';
11 | import {
12 | updateStructureLocally,
13 | deleteNote,
14 | editNote
15 | } from '../../redux/notes';
16 | import TagList from '../TagList/TagList';
17 | import cloneDeep from 'clone-deep';
18 | import Checkbox from './Checkbox';
19 | import {
20 | NoteContainer,
21 | Title,
22 | NoteContent,
23 | TextNote,
24 | CheckListItem
25 | } from './notes-list-elements';
26 | import uuid from 'uuid';
27 |
28 | class Task extends React.Component {
29 | state = {
30 | isHovered: false,
31 | isContextMenuOpen: false,
32 | editMode: false
33 | };
34 |
35 | handleMouseOver = () => {
36 | this.setState({ isHovered: true });
37 | };
38 |
39 | handleMouseLeave = () => {
40 | this.setState({ isHovered: false });
41 | };
42 |
43 | handleDeleteClick = () => {
44 | const {
45 | noteStructure,
46 | column,
47 | task,
48 | deleteNote,
49 | updateStructureLocally
50 | } = this.props;
51 | const newStructure = cloneDeep(noteStructure);
52 | newStructure[column].tasksIds = newStructure[column].tasksIds.filter(
53 | (taskId) => taskId !== task.uuid
54 | );
55 |
56 | deleteNote(task);
57 | updateStructureLocally(newStructure);
58 | removeNoteFromDB(task, column);
59 | };
60 |
61 | handleCheck = (listItem) => {
62 | const {
63 | task: { checkList, id }
64 | } = this.props;
65 | const newCheckList = { ...checkList };
66 | newCheckList[listItem.uid].status = !newCheckList[listItem.uid].status;
67 | updateNote({ checkList: newCheckList }, id);
68 | };
69 |
70 | handleToggleClick = () => {
71 | const { task } = this.props;
72 | if (Object.values(task.checkList).length === 0) {
73 | const newCheckList = {};
74 | task.note.split(/\n/).forEach((phrase) => {
75 | const newUid = uuid();
76 | newCheckList[newUid] = {
77 | uid: newUid,
78 | status: false,
79 | listItem: phrase
80 | };
81 | });
82 | updateNote({ checkList: newCheckList, note: '' }, task.id);
83 | } else {
84 | const newNote = Object.values(task.checkList)
85 | .map((note) => note.listItem)
86 | .join('\n');
87 | updateNote({ checkList: {}, note: newNote }, task.id);
88 | }
89 | };
90 |
91 | handlePinClick = (isPinned, id) => {
92 | updateNote(
93 | { isPinned: !this.props.task.isPinned },
94 | this.props.task.id
95 | ).then(() => {
96 | let noteStructure;
97 | if (!this.props.task.isPinned) {
98 | noteStructure = { ...this.props.noteStructure };
99 | for (let prop in noteStructure) {
100 | if (noteStructure[prop].hasOwnProperty('tasksIds')) {
101 | noteStructure[prop].tasksIds = noteStructure[prop].tasksIds.filter(
102 | (taskId) => taskId !== this.props.task.uuid
103 | );
104 | }
105 | }
106 | noteStructure['column-1'].tasksIds.push(this.props.task.uuid);
107 | } else {
108 | noteStructure = { ...this.props.noteStructure };
109 |
110 | for (let prop in noteStructure) {
111 | if (noteStructure[prop].hasOwnProperty('tasksIds')) {
112 | noteStructure[prop].tasksIds = noteStructure[prop].tasksIds.filter(
113 | (taskId) => taskId !== this.props.task.uuid
114 | );
115 | }
116 | }
117 | noteStructure['column-5'].tasksIds.push(this.props.task.uuid);
118 | }
119 | updateStructure(noteStructure);
120 | this.props.updateStructureLocally(noteStructure);
121 | });
122 | };
123 |
124 | getTop = () => {
125 | if (this.myRef && this.myRef.current) {
126 | const windowInnerTop = window.innerHeight;
127 | const offset = this.myRef.current.parentElement.offsetTop;
128 | const targetHeight = this.myRef.current.parentElement.parentElement
129 | .parentElement.offsetHeight;
130 |
131 | return windowInnerTop / 2 - (offset + targetHeight / 2);
132 | }
133 | };
134 |
135 | getLeft = () => {
136 | if (this.myRef && this.myRef.current) {
137 | const windowInnerWidth = window.innerWidth;
138 | const offset = this.myRef.current.parentElement.parentElement
139 | .parentElement.offsetLeft;
140 | const targetWidth = this.myRef.current.offsetWidth;
141 | return windowInnerWidth / 2 - (offset + targetWidth / 2);
142 | }
143 | };
144 |
145 | render() {
146 | const {
147 | task: { title, note, bgColor, tags, checkList, isPinned, id },
148 | index
149 | } = this.props;
150 |
151 | const { isHovered } = this.state;
152 | // TODO: try to refactor this place, create function which returns content
153 | let content =
154 | Object.values(checkList).length === 0 ? (
155 | {note}
156 | ) : (
157 | Object.values(checkList).reduce(
158 | (listsToDisplay, listItem) => {
159 | const itemToDisplay = (
160 |
161 |
162 |
167 | {listItem.listItem}
168 |
169 |
170 | );
171 |
172 | if (!listItem.status) {
173 | return [
174 | [...listsToDisplay[0], itemToDisplay],
175 | [...listsToDisplay[1]]
176 | ];
177 | } else {
178 | return [
179 | [...listsToDisplay[0]],
180 | [...listsToDisplay[1], itemToDisplay]
181 | ];
182 | }
183 | },
184 | [[], []]
185 | )
186 | );
187 |
188 | if (content.length > 1 && content[0].length > 0 && content[1].length > 0) {
189 | content = [
190 | ...content[0],
191 | ,
198 | ...content[1]
199 | ];
200 | }
201 |
202 | return (
203 |
204 | {(provided, snapshot) => {
205 | return (
206 | this.props.editNote(this.props.task)
213 | // this.setState({ editMode: !this.state.editMode })
214 | // fire on action with note to edit
215 | }
216 | isHovered={this.state.isHovered}
217 | bgColor={this.props.task.bgColor}
218 | ref={provided.innerRef}
219 | isDragging={snapshot.isDragging}
220 | {...provided.draggableProps}
221 | {...provided.dragHandleProps}
222 | >
223 |
224 | {title !== '' && (
225 |
226 | {title}
227 |
236 |
237 | )}
238 | {title === '' && (
239 |
248 | )}
249 | {content}
250 |
251 | updateNote({ tags: [...newTags] }, id)}
255 | />
256 |
257 | {
261 | updateNote({ tags: [...newTags] }, id);
262 | }}
263 | bgColor={bgColor}
264 | setBgColor={(color) => {
265 | updateNote({ bgColor: color }, id);
266 | }}
267 | noteEditorMode={note.trim() === ''}
268 | handleToggleClick={this.handleToggleClick}
269 | closeOption={false}
270 | >
271 |
278 |
279 |
280 | );
281 | }}
282 |
283 | );
284 | }
285 | }
286 | const mapStateToProps = (state) => {
287 | return {
288 | noteStructure: state.notes.noteStructure
289 | };
290 | };
291 |
292 | export default connect(mapStateToProps, {
293 | updateStructureLocally,
294 | deleteNote,
295 | editNote
296 | })(Task);
297 |
--------------------------------------------------------------------------------
/src/components/NotesList/NotesList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Redirect } from 'react-router-dom';
3 | import { DragDropContext } from 'react-beautiful-dnd';
4 | import Column from './Column';
5 | import { updateStructureLocally } from '../../redux/notes';
6 | import { connect } from 'react-redux';
7 | import {
8 | updateStructure,
9 | getStructureFromDB
10 | } from '../../firebase/firebaseAPI';
11 | import equal from 'deep-equal';
12 | import { Container } from './notes-list-elements';
13 | import PropTypes from 'prop-types';
14 | import Loader from 'react-loader-spinner';
15 |
16 | const NOT_PINNED_COLUMNS = ['column-1', 'column-2', 'column-3', 'column-4'];
17 | const PINNED_COLUMNS = ['column-5', 'column-6', 'column-7', 'column-8'];
18 |
19 | class NotesList extends React.Component {
20 | componentDidMount() {
21 | if (this.props.isPinnedList && this.props.isLoggedIn) {
22 | getStructureFromDB(this.props.userId);
23 | }
24 | }
25 |
26 | componentDidUpdate(prevProps) {
27 | if (!equal(prevProps.structure, this.props.structure)) {
28 | updateStructure(this.props.structure);
29 | }
30 | }
31 |
32 | onDragEndRedux = (result) => {
33 | const { destination, source, draggableId } = result;
34 |
35 | if (!destination) {
36 | return;
37 | }
38 |
39 | if (
40 | destination.droppableId === source.droppableId &&
41 | destination.index === source.index
42 | ) {
43 | return;
44 | }
45 | const start = this.props.structure[source.droppableId];
46 | const finish = this.props.structure[destination.droppableId];
47 |
48 | if (start === finish) {
49 | const newTasksIds = Array.from(start.tasksIds);
50 | newTasksIds.splice(source.index, 1);
51 | newTasksIds.splice(destination.index, 0, draggableId);
52 |
53 | const newColumn = {
54 | ...start,
55 | tasksIds: newTasksIds
56 | };
57 |
58 | const newStructure = {
59 | ...this.props.structure,
60 | [newColumn.id]: newColumn
61 | };
62 | this.props.updateStructureLocally(newStructure);
63 | return;
64 | }
65 | // Moving from one list to another
66 | const startTasksIds = Array.from(start.tasksIds);
67 | startTasksIds.splice(source.index, 1);
68 | const newStart = {
69 | ...start,
70 | tasksIds: startTasksIds
71 | };
72 |
73 | const finishTasksIds = Array.from(finish.tasksIds);
74 | finishTasksIds.splice(destination.index, 0, draggableId);
75 | const newFinish = {
76 | ...finish,
77 | tasksIds: finishTasksIds
78 | };
79 |
80 | const newStructure = {
81 | ...this.props.structure,
82 | [newStart.id]: newStart,
83 | [newFinish.id]: newFinish
84 | };
85 |
86 | this.props.updateStructureLocally(newStructure);
87 | };
88 |
89 | render() {
90 | if (!this.props.isLoggedIn) {
91 | return (
92 |
97 | );
98 | }
99 | const { notes, structure, isPinnedList = false } = this.props;
100 | const columns = isPinnedList ? PINNED_COLUMNS : NOT_PINNED_COLUMNS;
101 | if (
102 | (Object.keys(notes).length === 0 ||
103 | Object.keys(structure).length === 0) &&
104 | isPinnedList
105 | ) {
106 | return (
107 |
115 |
122 |
123 | );
124 | }
125 | return (
126 | <>
127 |
128 |
134 | {columns.map((columnId) => {
135 | const column = structure[columnId];
136 | const tasks = column.tasksIds.map(
137 | (taskId) =>
138 | Object.values(notes).filter((note) => note.uuid === taskId)[0]
139 | );
140 | return ;
141 | })}
142 |
143 |
144 | >
145 | );
146 | }
147 | }
148 |
149 | NotesList.propTypes = {
150 | isPinnedList: PropTypes.bool.isRequired,
151 | notes: PropTypes.object,
152 | structure: PropTypes.object
153 | };
154 |
155 | const mapStateToProps = (state) => {
156 | return {
157 | notes: { ...state.notes.notes },
158 | structure: { ...state.notes.noteStructure },
159 | isLoggedIn: state.auth.isLoggedIn,
160 | userId: state.auth.user && state.auth.user.uid ? state.auth.user.uid : ''
161 | };
162 | };
163 |
164 | export default connect(mapStateToProps, { updateStructureLocally })(NotesList);
165 |
--------------------------------------------------------------------------------
/src/components/NotesList/NotesListMobile.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import Note from './Note';
4 |
5 | const NotesListMobile = ({ notes }) => {
6 | return (
7 |
15 | {Object.values(notes).map((note, index) => (
16 |
17 | ))}
18 |
19 | );
20 | };
21 | const mapStateToProps = (state) => {
22 | return {
23 | notes: state.notes.notes
24 | };
25 | };
26 |
27 | export default connect(mapStateToProps, {})(NotesListMobile);
28 |
--------------------------------------------------------------------------------
/src/components/NotesList/notes-list-elements.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.div`
4 | display: flex;
5 | justify-content: center;
6 | `;
7 | // position: relative;
8 |
9 | // SINGLE NOTE
10 |
11 | export const TextNote = styled.p`
12 | word-break: break-word;
13 | white-space: pre-line;
14 | `;
15 |
16 | export const CheckListItem = styled.li`
17 | list-style: none;
18 | display: flex;
19 | align-items: center;
20 | margin-bottom: 5px;
21 | `;
22 |
23 | export const NoteContainer = styled.div`
24 | border: 1px solid lightgrey;
25 | border-radius: 8px;
26 | padding: 12px 0 10px 0px;
27 | margin-bottom: 15px;
28 | background-color: ${(props) => props.bgColor};
29 | cursor: pointer;
30 | min-width: ${(props) => (props.isDraggable ? 'fit-content' : '80%')};
31 | max-width: 200px;
32 | box-shadow: ${(props) =>
33 | props.isHovered ? '0px 0px 5px -2px rgba(0,0,0,0.75)' : ''};
34 | &:hover {
35 | cursor: default;
36 | }
37 | `;
38 |
39 | export const Title = styled.h4`
40 | margin-bottom: 5px;
41 | overflow: hidden;
42 | font-weight: 300;
43 | display: flex;
44 | justify-content: space-between;
45 | `;
46 |
47 | export const NoteContent = styled.div`
48 | padding-left: 15px;
49 | padding-right: 15px;
50 | `;
51 |
52 | /// COLUMN
53 |
54 | export const ColumnContainer = styled.div`
55 | margin: 8px;
56 | border-radius: 2px;
57 | width: 220px;
58 | display: flex;
59 | flex-direction: column;
60 | `;
61 | export const TaskList = styled.div`
62 | transition: background-color 0.2s ease;
63 | flex-grow: 1;
64 | min-height: 100px;
65 | `;
66 |
--------------------------------------------------------------------------------
/src/components/TagList/TagList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import styled from 'styled-components';
3 |
4 | const DeleteIcon = styled.span`
5 | opacity: ${(props) => (props.isHovered ? 1 : 0)};
6 | transition: all 0.2s;
7 | margin-left: 2px;
8 | cursor: pointer;
9 | `;
10 |
11 | export default function TagList({ tags, setTags, size = 'medium' }) {
12 | return (
13 | <>
14 |
23 | {tags.map((tag) => (
24 |
25 | ))}
26 |
27 | >
28 | );
29 | }
30 |
31 | const Tag = ({ tag, setTags, tags, size }) => {
32 | const [isHovered, setIsHovered] = useState(false);
33 |
34 | return (
35 | setIsHovered(true)}
37 | onMouseLeave={() => setIsHovered(false)}
38 | style={{
39 | color: '#666',
40 | background: 'rgba(129, 126, 121, 0.188)',
41 | borderRadius: '20px',
42 | padding: '3px 7px',
43 | margin: size === 'small' ? '3px 1px' : '5px 2px',
44 | fontSize: size === 'small' ? '12px' : ''
45 | }}
46 | key={tag}
47 | >
48 | {tag}
49 | {
52 | const newTags = tags.filter((el) => el !== tag);
53 | setTags(newTags);
54 | }}
55 | >
56 |
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/components/TagWidget/AvaiableTags.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { TagList, Tag, Checkbox, Label } from "./widget-elements";
4 |
5 | function AvaiableTags({ tags, setTags, chosenTags }) {
6 | const handleCheckboxChange = e => {
7 | const isChecked = e.target.checked;
8 | const tag = e.target.value;
9 | if (isChecked) {
10 | setTags([...chosenTags, tag]);
11 | } else {
12 | setTags(chosenTags.filter(chosenTag => chosenTag !== tag));
13 | }
14 | };
15 |
16 | return (
17 |
18 | {tags.length !== 0 &&
19 | tags.map(tag => (
20 |
21 |
27 |
28 |
29 | ))}
30 |
31 | );
32 | }
33 |
34 | AvaiableTags.propTypes = {
35 | tags: PropTypes.array.isRequired,
36 | setTags: PropTypes.func.isRequired,
37 | chosenTags: PropTypes.array.isRequired
38 | };
39 |
40 | export default AvaiableTags;
41 |
--------------------------------------------------------------------------------
/src/components/TagWidget/TagWidget.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import {
3 | TagWidgetContainer,
4 | TagFormContainer,
5 | Icon,
6 | IconButton, //change path
7 | WidgetTitle,
8 | TagInput,
9 | AddTagBtn
10 | } from './widget-elements';
11 | import './scrollbar.css';
12 | import { connect } from 'react-redux';
13 | import { addTag } from '../../redux/notes';
14 | import AvaiableTags from './AvaiableTags';
15 |
16 | function TagWidget({
17 | chosenTags,
18 | setTags,
19 | tags = [],
20 | addTag,
21 | isHovered = true
22 | }) {
23 | const [isWidgetOpen, setIsWidgetOpen] = useState(false);
24 | const [newTagName, setNewTagName] = useState('');
25 |
26 | const handleSubmit = (e) => {
27 | e.preventDefault();
28 | const tag = newTagName.trim();
29 | if (tag !== '' && tags.every((tag) => newTagName.trim() !== tag.name)) {
30 | addTag({ name: tag }); //Redux action creator
31 | setTags([...chosenTags, tag]); //Hook for note form // TODO: check if there is no better practise for that
32 | setNewTagName('');
33 | }
34 | };
35 |
36 | useEffect(() => {
37 | setIsWidgetOpen(false);
38 | }, [isHovered]);
39 |
40 | const checkDuplicates = () => {
41 | return tags.every((tag) => newTagName.trim() !== tag.name);
42 | };
43 |
44 | const handleChange = (e) => {
45 | setNewTagName(e.target.value);
46 | };
47 |
48 | let tagsToDisplay;
49 | if (newTagName.trim() === '') {
50 | tagsToDisplay = tags;
51 | } else {
52 | tagsToDisplay = tags.filter((tag) => tag.name.includes(newTagName));
53 | }
54 |
55 | return (
56 |
57 | setIsWidgetOpen(!isWidgetOpen)}>
58 |
59 |
60 | {isWidgetOpen && (
61 |
62 | Etykieta notatki
63 |
79 |
80 |
85 | {newTagName && checkDuplicates() && (
86 |
87 | {' '}
88 | Utwórz etykietę "{newTagName}"
89 |
90 | )}
91 |
92 | )}
93 |
94 | );
95 | }
96 |
97 | const mapStateToProps = (state) => {
98 | return {
99 | tags: state.notes.tags
100 | };
101 | };
102 |
103 | const mapDispatchToProps = {
104 | addTag
105 | };
106 |
107 | export default connect(mapStateToProps, mapDispatchToProps)(TagWidget);
108 |
--------------------------------------------------------------------------------
/src/components/TagWidget/scrollbar.css:
--------------------------------------------------------------------------------
1 | ::-webkit-scrollbar {
2 | width: 10px;
3 | }
4 |
5 | /* Track */
6 | ::-webkit-scrollbar-track {
7 | background: #f1f1f1;
8 | }
9 |
10 | /* Handle */
11 | ::-webkit-scrollbar-thumb {
12 | background: #aaa;
13 | }
14 |
15 | /* Handle on hover */
16 | ::-webkit-scrollbar-thumb:hover {
17 | background: #888;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/TagWidget/widget-elements.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | export const TagWidgetContainer = styled.div`
3 | position: relative;
4 | `;
5 | export const TagFormContainer = styled.div`
6 | position: absolute;
7 | top: 30px;
8 | left: -50px;
9 | z-index: 10;
10 | background: #fff;
11 | box-shadow: 0px 0px 2px 1px rgba(122, 122, 122, 0.5);
12 | width: fit-content;
13 | min-height: 160px;
14 | max-height: 260px;
15 | overflow: hidden;
16 | padding: 8px 0px 0px 0px;
17 | border-radius: 3px;
18 | `;
19 | export const Icon = styled.span`
20 | margin-right: 13px;
21 | `;
22 | export const IconButton = styled.button`
23 | color: #666;
24 | border: none;
25 | background: transparent;
26 | outline: none;
27 | cursor: pointer;
28 | &:hover {
29 | color: #333;
30 | }
31 | `;
32 | export const WidgetTitle = styled.p`
33 | margin: 0 13px;
34 | font-size: 16px;
35 | color: #333;
36 | `;
37 | export const TagInput = styled.input`
38 | margin: 15px 13px;
39 | outline: none;
40 | width: 180px;
41 | border: none;
42 | color: #666;
43 | font-size: 14px;
44 | line-height: 1.2;
45 | letter-spacing: 0.4px;
46 | `;
47 |
48 | export const AddTagBtn = styled.button`
49 | width: 100%;
50 | background: #fff;
51 | font-size: 14px;
52 | color: #666;
53 | background: #fff;
54 | border: none;
55 | outline: none;
56 | border-top: 1px solid rgba(220, 220, 220, 1);
57 | position: sticky;
58 | bottom: 0;
59 | left: 0;
60 | text-align: left;
61 | padding: 5px 13px;
62 | cursor: pointer;
63 | &:hover {
64 | background: rgb(247, 247, 247);
65 | }
66 | `;
67 | export const TagList = styled.ul`
68 | padding: 10px 0;
69 | overflow-y: scroll;
70 | height: 150px;
71 | list-style: none;
72 | `;
73 |
74 | export const Tag = styled.li`
75 | width: 100%;
76 | display: flex;
77 | align-items: center;
78 | padding: 3px 13px;
79 | &:hover {
80 | background: rgb(247, 247, 247);
81 | }
82 | `;
83 | export const Checkbox = styled.input`
84 | background: transparent;
85 | `;
86 | export const Label = styled.label`
87 | margin-left: 7px;
88 | color: #666;
89 | `;
90 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | import Navigation from './Navigation/Navigation';
2 | import Footer from './Footer/Footer';
3 |
4 | export { Navigation, Footer };
5 |
--------------------------------------------------------------------------------
/src/containers/About/About.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // @media (max-width: 959px) {
4 | // min-height: calc(100vh - 100px);
5 | // }
6 |
7 | function About() {
8 | return (
9 |
10 |
11 |
Google keep clone v1.0.0 is finished
12 |
Finally first version of google keep clone is available
13 |
14 | It was cool project for me where I could tested plenty of features and
15 | approaches
16 |
17 |
18 | Basic features which I included in my version are:
19 |
20 |
23 | -
24 | Possible to create own account trough password and email or Google
25 | account
26 |
27 | - Basic mobile version
28 | - CRUD in notes
29 | -
30 | DRAG&DROP feature
31 |
32 | -
33 | Note could has such things:
34 |
41 | - Title
42 | - Content of note or Bullet list
43 | - Specific background color for grouping
44 | - Tags
45 | - Pinned or not
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export default About;
55 |
--------------------------------------------------------------------------------
/src/containers/Home/Home.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AuthForm from '../../components/AuthForm/AuthForm';
3 | import styled from 'styled-components';
4 |
5 | const HomeContainer = styled.div`
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | min-height: 84vh;
10 | box-sizing: border-box;
11 | @media (max-width: 959px) {
12 | min-height: calc(100vh - 100px);
13 | }
14 | `;
15 |
16 | function Home() {
17 | return (
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | export default Home;
25 |
--------------------------------------------------------------------------------
/src/containers/Notes/Notes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { NotesContainer } from '../../components/NoteForm/NoteFormElements';
4 | import NoteForm from '../../components/NoteForm/NoteForm';
5 | import NotesList from '../../components/NotesList/NotesList';
6 | import EditNoteModal from '../../components/EditNoteModal/EditNoteModal';
7 | import NotesListMobile from '../../components/NotesList/NotesListMobile';
8 | import ScrollUpButton from 'react-scroll-up-button';
9 | // import { clearEditNote } from '../../redux/notes';
10 |
11 | function Notes({ notes }) {
12 | return (
13 |
14 |
15 | {window.innerWidth >= 700 ? (
16 | <>
17 |
28 | {notes.find((note) => note.isPinned === true) &&
PRZYPIĘTE
}
29 |
30 |
31 |
41 | {notes.find((note) => note.isPinned === false) &&
INNE
}
42 |
43 |
44 | >
45 | ) : (
46 | <>
47 |
48 | >
49 | )}
50 |
51 |
52 |
53 | );
54 | }
55 |
56 | const mapStateToProps = (state) => {
57 | return {
58 | notes: [...Object.values(state.notes.notes)]
59 | };
60 | };
61 |
62 | export default connect(mapStateToProps, {})(Notes);
63 |
--------------------------------------------------------------------------------
/src/containers/index.js:
--------------------------------------------------------------------------------
1 | import Notes from './Notes/Notes';
2 | import Home from './Home/Home';
3 | import About from './About/About';
4 |
5 | export { Notes, Home, About };
6 |
--------------------------------------------------------------------------------
/src/firebase/firebaseAPI.js:
--------------------------------------------------------------------------------
1 | import { app } from './firebaseConfig';
2 | import 'firebase/firestore';
3 | import { store } from '../redux/storeConfig';
4 | import {
5 | getNotes,
6 | getTags,
7 | getNoteStructure
8 | // updateStructureLocally
9 | } from '../redux/notes';
10 | import * as firebase from 'firebase/app';
11 | import { auth } from './firebaseAuth';
12 |
13 | const db = app.firestore();
14 | // const user = firebase.auth().currentUser;
15 | // if (auth.currentUser) {
16 | export const getNotesFromDB = (currentUserId) => {
17 | return db
18 | .collection('notes')
19 | .where('authorId', '==', currentUserId)
20 | .onSnapshot(
21 | (snapshot) => {
22 | const notes = {};
23 | snapshot.forEach((el) => {
24 | notes[el.id] = { ...el.data(), id: el.id };
25 | });
26 | store.dispatch(getNotes(notes));
27 | },
28 | (err) => {
29 | console.log(err);
30 | }
31 | );
32 | };
33 | export const getTagsFromDB = (currentUserId) => {
34 | return db
35 | .collection('tags')
36 | .where('authorId', '==', currentUserId)
37 | .onSnapshot(
38 | (snapshot) => {
39 | const tags = [];
40 | snapshot.forEach((el) => {
41 | tags.push({ ...el.data(), id: el.id });
42 | });
43 | store.dispatch(getTags(tags));
44 | },
45 | (err) => {
46 | console.log(err);
47 | }
48 | );
49 | };
50 | export const getStructureFromDBv2 = (currentUserId) => {
51 | return db
52 | .collection('structure')
53 | .where('authorId', '==', currentUserId)
54 | .onSnapshot(
55 | (doc) => {
56 | let structure;
57 | doc.forEach((el) => {
58 | structure = { ...el.data(), structureId: el.id };
59 | });
60 | if (!structure) {
61 | structure = {
62 | authorId: currentUserId,
63 | 'column-1': {
64 | id: 'column-1',
65 | tasksIds: []
66 | },
67 | 'column-2': {
68 | id: 'column-2',
69 | tasksIds: []
70 | },
71 | 'column-3': {
72 | id: 'column-3',
73 | tasksIds: []
74 | },
75 | 'column-4': {
76 | id: 'column-4',
77 | tasksIds: []
78 | },
79 | 'column-5': {
80 | id: 'column-5',
81 | tasksIds: []
82 | },
83 | 'column-6': {
84 | id: 'column-6',
85 | tasksIds: []
86 | },
87 | 'column-7': {
88 | id: 'column-7',
89 | tasksIds: []
90 | },
91 | 'column-8': {
92 | id: 'column-8',
93 | tasksIds: []
94 | }
95 | };
96 |
97 | db.collection('structure')
98 | .add(structure)
99 | .then((docRef) => {
100 | store.dispatch(
101 | getNoteStructure({ ...structure, structureId: docRef.id })
102 | );
103 | });
104 | } else {
105 | store.dispatch(getNoteStructure(structure));
106 | }
107 | },
108 | (err) => {
109 | console.log(err);
110 | }
111 | );
112 | };
113 | // }
114 |
115 | export const updateNote = async (fields, id) => {
116 | return db
117 | .collection('notes')
118 | .doc(id)
119 | .update(fields)
120 | .then(function() {
121 | // console.log('Document successfully updated!');
122 | })
123 | .catch(function(error) {
124 | console.error('Error updating document: ', error);
125 | });
126 | };
127 |
128 | export const getStructureFromDB = (currentUserId) => {
129 | return db
130 | .collection('structure')
131 | .where('authorId', '==', currentUserId)
132 | .get()
133 | .then(
134 | function(doc) {
135 | let structure;
136 | doc.forEach((el) => {
137 | structure = { ...el.data(), structureId: el.id };
138 | });
139 | store.dispatch(getNoteStructure(structure));
140 | },
141 | function(error) {
142 | //...
143 | }
144 | );
145 | };
146 |
147 | export const pushUidToStructure = (uuid, col) => {
148 | const structureId = store.getState().notes.noteStructure.structureId;
149 |
150 | return db
151 | .collection('structure')
152 | .doc(structureId)
153 | .set(
154 | {
155 | [`column-${col}`]: {
156 | id: `column-${col}`,
157 | tasksIds: firebase.firestore.FieldValue.arrayUnion(uuid)
158 | }
159 | },
160 | { merge: true }
161 | );
162 | };
163 |
164 | export const removeUidFromStructure = (uuid, column) => {
165 | const structureId = store.getState().notes.noteStructure.structureId;
166 |
167 | return db
168 | .collection('structure')
169 | .doc(structureId)
170 | .set(
171 | {
172 | [column]: {
173 | id: column,
174 | tasksIds: firebase.firestore.FieldValue.arrayRemove(uuid)
175 | }
176 | },
177 | { merge: true }
178 | );
179 | };
180 |
181 | export const updateStructure = (newStructure) => {
182 | const structureId = store.getState().notes.noteStructure.structureId;
183 |
184 | db.collection('structure')
185 | .doc(structureId)
186 | .update(newStructure)
187 | .then(function() {
188 | // console.log('Document successfully updated!');
189 | })
190 | .catch(function(error) {
191 | console.error('Error updating document: ', error);
192 | });
193 | };
194 |
195 | export const getNotesDB = () => db.collection('test1').get();
196 |
197 | export const updatePositionOnNoteList = (id, column, row) =>
198 | db
199 | .collection('notes')
200 | .doc(id)
201 | .update({
202 | column,
203 | row
204 | })
205 | .then(function() {
206 | // console.log('Document successfully updated!');
207 | })
208 | .catch(function(error) {
209 | console.error('Error updating document: ', error);
210 | });
211 |
212 | export const addNoteToDB = (note) => {
213 | return db
214 | .collection('notes')
215 | .add({ ...note, authorId: auth.currentUser.uid })
216 | .then(() => {
217 | pushUidToStructure(note.uuid, note.column);
218 | })
219 | .catch((err) => console.error(err));
220 | };
221 | export const removeNoteFromDB = (note, column) => {
222 | return db
223 | .collection('notes')
224 | .doc(note.id)
225 | .delete()
226 | .then(() => removeUidFromStructure(note.uuid, column))
227 | .catch((err) => console.error(err));
228 | };
229 |
230 | export const addTagToDB = (tag) => {
231 | db.collection('tags')
232 | .add({ ...tag, authorId: auth.currentUser.uid })
233 | .then(() => {
234 | // console.warn('added tag');
235 | })
236 | .catch((err) => console.error(err));
237 | };
238 |
--------------------------------------------------------------------------------
/src/firebase/firebaseAuth.js:
--------------------------------------------------------------------------------
1 | import * as firebase from 'firebase/app';
2 | import { app } from './firebaseConfig';
3 | import 'firebase/auth';
4 | import { logIn, logOut } from '../redux/auth';
5 | import { store } from '../redux/storeConfig';
6 | import {
7 | getStructureFromDBv2,
8 | getTagsFromDB,
9 | getNotesFromDB
10 | } from './firebaseAPI';
11 |
12 | const googleProvider = new firebase.auth.GoogleAuthProvider();
13 | const facebookProvider = new firebase.auth.FacebookAuthProvider();
14 | export const auth = app.auth();
15 |
16 | // let notesUnsubscribe, tagsUnsubscribe, structureUnsubscribe;
17 |
18 | auth.onAuthStateChanged((user) => {
19 | if (user) {
20 | const { email, uid } = user;
21 | store.dispatch(logIn({ email, uid }));
22 | // notesUnsubscribe =
23 | // tagsUnsubscribe =
24 | // structureUnsubscribe =
25 | getNotesFromDB(uid);
26 | getTagsFromDB(uid);
27 | getStructureFromDBv2(uid);
28 | } else {
29 | //redux action which is invoking after signOut (firebase)
30 | // notesUnsubscribe();
31 | // tagsUnsubscribe();
32 | // structureUnsubscribe();
33 | store.dispatch(logOut());
34 | }
35 | });
36 |
37 | export const signInWithGoogle = () => {
38 | auth
39 | .signInWithPopup(googleProvider)
40 | .then((result) => {
41 | // const { uid, email } = auth.currentUser();
42 | // store.dispatch(logIn({ email, uid }));
43 | })
44 | .catch((err) => {
45 | console.error(err);
46 | // var errorCode = error.code;
47 | // var errorMessage = error.message;
48 | });
49 | };
50 | export const signInWithFacebook = () => {
51 | auth
52 | .signInWithPopup(facebookProvider)
53 | .then((result) => {})
54 | .catch((error) => {
55 | const errorCode = error.code;
56 | const credential = error.credential;
57 | if (errorCode === 'auth/account-exists-with-different-credential') {
58 | const email = error.email;
59 | auth.fetchSignInMethodsForEmail(email).then((methods) => {
60 | if (methods[0] === 'password') {
61 | const newPassword = prompt('Enter password: ');
62 | auth
63 | .signInWithEmailAndPassword(email, newPassword)
64 | .then(({ user }) => {
65 | return user.linkWithCredential(credential);
66 | })
67 | .catch((err) => console.error(err));
68 | }
69 | });
70 | }
71 | });
72 | };
73 | export const createUserWithEmailAndPassword = (email, password) => {
74 | return auth.createUserWithEmailAndPassword(email, password);
75 | };
76 | export const signInWithEmailAndPassword = (email, password) => {
77 | return auth.signInWithEmailAndPassword(email, password);
78 | };
79 | //sign out from firebase service
80 | export const signOut = () => {
81 | auth.signOut();
82 | };
83 |
--------------------------------------------------------------------------------
/src/firebase/firebaseConfig.js:
--------------------------------------------------------------------------------
1 | import * as firebase from 'firebase/app';
2 |
3 | const firebaseConfig = {
4 | apiKey: 'AIzaSyDkwiqR5UIPumMbcAFjUEXqY1gTHjA3_kQ',
5 | authDomain: 'keep-clone-app.firebaseapp.com',
6 | databaseURL: 'https://keep-clone-app.firebaseio.com',
7 | projectId: 'keep-clone-app',
8 | storageBucket: '',
9 | messagingSenderId: '324619407062',
10 | appId: '1:324619407062:web:c66d15ae773c4655'
11 | };
12 |
13 | export const app = firebase.initializeApp(firebaseConfig);
14 |
--------------------------------------------------------------------------------
/src/images/font/fontello.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simon125/google-keep-clone/744b52c5b2a839a8612a0d6248f77881c66c4ec4/src/images/font/fontello.eot
--------------------------------------------------------------------------------
/src/images/font/fontello.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/images/font/fontello.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simon125/google-keep-clone/744b52c5b2a839a8612a0d6248f77881c66c4ec4/src/images/font/fontello.ttf
--------------------------------------------------------------------------------
/src/images/font/fontello.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simon125/google-keep-clone/744b52c5b2a839a8612a0d6248f77881c66c4ec4/src/images/font/fontello.woff
--------------------------------------------------------------------------------
/src/images/font/fontello.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simon125/google-keep-clone/744b52c5b2a839a8612a0d6248f77881c66c4ec4/src/images/font/fontello.woff2
--------------------------------------------------------------------------------
/src/images/fontello.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "fontello";
3 | src: url("./font/fontello.eot?54282943");
4 | src: url("./font/fontello.eot?54282943#iefix") format("embedded-opentype"),
5 | url("./font/fontello.woff2?54282943") format("woff2"),
6 | url("./font/fontello.woff?54282943") format("woff"),
7 | url("./font/fontello.ttf?54282943") format("truetype"),
8 | url("./font/fontello.svg?54282943#fontello") format("svg");
9 | font-weight: normal;
10 | font-style: normal;
11 | }
12 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
13 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
14 | /*
15 | @media screen and (-webkit-min-device-pixel-ratio:0) {
16 | @font-face {
17 | font-family: 'fontello';
18 | src: url('../font/fontello.svg?54282943#fontello') format('svg');
19 | }
20 | }
21 | */
22 |
23 | [class^="icon-"]:before,
24 | [class*=" icon-"]:before {
25 | font-family: "fontello";
26 | font-style: normal;
27 | font-weight: normal;
28 | speak: none;
29 |
30 | display: inline-block;
31 | text-decoration: inherit;
32 | width: 1em;
33 | margin-right: 0.2em;
34 | text-align: center;
35 | /* opacity: .8; */
36 |
37 | /* For safety - reset parent styles, that can break glyph codes*/
38 | font-variant: normal;
39 | text-transform: none;
40 |
41 | /* fix buttons height, for twitter bootstrap */
42 | line-height: 1em;
43 |
44 | /* Animation center compensation - margins should be symmetric */
45 | /* remove if not needed */
46 | margin-left: 0.2em;
47 |
48 | /* you can be more comfortable with increased icons size */
49 | /* font-size: 120%; */
50 |
51 | /* Font smoothing. That was taken from TWBS */
52 | -webkit-font-smoothing: antialiased;
53 | -moz-osx-font-smoothing: grayscale;
54 |
55 | /* Uncomment for 3D effect */
56 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
57 | }
58 |
59 | .icon-pin-outline:before {
60 | content: "\e803";
61 | } /* '' */
62 | .icon-pin:before {
63 | content: "\e804";
64 | } /* '' */
65 |
--------------------------------------------------------------------------------
/src/images/pin-in-diagonal-position.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simon125/google-keep-clone/744b52c5b2a839a8612a0d6248f77881c66c4ec4/src/images/pin-in-diagonal-position.png
--------------------------------------------------------------------------------
/src/images/pin-outline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simon125/google-keep-clone/744b52c5b2a839a8612a0d6248f77881c66c4ec4/src/images/pin-outline.png
--------------------------------------------------------------------------------
/src/images/pin1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simon125/google-keep-clone/744b52c5b2a839a8612a0d6248f77881c66c4ec4/src/images/pin1.png
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css?family=Nunito+Sans&display=swap");
2 |
3 | *,
4 | body {
5 | margin: 0;
6 | padding: 0;
7 | box-sizing: border-box;
8 | font-family: "Nunito Sans", sans-serif;
9 | }
10 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import "normalize.css";
5 | import "./index.css";
6 | import "./images/fontello.css";
7 |
8 | ReactDOM.render(, document.getElementById("root"));
9 |
--------------------------------------------------------------------------------
/src/redux/auth.js:
--------------------------------------------------------------------------------
1 | const LOG_IN = "LOG_IN";
2 | const LOG_OUT = "LOG_OUT";
3 |
4 | export const logIn = user => {
5 | return {
6 | type: LOG_IN,
7 | isLoggedIn: true,
8 | payload: user
9 | };
10 | };
11 | export const logOut = () => {
12 | return {
13 | type: LOG_OUT,
14 | payload: null
15 | };
16 | };
17 |
18 | const initialState = {
19 | isLoggedIn: false,
20 | user: null
21 | };
22 |
23 | export const auth = (state = initialState, action) => {
24 | switch (action.type) {
25 | case "LOG_IN":
26 | return {
27 | ...state,
28 | isLoggedIn: true,
29 | user: action.payload
30 | };
31 | case "LOG_OUT":
32 | return {
33 | ...state,
34 | isLoggedIn: false,
35 | user: null
36 | };
37 | default:
38 | return { ...state };
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/src/redux/notes.js:
--------------------------------------------------------------------------------
1 | import { addNoteToDB, addTagToDB } from '../firebase/firebaseAPI';
2 |
3 | const REMOVE_NOTE = 'REMOVE_NOTE';
4 | const GET_NOTES = 'GET_NOTES';
5 | const GET_TAGS = 'GET_TAGS';
6 | const SET_LAST_INDEX = 'SET_LAST_INDEX';
7 | const GET_NOTE_STRUCTURE = 'GET_NOTE_STRUCTURE';
8 | const UPDATE_STRUCTURE = 'UPDATE_STRUCTURE';
9 | const EDIT_NOTE = 'EDIT_NOTE';
10 | const CLEAR_EDIT_FORM = 'CLEAR_EDIT_FORM';
11 |
12 | export const addNote = (note) => (dispatch, getState) => {
13 | addNoteToDB(note);
14 | };
15 | export const getNotes = (notes) => {
16 | return {
17 | type: GET_NOTES,
18 | payload: notes
19 | };
20 | };
21 | export const deleteNote = (note) => ({
22 | type: REMOVE_NOTE,
23 | payload: note.id
24 | });
25 | export const addTag = (tag) => (dispatch, getState) => {
26 | addTagToDB(tag);
27 | };
28 | export const getTags = (tags) => {
29 | return {
30 | type: GET_TAGS,
31 | payload: tags
32 | };
33 | };
34 |
35 | export const setLastIndex = (lastIndex) => {
36 | return {
37 | type: SET_LAST_INDEX,
38 | payload: lastIndex
39 | };
40 | };
41 | export const getNoteStructure = (noteStructure) => {
42 | return {
43 | type: GET_NOTE_STRUCTURE,
44 | payload: noteStructure
45 | };
46 | };
47 | export const updateStructureLocally = (newStructure) => {
48 | return {
49 | type: UPDATE_STRUCTURE,
50 | payload: newStructure
51 | };
52 | };
53 |
54 | export const editNote = (note) => {
55 | return {
56 | type: EDIT_NOTE,
57 | payload: note
58 | };
59 | };
60 |
61 | export const clearEditNote = () => {
62 | return {
63 | type: CLEAR_EDIT_FORM
64 | };
65 | };
66 |
67 | const initialState = {
68 | notes: [],
69 | structure: {},
70 | tags: [],
71 | noteStructure: {},
72 | lastIndex: 0,
73 | editedNote: {}
74 | };
75 |
76 | export const notes = (state = initialState, action) => {
77 | switch (action.type) {
78 | case 'GET_NOTES':
79 | return {
80 | ...state,
81 | notes: action.payload
82 | };
83 | case 'REMOVE_NOTE':
84 | const newNotes = {};
85 | for (let prop in state.notes) {
86 | if (prop !== action.payload) {
87 | newNotes[prop] = state.notes[prop];
88 | }
89 | }
90 | return {
91 | ...state,
92 | notes: { ...newNotes }
93 | };
94 | case 'GET_TAGS':
95 | return {
96 | ...state,
97 | tags: action.payload
98 | };
99 | case 'SET_LAST_INDEX':
100 | return {
101 | ...state,
102 | lastIndex: action.payload
103 | };
104 | case 'GET_NOTE_STRUCTURE':
105 | return {
106 | ...state,
107 | noteStructure: action.payload
108 | };
109 | case 'UPDATE_STRUCTURE':
110 | return {
111 | ...state,
112 | noteStructure: action.payload
113 | };
114 | case 'EDIT_NOTE':
115 | return {
116 | ...state,
117 | editedNote: action.payload
118 | };
119 | case 'CLEAR_EDIT_FORM':
120 | return {
121 | ...state,
122 | editedNote: {}
123 | };
124 | // case "DELETE_NOTE":
125 | // return {
126 | // ...state,
127 | // isLoggedIn: false,
128 | // user: null
129 | // };
130 | default:
131 | return { ...state };
132 | }
133 | };
134 |
--------------------------------------------------------------------------------
/src/redux/storeConfig.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
2 | import ReduxThunk from 'redux-thunk';
3 | import { auth } from './auth';
4 | import { notes } from './notes';
5 |
6 | const combinedReducers = combineReducers({ auth, notes });
7 |
8 | export const store = createStore(
9 | combinedReducers,
10 | compose(
11 | applyMiddleware(ReduxThunk)
12 | // window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
13 | )
14 | );
15 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import uuid from 'uuid';
2 |
3 | // getListBasedOnLineTextBreak move to the file which is using this function
4 | export const getListBasedOnLineTextBreak = (text) => {
5 | return text.split(/\r?\n/).reduce((newCheckList, nameOfListItem) => {
6 | const uid = uuid();
7 | return nameOfListItem.trim() === ''
8 | ? { ...newCheckList }
9 | : {
10 | ...newCheckList,
11 | [uid]: {
12 | listItem: nameOfListItem,
13 | uid
14 | }
15 | };
16 | }, {});
17 | };
18 | export const getSingleNoteBasedOnList = (list) => {
19 | return Object.values(list)
20 | .map((listItem) => listItem.listItem)
21 | .join('\r\n');
22 | };
23 | export const checkIfTargetIsForm = (target) => {
24 | if (!target) return false;
25 | const className = target.className;
26 | if (className && className.includes && className.includes('note-form')) {
27 | return true;
28 | }
29 | return checkIfTargetIsForm(target.parentElement);
30 | };
31 |
--------------------------------------------------------------------------------