diff --git a/code/01-starting-project/package.json b/code/01-starting-project/package.json
new file mode 100644
index 0000000000..6c1b63a022
--- /dev/null
+++ b/code/01-starting-project/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/01-starting-project/public/favicon.ico b/code/01-starting-project/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/01-starting-project/public/favicon.ico differ
diff --git a/code/01-starting-project/public/index.html b/code/01-starting-project/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/01-starting-project/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/01-starting-project/public/logo192.png b/code/01-starting-project/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/01-starting-project/public/logo192.png differ
diff --git a/code/01-starting-project/public/logo512.png b/code/01-starting-project/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/01-starting-project/public/logo512.png differ
diff --git a/code/01-starting-project/public/manifest.json b/code/01-starting-project/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/01-starting-project/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/01-starting-project/public/robots.txt b/code/01-starting-project/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/01-starting-project/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/01-starting-project/src/App.js b/code/01-starting-project/src/App.js
new file mode 100644
index 0000000000..4a194e6079
--- /dev/null
+++ b/code/01-starting-project/src/App.js
@@ -0,0 +1,9 @@
+function App() {
+ return (
+
+
Let's get started!
+
+ );
+}
+
+export default App;
diff --git a/code/01-starting-project/src/index.css b/code/01-starting-project/src/index.css
new file mode 100644
index 0000000000..72399cc5c6
--- /dev/null
+++ b/code/01-starting-project/src/index.css
@@ -0,0 +1,15 @@
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ font-family: 'Noto Sans JP', sans-serif;
+}
+
+body {
+ margin: 0;
+ background-color: #3f3f3f;
+}
+
diff --git a/code/01-starting-project/src/index.js b/code/01-starting-project/src/index.js
new file mode 100644
index 0000000000..778ec1ba20
--- /dev/null
+++ b/code/01-starting-project/src/index.js
@@ -0,0 +1,7 @@
+import ReactDOM from 'react-dom/client';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render();
diff --git a/code/02-defining-and-using-routes/package.json b/code/02-defining-and-using-routes/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/02-defining-and-using-routes/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/02-defining-and-using-routes/public/favicon.ico b/code/02-defining-and-using-routes/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/02-defining-and-using-routes/public/favicon.ico differ
diff --git a/code/02-defining-and-using-routes/public/index.html b/code/02-defining-and-using-routes/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/02-defining-and-using-routes/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/02-defining-and-using-routes/public/logo192.png b/code/02-defining-and-using-routes/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/02-defining-and-using-routes/public/logo192.png differ
diff --git a/code/02-defining-and-using-routes/public/logo512.png b/code/02-defining-and-using-routes/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/02-defining-and-using-routes/public/logo512.png differ
diff --git a/code/02-defining-and-using-routes/public/manifest.json b/code/02-defining-and-using-routes/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/02-defining-and-using-routes/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/02-defining-and-using-routes/public/robots.txt b/code/02-defining-and-using-routes/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/02-defining-and-using-routes/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/02-defining-and-using-routes/src/App.js b/code/02-defining-and-using-routes/src/App.js
new file mode 100644
index 0000000000..86364d8498
--- /dev/null
+++ b/code/02-defining-and-using-routes/src/App.js
@@ -0,0 +1,22 @@
+import { Route } from 'react-router-dom';
+
+import Welcome from './pages/Welcome';
+import Products from './pages/Products';
+
+function App() {
+ return (
+
+ );
+}
+
+export default App;
+
+// our-domain.com/welcome => Welcome Component
+// our-domain.com/products => Products Component
\ No newline at end of file
diff --git a/code/02-defining-and-using-routes/src/index.css b/code/02-defining-and-using-routes/src/index.css
new file mode 100644
index 0000000000..72399cc5c6
--- /dev/null
+++ b/code/02-defining-and-using-routes/src/index.css
@@ -0,0 +1,15 @@
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ font-family: 'Noto Sans JP', sans-serif;
+}
+
+body {
+ margin: 0;
+ background-color: #3f3f3f;
+}
+
diff --git a/code/02-defining-and-using-routes/src/index.js b/code/02-defining-and-using-routes/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/02-defining-and-using-routes/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/02-defining-and-using-routes/src/pages/Products.js b/code/02-defining-and-using-routes/src/pages/Products.js
new file mode 100644
index 0000000000..5755202240
--- /dev/null
+++ b/code/02-defining-and-using-routes/src/pages/Products.js
@@ -0,0 +1,5 @@
+const Products = () => {
+ return The Products Page
;
+};
+
+export default Products;
\ No newline at end of file
diff --git a/code/02-defining-and-using-routes/src/pages/Welcome.js b/code/02-defining-and-using-routes/src/pages/Welcome.js
new file mode 100644
index 0000000000..1453342bae
--- /dev/null
+++ b/code/02-defining-and-using-routes/src/pages/Welcome.js
@@ -0,0 +1,5 @@
+const Welcome = () => {
+ return The Welcome Page
;
+};
+
+export default Welcome;
\ No newline at end of file
diff --git a/code/03-working-with-links/package.json b/code/03-working-with-links/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/03-working-with-links/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/03-working-with-links/public/favicon.ico b/code/03-working-with-links/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/03-working-with-links/public/favicon.ico differ
diff --git a/code/03-working-with-links/public/index.html b/code/03-working-with-links/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/03-working-with-links/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/03-working-with-links/public/logo192.png b/code/03-working-with-links/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/03-working-with-links/public/logo192.png differ
diff --git a/code/03-working-with-links/public/logo512.png b/code/03-working-with-links/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/03-working-with-links/public/logo512.png differ
diff --git a/code/03-working-with-links/public/manifest.json b/code/03-working-with-links/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/03-working-with-links/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/03-working-with-links/public/robots.txt b/code/03-working-with-links/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/03-working-with-links/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/03-working-with-links/src/App.js b/code/03-working-with-links/src/App.js
new file mode 100644
index 0000000000..52f9031155
--- /dev/null
+++ b/code/03-working-with-links/src/App.js
@@ -0,0 +1,26 @@
+import { Route } from 'react-router-dom';
+
+import Welcome from './pages/Welcome';
+import Products from './pages/Products';
+import MainHeader from './components/MainHeader';
+
+function App() {
+ return (
+
+ );
+}
+
+export default App;
+
+// our-domain.com/welcome => Welcome Component
+// our-domain.com/products => Products Component
diff --git a/code/03-working-with-links/src/components/MainHeader.js b/code/03-working-with-links/src/components/MainHeader.js
new file mode 100644
index 0000000000..66befe0eee
--- /dev/null
+++ b/code/03-working-with-links/src/components/MainHeader.js
@@ -0,0 +1,22 @@
+import { Link } from 'react-router-dom';
+
+import classes from './MainHeader.module.css';
+
+const MainHeader = () => {
+ return (
+
+
+
+ );
+};
+
+export default MainHeader;
diff --git a/code/03-working-with-links/src/components/MainHeader.module.css b/code/03-working-with-links/src/components/MainHeader.module.css
new file mode 100644
index 0000000000..ea62a274cf
--- /dev/null
+++ b/code/03-working-with-links/src/components/MainHeader.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ background-color: #044599;
+ padding: 0 10%;
+}
+
+.header nav {
+ height: 100%;
+}
+
+.header ul {
+ height: 100%;
+ list-style: none;
+ display: flex;
+ padding: 0;
+ margin: 0;
+ align-items: center;
+ justify-content: center;
+}
+
+.header li {
+ margin: 0 1rem;
+ width: 5rem;
+}
+
+.header a {
+ color: white;
+ text-decoration: none;
+}
+
+.header a:hover,
+.header a:active {
+ color: #95bcf0;
+ padding-bottom: 0.25rem;
+ border-bottom: 4px solid #95bcf0;
+}
\ No newline at end of file
diff --git a/code/03-working-with-links/src/index.css b/code/03-working-with-links/src/index.css
new file mode 100644
index 0000000000..44a860502a
--- /dev/null
+++ b/code/03-working-with-links/src/index.css
@@ -0,0 +1,26 @@
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ font-family: 'Noto Sans JP', sans-serif;
+}
+
+body {
+ margin: 0;
+ background-color: #e0e9f5;
+}
+
+main {
+ margin-top: 7rem;
+ text-align: center;
+}
+
+h1,
+h2,
+h3,
+p {
+ color: #042b5f;
+}
diff --git a/code/03-working-with-links/src/index.js b/code/03-working-with-links/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/03-working-with-links/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/03-working-with-links/src/pages/Products.js b/code/03-working-with-links/src/pages/Products.js
new file mode 100644
index 0000000000..5755202240
--- /dev/null
+++ b/code/03-working-with-links/src/pages/Products.js
@@ -0,0 +1,5 @@
+const Products = () => {
+ return The Products Page
;
+};
+
+export default Products;
\ No newline at end of file
diff --git a/code/03-working-with-links/src/pages/Welcome.js b/code/03-working-with-links/src/pages/Welcome.js
new file mode 100644
index 0000000000..1453342bae
--- /dev/null
+++ b/code/03-working-with-links/src/pages/Welcome.js
@@ -0,0 +1,5 @@
+const Welcome = () => {
+ return The Welcome Page
;
+};
+
+export default Welcome;
\ No newline at end of file
diff --git a/code/04-using-navlinks/package.json b/code/04-using-navlinks/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/04-using-navlinks/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/04-using-navlinks/public/favicon.ico b/code/04-using-navlinks/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/04-using-navlinks/public/favicon.ico differ
diff --git a/code/04-using-navlinks/public/index.html b/code/04-using-navlinks/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/04-using-navlinks/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/04-using-navlinks/public/logo192.png b/code/04-using-navlinks/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/04-using-navlinks/public/logo192.png differ
diff --git a/code/04-using-navlinks/public/logo512.png b/code/04-using-navlinks/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/04-using-navlinks/public/logo512.png differ
diff --git a/code/04-using-navlinks/public/manifest.json b/code/04-using-navlinks/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/04-using-navlinks/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/04-using-navlinks/public/robots.txt b/code/04-using-navlinks/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/04-using-navlinks/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/04-using-navlinks/src/App.js b/code/04-using-navlinks/src/App.js
new file mode 100644
index 0000000000..52f9031155
--- /dev/null
+++ b/code/04-using-navlinks/src/App.js
@@ -0,0 +1,26 @@
+import { Route } from 'react-router-dom';
+
+import Welcome from './pages/Welcome';
+import Products from './pages/Products';
+import MainHeader from './components/MainHeader';
+
+function App() {
+ return (
+
+ );
+}
+
+export default App;
+
+// our-domain.com/welcome => Welcome Component
+// our-domain.com/products => Products Component
diff --git a/code/04-using-navlinks/src/components/MainHeader.js b/code/04-using-navlinks/src/components/MainHeader.js
new file mode 100644
index 0000000000..5f5b28db4e
--- /dev/null
+++ b/code/04-using-navlinks/src/components/MainHeader.js
@@ -0,0 +1,26 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainHeader.module.css';
+
+const MainHeader = () => {
+ return (
+
+
+
+ );
+};
+
+export default MainHeader;
diff --git a/code/04-using-navlinks/src/components/MainHeader.module.css b/code/04-using-navlinks/src/components/MainHeader.module.css
new file mode 100644
index 0000000000..0d792b4c0d
--- /dev/null
+++ b/code/04-using-navlinks/src/components/MainHeader.module.css
@@ -0,0 +1,38 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ background-color: #044599;
+ padding: 0 10%;
+}
+
+.header nav {
+ height: 100%;
+}
+
+.header ul {
+ height: 100%;
+ list-style: none;
+ display: flex;
+ padding: 0;
+ margin: 0;
+ align-items: center;
+ justify-content: center;
+}
+
+.header li {
+ margin: 0 1rem;
+ width: 5rem;
+}
+
+.header a {
+ color: white;
+ text-decoration: none;
+}
+
+.header a:hover,
+.header a:active,
+.header a.active {
+ color: #95bcf0;
+ padding-bottom: 0.25rem;
+ border-bottom: 4px solid #95bcf0;
+}
\ No newline at end of file
diff --git a/code/04-using-navlinks/src/index.css b/code/04-using-navlinks/src/index.css
new file mode 100644
index 0000000000..44a860502a
--- /dev/null
+++ b/code/04-using-navlinks/src/index.css
@@ -0,0 +1,26 @@
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ font-family: 'Noto Sans JP', sans-serif;
+}
+
+body {
+ margin: 0;
+ background-color: #e0e9f5;
+}
+
+main {
+ margin-top: 7rem;
+ text-align: center;
+}
+
+h1,
+h2,
+h3,
+p {
+ color: #042b5f;
+}
diff --git a/code/04-using-navlinks/src/index.js b/code/04-using-navlinks/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/04-using-navlinks/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/04-using-navlinks/src/pages/Products.js b/code/04-using-navlinks/src/pages/Products.js
new file mode 100644
index 0000000000..5755202240
--- /dev/null
+++ b/code/04-using-navlinks/src/pages/Products.js
@@ -0,0 +1,5 @@
+const Products = () => {
+ return The Products Page
;
+};
+
+export default Products;
\ No newline at end of file
diff --git a/code/04-using-navlinks/src/pages/Welcome.js b/code/04-using-navlinks/src/pages/Welcome.js
new file mode 100644
index 0000000000..1453342bae
--- /dev/null
+++ b/code/04-using-navlinks/src/pages/Welcome.js
@@ -0,0 +1,5 @@
+const Welcome = () => {
+ return The Welcome Page
;
+};
+
+export default Welcome;
\ No newline at end of file
diff --git a/code/05-extracting-route-params/package.json b/code/05-extracting-route-params/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/05-extracting-route-params/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/05-extracting-route-params/public/favicon.ico b/code/05-extracting-route-params/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/05-extracting-route-params/public/favicon.ico differ
diff --git a/code/05-extracting-route-params/public/index.html b/code/05-extracting-route-params/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/05-extracting-route-params/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/05-extracting-route-params/public/logo192.png b/code/05-extracting-route-params/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/05-extracting-route-params/public/logo192.png differ
diff --git a/code/05-extracting-route-params/public/logo512.png b/code/05-extracting-route-params/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/05-extracting-route-params/public/logo512.png differ
diff --git a/code/05-extracting-route-params/public/manifest.json b/code/05-extracting-route-params/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/05-extracting-route-params/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/05-extracting-route-params/public/robots.txt b/code/05-extracting-route-params/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/05-extracting-route-params/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/05-extracting-route-params/src/App.js b/code/05-extracting-route-params/src/App.js
new file mode 100644
index 0000000000..a6fbf09ba1
--- /dev/null
+++ b/code/05-extracting-route-params/src/App.js
@@ -0,0 +1,31 @@
+import { Route } from 'react-router-dom';
+
+import Welcome from './pages/Welcome';
+import Products from './pages/Products';
+import ProductDetail from './pages/ProductDetail';
+import MainHeader from './components/MainHeader';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
+
+// our-domain.com/welcome => Welcome Component
+// our-domain.com/products => Products Component
+// our-domain.com/product-detail/a-book
diff --git a/code/05-extracting-route-params/src/components/MainHeader.js b/code/05-extracting-route-params/src/components/MainHeader.js
new file mode 100644
index 0000000000..5f5b28db4e
--- /dev/null
+++ b/code/05-extracting-route-params/src/components/MainHeader.js
@@ -0,0 +1,26 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainHeader.module.css';
+
+const MainHeader = () => {
+ return (
+
+
+
+ );
+};
+
+export default MainHeader;
diff --git a/code/05-extracting-route-params/src/components/MainHeader.module.css b/code/05-extracting-route-params/src/components/MainHeader.module.css
new file mode 100644
index 0000000000..0d792b4c0d
--- /dev/null
+++ b/code/05-extracting-route-params/src/components/MainHeader.module.css
@@ -0,0 +1,38 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ background-color: #044599;
+ padding: 0 10%;
+}
+
+.header nav {
+ height: 100%;
+}
+
+.header ul {
+ height: 100%;
+ list-style: none;
+ display: flex;
+ padding: 0;
+ margin: 0;
+ align-items: center;
+ justify-content: center;
+}
+
+.header li {
+ margin: 0 1rem;
+ width: 5rem;
+}
+
+.header a {
+ color: white;
+ text-decoration: none;
+}
+
+.header a:hover,
+.header a:active,
+.header a.active {
+ color: #95bcf0;
+ padding-bottom: 0.25rem;
+ border-bottom: 4px solid #95bcf0;
+}
\ No newline at end of file
diff --git a/code/05-extracting-route-params/src/index.css b/code/05-extracting-route-params/src/index.css
new file mode 100644
index 0000000000..44a860502a
--- /dev/null
+++ b/code/05-extracting-route-params/src/index.css
@@ -0,0 +1,26 @@
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ font-family: 'Noto Sans JP', sans-serif;
+}
+
+body {
+ margin: 0;
+ background-color: #e0e9f5;
+}
+
+main {
+ margin-top: 7rem;
+ text-align: center;
+}
+
+h1,
+h2,
+h3,
+p {
+ color: #042b5f;
+}
diff --git a/code/05-extracting-route-params/src/index.js b/code/05-extracting-route-params/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/05-extracting-route-params/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/05-extracting-route-params/src/pages/ProductDetail.js b/code/05-extracting-route-params/src/pages/ProductDetail.js
new file mode 100644
index 0000000000..3923681f8d
--- /dev/null
+++ b/code/05-extracting-route-params/src/pages/ProductDetail.js
@@ -0,0 +1,16 @@
+import { useParams } from 'react-router-dom';
+
+const ProductDetail = () => {
+ const params = useParams();
+
+ console.log(params.productId);
+
+ return (
+
+ Product Detail
+ {params.productId}
+
+ );
+};
+
+export default ProductDetail;
diff --git a/code/05-extracting-route-params/src/pages/Products.js b/code/05-extracting-route-params/src/pages/Products.js
new file mode 100644
index 0000000000..7212d35cb6
--- /dev/null
+++ b/code/05-extracting-route-params/src/pages/Products.js
@@ -0,0 +1,14 @@
+const Products = () => {
+ return (
+
+ The Products Page
+
+ - A Book
+ - A Carpet
+ - An Online Course
+
+
+ );
+};
+
+export default Products;
diff --git a/code/05-extracting-route-params/src/pages/Welcome.js b/code/05-extracting-route-params/src/pages/Welcome.js
new file mode 100644
index 0000000000..1453342bae
--- /dev/null
+++ b/code/05-extracting-route-params/src/pages/Welcome.js
@@ -0,0 +1,5 @@
+const Welcome = () => {
+ return The Welcome Page
;
+};
+
+export default Welcome;
\ No newline at end of file
diff --git a/code/06-using-switch-and-exact/package.json b/code/06-using-switch-and-exact/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/06-using-switch-and-exact/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/06-using-switch-and-exact/public/favicon.ico b/code/06-using-switch-and-exact/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/06-using-switch-and-exact/public/favicon.ico differ
diff --git a/code/06-using-switch-and-exact/public/index.html b/code/06-using-switch-and-exact/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/06-using-switch-and-exact/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/06-using-switch-and-exact/public/logo192.png b/code/06-using-switch-and-exact/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/06-using-switch-and-exact/public/logo192.png differ
diff --git a/code/06-using-switch-and-exact/public/logo512.png b/code/06-using-switch-and-exact/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/06-using-switch-and-exact/public/logo512.png differ
diff --git a/code/06-using-switch-and-exact/public/manifest.json b/code/06-using-switch-and-exact/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/06-using-switch-and-exact/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/06-using-switch-and-exact/public/robots.txt b/code/06-using-switch-and-exact/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/06-using-switch-and-exact/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/06-using-switch-and-exact/src/App.js b/code/06-using-switch-and-exact/src/App.js
new file mode 100644
index 0000000000..3b6e49c201
--- /dev/null
+++ b/code/06-using-switch-and-exact/src/App.js
@@ -0,0 +1,33 @@
+import { Route, Switch } from 'react-router-dom';
+
+import Welcome from './pages/Welcome';
+import Products from './pages/Products';
+import ProductDetail from './pages/ProductDetail';
+import MainHeader from './components/MainHeader';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
+
+// our-domain.com/welcome => Welcome Component
+// our-domain.com/products => Products Component
+// our-domain.com/product-detail/a-book
diff --git a/code/06-using-switch-and-exact/src/components/MainHeader.js b/code/06-using-switch-and-exact/src/components/MainHeader.js
new file mode 100644
index 0000000000..5f5b28db4e
--- /dev/null
+++ b/code/06-using-switch-and-exact/src/components/MainHeader.js
@@ -0,0 +1,26 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainHeader.module.css';
+
+const MainHeader = () => {
+ return (
+
+
+
+ );
+};
+
+export default MainHeader;
diff --git a/code/06-using-switch-and-exact/src/components/MainHeader.module.css b/code/06-using-switch-and-exact/src/components/MainHeader.module.css
new file mode 100644
index 0000000000..0d792b4c0d
--- /dev/null
+++ b/code/06-using-switch-and-exact/src/components/MainHeader.module.css
@@ -0,0 +1,38 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ background-color: #044599;
+ padding: 0 10%;
+}
+
+.header nav {
+ height: 100%;
+}
+
+.header ul {
+ height: 100%;
+ list-style: none;
+ display: flex;
+ padding: 0;
+ margin: 0;
+ align-items: center;
+ justify-content: center;
+}
+
+.header li {
+ margin: 0 1rem;
+ width: 5rem;
+}
+
+.header a {
+ color: white;
+ text-decoration: none;
+}
+
+.header a:hover,
+.header a:active,
+.header a.active {
+ color: #95bcf0;
+ padding-bottom: 0.25rem;
+ border-bottom: 4px solid #95bcf0;
+}
\ No newline at end of file
diff --git a/code/06-using-switch-and-exact/src/index.css b/code/06-using-switch-and-exact/src/index.css
new file mode 100644
index 0000000000..44a860502a
--- /dev/null
+++ b/code/06-using-switch-and-exact/src/index.css
@@ -0,0 +1,26 @@
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ font-family: 'Noto Sans JP', sans-serif;
+}
+
+body {
+ margin: 0;
+ background-color: #e0e9f5;
+}
+
+main {
+ margin-top: 7rem;
+ text-align: center;
+}
+
+h1,
+h2,
+h3,
+p {
+ color: #042b5f;
+}
diff --git a/code/06-using-switch-and-exact/src/index.js b/code/06-using-switch-and-exact/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/06-using-switch-and-exact/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/06-using-switch-and-exact/src/pages/ProductDetail.js b/code/06-using-switch-and-exact/src/pages/ProductDetail.js
new file mode 100644
index 0000000000..3923681f8d
--- /dev/null
+++ b/code/06-using-switch-and-exact/src/pages/ProductDetail.js
@@ -0,0 +1,16 @@
+import { useParams } from 'react-router-dom';
+
+const ProductDetail = () => {
+ const params = useParams();
+
+ console.log(params.productId);
+
+ return (
+
+ Product Detail
+ {params.productId}
+
+ );
+};
+
+export default ProductDetail;
diff --git a/code/06-using-switch-and-exact/src/pages/Products.js b/code/06-using-switch-and-exact/src/pages/Products.js
new file mode 100644
index 0000000000..834f867bfd
--- /dev/null
+++ b/code/06-using-switch-and-exact/src/pages/Products.js
@@ -0,0 +1,22 @@
+import { Link } from 'react-router-dom';
+
+const Products = () => {
+ return (
+
+ The Products Page
+
+ -
+ A Book
+
+ -
+ A Carpet
+
+ -
+ An Online Course
+
+
+
+ );
+};
+
+export default Products;
diff --git a/code/06-using-switch-and-exact/src/pages/Welcome.js b/code/06-using-switch-and-exact/src/pages/Welcome.js
new file mode 100644
index 0000000000..1453342bae
--- /dev/null
+++ b/code/06-using-switch-and-exact/src/pages/Welcome.js
@@ -0,0 +1,5 @@
+const Welcome = () => {
+ return The Welcome Page
;
+};
+
+export default Welcome;
\ No newline at end of file
diff --git a/code/07-working-with-nested-routes/package.json b/code/07-working-with-nested-routes/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/07-working-with-nested-routes/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/07-working-with-nested-routes/public/favicon.ico b/code/07-working-with-nested-routes/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/07-working-with-nested-routes/public/favicon.ico differ
diff --git a/code/07-working-with-nested-routes/public/index.html b/code/07-working-with-nested-routes/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/07-working-with-nested-routes/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/07-working-with-nested-routes/public/logo192.png b/code/07-working-with-nested-routes/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/07-working-with-nested-routes/public/logo192.png differ
diff --git a/code/07-working-with-nested-routes/public/logo512.png b/code/07-working-with-nested-routes/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/07-working-with-nested-routes/public/logo512.png differ
diff --git a/code/07-working-with-nested-routes/public/manifest.json b/code/07-working-with-nested-routes/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/07-working-with-nested-routes/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/07-working-with-nested-routes/public/robots.txt b/code/07-working-with-nested-routes/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/07-working-with-nested-routes/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/07-working-with-nested-routes/src/App.js b/code/07-working-with-nested-routes/src/App.js
new file mode 100644
index 0000000000..3b6e49c201
--- /dev/null
+++ b/code/07-working-with-nested-routes/src/App.js
@@ -0,0 +1,33 @@
+import { Route, Switch } from 'react-router-dom';
+
+import Welcome from './pages/Welcome';
+import Products from './pages/Products';
+import ProductDetail from './pages/ProductDetail';
+import MainHeader from './components/MainHeader';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
+
+// our-domain.com/welcome => Welcome Component
+// our-domain.com/products => Products Component
+// our-domain.com/product-detail/a-book
diff --git a/code/07-working-with-nested-routes/src/components/MainHeader.js b/code/07-working-with-nested-routes/src/components/MainHeader.js
new file mode 100644
index 0000000000..5f5b28db4e
--- /dev/null
+++ b/code/07-working-with-nested-routes/src/components/MainHeader.js
@@ -0,0 +1,26 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainHeader.module.css';
+
+const MainHeader = () => {
+ return (
+
+
+
+ );
+};
+
+export default MainHeader;
diff --git a/code/07-working-with-nested-routes/src/components/MainHeader.module.css b/code/07-working-with-nested-routes/src/components/MainHeader.module.css
new file mode 100644
index 0000000000..0d792b4c0d
--- /dev/null
+++ b/code/07-working-with-nested-routes/src/components/MainHeader.module.css
@@ -0,0 +1,38 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ background-color: #044599;
+ padding: 0 10%;
+}
+
+.header nav {
+ height: 100%;
+}
+
+.header ul {
+ height: 100%;
+ list-style: none;
+ display: flex;
+ padding: 0;
+ margin: 0;
+ align-items: center;
+ justify-content: center;
+}
+
+.header li {
+ margin: 0 1rem;
+ width: 5rem;
+}
+
+.header a {
+ color: white;
+ text-decoration: none;
+}
+
+.header a:hover,
+.header a:active,
+.header a.active {
+ color: #95bcf0;
+ padding-bottom: 0.25rem;
+ border-bottom: 4px solid #95bcf0;
+}
\ No newline at end of file
diff --git a/code/07-working-with-nested-routes/src/index.css b/code/07-working-with-nested-routes/src/index.css
new file mode 100644
index 0000000000..44a860502a
--- /dev/null
+++ b/code/07-working-with-nested-routes/src/index.css
@@ -0,0 +1,26 @@
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ font-family: 'Noto Sans JP', sans-serif;
+}
+
+body {
+ margin: 0;
+ background-color: #e0e9f5;
+}
+
+main {
+ margin-top: 7rem;
+ text-align: center;
+}
+
+h1,
+h2,
+h3,
+p {
+ color: #042b5f;
+}
diff --git a/code/07-working-with-nested-routes/src/index.js b/code/07-working-with-nested-routes/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/07-working-with-nested-routes/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/07-working-with-nested-routes/src/pages/ProductDetail.js b/code/07-working-with-nested-routes/src/pages/ProductDetail.js
new file mode 100644
index 0000000000..3923681f8d
--- /dev/null
+++ b/code/07-working-with-nested-routes/src/pages/ProductDetail.js
@@ -0,0 +1,16 @@
+import { useParams } from 'react-router-dom';
+
+const ProductDetail = () => {
+ const params = useParams();
+
+ console.log(params.productId);
+
+ return (
+
+ Product Detail
+ {params.productId}
+
+ );
+};
+
+export default ProductDetail;
diff --git a/code/07-working-with-nested-routes/src/pages/Products.js b/code/07-working-with-nested-routes/src/pages/Products.js
new file mode 100644
index 0000000000..834f867bfd
--- /dev/null
+++ b/code/07-working-with-nested-routes/src/pages/Products.js
@@ -0,0 +1,22 @@
+import { Link } from 'react-router-dom';
+
+const Products = () => {
+ return (
+
+ The Products Page
+
+ -
+ A Book
+
+ -
+ A Carpet
+
+ -
+ An Online Course
+
+
+
+ );
+};
+
+export default Products;
diff --git a/code/07-working-with-nested-routes/src/pages/Welcome.js b/code/07-working-with-nested-routes/src/pages/Welcome.js
new file mode 100644
index 0000000000..c5ac2fa18c
--- /dev/null
+++ b/code/07-working-with-nested-routes/src/pages/Welcome.js
@@ -0,0 +1,14 @@
+import { Route } from 'react-router-dom';
+
+const Welcome = () => {
+ return (
+
+ The Welcome Page
+
+ Welcome, new user!
+
+
+ );
+};
+
+export default Welcome;
diff --git a/code/08-redirecting-the-user/package.json b/code/08-redirecting-the-user/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/08-redirecting-the-user/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/08-redirecting-the-user/public/favicon.ico b/code/08-redirecting-the-user/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/08-redirecting-the-user/public/favicon.ico differ
diff --git a/code/08-redirecting-the-user/public/index.html b/code/08-redirecting-the-user/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/08-redirecting-the-user/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/08-redirecting-the-user/public/logo192.png b/code/08-redirecting-the-user/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/08-redirecting-the-user/public/logo192.png differ
diff --git a/code/08-redirecting-the-user/public/logo512.png b/code/08-redirecting-the-user/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/08-redirecting-the-user/public/logo512.png differ
diff --git a/code/08-redirecting-the-user/public/manifest.json b/code/08-redirecting-the-user/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/08-redirecting-the-user/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/08-redirecting-the-user/public/robots.txt b/code/08-redirecting-the-user/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/08-redirecting-the-user/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/08-redirecting-the-user/src/App.js b/code/08-redirecting-the-user/src/App.js
new file mode 100644
index 0000000000..b78259ef9c
--- /dev/null
+++ b/code/08-redirecting-the-user/src/App.js
@@ -0,0 +1,36 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import Welcome from './pages/Welcome';
+import Products from './pages/Products';
+import ProductDetail from './pages/ProductDetail';
+import MainHeader from './components/MainHeader';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
+
+// our-domain.com/welcome => Welcome Component
+// our-domain.com/products => Products Component
+// our-domain.com/product-detail/a-book
diff --git a/code/08-redirecting-the-user/src/components/MainHeader.js b/code/08-redirecting-the-user/src/components/MainHeader.js
new file mode 100644
index 0000000000..5f5b28db4e
--- /dev/null
+++ b/code/08-redirecting-the-user/src/components/MainHeader.js
@@ -0,0 +1,26 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainHeader.module.css';
+
+const MainHeader = () => {
+ return (
+
+
+
+ );
+};
+
+export default MainHeader;
diff --git a/code/08-redirecting-the-user/src/components/MainHeader.module.css b/code/08-redirecting-the-user/src/components/MainHeader.module.css
new file mode 100644
index 0000000000..0d792b4c0d
--- /dev/null
+++ b/code/08-redirecting-the-user/src/components/MainHeader.module.css
@@ -0,0 +1,38 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ background-color: #044599;
+ padding: 0 10%;
+}
+
+.header nav {
+ height: 100%;
+}
+
+.header ul {
+ height: 100%;
+ list-style: none;
+ display: flex;
+ padding: 0;
+ margin: 0;
+ align-items: center;
+ justify-content: center;
+}
+
+.header li {
+ margin: 0 1rem;
+ width: 5rem;
+}
+
+.header a {
+ color: white;
+ text-decoration: none;
+}
+
+.header a:hover,
+.header a:active,
+.header a.active {
+ color: #95bcf0;
+ padding-bottom: 0.25rem;
+ border-bottom: 4px solid #95bcf0;
+}
\ No newline at end of file
diff --git a/code/08-redirecting-the-user/src/index.css b/code/08-redirecting-the-user/src/index.css
new file mode 100644
index 0000000000..44a860502a
--- /dev/null
+++ b/code/08-redirecting-the-user/src/index.css
@@ -0,0 +1,26 @@
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ font-family: 'Noto Sans JP', sans-serif;
+}
+
+body {
+ margin: 0;
+ background-color: #e0e9f5;
+}
+
+main {
+ margin-top: 7rem;
+ text-align: center;
+}
+
+h1,
+h2,
+h3,
+p {
+ color: #042b5f;
+}
diff --git a/code/08-redirecting-the-user/src/index.js b/code/08-redirecting-the-user/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/08-redirecting-the-user/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/08-redirecting-the-user/src/pages/ProductDetail.js b/code/08-redirecting-the-user/src/pages/ProductDetail.js
new file mode 100644
index 0000000000..3923681f8d
--- /dev/null
+++ b/code/08-redirecting-the-user/src/pages/ProductDetail.js
@@ -0,0 +1,16 @@
+import { useParams } from 'react-router-dom';
+
+const ProductDetail = () => {
+ const params = useParams();
+
+ console.log(params.productId);
+
+ return (
+
+ Product Detail
+ {params.productId}
+
+ );
+};
+
+export default ProductDetail;
diff --git a/code/08-redirecting-the-user/src/pages/Products.js b/code/08-redirecting-the-user/src/pages/Products.js
new file mode 100644
index 0000000000..834f867bfd
--- /dev/null
+++ b/code/08-redirecting-the-user/src/pages/Products.js
@@ -0,0 +1,22 @@
+import { Link } from 'react-router-dom';
+
+const Products = () => {
+ return (
+
+ The Products Page
+
+ -
+ A Book
+
+ -
+ A Carpet
+
+ -
+ An Online Course
+
+
+
+ );
+};
+
+export default Products;
diff --git a/code/08-redirecting-the-user/src/pages/Welcome.js b/code/08-redirecting-the-user/src/pages/Welcome.js
new file mode 100644
index 0000000000..c5ac2fa18c
--- /dev/null
+++ b/code/08-redirecting-the-user/src/pages/Welcome.js
@@ -0,0 +1,14 @@
+import { Route } from 'react-router-dom';
+
+const Welcome = () => {
+ return (
+
+ The Welcome Page
+
+ Welcome, new user!
+
+
+ );
+};
+
+export default Welcome;
diff --git a/code/09-time-to-practice-starting-code/package.json b/code/09-time-to-practice-starting-code/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/09-time-to-practice-starting-code/public/favicon.ico b/code/09-time-to-practice-starting-code/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/09-time-to-practice-starting-code/public/favicon.ico differ
diff --git a/code/09-time-to-practice-starting-code/public/index.html b/code/09-time-to-practice-starting-code/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/09-time-to-practice-starting-code/public/logo192.png b/code/09-time-to-practice-starting-code/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/09-time-to-practice-starting-code/public/logo192.png differ
diff --git a/code/09-time-to-practice-starting-code/public/logo512.png b/code/09-time-to-practice-starting-code/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/09-time-to-practice-starting-code/public/logo512.png differ
diff --git a/code/09-time-to-practice-starting-code/public/manifest.json b/code/09-time-to-practice-starting-code/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/09-time-to-practice-starting-code/public/robots.txt b/code/09-time-to-practice-starting-code/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/09-time-to-practice-starting-code/src/App.js b/code/09-time-to-practice-starting-code/src/App.js
new file mode 100644
index 0000000000..05d9bcca2e
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/App.js
@@ -0,0 +1,9 @@
+function App() {
+ return (
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/09-time-to-practice-starting-code/src/components/UI/Card.js b/code/09-time-to-practice-starting-code/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/09-time-to-practice-starting-code/src/components/UI/Card.module.css b/code/09-time-to-practice-starting-code/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.js b/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.module.css b/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.js b/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.module.css b/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/09-time-to-practice-starting-code/src/components/comments/Comments.js b/code/09-time-to-practice-starting-code/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/09-time-to-practice-starting-code/src/components/comments/Comments.module.css b/code/09-time-to-practice-starting-code/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.js b/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.module.css b/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.js b/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.module.css b/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/09-time-to-practice-starting-code/src/components/layout/Layout.module.css b/code/09-time-to-practice-starting-code/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/09-time-to-practice-starting-code/src/components/layout/MainNavigation.module.css b/code/09-time-to-practice-starting-code/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.js b/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.module.css b/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.js b/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..14a83fa67c
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,14 @@
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.module.css b/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.js b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..9a49c7fb7c
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.js
@@ -0,0 +1,47 @@
+import { useRef } from 'react';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.module.css b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.js b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..06cf8b1221
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.js
@@ -0,0 +1,19 @@
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.module.css b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.js b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..a37943f11c
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.js
@@ -0,0 +1,23 @@
+import { Fragment } from 'react';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const QuoteList = (props) => {
+ return (
+
+
+ {props.quotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.module.css b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/09-time-to-practice-starting-code/src/index.css b/code/09-time-to-practice-starting-code/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/09-time-to-practice-starting-code/src/index.js b/code/09-time-to-practice-starting-code/src/index.js
new file mode 100644
index 0000000000..778ec1ba20
--- /dev/null
+++ b/code/09-time-to-practice-starting-code/src/index.js
@@ -0,0 +1,7 @@
+import ReactDOM from 'react-dom/client';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render();
diff --git a/code/10-practice-redirecting-and-extracting-params/package.json b/code/10-practice-redirecting-and-extracting-params/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/10-practice-redirecting-and-extracting-params/public/favicon.ico b/code/10-practice-redirecting-and-extracting-params/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/10-practice-redirecting-and-extracting-params/public/favicon.ico differ
diff --git a/code/10-practice-redirecting-and-extracting-params/public/index.html b/code/10-practice-redirecting-and-extracting-params/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/10-practice-redirecting-and-extracting-params/public/logo192.png b/code/10-practice-redirecting-and-extracting-params/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/10-practice-redirecting-and-extracting-params/public/logo192.png differ
diff --git a/code/10-practice-redirecting-and-extracting-params/public/logo512.png b/code/10-practice-redirecting-and-extracting-params/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/10-practice-redirecting-and-extracting-params/public/logo512.png differ
diff --git a/code/10-practice-redirecting-and-extracting-params/public/manifest.json b/code/10-practice-redirecting-and-extracting-params/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/10-practice-redirecting-and-extracting-params/public/robots.txt b/code/10-practice-redirecting-and-extracting-params/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/10-practice-redirecting-and-extracting-params/src/App.js b/code/10-practice-redirecting-and-extracting-params/src/App.js
new file mode 100644
index 0000000000..82436c9d54
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/App.js
@@ -0,0 +1,26 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.js b/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.js b/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.js b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.js b/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.js b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.js b/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/layout/Layout.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/layout/MainNavigation.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.js b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.js b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..14a83fa67c
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,14 @@
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.js b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..9a49c7fb7c
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.js
@@ -0,0 +1,47 @@
+import { useRef } from 'react';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.js b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..ee1ccb3ddc
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.js
@@ -0,0 +1,21 @@
+import { Link } from 'react-router-dom';
+
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.js b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..a37943f11c
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.js
@@ -0,0 +1,23 @@
+import { Fragment } from 'react';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const QuoteList = (props) => {
+ return (
+
+
+ {props.quotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/10-practice-redirecting-and-extracting-params/src/index.css b/code/10-practice-redirecting-and-extracting-params/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/10-practice-redirecting-and-extracting-params/src/index.js b/code/10-practice-redirecting-and-extracting-params/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/10-practice-redirecting-and-extracting-params/src/pages/AllQuotes.js b/code/10-practice-redirecting-and-extracting-params/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..0c50d04a6f
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/pages/AllQuotes.js
@@ -0,0 +1,5 @@
+const AllQuotes = () => {
+ return All Quotes Page
+};
+
+export default AllQuotes;
\ No newline at end of file
diff --git a/code/10-practice-redirecting-and-extracting-params/src/pages/NewQuote.js b/code/10-practice-redirecting-and-extracting-params/src/pages/NewQuote.js
new file mode 100644
index 0000000000..0b0d779a5c
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/pages/NewQuote.js
@@ -0,0 +1,5 @@
+const NewQuote = () => {
+ return New Quote Page
+};
+
+export default NewQuote;
\ No newline at end of file
diff --git a/code/10-practice-redirecting-and-extracting-params/src/pages/QuoteDetail.js b/code/10-practice-redirecting-and-extracting-params/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..9c5cd4c72e
--- /dev/null
+++ b/code/10-practice-redirecting-and-extracting-params/src/pages/QuoteDetail.js
@@ -0,0 +1,15 @@
+import { Fragment } from 'react';
+import { useParams } from 'react-router-dom';
+
+const QuoteDetail = () => {
+ const params = useParams();
+
+ return (
+
+ Quote Detail Page
+ {params.quoteId}
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/code/11-practicing-nested-routes/package.json b/code/11-practicing-nested-routes/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/11-practicing-nested-routes/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/11-practicing-nested-routes/public/favicon.ico b/code/11-practicing-nested-routes/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/11-practicing-nested-routes/public/favicon.ico differ
diff --git a/code/11-practicing-nested-routes/public/index.html b/code/11-practicing-nested-routes/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/11-practicing-nested-routes/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/11-practicing-nested-routes/public/logo192.png b/code/11-practicing-nested-routes/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/11-practicing-nested-routes/public/logo192.png differ
diff --git a/code/11-practicing-nested-routes/public/logo512.png b/code/11-practicing-nested-routes/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/11-practicing-nested-routes/public/logo512.png differ
diff --git a/code/11-practicing-nested-routes/public/manifest.json b/code/11-practicing-nested-routes/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/11-practicing-nested-routes/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/11-practicing-nested-routes/public/robots.txt b/code/11-practicing-nested-routes/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/11-practicing-nested-routes/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/11-practicing-nested-routes/src/App.js b/code/11-practicing-nested-routes/src/App.js
new file mode 100644
index 0000000000..82436c9d54
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/App.js
@@ -0,0 +1,26 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/11-practicing-nested-routes/src/components/UI/Card.js b/code/11-practicing-nested-routes/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/11-practicing-nested-routes/src/components/UI/Card.module.css b/code/11-practicing-nested-routes/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.js b/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.module.css b/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/11-practicing-nested-routes/src/components/comments/CommentItem.js b/code/11-practicing-nested-routes/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/11-practicing-nested-routes/src/components/comments/CommentItem.module.css b/code/11-practicing-nested-routes/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/11-practicing-nested-routes/src/components/comments/Comments.js b/code/11-practicing-nested-routes/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/11-practicing-nested-routes/src/components/comments/Comments.module.css b/code/11-practicing-nested-routes/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/11-practicing-nested-routes/src/components/comments/CommentsList.js b/code/11-practicing-nested-routes/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/11-practicing-nested-routes/src/components/comments/CommentsList.module.css b/code/11-practicing-nested-routes/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.js b/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.module.css b/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/11-practicing-nested-routes/src/components/layout/Layout.module.css b/code/11-practicing-nested-routes/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/11-practicing-nested-routes/src/components/layout/MainNavigation.module.css b/code/11-practicing-nested-routes/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.js b/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.module.css b/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.js b/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..14a83fa67c
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,14 @@
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.module.css b/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.js b/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..9a49c7fb7c
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.js
@@ -0,0 +1,47 @@
+import { useRef } from 'react';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.module.css b/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.js b/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..ee1ccb3ddc
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.js
@@ -0,0 +1,21 @@
+import { Link } from 'react-router-dom';
+
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.module.css b/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteList.js b/code/11-practicing-nested-routes/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..a37943f11c
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteList.js
@@ -0,0 +1,23 @@
+import { Fragment } from 'react';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const QuoteList = (props) => {
+ return (
+
+
+ {props.quotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteList.module.css b/code/11-practicing-nested-routes/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/11-practicing-nested-routes/src/index.css b/code/11-practicing-nested-routes/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/11-practicing-nested-routes/src/index.js b/code/11-practicing-nested-routes/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/11-practicing-nested-routes/src/pages/AllQuotes.js b/code/11-practicing-nested-routes/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..0c50d04a6f
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/pages/AllQuotes.js
@@ -0,0 +1,5 @@
+const AllQuotes = () => {
+ return All Quotes Page
+};
+
+export default AllQuotes;
\ No newline at end of file
diff --git a/code/11-practicing-nested-routes/src/pages/NewQuote.js b/code/11-practicing-nested-routes/src/pages/NewQuote.js
new file mode 100644
index 0000000000..0b0d779a5c
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/pages/NewQuote.js
@@ -0,0 +1,5 @@
+const NewQuote = () => {
+ return New Quote Page
+};
+
+export default NewQuote;
\ No newline at end of file
diff --git a/code/11-practicing-nested-routes/src/pages/QuoteDetail.js b/code/11-practicing-nested-routes/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..5631383a1d
--- /dev/null
+++ b/code/11-practicing-nested-routes/src/pages/QuoteDetail.js
@@ -0,0 +1,20 @@
+import { Fragment } from 'react';
+import { useParams, Route } from 'react-router-dom';
+
+import Comments from '../components/comments/Comments';
+
+const QuoteDetail = () => {
+ const params = useParams();
+
+ return (
+
+ Quote Detail Page
+ {params.quoteId}
+
+
+
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/code/12-adding-a-layout-wrapper/package.json b/code/12-adding-a-layout-wrapper/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/12-adding-a-layout-wrapper/public/favicon.ico b/code/12-adding-a-layout-wrapper/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/12-adding-a-layout-wrapper/public/favicon.ico differ
diff --git a/code/12-adding-a-layout-wrapper/public/index.html b/code/12-adding-a-layout-wrapper/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/12-adding-a-layout-wrapper/public/logo192.png b/code/12-adding-a-layout-wrapper/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/12-adding-a-layout-wrapper/public/logo192.png differ
diff --git a/code/12-adding-a-layout-wrapper/public/logo512.png b/code/12-adding-a-layout-wrapper/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/12-adding-a-layout-wrapper/public/logo512.png differ
diff --git a/code/12-adding-a-layout-wrapper/public/manifest.json b/code/12-adding-a-layout-wrapper/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/12-adding-a-layout-wrapper/public/robots.txt b/code/12-adding-a-layout-wrapper/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/12-adding-a-layout-wrapper/src/App.js b/code/12-adding-a-layout-wrapper/src/App.js
new file mode 100644
index 0000000000..5f90130429
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/App.js
@@ -0,0 +1,29 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+import Layout from './components/layout/Layout';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/12-adding-a-layout-wrapper/src/components/UI/Card.js b/code/12-adding-a-layout-wrapper/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/12-adding-a-layout-wrapper/src/components/UI/Card.module.css b/code/12-adding-a-layout-wrapper/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.js b/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.module.css b/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.js b/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.module.css b/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/Comments.js b/code/12-adding-a-layout-wrapper/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/Comments.module.css b/code/12-adding-a-layout-wrapper/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.js b/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.module.css b/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.js b/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.module.css b/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/12-adding-a-layout-wrapper/src/components/layout/Layout.js b/code/12-adding-a-layout-wrapper/src/components/layout/Layout.js
new file mode 100644
index 0000000000..2eff00a625
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/layout/Layout.js
@@ -0,0 +1,15 @@
+import { Fragment } from 'react';
+
+import classes from './Layout.module.css';
+import MainNavigation from './MainNavigation';
+
+const Layout = (props) => {
+ return (
+
+
+ {props.children}
+
+ );
+};
+
+export default Layout;
diff --git a/code/12-adding-a-layout-wrapper/src/components/layout/Layout.module.css b/code/12-adding-a-layout-wrapper/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.js b/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.js
new file mode 100644
index 0000000000..76d790a1c3
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.js
@@ -0,0 +1,27 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+const MainNavigation = () => {
+ return (
+
+ Great Quotes
+
+
+ );
+};
+
+export default MainNavigation;
diff --git a/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.module.css b/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.js b/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.module.css b/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.js b/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..14a83fa67c
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,14 @@
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.module.css b/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.js b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..9a49c7fb7c
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.js
@@ -0,0 +1,47 @@
+import { useRef } from 'react';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.module.css b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.js b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..ee1ccb3ddc
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.js
@@ -0,0 +1,21 @@
+import { Link } from 'react-router-dom';
+
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.module.css b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.js b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..a37943f11c
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.js
@@ -0,0 +1,23 @@
+import { Fragment } from 'react';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const QuoteList = (props) => {
+ return (
+
+
+ {props.quotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.module.css b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/12-adding-a-layout-wrapper/src/index.css b/code/12-adding-a-layout-wrapper/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/12-adding-a-layout-wrapper/src/index.js b/code/12-adding-a-layout-wrapper/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/12-adding-a-layout-wrapper/src/pages/AllQuotes.js b/code/12-adding-a-layout-wrapper/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..0c50d04a6f
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/pages/AllQuotes.js
@@ -0,0 +1,5 @@
+const AllQuotes = () => {
+ return All Quotes Page
+};
+
+export default AllQuotes;
\ No newline at end of file
diff --git a/code/12-adding-a-layout-wrapper/src/pages/NewQuote.js b/code/12-adding-a-layout-wrapper/src/pages/NewQuote.js
new file mode 100644
index 0000000000..0b0d779a5c
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/pages/NewQuote.js
@@ -0,0 +1,5 @@
+const NewQuote = () => {
+ return New Quote Page
+};
+
+export default NewQuote;
\ No newline at end of file
diff --git a/code/12-adding-a-layout-wrapper/src/pages/QuoteDetail.js b/code/12-adding-a-layout-wrapper/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..5631383a1d
--- /dev/null
+++ b/code/12-adding-a-layout-wrapper/src/pages/QuoteDetail.js
@@ -0,0 +1,20 @@
+import { Fragment } from 'react';
+import { useParams, Route } from 'react-router-dom';
+
+import Comments from '../components/comments/Comments';
+
+const QuoteDetail = () => {
+ const params = useParams();
+
+ return (
+
+ Quote Detail Page
+ {params.quoteId}
+
+
+
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/code/13-adding-dummy-data-and-more-content/package.json b/code/13-adding-dummy-data-and-more-content/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/13-adding-dummy-data-and-more-content/public/favicon.ico b/code/13-adding-dummy-data-and-more-content/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/13-adding-dummy-data-and-more-content/public/favicon.ico differ
diff --git a/code/13-adding-dummy-data-and-more-content/public/index.html b/code/13-adding-dummy-data-and-more-content/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/13-adding-dummy-data-and-more-content/public/logo192.png b/code/13-adding-dummy-data-and-more-content/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/13-adding-dummy-data-and-more-content/public/logo192.png differ
diff --git a/code/13-adding-dummy-data-and-more-content/public/logo512.png b/code/13-adding-dummy-data-and-more-content/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/13-adding-dummy-data-and-more-content/public/logo512.png differ
diff --git a/code/13-adding-dummy-data-and-more-content/public/manifest.json b/code/13-adding-dummy-data-and-more-content/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/13-adding-dummy-data-and-more-content/public/robots.txt b/code/13-adding-dummy-data-and-more-content/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/13-adding-dummy-data-and-more-content/src/App.js b/code/13-adding-dummy-data-and-more-content/src/App.js
new file mode 100644
index 0000000000..5f90130429
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/App.js
@@ -0,0 +1,29 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+import Layout from './components/layout/Layout';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.js b/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.module.css b/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.js b/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.module.css b/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.js b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.module.css b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.js b/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.module.css b/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.js b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.module.css b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.js b/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.module.css b/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.js b/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.js
new file mode 100644
index 0000000000..2eff00a625
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.js
@@ -0,0 +1,15 @@
+import { Fragment } from 'react';
+
+import classes from './Layout.module.css';
+import MainNavigation from './MainNavigation';
+
+const Layout = (props) => {
+ return (
+
+
+ {props.children}
+
+ );
+};
+
+export default Layout;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.module.css b/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.js b/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.js
new file mode 100644
index 0000000000..76d790a1c3
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.js
@@ -0,0 +1,27 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+const MainNavigation = () => {
+ return (
+
+ Great Quotes
+
+
+ );
+};
+
+export default MainNavigation;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.module.css b/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.js b/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.module.css b/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.js b/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..14a83fa67c
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,14 @@
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.module.css b/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.js b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..9a49c7fb7c
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.js
@@ -0,0 +1,47 @@
+import { useRef } from 'react';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.module.css b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.js b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..06cf8b1221
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.js
@@ -0,0 +1,19 @@
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.module.css b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.js b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..a37943f11c
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.js
@@ -0,0 +1,23 @@
+import { Fragment } from 'react';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const QuoteList = (props) => {
+ return (
+
+
+ {props.quotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.module.css b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/13-adding-dummy-data-and-more-content/src/index.css b/code/13-adding-dummy-data-and-more-content/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/13-adding-dummy-data-and-more-content/src/index.js b/code/13-adding-dummy-data-and-more-content/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/13-adding-dummy-data-and-more-content/src/pages/AllQuotes.js b/code/13-adding-dummy-data-and-more-content/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..6362e57d3f
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/pages/AllQuotes.js
@@ -0,0 +1,12 @@
+import QuoteList from '../components/quotes/QuoteList';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const AllQuotes = () => {
+ return
+};
+
+export default AllQuotes;
\ No newline at end of file
diff --git a/code/13-adding-dummy-data-and-more-content/src/pages/NewQuote.js b/code/13-adding-dummy-data-and-more-content/src/pages/NewQuote.js
new file mode 100644
index 0000000000..d5785be1fb
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/pages/NewQuote.js
@@ -0,0 +1,11 @@
+import QuoteForm from '../components/quotes/QuoteForm';
+
+const NewQuote = () => {
+ const addQuoteHandler = (quoteData) => {
+ console.log(quoteData);
+ };
+
+ return ;
+};
+
+export default NewQuote;
diff --git a/code/13-adding-dummy-data-and-more-content/src/pages/QuoteDetail.js b/code/13-adding-dummy-data-and-more-content/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..5631383a1d
--- /dev/null
+++ b/code/13-adding-dummy-data-and-more-content/src/pages/QuoteDetail.js
@@ -0,0 +1,20 @@
+import { Fragment } from 'react';
+import { useParams, Route } from 'react-router-dom';
+
+import Comments from '../components/comments/Comments';
+
+const QuoteDetail = () => {
+ const params = useParams();
+
+ return (
+
+ Quote Detail Page
+ {params.quoteId}
+
+
+
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/code/14-outputting-data-on-details-page/package.json b/code/14-outputting-data-on-details-page/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/14-outputting-data-on-details-page/public/favicon.ico b/code/14-outputting-data-on-details-page/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/14-outputting-data-on-details-page/public/favicon.ico differ
diff --git a/code/14-outputting-data-on-details-page/public/index.html b/code/14-outputting-data-on-details-page/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/14-outputting-data-on-details-page/public/logo192.png b/code/14-outputting-data-on-details-page/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/14-outputting-data-on-details-page/public/logo192.png differ
diff --git a/code/14-outputting-data-on-details-page/public/logo512.png b/code/14-outputting-data-on-details-page/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/14-outputting-data-on-details-page/public/logo512.png differ
diff --git a/code/14-outputting-data-on-details-page/public/manifest.json b/code/14-outputting-data-on-details-page/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/14-outputting-data-on-details-page/public/robots.txt b/code/14-outputting-data-on-details-page/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/14-outputting-data-on-details-page/src/App.js b/code/14-outputting-data-on-details-page/src/App.js
new file mode 100644
index 0000000000..5f90130429
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/App.js
@@ -0,0 +1,29 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+import Layout from './components/layout/Layout';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/14-outputting-data-on-details-page/src/components/UI/Card.js b/code/14-outputting-data-on-details-page/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/14-outputting-data-on-details-page/src/components/UI/Card.module.css b/code/14-outputting-data-on-details-page/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.js b/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.module.css b/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.js b/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.module.css b/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/14-outputting-data-on-details-page/src/components/comments/Comments.js b/code/14-outputting-data-on-details-page/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/14-outputting-data-on-details-page/src/components/comments/Comments.module.css b/code/14-outputting-data-on-details-page/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.js b/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.module.css b/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.js b/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.module.css b/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/14-outputting-data-on-details-page/src/components/layout/Layout.js b/code/14-outputting-data-on-details-page/src/components/layout/Layout.js
new file mode 100644
index 0000000000..2eff00a625
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/layout/Layout.js
@@ -0,0 +1,15 @@
+import { Fragment } from 'react';
+
+import classes from './Layout.module.css';
+import MainNavigation from './MainNavigation';
+
+const Layout = (props) => {
+ return (
+
+
+ {props.children}
+
+ );
+};
+
+export default Layout;
diff --git a/code/14-outputting-data-on-details-page/src/components/layout/Layout.module.css b/code/14-outputting-data-on-details-page/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.js b/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.js
new file mode 100644
index 0000000000..76d790a1c3
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.js
@@ -0,0 +1,27 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+const MainNavigation = () => {
+ return (
+
+ Great Quotes
+
+
+ );
+};
+
+export default MainNavigation;
diff --git a/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.module.css b/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.js b/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.module.css b/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.js b/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..14a83fa67c
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,14 @@
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.module.css b/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.js b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..9a49c7fb7c
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.js
@@ -0,0 +1,47 @@
+import { useRef } from 'react';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.module.css b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.js b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..ee1ccb3ddc
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.js
@@ -0,0 +1,21 @@
+import { Link } from 'react-router-dom';
+
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.module.css b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.js b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..a37943f11c
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.js
@@ -0,0 +1,23 @@
+import { Fragment } from 'react';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const QuoteList = (props) => {
+ return (
+
+
+ {props.quotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.module.css b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/14-outputting-data-on-details-page/src/index.css b/code/14-outputting-data-on-details-page/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/14-outputting-data-on-details-page/src/index.js b/code/14-outputting-data-on-details-page/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/14-outputting-data-on-details-page/src/pages/AllQuotes.js b/code/14-outputting-data-on-details-page/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..6362e57d3f
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/pages/AllQuotes.js
@@ -0,0 +1,12 @@
+import QuoteList from '../components/quotes/QuoteList';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const AllQuotes = () => {
+ return
+};
+
+export default AllQuotes;
\ No newline at end of file
diff --git a/code/14-outputting-data-on-details-page/src/pages/NewQuote.js b/code/14-outputting-data-on-details-page/src/pages/NewQuote.js
new file mode 100644
index 0000000000..d5785be1fb
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/pages/NewQuote.js
@@ -0,0 +1,11 @@
+import QuoteForm from '../components/quotes/QuoteForm';
+
+const NewQuote = () => {
+ const addQuoteHandler = (quoteData) => {
+ console.log(quoteData);
+ };
+
+ return ;
+};
+
+export default NewQuote;
diff --git a/code/14-outputting-data-on-details-page/src/pages/QuoteDetail.js b/code/14-outputting-data-on-details-page/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..8381efa2ac
--- /dev/null
+++ b/code/14-outputting-data-on-details-page/src/pages/QuoteDetail.js
@@ -0,0 +1,31 @@
+import { Fragment } from 'react';
+import { useParams, Route } from 'react-router-dom';
+
+import HighlightedQuote from '../components/quotes/HighlightedQuote';
+import Comments from '../components/comments/Comments';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const QuoteDetail = () => {
+ const params = useParams();
+
+ const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId);
+
+ if (!quote) {
+ return No quote found!
;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/code/15-adding-a-notfound-page/package.json b/code/15-adding-a-notfound-page/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/15-adding-a-notfound-page/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/15-adding-a-notfound-page/public/favicon.ico b/code/15-adding-a-notfound-page/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/15-adding-a-notfound-page/public/favicon.ico differ
diff --git a/code/15-adding-a-notfound-page/public/index.html b/code/15-adding-a-notfound-page/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/15-adding-a-notfound-page/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/15-adding-a-notfound-page/public/logo192.png b/code/15-adding-a-notfound-page/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/15-adding-a-notfound-page/public/logo192.png differ
diff --git a/code/15-adding-a-notfound-page/public/logo512.png b/code/15-adding-a-notfound-page/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/15-adding-a-notfound-page/public/logo512.png differ
diff --git a/code/15-adding-a-notfound-page/public/manifest.json b/code/15-adding-a-notfound-page/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/15-adding-a-notfound-page/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/15-adding-a-notfound-page/public/robots.txt b/code/15-adding-a-notfound-page/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/15-adding-a-notfound-page/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/15-adding-a-notfound-page/src/App.js b/code/15-adding-a-notfound-page/src/App.js
new file mode 100644
index 0000000000..97eb33fc84
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/App.js
@@ -0,0 +1,33 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+import NotFound from './pages/NotFound';
+import Layout from './components/layout/Layout';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/15-adding-a-notfound-page/src/components/UI/Card.js b/code/15-adding-a-notfound-page/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/15-adding-a-notfound-page/src/components/UI/Card.module.css b/code/15-adding-a-notfound-page/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.js b/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.module.css b/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/15-adding-a-notfound-page/src/components/comments/CommentItem.js b/code/15-adding-a-notfound-page/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/15-adding-a-notfound-page/src/components/comments/CommentItem.module.css b/code/15-adding-a-notfound-page/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/15-adding-a-notfound-page/src/components/comments/Comments.js b/code/15-adding-a-notfound-page/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/15-adding-a-notfound-page/src/components/comments/Comments.module.css b/code/15-adding-a-notfound-page/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/15-adding-a-notfound-page/src/components/comments/CommentsList.js b/code/15-adding-a-notfound-page/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/15-adding-a-notfound-page/src/components/comments/CommentsList.module.css b/code/15-adding-a-notfound-page/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.js b/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.module.css b/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/15-adding-a-notfound-page/src/components/layout/Layout.js b/code/15-adding-a-notfound-page/src/components/layout/Layout.js
new file mode 100644
index 0000000000..2eff00a625
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/layout/Layout.js
@@ -0,0 +1,15 @@
+import { Fragment } from 'react';
+
+import classes from './Layout.module.css';
+import MainNavigation from './MainNavigation';
+
+const Layout = (props) => {
+ return (
+
+
+ {props.children}
+
+ );
+};
+
+export default Layout;
diff --git a/code/15-adding-a-notfound-page/src/components/layout/Layout.module.css b/code/15-adding-a-notfound-page/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.js b/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.js
new file mode 100644
index 0000000000..76d790a1c3
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.js
@@ -0,0 +1,27 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+const MainNavigation = () => {
+ return (
+
+ Great Quotes
+
+
+ );
+};
+
+export default MainNavigation;
diff --git a/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.module.css b/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.js b/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.module.css b/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.js b/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..14a83fa67c
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,14 @@
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.module.css b/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.js b/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..9a49c7fb7c
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.js
@@ -0,0 +1,47 @@
+import { useRef } from 'react';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.module.css b/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.js b/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..ee1ccb3ddc
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.js
@@ -0,0 +1,21 @@
+import { Link } from 'react-router-dom';
+
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.module.css b/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.js b/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..a37943f11c
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.js
@@ -0,0 +1,23 @@
+import { Fragment } from 'react';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const QuoteList = (props) => {
+ return (
+
+
+ {props.quotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.module.css b/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/15-adding-a-notfound-page/src/index.css b/code/15-adding-a-notfound-page/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/15-adding-a-notfound-page/src/index.js b/code/15-adding-a-notfound-page/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/15-adding-a-notfound-page/src/pages/AllQuotes.js b/code/15-adding-a-notfound-page/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..6362e57d3f
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/pages/AllQuotes.js
@@ -0,0 +1,12 @@
+import QuoteList from '../components/quotes/QuoteList';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const AllQuotes = () => {
+ return
+};
+
+export default AllQuotes;
\ No newline at end of file
diff --git a/code/15-adding-a-notfound-page/src/pages/NewQuote.js b/code/15-adding-a-notfound-page/src/pages/NewQuote.js
new file mode 100644
index 0000000000..d5785be1fb
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/pages/NewQuote.js
@@ -0,0 +1,11 @@
+import QuoteForm from '../components/quotes/QuoteForm';
+
+const NewQuote = () => {
+ const addQuoteHandler = (quoteData) => {
+ console.log(quoteData);
+ };
+
+ return ;
+};
+
+export default NewQuote;
diff --git a/code/15-adding-a-notfound-page/src/pages/NotFound.js b/code/15-adding-a-notfound-page/src/pages/NotFound.js
new file mode 100644
index 0000000000..bb7c85baac
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/pages/NotFound.js
@@ -0,0 +1,9 @@
+const NotFound = () => {
+ return (
+
+ );
+};
+
+export default NotFound;
diff --git a/code/15-adding-a-notfound-page/src/pages/QuoteDetail.js b/code/15-adding-a-notfound-page/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..8381efa2ac
--- /dev/null
+++ b/code/15-adding-a-notfound-page/src/pages/QuoteDetail.js
@@ -0,0 +1,31 @@
+import { Fragment } from 'react';
+import { useParams, Route } from 'react-router-dom';
+
+import HighlightedQuote from '../components/quotes/HighlightedQuote';
+import Comments from '../components/comments/Comments';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const QuoteDetail = () => {
+ const params = useParams();
+
+ const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId);
+
+ if (!quote) {
+ return No quote found!
;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/code/16-implementing-programmatic-navigation/package.json b/code/16-implementing-programmatic-navigation/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/16-implementing-programmatic-navigation/public/favicon.ico b/code/16-implementing-programmatic-navigation/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/16-implementing-programmatic-navigation/public/favicon.ico differ
diff --git a/code/16-implementing-programmatic-navigation/public/index.html b/code/16-implementing-programmatic-navigation/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/16-implementing-programmatic-navigation/public/logo192.png b/code/16-implementing-programmatic-navigation/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/16-implementing-programmatic-navigation/public/logo192.png differ
diff --git a/code/16-implementing-programmatic-navigation/public/logo512.png b/code/16-implementing-programmatic-navigation/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/16-implementing-programmatic-navigation/public/logo512.png differ
diff --git a/code/16-implementing-programmatic-navigation/public/manifest.json b/code/16-implementing-programmatic-navigation/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/16-implementing-programmatic-navigation/public/robots.txt b/code/16-implementing-programmatic-navigation/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/16-implementing-programmatic-navigation/src/App.js b/code/16-implementing-programmatic-navigation/src/App.js
new file mode 100644
index 0000000000..97eb33fc84
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/App.js
@@ -0,0 +1,33 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+import NotFound from './pages/NotFound';
+import Layout from './components/layout/Layout';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/16-implementing-programmatic-navigation/src/components/UI/Card.js b/code/16-implementing-programmatic-navigation/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/16-implementing-programmatic-navigation/src/components/UI/Card.module.css b/code/16-implementing-programmatic-navigation/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.js b/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.module.css b/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.js b/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.module.css b/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/Comments.js b/code/16-implementing-programmatic-navigation/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/Comments.module.css b/code/16-implementing-programmatic-navigation/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.js b/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.module.css b/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.js b/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.module.css b/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/16-implementing-programmatic-navigation/src/components/layout/Layout.js b/code/16-implementing-programmatic-navigation/src/components/layout/Layout.js
new file mode 100644
index 0000000000..2eff00a625
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/layout/Layout.js
@@ -0,0 +1,15 @@
+import { Fragment } from 'react';
+
+import classes from './Layout.module.css';
+import MainNavigation from './MainNavigation';
+
+const Layout = (props) => {
+ return (
+
+
+ {props.children}
+
+ );
+};
+
+export default Layout;
diff --git a/code/16-implementing-programmatic-navigation/src/components/layout/Layout.module.css b/code/16-implementing-programmatic-navigation/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.js b/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.js
new file mode 100644
index 0000000000..76d790a1c3
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.js
@@ -0,0 +1,27 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+const MainNavigation = () => {
+ return (
+
+ Great Quotes
+
+
+ );
+};
+
+export default MainNavigation;
diff --git a/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.module.css b/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.js b/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.module.css b/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.js b/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..14a83fa67c
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,14 @@
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.module.css b/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.js b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..9a49c7fb7c
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.js
@@ -0,0 +1,47 @@
+import { useRef } from 'react';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.module.css b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.js b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..ee1ccb3ddc
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.js
@@ -0,0 +1,21 @@
+import { Link } from 'react-router-dom';
+
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.module.css b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.js b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..a37943f11c
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.js
@@ -0,0 +1,23 @@
+import { Fragment } from 'react';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const QuoteList = (props) => {
+ return (
+
+
+ {props.quotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.module.css b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/16-implementing-programmatic-navigation/src/index.css b/code/16-implementing-programmatic-navigation/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/16-implementing-programmatic-navigation/src/index.js b/code/16-implementing-programmatic-navigation/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/16-implementing-programmatic-navigation/src/pages/AllQuotes.js b/code/16-implementing-programmatic-navigation/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..6362e57d3f
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/pages/AllQuotes.js
@@ -0,0 +1,12 @@
+import QuoteList from '../components/quotes/QuoteList';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const AllQuotes = () => {
+ return
+};
+
+export default AllQuotes;
\ No newline at end of file
diff --git a/code/16-implementing-programmatic-navigation/src/pages/NewQuote.js b/code/16-implementing-programmatic-navigation/src/pages/NewQuote.js
new file mode 100644
index 0000000000..2bf8efb2d2
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/pages/NewQuote.js
@@ -0,0 +1,17 @@
+import { useHistory } from 'react-router-dom';
+
+import QuoteForm from '../components/quotes/QuoteForm';
+
+const NewQuote = () => {
+ const history = useHistory();
+
+ const addQuoteHandler = (quoteData) => {
+ console.log(quoteData);
+
+ history.push('/quotes');
+ };
+
+ return ;
+};
+
+export default NewQuote;
diff --git a/code/16-implementing-programmatic-navigation/src/pages/NotFound.js b/code/16-implementing-programmatic-navigation/src/pages/NotFound.js
new file mode 100644
index 0000000000..bb7c85baac
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/pages/NotFound.js
@@ -0,0 +1,9 @@
+const NotFound = () => {
+ return (
+
+ );
+};
+
+export default NotFound;
diff --git a/code/16-implementing-programmatic-navigation/src/pages/QuoteDetail.js b/code/16-implementing-programmatic-navigation/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..8381efa2ac
--- /dev/null
+++ b/code/16-implementing-programmatic-navigation/src/pages/QuoteDetail.js
@@ -0,0 +1,31 @@
+import { Fragment } from 'react';
+import { useParams, Route } from 'react-router-dom';
+
+import HighlightedQuote from '../components/quotes/HighlightedQuote';
+import Comments from '../components/comments/Comments';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const QuoteDetail = () => {
+ const params = useParams();
+
+ const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId);
+
+ if (!quote) {
+ return No quote found!
;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/code/17-preventing-unwanted-route-transitions/package.json b/code/17-preventing-unwanted-route-transitions/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/17-preventing-unwanted-route-transitions/public/favicon.ico b/code/17-preventing-unwanted-route-transitions/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/17-preventing-unwanted-route-transitions/public/favicon.ico differ
diff --git a/code/17-preventing-unwanted-route-transitions/public/index.html b/code/17-preventing-unwanted-route-transitions/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/17-preventing-unwanted-route-transitions/public/logo192.png b/code/17-preventing-unwanted-route-transitions/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/17-preventing-unwanted-route-transitions/public/logo192.png differ
diff --git a/code/17-preventing-unwanted-route-transitions/public/logo512.png b/code/17-preventing-unwanted-route-transitions/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/17-preventing-unwanted-route-transitions/public/logo512.png differ
diff --git a/code/17-preventing-unwanted-route-transitions/public/manifest.json b/code/17-preventing-unwanted-route-transitions/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/17-preventing-unwanted-route-transitions/public/robots.txt b/code/17-preventing-unwanted-route-transitions/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/17-preventing-unwanted-route-transitions/src/App.js b/code/17-preventing-unwanted-route-transitions/src/App.js
new file mode 100644
index 0000000000..97eb33fc84
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/App.js
@@ -0,0 +1,33 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+import NotFound from './pages/NotFound';
+import Layout from './components/layout/Layout';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.js b/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.module.css b/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.js b/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.module.css b/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.js b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.module.css b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.js b/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.module.css b/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.js b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.module.css b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.js b/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.module.css b/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.js b/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.js
new file mode 100644
index 0000000000..2eff00a625
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.js
@@ -0,0 +1,15 @@
+import { Fragment } from 'react';
+
+import classes from './Layout.module.css';
+import MainNavigation from './MainNavigation';
+
+const Layout = (props) => {
+ return (
+
+
+ {props.children}
+
+ );
+};
+
+export default Layout;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.module.css b/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.js b/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.js
new file mode 100644
index 0000000000..76d790a1c3
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.js
@@ -0,0 +1,27 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+const MainNavigation = () => {
+ return (
+
+ Great Quotes
+
+
+ );
+};
+
+export default MainNavigation;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.module.css b/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.js b/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.module.css b/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.js b/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..14a83fa67c
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,14 @@
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.module.css b/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.js b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..c50916ab91
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.js
@@ -0,0 +1,70 @@
+import { Fragment, useRef, useState } from 'react';
+import { Prompt } from 'react-router-dom';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const [isEntering, setIsEntering] = useState(false);
+
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ const finishEnteringHandler = () => {
+ setIsEntering(false);
+ };
+
+ const formFocusedHandler = () => {
+ setIsEntering(true);
+ };
+
+ return (
+
+
+ 'Are you sure you want to leave? All your entered data will be lost!'
+ }
+ />
+
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.module.css b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.js b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..ee1ccb3ddc
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.js
@@ -0,0 +1,21 @@
+import { Link } from 'react-router-dom';
+
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.module.css b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.js b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..a37943f11c
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.js
@@ -0,0 +1,23 @@
+import { Fragment } from 'react';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const QuoteList = (props) => {
+ return (
+
+
+ {props.quotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.module.css b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/17-preventing-unwanted-route-transitions/src/index.css b/code/17-preventing-unwanted-route-transitions/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/17-preventing-unwanted-route-transitions/src/index.js b/code/17-preventing-unwanted-route-transitions/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/17-preventing-unwanted-route-transitions/src/pages/AllQuotes.js b/code/17-preventing-unwanted-route-transitions/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..6362e57d3f
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/pages/AllQuotes.js
@@ -0,0 +1,12 @@
+import QuoteList from '../components/quotes/QuoteList';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const AllQuotes = () => {
+ return
+};
+
+export default AllQuotes;
\ No newline at end of file
diff --git a/code/17-preventing-unwanted-route-transitions/src/pages/NewQuote.js b/code/17-preventing-unwanted-route-transitions/src/pages/NewQuote.js
new file mode 100644
index 0000000000..2bf8efb2d2
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/pages/NewQuote.js
@@ -0,0 +1,17 @@
+import { useHistory } from 'react-router-dom';
+
+import QuoteForm from '../components/quotes/QuoteForm';
+
+const NewQuote = () => {
+ const history = useHistory();
+
+ const addQuoteHandler = (quoteData) => {
+ console.log(quoteData);
+
+ history.push('/quotes');
+ };
+
+ return ;
+};
+
+export default NewQuote;
diff --git a/code/17-preventing-unwanted-route-transitions/src/pages/NotFound.js b/code/17-preventing-unwanted-route-transitions/src/pages/NotFound.js
new file mode 100644
index 0000000000..bb7c85baac
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/pages/NotFound.js
@@ -0,0 +1,9 @@
+const NotFound = () => {
+ return (
+
+ );
+};
+
+export default NotFound;
diff --git a/code/17-preventing-unwanted-route-transitions/src/pages/QuoteDetail.js b/code/17-preventing-unwanted-route-transitions/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..8381efa2ac
--- /dev/null
+++ b/code/17-preventing-unwanted-route-transitions/src/pages/QuoteDetail.js
@@ -0,0 +1,31 @@
+import { Fragment } from 'react';
+import { useParams, Route } from 'react-router-dom';
+
+import HighlightedQuote from '../components/quotes/HighlightedQuote';
+import Comments from '../components/comments/Comments';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const QuoteDetail = () => {
+ const params = useParams();
+
+ const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId);
+
+ if (!quote) {
+ return No quote found!
;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/code/18-working-with-query-params/package.json b/code/18-working-with-query-params/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/18-working-with-query-params/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/18-working-with-query-params/public/favicon.ico b/code/18-working-with-query-params/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/18-working-with-query-params/public/favicon.ico differ
diff --git a/code/18-working-with-query-params/public/index.html b/code/18-working-with-query-params/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/18-working-with-query-params/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/18-working-with-query-params/public/logo192.png b/code/18-working-with-query-params/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/18-working-with-query-params/public/logo192.png differ
diff --git a/code/18-working-with-query-params/public/logo512.png b/code/18-working-with-query-params/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/18-working-with-query-params/public/logo512.png differ
diff --git a/code/18-working-with-query-params/public/manifest.json b/code/18-working-with-query-params/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/18-working-with-query-params/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/18-working-with-query-params/public/robots.txt b/code/18-working-with-query-params/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/18-working-with-query-params/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/18-working-with-query-params/src/App.js b/code/18-working-with-query-params/src/App.js
new file mode 100644
index 0000000000..97eb33fc84
--- /dev/null
+++ b/code/18-working-with-query-params/src/App.js
@@ -0,0 +1,33 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+import NotFound from './pages/NotFound';
+import Layout from './components/layout/Layout';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/18-working-with-query-params/src/components/UI/Card.js b/code/18-working-with-query-params/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/18-working-with-query-params/src/components/UI/Card.module.css b/code/18-working-with-query-params/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/18-working-with-query-params/src/components/UI/LoadingSpinner.js b/code/18-working-with-query-params/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/18-working-with-query-params/src/components/UI/LoadingSpinner.module.css b/code/18-working-with-query-params/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/18-working-with-query-params/src/components/comments/CommentItem.js b/code/18-working-with-query-params/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/18-working-with-query-params/src/components/comments/CommentItem.module.css b/code/18-working-with-query-params/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/18-working-with-query-params/src/components/comments/Comments.js b/code/18-working-with-query-params/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/18-working-with-query-params/src/components/comments/Comments.module.css b/code/18-working-with-query-params/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/18-working-with-query-params/src/components/comments/CommentsList.js b/code/18-working-with-query-params/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/18-working-with-query-params/src/components/comments/CommentsList.module.css b/code/18-working-with-query-params/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/18-working-with-query-params/src/components/comments/NewCommentForm.js b/code/18-working-with-query-params/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/18-working-with-query-params/src/components/comments/NewCommentForm.module.css b/code/18-working-with-query-params/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/18-working-with-query-params/src/components/layout/Layout.js b/code/18-working-with-query-params/src/components/layout/Layout.js
new file mode 100644
index 0000000000..2eff00a625
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/layout/Layout.js
@@ -0,0 +1,15 @@
+import { Fragment } from 'react';
+
+import classes from './Layout.module.css';
+import MainNavigation from './MainNavigation';
+
+const Layout = (props) => {
+ return (
+
+
+ {props.children}
+
+ );
+};
+
+export default Layout;
diff --git a/code/18-working-with-query-params/src/components/layout/Layout.module.css b/code/18-working-with-query-params/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/18-working-with-query-params/src/components/layout/MainNavigation.js b/code/18-working-with-query-params/src/components/layout/MainNavigation.js
new file mode 100644
index 0000000000..76d790a1c3
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/layout/MainNavigation.js
@@ -0,0 +1,27 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+const MainNavigation = () => {
+ return (
+
+ Great Quotes
+
+
+ );
+};
+
+export default MainNavigation;
diff --git a/code/18-working-with-query-params/src/components/layout/MainNavigation.module.css b/code/18-working-with-query-params/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.js b/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.module.css b/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.js b/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..14a83fa67c
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,14 @@
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.module.css b/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteForm.js b/code/18-working-with-query-params/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..c50916ab91
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/quotes/QuoteForm.js
@@ -0,0 +1,70 @@
+import { Fragment, useRef, useState } from 'react';
+import { Prompt } from 'react-router-dom';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const [isEntering, setIsEntering] = useState(false);
+
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ const finishEnteringHandler = () => {
+ setIsEntering(false);
+ };
+
+ const formFocusedHandler = () => {
+ setIsEntering(true);
+ };
+
+ return (
+
+
+ 'Are you sure you want to leave? All your entered data will be lost!'
+ }
+ />
+
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteForm.module.css b/code/18-working-with-query-params/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteItem.js b/code/18-working-with-query-params/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..ee1ccb3ddc
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/quotes/QuoteItem.js
@@ -0,0 +1,21 @@
+import { Link } from 'react-router-dom';
+
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteItem.module.css b/code/18-working-with-query-params/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteList.js b/code/18-working-with-query-params/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..d320fbdf83
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/quotes/QuoteList.js
@@ -0,0 +1,52 @@
+import { Fragment } from 'react';
+import { useHistory, useLocation } from 'react-router-dom';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const sortQuotes = (quotes, ascending) => {
+ return quotes.sort((quoteA, quoteB) => {
+ if (ascending) {
+ return quoteA.id > quoteB.id ? 1 : -1;
+ } else {
+ return quoteA.id < quoteB.id ? 1 : -1;
+ }
+ });
+};
+
+const QuoteList = (props) => {
+ const history = useHistory();
+ const location = useLocation();
+
+ const queryParams = new URLSearchParams(location.search);
+
+ const isSortingAscending = queryParams.get('sort') === 'asc';
+
+ const sortedQuotes = sortQuotes(props.quotes, isSortingAscending);
+
+ const changeSortingHandler = () => {
+ history.push('/quotes?sort=' + (isSortingAscending ? 'desc' : 'asc'));
+ };
+
+ return (
+
+
+
+
+
+ {sortedQuotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteList.module.css b/code/18-working-with-query-params/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/18-working-with-query-params/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/18-working-with-query-params/src/index.css b/code/18-working-with-query-params/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/18-working-with-query-params/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/18-working-with-query-params/src/index.js b/code/18-working-with-query-params/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/18-working-with-query-params/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/18-working-with-query-params/src/pages/AllQuotes.js b/code/18-working-with-query-params/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..6362e57d3f
--- /dev/null
+++ b/code/18-working-with-query-params/src/pages/AllQuotes.js
@@ -0,0 +1,12 @@
+import QuoteList from '../components/quotes/QuoteList';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const AllQuotes = () => {
+ return
+};
+
+export default AllQuotes;
\ No newline at end of file
diff --git a/code/18-working-with-query-params/src/pages/NewQuote.js b/code/18-working-with-query-params/src/pages/NewQuote.js
new file mode 100644
index 0000000000..2bf8efb2d2
--- /dev/null
+++ b/code/18-working-with-query-params/src/pages/NewQuote.js
@@ -0,0 +1,17 @@
+import { useHistory } from 'react-router-dom';
+
+import QuoteForm from '../components/quotes/QuoteForm';
+
+const NewQuote = () => {
+ const history = useHistory();
+
+ const addQuoteHandler = (quoteData) => {
+ console.log(quoteData);
+
+ history.push('/quotes');
+ };
+
+ return ;
+};
+
+export default NewQuote;
diff --git a/code/18-working-with-query-params/src/pages/NotFound.js b/code/18-working-with-query-params/src/pages/NotFound.js
new file mode 100644
index 0000000000..bb7c85baac
--- /dev/null
+++ b/code/18-working-with-query-params/src/pages/NotFound.js
@@ -0,0 +1,9 @@
+const NotFound = () => {
+ return (
+
+ );
+};
+
+export default NotFound;
diff --git a/code/18-working-with-query-params/src/pages/QuoteDetail.js b/code/18-working-with-query-params/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..8381efa2ac
--- /dev/null
+++ b/code/18-working-with-query-params/src/pages/QuoteDetail.js
@@ -0,0 +1,31 @@
+import { Fragment } from 'react';
+import { useParams, Route } from 'react-router-dom';
+
+import HighlightedQuote from '../components/quotes/HighlightedQuote';
+import Comments from '../components/comments/Comments';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const QuoteDetail = () => {
+ const params = useParams();
+
+ const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId);
+
+ if (!quote) {
+ return No quote found!
;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/code/19-writing-more-flexible-routing-code/package.json b/code/19-writing-more-flexible-routing-code/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/19-writing-more-flexible-routing-code/public/favicon.ico b/code/19-writing-more-flexible-routing-code/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/19-writing-more-flexible-routing-code/public/favicon.ico differ
diff --git a/code/19-writing-more-flexible-routing-code/public/index.html b/code/19-writing-more-flexible-routing-code/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/19-writing-more-flexible-routing-code/public/logo192.png b/code/19-writing-more-flexible-routing-code/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/19-writing-more-flexible-routing-code/public/logo192.png differ
diff --git a/code/19-writing-more-flexible-routing-code/public/logo512.png b/code/19-writing-more-flexible-routing-code/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/19-writing-more-flexible-routing-code/public/logo512.png differ
diff --git a/code/19-writing-more-flexible-routing-code/public/manifest.json b/code/19-writing-more-flexible-routing-code/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/19-writing-more-flexible-routing-code/public/robots.txt b/code/19-writing-more-flexible-routing-code/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/19-writing-more-flexible-routing-code/src/App.js b/code/19-writing-more-flexible-routing-code/src/App.js
new file mode 100644
index 0000000000..97eb33fc84
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/App.js
@@ -0,0 +1,33 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+import NotFound from './pages/NotFound';
+import Layout from './components/layout/Layout';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/UI/Card.js b/code/19-writing-more-flexible-routing-code/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/UI/Card.module.css b/code/19-writing-more-flexible-routing-code/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.js b/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.module.css b/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.js b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.module.css b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.js b/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.module.css b/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.js b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.module.css b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.js b/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.module.css b/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.js b/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.js
new file mode 100644
index 0000000000..2eff00a625
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.js
@@ -0,0 +1,15 @@
+import { Fragment } from 'react';
+
+import classes from './Layout.module.css';
+import MainNavigation from './MainNavigation';
+
+const Layout = (props) => {
+ return (
+
+
+ {props.children}
+
+ );
+};
+
+export default Layout;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.module.css b/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.js b/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.js
new file mode 100644
index 0000000000..76d790a1c3
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.js
@@ -0,0 +1,27 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+const MainNavigation = () => {
+ return (
+
+ Great Quotes
+
+
+ );
+};
+
+export default MainNavigation;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.module.css b/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.js b/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.module.css b/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.js b/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..14a83fa67c
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,14 @@
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.module.css b/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.js b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..c50916ab91
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.js
@@ -0,0 +1,70 @@
+import { Fragment, useRef, useState } from 'react';
+import { Prompt } from 'react-router-dom';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const [isEntering, setIsEntering] = useState(false);
+
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ const finishEnteringHandler = () => {
+ setIsEntering(false);
+ };
+
+ const formFocusedHandler = () => {
+ setIsEntering(true);
+ };
+
+ return (
+
+
+ 'Are you sure you want to leave? All your entered data will be lost!'
+ }
+ />
+
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.module.css b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.js b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..ee1ccb3ddc
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.js
@@ -0,0 +1,21 @@
+import { Link } from 'react-router-dom';
+
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.module.css b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.js b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..05cab9d98b
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.js
@@ -0,0 +1,55 @@
+import { Fragment } from 'react';
+import { useHistory, useLocation } from 'react-router-dom';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const sortQuotes = (quotes, ascending) => {
+ return quotes.sort((quoteA, quoteB) => {
+ if (ascending) {
+ return quoteA.id > quoteB.id ? 1 : -1;
+ } else {
+ return quoteA.id < quoteB.id ? 1 : -1;
+ }
+ });
+};
+
+const QuoteList = (props) => {
+ const history = useHistory();
+ const location = useLocation();
+
+ const queryParams = new URLSearchParams(location.search);
+
+ const isSortingAscending = queryParams.get('sort') === 'asc';
+
+ const sortedQuotes = sortQuotes(props.quotes, isSortingAscending);
+
+ const changeSortingHandler = () => {
+ history.push({
+ pathname: location.pathname,
+ search: `?sort=${(isSortingAscending ? 'desc' : 'asc')}`
+ });
+ };
+
+ return (
+
+
+
+
+
+ {sortedQuotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.module.css b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/19-writing-more-flexible-routing-code/src/index.css b/code/19-writing-more-flexible-routing-code/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/19-writing-more-flexible-routing-code/src/index.js b/code/19-writing-more-flexible-routing-code/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/19-writing-more-flexible-routing-code/src/pages/AllQuotes.js b/code/19-writing-more-flexible-routing-code/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..6362e57d3f
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/pages/AllQuotes.js
@@ -0,0 +1,12 @@
+import QuoteList from '../components/quotes/QuoteList';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const AllQuotes = () => {
+ return
+};
+
+export default AllQuotes;
\ No newline at end of file
diff --git a/code/19-writing-more-flexible-routing-code/src/pages/NewQuote.js b/code/19-writing-more-flexible-routing-code/src/pages/NewQuote.js
new file mode 100644
index 0000000000..2bf8efb2d2
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/pages/NewQuote.js
@@ -0,0 +1,17 @@
+import { useHistory } from 'react-router-dom';
+
+import QuoteForm from '../components/quotes/QuoteForm';
+
+const NewQuote = () => {
+ const history = useHistory();
+
+ const addQuoteHandler = (quoteData) => {
+ console.log(quoteData);
+
+ history.push('/quotes');
+ };
+
+ return ;
+};
+
+export default NewQuote;
diff --git a/code/19-writing-more-flexible-routing-code/src/pages/NotFound.js b/code/19-writing-more-flexible-routing-code/src/pages/NotFound.js
new file mode 100644
index 0000000000..bb7c85baac
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/pages/NotFound.js
@@ -0,0 +1,9 @@
+const NotFound = () => {
+ return (
+
+ );
+};
+
+export default NotFound;
diff --git a/code/19-writing-more-flexible-routing-code/src/pages/QuoteDetail.js b/code/19-writing-more-flexible-routing-code/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..db5ef5d861
--- /dev/null
+++ b/code/19-writing-more-flexible-routing-code/src/pages/QuoteDetail.js
@@ -0,0 +1,39 @@
+import { Fragment } from 'react';
+import { useParams, Route, Link, useRouteMatch } from 'react-router-dom';
+
+import HighlightedQuote from '../components/quotes/HighlightedQuote';
+import Comments from '../components/comments/Comments';
+
+const DUMMY_QUOTES = [
+ { id: 'q1', author: 'Max', text: 'Learning React is fun!' },
+ { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' },
+];
+
+const QuoteDetail = () => {
+ const match = useRouteMatch();
+ const params = useParams();
+
+ const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId);
+
+ if (!quote) {
+ return No quote found!
;
+ }
+
+ return (
+
+
+
+
+
+ Load Comments
+
+
+
+
+
+
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/code/20-sending-getting-quote-data/package.json b/code/20-sending-getting-quote-data/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/20-sending-getting-quote-data/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/20-sending-getting-quote-data/public/favicon.ico b/code/20-sending-getting-quote-data/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/20-sending-getting-quote-data/public/favicon.ico differ
diff --git a/code/20-sending-getting-quote-data/public/index.html b/code/20-sending-getting-quote-data/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/20-sending-getting-quote-data/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/20-sending-getting-quote-data/public/logo192.png b/code/20-sending-getting-quote-data/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/20-sending-getting-quote-data/public/logo192.png differ
diff --git a/code/20-sending-getting-quote-data/public/logo512.png b/code/20-sending-getting-quote-data/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/20-sending-getting-quote-data/public/logo512.png differ
diff --git a/code/20-sending-getting-quote-data/public/manifest.json b/code/20-sending-getting-quote-data/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/20-sending-getting-quote-data/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/20-sending-getting-quote-data/public/robots.txt b/code/20-sending-getting-quote-data/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/20-sending-getting-quote-data/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/20-sending-getting-quote-data/src/App.js b/code/20-sending-getting-quote-data/src/App.js
new file mode 100644
index 0000000000..97eb33fc84
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/App.js
@@ -0,0 +1,33 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+import NotFound from './pages/NotFound';
+import Layout from './components/layout/Layout';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/20-sending-getting-quote-data/src/components/UI/Card.js b/code/20-sending-getting-quote-data/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/20-sending-getting-quote-data/src/components/UI/Card.module.css b/code/20-sending-getting-quote-data/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.js b/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.module.css b/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/20-sending-getting-quote-data/src/components/comments/CommentItem.js b/code/20-sending-getting-quote-data/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/20-sending-getting-quote-data/src/components/comments/CommentItem.module.css b/code/20-sending-getting-quote-data/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/20-sending-getting-quote-data/src/components/comments/Comments.js b/code/20-sending-getting-quote-data/src/components/comments/Comments.js
new file mode 100644
index 0000000000..6dbd006177
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/comments/Comments.js
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && }
+ Comments...
+
+ );
+};
+
+export default Comments;
diff --git a/code/20-sending-getting-quote-data/src/components/comments/Comments.module.css b/code/20-sending-getting-quote-data/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/20-sending-getting-quote-data/src/components/comments/CommentsList.js b/code/20-sending-getting-quote-data/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/20-sending-getting-quote-data/src/components/comments/CommentsList.module.css b/code/20-sending-getting-quote-data/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.js b/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..2950b1e837
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.js
@@ -0,0 +1,29 @@
+import { useRef } from 'react';
+
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ // optional: Could validate here
+
+ // send comment to server
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.module.css b/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/20-sending-getting-quote-data/src/components/layout/Layout.js b/code/20-sending-getting-quote-data/src/components/layout/Layout.js
new file mode 100644
index 0000000000..2eff00a625
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/layout/Layout.js
@@ -0,0 +1,15 @@
+import { Fragment } from 'react';
+
+import classes from './Layout.module.css';
+import MainNavigation from './MainNavigation';
+
+const Layout = (props) => {
+ return (
+
+
+ {props.children}
+
+ );
+};
+
+export default Layout;
diff --git a/code/20-sending-getting-quote-data/src/components/layout/Layout.module.css b/code/20-sending-getting-quote-data/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.js b/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.js
new file mode 100644
index 0000000000..76d790a1c3
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.js
@@ -0,0 +1,27 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+const MainNavigation = () => {
+ return (
+
+ Great Quotes
+
+
+ );
+};
+
+export default MainNavigation;
diff --git a/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.module.css b/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.js b/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.module.css b/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.js b/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..8642a10db4
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,16 @@
+import { Link } from 'react-router-dom';
+
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+
No quotes found!
+
+ Add a Quote
+
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.module.css b/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.js b/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..c50916ab91
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.js
@@ -0,0 +1,70 @@
+import { Fragment, useRef, useState } from 'react';
+import { Prompt } from 'react-router-dom';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const [isEntering, setIsEntering] = useState(false);
+
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ const finishEnteringHandler = () => {
+ setIsEntering(false);
+ };
+
+ const formFocusedHandler = () => {
+ setIsEntering(true);
+ };
+
+ return (
+
+
+ 'Are you sure you want to leave? All your entered data will be lost!'
+ }
+ />
+
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.module.css b/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.js b/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..ee1ccb3ddc
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.js
@@ -0,0 +1,21 @@
+import { Link } from 'react-router-dom';
+
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.module.css b/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.js b/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..05cab9d98b
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.js
@@ -0,0 +1,55 @@
+import { Fragment } from 'react';
+import { useHistory, useLocation } from 'react-router-dom';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const sortQuotes = (quotes, ascending) => {
+ return quotes.sort((quoteA, quoteB) => {
+ if (ascending) {
+ return quoteA.id > quoteB.id ? 1 : -1;
+ } else {
+ return quoteA.id < quoteB.id ? 1 : -1;
+ }
+ });
+};
+
+const QuoteList = (props) => {
+ const history = useHistory();
+ const location = useLocation();
+
+ const queryParams = new URLSearchParams(location.search);
+
+ const isSortingAscending = queryParams.get('sort') === 'asc';
+
+ const sortedQuotes = sortQuotes(props.quotes, isSortingAscending);
+
+ const changeSortingHandler = () => {
+ history.push({
+ pathname: location.pathname,
+ search: `?sort=${(isSortingAscending ? 'desc' : 'asc')}`
+ });
+ };
+
+ return (
+
+
+
+
+
+ {sortedQuotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.module.css b/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/20-sending-getting-quote-data/src/hooks/use-http.js b/code/20-sending-getting-quote-data/src/hooks/use-http.js
new file mode 100644
index 0000000000..b7c56bc76a
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/hooks/use-http.js
@@ -0,0 +1,60 @@
+import { useReducer, useCallback } from 'react';
+
+function httpReducer(state, action) {
+ if (action.type === 'SEND') {
+ return {
+ data: null,
+ error: null,
+ status: 'pending',
+ };
+ }
+
+ if (action.type === 'SUCCESS') {
+ return {
+ data: action.responseData,
+ error: null,
+ status: 'completed',
+ };
+ }
+
+ if (action.type === 'ERROR') {
+ return {
+ data: null,
+ error: action.errorMessage,
+ status: 'completed',
+ };
+ }
+
+ return state;
+}
+
+function useHttp(requestFunction, startWithPending = false) {
+ const [httpState, dispatch] = useReducer(httpReducer, {
+ status: startWithPending ? 'pending' : null,
+ data: null,
+ error: null,
+ });
+
+ const sendRequest = useCallback(
+ async function (requestData) {
+ dispatch({ type: 'SEND' });
+ try {
+ const responseData = await requestFunction(requestData);
+ dispatch({ type: 'SUCCESS', responseData });
+ } catch (error) {
+ dispatch({
+ type: 'ERROR',
+ errorMessage: error.message || 'Something went wrong!',
+ });
+ }
+ },
+ [requestFunction]
+ );
+
+ return {
+ sendRequest,
+ ...httpState,
+ };
+}
+
+export default useHttp;
diff --git a/code/20-sending-getting-quote-data/src/index.css b/code/20-sending-getting-quote-data/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/20-sending-getting-quote-data/src/index.js b/code/20-sending-getting-quote-data/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/20-sending-getting-quote-data/src/lib/api.js b/code/20-sending-getting-quote-data/src/lib/api.js
new file mode 100644
index 0000000000..17d2b1a359
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/lib/api.js
@@ -0,0 +1,96 @@
+const FIREBASE_DOMAIN = 'https://react-prep-default-rtdb.firebaseio.com';
+
+export async function getAllQuotes() {
+ const response = await fetch(`${FIREBASE_DOMAIN}/quotes.json`);
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Could not fetch quotes.');
+ }
+
+ const transformedQuotes = [];
+
+ for (const key in data) {
+ const quoteObj = {
+ id: key,
+ ...data[key],
+ };
+
+ transformedQuotes.push(quoteObj);
+ }
+
+ return transformedQuotes;
+}
+
+export async function getSingleQuote(quoteId) {
+ const response = await fetch(`${FIREBASE_DOMAIN}/quotes/${quoteId}.json`);
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Could not fetch quote.');
+ }
+
+ const loadedQuote = {
+ id: quoteId,
+ ...data,
+ };
+
+ return loadedQuote;
+}
+
+export async function addQuote(quoteData) {
+ const response = await fetch(`${FIREBASE_DOMAIN}/quotes.json`, {
+ method: 'POST',
+ body: JSON.stringify(quoteData),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Could not create quote.');
+ }
+
+ return null;
+}
+
+export async function addComment(commentData, quoteId) {
+ const response = await fetch(`${FIREBASE_DOMAIN}/comments/${quoteId}.json`, {
+ method: 'POST',
+ body: JSON.stringify(commentData),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Could not add comment.');
+ }
+
+ return { commentId: data.name };
+}
+
+export async function getAllComments(quoteId) {
+ const response = await fetch(`${FIREBASE_DOMAIN}/comments/${quoteId}.json`);
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Could not get comments.');
+ }
+
+ const transformedComments = [];
+
+ for (const key in data) {
+ const commentObj = {
+ id: key,
+ ...data[key],
+ };
+
+ transformedComments.push(commentObj);
+ }
+
+ return transformedComments;
+}
diff --git a/code/20-sending-getting-quote-data/src/pages/AllQuotes.js b/code/20-sending-getting-quote-data/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..ffd486ebc9
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/pages/AllQuotes.js
@@ -0,0 +1,38 @@
+import { useEffect } from 'react';
+
+import QuoteList from '../components/quotes/QuoteList';
+import LoadingSpinner from '../components/UI/LoadingSpinner';
+import NoQuotesFound from '../components/quotes/NoQuotesFound';
+import useHttp from '../hooks/use-http';
+import { getAllQuotes } from '../lib/api';
+
+const AllQuotes = () => {
+ const { sendRequest, status, data: loadedQuotes, error } = useHttp(
+ getAllQuotes,
+ true
+ );
+
+ useEffect(() => {
+ sendRequest();
+ }, [sendRequest]);
+
+ if (status === 'pending') {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return {error}
;
+ }
+
+ if (status === 'completed' && (!loadedQuotes || loadedQuotes.length === 0)) {
+ return ;
+ }
+
+ return ;
+};
+
+export default AllQuotes;
diff --git a/code/20-sending-getting-quote-data/src/pages/NewQuote.js b/code/20-sending-getting-quote-data/src/pages/NewQuote.js
new file mode 100644
index 0000000000..b571e25788
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/pages/NewQuote.js
@@ -0,0 +1,25 @@
+import { useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
+
+import QuoteForm from '../components/quotes/QuoteForm';
+import useHttp from '../hooks/use-http';
+import { addQuote } from '../lib/api';
+
+const NewQuote = () => {
+ const { sendRequest, status } = useHttp(addQuote);
+ const history = useHistory();
+
+ useEffect(() => {
+ if (status === 'completed') {
+ history.push('/quotes');
+ }
+ }, [status, history]);
+
+ const addQuoteHandler = (quoteData) => {
+ sendRequest(quoteData);
+ };
+
+ return ;
+};
+
+export default NewQuote;
diff --git a/code/20-sending-getting-quote-data/src/pages/NotFound.js b/code/20-sending-getting-quote-data/src/pages/NotFound.js
new file mode 100644
index 0000000000..bb7c85baac
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/pages/NotFound.js
@@ -0,0 +1,9 @@
+const NotFound = () => {
+ return (
+
+ );
+};
+
+export default NotFound;
diff --git a/code/20-sending-getting-quote-data/src/pages/QuoteDetail.js b/code/20-sending-getting-quote-data/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..3992b44738
--- /dev/null
+++ b/code/20-sending-getting-quote-data/src/pages/QuoteDetail.js
@@ -0,0 +1,58 @@
+import { Fragment, useEffect } from 'react';
+import { useParams, Route, Link, useRouteMatch } from 'react-router-dom';
+
+import HighlightedQuote from '../components/quotes/HighlightedQuote';
+import Comments from '../components/comments/Comments';
+import useHttp from '../hooks/use-http';
+import { getSingleQuote } from '../lib/api';
+import LoadingSpinner from '../components/UI/LoadingSpinner';
+
+const QuoteDetail = () => {
+ const match = useRouteMatch();
+ const params = useParams();
+
+ const { quoteId } = params;
+
+ const { sendRequest, status, data: loadedQuote, error } = useHttp(
+ getSingleQuote,
+ true
+ );
+
+ useEffect(() => {
+ sendRequest(quoteId);
+ }, [sendRequest, quoteId]);
+
+ if (status === 'pending') {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return {error}
;
+ }
+
+ if (!loadedQuote.text) {
+ return No quote found!
;
+ }
+
+ return (
+
+
+
+
+
+ Load Comments
+
+
+
+
+
+
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/code/21-finished/package.json b/code/21-finished/package.json
new file mode 100644
index 0000000000..c0645e836b
--- /dev/null
+++ b/code/21-finished/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.11.6",
+ "@testing-library/react": "^11.2.2",
+ "@testing-library/user-event": "^12.5.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^5.2.0",
+ "react-scripts": "^5.0.1",
+ "web-vitals": "^0.2.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/code/21-finished/public/favicon.ico b/code/21-finished/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/21-finished/public/favicon.ico differ
diff --git a/code/21-finished/public/index.html b/code/21-finished/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/21-finished/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/21-finished/public/logo192.png b/code/21-finished/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/21-finished/public/logo192.png differ
diff --git a/code/21-finished/public/logo512.png b/code/21-finished/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/21-finished/public/logo512.png differ
diff --git a/code/21-finished/public/manifest.json b/code/21-finished/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/21-finished/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/code/21-finished/public/robots.txt b/code/21-finished/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/21-finished/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/21-finished/src/App.js b/code/21-finished/src/App.js
new file mode 100644
index 0000000000..97eb33fc84
--- /dev/null
+++ b/code/21-finished/src/App.js
@@ -0,0 +1,33 @@
+import { Route, Switch, Redirect } from 'react-router-dom';
+
+import AllQuotes from './pages/AllQuotes';
+import QuoteDetail from './pages/QuoteDetail';
+import NewQuote from './pages/NewQuote';
+import NotFound from './pages/NotFound';
+import Layout from './components/layout/Layout';
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/21-finished/src/components/UI/Card.js b/code/21-finished/src/components/UI/Card.js
new file mode 100644
index 0000000000..03c3af7db2
--- /dev/null
+++ b/code/21-finished/src/components/UI/Card.js
@@ -0,0 +1,7 @@
+import classes from './Card.module.css';
+
+const Card = (props) => {
+ return {props.children}
;
+};
+
+export default Card;
diff --git a/code/21-finished/src/components/UI/Card.module.css b/code/21-finished/src/components/UI/Card.module.css
new file mode 100644
index 0000000000..dad43b15fb
--- /dev/null
+++ b/code/21-finished/src/components/UI/Card.module.css
@@ -0,0 +1,7 @@
+.card {
+ padding: 1rem;
+ margin: 1rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: white;
+}
diff --git a/code/21-finished/src/components/UI/LoadingSpinner.js b/code/21-finished/src/components/UI/LoadingSpinner.js
new file mode 100644
index 0000000000..4b88a52d45
--- /dev/null
+++ b/code/21-finished/src/components/UI/LoadingSpinner.js
@@ -0,0 +1,7 @@
+import classes from './LoadingSpinner.module.css';
+
+const LoadingSpinner = () => {
+ return ;
+}
+
+export default LoadingSpinner;
diff --git a/code/21-finished/src/components/UI/LoadingSpinner.module.css b/code/21-finished/src/components/UI/LoadingSpinner.module.css
new file mode 100644
index 0000000000..7b38ef62f1
--- /dev/null
+++ b/code/21-finished/src/components/UI/LoadingSpinner.module.css
@@ -0,0 +1,24 @@
+.spinner {
+ display: inline-block;
+ width: 80px;
+ height: 80px;
+}
+.spinner:after {
+ content: ' ';
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border-radius: 50%;
+ border: 6px solid teal;
+ border-color: teal transparent teal transparent;
+ animation: spinner 1.2s linear infinite;
+}
+@keyframes spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/code/21-finished/src/components/comments/CommentItem.js b/code/21-finished/src/components/comments/CommentItem.js
new file mode 100644
index 0000000000..448654f269
--- /dev/null
+++ b/code/21-finished/src/components/comments/CommentItem.js
@@ -0,0 +1,11 @@
+import classes from './CommentItem.module.css';
+
+const CommentItem = (props) => {
+ return (
+
+ {props.text}
+
+ );
+};
+
+export default CommentItem;
diff --git a/code/21-finished/src/components/comments/CommentItem.module.css b/code/21-finished/src/components/comments/CommentItem.module.css
new file mode 100644
index 0000000000..21b1bef872
--- /dev/null
+++ b/code/21-finished/src/components/comments/CommentItem.module.css
@@ -0,0 +1,7 @@
+.item {
+ margin: 1rem 0;
+ color: #4a5555;
+ font-size: 1.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid teal;;
+}
\ No newline at end of file
diff --git a/code/21-finished/src/components/comments/Comments.js b/code/21-finished/src/components/comments/Comments.js
new file mode 100644
index 0000000000..99b9ae2eab
--- /dev/null
+++ b/code/21-finished/src/components/comments/Comments.js
@@ -0,0 +1,71 @@
+import { useState, useEffect, useCallback } from 'react';
+import { useParams } from 'react-router-dom';
+
+import classes from './Comments.module.css';
+import NewCommentForm from './NewCommentForm';
+import useHttp from '../../hooks/use-http';
+import { getAllComments } from '../../lib/api';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import CommentsList from './CommentsList';
+
+const Comments = () => {
+ const [isAddingComment, setIsAddingComment] = useState(false);
+ const params = useParams();
+
+ const { quoteId } = params;
+
+ const { sendRequest, status, data: loadedComments } = useHttp(getAllComments);
+
+ useEffect(() => {
+ sendRequest(quoteId);
+ }, [quoteId, sendRequest]);
+
+ const startAddCommentHandler = () => {
+ setIsAddingComment(true);
+ };
+
+ const addedCommentHandler = useCallback(() => {
+ sendRequest(quoteId);
+ }, [sendRequest, quoteId]);
+
+ let comments;
+
+ if (status === 'pending') {
+ comments = (
+
+
+
+ );
+ }
+
+ if (status === 'completed' && loadedComments && loadedComments.length > 0) {
+ comments = ;
+ }
+
+ if (
+ status === 'completed' &&
+ (!loadedComments || loadedComments.length === 0)
+ ) {
+ comments = No comments were added yet!
;
+ }
+
+ return (
+
+ User Comments
+ {!isAddingComment && (
+
+ )}
+ {isAddingComment && (
+
+ )}
+ {comments}
+
+ );
+};
+
+export default Comments;
diff --git a/code/21-finished/src/components/comments/Comments.module.css b/code/21-finished/src/components/comments/Comments.module.css
new file mode 100644
index 0000000000..0fad756422
--- /dev/null
+++ b/code/21-finished/src/components/comments/Comments.module.css
@@ -0,0 +1,7 @@
+.comments {
+ text-align: center;
+}
+
+.comments > button {
+ font-size: 1.25rem;
+}
\ No newline at end of file
diff --git a/code/21-finished/src/components/comments/CommentsList.js b/code/21-finished/src/components/comments/CommentsList.js
new file mode 100644
index 0000000000..5e800a22e9
--- /dev/null
+++ b/code/21-finished/src/components/comments/CommentsList.js
@@ -0,0 +1,14 @@
+import CommentItem from './CommentItem';
+import classes from './CommentsList.module.css';
+
+const CommentsList = (props) => {
+ return (
+
+ {props.comments.map((comment) => (
+
+ ))}
+
+ );
+};
+
+export default CommentsList;
diff --git a/code/21-finished/src/components/comments/CommentsList.module.css b/code/21-finished/src/components/comments/CommentsList.module.css
new file mode 100644
index 0000000000..6b7aaac226
--- /dev/null
+++ b/code/21-finished/src/components/comments/CommentsList.module.css
@@ -0,0 +1,5 @@
+.comments {
+ list-style: none;
+ margin: 2.5rem 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/code/21-finished/src/components/comments/NewCommentForm.js b/code/21-finished/src/components/comments/NewCommentForm.js
new file mode 100644
index 0000000000..821f8c25a0
--- /dev/null
+++ b/code/21-finished/src/components/comments/NewCommentForm.js
@@ -0,0 +1,49 @@
+import { useRef, useEffect } from 'react';
+
+import useHttp from '../../hooks/use-http';
+import { addComment } from '../../lib/api';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './NewCommentForm.module.css';
+
+const NewCommentForm = (props) => {
+ const commentTextRef = useRef();
+
+ const { sendRequest, status, error } = useHttp(addComment);
+
+ const { onAddedComment } = props;
+
+ useEffect(() => {
+ if (status === 'completed' && !error) {
+ onAddedComment();
+ }
+ }, [status, error, onAddedComment]);
+
+ const submitFormHandler = (event) => {
+ event.preventDefault();
+
+ const enteredText = commentTextRef.current.value;
+
+ // optional: Could validate here
+
+ sendRequest({ commentData: { text: enteredText }, quoteId: props.quoteId });
+ };
+
+ return (
+
+ );
+};
+
+export default NewCommentForm;
diff --git a/code/21-finished/src/components/comments/NewCommentForm.module.css b/code/21-finished/src/components/comments/NewCommentForm.module.css
new file mode 100644
index 0000000000..3b2565652d
--- /dev/null
+++ b/code/21-finished/src/components/comments/NewCommentForm.module.css
@@ -0,0 +1,45 @@
+.form {
+ margin-top: 1rem;
+ position: relative;
+ text-align: center;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/21-finished/src/components/layout/Layout.js b/code/21-finished/src/components/layout/Layout.js
new file mode 100644
index 0000000000..2eff00a625
--- /dev/null
+++ b/code/21-finished/src/components/layout/Layout.js
@@ -0,0 +1,15 @@
+import { Fragment } from 'react';
+
+import classes from './Layout.module.css';
+import MainNavigation from './MainNavigation';
+
+const Layout = (props) => {
+ return (
+
+
+ {props.children}
+
+ );
+};
+
+export default Layout;
diff --git a/code/21-finished/src/components/layout/Layout.module.css b/code/21-finished/src/components/layout/Layout.module.css
new file mode 100644
index 0000000000..eb13837358
--- /dev/null
+++ b/code/21-finished/src/components/layout/Layout.module.css
@@ -0,0 +1,5 @@
+.main {
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
\ No newline at end of file
diff --git a/code/21-finished/src/components/layout/MainNavigation.js b/code/21-finished/src/components/layout/MainNavigation.js
new file mode 100644
index 0000000000..76d790a1c3
--- /dev/null
+++ b/code/21-finished/src/components/layout/MainNavigation.js
@@ -0,0 +1,27 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+const MainNavigation = () => {
+ return (
+
+ Great Quotes
+
+
+ );
+};
+
+export default MainNavigation;
diff --git a/code/21-finished/src/components/layout/MainNavigation.module.css b/code/21-finished/src/components/layout/MainNavigation.module.css
new file mode 100644
index 0000000000..be9d206679
--- /dev/null
+++ b/code/21-finished/src/components/layout/MainNavigation.module.css
@@ -0,0 +1,37 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ display: flex;
+ padding: 0 10%;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #008080;
+}
+
+.logo {
+ font-size: 2rem;
+ color: white;
+}
+
+.nav ul {
+ list-style: none;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ margin-left: 1.5rem;
+ font-size: 1.25rem;
+}
+
+.nav a {
+ text-decoration: none;
+ color: #88dfdf;
+}
+
+.nav a:hover,
+.nav a:active,
+.nav a.active {
+ color: #e6fcfc;
+}
diff --git a/code/21-finished/src/components/quotes/HighlightedQuote.js b/code/21-finished/src/components/quotes/HighlightedQuote.js
new file mode 100644
index 0000000000..b6d3445c28
--- /dev/null
+++ b/code/21-finished/src/components/quotes/HighlightedQuote.js
@@ -0,0 +1,12 @@
+import classes from './HighlightedQuote.module.css';
+
+const HighlightedQuote = (props) => {
+ return (
+
+ {props.text}
+ {props.author}
+
+ );
+};
+
+export default HighlightedQuote;
diff --git a/code/21-finished/src/components/quotes/HighlightedQuote.module.css b/code/21-finished/src/components/quotes/HighlightedQuote.module.css
new file mode 100644
index 0000000000..466b463010
--- /dev/null
+++ b/code/21-finished/src/components/quotes/HighlightedQuote.module.css
@@ -0,0 +1,20 @@
+.quote {
+ background-color: #162b2b;
+ color: white;
+ border-radius: 6px;
+ padding: 3rem;
+ margin: 3rem auto;
+ width: 90%;
+ max-width: 40rem;
+}
+
+.quote p {
+ font-size: 2.5rem;
+}
+
+.quote figcaption {
+ font-style: italic;
+ font-size: 1.5rem;
+ text-align: right;
+ color: #a1e0e0;
+}
\ No newline at end of file
diff --git a/code/21-finished/src/components/quotes/NoQuotesFound.js b/code/21-finished/src/components/quotes/NoQuotesFound.js
new file mode 100644
index 0000000000..8642a10db4
--- /dev/null
+++ b/code/21-finished/src/components/quotes/NoQuotesFound.js
@@ -0,0 +1,16 @@
+import { Link } from 'react-router-dom';
+
+import classes from './NoQuotesFound.module.css';
+
+const NoQuotesFound = () => {
+ return (
+
+
No quotes found!
+
+ Add a Quote
+
+
+ );
+};
+
+export default NoQuotesFound;
diff --git a/code/21-finished/src/components/quotes/NoQuotesFound.module.css b/code/21-finished/src/components/quotes/NoQuotesFound.module.css
new file mode 100644
index 0000000000..0d48b19f9b
--- /dev/null
+++ b/code/21-finished/src/components/quotes/NoQuotesFound.module.css
@@ -0,0 +1,17 @@
+.noquotes {
+ height: 20rem;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.noquotes p {
+ color: #262c2c;
+ font-size: 3rem;
+ font-weight: bold;
+}
+
diff --git a/code/21-finished/src/components/quotes/QuoteForm.js b/code/21-finished/src/components/quotes/QuoteForm.js
new file mode 100644
index 0000000000..c50916ab91
--- /dev/null
+++ b/code/21-finished/src/components/quotes/QuoteForm.js
@@ -0,0 +1,70 @@
+import { Fragment, useRef, useState } from 'react';
+import { Prompt } from 'react-router-dom';
+
+import Card from '../UI/Card';
+import LoadingSpinner from '../UI/LoadingSpinner';
+import classes from './QuoteForm.module.css';
+
+const QuoteForm = (props) => {
+ const [isEntering, setIsEntering] = useState(false);
+
+ const authorInputRef = useRef();
+ const textInputRef = useRef();
+
+ function submitFormHandler(event) {
+ event.preventDefault();
+
+ const enteredAuthor = authorInputRef.current.value;
+ const enteredText = textInputRef.current.value;
+
+ // optional: Could validate here
+
+ props.onAddQuote({ author: enteredAuthor, text: enteredText });
+ }
+
+ const finishEnteringHandler = () => {
+ setIsEntering(false);
+ };
+
+ const formFocusedHandler = () => {
+ setIsEntering(true);
+ };
+
+ return (
+
+
+ 'Are you sure you want to leave? All your entered data will be lost!'
+ }
+ />
+
+
+
+
+ );
+};
+
+export default QuoteForm;
diff --git a/code/21-finished/src/components/quotes/QuoteForm.module.css b/code/21-finished/src/components/quotes/QuoteForm.module.css
new file mode 100644
index 0000000000..ee8d855137
--- /dev/null
+++ b/code/21-finished/src/components/quotes/QuoteForm.module.css
@@ -0,0 +1,49 @@
+.form {
+ position: relative;
+}
+
+.loading {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.control {
+ margin-bottom: 0.5rem;
+}
+
+.control label {
+ font-weight: bold;
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.control input,
+.control textarea {
+ font: inherit;
+ padding: 0.35rem;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #c1d1d1;
+ display: block;
+ width: 100%;
+ font-size: 1.25rem;
+}
+
+.control input:focus,
+.control textarea:focus {
+ background-color: #cbf8f8;
+ outline-color: teal;
+}
+
+.actions {
+ text-align: right;
+}
+
+.actions button {
+ font-size: 1.25rem;
+}
diff --git a/code/21-finished/src/components/quotes/QuoteItem.js b/code/21-finished/src/components/quotes/QuoteItem.js
new file mode 100644
index 0000000000..ee1ccb3ddc
--- /dev/null
+++ b/code/21-finished/src/components/quotes/QuoteItem.js
@@ -0,0 +1,21 @@
+import { Link } from 'react-router-dom';
+
+import classes from './QuoteItem.module.css';
+
+const QuoteItem = (props) => {
+ return (
+
+
+
+ {props.text}
+
+ {props.author}
+
+
+ View Fullscreen
+
+
+ );
+};
+
+export default QuoteItem;
diff --git a/code/21-finished/src/components/quotes/QuoteItem.module.css b/code/21-finished/src/components/quotes/QuoteItem.module.css
new file mode 100644
index 0000000000..74cd09b8b7
--- /dev/null
+++ b/code/21-finished/src/components/quotes/QuoteItem.module.css
@@ -0,0 +1,37 @@
+.item {
+ margin: 1rem 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ background-color: #c2e7f0;
+}
+
+.item:last-of-type {
+ border-bottom: none;
+}
+
+.item figure {
+ margin: 0;
+ padding: 0;
+ width: 70%;
+}
+
+.item blockquote {
+ margin: 0;
+ text-align: left;
+ font-size: 1.5rem;
+ color: #212929;
+}
+
+.item p {
+ margin: 0;
+ margin-bottom: 0.25rem;
+}
+
+.item figcaption {
+ font-style: italic;
+ color: #566d6d;
+}
diff --git a/code/21-finished/src/components/quotes/QuoteList.js b/code/21-finished/src/components/quotes/QuoteList.js
new file mode 100644
index 0000000000..05cab9d98b
--- /dev/null
+++ b/code/21-finished/src/components/quotes/QuoteList.js
@@ -0,0 +1,55 @@
+import { Fragment } from 'react';
+import { useHistory, useLocation } from 'react-router-dom';
+
+import QuoteItem from './QuoteItem';
+import classes from './QuoteList.module.css';
+
+const sortQuotes = (quotes, ascending) => {
+ return quotes.sort((quoteA, quoteB) => {
+ if (ascending) {
+ return quoteA.id > quoteB.id ? 1 : -1;
+ } else {
+ return quoteA.id < quoteB.id ? 1 : -1;
+ }
+ });
+};
+
+const QuoteList = (props) => {
+ const history = useHistory();
+ const location = useLocation();
+
+ const queryParams = new URLSearchParams(location.search);
+
+ const isSortingAscending = queryParams.get('sort') === 'asc';
+
+ const sortedQuotes = sortQuotes(props.quotes, isSortingAscending);
+
+ const changeSortingHandler = () => {
+ history.push({
+ pathname: location.pathname,
+ search: `?sort=${(isSortingAscending ? 'desc' : 'asc')}`
+ });
+ };
+
+ return (
+
+
+
+
+
+ {sortedQuotes.map((quote) => (
+
+ ))}
+
+
+ );
+};
+
+export default QuoteList;
diff --git a/code/21-finished/src/components/quotes/QuoteList.module.css b/code/21-finished/src/components/quotes/QuoteList.module.css
new file mode 100644
index 0000000000..cfb5fbf9ab
--- /dev/null
+++ b/code/21-finished/src/components/quotes/QuoteList.module.css
@@ -0,0 +1,25 @@
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.sorting {
+ padding-bottom: 1rem;
+ border-bottom: 3px solid #b2d4d4;
+ margin-bottom: 2rem;
+}
+
+.sorting button {
+ font: inherit;
+ color: teal;
+ border: 1px solid teal;
+ background-color: transparent;
+ border-radius: 4px;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+}
+
+.sorting button:hover {
+ background-color: #c2fafa;
+}
\ No newline at end of file
diff --git a/code/21-finished/src/hooks.zip b/code/21-finished/src/hooks.zip
new file mode 100644
index 0000000000..41fb9a916d
Binary files /dev/null and b/code/21-finished/src/hooks.zip differ
diff --git a/code/21-finished/src/hooks/use-http.js b/code/21-finished/src/hooks/use-http.js
new file mode 100644
index 0000000000..b7c56bc76a
--- /dev/null
+++ b/code/21-finished/src/hooks/use-http.js
@@ -0,0 +1,60 @@
+import { useReducer, useCallback } from 'react';
+
+function httpReducer(state, action) {
+ if (action.type === 'SEND') {
+ return {
+ data: null,
+ error: null,
+ status: 'pending',
+ };
+ }
+
+ if (action.type === 'SUCCESS') {
+ return {
+ data: action.responseData,
+ error: null,
+ status: 'completed',
+ };
+ }
+
+ if (action.type === 'ERROR') {
+ return {
+ data: null,
+ error: action.errorMessage,
+ status: 'completed',
+ };
+ }
+
+ return state;
+}
+
+function useHttp(requestFunction, startWithPending = false) {
+ const [httpState, dispatch] = useReducer(httpReducer, {
+ status: startWithPending ? 'pending' : null,
+ data: null,
+ error: null,
+ });
+
+ const sendRequest = useCallback(
+ async function (requestData) {
+ dispatch({ type: 'SEND' });
+ try {
+ const responseData = await requestFunction(requestData);
+ dispatch({ type: 'SUCCESS', responseData });
+ } catch (error) {
+ dispatch({
+ type: 'ERROR',
+ errorMessage: error.message || 'Something went wrong!',
+ });
+ }
+ },
+ [requestFunction]
+ );
+
+ return {
+ sendRequest,
+ ...httpState,
+ };
+}
+
+export default useHttp;
diff --git a/code/21-finished/src/index.css b/code/21-finished/src/index.css
new file mode 100644
index 0000000000..039c6d7fe6
--- /dev/null
+++ b/code/21-finished/src/index.css
@@ -0,0 +1,56 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ background-color: #e7f8f8;
+}
+
+.centered {
+ margin: 3rem auto;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.focused {
+ font-size: 3rem;
+ font-weight: bold;
+ color: white;
+}
+
+.btn {
+ text-decoration: none;
+ background-color: teal;
+ color: white;
+ border-radius: 4px;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid teal;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:active {
+ background-color: #11acac;
+ border-color: #11acac;
+}
+
+.btn--flat {
+ cursor: pointer;
+ font: inherit;
+ color: teal;
+ border: none;
+ background-color: none;
+ text-decoration: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+}
+
+.btn--flat:hover,
+.btn--flat:active {
+ background-color: teal;
+ color: white;
+}
\ No newline at end of file
diff --git a/code/21-finished/src/index.js b/code/21-finished/src/index.js
new file mode 100644
index 0000000000..c2a6402346
--- /dev/null
+++ b/code/21-finished/src/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/code/21-finished/src/lib.zip b/code/21-finished/src/lib.zip
new file mode 100644
index 0000000000..3cd03daba9
Binary files /dev/null and b/code/21-finished/src/lib.zip differ
diff --git a/code/21-finished/src/lib/api.js b/code/21-finished/src/lib/api.js
new file mode 100644
index 0000000000..2bc84aac81
--- /dev/null
+++ b/code/21-finished/src/lib/api.js
@@ -0,0 +1,96 @@
+const FIREBASE_DOMAIN = 'https://react-prep-default-rtdb.firebaseio.com';
+
+export async function getAllQuotes() {
+ const response = await fetch(`${FIREBASE_DOMAIN}/quotes.json`);
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Could not fetch quotes.');
+ }
+
+ const transformedQuotes = [];
+
+ for (const key in data) {
+ const quoteObj = {
+ id: key,
+ ...data[key],
+ };
+
+ transformedQuotes.push(quoteObj);
+ }
+
+ return transformedQuotes;
+}
+
+export async function getSingleQuote(quoteId) {
+ const response = await fetch(`${FIREBASE_DOMAIN}/quotes/${quoteId}.json`);
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Could not fetch quote.');
+ }
+
+ const loadedQuote = {
+ id: quoteId,
+ ...data,
+ };
+
+ return loadedQuote;
+}
+
+export async function addQuote(quoteData) {
+ const response = await fetch(`${FIREBASE_DOMAIN}/quotes.json`, {
+ method: 'POST',
+ body: JSON.stringify(quoteData),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Could not create quote.');
+ }
+
+ return null;
+}
+
+export async function addComment(requestData) {
+ const response = await fetch(`${FIREBASE_DOMAIN}/comments/${requestData.quoteId}.json`, {
+ method: 'POST',
+ body: JSON.stringify(requestData.commentData),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Could not add comment.');
+ }
+
+ return { commentId: data.name };
+}
+
+export async function getAllComments(quoteId) {
+ const response = await fetch(`${FIREBASE_DOMAIN}/comments/${quoteId}.json`);
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Could not get comments.');
+ }
+
+ const transformedComments = [];
+
+ for (const key in data) {
+ const commentObj = {
+ id: key,
+ ...data[key],
+ };
+
+ transformedComments.push(commentObj);
+ }
+
+ return transformedComments;
+}
diff --git a/code/21-finished/src/pages/AllQuotes.js b/code/21-finished/src/pages/AllQuotes.js
new file mode 100644
index 0000000000..ffd486ebc9
--- /dev/null
+++ b/code/21-finished/src/pages/AllQuotes.js
@@ -0,0 +1,38 @@
+import { useEffect } from 'react';
+
+import QuoteList from '../components/quotes/QuoteList';
+import LoadingSpinner from '../components/UI/LoadingSpinner';
+import NoQuotesFound from '../components/quotes/NoQuotesFound';
+import useHttp from '../hooks/use-http';
+import { getAllQuotes } from '../lib/api';
+
+const AllQuotes = () => {
+ const { sendRequest, status, data: loadedQuotes, error } = useHttp(
+ getAllQuotes,
+ true
+ );
+
+ useEffect(() => {
+ sendRequest();
+ }, [sendRequest]);
+
+ if (status === 'pending') {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return {error}
;
+ }
+
+ if (status === 'completed' && (!loadedQuotes || loadedQuotes.length === 0)) {
+ return ;
+ }
+
+ return ;
+};
+
+export default AllQuotes;
diff --git a/code/21-finished/src/pages/NewQuote.js b/code/21-finished/src/pages/NewQuote.js
new file mode 100644
index 0000000000..b571e25788
--- /dev/null
+++ b/code/21-finished/src/pages/NewQuote.js
@@ -0,0 +1,25 @@
+import { useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
+
+import QuoteForm from '../components/quotes/QuoteForm';
+import useHttp from '../hooks/use-http';
+import { addQuote } from '../lib/api';
+
+const NewQuote = () => {
+ const { sendRequest, status } = useHttp(addQuote);
+ const history = useHistory();
+
+ useEffect(() => {
+ if (status === 'completed') {
+ history.push('/quotes');
+ }
+ }, [status, history]);
+
+ const addQuoteHandler = (quoteData) => {
+ sendRequest(quoteData);
+ };
+
+ return ;
+};
+
+export default NewQuote;
diff --git a/code/21-finished/src/pages/NotFound.js b/code/21-finished/src/pages/NotFound.js
new file mode 100644
index 0000000000..bb7c85baac
--- /dev/null
+++ b/code/21-finished/src/pages/NotFound.js
@@ -0,0 +1,9 @@
+const NotFound = () => {
+ return (
+
+ );
+};
+
+export default NotFound;
diff --git a/code/21-finished/src/pages/QuoteDetail.js b/code/21-finished/src/pages/QuoteDetail.js
new file mode 100644
index 0000000000..3992b44738
--- /dev/null
+++ b/code/21-finished/src/pages/QuoteDetail.js
@@ -0,0 +1,58 @@
+import { Fragment, useEffect } from 'react';
+import { useParams, Route, Link, useRouteMatch } from 'react-router-dom';
+
+import HighlightedQuote from '../components/quotes/HighlightedQuote';
+import Comments from '../components/comments/Comments';
+import useHttp from '../hooks/use-http';
+import { getSingleQuote } from '../lib/api';
+import LoadingSpinner from '../components/UI/LoadingSpinner';
+
+const QuoteDetail = () => {
+ const match = useRouteMatch();
+ const params = useParams();
+
+ const { quoteId } = params;
+
+ const { sendRequest, status, data: loadedQuote, error } = useHttp(
+ getSingleQuote,
+ true
+ );
+
+ useEffect(() => {
+ sendRequest(quoteId);
+ }, [sendRequest, quoteId]);
+
+ if (status === 'pending') {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return {error}
;
+ }
+
+ if (!loadedQuote.text) {
+ return No quote found!
;
+ }
+
+ return (
+
+
+
+
+
+ Load Comments
+
+
+
+
+
+
+
+ );
+};
+
+export default QuoteDetail;
diff --git a/extra-files/MainHeader.module.css b/extra-files/MainHeader.module.css
new file mode 100644
index 0000000000..0d792b4c0d
--- /dev/null
+++ b/extra-files/MainHeader.module.css
@@ -0,0 +1,38 @@
+.header {
+ width: 100%;
+ height: 5rem;
+ background-color: #044599;
+ padding: 0 10%;
+}
+
+.header nav {
+ height: 100%;
+}
+
+.header ul {
+ height: 100%;
+ list-style: none;
+ display: flex;
+ padding: 0;
+ margin: 0;
+ align-items: center;
+ justify-content: center;
+}
+
+.header li {
+ margin: 0 1rem;
+ width: 5rem;
+}
+
+.header a {
+ color: white;
+ text-decoration: none;
+}
+
+.header a:hover,
+.header a:active,
+.header a.active {
+ color: #95bcf0;
+ padding-bottom: 0.25rem;
+ border-bottom: 4px solid #95bcf0;
+}
\ No newline at end of file
diff --git a/extra-files/hooks.zip b/extra-files/hooks.zip
new file mode 100644
index 0000000000..41fb9a916d
Binary files /dev/null and b/extra-files/hooks.zip differ
diff --git a/extra-files/index.css b/extra-files/index.css
new file mode 100644
index 0000000000..44a860502a
--- /dev/null
+++ b/extra-files/index.css
@@ -0,0 +1,26 @@
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ font-family: 'Noto Sans JP', sans-serif;
+}
+
+body {
+ margin: 0;
+ background-color: #e0e9f5;
+}
+
+main {
+ margin-top: 7rem;
+ text-align: center;
+}
+
+h1,
+h2,
+h3,
+p {
+ color: #042b5f;
+}
diff --git a/extra-files/lib.zip b/extra-files/lib.zip
new file mode 100644
index 0000000000..3cd03daba9
Binary files /dev/null and b/extra-files/lib.zip differ
diff --git a/extra-files/sorting.js b/extra-files/sorting.js
new file mode 100644
index 0000000000..3423908b6a
--- /dev/null
+++ b/extra-files/sorting.js
@@ -0,0 +1,9 @@
+const sortQuotes = (quotes, ascending) => {
+ return quotes.sort((quoteA, quoteB) => {
+ if (ascending) {
+ return quoteA.id > quoteB.id ? 1 : -1;
+ } else {
+ return quoteA.id < quoteB.id ? 1 : -1;
+ }
+ });
+};
diff --git a/slides/slides.pdf b/slides/slides.pdf
new file mode 100644
index 0000000000..395967faef
Binary files /dev/null and b/slides/slides.pdf differ