├── .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 |
13 | handleToggleDarkMode((previousDarkMode) => !previousDarkMode)
14 | }
15 | >
16 | {icon}
17 |
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 |
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 | 
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 | 
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 | handleTagFilter(tag)}
29 | >
30 | {tag}
31 |
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 |
54 | Filter
55 |
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 | You need to enable JavaScript to run this app.
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 |
41 |
42 |
43 |
50 |
51 |
56 |
57 |
58 |
62 | Save
63 |
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 |
--------------------------------------------------------------------------------