├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── index.js
├── main
│ └── Main.jsx
├── sidebar
│ └── Sidebar.jsx
├── App.jsx
└── App.css
├── .gitignore
├── README.md
└── package.json
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrgrimshaw/notes-app-tutorial/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrgrimshaw/notes-app-tutorial/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrgrimshaw/notes-app-tutorial/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App.jsx";
4 |
5 | ReactDOM.render(
6 |
7 |
8 | ,
9 | document.getElementById("root")
10 | );
11 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Notes App Tutorial
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Tutorial
6 |
7 | This source code is from my React.js project guide. Watch it here: https://www.youtube.com/watch?v=ulOKYl5sHGk&t=1932s
8 |
9 | ## Available Scripts
10 |
11 | In the project directory, you can run:
12 |
13 | ### `npm start`
14 |
15 | Runs the app in the development mode.\
16 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
17 |
18 | The page will reload if you make edits.\
19 | You will also see any lint errors in the console.
20 |
21 | ### `npm run build`
22 |
23 | Builds the app for production to the `build` folder.\
24 | It correctly bundles React in production mode and optimizes the build for the best performance.
25 |
26 | The build is minified and the filenames include the hashes.\
27 | Your app is ready to be deployed!
28 |
29 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
30 |
31 | ## Acknowledgements
32 |
33 | (c) 2021 James Grimshaw
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "homepage": "https://jrgrimshaw.github.io/notes-app-tutorial",
3 | "name": "notes-app",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "@testing-library/jest-dom": "^5.11.9",
8 | "@testing-library/react": "^11.2.5",
9 | "@testing-library/user-event": "^12.6.3",
10 | "react": "^17.0.1",
11 | "react-dom": "^17.0.1",
12 | "react-markdown": "^5.0.3",
13 | "react-scripts": "4.0.2",
14 | "react-uuid": "^1.0.2",
15 | "web-vitals": "^1.1.0"
16 | },
17 | "scripts": {
18 | "predeploy": "npm run build",
19 | "deploy": "gh-pages -d build",
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject"
24 | },
25 | "eslintConfig": {
26 | "extends": [
27 | "react-app",
28 | "react-app/jest"
29 | ]
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | },
43 | "devDependencies": {
44 | "gh-pages": "^3.1.0"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/Main.jsx:
--------------------------------------------------------------------------------
1 | import ReactMarkdown from "react-markdown";
2 |
3 | const Main = ({ activeNote, onUpdateNote }) => {
4 | const onEditField = (field, value) => {
5 | onUpdateNote({
6 | ...activeNote,
7 | [field]: value,
8 | lastModified: Date.now(),
9 | });
10 | };
11 |
12 | if (!activeNote) return
No Active Note
;
13 |
14 | return (
15 |
16 |
17 | onEditField("title", e.target.value)}
23 | autoFocus
24 | />
25 |
32 |
33 |
{activeNote.title}
34 |
35 | {activeNote.body}
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default Main;
43 |
--------------------------------------------------------------------------------
/src/sidebar/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | const Sidebar = ({
2 | notes,
3 | onAddNote,
4 | onDeleteNote,
5 | activeNote,
6 | setActiveNote,
7 | }) => {
8 | const sortedNotes = notes.sort((a, b) => b.lastModified - a.lastModified);
9 |
10 | return (
11 |
12 |
13 |
Notes
14 |
15 |
16 |
17 | {sortedNotes.map(({ id, title, body, lastModified }, i) => (
18 |
setActiveNote(id)}
21 | >
22 |
23 | {title}
24 |
25 |
26 |
27 |
{body && body.substr(0, 100) + "..."}
28 |
29 | Last Modified{" "}
30 | {new Date(lastModified).toLocaleDateString("en-GB", {
31 | hour: "2-digit",
32 | minute: "2-digit",
33 | })}
34 |
35 |
36 | ))}
37 |
38 |
39 | );
40 | };
41 |
42 | export default Sidebar;
43 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import uuid from "react-uuid";
3 | import "./App.css";
4 | import Main from "./main/Main";
5 | import Sidebar from "./sidebar/Sidebar";
6 |
7 | function App() {
8 | const [notes, setNotes] = useState(
9 | localStorage.notes ? JSON.parse(localStorage.notes) : []
10 | );
11 | const [activeNote, setActiveNote] = useState(false);
12 |
13 | useEffect(() => {
14 | localStorage.setItem("notes", JSON.stringify(notes));
15 | }, [notes]);
16 |
17 | const onAddNote = () => {
18 | const newNote = {
19 | id: uuid(),
20 | title: "Untitled Note",
21 | body: "",
22 | lastModified: Date.now(),
23 | };
24 |
25 | setNotes([newNote, ...notes]);
26 | setActiveNote(newNote.id);
27 | };
28 |
29 | const onDeleteNote = (noteId) => {
30 | setNotes(notes.filter(({ id }) => id !== noteId));
31 | };
32 |
33 | const onUpdateNote = (updatedNote) => {
34 | const updatedNotesArr = notes.map((note) => {
35 | if (note.id === updatedNote.id) {
36 | return updatedNote;
37 | }
38 |
39 | return note;
40 | });
41 |
42 | setNotes(updatedNotesArr);
43 | };
44 |
45 | const getActiveNote = () => {
46 | return notes.find(({ id }) => id === activeNote);
47 | };
48 |
49 | return (
50 |
51 |
58 |
59 |
60 | );
61 | }
62 |
63 | export default App;
64 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | @import url("https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css");
2 |
3 | * {
4 | box-sizing: border-box;
5 | }
6 |
7 | /* GLOBAL STYLES */
8 |
9 | body,
10 | .App {
11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
12 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
13 | width: 100%;
14 | height: 100vh;
15 | overflow: hidden;
16 | font-size: 16px;
17 | background: url(https://www.toptal.com/designers/subtlepatterns/patterns/lightpaperfibers.png);
18 | }
19 |
20 | button {
21 | background: none;
22 | border: 0;
23 | margin: 0;
24 | padding: 0;
25 | font-size: inherit;
26 | font-family: inherit;
27 | cursor: pointer;
28 | color: #08c;
29 | }
30 | button:hover {
31 | color: #04c;
32 | }
33 |
34 | .App {
35 | display: flex;
36 | }
37 |
38 | /* SIDEBAR STYLES */
39 |
40 | .app-sidebar {
41 | width: 30%;
42 | height: 100vh;
43 | border-right: 1px solid #ddd;
44 | }
45 |
46 | .app-sidebar-header {
47 | display: flex;
48 | justify-content: space-between;
49 | padding: 25px;
50 | }
51 | .app-sidebar-header h1 {
52 | margin: 0;
53 | }
54 |
55 | .app-sidebar-notes {
56 | height: calc(100vh - 78px);
57 | overflow-y: scroll;
58 | }
59 | .app-sidebar-note {
60 | padding: 25px;
61 | cursor: pointer;
62 | }
63 | .sidebar-note-title {
64 | display: flex;
65 | justify-content: space-between;
66 | }
67 | .app-sidebar-note button {
68 | color: crimson;
69 | }
70 | .app-sidebar-note p {
71 | margin: 10px 0;
72 | }
73 | .app-sidebar-note small {
74 | display: block;
75 | color: #999;
76 | }
77 |
78 | .app-sidebar-note:hover {
79 | background: #ddd;
80 | }
81 | .app-sidebar-note.active,
82 | .app-sidebar-note.active small {
83 | background: #08c;
84 | color: white;
85 | }
86 |
87 | /* NOTE EDITOR/PREVIEW (MAIN) STYLES */
88 |
89 | .app-main {
90 | width: 70%;
91 | height: 100vh;
92 | }
93 | .app-main-note-edit,
94 | .app-main-note-preview {
95 | height: 50vh;
96 | }
97 |
98 | .no-active-note {
99 | width: 70%;
100 | height: 100vh;
101 | line-height: 100vh;
102 | text-align: center;
103 | font-size: 2rem;
104 | color: #999;
105 | }
106 |
107 | /* Editing */
108 | .app-main-note-edit {
109 | padding: 25px;
110 | }
111 |
112 | .app-main-note-edit input,
113 | textarea {
114 | display: block;
115 | border: 1px solid #ddd;
116 | margin-bottom: 20px;
117 | width: 100%;
118 | height: calc(50vh - 130px);
119 | padding: 10px;
120 | resize: none;
121 | font-size: inherit;
122 | font-family: inherit;
123 | }
124 | .app-main-note-edit input {
125 | height: 50px;
126 | font-size: 2rem;
127 | }
128 |
129 | /* Preview */
130 | .app-main-note-preview {
131 | border-top: 1px solid #ddd;
132 | overflow-y: scroll;
133 | background: rgba(0, 0, 0, 0.02);
134 | }
135 |
136 | .preview-title {
137 | padding: 25px 25px 0 25px;
138 | margin: 0;
139 | }
140 |
141 | .markdown-preview {
142 | padding: 0 25px 25px 25px;
143 | font-size: 1rem;
144 | line-height: 2rem;
145 | }
146 |
--------------------------------------------------------------------------------