├── .gitattributes ├── public ├── notes.png ├── robots.txt ├── favicon.ico ├── manifest.json └── index.html ├── craco.config.js ├── src ├── index.js ├── index.css ├── components │ ├── NotesList.js │ ├── Header.js │ ├── NoteView.js │ ├── Note.js │ ├── Tag.js │ ├── Search.js │ └── NoteForm.js └── App.js ├── tailwind.config.js ├── .gitignore ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /public/notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellodeborahuk/coding-notebook/HEAD/public/notes.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellodeborahuk/coding-notebook/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | // craco.config.js 2 | module.exports = { 3 | style: { 4 | postcss: { 5 | plugins: [require("tailwindcss"), require("autoprefixer")], 6 | }, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* ./src/index.css */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | 7 | .Note { 8 | min-height: 170px; 9 | } 10 | 11 | .dark-mode { 12 | background-color: #14532D; 13 | 14 | } 15 | 16 | .dark-mode h1 { 17 | color: 18 | #F0FDF4; 19 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/components/NotesList.js: -------------------------------------------------------------------------------- 1 | import Note from "./Note"; 2 | import NoteForm from "./NoteForm"; 3 | 4 | const NotesList = ({ notes, handleAddNote, handleDeleteNote, handleEditNote }) => { 5 | return ( 6 |
7 | 8 | {notes.map((note) => ( 9 | 10 | ))} 11 |
12 | ); 13 | }; 14 | 15 | export default NotesList; 16 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import { FaMoon, FaSun } from "react-icons/fa"; 2 | 3 | const Header = ({handleToggleDarkMode, darkMode}) => { 4 | 5 | const icon = darkMode? :; 6 | 7 | return ( 8 |
9 |

Notes

10 | 18 |
19 | ); 20 | } 21 | 22 | export default Header; -------------------------------------------------------------------------------- /src/components/NoteView.js: -------------------------------------------------------------------------------- 1 | 2 | function NoteView({note}) { 3 | 4 | return ( 5 | <> 6 |
7 |

{note.title}

8 |
9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 |

{note.text}

17 |
18 | {note.tags.map((tag, index) => { 19 | return ( 20 |
24 | {tag} 25 |
26 | ); 27 | })} 28 |
29 |
30 | 31 | ); 32 | } 33 | 34 | export default NoteView; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coding-notebook", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^6.2.0", 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^11.2.7", 9 | "@testing-library/user-event": "^12.8.3", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-icons": "^4.2.0", 13 | "react-scripts": "4.0.3", 14 | "web-vitals": "^1.1.2" 15 | }, 16 | "scripts": { 17 | "start": "craco start", 18 | "build": "craco build", 19 | "test": "craco test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "autoprefixer": "^9.8.6", 42 | "postcss": "^7.0.36", 43 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.7" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Note.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { MdDeleteForever } from "react-icons/md"; 3 | import { MdEdit } from "react-icons/md"; 4 | import NoteView from "./NoteView"; 5 | import NoteForm from "./NoteForm"; 6 | 7 | const Note = ({ note, handleDeleteNote, handleEditNote }) => { 8 | const [editing, setEditing] = useState(false); 9 | 10 | const editHandler = (data) => { 11 | handleEditNote(data); 12 | setEditing(false); 13 | }; 14 | 15 | const footer = editing ? ( 16 | "" 17 | ) : ( 18 |
19 | {note.date} 20 |
21 | setEditing(true)} 26 | /> 27 | handleDeleteNote(note.id)} 29 | className="delete-icon text-green-900 hover:text-green-600 cursor-pointer" 30 | size="1.3em" 31 | /> 32 |
33 |
34 | ); 35 | 36 | return ( 37 |
38 | {editing ? ( 39 | 40 | ) : ( 41 | 42 | )} 43 | {footer} 44 |
45 | ); 46 | }; 47 | export default Note; 48 | -------------------------------------------------------------------------------- /src/components/Tag.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | function Tag({ updateTags, clearTags, defaultTags }) { 4 | const [tags, setTags] = useState([]); 5 | 6 | useEffect(() => { 7 | setTags(defaultTags || []); 8 | // eslint-disable-next-line 9 | }, [clearTags]); 10 | 11 | useEffect(() => { 12 | updateTags(tags); 13 | // eslint-disable-next-line 14 | }, [tags]); 15 | 16 | const addTag = (event) => { 17 | if (event.key === "Enter") { 18 | if (event.target.value.length > 0) { 19 | setTags([...tags, event.target.value]); 20 | 21 | event.target.value = ""; 22 | } 23 | } 24 | }; 25 | const removeTag = (removedTag) => { 26 | const newTags = tags.filter((tag) => tag !== removedTag); 27 | setTags(newTags); 28 | }; 29 | 30 | return ( 31 |
32 |
33 | {tags.map((tag, index) => { 34 | return ( 35 |
39 | {tag}{" "} 40 | removeTag(tag)} 43 | > 44 | x 45 | 46 |
47 | ); 48 | })} 49 |
50 | 55 |
56 | ); 57 | } 58 | 59 | export default Tag; 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notes app 2 | 3 | ![Notes app gif](https://user-images.githubusercontent.com/29425781/154816386-2c00b005-a4ec-4bbb-a4f2-5b17047c0db6.gif) 4 | 5 | ## About the project 6 | 7 | I wanted to create an CRUD project that would allow me to track my notes from #100DaysOfCode challenge on Twitter. 8 | 9 | I followed a [tutorial on YouTube](https://www.youtube.com/watch?v=8KB3DHI-QbM) where I encountered issues as I wanted inputs from two fields. This was also a challenge when it came to searching within the two fields. 10 | 11 | I decided to use Tailwind CSS as it's something I've recently been learning and I really liked how simple it was to create the look of the note component, similar to a window. 12 | 13 | After the initial setup of the notes and saving them to local storage, I added tagging functionality. I could now add and remove multiple tags from each note. 14 | 15 | With the ability to add tags, I thought it would be useful to filter notes by tag so I added a filter button next to the search. 16 | 17 | To create the edit functionality I had to refactor my components so that the form stood alone but also was included in the Note. The Note component contains two further components: the form and the note view. This meant went the edit button was clicked it would show the form and when saved it would show the note view. 18 | 19 | ![coding-notebook-screenshot](https://user-images.githubusercontent.com/29425781/169498167-48b7c92e-77fa-4218-b7e1-fb470a1242b1.png) 20 | 21 | ## Technologies used 22 | 23 | * React 24 | * HTML 25 | * Tailwind CCS 26 | 27 | ## Roadmap 28 | 29 | 1. Save information to a database. 30 | 2. Ability to login. 31 | 3. Pin notes to the top. 32 | 33 | ## Contact 34 | 35 | Debbie Dann @debbie_digital on Twitter 36 | 37 | Project link: [#100DaysOfCode Notes](https://awesome-lamport-4fcaff.netlify.app/) 38 | -------------------------------------------------------------------------------- /src/components/Search.js: -------------------------------------------------------------------------------- 1 | import { MdSearch } from "react-icons/md"; 2 | import { useState } from "react"; 3 | 4 | const Search = ({ notes, handleSearchNote, handleTagFilter }) => { 5 | const [showTags, setShowTags] = useState(false); 6 | 7 | const onClick = () => { 8 | setShowTags(!showTags); 9 | handleTagFilter(null); 10 | }; 11 | 12 | const TagList = () => { 13 | const tags = notes.reduce((carry, note) => { 14 | note.tags.forEach((element) => { 15 | if (!carry.includes(element)) { 16 | carry.push(element); 17 | } 18 | }); 19 | return carry; 20 | }, []); 21 | return ( 22 |
23 | {tags.map((tag, index) => { 24 | return ( 25 | 32 | ); 33 | })} 34 |
35 | ); 36 | }; 37 | 38 | return ( 39 |
40 |
41 |
42 | 43 | handleSearchNote(event.target.value)} 45 | type="text" 46 | placeholder="Type to search..." 47 | className="bg-gray-200 lg:w-1/2" 48 | /> 49 |
50 | 56 |
57 |
{showTags ? : null}
58 |
59 | ); 60 | }; 61 | 62 | export default Search; 63 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 26 | Notes app 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/NoteForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Tag from "./Tag"; 3 | 4 | const AddNote = ({ handleAddNote, defaultState }) => { 5 | const [data, setData] = useState(defaultState || { title: "", text: "", tags: [] }); 6 | const [saveCounter, setSaveCounter] = useState(0); 7 | 8 | const handleChange = (event) => { 9 | setData({ ...data, [event.target.name]: event.target.value }); 10 | }; 11 | 12 | const updateTags = (tags) => { 13 | setData({...data, tags}); 14 | } 15 | 16 | const handleSaveClick = () => { 17 | if (data.title.trim().length > 0 && data.text.trim().length > 0) { 18 | handleAddNote(data); 19 | setData({ title: "", text: "", tags: [] }); // reset data to new object 20 | setSaveCounter(saveCounter +1); 21 | } 22 | }; 23 | 24 | return ( 25 |
26 |
27 | 35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 |
43 | 50 |
51 | 56 |
57 |
58 | 64 |
65 |
66 |
67 | ); 68 | }; 69 | 70 | export default AddNote; 71 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { nanoid } from "nanoid"; 3 | import NotesList from "./components/NotesList"; 4 | import Search from "./components/Search"; 5 | import Header from "./components/Header"; 6 | 7 | const App = () => { 8 | const [notes, setNotes] = useState([]); 9 | const [searchText, setSearchText] = useState(""); 10 | const [tagFilter, setTagFilter] = useState(null); 11 | const [darkMode, setDarkMode] = useState(false); 12 | 13 | useEffect(() => { 14 | const savedNotes = JSON.parse(localStorage.getItem("react-notes-app-data")); 15 | 16 | if (savedNotes) { 17 | setNotes(savedNotes); 18 | } 19 | }, []); // empty array runs only on first load 20 | 21 | useEffect(() => { 22 | localStorage.setItem("react-notes-app-data", JSON.stringify(notes)); 23 | }, [notes]); 24 | 25 | const addNote = (name) => { 26 | const date = new Date(); 27 | const newNote = { 28 | id: nanoid(), 29 | title: name.title, 30 | text: name.text, 31 | date: date.toLocaleDateString(), 32 | tags: name.tags, 33 | }; 34 | const newNotes = [...notes, newNote]; 35 | setNotes(newNotes); 36 | }; 37 | 38 | const editNote = (data) => { 39 | let tmp = [...notes]; 40 | const index = tmp.findIndex((note) => note.id === data.id); 41 | if (index > -1) { 42 | tmp.splice(index, 1, data); 43 | setNotes(tmp); 44 | } 45 | } 46 | 47 | const deleteNote = (id) => { 48 | const newNotes = notes.filter((note) => note.id !== id); 49 | setNotes(newNotes); 50 | }; 51 | 52 | const searchTagFilter = (note) => { 53 | if (tagFilter !== null) { 54 | return note.tags.includes(tagFilter); 55 | } 56 | 57 | return true; 58 | }; 59 | 60 | const searchTextFilter = (note) => { 61 | if (searchText !== "") { 62 | return ( 63 | note.title.toLowerCase().includes(searchText.toLowerCase()) || 64 | note.text.toLowerCase().includes(searchText.toLowerCase()) 65 | ); 66 | } 67 | return true; 68 | }; 69 | 70 | return ( 71 |
72 |
73 |
74 | 79 | 85 |
86 |
87 | ); 88 | }; 89 | 90 | export default App; 91 | --------------------------------------------------------------------------------