diff --git a/code/01-starting-project/package.json b/code/01-starting-project/package.json
new file mode 100644
index 0000000000..edaa0ea9e5
--- /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.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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..6f74d94b5d
--- /dev/null
+++ b/code/01-starting-project/src/App.js
@@ -0,0 +1,5 @@
+function App() {
+ return ;
+}
+
+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..70bfa7fb2a
--- /dev/null
+++ b/code/01-starting-project/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/01-starting-project/src/index.js b/code/01-starting-project/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/01-starting-project/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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-route-setup/package.json b/code/02-route-setup/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/02-route-setup/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-route-setup/public/favicon.ico b/code/02-route-setup/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/02-route-setup/public/favicon.ico differ
diff --git a/code/02-route-setup/public/index.html b/code/02-route-setup/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/02-route-setup/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/02-route-setup/public/logo192.png b/code/02-route-setup/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/02-route-setup/public/logo192.png differ
diff --git a/code/02-route-setup/public/logo512.png b/code/02-route-setup/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/02-route-setup/public/logo512.png differ
diff --git a/code/02-route-setup/public/manifest.json b/code/02-route-setup/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/02-route-setup/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-route-setup/public/robots.txt b/code/02-route-setup/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/02-route-setup/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/02-route-setup/src/App.js b/code/02-route-setup/src/App.js
new file mode 100644
index 0000000000..82e1a0f0fe
--- /dev/null
+++ b/code/02-route-setup/src/App.js
@@ -0,0 +1,13 @@
+import { createBrowserRouter, RouterProvider } from 'react-router-dom';
+
+import HomePage from './pages/Home';
+
+const router = createBrowserRouter([
+ { path: '/', element: },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/02-route-setup/src/index.css b/code/02-route-setup/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/02-route-setup/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/02-route-setup/src/index.js b/code/02-route-setup/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/02-route-setup/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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-route-setup/src/pages/Home.js b/code/02-route-setup/src/pages/Home.js
new file mode 100644
index 0000000000..e101a222ae
--- /dev/null
+++ b/code/02-route-setup/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return My Home Page
;
+}
+
+export default HomePage;
diff --git a/code/03-alternative-route-definitions/package.json b/code/03-alternative-route-definitions/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/03-alternative-route-definitions/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-alternative-route-definitions/public/favicon.ico b/code/03-alternative-route-definitions/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/03-alternative-route-definitions/public/favicon.ico differ
diff --git a/code/03-alternative-route-definitions/public/index.html b/code/03-alternative-route-definitions/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/03-alternative-route-definitions/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/03-alternative-route-definitions/public/logo192.png b/code/03-alternative-route-definitions/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/03-alternative-route-definitions/public/logo192.png differ
diff --git a/code/03-alternative-route-definitions/public/logo512.png b/code/03-alternative-route-definitions/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/03-alternative-route-definitions/public/logo512.png differ
diff --git a/code/03-alternative-route-definitions/public/manifest.json b/code/03-alternative-route-definitions/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/03-alternative-route-definitions/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-alternative-route-definitions/public/robots.txt b/code/03-alternative-route-definitions/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/03-alternative-route-definitions/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/03-alternative-route-definitions/src/App.js b/code/03-alternative-route-definitions/src/App.js
new file mode 100644
index 0000000000..e072a1554f
--- /dev/null
+++ b/code/03-alternative-route-definitions/src/App.js
@@ -0,0 +1,29 @@
+import {
+ createBrowserRouter,
+ // createRoutesFromElements,
+ RouterProvider,
+ // Route,
+} from 'react-router-dom';
+
+import HomePage from './pages/Home';
+import ProductsPage from './pages/Products';
+
+// const routeDefinitions = createRoutesFromElements(
+//
+// } />
+// } />
+//
+// );
+
+const router = createBrowserRouter([
+ { path: '/', element: },
+ { path: '/products', element: },
+]);
+
+// const router = createBrowserRouter(routeDefinitions);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/03-alternative-route-definitions/src/index.css b/code/03-alternative-route-definitions/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/03-alternative-route-definitions/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/03-alternative-route-definitions/src/index.js b/code/03-alternative-route-definitions/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/03-alternative-route-definitions/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/03-alternative-route-definitions/src/pages/Home.js b/code/03-alternative-route-definitions/src/pages/Home.js
new file mode 100644
index 0000000000..e101a222ae
--- /dev/null
+++ b/code/03-alternative-route-definitions/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return My Home Page
;
+}
+
+export default HomePage;
diff --git a/code/03-alternative-route-definitions/src/pages/Products.js b/code/03-alternative-route-definitions/src/pages/Products.js
new file mode 100644
index 0000000000..6f2dd748d8
--- /dev/null
+++ b/code/03-alternative-route-definitions/src/pages/Products.js
@@ -0,0 +1,5 @@
+function ProductsPage() {
+ return The Products Page
;
+}
+
+export default ProductsPage;
diff --git a/code/04-adding-links/package.json b/code/04-adding-links/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/04-adding-links/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-adding-links/public/favicon.ico b/code/04-adding-links/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/04-adding-links/public/favicon.ico differ
diff --git a/code/04-adding-links/public/index.html b/code/04-adding-links/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/04-adding-links/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/04-adding-links/public/logo192.png b/code/04-adding-links/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/04-adding-links/public/logo192.png differ
diff --git a/code/04-adding-links/public/logo512.png b/code/04-adding-links/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/04-adding-links/public/logo512.png differ
diff --git a/code/04-adding-links/public/manifest.json b/code/04-adding-links/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/04-adding-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/04-adding-links/public/robots.txt b/code/04-adding-links/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/04-adding-links/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/04-adding-links/src/App.js b/code/04-adding-links/src/App.js
new file mode 100644
index 0000000000..e072a1554f
--- /dev/null
+++ b/code/04-adding-links/src/App.js
@@ -0,0 +1,29 @@
+import {
+ createBrowserRouter,
+ // createRoutesFromElements,
+ RouterProvider,
+ // Route,
+} from 'react-router-dom';
+
+import HomePage from './pages/Home';
+import ProductsPage from './pages/Products';
+
+// const routeDefinitions = createRoutesFromElements(
+//
+// } />
+// } />
+//
+// );
+
+const router = createBrowserRouter([
+ { path: '/', element: },
+ { path: '/products', element: },
+]);
+
+// const router = createBrowserRouter(routeDefinitions);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/04-adding-links/src/index.css b/code/04-adding-links/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/04-adding-links/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/04-adding-links/src/index.js b/code/04-adding-links/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/04-adding-links/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/04-adding-links/src/pages/Home.js b/code/04-adding-links/src/pages/Home.js
new file mode 100644
index 0000000000..adc0e53b5d
--- /dev/null
+++ b/code/04-adding-links/src/pages/Home.js
@@ -0,0 +1,14 @@
+import { Link } from 'react-router-dom';
+
+function HomePage() {
+ return (
+ <>
+ My Home Page
+
+ Go to the list of products.
+
+ >
+ );
+}
+
+export default HomePage;
diff --git a/code/04-adding-links/src/pages/Products.js b/code/04-adding-links/src/pages/Products.js
new file mode 100644
index 0000000000..6f2dd748d8
--- /dev/null
+++ b/code/04-adding-links/src/pages/Products.js
@@ -0,0 +1,5 @@
+function ProductsPage() {
+ return The Products Page
;
+}
+
+export default ProductsPage;
diff --git a/code/05-layout-wrapper-route/package.json b/code/05-layout-wrapper-route/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/05-layout-wrapper-route/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-layout-wrapper-route/public/favicon.ico b/code/05-layout-wrapper-route/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/05-layout-wrapper-route/public/favicon.ico differ
diff --git a/code/05-layout-wrapper-route/public/index.html b/code/05-layout-wrapper-route/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/05-layout-wrapper-route/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/05-layout-wrapper-route/public/logo192.png b/code/05-layout-wrapper-route/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/05-layout-wrapper-route/public/logo192.png differ
diff --git a/code/05-layout-wrapper-route/public/logo512.png b/code/05-layout-wrapper-route/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/05-layout-wrapper-route/public/logo512.png differ
diff --git a/code/05-layout-wrapper-route/public/manifest.json b/code/05-layout-wrapper-route/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/05-layout-wrapper-route/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-layout-wrapper-route/public/robots.txt b/code/05-layout-wrapper-route/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/05-layout-wrapper-route/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/05-layout-wrapper-route/src/App.js b/code/05-layout-wrapper-route/src/App.js
new file mode 100644
index 0000000000..5be4b32b70
--- /dev/null
+++ b/code/05-layout-wrapper-route/src/App.js
@@ -0,0 +1,36 @@
+import {
+ createBrowserRouter,
+ // createRoutesFromElements,
+ RouterProvider,
+ // Route,
+} from 'react-router-dom';
+
+import HomePage from './pages/Home';
+import ProductsPage from './pages/Products';
+import RootLayout from './pages/Root';
+
+// const routeDefinitions = createRoutesFromElements(
+//
+// } />
+// } />
+//
+// );
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ children: [
+ { path: '/', element: },
+ { path: '/products', element: },
+ ],
+ }
+]);
+
+// const router = createBrowserRouter(routeDefinitions);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/05-layout-wrapper-route/src/components/MainNavigation.js b/code/05-layout-wrapper-route/src/components/MainNavigation.js
new file mode 100644
index 0000000000..0180bd334a
--- /dev/null
+++ b/code/05-layout-wrapper-route/src/components/MainNavigation.js
@@ -0,0 +1,22 @@
+import { Link } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/05-layout-wrapper-route/src/components/MainNavigation.module.css b/code/05-layout-wrapper-route/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..22cf3202b8
--- /dev/null
+++ b/code/05-layout-wrapper-route/src/components/MainNavigation.module.css
@@ -0,0 +1,17 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
diff --git a/code/05-layout-wrapper-route/src/index.css b/code/05-layout-wrapper-route/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/05-layout-wrapper-route/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/05-layout-wrapper-route/src/index.js b/code/05-layout-wrapper-route/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/05-layout-wrapper-route/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/05-layout-wrapper-route/src/pages/Home.js b/code/05-layout-wrapper-route/src/pages/Home.js
new file mode 100644
index 0000000000..adc0e53b5d
--- /dev/null
+++ b/code/05-layout-wrapper-route/src/pages/Home.js
@@ -0,0 +1,14 @@
+import { Link } from 'react-router-dom';
+
+function HomePage() {
+ return (
+ <>
+ My Home Page
+
+ Go to the list of products.
+
+ >
+ );
+}
+
+export default HomePage;
diff --git a/code/05-layout-wrapper-route/src/pages/Products.js b/code/05-layout-wrapper-route/src/pages/Products.js
new file mode 100644
index 0000000000..6f2dd748d8
--- /dev/null
+++ b/code/05-layout-wrapper-route/src/pages/Products.js
@@ -0,0 +1,5 @@
+function ProductsPage() {
+ return The Products Page
;
+}
+
+export default ProductsPage;
diff --git a/code/05-layout-wrapper-route/src/pages/Root.js b/code/05-layout-wrapper-route/src/pages/Root.js
new file mode 100644
index 0000000000..2d079527b1
--- /dev/null
+++ b/code/05-layout-wrapper-route/src/pages/Root.js
@@ -0,0 +1,17 @@
+import { Outlet } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+import classes from './Root.module.css';
+
+function RootLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/05-layout-wrapper-route/src/pages/Root.module.css b/code/05-layout-wrapper-route/src/pages/Root.module.css
new file mode 100644
index 0000000000..bbf187d826
--- /dev/null
+++ b/code/05-layout-wrapper-route/src/pages/Root.module.css
@@ -0,0 +1,4 @@
+.content {
+ margin: 2rem auto;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/06-error-route-404/package.json b/code/06-error-route-404/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/06-error-route-404/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-error-route-404/public/favicon.ico b/code/06-error-route-404/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/06-error-route-404/public/favicon.ico differ
diff --git a/code/06-error-route-404/public/index.html b/code/06-error-route-404/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/06-error-route-404/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/06-error-route-404/public/logo192.png b/code/06-error-route-404/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/06-error-route-404/public/logo192.png differ
diff --git a/code/06-error-route-404/public/logo512.png b/code/06-error-route-404/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/06-error-route-404/public/logo512.png differ
diff --git a/code/06-error-route-404/public/manifest.json b/code/06-error-route-404/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/06-error-route-404/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-error-route-404/public/robots.txt b/code/06-error-route-404/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/06-error-route-404/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/06-error-route-404/src/App.js b/code/06-error-route-404/src/App.js
new file mode 100644
index 0000000000..2eabec06d9
--- /dev/null
+++ b/code/06-error-route-404/src/App.js
@@ -0,0 +1,38 @@
+import {
+ createBrowserRouter,
+ // createRoutesFromElements,
+ RouterProvider,
+ // Route,
+} from 'react-router-dom';
+
+import ErrorPage from './pages/Error';
+import HomePage from './pages/Home';
+import ProductsPage from './pages/Products';
+import RootLayout from './pages/Root';
+
+// const routeDefinitions = createRoutesFromElements(
+//
+// } />
+// } />
+//
+// );
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { path: '/', element: },
+ { path: '/products', element: },
+ ],
+ }
+]);
+
+// const router = createBrowserRouter(routeDefinitions);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/06-error-route-404/src/components/MainNavigation.js b/code/06-error-route-404/src/components/MainNavigation.js
new file mode 100644
index 0000000000..0180bd334a
--- /dev/null
+++ b/code/06-error-route-404/src/components/MainNavigation.js
@@ -0,0 +1,22 @@
+import { Link } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/06-error-route-404/src/components/MainNavigation.module.css b/code/06-error-route-404/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..22cf3202b8
--- /dev/null
+++ b/code/06-error-route-404/src/components/MainNavigation.module.css
@@ -0,0 +1,17 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
diff --git a/code/06-error-route-404/src/index.css b/code/06-error-route-404/src/index.css
new file mode 100644
index 0000000000..8f17b6184c
--- /dev/null
+++ b/code/06-error-route-404/src/index.css
@@ -0,0 +1,55 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+main {
+ margin: 2rem auto;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/06-error-route-404/src/index.js b/code/06-error-route-404/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/06-error-route-404/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/06-error-route-404/src/pages/Error.js b/code/06-error-route-404/src/pages/Error.js
new file mode 100644
index 0000000000..d36a0318eb
--- /dev/null
+++ b/code/06-error-route-404/src/pages/Error.js
@@ -0,0 +1,15 @@
+import MainNavigation from '../components/MainNavigation';
+
+function ErrorPage() {
+ return (
+ <>
+
+
+ An error occurred!
+ Could not find this page!
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/06-error-route-404/src/pages/Home.js b/code/06-error-route-404/src/pages/Home.js
new file mode 100644
index 0000000000..adc0e53b5d
--- /dev/null
+++ b/code/06-error-route-404/src/pages/Home.js
@@ -0,0 +1,14 @@
+import { Link } from 'react-router-dom';
+
+function HomePage() {
+ return (
+ <>
+ My Home Page
+
+ Go to the list of products.
+
+ >
+ );
+}
+
+export default HomePage;
diff --git a/code/06-error-route-404/src/pages/Products.js b/code/06-error-route-404/src/pages/Products.js
new file mode 100644
index 0000000000..6f2dd748d8
--- /dev/null
+++ b/code/06-error-route-404/src/pages/Products.js
@@ -0,0 +1,5 @@
+function ProductsPage() {
+ return The Products Page
;
+}
+
+export default ProductsPage;
diff --git a/code/06-error-route-404/src/pages/Root.js b/code/06-error-route-404/src/pages/Root.js
new file mode 100644
index 0000000000..db6babb23a
--- /dev/null
+++ b/code/06-error-route-404/src/pages/Root.js
@@ -0,0 +1,16 @@
+import { Outlet } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/07-using-navlinks/package.json b/code/07-using-navlinks/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/07-using-navlinks/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-using-navlinks/public/favicon.ico b/code/07-using-navlinks/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/07-using-navlinks/public/favicon.ico differ
diff --git a/code/07-using-navlinks/public/index.html b/code/07-using-navlinks/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/07-using-navlinks/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/07-using-navlinks/public/logo192.png b/code/07-using-navlinks/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/07-using-navlinks/public/logo192.png differ
diff --git a/code/07-using-navlinks/public/logo512.png b/code/07-using-navlinks/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/07-using-navlinks/public/logo512.png differ
diff --git a/code/07-using-navlinks/public/manifest.json b/code/07-using-navlinks/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/07-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/07-using-navlinks/public/robots.txt b/code/07-using-navlinks/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/07-using-navlinks/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/07-using-navlinks/src/App.js b/code/07-using-navlinks/src/App.js
new file mode 100644
index 0000000000..2eabec06d9
--- /dev/null
+++ b/code/07-using-navlinks/src/App.js
@@ -0,0 +1,38 @@
+import {
+ createBrowserRouter,
+ // createRoutesFromElements,
+ RouterProvider,
+ // Route,
+} from 'react-router-dom';
+
+import ErrorPage from './pages/Error';
+import HomePage from './pages/Home';
+import ProductsPage from './pages/Products';
+import RootLayout from './pages/Root';
+
+// const routeDefinitions = createRoutesFromElements(
+//
+// } />
+// } />
+//
+// );
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { path: '/', element: },
+ { path: '/products', element: },
+ ],
+ }
+]);
+
+// const router = createBrowserRouter(routeDefinitions);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/07-using-navlinks/src/components/MainNavigation.js b/code/07-using-navlinks/src/components/MainNavigation.js
new file mode 100644
index 0000000000..4eb6ac404a
--- /dev/null
+++ b/code/07-using-navlinks/src/components/MainNavigation.js
@@ -0,0 +1,40 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/07-using-navlinks/src/components/MainNavigation.module.css b/code/07-using-navlinks/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..cb0d5689ea
--- /dev/null
+++ b/code/07-using-navlinks/src/components/MainNavigation.module.css
@@ -0,0 +1,23 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/code/07-using-navlinks/src/index.css b/code/07-using-navlinks/src/index.css
new file mode 100644
index 0000000000..8f17b6184c
--- /dev/null
+++ b/code/07-using-navlinks/src/index.css
@@ -0,0 +1,55 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+main {
+ margin: 2rem auto;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/07-using-navlinks/src/index.js b/code/07-using-navlinks/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/07-using-navlinks/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/07-using-navlinks/src/pages/Error.js b/code/07-using-navlinks/src/pages/Error.js
new file mode 100644
index 0000000000..d36a0318eb
--- /dev/null
+++ b/code/07-using-navlinks/src/pages/Error.js
@@ -0,0 +1,15 @@
+import MainNavigation from '../components/MainNavigation';
+
+function ErrorPage() {
+ return (
+ <>
+
+
+ An error occurred!
+ Could not find this page!
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/07-using-navlinks/src/pages/Home.js b/code/07-using-navlinks/src/pages/Home.js
new file mode 100644
index 0000000000..adc0e53b5d
--- /dev/null
+++ b/code/07-using-navlinks/src/pages/Home.js
@@ -0,0 +1,14 @@
+import { Link } from 'react-router-dom';
+
+function HomePage() {
+ return (
+ <>
+ My Home Page
+
+ Go to the list of products.
+
+ >
+ );
+}
+
+export default HomePage;
diff --git a/code/07-using-navlinks/src/pages/Products.js b/code/07-using-navlinks/src/pages/Products.js
new file mode 100644
index 0000000000..6f2dd748d8
--- /dev/null
+++ b/code/07-using-navlinks/src/pages/Products.js
@@ -0,0 +1,5 @@
+function ProductsPage() {
+ return The Products Page
;
+}
+
+export default ProductsPage;
diff --git a/code/07-using-navlinks/src/pages/Root.js b/code/07-using-navlinks/src/pages/Root.js
new file mode 100644
index 0000000000..db6babb23a
--- /dev/null
+++ b/code/07-using-navlinks/src/pages/Root.js
@@ -0,0 +1,16 @@
+import { Outlet } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/08-navigating-programmatically/package.json b/code/08-navigating-programmatically/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/08-navigating-programmatically/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-navigating-programmatically/public/favicon.ico b/code/08-navigating-programmatically/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/08-navigating-programmatically/public/favicon.ico differ
diff --git a/code/08-navigating-programmatically/public/index.html b/code/08-navigating-programmatically/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/08-navigating-programmatically/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/08-navigating-programmatically/public/logo192.png b/code/08-navigating-programmatically/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/08-navigating-programmatically/public/logo192.png differ
diff --git a/code/08-navigating-programmatically/public/logo512.png b/code/08-navigating-programmatically/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/08-navigating-programmatically/public/logo512.png differ
diff --git a/code/08-navigating-programmatically/public/manifest.json b/code/08-navigating-programmatically/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/08-navigating-programmatically/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-navigating-programmatically/public/robots.txt b/code/08-navigating-programmatically/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/08-navigating-programmatically/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/08-navigating-programmatically/src/App.js b/code/08-navigating-programmatically/src/App.js
new file mode 100644
index 0000000000..2eabec06d9
--- /dev/null
+++ b/code/08-navigating-programmatically/src/App.js
@@ -0,0 +1,38 @@
+import {
+ createBrowserRouter,
+ // createRoutesFromElements,
+ RouterProvider,
+ // Route,
+} from 'react-router-dom';
+
+import ErrorPage from './pages/Error';
+import HomePage from './pages/Home';
+import ProductsPage from './pages/Products';
+import RootLayout from './pages/Root';
+
+// const routeDefinitions = createRoutesFromElements(
+//
+// } />
+// } />
+//
+// );
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { path: '/', element: },
+ { path: '/products', element: },
+ ],
+ }
+]);
+
+// const router = createBrowserRouter(routeDefinitions);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/08-navigating-programmatically/src/components/MainNavigation.js b/code/08-navigating-programmatically/src/components/MainNavigation.js
new file mode 100644
index 0000000000..4eb6ac404a
--- /dev/null
+++ b/code/08-navigating-programmatically/src/components/MainNavigation.js
@@ -0,0 +1,40 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/08-navigating-programmatically/src/components/MainNavigation.module.css b/code/08-navigating-programmatically/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..cb0d5689ea
--- /dev/null
+++ b/code/08-navigating-programmatically/src/components/MainNavigation.module.css
@@ -0,0 +1,23 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/code/08-navigating-programmatically/src/index.css b/code/08-navigating-programmatically/src/index.css
new file mode 100644
index 0000000000..8f17b6184c
--- /dev/null
+++ b/code/08-navigating-programmatically/src/index.css
@@ -0,0 +1,55 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+main {
+ margin: 2rem auto;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/08-navigating-programmatically/src/index.js b/code/08-navigating-programmatically/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/08-navigating-programmatically/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/08-navigating-programmatically/src/pages/Error.js b/code/08-navigating-programmatically/src/pages/Error.js
new file mode 100644
index 0000000000..d36a0318eb
--- /dev/null
+++ b/code/08-navigating-programmatically/src/pages/Error.js
@@ -0,0 +1,15 @@
+import MainNavigation from '../components/MainNavigation';
+
+function ErrorPage() {
+ return (
+ <>
+
+
+ An error occurred!
+ Could not find this page!
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/08-navigating-programmatically/src/pages/Home.js b/code/08-navigating-programmatically/src/pages/Home.js
new file mode 100644
index 0000000000..e4a6a4e858
--- /dev/null
+++ b/code/08-navigating-programmatically/src/pages/Home.js
@@ -0,0 +1,23 @@
+import { Link, useNavigate } from 'react-router-dom';
+
+function HomePage() {
+ const navigate = useNavigate();
+
+ function navigateHandler() {
+ navigate('/products');
+ }
+
+ return (
+ <>
+ My Home Page
+
+ Go to the list of products.
+
+
+
+
+ >
+ );
+}
+
+export default HomePage;
diff --git a/code/08-navigating-programmatically/src/pages/Products.js b/code/08-navigating-programmatically/src/pages/Products.js
new file mode 100644
index 0000000000..6f2dd748d8
--- /dev/null
+++ b/code/08-navigating-programmatically/src/pages/Products.js
@@ -0,0 +1,5 @@
+function ProductsPage() {
+ return The Products Page
;
+}
+
+export default ProductsPage;
diff --git a/code/08-navigating-programmatically/src/pages/Root.js b/code/08-navigating-programmatically/src/pages/Root.js
new file mode 100644
index 0000000000..db6babb23a
--- /dev/null
+++ b/code/08-navigating-programmatically/src/pages/Root.js
@@ -0,0 +1,16 @@
+import { Outlet } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/09-dynamic-routes/package.json b/code/09-dynamic-routes/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/09-dynamic-routes/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-dynamic-routes/public/favicon.ico b/code/09-dynamic-routes/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/09-dynamic-routes/public/favicon.ico differ
diff --git a/code/09-dynamic-routes/public/index.html b/code/09-dynamic-routes/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/09-dynamic-routes/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/09-dynamic-routes/public/logo192.png b/code/09-dynamic-routes/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/09-dynamic-routes/public/logo192.png differ
diff --git a/code/09-dynamic-routes/public/logo512.png b/code/09-dynamic-routes/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/09-dynamic-routes/public/logo512.png differ
diff --git a/code/09-dynamic-routes/public/manifest.json b/code/09-dynamic-routes/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/09-dynamic-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/09-dynamic-routes/public/robots.txt b/code/09-dynamic-routes/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/09-dynamic-routes/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/09-dynamic-routes/src/App.js b/code/09-dynamic-routes/src/App.js
new file mode 100644
index 0000000000..96b76ceae8
--- /dev/null
+++ b/code/09-dynamic-routes/src/App.js
@@ -0,0 +1,40 @@
+import {
+ createBrowserRouter,
+ // createRoutesFromElements,
+ RouterProvider,
+ // Route,
+} from 'react-router-dom';
+
+import ErrorPage from './pages/Error';
+import HomePage from './pages/Home';
+import ProductDetailPage from './pages/ProductDetail';
+import ProductsPage from './pages/Products';
+import RootLayout from './pages/Root';
+
+// const routeDefinitions = createRoutesFromElements(
+//
+// } />
+// } />
+//
+// );
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { path: '/', element: },
+ { path: '/products', element: },
+ { path: '/products/:productId', element: }
+ ],
+ }
+]);
+
+// const router = createBrowserRouter(routeDefinitions);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/09-dynamic-routes/src/components/MainNavigation.js b/code/09-dynamic-routes/src/components/MainNavigation.js
new file mode 100644
index 0000000000..4eb6ac404a
--- /dev/null
+++ b/code/09-dynamic-routes/src/components/MainNavigation.js
@@ -0,0 +1,40 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/09-dynamic-routes/src/components/MainNavigation.module.css b/code/09-dynamic-routes/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..cb0d5689ea
--- /dev/null
+++ b/code/09-dynamic-routes/src/components/MainNavigation.module.css
@@ -0,0 +1,23 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/code/09-dynamic-routes/src/index.css b/code/09-dynamic-routes/src/index.css
new file mode 100644
index 0000000000..8f17b6184c
--- /dev/null
+++ b/code/09-dynamic-routes/src/index.css
@@ -0,0 +1,55 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+main {
+ margin: 2rem auto;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/09-dynamic-routes/src/index.js b/code/09-dynamic-routes/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/09-dynamic-routes/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/09-dynamic-routes/src/pages/Error.js b/code/09-dynamic-routes/src/pages/Error.js
new file mode 100644
index 0000000000..d36a0318eb
--- /dev/null
+++ b/code/09-dynamic-routes/src/pages/Error.js
@@ -0,0 +1,15 @@
+import MainNavigation from '../components/MainNavigation';
+
+function ErrorPage() {
+ return (
+ <>
+
+
+ An error occurred!
+ Could not find this page!
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/09-dynamic-routes/src/pages/Home.js b/code/09-dynamic-routes/src/pages/Home.js
new file mode 100644
index 0000000000..e4a6a4e858
--- /dev/null
+++ b/code/09-dynamic-routes/src/pages/Home.js
@@ -0,0 +1,23 @@
+import { Link, useNavigate } from 'react-router-dom';
+
+function HomePage() {
+ const navigate = useNavigate();
+
+ function navigateHandler() {
+ navigate('/products');
+ }
+
+ return (
+ <>
+ My Home Page
+
+ Go to the list of products.
+
+
+
+
+ >
+ );
+}
+
+export default HomePage;
diff --git a/code/09-dynamic-routes/src/pages/ProductDetail.js b/code/09-dynamic-routes/src/pages/ProductDetail.js
new file mode 100644
index 0000000000..5d160ce347
--- /dev/null
+++ b/code/09-dynamic-routes/src/pages/ProductDetail.js
@@ -0,0 +1,14 @@
+import { useParams } from 'react-router-dom';
+
+function ProductDetailPage() {
+ const params = useParams();
+
+ return (
+ <>
+ Product Details!
+ {params.productId}
+ >
+ );
+}
+
+export default ProductDetailPage;
diff --git a/code/09-dynamic-routes/src/pages/Products.js b/code/09-dynamic-routes/src/pages/Products.js
new file mode 100644
index 0000000000..d85f2a850c
--- /dev/null
+++ b/code/09-dynamic-routes/src/pages/Products.js
@@ -0,0 +1,24 @@
+import { Link } from 'react-router-dom';
+
+const PRODUCTS = [
+ { id: 'p1', title: 'Product 1' },
+ { id: 'p2', title: 'Product 2' },
+ { id: 'p3', title: 'Product 3' },
+];
+
+function ProductsPage() {
+ return (
+ <>
+ The Products Page
+
+ {PRODUCTS.map((prod) => (
+ -
+ {prod.title}
+
+ ))}
+
+ >
+ );
+}
+
+export default ProductsPage;
diff --git a/code/09-dynamic-routes/src/pages/Root.js b/code/09-dynamic-routes/src/pages/Root.js
new file mode 100644
index 0000000000..db6babb23a
--- /dev/null
+++ b/code/09-dynamic-routes/src/pages/Root.js
@@ -0,0 +1,16 @@
+import { Outlet } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/10-absolute-relative-paths/package.json b/code/10-absolute-relative-paths/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/10-absolute-relative-paths/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-absolute-relative-paths/public/favicon.ico b/code/10-absolute-relative-paths/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/10-absolute-relative-paths/public/favicon.ico differ
diff --git a/code/10-absolute-relative-paths/public/index.html b/code/10-absolute-relative-paths/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/10-absolute-relative-paths/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/10-absolute-relative-paths/public/logo192.png b/code/10-absolute-relative-paths/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/10-absolute-relative-paths/public/logo192.png differ
diff --git a/code/10-absolute-relative-paths/public/logo512.png b/code/10-absolute-relative-paths/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/10-absolute-relative-paths/public/logo512.png differ
diff --git a/code/10-absolute-relative-paths/public/manifest.json b/code/10-absolute-relative-paths/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/10-absolute-relative-paths/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-absolute-relative-paths/public/robots.txt b/code/10-absolute-relative-paths/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/10-absolute-relative-paths/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/10-absolute-relative-paths/src/App.js b/code/10-absolute-relative-paths/src/App.js
new file mode 100644
index 0000000000..6d07cf5198
--- /dev/null
+++ b/code/10-absolute-relative-paths/src/App.js
@@ -0,0 +1,40 @@
+import {
+ createBrowserRouter,
+ // createRoutesFromElements,
+ RouterProvider,
+ // Route,
+} from 'react-router-dom';
+
+import ErrorPage from './pages/Error';
+import HomePage from './pages/Home';
+import ProductDetailPage from './pages/ProductDetail';
+import ProductsPage from './pages/Products';
+import RootLayout from './pages/Root';
+
+// const routeDefinitions = createRoutesFromElements(
+//
+// } />
+// } />
+//
+// );
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { path: '', element: },
+ { path: 'products', element: },
+ { path: 'products/:productId', element: }
+ ],
+ }
+]);
+
+// const router = createBrowserRouter(routeDefinitions);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/10-absolute-relative-paths/src/components/MainNavigation.js b/code/10-absolute-relative-paths/src/components/MainNavigation.js
new file mode 100644
index 0000000000..4eb6ac404a
--- /dev/null
+++ b/code/10-absolute-relative-paths/src/components/MainNavigation.js
@@ -0,0 +1,40 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/10-absolute-relative-paths/src/components/MainNavigation.module.css b/code/10-absolute-relative-paths/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..cb0d5689ea
--- /dev/null
+++ b/code/10-absolute-relative-paths/src/components/MainNavigation.module.css
@@ -0,0 +1,23 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/code/10-absolute-relative-paths/src/index.css b/code/10-absolute-relative-paths/src/index.css
new file mode 100644
index 0000000000..8f17b6184c
--- /dev/null
+++ b/code/10-absolute-relative-paths/src/index.css
@@ -0,0 +1,55 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+main {
+ margin: 2rem auto;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/10-absolute-relative-paths/src/index.js b/code/10-absolute-relative-paths/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/10-absolute-relative-paths/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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-absolute-relative-paths/src/pages/Error.js b/code/10-absolute-relative-paths/src/pages/Error.js
new file mode 100644
index 0000000000..d36a0318eb
--- /dev/null
+++ b/code/10-absolute-relative-paths/src/pages/Error.js
@@ -0,0 +1,15 @@
+import MainNavigation from '../components/MainNavigation';
+
+function ErrorPage() {
+ return (
+ <>
+
+
+ An error occurred!
+ Could not find this page!
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/10-absolute-relative-paths/src/pages/Home.js b/code/10-absolute-relative-paths/src/pages/Home.js
new file mode 100644
index 0000000000..7190e222a3
--- /dev/null
+++ b/code/10-absolute-relative-paths/src/pages/Home.js
@@ -0,0 +1,23 @@
+import { Link, useNavigate } from 'react-router-dom';
+
+function HomePage() {
+ const navigate = useNavigate();
+
+ function navigateHandler() {
+ navigate('/products');
+ }
+
+ return (
+ <>
+ My Home Page
+
+ Go to the list of products.
+
+
+
+
+ >
+ );
+}
+
+export default HomePage;
diff --git a/code/10-absolute-relative-paths/src/pages/ProductDetail.js b/code/10-absolute-relative-paths/src/pages/ProductDetail.js
new file mode 100644
index 0000000000..0f8c86b516
--- /dev/null
+++ b/code/10-absolute-relative-paths/src/pages/ProductDetail.js
@@ -0,0 +1,15 @@
+import { useParams, Link } from 'react-router-dom';
+
+function ProductDetailPage() {
+ const params = useParams();
+
+ return (
+ <>
+ Product Details!
+ {params.productId}
+ Back
+ >
+ );
+}
+
+export default ProductDetailPage;
diff --git a/code/10-absolute-relative-paths/src/pages/Products.js b/code/10-absolute-relative-paths/src/pages/Products.js
new file mode 100644
index 0000000000..d15679e5ab
--- /dev/null
+++ b/code/10-absolute-relative-paths/src/pages/Products.js
@@ -0,0 +1,24 @@
+import { Link } from 'react-router-dom';
+
+const PRODUCTS = [
+ { id: 'p1', title: 'Product 1' },
+ { id: 'p2', title: 'Product 2' },
+ { id: 'p3', title: 'Product 3' },
+];
+
+function ProductsPage() {
+ return (
+ <>
+ The Products Page
+
+ {PRODUCTS.map((prod) => (
+ -
+ {prod.title}
+
+ ))}
+
+ >
+ );
+}
+
+export default ProductsPage;
diff --git a/code/10-absolute-relative-paths/src/pages/Root.js b/code/10-absolute-relative-paths/src/pages/Root.js
new file mode 100644
index 0000000000..db6babb23a
--- /dev/null
+++ b/code/10-absolute-relative-paths/src/pages/Root.js
@@ -0,0 +1,16 @@
+import { Outlet } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/11-index-routes/package.json b/code/11-index-routes/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/11-index-routes/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-index-routes/public/favicon.ico b/code/11-index-routes/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/11-index-routes/public/favicon.ico differ
diff --git a/code/11-index-routes/public/index.html b/code/11-index-routes/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/11-index-routes/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/11-index-routes/public/logo192.png b/code/11-index-routes/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/11-index-routes/public/logo192.png differ
diff --git a/code/11-index-routes/public/logo512.png b/code/11-index-routes/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/11-index-routes/public/logo512.png differ
diff --git a/code/11-index-routes/public/manifest.json b/code/11-index-routes/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/11-index-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-index-routes/public/robots.txt b/code/11-index-routes/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/11-index-routes/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/11-index-routes/src/App.js b/code/11-index-routes/src/App.js
new file mode 100644
index 0000000000..dd3673b5f8
--- /dev/null
+++ b/code/11-index-routes/src/App.js
@@ -0,0 +1,40 @@
+import {
+ createBrowserRouter,
+ // createRoutesFromElements,
+ RouterProvider,
+ // Route,
+} from 'react-router-dom';
+
+import ErrorPage from './pages/Error';
+import HomePage from './pages/Home';
+import ProductDetailPage from './pages/ProductDetail';
+import ProductsPage from './pages/Products';
+import RootLayout from './pages/Root';
+
+// const routeDefinitions = createRoutesFromElements(
+//
+// } />
+// } />
+//
+// );
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ { path: 'products', element: },
+ { path: 'products/:productId', element: }
+ ],
+ }
+]);
+
+// const router = createBrowserRouter(routeDefinitions);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/11-index-routes/src/components/MainNavigation.js b/code/11-index-routes/src/components/MainNavigation.js
new file mode 100644
index 0000000000..4eb6ac404a
--- /dev/null
+++ b/code/11-index-routes/src/components/MainNavigation.js
@@ -0,0 +1,40 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/11-index-routes/src/components/MainNavigation.module.css b/code/11-index-routes/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..cb0d5689ea
--- /dev/null
+++ b/code/11-index-routes/src/components/MainNavigation.module.css
@@ -0,0 +1,23 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/code/11-index-routes/src/index.css b/code/11-index-routes/src/index.css
new file mode 100644
index 0000000000..8f17b6184c
--- /dev/null
+++ b/code/11-index-routes/src/index.css
@@ -0,0 +1,55 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+main {
+ margin: 2rem auto;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/11-index-routes/src/index.js b/code/11-index-routes/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/11-index-routes/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/11-index-routes/src/pages/Error.js b/code/11-index-routes/src/pages/Error.js
new file mode 100644
index 0000000000..d36a0318eb
--- /dev/null
+++ b/code/11-index-routes/src/pages/Error.js
@@ -0,0 +1,15 @@
+import MainNavigation from '../components/MainNavigation';
+
+function ErrorPage() {
+ return (
+ <>
+
+
+ An error occurred!
+ Could not find this page!
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/11-index-routes/src/pages/Home.js b/code/11-index-routes/src/pages/Home.js
new file mode 100644
index 0000000000..7190e222a3
--- /dev/null
+++ b/code/11-index-routes/src/pages/Home.js
@@ -0,0 +1,23 @@
+import { Link, useNavigate } from 'react-router-dom';
+
+function HomePage() {
+ const navigate = useNavigate();
+
+ function navigateHandler() {
+ navigate('/products');
+ }
+
+ return (
+ <>
+ My Home Page
+
+ Go to the list of products.
+
+
+
+
+ >
+ );
+}
+
+export default HomePage;
diff --git a/code/11-index-routes/src/pages/ProductDetail.js b/code/11-index-routes/src/pages/ProductDetail.js
new file mode 100644
index 0000000000..0f8c86b516
--- /dev/null
+++ b/code/11-index-routes/src/pages/ProductDetail.js
@@ -0,0 +1,15 @@
+import { useParams, Link } from 'react-router-dom';
+
+function ProductDetailPage() {
+ const params = useParams();
+
+ return (
+ <>
+ Product Details!
+ {params.productId}
+ Back
+ >
+ );
+}
+
+export default ProductDetailPage;
diff --git a/code/11-index-routes/src/pages/Products.js b/code/11-index-routes/src/pages/Products.js
new file mode 100644
index 0000000000..d15679e5ab
--- /dev/null
+++ b/code/11-index-routes/src/pages/Products.js
@@ -0,0 +1,24 @@
+import { Link } from 'react-router-dom';
+
+const PRODUCTS = [
+ { id: 'p1', title: 'Product 1' },
+ { id: 'p2', title: 'Product 2' },
+ { id: 'p3', title: 'Product 3' },
+];
+
+function ProductsPage() {
+ return (
+ <>
+ The Products Page
+
+ {PRODUCTS.map((prod) => (
+ -
+ {prod.title}
+
+ ))}
+
+ >
+ );
+}
+
+export default ProductsPage;
diff --git a/code/11-index-routes/src/pages/Root.js b/code/11-index-routes/src/pages/Root.js
new file mode 100644
index 0000000000..db6babb23a
--- /dev/null
+++ b/code/11-index-routes/src/pages/Root.js
@@ -0,0 +1,16 @@
+import { Outlet } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/12-adv-starting-project/backend/app.js b/code/12-adv-starting-project/backend/app.js
new file mode 100644
index 0000000000..be62c0f3cd
--- /dev/null
+++ b/code/12-adv-starting-project/backend/app.js
@@ -0,0 +1,24 @@
+const bodyParser = require('body-parser');
+const express = require('express');
+
+const eventRoutes = require('./routes/events');
+
+const app = express();
+
+app.use(bodyParser.json());
+app.use((req, res, next) => {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PATCH,DELETE');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
+ next();
+});
+
+app.use('/events', eventRoutes);
+
+app.use((error, req, res, next) => {
+ const status = error.status || 500;
+ const message = error.message || 'Something went wrong.';
+ res.status(status).json({ message: message });
+});
+
+app.listen(8080);
diff --git a/code/12-adv-starting-project/backend/data/event.js b/code/12-adv-starting-project/backend/data/event.js
new file mode 100644
index 0000000000..3a5db5ab92
--- /dev/null
+++ b/code/12-adv-starting-project/backend/data/event.js
@@ -0,0 +1,70 @@
+const fs = require('node:fs/promises');
+
+const { v4: generateId } = require('uuid');
+
+const { NotFoundError } = require('../util/errors');
+
+async function readData() {
+ const data = await fs.readFile('events.json', 'utf8');
+ return JSON.parse(data);
+}
+
+async function writeData(data) {
+ await fs.writeFile('events.json', JSON.stringify(data));
+}
+
+async function getAll() {
+ const storedData = await readData();
+ if (!storedData.events) {
+ throw new NotFoundError('Could not find any events.');
+ }
+ return storedData.events;
+}
+
+async function get(id) {
+ const storedData = await readData();
+ if (!storedData.events || storedData.events.length === 0) {
+ throw new NotFoundError('Could not find any events.');
+ }
+
+ const event = storedData.events.find((ev) => ev.id === id);
+ if (!event) {
+ throw new NotFoundError('Could not find event for id ' + id);
+ }
+
+ return event;
+}
+
+async function add(data) {
+ const storedData = await readData();
+ storedData.events.unshift({ ...data, id: generateId() });
+ await writeData(storedData);
+}
+
+async function replace(id, data) {
+ const storedData = await readData();
+ if (!storedData.events || storedData.events.length === 0) {
+ throw new NotFoundError('Could not find any events.');
+ }
+
+ const index = storedData.events.findIndex((ev) => ev.id === id);
+ if (index < 0) {
+ throw new NotFoundError('Could not find event for id ' + id);
+ }
+
+ storedData.events[index] = { ...data, id };
+
+ await writeData(storedData);
+}
+
+async function remove(id) {
+ const storedData = await readData();
+ const updatedData = storedData.events.filter((ev) => ev.id !== id);
+ await writeData({events: updatedData});
+}
+
+exports.getAll = getAll;
+exports.get = get;
+exports.add = add;
+exports.replace = replace;
+exports.remove = remove;
diff --git a/code/12-adv-starting-project/backend/events.json b/code/12-adv-starting-project/backend/events.json
new file mode 100644
index 0000000000..3f458bc3c2
--- /dev/null
+++ b/code/12-adv-starting-project/backend/events.json
@@ -0,0 +1,11 @@
+{
+ "events": [
+ {
+ "id": "e1",
+ "title": "A dummy event",
+ "date": "2023-02-22",
+ "image": "https://blog.hubspot.de/hubfs/Germany/Blog_images/Optimize_Marketing%20Events%20DACH%202021.jpg",
+ "description": "Join this amazing event and connect with fellow developers."
+ }
+ ]
+}
diff --git a/code/12-adv-starting-project/backend/package.json b/code/12-adv-starting-project/backend/package.json
new file mode 100644
index 0000000000..b46c8d32d8
--- /dev/null
+++ b/code/12-adv-starting-project/backend/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "backend-api",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "start": "node app.js"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "body-parser": "^1.20.0",
+ "express": "^4.18.1",
+ "uuid": "^9.0.0"
+ }
+}
diff --git a/code/12-adv-starting-project/backend/routes/events.js b/code/12-adv-starting-project/backend/routes/events.js
new file mode 100644
index 0000000000..d8dbf375fc
--- /dev/null
+++ b/code/12-adv-starting-project/backend/routes/events.js
@@ -0,0 +1,111 @@
+const express = require('express');
+
+const { getAll, get, add, replace, remove } = require('../data/event');
+const {
+ isValidText,
+ isValidDate,
+ isValidImageUrl,
+} = require('../util/validation');
+
+const router = express.Router();
+
+router.get('/', async (req, res, next) => {
+ try {
+ const events = await getAll();
+ res.json({ events: events });
+ } catch (error) {
+ next(error);
+ }
+});
+
+router.get('/:id', async (req, res, next) => {
+ try {
+ const event = await get(req.params.id);
+ res.json({ event: event });
+ } catch (error) {
+ next(error);
+ }
+});
+
+router.post('/', async (req, res, next) => {
+ const data = req.body;
+
+ let errors = {};
+
+ if (!isValidText(data.title)) {
+ errors.title = 'Invalid title.';
+ }
+
+ if (!isValidText(data.description)) {
+ errors.description = 'Invalid description.';
+ }
+
+ if (!isValidDate(data.date)) {
+ errors.date = 'Invalid date.';
+ }
+
+ if (!isValidImageUrl(data.image)) {
+ errors.image = 'Invalid image.';
+ }
+
+ if (Object.keys(errors).length > 0) {
+ return res.status(422).json({
+ message: 'Adding the event failed due to validation errors.',
+ errors,
+ });
+ }
+
+ try {
+ await add(data);
+ res.status(201).json({ message: 'Event saved.', event: data });
+ } catch (error) {
+ next(error);
+ }
+});
+
+router.patch('/:id', async (req, res, next) => {
+ const data = req.body;
+
+ let errors = {};
+
+ if (!isValidText(data.title)) {
+ errors.title = 'Invalid title.';
+ }
+
+ if (!isValidText(data.description)) {
+ errors.description = 'Invalid description.';
+ }
+
+ if (!isValidDate(data.date)) {
+ errors.date = 'Invalid date.';
+ }
+
+ if (!isValidImageUrl(data.image)) {
+ errors.image = 'Invalid image.';
+ }
+
+ if (Object.keys(errors).length > 0) {
+ return res.status(422).json({
+ message: 'Updating the event failed due to validation errors.',
+ errors,
+ });
+ }
+
+ try {
+ await replace(req.params.id, data);
+ res.json({ message: 'Event updated.', event: data });
+ } catch (error) {
+ next(error);
+ }
+});
+
+router.delete('/:id', async (req, res, next) => {
+ try {
+ await remove(req.params.id);
+ res.json({ message: 'Event deleted.' });
+ } catch (error) {
+ next(error);
+ }
+});
+
+module.exports = router;
diff --git a/code/12-adv-starting-project/backend/util/errors.js b/code/12-adv-starting-project/backend/util/errors.js
new file mode 100644
index 0000000000..db9cf9d805
--- /dev/null
+++ b/code/12-adv-starting-project/backend/util/errors.js
@@ -0,0 +1,8 @@
+class NotFoundError {
+ constructor(message) {
+ this.message = message;
+ this.status = 404;
+ }
+}
+
+exports.NotFoundError = NotFoundError;
\ No newline at end of file
diff --git a/code/12-adv-starting-project/backend/util/validation.js b/code/12-adv-starting-project/backend/util/validation.js
new file mode 100644
index 0000000000..c2ddbabd9f
--- /dev/null
+++ b/code/12-adv-starting-project/backend/util/validation.js
@@ -0,0 +1,16 @@
+function isValidText(value) {
+ return value && value.trim().length > 0;
+}
+
+function isValidDate(value) {
+ const date = new Date(value);
+ return value && date !== 'Invalid Date';
+}
+
+function isValidImageUrl(value) {
+ return value && value.startsWith('http');
+}
+
+exports.isValidText = isValidText;
+exports.isValidDate = isValidDate;
+exports.isValidImageUrl = isValidImageUrl;
diff --git a/code/12-adv-starting-project/frontend/package.json b/code/12-adv-starting-project/frontend/package.json
new file mode 100644
index 0000000000..edaa0ea9e5
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-adv-starting-project/frontend/public/favicon.ico b/code/12-adv-starting-project/frontend/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/12-adv-starting-project/frontend/public/favicon.ico differ
diff --git a/code/12-adv-starting-project/frontend/public/index.html b/code/12-adv-starting-project/frontend/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/12-adv-starting-project/frontend/public/logo192.png b/code/12-adv-starting-project/frontend/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/12-adv-starting-project/frontend/public/logo192.png differ
diff --git a/code/12-adv-starting-project/frontend/public/logo512.png b/code/12-adv-starting-project/frontend/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/12-adv-starting-project/frontend/public/logo512.png differ
diff --git a/code/12-adv-starting-project/frontend/public/manifest.json b/code/12-adv-starting-project/frontend/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/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-adv-starting-project/frontend/public/robots.txt b/code/12-adv-starting-project/frontend/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/12-adv-starting-project/frontend/src/App.js b/code/12-adv-starting-project/frontend/src/App.js
new file mode 100644
index 0000000000..884fccbe64
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/App.js
@@ -0,0 +1,27 @@
+// Challenge / Exercise
+
+// 1. Add five new (dummy) page components (content can be simple elements)
+// - HomePage
+// - EventsPage
+// - EventDetailPage
+// - NewEventPage
+// - EditEventPage
+// 2. Add routing & route definitions for these five pages
+// - / => HomePage
+// - /events => EventsPage
+// - /events/ => EventDetailPage
+// - /events/new => NewEventPage
+// - /events//edit => EditEventPage
+// 3. Add a root layout that adds the component above all page components
+// 4. Add properly working links to the MainNavigation
+// 5. Ensure that the links in MainNavigation receive an "active" class when active
+// 6. Output a list of dummy events to the EventsPage
+// Every list item should include a link to the respective EventDetailPage
+// 7. Output the ID of the selected event on the EventDetailPage
+// BONUS: Add another (nested) layout route that adds the component above all /events... page components
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/12-adv-starting-project/frontend/src/components/EventForm.js b/code/12-adv-starting-project/frontend/src/components/EventForm.js
new file mode 100644
index 0000000000..f0bc84b08c
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/components/EventForm.js
@@ -0,0 +1,39 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/12-adv-starting-project/frontend/src/components/EventForm.module.css b/code/12-adv-starting-project/frontend/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/12-adv-starting-project/frontend/src/components/EventItem.js b/code/12-adv-starting-project/frontend/src/components/EventItem.js
new file mode 100644
index 0000000000..ac8de8258e
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/components/EventItem.js
@@ -0,0 +1,22 @@
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/12-adv-starting-project/frontend/src/components/EventItem.module.css b/code/12-adv-starting-project/frontend/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/12-adv-starting-project/frontend/src/components/EventsList.js b/code/12-adv-starting-project/frontend/src/components/EventsList.js
new file mode 100644
index 0000000000..b17cfaf94c
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/components/EventsList.js
@@ -0,0 +1,24 @@
+import classes from './EventsList.module.css';
+
+function EventsList({ events }) {
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/12-adv-starting-project/frontend/src/components/EventsList.module.css b/code/12-adv-starting-project/frontend/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/12-adv-starting-project/frontend/src/components/EventsNavigation.js b/code/12-adv-starting-project/frontend/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..31fba9e96e
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/components/EventsNavigation.js
@@ -0,0 +1,20 @@
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/12-adv-starting-project/frontend/src/components/EventsNavigation.module.css b/code/12-adv-starting-project/frontend/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/12-adv-starting-project/frontend/src/components/MainNavigation.js b/code/12-adv-starting-project/frontend/src/components/MainNavigation.js
new file mode 100644
index 0000000000..769386c592
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/components/MainNavigation.js
@@ -0,0 +1,20 @@
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/12-adv-starting-project/frontend/src/components/MainNavigation.module.css b/code/12-adv-starting-project/frontend/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/12-adv-starting-project/frontend/src/index.css b/code/12-adv-starting-project/frontend/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/12-adv-starting-project/frontend/src/index.js b/code/12-adv-starting-project/frontend/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/12-adv-starting-project/frontend/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/12-adv-starting-project/how-to-use.txt b/code/12-adv-starting-project/how-to-use.txt
new file mode 100644
index 0000000000..95a1e31db0
--- /dev/null
+++ b/code/12-adv-starting-project/how-to-use.txt
@@ -0,0 +1,12 @@
+This project actually contains two projects:
+- A React.js application (i.e., the frontend SPA)
+- A dummy backend API to which the React app can "talk" (to send + fetch data)
+
+You must run "npm install" in both project folders.
+
+Thereafter, you can start the dummy backend API server via "npm start" (inside the "backend-api" folder).
+The React app dev server is then also started via "npm start" (though inside the "react-frontend" folder).
+
+You MUST have both servers (backend + frontend) up and running for the projects to work.
+
+The dummy backend API does not use any external database - instead the dummy data is saved to an "events.json" file inside the project folder.
diff --git a/code/13-challenge-problem/package.json b/code/13-challenge-problem/package.json
new file mode 100644
index 0000000000..edaa0ea9e5
--- /dev/null
+++ b/code/13-challenge-problem/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-challenge-problem/public/favicon.ico b/code/13-challenge-problem/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/13-challenge-problem/public/favicon.ico differ
diff --git a/code/13-challenge-problem/public/index.html b/code/13-challenge-problem/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/13-challenge-problem/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/13-challenge-problem/public/logo192.png b/code/13-challenge-problem/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/13-challenge-problem/public/logo192.png differ
diff --git a/code/13-challenge-problem/public/logo512.png b/code/13-challenge-problem/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/13-challenge-problem/public/logo512.png differ
diff --git a/code/13-challenge-problem/public/manifest.json b/code/13-challenge-problem/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/13-challenge-problem/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-challenge-problem/public/robots.txt b/code/13-challenge-problem/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/13-challenge-problem/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/13-challenge-problem/src/App.js b/code/13-challenge-problem/src/App.js
new file mode 100644
index 0000000000..884fccbe64
--- /dev/null
+++ b/code/13-challenge-problem/src/App.js
@@ -0,0 +1,27 @@
+// Challenge / Exercise
+
+// 1. Add five new (dummy) page components (content can be simple elements)
+// - HomePage
+// - EventsPage
+// - EventDetailPage
+// - NewEventPage
+// - EditEventPage
+// 2. Add routing & route definitions for these five pages
+// - / => HomePage
+// - /events => EventsPage
+// - /events/ => EventDetailPage
+// - /events/new => NewEventPage
+// - /events//edit => EditEventPage
+// 3. Add a root layout that adds the component above all page components
+// 4. Add properly working links to the MainNavigation
+// 5. Ensure that the links in MainNavigation receive an "active" class when active
+// 6. Output a list of dummy events to the EventsPage
+// Every list item should include a link to the respective EventDetailPage
+// 7. Output the ID of the selected event on the EventDetailPage
+// BONUS: Add another (nested) layout route that adds the component above all /events... page components
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/13-challenge-problem/src/components/EventForm.js b/code/13-challenge-problem/src/components/EventForm.js
new file mode 100644
index 0000000000..f0bc84b08c
--- /dev/null
+++ b/code/13-challenge-problem/src/components/EventForm.js
@@ -0,0 +1,39 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/13-challenge-problem/src/components/EventForm.module.css b/code/13-challenge-problem/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/13-challenge-problem/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/13-challenge-problem/src/components/EventItem.js b/code/13-challenge-problem/src/components/EventItem.js
new file mode 100644
index 0000000000..ac8de8258e
--- /dev/null
+++ b/code/13-challenge-problem/src/components/EventItem.js
@@ -0,0 +1,22 @@
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/13-challenge-problem/src/components/EventItem.module.css b/code/13-challenge-problem/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/13-challenge-problem/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/13-challenge-problem/src/components/EventsList.js b/code/13-challenge-problem/src/components/EventsList.js
new file mode 100644
index 0000000000..b17cfaf94c
--- /dev/null
+++ b/code/13-challenge-problem/src/components/EventsList.js
@@ -0,0 +1,24 @@
+import classes from './EventsList.module.css';
+
+function EventsList({ events }) {
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/13-challenge-problem/src/components/EventsList.module.css b/code/13-challenge-problem/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/13-challenge-problem/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/13-challenge-problem/src/components/EventsNavigation.js b/code/13-challenge-problem/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..31fba9e96e
--- /dev/null
+++ b/code/13-challenge-problem/src/components/EventsNavigation.js
@@ -0,0 +1,20 @@
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/13-challenge-problem/src/components/EventsNavigation.module.css b/code/13-challenge-problem/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/13-challenge-problem/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/13-challenge-problem/src/components/MainNavigation.js b/code/13-challenge-problem/src/components/MainNavigation.js
new file mode 100644
index 0000000000..769386c592
--- /dev/null
+++ b/code/13-challenge-problem/src/components/MainNavigation.js
@@ -0,0 +1,20 @@
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/13-challenge-problem/src/components/MainNavigation.module.css b/code/13-challenge-problem/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/13-challenge-problem/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/13-challenge-problem/src/index.css b/code/13-challenge-problem/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/13-challenge-problem/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/13-challenge-problem/src/index.js b/code/13-challenge-problem/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/13-challenge-problem/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/14-challenge-solution/package.json b/code/14-challenge-solution/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/14-challenge-solution/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-challenge-solution/public/favicon.ico b/code/14-challenge-solution/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/14-challenge-solution/public/favicon.ico differ
diff --git a/code/14-challenge-solution/public/index.html b/code/14-challenge-solution/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/14-challenge-solution/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/14-challenge-solution/public/logo192.png b/code/14-challenge-solution/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/14-challenge-solution/public/logo192.png differ
diff --git a/code/14-challenge-solution/public/logo512.png b/code/14-challenge-solution/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/14-challenge-solution/public/logo512.png differ
diff --git a/code/14-challenge-solution/public/manifest.json b/code/14-challenge-solution/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/14-challenge-solution/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-challenge-solution/public/robots.txt b/code/14-challenge-solution/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/14-challenge-solution/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/14-challenge-solution/src/App.js b/code/14-challenge-solution/src/App.js
new file mode 100644
index 0000000000..77d6d4a8a3
--- /dev/null
+++ b/code/14-challenge-solution/src/App.js
@@ -0,0 +1,64 @@
+// Challenge / Exercise
+
+// 1. Add five new (dummy) page components (content can be simple elements)
+// - HomePage
+// - EventsPage
+// - EventDetailPage
+// - NewEventPage
+// - EditEventPage
+// DONE
+// 2. Add routing & route definitions for these five pages
+// - / => HomePage
+// - /events => EventsPage
+// - /events/ => EventDetailPage
+// - /events/new => NewEventPage
+// - /events//edit => EditEventPage
+// DONE
+// 3. Add a root layout that adds the component above all page components
+// DONE
+// 4. Add properly working links to the MainNavigation
+// DONE
+// 5. Ensure that the links in MainNavigation receive an "active" class when active
+// DONE
+// 6. Output a list of dummy events to the EventsPage
+// Every list item should include a link to the respective EventDetailPage
+// DONE
+// 7. Output the ID of the selected event on the EventDetailPage
+// DONE
+// BONUS: Add another (nested) layout route that adds the component above all /events... page components
+// DONE
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import EventDetailPage from './pages/EventDetail';
+import EventsPage from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ { index: true, element: },
+ { path: ':eventId', element: },
+ { path: 'new', element: },
+ { path: ':eventId/edit', element: },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/14-challenge-solution/src/components/EventForm.js b/code/14-challenge-solution/src/components/EventForm.js
new file mode 100644
index 0000000000..f0bc84b08c
--- /dev/null
+++ b/code/14-challenge-solution/src/components/EventForm.js
@@ -0,0 +1,39 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/14-challenge-solution/src/components/EventForm.module.css b/code/14-challenge-solution/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/14-challenge-solution/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/14-challenge-solution/src/components/EventItem.js b/code/14-challenge-solution/src/components/EventItem.js
new file mode 100644
index 0000000000..ac8de8258e
--- /dev/null
+++ b/code/14-challenge-solution/src/components/EventItem.js
@@ -0,0 +1,22 @@
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/14-challenge-solution/src/components/EventItem.module.css b/code/14-challenge-solution/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/14-challenge-solution/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/14-challenge-solution/src/components/EventsList.js b/code/14-challenge-solution/src/components/EventsList.js
new file mode 100644
index 0000000000..b17cfaf94c
--- /dev/null
+++ b/code/14-challenge-solution/src/components/EventsList.js
@@ -0,0 +1,24 @@
+import classes from './EventsList.module.css';
+
+function EventsList({ events }) {
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/14-challenge-solution/src/components/EventsList.module.css b/code/14-challenge-solution/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/14-challenge-solution/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/14-challenge-solution/src/components/EventsNavigation.js b/code/14-challenge-solution/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/14-challenge-solution/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/14-challenge-solution/src/components/EventsNavigation.module.css b/code/14-challenge-solution/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/14-challenge-solution/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/14-challenge-solution/src/components/MainNavigation.js b/code/14-challenge-solution/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/14-challenge-solution/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/14-challenge-solution/src/components/MainNavigation.module.css b/code/14-challenge-solution/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/14-challenge-solution/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/14-challenge-solution/src/index.css b/code/14-challenge-solution/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/14-challenge-solution/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/14-challenge-solution/src/index.js b/code/14-challenge-solution/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/14-challenge-solution/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/14-challenge-solution/src/pages/EditEvent.js b/code/14-challenge-solution/src/pages/EditEvent.js
new file mode 100644
index 0000000000..aa0ae97bbf
--- /dev/null
+++ b/code/14-challenge-solution/src/pages/EditEvent.js
@@ -0,0 +1,5 @@
+function EditEventPage() {
+ return EditEventPage
;
+}
+
+export default EditEventPage;
diff --git a/code/14-challenge-solution/src/pages/EventDetail.js b/code/14-challenge-solution/src/pages/EventDetail.js
new file mode 100644
index 0000000000..a3947487be
--- /dev/null
+++ b/code/14-challenge-solution/src/pages/EventDetail.js
@@ -0,0 +1,14 @@
+import { useParams } from 'react-router-dom';
+
+function EventDetailPage() {
+ const params = useParams();
+
+ return (
+ <>
+ EventDetailPage
+ Event ID: {params.eventId}
+ >
+ );
+}
+
+export default EventDetailPage;
diff --git a/code/14-challenge-solution/src/pages/Events.js b/code/14-challenge-solution/src/pages/Events.js
new file mode 100644
index 0000000000..f4b1ca508f
--- /dev/null
+++ b/code/14-challenge-solution/src/pages/Events.js
@@ -0,0 +1,29 @@
+import { Link } from 'react-router-dom';
+
+const DUMMY_EVENTS = [
+ {
+ id: 'e1',
+ title: 'Some event',
+ },
+ {
+ id: 'e2',
+ title: 'Another event',
+ },
+];
+
+function EventsPage() {
+ return (
+ <>
+ EventsPage
+
+ {DUMMY_EVENTS.map((event) => (
+ -
+ {event.title}
+
+ ))}
+
+ >
+ );
+}
+
+export default EventsPage;
diff --git a/code/14-challenge-solution/src/pages/EventsRoot.js b/code/14-challenge-solution/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/14-challenge-solution/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/14-challenge-solution/src/pages/Home.js b/code/14-challenge-solution/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/14-challenge-solution/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/14-challenge-solution/src/pages/NewEvent.js b/code/14-challenge-solution/src/pages/NewEvent.js
new file mode 100644
index 0000000000..189ca3b061
--- /dev/null
+++ b/code/14-challenge-solution/src/pages/NewEvent.js
@@ -0,0 +1,5 @@
+function NewEventPage() {
+ return NewEventPage
;
+}
+
+export default NewEventPage;
diff --git a/code/14-challenge-solution/src/pages/Root.js b/code/14-challenge-solution/src/pages/Root.js
new file mode 100644
index 0000000000..db6babb23a
--- /dev/null
+++ b/code/14-challenge-solution/src/pages/Root.js
@@ -0,0 +1,16 @@
+import { Outlet } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/15-data-fetching-example/package.json b/code/15-data-fetching-example/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/15-data-fetching-example/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-data-fetching-example/public/favicon.ico b/code/15-data-fetching-example/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/15-data-fetching-example/public/favicon.ico differ
diff --git a/code/15-data-fetching-example/public/index.html b/code/15-data-fetching-example/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/15-data-fetching-example/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/15-data-fetching-example/public/logo192.png b/code/15-data-fetching-example/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/15-data-fetching-example/public/logo192.png differ
diff --git a/code/15-data-fetching-example/public/logo512.png b/code/15-data-fetching-example/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/15-data-fetching-example/public/logo512.png differ
diff --git a/code/15-data-fetching-example/public/manifest.json b/code/15-data-fetching-example/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/15-data-fetching-example/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-data-fetching-example/public/robots.txt b/code/15-data-fetching-example/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/15-data-fetching-example/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/15-data-fetching-example/src/App.js b/code/15-data-fetching-example/src/App.js
new file mode 100644
index 0000000000..c088571b70
--- /dev/null
+++ b/code/15-data-fetching-example/src/App.js
@@ -0,0 +1,35 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import EventDetailPage from './pages/EventDetail';
+import EventsPage from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ { index: true, element: },
+ { path: ':eventId', element: },
+ { path: 'new', element: },
+ { path: ':eventId/edit', element: },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/15-data-fetching-example/src/components/EventForm.js b/code/15-data-fetching-example/src/components/EventForm.js
new file mode 100644
index 0000000000..f0bc84b08c
--- /dev/null
+++ b/code/15-data-fetching-example/src/components/EventForm.js
@@ -0,0 +1,39 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/15-data-fetching-example/src/components/EventForm.module.css b/code/15-data-fetching-example/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/15-data-fetching-example/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/15-data-fetching-example/src/components/EventItem.js b/code/15-data-fetching-example/src/components/EventItem.js
new file mode 100644
index 0000000000..ac8de8258e
--- /dev/null
+++ b/code/15-data-fetching-example/src/components/EventItem.js
@@ -0,0 +1,22 @@
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/15-data-fetching-example/src/components/EventItem.module.css b/code/15-data-fetching-example/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/15-data-fetching-example/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/15-data-fetching-example/src/components/EventsList.js b/code/15-data-fetching-example/src/components/EventsList.js
new file mode 100644
index 0000000000..b17cfaf94c
--- /dev/null
+++ b/code/15-data-fetching-example/src/components/EventsList.js
@@ -0,0 +1,24 @@
+import classes from './EventsList.module.css';
+
+function EventsList({ events }) {
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/15-data-fetching-example/src/components/EventsList.module.css b/code/15-data-fetching-example/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/15-data-fetching-example/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/15-data-fetching-example/src/components/EventsNavigation.js b/code/15-data-fetching-example/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/15-data-fetching-example/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/15-data-fetching-example/src/components/EventsNavigation.module.css b/code/15-data-fetching-example/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/15-data-fetching-example/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/15-data-fetching-example/src/components/MainNavigation.js b/code/15-data-fetching-example/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/15-data-fetching-example/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/15-data-fetching-example/src/components/MainNavigation.module.css b/code/15-data-fetching-example/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/15-data-fetching-example/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/15-data-fetching-example/src/index.css b/code/15-data-fetching-example/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/15-data-fetching-example/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/15-data-fetching-example/src/index.js b/code/15-data-fetching-example/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/15-data-fetching-example/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/15-data-fetching-example/src/pages/EditEvent.js b/code/15-data-fetching-example/src/pages/EditEvent.js
new file mode 100644
index 0000000000..aa0ae97bbf
--- /dev/null
+++ b/code/15-data-fetching-example/src/pages/EditEvent.js
@@ -0,0 +1,5 @@
+function EditEventPage() {
+ return EditEventPage
;
+}
+
+export default EditEventPage;
diff --git a/code/15-data-fetching-example/src/pages/EventDetail.js b/code/15-data-fetching-example/src/pages/EventDetail.js
new file mode 100644
index 0000000000..a3947487be
--- /dev/null
+++ b/code/15-data-fetching-example/src/pages/EventDetail.js
@@ -0,0 +1,14 @@
+import { useParams } from 'react-router-dom';
+
+function EventDetailPage() {
+ const params = useParams();
+
+ return (
+ <>
+ EventDetailPage
+ Event ID: {params.eventId}
+ >
+ );
+}
+
+export default EventDetailPage;
diff --git a/code/15-data-fetching-example/src/pages/Events.js b/code/15-data-fetching-example/src/pages/Events.js
new file mode 100644
index 0000000000..d8eaf25c05
--- /dev/null
+++ b/code/15-data-fetching-example/src/pages/Events.js
@@ -0,0 +1,37 @@
+import { useEffect, useState } from 'react';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const [isLoading, setIsLoading] = useState(false);
+ const [fetchedEvents, setFetchedEvents] = useState();
+ const [error, setError] = useState();
+
+ useEffect(() => {
+ async function fetchEvents() {
+ setIsLoading(true);
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ setError('Fetching events failed.');
+ } else {
+ const resData = await response.json();
+ setFetchedEvents(resData.events);
+ }
+ setIsLoading(false);
+ }
+
+ fetchEvents();
+ }, []);
+ return (
+ <>
+
+ {isLoading &&
Loading...
}
+ {error &&
{error}
}
+
+ {!isLoading && fetchedEvents && }
+ >
+ );
+}
+
+export default EventsPage;
diff --git a/code/15-data-fetching-example/src/pages/EventsRoot.js b/code/15-data-fetching-example/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/15-data-fetching-example/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/15-data-fetching-example/src/pages/Home.js b/code/15-data-fetching-example/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/15-data-fetching-example/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/15-data-fetching-example/src/pages/NewEvent.js b/code/15-data-fetching-example/src/pages/NewEvent.js
new file mode 100644
index 0000000000..189ca3b061
--- /dev/null
+++ b/code/15-data-fetching-example/src/pages/NewEvent.js
@@ -0,0 +1,5 @@
+function NewEventPage() {
+ return NewEventPage
;
+}
+
+export default NewEventPage;
diff --git a/code/15-data-fetching-example/src/pages/Root.js b/code/15-data-fetching-example/src/pages/Root.js
new file mode 100644
index 0000000000..db6babb23a
--- /dev/null
+++ b/code/15-data-fetching-example/src/pages/Root.js
@@ -0,0 +1,16 @@
+import { Outlet } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/16-added-loader/package.json b/code/16-added-loader/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/16-added-loader/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-added-loader/public/favicon.ico b/code/16-added-loader/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/16-added-loader/public/favicon.ico differ
diff --git a/code/16-added-loader/public/index.html b/code/16-added-loader/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/16-added-loader/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/16-added-loader/public/logo192.png b/code/16-added-loader/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/16-added-loader/public/logo192.png differ
diff --git a/code/16-added-loader/public/logo512.png b/code/16-added-loader/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/16-added-loader/public/logo512.png differ
diff --git a/code/16-added-loader/public/manifest.json b/code/16-added-loader/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/16-added-loader/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-added-loader/public/robots.txt b/code/16-added-loader/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/16-added-loader/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/16-added-loader/src/App.js b/code/16-added-loader/src/App.js
new file mode 100644
index 0000000000..82b0a04ae7
--- /dev/null
+++ b/code/16-added-loader/src/App.js
@@ -0,0 +1,48 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import EventDetailPage from './pages/EventDetail';
+import EventsPage from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: async () => {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // ...
+ } else {
+ const resData = await response.json();
+ return resData.events;
+ }
+ },
+ },
+ { path: ':eventId', element: },
+ { path: 'new', element: },
+ { path: ':eventId/edit', element: },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/16-added-loader/src/components/EventForm.js b/code/16-added-loader/src/components/EventForm.js
new file mode 100644
index 0000000000..f0bc84b08c
--- /dev/null
+++ b/code/16-added-loader/src/components/EventForm.js
@@ -0,0 +1,39 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/16-added-loader/src/components/EventForm.module.css b/code/16-added-loader/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/16-added-loader/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/16-added-loader/src/components/EventItem.js b/code/16-added-loader/src/components/EventItem.js
new file mode 100644
index 0000000000..ac8de8258e
--- /dev/null
+++ b/code/16-added-loader/src/components/EventItem.js
@@ -0,0 +1,22 @@
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/16-added-loader/src/components/EventItem.module.css b/code/16-added-loader/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/16-added-loader/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/16-added-loader/src/components/EventsList.js b/code/16-added-loader/src/components/EventsList.js
new file mode 100644
index 0000000000..57886b018d
--- /dev/null
+++ b/code/16-added-loader/src/components/EventsList.js
@@ -0,0 +1,28 @@
+// import { useLoaderData } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/16-added-loader/src/components/EventsList.module.css b/code/16-added-loader/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/16-added-loader/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/16-added-loader/src/components/EventsNavigation.js b/code/16-added-loader/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/16-added-loader/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/16-added-loader/src/components/EventsNavigation.module.css b/code/16-added-loader/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/16-added-loader/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/16-added-loader/src/components/MainNavigation.js b/code/16-added-loader/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/16-added-loader/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/16-added-loader/src/components/MainNavigation.module.css b/code/16-added-loader/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/16-added-loader/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/16-added-loader/src/index.css b/code/16-added-loader/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/16-added-loader/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/16-added-loader/src/index.js b/code/16-added-loader/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/16-added-loader/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/16-added-loader/src/pages/EditEvent.js b/code/16-added-loader/src/pages/EditEvent.js
new file mode 100644
index 0000000000..aa0ae97bbf
--- /dev/null
+++ b/code/16-added-loader/src/pages/EditEvent.js
@@ -0,0 +1,5 @@
+function EditEventPage() {
+ return EditEventPage
;
+}
+
+export default EditEventPage;
diff --git a/code/16-added-loader/src/pages/EventDetail.js b/code/16-added-loader/src/pages/EventDetail.js
new file mode 100644
index 0000000000..a3947487be
--- /dev/null
+++ b/code/16-added-loader/src/pages/EventDetail.js
@@ -0,0 +1,14 @@
+import { useParams } from 'react-router-dom';
+
+function EventDetailPage() {
+ const params = useParams();
+
+ return (
+ <>
+ EventDetailPage
+ Event ID: {params.eventId}
+ >
+ );
+}
+
+export default EventDetailPage;
diff --git a/code/16-added-loader/src/pages/Events.js b/code/16-added-loader/src/pages/Events.js
new file mode 100644
index 0000000000..d3fbdd4c9a
--- /dev/null
+++ b/code/16-added-loader/src/pages/Events.js
@@ -0,0 +1,11 @@
+import { useLoaderData } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const events = useLoaderData();
+
+ return ;
+}
+
+export default EventsPage;
diff --git a/code/16-added-loader/src/pages/EventsRoot.js b/code/16-added-loader/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/16-added-loader/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/16-added-loader/src/pages/Home.js b/code/16-added-loader/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/16-added-loader/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/16-added-loader/src/pages/NewEvent.js b/code/16-added-loader/src/pages/NewEvent.js
new file mode 100644
index 0000000000..189ca3b061
--- /dev/null
+++ b/code/16-added-loader/src/pages/NewEvent.js
@@ -0,0 +1,5 @@
+function NewEventPage() {
+ return NewEventPage
;
+}
+
+export default NewEventPage;
diff --git a/code/16-added-loader/src/pages/Root.js b/code/16-added-loader/src/pages/Root.js
new file mode 100644
index 0000000000..2321492259
--- /dev/null
+++ b/code/16-added-loader/src/pages/Root.js
@@ -0,0 +1,16 @@
+import { Outlet, useLoaderData } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/17-loader-in-separate-file/package.json b/code/17-loader-in-separate-file/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/17-loader-in-separate-file/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-loader-in-separate-file/public/favicon.ico b/code/17-loader-in-separate-file/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/17-loader-in-separate-file/public/favicon.ico differ
diff --git a/code/17-loader-in-separate-file/public/index.html b/code/17-loader-in-separate-file/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/17-loader-in-separate-file/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/17-loader-in-separate-file/public/logo192.png b/code/17-loader-in-separate-file/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/17-loader-in-separate-file/public/logo192.png differ
diff --git a/code/17-loader-in-separate-file/public/logo512.png b/code/17-loader-in-separate-file/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/17-loader-in-separate-file/public/logo512.png differ
diff --git a/code/17-loader-in-separate-file/public/manifest.json b/code/17-loader-in-separate-file/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/17-loader-in-separate-file/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-loader-in-separate-file/public/robots.txt b/code/17-loader-in-separate-file/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/17-loader-in-separate-file/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/17-loader-in-separate-file/src/App.js b/code/17-loader-in-separate-file/src/App.js
new file mode 100644
index 0000000000..c2b4bc30e8
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/App.js
@@ -0,0 +1,39 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import EventDetailPage from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ { path: ':eventId', element: },
+ { path: 'new', element: },
+ { path: ':eventId/edit', element: },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/17-loader-in-separate-file/src/components/EventForm.js b/code/17-loader-in-separate-file/src/components/EventForm.js
new file mode 100644
index 0000000000..f0bc84b08c
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/components/EventForm.js
@@ -0,0 +1,39 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/17-loader-in-separate-file/src/components/EventForm.module.css b/code/17-loader-in-separate-file/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/17-loader-in-separate-file/src/components/EventItem.js b/code/17-loader-in-separate-file/src/components/EventItem.js
new file mode 100644
index 0000000000..ac8de8258e
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/components/EventItem.js
@@ -0,0 +1,22 @@
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/17-loader-in-separate-file/src/components/EventItem.module.css b/code/17-loader-in-separate-file/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/17-loader-in-separate-file/src/components/EventsList.js b/code/17-loader-in-separate-file/src/components/EventsList.js
new file mode 100644
index 0000000000..57886b018d
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/components/EventsList.js
@@ -0,0 +1,28 @@
+// import { useLoaderData } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/17-loader-in-separate-file/src/components/EventsList.module.css b/code/17-loader-in-separate-file/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/17-loader-in-separate-file/src/components/EventsNavigation.js b/code/17-loader-in-separate-file/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/17-loader-in-separate-file/src/components/EventsNavigation.module.css b/code/17-loader-in-separate-file/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/17-loader-in-separate-file/src/components/MainNavigation.js b/code/17-loader-in-separate-file/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/17-loader-in-separate-file/src/components/MainNavigation.module.css b/code/17-loader-in-separate-file/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/17-loader-in-separate-file/src/index.css b/code/17-loader-in-separate-file/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/17-loader-in-separate-file/src/index.js b/code/17-loader-in-separate-file/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/17-loader-in-separate-file/src/pages/EditEvent.js b/code/17-loader-in-separate-file/src/pages/EditEvent.js
new file mode 100644
index 0000000000..aa0ae97bbf
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/pages/EditEvent.js
@@ -0,0 +1,5 @@
+function EditEventPage() {
+ return EditEventPage
;
+}
+
+export default EditEventPage;
diff --git a/code/17-loader-in-separate-file/src/pages/EventDetail.js b/code/17-loader-in-separate-file/src/pages/EventDetail.js
new file mode 100644
index 0000000000..a3947487be
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/pages/EventDetail.js
@@ -0,0 +1,14 @@
+import { useParams } from 'react-router-dom';
+
+function EventDetailPage() {
+ const params = useParams();
+
+ return (
+ <>
+ EventDetailPage
+ Event ID: {params.eventId}
+ >
+ );
+}
+
+export default EventDetailPage;
diff --git a/code/17-loader-in-separate-file/src/pages/Events.js b/code/17-loader-in-separate-file/src/pages/Events.js
new file mode 100644
index 0000000000..aba04522fb
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/pages/Events.js
@@ -0,0 +1,22 @@
+import { useLoaderData } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const events = useLoaderData();
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // ...
+ } else {
+ const resData = await response.json();
+ return resData.events;
+ }
+}
diff --git a/code/17-loader-in-separate-file/src/pages/EventsRoot.js b/code/17-loader-in-separate-file/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/17-loader-in-separate-file/src/pages/Home.js b/code/17-loader-in-separate-file/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/17-loader-in-separate-file/src/pages/NewEvent.js b/code/17-loader-in-separate-file/src/pages/NewEvent.js
new file mode 100644
index 0000000000..189ca3b061
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/pages/NewEvent.js
@@ -0,0 +1,5 @@
+function NewEventPage() {
+ return NewEventPage
;
+}
+
+export default NewEventPage;
diff --git a/code/17-loader-in-separate-file/src/pages/Root.js b/code/17-loader-in-separate-file/src/pages/Root.js
new file mode 100644
index 0000000000..2321492259
--- /dev/null
+++ b/code/17-loader-in-separate-file/src/pages/Root.js
@@ -0,0 +1,16 @@
+import { Outlet, useLoaderData } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/18-user-feedback-usenavigation/package.json b/code/18-user-feedback-usenavigation/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-user-feedback-usenavigation/public/favicon.ico b/code/18-user-feedback-usenavigation/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/18-user-feedback-usenavigation/public/favicon.ico differ
diff --git a/code/18-user-feedback-usenavigation/public/index.html b/code/18-user-feedback-usenavigation/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/18-user-feedback-usenavigation/public/logo192.png b/code/18-user-feedback-usenavigation/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/18-user-feedback-usenavigation/public/logo192.png differ
diff --git a/code/18-user-feedback-usenavigation/public/logo512.png b/code/18-user-feedback-usenavigation/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/18-user-feedback-usenavigation/public/logo512.png differ
diff --git a/code/18-user-feedback-usenavigation/public/manifest.json b/code/18-user-feedback-usenavigation/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/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-user-feedback-usenavigation/public/robots.txt b/code/18-user-feedback-usenavigation/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/18-user-feedback-usenavigation/src/App.js b/code/18-user-feedback-usenavigation/src/App.js
new file mode 100644
index 0000000000..c2b4bc30e8
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/App.js
@@ -0,0 +1,39 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import EventDetailPage from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ { path: ':eventId', element: },
+ { path: 'new', element: },
+ { path: ':eventId/edit', element: },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/18-user-feedback-usenavigation/src/components/EventForm.js b/code/18-user-feedback-usenavigation/src/components/EventForm.js
new file mode 100644
index 0000000000..f0bc84b08c
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/components/EventForm.js
@@ -0,0 +1,39 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/18-user-feedback-usenavigation/src/components/EventForm.module.css b/code/18-user-feedback-usenavigation/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/18-user-feedback-usenavigation/src/components/EventItem.js b/code/18-user-feedback-usenavigation/src/components/EventItem.js
new file mode 100644
index 0000000000..ac8de8258e
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/components/EventItem.js
@@ -0,0 +1,22 @@
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/18-user-feedback-usenavigation/src/components/EventItem.module.css b/code/18-user-feedback-usenavigation/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/18-user-feedback-usenavigation/src/components/EventsList.js b/code/18-user-feedback-usenavigation/src/components/EventsList.js
new file mode 100644
index 0000000000..57886b018d
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/components/EventsList.js
@@ -0,0 +1,28 @@
+// import { useLoaderData } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/18-user-feedback-usenavigation/src/components/EventsList.module.css b/code/18-user-feedback-usenavigation/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/18-user-feedback-usenavigation/src/components/EventsNavigation.js b/code/18-user-feedback-usenavigation/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/18-user-feedback-usenavigation/src/components/EventsNavigation.module.css b/code/18-user-feedback-usenavigation/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/18-user-feedback-usenavigation/src/components/MainNavigation.js b/code/18-user-feedback-usenavigation/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/18-user-feedback-usenavigation/src/components/MainNavigation.module.css b/code/18-user-feedback-usenavigation/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/18-user-feedback-usenavigation/src/index.css b/code/18-user-feedback-usenavigation/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/18-user-feedback-usenavigation/src/index.js b/code/18-user-feedback-usenavigation/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/18-user-feedback-usenavigation/src/pages/EditEvent.js b/code/18-user-feedback-usenavigation/src/pages/EditEvent.js
new file mode 100644
index 0000000000..aa0ae97bbf
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/pages/EditEvent.js
@@ -0,0 +1,5 @@
+function EditEventPage() {
+ return EditEventPage
;
+}
+
+export default EditEventPage;
diff --git a/code/18-user-feedback-usenavigation/src/pages/EventDetail.js b/code/18-user-feedback-usenavigation/src/pages/EventDetail.js
new file mode 100644
index 0000000000..a3947487be
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/pages/EventDetail.js
@@ -0,0 +1,14 @@
+import { useParams } from 'react-router-dom';
+
+function EventDetailPage() {
+ const params = useParams();
+
+ return (
+ <>
+ EventDetailPage
+ Event ID: {params.eventId}
+ >
+ );
+}
+
+export default EventDetailPage;
diff --git a/code/18-user-feedback-usenavigation/src/pages/Events.js b/code/18-user-feedback-usenavigation/src/pages/Events.js
new file mode 100644
index 0000000000..aba04522fb
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/pages/Events.js
@@ -0,0 +1,22 @@
+import { useLoaderData } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const events = useLoaderData();
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // ...
+ } else {
+ const resData = await response.json();
+ return resData.events;
+ }
+}
diff --git a/code/18-user-feedback-usenavigation/src/pages/EventsRoot.js b/code/18-user-feedback-usenavigation/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/18-user-feedback-usenavigation/src/pages/Home.js b/code/18-user-feedback-usenavigation/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/18-user-feedback-usenavigation/src/pages/NewEvent.js b/code/18-user-feedback-usenavigation/src/pages/NewEvent.js
new file mode 100644
index 0000000000..189ca3b061
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/pages/NewEvent.js
@@ -0,0 +1,5 @@
+function NewEventPage() {
+ return NewEventPage
;
+}
+
+export default NewEventPage;
diff --git a/code/18-user-feedback-usenavigation/src/pages/Root.js b/code/18-user-feedback-usenavigation/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/18-user-feedback-usenavigation/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/19-returning-responses/package.json b/code/19-returning-responses/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/19-returning-responses/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-returning-responses/public/favicon.ico b/code/19-returning-responses/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/19-returning-responses/public/favicon.ico differ
diff --git a/code/19-returning-responses/public/index.html b/code/19-returning-responses/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/19-returning-responses/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/19-returning-responses/public/logo192.png b/code/19-returning-responses/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/19-returning-responses/public/logo192.png differ
diff --git a/code/19-returning-responses/public/logo512.png b/code/19-returning-responses/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/19-returning-responses/public/logo512.png differ
diff --git a/code/19-returning-responses/public/manifest.json b/code/19-returning-responses/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/19-returning-responses/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-returning-responses/public/robots.txt b/code/19-returning-responses/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/19-returning-responses/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/19-returning-responses/src/App.js b/code/19-returning-responses/src/App.js
new file mode 100644
index 0000000000..c2b4bc30e8
--- /dev/null
+++ b/code/19-returning-responses/src/App.js
@@ -0,0 +1,39 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import EventDetailPage from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ { path: ':eventId', element: },
+ { path: 'new', element: },
+ { path: ':eventId/edit', element: },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/19-returning-responses/src/components/EventForm.js b/code/19-returning-responses/src/components/EventForm.js
new file mode 100644
index 0000000000..f0bc84b08c
--- /dev/null
+++ b/code/19-returning-responses/src/components/EventForm.js
@@ -0,0 +1,39 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/19-returning-responses/src/components/EventForm.module.css b/code/19-returning-responses/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/19-returning-responses/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/19-returning-responses/src/components/EventItem.js b/code/19-returning-responses/src/components/EventItem.js
new file mode 100644
index 0000000000..ac8de8258e
--- /dev/null
+++ b/code/19-returning-responses/src/components/EventItem.js
@@ -0,0 +1,22 @@
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/19-returning-responses/src/components/EventItem.module.css b/code/19-returning-responses/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/19-returning-responses/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/19-returning-responses/src/components/EventsList.js b/code/19-returning-responses/src/components/EventsList.js
new file mode 100644
index 0000000000..57886b018d
--- /dev/null
+++ b/code/19-returning-responses/src/components/EventsList.js
@@ -0,0 +1,28 @@
+// import { useLoaderData } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/19-returning-responses/src/components/EventsList.module.css b/code/19-returning-responses/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/19-returning-responses/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/19-returning-responses/src/components/EventsNavigation.js b/code/19-returning-responses/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/19-returning-responses/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/19-returning-responses/src/components/EventsNavigation.module.css b/code/19-returning-responses/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/19-returning-responses/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/19-returning-responses/src/components/MainNavigation.js b/code/19-returning-responses/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/19-returning-responses/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/19-returning-responses/src/components/MainNavigation.module.css b/code/19-returning-responses/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/19-returning-responses/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/19-returning-responses/src/index.css b/code/19-returning-responses/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/19-returning-responses/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/19-returning-responses/src/index.js b/code/19-returning-responses/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/19-returning-responses/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/19-returning-responses/src/pages/EditEvent.js b/code/19-returning-responses/src/pages/EditEvent.js
new file mode 100644
index 0000000000..aa0ae97bbf
--- /dev/null
+++ b/code/19-returning-responses/src/pages/EditEvent.js
@@ -0,0 +1,5 @@
+function EditEventPage() {
+ return EditEventPage
;
+}
+
+export default EditEventPage;
diff --git a/code/19-returning-responses/src/pages/EventDetail.js b/code/19-returning-responses/src/pages/EventDetail.js
new file mode 100644
index 0000000000..a3947487be
--- /dev/null
+++ b/code/19-returning-responses/src/pages/EventDetail.js
@@ -0,0 +1,14 @@
+import { useParams } from 'react-router-dom';
+
+function EventDetailPage() {
+ const params = useParams();
+
+ return (
+ <>
+ EventDetailPage
+ Event ID: {params.eventId}
+ >
+ );
+}
+
+export default EventDetailPage;
diff --git a/code/19-returning-responses/src/pages/Events.js b/code/19-returning-responses/src/pages/Events.js
new file mode 100644
index 0000000000..45df659920
--- /dev/null
+++ b/code/19-returning-responses/src/pages/Events.js
@@ -0,0 +1,22 @@
+import { useLoaderData } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // ...
+ } else {
+ return response;
+ }
+}
diff --git a/code/19-returning-responses/src/pages/EventsRoot.js b/code/19-returning-responses/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/19-returning-responses/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/19-returning-responses/src/pages/Home.js b/code/19-returning-responses/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/19-returning-responses/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/19-returning-responses/src/pages/NewEvent.js b/code/19-returning-responses/src/pages/NewEvent.js
new file mode 100644
index 0000000000..189ca3b061
--- /dev/null
+++ b/code/19-returning-responses/src/pages/NewEvent.js
@@ -0,0 +1,5 @@
+function NewEventPage() {
+ return NewEventPage
;
+}
+
+export default NewEventPage;
diff --git a/code/19-returning-responses/src/pages/Root.js b/code/19-returning-responses/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/19-returning-responses/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/20-error-handling/package.json b/code/20-error-handling/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/20-error-handling/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-error-handling/public/favicon.ico b/code/20-error-handling/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/20-error-handling/public/favicon.ico differ
diff --git a/code/20-error-handling/public/index.html b/code/20-error-handling/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/20-error-handling/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/20-error-handling/public/logo192.png b/code/20-error-handling/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/20-error-handling/public/logo192.png differ
diff --git a/code/20-error-handling/public/logo512.png b/code/20-error-handling/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/20-error-handling/public/logo512.png differ
diff --git a/code/20-error-handling/public/manifest.json b/code/20-error-handling/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/20-error-handling/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-error-handling/public/robots.txt b/code/20-error-handling/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/20-error-handling/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/20-error-handling/src/App.js b/code/20-error-handling/src/App.js
new file mode 100644
index 0000000000..8334333d0c
--- /dev/null
+++ b/code/20-error-handling/src/App.js
@@ -0,0 +1,41 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ { path: ':eventId', element: },
+ { path: 'new', element: },
+ { path: ':eventId/edit', element: },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/20-error-handling/src/components/EventForm.js b/code/20-error-handling/src/components/EventForm.js
new file mode 100644
index 0000000000..f0bc84b08c
--- /dev/null
+++ b/code/20-error-handling/src/components/EventForm.js
@@ -0,0 +1,39 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/20-error-handling/src/components/EventForm.module.css b/code/20-error-handling/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/20-error-handling/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/20-error-handling/src/components/EventItem.js b/code/20-error-handling/src/components/EventItem.js
new file mode 100644
index 0000000000..ac8de8258e
--- /dev/null
+++ b/code/20-error-handling/src/components/EventItem.js
@@ -0,0 +1,22 @@
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/20-error-handling/src/components/EventItem.module.css b/code/20-error-handling/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/20-error-handling/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/20-error-handling/src/components/EventsList.js b/code/20-error-handling/src/components/EventsList.js
new file mode 100644
index 0000000000..57886b018d
--- /dev/null
+++ b/code/20-error-handling/src/components/EventsList.js
@@ -0,0 +1,28 @@
+// import { useLoaderData } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/20-error-handling/src/components/EventsList.module.css b/code/20-error-handling/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/20-error-handling/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/20-error-handling/src/components/EventsNavigation.js b/code/20-error-handling/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/20-error-handling/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/20-error-handling/src/components/EventsNavigation.module.css b/code/20-error-handling/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/20-error-handling/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/20-error-handling/src/components/MainNavigation.js b/code/20-error-handling/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/20-error-handling/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/20-error-handling/src/components/MainNavigation.module.css b/code/20-error-handling/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/20-error-handling/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/20-error-handling/src/components/PageContent.js b/code/20-error-handling/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/20-error-handling/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/20-error-handling/src/components/PageContent.module.css b/code/20-error-handling/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/20-error-handling/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/20-error-handling/src/index.css b/code/20-error-handling/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/20-error-handling/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/20-error-handling/src/index.js b/code/20-error-handling/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/20-error-handling/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/20-error-handling/src/pages/EditEvent.js b/code/20-error-handling/src/pages/EditEvent.js
new file mode 100644
index 0000000000..aa0ae97bbf
--- /dev/null
+++ b/code/20-error-handling/src/pages/EditEvent.js
@@ -0,0 +1,5 @@
+function EditEventPage() {
+ return EditEventPage
;
+}
+
+export default EditEventPage;
diff --git a/code/20-error-handling/src/pages/Error.js b/code/20-error-handling/src/pages/Error.js
new file mode 100644
index 0000000000..6dd76db372
--- /dev/null
+++ b/code/20-error-handling/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = JSON.parse(error.data).message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/20-error-handling/src/pages/EventDetail.js b/code/20-error-handling/src/pages/EventDetail.js
new file mode 100644
index 0000000000..a3947487be
--- /dev/null
+++ b/code/20-error-handling/src/pages/EventDetail.js
@@ -0,0 +1,14 @@
+import { useParams } from 'react-router-dom';
+
+function EventDetailPage() {
+ const params = useParams();
+
+ return (
+ <>
+ EventDetailPage
+ Event ID: {params.eventId}
+ >
+ );
+}
+
+export default EventDetailPage;
diff --git a/code/20-error-handling/src/pages/Events.js b/code/20-error-handling/src/pages/Events.js
new file mode 100644
index 0000000000..faebdd2f36
--- /dev/null
+++ b/code/20-error-handling/src/pages/Events.js
@@ -0,0 +1,29 @@
+import { useLoaderData } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+
+ // if (data.isError) {
+ // return {data.message}
;
+ // }
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ status: 500,
+ });
+ } else {
+ return response;
+ }
+}
diff --git a/code/20-error-handling/src/pages/EventsRoot.js b/code/20-error-handling/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/20-error-handling/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/20-error-handling/src/pages/Home.js b/code/20-error-handling/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/20-error-handling/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/20-error-handling/src/pages/NewEvent.js b/code/20-error-handling/src/pages/NewEvent.js
new file mode 100644
index 0000000000..189ca3b061
--- /dev/null
+++ b/code/20-error-handling/src/pages/NewEvent.js
@@ -0,0 +1,5 @@
+function NewEventPage() {
+ return NewEventPage
;
+}
+
+export default NewEventPage;
diff --git a/code/20-error-handling/src/pages/Root.js b/code/20-error-handling/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/20-error-handling/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/21-json-helper-function/package.json b/code/21-json-helper-function/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/21-json-helper-function/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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-json-helper-function/public/favicon.ico b/code/21-json-helper-function/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/21-json-helper-function/public/favicon.ico differ
diff --git a/code/21-json-helper-function/public/index.html b/code/21-json-helper-function/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/21-json-helper-function/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/21-json-helper-function/public/logo192.png b/code/21-json-helper-function/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/21-json-helper-function/public/logo192.png differ
diff --git a/code/21-json-helper-function/public/logo512.png b/code/21-json-helper-function/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/21-json-helper-function/public/logo512.png differ
diff --git a/code/21-json-helper-function/public/manifest.json b/code/21-json-helper-function/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/21-json-helper-function/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-json-helper-function/public/robots.txt b/code/21-json-helper-function/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/21-json-helper-function/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/21-json-helper-function/src/App.js b/code/21-json-helper-function/src/App.js
new file mode 100644
index 0000000000..8334333d0c
--- /dev/null
+++ b/code/21-json-helper-function/src/App.js
@@ -0,0 +1,41 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ { path: ':eventId', element: },
+ { path: 'new', element: },
+ { path: ':eventId/edit', element: },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/21-json-helper-function/src/components/EventForm.js b/code/21-json-helper-function/src/components/EventForm.js
new file mode 100644
index 0000000000..f0bc84b08c
--- /dev/null
+++ b/code/21-json-helper-function/src/components/EventForm.js
@@ -0,0 +1,39 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/21-json-helper-function/src/components/EventForm.module.css b/code/21-json-helper-function/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/21-json-helper-function/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/21-json-helper-function/src/components/EventItem.js b/code/21-json-helper-function/src/components/EventItem.js
new file mode 100644
index 0000000000..ac8de8258e
--- /dev/null
+++ b/code/21-json-helper-function/src/components/EventItem.js
@@ -0,0 +1,22 @@
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/21-json-helper-function/src/components/EventItem.module.css b/code/21-json-helper-function/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/21-json-helper-function/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/21-json-helper-function/src/components/EventsList.js b/code/21-json-helper-function/src/components/EventsList.js
new file mode 100644
index 0000000000..57886b018d
--- /dev/null
+++ b/code/21-json-helper-function/src/components/EventsList.js
@@ -0,0 +1,28 @@
+// import { useLoaderData } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/21-json-helper-function/src/components/EventsList.module.css b/code/21-json-helper-function/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/21-json-helper-function/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/21-json-helper-function/src/components/EventsNavigation.js b/code/21-json-helper-function/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/21-json-helper-function/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/21-json-helper-function/src/components/EventsNavigation.module.css b/code/21-json-helper-function/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/21-json-helper-function/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/21-json-helper-function/src/components/MainNavigation.js b/code/21-json-helper-function/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/21-json-helper-function/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/21-json-helper-function/src/components/MainNavigation.module.css b/code/21-json-helper-function/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/21-json-helper-function/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/21-json-helper-function/src/components/PageContent.js b/code/21-json-helper-function/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/21-json-helper-function/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/21-json-helper-function/src/components/PageContent.module.css b/code/21-json-helper-function/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/21-json-helper-function/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/21-json-helper-function/src/index.css b/code/21-json-helper-function/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/21-json-helper-function/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/21-json-helper-function/src/index.js b/code/21-json-helper-function/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/21-json-helper-function/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/21-json-helper-function/src/pages/EditEvent.js b/code/21-json-helper-function/src/pages/EditEvent.js
new file mode 100644
index 0000000000..aa0ae97bbf
--- /dev/null
+++ b/code/21-json-helper-function/src/pages/EditEvent.js
@@ -0,0 +1,5 @@
+function EditEventPage() {
+ return EditEventPage
;
+}
+
+export default EditEventPage;
diff --git a/code/21-json-helper-function/src/pages/Error.js b/code/21-json-helper-function/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/21-json-helper-function/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/21-json-helper-function/src/pages/EventDetail.js b/code/21-json-helper-function/src/pages/EventDetail.js
new file mode 100644
index 0000000000..a3947487be
--- /dev/null
+++ b/code/21-json-helper-function/src/pages/EventDetail.js
@@ -0,0 +1,14 @@
+import { useParams } from 'react-router-dom';
+
+function EventDetailPage() {
+ const params = useParams();
+
+ return (
+ <>
+ EventDetailPage
+ Event ID: {params.eventId}
+ >
+ );
+}
+
+export default EventDetailPage;
diff --git a/code/21-json-helper-function/src/pages/Events.js b/code/21-json-helper-function/src/pages/Events.js
new file mode 100644
index 0000000000..ce4bceb5e2
--- /dev/null
+++ b/code/21-json-helper-function/src/pages/Events.js
@@ -0,0 +1,35 @@
+import { useLoaderData, json } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+
+ // if (data.isError) {
+ // return {data.message}
;
+ // }
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/21-json-helper-function/src/pages/EventsRoot.js b/code/21-json-helper-function/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/21-json-helper-function/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/21-json-helper-function/src/pages/Home.js b/code/21-json-helper-function/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/21-json-helper-function/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/21-json-helper-function/src/pages/NewEvent.js b/code/21-json-helper-function/src/pages/NewEvent.js
new file mode 100644
index 0000000000..189ca3b061
--- /dev/null
+++ b/code/21-json-helper-function/src/pages/NewEvent.js
@@ -0,0 +1,5 @@
+function NewEventPage() {
+ return NewEventPage
;
+}
+
+export default NewEventPage;
diff --git a/code/21-json-helper-function/src/pages/Root.js b/code/21-json-helper-function/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/21-json-helper-function/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/22-dynamic-routes-loader/package.json b/code/22-dynamic-routes-loader/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/22-dynamic-routes-loader/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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/22-dynamic-routes-loader/public/favicon.ico b/code/22-dynamic-routes-loader/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/22-dynamic-routes-loader/public/favicon.ico differ
diff --git a/code/22-dynamic-routes-loader/public/index.html b/code/22-dynamic-routes-loader/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/22-dynamic-routes-loader/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/22-dynamic-routes-loader/public/logo192.png b/code/22-dynamic-routes-loader/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/22-dynamic-routes-loader/public/logo192.png differ
diff --git a/code/22-dynamic-routes-loader/public/logo512.png b/code/22-dynamic-routes-loader/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/22-dynamic-routes-loader/public/logo512.png differ
diff --git a/code/22-dynamic-routes-loader/public/manifest.json b/code/22-dynamic-routes-loader/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/22-dynamic-routes-loader/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/22-dynamic-routes-loader/public/robots.txt b/code/22-dynamic-routes-loader/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/22-dynamic-routes-loader/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/22-dynamic-routes-loader/src/App.js b/code/22-dynamic-routes-loader/src/App.js
new file mode 100644
index 0000000000..c88ad89d37
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/App.js
@@ -0,0 +1,47 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ element: ,
+ loader: eventDetailLoader,
+ },
+ { path: 'new', element: },
+ { path: ':eventId/edit', element: },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/22-dynamic-routes-loader/src/components/EventForm.js b/code/22-dynamic-routes-loader/src/components/EventForm.js
new file mode 100644
index 0000000000..f0bc84b08c
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/EventForm.js
@@ -0,0 +1,39 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/22-dynamic-routes-loader/src/components/EventForm.module.css b/code/22-dynamic-routes-loader/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/22-dynamic-routes-loader/src/components/EventItem.js b/code/22-dynamic-routes-loader/src/components/EventItem.js
new file mode 100644
index 0000000000..ac8de8258e
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/EventItem.js
@@ -0,0 +1,22 @@
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/22-dynamic-routes-loader/src/components/EventItem.module.css b/code/22-dynamic-routes-loader/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/22-dynamic-routes-loader/src/components/EventsList.js b/code/22-dynamic-routes-loader/src/components/EventsList.js
new file mode 100644
index 0000000000..0488cb0087
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/EventsList.js
@@ -0,0 +1,29 @@
+// import { useLoaderData } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/22-dynamic-routes-loader/src/components/EventsList.module.css b/code/22-dynamic-routes-loader/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/22-dynamic-routes-loader/src/components/EventsNavigation.js b/code/22-dynamic-routes-loader/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/22-dynamic-routes-loader/src/components/EventsNavigation.module.css b/code/22-dynamic-routes-loader/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/22-dynamic-routes-loader/src/components/MainNavigation.js b/code/22-dynamic-routes-loader/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/22-dynamic-routes-loader/src/components/MainNavigation.module.css b/code/22-dynamic-routes-loader/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/22-dynamic-routes-loader/src/components/PageContent.js b/code/22-dynamic-routes-loader/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/22-dynamic-routes-loader/src/components/PageContent.module.css b/code/22-dynamic-routes-loader/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/22-dynamic-routes-loader/src/index.css b/code/22-dynamic-routes-loader/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/22-dynamic-routes-loader/src/index.js b/code/22-dynamic-routes-loader/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/22-dynamic-routes-loader/src/pages/EditEvent.js b/code/22-dynamic-routes-loader/src/pages/EditEvent.js
new file mode 100644
index 0000000000..aa0ae97bbf
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/pages/EditEvent.js
@@ -0,0 +1,5 @@
+function EditEventPage() {
+ return EditEventPage
;
+}
+
+export default EditEventPage;
diff --git a/code/22-dynamic-routes-loader/src/pages/Error.js b/code/22-dynamic-routes-loader/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/22-dynamic-routes-loader/src/pages/EventDetail.js b/code/22-dynamic-routes-loader/src/pages/EventDetail.js
new file mode 100644
index 0000000000..c11ac3e1a2
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/pages/EventDetail.js
@@ -0,0 +1,27 @@
+import { useLoaderData, json } from 'react-router-dom';
+
+import EventItem from '../components/EventItem';
+
+function EventDetailPage() {
+ const data = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventDetailPage;
+
+export async function loader({request, params}) {
+ const id = params.eventId;
+
+ const response = await fetch('http://localhost:8080/events/' + id);
+
+ if (!response.ok) {
+ throw json({message: 'Could not fetch details for selected event.'}, {
+ status: 500
+ })
+ } else {
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/code/22-dynamic-routes-loader/src/pages/Events.js b/code/22-dynamic-routes-loader/src/pages/Events.js
new file mode 100644
index 0000000000..ce4bceb5e2
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/pages/Events.js
@@ -0,0 +1,35 @@
+import { useLoaderData, json } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+
+ // if (data.isError) {
+ // return {data.message}
;
+ // }
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/22-dynamic-routes-loader/src/pages/EventsRoot.js b/code/22-dynamic-routes-loader/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/22-dynamic-routes-loader/src/pages/Home.js b/code/22-dynamic-routes-loader/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/22-dynamic-routes-loader/src/pages/NewEvent.js b/code/22-dynamic-routes-loader/src/pages/NewEvent.js
new file mode 100644
index 0000000000..189ca3b061
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/pages/NewEvent.js
@@ -0,0 +1,5 @@
+function NewEventPage() {
+ return NewEventPage
;
+}
+
+export default NewEventPage;
diff --git a/code/22-dynamic-routes-loader/src/pages/Root.js b/code/22-dynamic-routes-loader/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/22-dynamic-routes-loader/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/23-userouteloaderdata/package.json b/code/23-userouteloaderdata/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/23-userouteloaderdata/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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/23-userouteloaderdata/public/favicon.ico b/code/23-userouteloaderdata/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/23-userouteloaderdata/public/favicon.ico differ
diff --git a/code/23-userouteloaderdata/public/index.html b/code/23-userouteloaderdata/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/23-userouteloaderdata/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/23-userouteloaderdata/public/logo192.png b/code/23-userouteloaderdata/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/23-userouteloaderdata/public/logo192.png differ
diff --git a/code/23-userouteloaderdata/public/logo512.png b/code/23-userouteloaderdata/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/23-userouteloaderdata/public/logo512.png differ
diff --git a/code/23-userouteloaderdata/public/manifest.json b/code/23-userouteloaderdata/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/23-userouteloaderdata/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/23-userouteloaderdata/public/robots.txt b/code/23-userouteloaderdata/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/23-userouteloaderdata/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/23-userouteloaderdata/src/App.js b/code/23-userouteloaderdata/src/App.js
new file mode 100644
index 0000000000..8a159385fa
--- /dev/null
+++ b/code/23-userouteloaderdata/src/App.js
@@ -0,0 +1,53 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ id: 'event-detail',
+ loader: eventDetailLoader,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ { path: 'edit', element: },
+ ],
+ },
+ { path: 'new', element: },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/23-userouteloaderdata/src/components/EventForm.js b/code/23-userouteloaderdata/src/components/EventForm.js
new file mode 100644
index 0000000000..2255167e7a
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/EventForm.js
@@ -0,0 +1,63 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/23-userouteloaderdata/src/components/EventForm.module.css b/code/23-userouteloaderdata/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/23-userouteloaderdata/src/components/EventItem.js b/code/23-userouteloaderdata/src/components/EventItem.js
new file mode 100644
index 0000000000..b68551cea3
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/EventItem.js
@@ -0,0 +1,24 @@
+import { Link } from 'react-router-dom';
+
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/23-userouteloaderdata/src/components/EventItem.module.css b/code/23-userouteloaderdata/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/23-userouteloaderdata/src/components/EventsList.js b/code/23-userouteloaderdata/src/components/EventsList.js
new file mode 100644
index 0000000000..0488cb0087
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/EventsList.js
@@ -0,0 +1,29 @@
+// import { useLoaderData } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/23-userouteloaderdata/src/components/EventsList.module.css b/code/23-userouteloaderdata/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/23-userouteloaderdata/src/components/EventsNavigation.js b/code/23-userouteloaderdata/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/23-userouteloaderdata/src/components/EventsNavigation.module.css b/code/23-userouteloaderdata/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/23-userouteloaderdata/src/components/MainNavigation.js b/code/23-userouteloaderdata/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/23-userouteloaderdata/src/components/MainNavigation.module.css b/code/23-userouteloaderdata/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/23-userouteloaderdata/src/components/PageContent.js b/code/23-userouteloaderdata/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/23-userouteloaderdata/src/components/PageContent.module.css b/code/23-userouteloaderdata/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/23-userouteloaderdata/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/23-userouteloaderdata/src/index.css b/code/23-userouteloaderdata/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/23-userouteloaderdata/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/23-userouteloaderdata/src/index.js b/code/23-userouteloaderdata/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/23-userouteloaderdata/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/23-userouteloaderdata/src/pages/EditEvent.js b/code/23-userouteloaderdata/src/pages/EditEvent.js
new file mode 100644
index 0000000000..2475ed3710
--- /dev/null
+++ b/code/23-userouteloaderdata/src/pages/EditEvent.js
@@ -0,0 +1,11 @@
+import { useRouteLoaderData } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function EditEventPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EditEventPage;
diff --git a/code/23-userouteloaderdata/src/pages/Error.js b/code/23-userouteloaderdata/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/23-userouteloaderdata/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/23-userouteloaderdata/src/pages/EventDetail.js b/code/23-userouteloaderdata/src/pages/EventDetail.js
new file mode 100644
index 0000000000..f3b78b6017
--- /dev/null
+++ b/code/23-userouteloaderdata/src/pages/EventDetail.js
@@ -0,0 +1,28 @@
+import { useRouteLoaderData, json } from 'react-router-dom';
+
+import EventItem from '../components/EventItem';
+
+function EventDetailPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EventDetailPage;
+
+export async function loader({ request, params }) {
+ const id = params.eventId;
+
+ const response = await fetch('http://localhost:8080/events/' + id);
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not fetch details for selected event.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/23-userouteloaderdata/src/pages/Events.js b/code/23-userouteloaderdata/src/pages/Events.js
new file mode 100644
index 0000000000..ce4bceb5e2
--- /dev/null
+++ b/code/23-userouteloaderdata/src/pages/Events.js
@@ -0,0 +1,35 @@
+import { useLoaderData, json } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+
+ // if (data.isError) {
+ // return {data.message}
;
+ // }
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/23-userouteloaderdata/src/pages/EventsRoot.js b/code/23-userouteloaderdata/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/23-userouteloaderdata/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/23-userouteloaderdata/src/pages/Home.js b/code/23-userouteloaderdata/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/23-userouteloaderdata/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/23-userouteloaderdata/src/pages/NewEvent.js b/code/23-userouteloaderdata/src/pages/NewEvent.js
new file mode 100644
index 0000000000..189ca3b061
--- /dev/null
+++ b/code/23-userouteloaderdata/src/pages/NewEvent.js
@@ -0,0 +1,5 @@
+function NewEventPage() {
+ return NewEventPage
;
+}
+
+export default NewEventPage;
diff --git a/code/23-userouteloaderdata/src/pages/Root.js b/code/23-userouteloaderdata/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/23-userouteloaderdata/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/24-action-data-submission/package.json b/code/24-action-data-submission/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/24-action-data-submission/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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/24-action-data-submission/public/favicon.ico b/code/24-action-data-submission/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/24-action-data-submission/public/favicon.ico differ
diff --git a/code/24-action-data-submission/public/index.html b/code/24-action-data-submission/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/24-action-data-submission/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/24-action-data-submission/public/logo192.png b/code/24-action-data-submission/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/24-action-data-submission/public/logo192.png differ
diff --git a/code/24-action-data-submission/public/logo512.png b/code/24-action-data-submission/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/24-action-data-submission/public/logo512.png differ
diff --git a/code/24-action-data-submission/public/manifest.json b/code/24-action-data-submission/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/24-action-data-submission/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/24-action-data-submission/public/robots.txt b/code/24-action-data-submission/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/24-action-data-submission/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/24-action-data-submission/src/App.js b/code/24-action-data-submission/src/App.js
new file mode 100644
index 0000000000..892157d196
--- /dev/null
+++ b/code/24-action-data-submission/src/App.js
@@ -0,0 +1,53 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage, { action as newEventAction } from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ id: 'event-detail',
+ loader: eventDetailLoader,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ { path: 'edit', element: },
+ ],
+ },
+ { path: 'new', element: , action: newEventAction },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/24-action-data-submission/src/components/EventForm.js b/code/24-action-data-submission/src/components/EventForm.js
new file mode 100644
index 0000000000..afd31b88f1
--- /dev/null
+++ b/code/24-action-data-submission/src/components/EventForm.js
@@ -0,0 +1,63 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/24-action-data-submission/src/components/EventForm.module.css b/code/24-action-data-submission/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/24-action-data-submission/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/24-action-data-submission/src/components/EventItem.js b/code/24-action-data-submission/src/components/EventItem.js
new file mode 100644
index 0000000000..b68551cea3
--- /dev/null
+++ b/code/24-action-data-submission/src/components/EventItem.js
@@ -0,0 +1,24 @@
+import { Link } from 'react-router-dom';
+
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ function startDeleteHandler() {
+ // ...
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/24-action-data-submission/src/components/EventItem.module.css b/code/24-action-data-submission/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/24-action-data-submission/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/24-action-data-submission/src/components/EventsList.js b/code/24-action-data-submission/src/components/EventsList.js
new file mode 100644
index 0000000000..0488cb0087
--- /dev/null
+++ b/code/24-action-data-submission/src/components/EventsList.js
@@ -0,0 +1,29 @@
+// import { useLoaderData } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/24-action-data-submission/src/components/EventsList.module.css b/code/24-action-data-submission/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/24-action-data-submission/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/24-action-data-submission/src/components/EventsNavigation.js b/code/24-action-data-submission/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/24-action-data-submission/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/24-action-data-submission/src/components/EventsNavigation.module.css b/code/24-action-data-submission/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/24-action-data-submission/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/24-action-data-submission/src/components/MainNavigation.js b/code/24-action-data-submission/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/24-action-data-submission/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/24-action-data-submission/src/components/MainNavigation.module.css b/code/24-action-data-submission/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/24-action-data-submission/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/24-action-data-submission/src/components/PageContent.js b/code/24-action-data-submission/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/24-action-data-submission/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/24-action-data-submission/src/components/PageContent.module.css b/code/24-action-data-submission/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/24-action-data-submission/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/24-action-data-submission/src/index.css b/code/24-action-data-submission/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/24-action-data-submission/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/24-action-data-submission/src/index.js b/code/24-action-data-submission/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/24-action-data-submission/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/24-action-data-submission/src/pages/EditEvent.js b/code/24-action-data-submission/src/pages/EditEvent.js
new file mode 100644
index 0000000000..2475ed3710
--- /dev/null
+++ b/code/24-action-data-submission/src/pages/EditEvent.js
@@ -0,0 +1,11 @@
+import { useRouteLoaderData } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function EditEventPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EditEventPage;
diff --git a/code/24-action-data-submission/src/pages/Error.js b/code/24-action-data-submission/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/24-action-data-submission/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/24-action-data-submission/src/pages/EventDetail.js b/code/24-action-data-submission/src/pages/EventDetail.js
new file mode 100644
index 0000000000..f3b78b6017
--- /dev/null
+++ b/code/24-action-data-submission/src/pages/EventDetail.js
@@ -0,0 +1,28 @@
+import { useRouteLoaderData, json } from 'react-router-dom';
+
+import EventItem from '../components/EventItem';
+
+function EventDetailPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EventDetailPage;
+
+export async function loader({ request, params }) {
+ const id = params.eventId;
+
+ const response = await fetch('http://localhost:8080/events/' + id);
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not fetch details for selected event.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/24-action-data-submission/src/pages/Events.js b/code/24-action-data-submission/src/pages/Events.js
new file mode 100644
index 0000000000..ce4bceb5e2
--- /dev/null
+++ b/code/24-action-data-submission/src/pages/Events.js
@@ -0,0 +1,35 @@
+import { useLoaderData, json } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+
+ // if (data.isError) {
+ // return {data.message}
;
+ // }
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/24-action-data-submission/src/pages/EventsRoot.js b/code/24-action-data-submission/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/24-action-data-submission/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/24-action-data-submission/src/pages/Home.js b/code/24-action-data-submission/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/24-action-data-submission/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/24-action-data-submission/src/pages/NewEvent.js b/code/24-action-data-submission/src/pages/NewEvent.js
new file mode 100644
index 0000000000..258539d62c
--- /dev/null
+++ b/code/24-action-data-submission/src/pages/NewEvent.js
@@ -0,0 +1,34 @@
+import { json, redirect } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function NewEventPage() {
+ return ;
+}
+
+export default NewEventPage;
+
+export async function action({ request, params }) {
+ const data = await request.formData();
+
+ const eventData = {
+ title: data.get('title'),
+ image: data.get('image'),
+ date: data.get('date'),
+ description: data.get('description'),
+ };
+
+ const response = await fetch('http://localhost:8080/events', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(eventData),
+ });
+
+ if (!response.ok) {
+ throw json({ message: 'Could not save event.' }, { status: 500 });
+ }
+
+ return redirect('/events');
+}
diff --git a/code/24-action-data-submission/src/pages/Root.js b/code/24-action-data-submission/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/24-action-data-submission/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/25-deleting/package.json b/code/25-deleting/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/25-deleting/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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/25-deleting/public/favicon.ico b/code/25-deleting/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/25-deleting/public/favicon.ico differ
diff --git a/code/25-deleting/public/index.html b/code/25-deleting/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/25-deleting/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/25-deleting/public/logo192.png b/code/25-deleting/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/25-deleting/public/logo192.png differ
diff --git a/code/25-deleting/public/logo512.png b/code/25-deleting/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/25-deleting/public/logo512.png differ
diff --git a/code/25-deleting/public/manifest.json b/code/25-deleting/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/25-deleting/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/25-deleting/public/robots.txt b/code/25-deleting/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/25-deleting/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/25-deleting/src/App.js b/code/25-deleting/src/App.js
new file mode 100644
index 0000000000..df1933ca93
--- /dev/null
+++ b/code/25-deleting/src/App.js
@@ -0,0 +1,55 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+ action as deleteEventAction,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage, { action as newEventAction } from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ id: 'event-detail',
+ loader: eventDetailLoader,
+ children: [
+ {
+ index: true,
+ element: ,
+ action: deleteEventAction,
+ },
+ { path: 'edit', element: },
+ ],
+ },
+ { path: 'new', element: , action: newEventAction },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/25-deleting/src/components/EventForm.js b/code/25-deleting/src/components/EventForm.js
new file mode 100644
index 0000000000..afd31b88f1
--- /dev/null
+++ b/code/25-deleting/src/components/EventForm.js
@@ -0,0 +1,63 @@
+import { useNavigate } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/25-deleting/src/components/EventForm.module.css b/code/25-deleting/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/25-deleting/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/25-deleting/src/components/EventItem.js b/code/25-deleting/src/components/EventItem.js
new file mode 100644
index 0000000000..f23bb496ee
--- /dev/null
+++ b/code/25-deleting/src/components/EventItem.js
@@ -0,0 +1,30 @@
+import { Link, useSubmit } from 'react-router-dom';
+
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ const submit = useSubmit();
+
+ function startDeleteHandler() {
+ const proceed = window.confirm('Are you sure?');
+
+ if (proceed) {
+ submit(null, { method: 'delete' });
+ }
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/25-deleting/src/components/EventItem.module.css b/code/25-deleting/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/25-deleting/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/25-deleting/src/components/EventsList.js b/code/25-deleting/src/components/EventsList.js
new file mode 100644
index 0000000000..0488cb0087
--- /dev/null
+++ b/code/25-deleting/src/components/EventsList.js
@@ -0,0 +1,29 @@
+// import { useLoaderData } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/25-deleting/src/components/EventsList.module.css b/code/25-deleting/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/25-deleting/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/25-deleting/src/components/EventsNavigation.js b/code/25-deleting/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/25-deleting/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/25-deleting/src/components/EventsNavigation.module.css b/code/25-deleting/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/25-deleting/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/25-deleting/src/components/MainNavigation.js b/code/25-deleting/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/25-deleting/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/25-deleting/src/components/MainNavigation.module.css b/code/25-deleting/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/25-deleting/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/25-deleting/src/components/PageContent.js b/code/25-deleting/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/25-deleting/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/25-deleting/src/components/PageContent.module.css b/code/25-deleting/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/25-deleting/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/25-deleting/src/index.css b/code/25-deleting/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/25-deleting/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/25-deleting/src/index.js b/code/25-deleting/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/25-deleting/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/25-deleting/src/pages/EditEvent.js b/code/25-deleting/src/pages/EditEvent.js
new file mode 100644
index 0000000000..2475ed3710
--- /dev/null
+++ b/code/25-deleting/src/pages/EditEvent.js
@@ -0,0 +1,11 @@
+import { useRouteLoaderData } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function EditEventPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EditEventPage;
diff --git a/code/25-deleting/src/pages/Error.js b/code/25-deleting/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/25-deleting/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/25-deleting/src/pages/EventDetail.js b/code/25-deleting/src/pages/EventDetail.js
new file mode 100644
index 0000000000..65cecde564
--- /dev/null
+++ b/code/25-deleting/src/pages/EventDetail.js
@@ -0,0 +1,45 @@
+import { useRouteLoaderData, json, redirect } from 'react-router-dom';
+
+import EventItem from '../components/EventItem';
+
+function EventDetailPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EventDetailPage;
+
+export async function loader({ request, params }) {
+ const id = params.eventId;
+
+ const response = await fetch('http://localhost:8080/events/' + id);
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not fetch details for selected event.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
+
+export async function action({ params, request }) {
+ const eventId = params.eventId;
+ const response = await fetch('http://localhost:8080/events/' + eventId, {
+ method: request.method,
+ });
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not delete event.' },
+ {
+ status: 500,
+ }
+ );
+ }
+ return redirect('/events');
+}
diff --git a/code/25-deleting/src/pages/Events.js b/code/25-deleting/src/pages/Events.js
new file mode 100644
index 0000000000..ce4bceb5e2
--- /dev/null
+++ b/code/25-deleting/src/pages/Events.js
@@ -0,0 +1,35 @@
+import { useLoaderData, json } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+
+ // if (data.isError) {
+ // return {data.message}
;
+ // }
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/25-deleting/src/pages/EventsRoot.js b/code/25-deleting/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/25-deleting/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/25-deleting/src/pages/Home.js b/code/25-deleting/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/25-deleting/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/25-deleting/src/pages/NewEvent.js b/code/25-deleting/src/pages/NewEvent.js
new file mode 100644
index 0000000000..258539d62c
--- /dev/null
+++ b/code/25-deleting/src/pages/NewEvent.js
@@ -0,0 +1,34 @@
+import { json, redirect } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function NewEventPage() {
+ return ;
+}
+
+export default NewEventPage;
+
+export async function action({ request, params }) {
+ const data = await request.formData();
+
+ const eventData = {
+ title: data.get('title'),
+ image: data.get('image'),
+ date: data.get('date'),
+ description: data.get('description'),
+ };
+
+ const response = await fetch('http://localhost:8080/events', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(eventData),
+ });
+
+ if (!response.ok) {
+ throw json({ message: 'Could not save event.' }, { status: 500 });
+ }
+
+ return redirect('/events');
+}
diff --git a/code/25-deleting/src/pages/Root.js b/code/25-deleting/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/25-deleting/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/26-form-submission-usenavigation/package.json b/code/26-form-submission-usenavigation/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/26-form-submission-usenavigation/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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/26-form-submission-usenavigation/public/favicon.ico b/code/26-form-submission-usenavigation/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/26-form-submission-usenavigation/public/favicon.ico differ
diff --git a/code/26-form-submission-usenavigation/public/index.html b/code/26-form-submission-usenavigation/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/26-form-submission-usenavigation/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/26-form-submission-usenavigation/public/logo192.png b/code/26-form-submission-usenavigation/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/26-form-submission-usenavigation/public/logo192.png differ
diff --git a/code/26-form-submission-usenavigation/public/logo512.png b/code/26-form-submission-usenavigation/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/26-form-submission-usenavigation/public/logo512.png differ
diff --git a/code/26-form-submission-usenavigation/public/manifest.json b/code/26-form-submission-usenavigation/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/26-form-submission-usenavigation/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/26-form-submission-usenavigation/public/robots.txt b/code/26-form-submission-usenavigation/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/26-form-submission-usenavigation/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/26-form-submission-usenavigation/src/App.js b/code/26-form-submission-usenavigation/src/App.js
new file mode 100644
index 0000000000..df1933ca93
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/App.js
@@ -0,0 +1,55 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+ action as deleteEventAction,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage, { action as newEventAction } from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ id: 'event-detail',
+ loader: eventDetailLoader,
+ children: [
+ {
+ index: true,
+ element: ,
+ action: deleteEventAction,
+ },
+ { path: 'edit', element: },
+ ],
+ },
+ { path: 'new', element: , action: newEventAction },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/26-form-submission-usenavigation/src/components/EventForm.js b/code/26-form-submission-usenavigation/src/components/EventForm.js
new file mode 100644
index 0000000000..5eac2fda75
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/EventForm.js
@@ -0,0 +1,69 @@
+import { Form, useNavigate, useNavigation } from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const navigate = useNavigate();
+ const navigation = useNavigation();
+
+ const isSubmitting = navigation.state === 'submitting';
+
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/26-form-submission-usenavigation/src/components/EventForm.module.css b/code/26-form-submission-usenavigation/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/26-form-submission-usenavigation/src/components/EventItem.js b/code/26-form-submission-usenavigation/src/components/EventItem.js
new file mode 100644
index 0000000000..f23bb496ee
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/EventItem.js
@@ -0,0 +1,30 @@
+import { Link, useSubmit } from 'react-router-dom';
+
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ const submit = useSubmit();
+
+ function startDeleteHandler() {
+ const proceed = window.confirm('Are you sure?');
+
+ if (proceed) {
+ submit(null, { method: 'delete' });
+ }
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/26-form-submission-usenavigation/src/components/EventItem.module.css b/code/26-form-submission-usenavigation/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/26-form-submission-usenavigation/src/components/EventsList.js b/code/26-form-submission-usenavigation/src/components/EventsList.js
new file mode 100644
index 0000000000..0488cb0087
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/EventsList.js
@@ -0,0 +1,29 @@
+// import { useLoaderData } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/26-form-submission-usenavigation/src/components/EventsList.module.css b/code/26-form-submission-usenavigation/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/26-form-submission-usenavigation/src/components/EventsNavigation.js b/code/26-form-submission-usenavigation/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/26-form-submission-usenavigation/src/components/EventsNavigation.module.css b/code/26-form-submission-usenavigation/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/26-form-submission-usenavigation/src/components/MainNavigation.js b/code/26-form-submission-usenavigation/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/26-form-submission-usenavigation/src/components/MainNavigation.module.css b/code/26-form-submission-usenavigation/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/26-form-submission-usenavigation/src/components/PageContent.js b/code/26-form-submission-usenavigation/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/26-form-submission-usenavigation/src/components/PageContent.module.css b/code/26-form-submission-usenavigation/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/26-form-submission-usenavigation/src/index.css b/code/26-form-submission-usenavigation/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/26-form-submission-usenavigation/src/index.js b/code/26-form-submission-usenavigation/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/26-form-submission-usenavigation/src/pages/EditEvent.js b/code/26-form-submission-usenavigation/src/pages/EditEvent.js
new file mode 100644
index 0000000000..2475ed3710
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/pages/EditEvent.js
@@ -0,0 +1,11 @@
+import { useRouteLoaderData } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function EditEventPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EditEventPage;
diff --git a/code/26-form-submission-usenavigation/src/pages/Error.js b/code/26-form-submission-usenavigation/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/26-form-submission-usenavigation/src/pages/EventDetail.js b/code/26-form-submission-usenavigation/src/pages/EventDetail.js
new file mode 100644
index 0000000000..65cecde564
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/pages/EventDetail.js
@@ -0,0 +1,45 @@
+import { useRouteLoaderData, json, redirect } from 'react-router-dom';
+
+import EventItem from '../components/EventItem';
+
+function EventDetailPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EventDetailPage;
+
+export async function loader({ request, params }) {
+ const id = params.eventId;
+
+ const response = await fetch('http://localhost:8080/events/' + id);
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not fetch details for selected event.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
+
+export async function action({ params, request }) {
+ const eventId = params.eventId;
+ const response = await fetch('http://localhost:8080/events/' + eventId, {
+ method: request.method,
+ });
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not delete event.' },
+ {
+ status: 500,
+ }
+ );
+ }
+ return redirect('/events');
+}
diff --git a/code/26-form-submission-usenavigation/src/pages/Events.js b/code/26-form-submission-usenavigation/src/pages/Events.js
new file mode 100644
index 0000000000..ce4bceb5e2
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/pages/Events.js
@@ -0,0 +1,35 @@
+import { useLoaderData, json } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+
+ // if (data.isError) {
+ // return {data.message}
;
+ // }
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/26-form-submission-usenavigation/src/pages/EventsRoot.js b/code/26-form-submission-usenavigation/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/26-form-submission-usenavigation/src/pages/Home.js b/code/26-form-submission-usenavigation/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/26-form-submission-usenavigation/src/pages/NewEvent.js b/code/26-form-submission-usenavigation/src/pages/NewEvent.js
new file mode 100644
index 0000000000..258539d62c
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/pages/NewEvent.js
@@ -0,0 +1,34 @@
+import { json, redirect } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function NewEventPage() {
+ return ;
+}
+
+export default NewEventPage;
+
+export async function action({ request, params }) {
+ const data = await request.formData();
+
+ const eventData = {
+ title: data.get('title'),
+ image: data.get('image'),
+ date: data.get('date'),
+ description: data.get('description'),
+ };
+
+ const response = await fetch('http://localhost:8080/events', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(eventData),
+ });
+
+ if (!response.ok) {
+ throw json({ message: 'Could not save event.' }, { status: 500 });
+ }
+
+ return redirect('/events');
+}
diff --git a/code/26-form-submission-usenavigation/src/pages/Root.js b/code/26-form-submission-usenavigation/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/26-form-submission-usenavigation/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/27-validation-returning-data/package.json b/code/27-validation-returning-data/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/27-validation-returning-data/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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/27-validation-returning-data/public/favicon.ico b/code/27-validation-returning-data/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/27-validation-returning-data/public/favicon.ico differ
diff --git a/code/27-validation-returning-data/public/index.html b/code/27-validation-returning-data/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/27-validation-returning-data/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/27-validation-returning-data/public/logo192.png b/code/27-validation-returning-data/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/27-validation-returning-data/public/logo192.png differ
diff --git a/code/27-validation-returning-data/public/logo512.png b/code/27-validation-returning-data/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/27-validation-returning-data/public/logo512.png differ
diff --git a/code/27-validation-returning-data/public/manifest.json b/code/27-validation-returning-data/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/27-validation-returning-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/27-validation-returning-data/public/robots.txt b/code/27-validation-returning-data/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/27-validation-returning-data/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/27-validation-returning-data/src/App.js b/code/27-validation-returning-data/src/App.js
new file mode 100644
index 0000000000..df1933ca93
--- /dev/null
+++ b/code/27-validation-returning-data/src/App.js
@@ -0,0 +1,55 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+ action as deleteEventAction,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage, { action as newEventAction } from './pages/NewEvent';
+import RootLayout from './pages/Root';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ id: 'event-detail',
+ loader: eventDetailLoader,
+ children: [
+ {
+ index: true,
+ element: ,
+ action: deleteEventAction,
+ },
+ { path: 'edit', element: },
+ ],
+ },
+ { path: 'new', element: , action: newEventAction },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/27-validation-returning-data/src/components/EventForm.js b/code/27-validation-returning-data/src/components/EventForm.js
new file mode 100644
index 0000000000..1ea3e4fdb5
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/EventForm.js
@@ -0,0 +1,82 @@
+import {
+ Form,
+ useNavigate,
+ useNavigation,
+ useActionData,
+} from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const data = useActionData();
+ const navigate = useNavigate();
+ const navigation = useNavigation();
+
+ const isSubmitting = navigation.state === 'submitting';
+
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
diff --git a/code/27-validation-returning-data/src/components/EventForm.module.css b/code/27-validation-returning-data/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/27-validation-returning-data/src/components/EventItem.js b/code/27-validation-returning-data/src/components/EventItem.js
new file mode 100644
index 0000000000..f23bb496ee
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/EventItem.js
@@ -0,0 +1,30 @@
+import { Link, useSubmit } from 'react-router-dom';
+
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ const submit = useSubmit();
+
+ function startDeleteHandler() {
+ const proceed = window.confirm('Are you sure?');
+
+ if (proceed) {
+ submit(null, { method: 'delete' });
+ }
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/27-validation-returning-data/src/components/EventItem.module.css b/code/27-validation-returning-data/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/27-validation-returning-data/src/components/EventsList.js b/code/27-validation-returning-data/src/components/EventsList.js
new file mode 100644
index 0000000000..0488cb0087
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/EventsList.js
@@ -0,0 +1,29 @@
+// import { useLoaderData } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/27-validation-returning-data/src/components/EventsList.module.css b/code/27-validation-returning-data/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/27-validation-returning-data/src/components/EventsNavigation.js b/code/27-validation-returning-data/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/27-validation-returning-data/src/components/EventsNavigation.module.css b/code/27-validation-returning-data/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/27-validation-returning-data/src/components/MainNavigation.js b/code/27-validation-returning-data/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/27-validation-returning-data/src/components/MainNavigation.module.css b/code/27-validation-returning-data/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/27-validation-returning-data/src/components/PageContent.js b/code/27-validation-returning-data/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/27-validation-returning-data/src/components/PageContent.module.css b/code/27-validation-returning-data/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/27-validation-returning-data/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/27-validation-returning-data/src/index.css b/code/27-validation-returning-data/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/27-validation-returning-data/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/27-validation-returning-data/src/index.js b/code/27-validation-returning-data/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/27-validation-returning-data/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/27-validation-returning-data/src/pages/EditEvent.js b/code/27-validation-returning-data/src/pages/EditEvent.js
new file mode 100644
index 0000000000..2475ed3710
--- /dev/null
+++ b/code/27-validation-returning-data/src/pages/EditEvent.js
@@ -0,0 +1,11 @@
+import { useRouteLoaderData } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function EditEventPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EditEventPage;
diff --git a/code/27-validation-returning-data/src/pages/Error.js b/code/27-validation-returning-data/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/27-validation-returning-data/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/27-validation-returning-data/src/pages/EventDetail.js b/code/27-validation-returning-data/src/pages/EventDetail.js
new file mode 100644
index 0000000000..65cecde564
--- /dev/null
+++ b/code/27-validation-returning-data/src/pages/EventDetail.js
@@ -0,0 +1,45 @@
+import { useRouteLoaderData, json, redirect } from 'react-router-dom';
+
+import EventItem from '../components/EventItem';
+
+function EventDetailPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EventDetailPage;
+
+export async function loader({ request, params }) {
+ const id = params.eventId;
+
+ const response = await fetch('http://localhost:8080/events/' + id);
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not fetch details for selected event.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
+
+export async function action({ params, request }) {
+ const eventId = params.eventId;
+ const response = await fetch('http://localhost:8080/events/' + eventId, {
+ method: request.method,
+ });
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not delete event.' },
+ {
+ status: 500,
+ }
+ );
+ }
+ return redirect('/events');
+}
diff --git a/code/27-validation-returning-data/src/pages/Events.js b/code/27-validation-returning-data/src/pages/Events.js
new file mode 100644
index 0000000000..ce4bceb5e2
--- /dev/null
+++ b/code/27-validation-returning-data/src/pages/Events.js
@@ -0,0 +1,35 @@
+import { useLoaderData, json } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+
+ // if (data.isError) {
+ // return {data.message}
;
+ // }
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/27-validation-returning-data/src/pages/EventsRoot.js b/code/27-validation-returning-data/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/27-validation-returning-data/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/27-validation-returning-data/src/pages/Home.js b/code/27-validation-returning-data/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/27-validation-returning-data/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/27-validation-returning-data/src/pages/NewEvent.js b/code/27-validation-returning-data/src/pages/NewEvent.js
new file mode 100644
index 0000000000..ccc9d8f075
--- /dev/null
+++ b/code/27-validation-returning-data/src/pages/NewEvent.js
@@ -0,0 +1,38 @@
+import { json, redirect } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function NewEventPage() {
+ return ;
+}
+
+export default NewEventPage;
+
+export async function action({ request, params }) {
+ const data = await request.formData();
+
+ const eventData = {
+ title: data.get('title'),
+ image: data.get('image'),
+ date: data.get('date'),
+ description: data.get('description'),
+ };
+
+ const response = await fetch('http://localhost:8080/events', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(eventData),
+ });
+
+ if (response.status === 422) {
+ return response;
+ }
+
+ if (!response.ok) {
+ throw json({ message: 'Could not save event.' }, { status: 500 });
+ }
+
+ return redirect('/events');
+}
diff --git a/code/27-validation-returning-data/src/pages/Root.js b/code/27-validation-returning-data/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/27-validation-returning-data/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/28-reusing-action/package.json b/code/28-reusing-action/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/28-reusing-action/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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/28-reusing-action/public/favicon.ico b/code/28-reusing-action/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/28-reusing-action/public/favicon.ico differ
diff --git a/code/28-reusing-action/public/index.html b/code/28-reusing-action/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/28-reusing-action/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/28-reusing-action/public/logo192.png b/code/28-reusing-action/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/28-reusing-action/public/logo192.png differ
diff --git a/code/28-reusing-action/public/logo512.png b/code/28-reusing-action/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/28-reusing-action/public/logo512.png differ
diff --git a/code/28-reusing-action/public/manifest.json b/code/28-reusing-action/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/28-reusing-action/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/28-reusing-action/public/robots.txt b/code/28-reusing-action/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/28-reusing-action/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/28-reusing-action/src/App.js b/code/28-reusing-action/src/App.js
new file mode 100644
index 0000000000..38f4e01702
--- /dev/null
+++ b/code/28-reusing-action/src/App.js
@@ -0,0 +1,64 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+ action as deleteEventAction,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+import { action as manipulateEventAction } from './components/EventForm';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ id: 'event-detail',
+ loader: eventDetailLoader,
+ children: [
+ {
+ index: true,
+ element: ,
+ action: deleteEventAction,
+ },
+ {
+ path: 'edit',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ {
+ path: 'new',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/28-reusing-action/src/components/EventForm.js b/code/28-reusing-action/src/components/EventForm.js
new file mode 100644
index 0000000000..4798c3404b
--- /dev/null
+++ b/code/28-reusing-action/src/components/EventForm.js
@@ -0,0 +1,122 @@
+import {
+ Form,
+ useNavigate,
+ useNavigation,
+ useActionData,
+ json,
+ redirect
+} from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const data = useActionData();
+ const navigate = useNavigate();
+ const navigation = useNavigation();
+
+ const isSubmitting = navigation.state === 'submitting';
+
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
+
+export async function action({ request, params }) {
+ const method = request.method;
+ const data = await request.formData();
+
+ const eventData = {
+ title: data.get('title'),
+ image: data.get('image'),
+ date: data.get('date'),
+ description: data.get('description'),
+ };
+
+ let url = 'http://localhost:8080/events';
+
+ if (method === 'PATCH') {
+ const eventId = params.eventId;
+ url = 'http://localhost:8080/events/' + eventId;
+ }
+
+ const response = await fetch(url, {
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(eventData),
+ });
+
+ if (response.status === 422) {
+ return response;
+ }
+
+ if (!response.ok) {
+ throw json({ message: 'Could not save event.' }, { status: 500 });
+ }
+
+ return redirect('/events');
+}
+
diff --git a/code/28-reusing-action/src/components/EventForm.module.css b/code/28-reusing-action/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/28-reusing-action/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/28-reusing-action/src/components/EventItem.js b/code/28-reusing-action/src/components/EventItem.js
new file mode 100644
index 0000000000..f23bb496ee
--- /dev/null
+++ b/code/28-reusing-action/src/components/EventItem.js
@@ -0,0 +1,30 @@
+import { Link, useSubmit } from 'react-router-dom';
+
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ const submit = useSubmit();
+
+ function startDeleteHandler() {
+ const proceed = window.confirm('Are you sure?');
+
+ if (proceed) {
+ submit(null, { method: 'delete' });
+ }
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/28-reusing-action/src/components/EventItem.module.css b/code/28-reusing-action/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/28-reusing-action/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/28-reusing-action/src/components/EventsList.js b/code/28-reusing-action/src/components/EventsList.js
new file mode 100644
index 0000000000..0488cb0087
--- /dev/null
+++ b/code/28-reusing-action/src/components/EventsList.js
@@ -0,0 +1,29 @@
+// import { useLoaderData } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/28-reusing-action/src/components/EventsList.module.css b/code/28-reusing-action/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/28-reusing-action/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/28-reusing-action/src/components/EventsNavigation.js b/code/28-reusing-action/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/28-reusing-action/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/28-reusing-action/src/components/EventsNavigation.module.css b/code/28-reusing-action/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/28-reusing-action/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/28-reusing-action/src/components/MainNavigation.js b/code/28-reusing-action/src/components/MainNavigation.js
new file mode 100644
index 0000000000..e466e69db7
--- /dev/null
+++ b/code/28-reusing-action/src/components/MainNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+
+function MainNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/28-reusing-action/src/components/MainNavigation.module.css b/code/28-reusing-action/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/28-reusing-action/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/28-reusing-action/src/components/PageContent.js b/code/28-reusing-action/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/28-reusing-action/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/28-reusing-action/src/components/PageContent.module.css b/code/28-reusing-action/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/28-reusing-action/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/28-reusing-action/src/index.css b/code/28-reusing-action/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/28-reusing-action/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/28-reusing-action/src/index.js b/code/28-reusing-action/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/28-reusing-action/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/28-reusing-action/src/pages/EditEvent.js b/code/28-reusing-action/src/pages/EditEvent.js
new file mode 100644
index 0000000000..a56d343b72
--- /dev/null
+++ b/code/28-reusing-action/src/pages/EditEvent.js
@@ -0,0 +1,11 @@
+import { useRouteLoaderData } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function EditEventPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EditEventPage;
diff --git a/code/28-reusing-action/src/pages/Error.js b/code/28-reusing-action/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/28-reusing-action/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/28-reusing-action/src/pages/EventDetail.js b/code/28-reusing-action/src/pages/EventDetail.js
new file mode 100644
index 0000000000..65cecde564
--- /dev/null
+++ b/code/28-reusing-action/src/pages/EventDetail.js
@@ -0,0 +1,45 @@
+import { useRouteLoaderData, json, redirect } from 'react-router-dom';
+
+import EventItem from '../components/EventItem';
+
+function EventDetailPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EventDetailPage;
+
+export async function loader({ request, params }) {
+ const id = params.eventId;
+
+ const response = await fetch('http://localhost:8080/events/' + id);
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not fetch details for selected event.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
+
+export async function action({ params, request }) {
+ const eventId = params.eventId;
+ const response = await fetch('http://localhost:8080/events/' + eventId, {
+ method: request.method,
+ });
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not delete event.' },
+ {
+ status: 500,
+ }
+ );
+ }
+ return redirect('/events');
+}
diff --git a/code/28-reusing-action/src/pages/Events.js b/code/28-reusing-action/src/pages/Events.js
new file mode 100644
index 0000000000..ce4bceb5e2
--- /dev/null
+++ b/code/28-reusing-action/src/pages/Events.js
@@ -0,0 +1,35 @@
+import { useLoaderData, json } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+
+ // if (data.isError) {
+ // return {data.message}
;
+ // }
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/28-reusing-action/src/pages/EventsRoot.js b/code/28-reusing-action/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/28-reusing-action/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/28-reusing-action/src/pages/Home.js b/code/28-reusing-action/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/28-reusing-action/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/28-reusing-action/src/pages/NewEvent.js b/code/28-reusing-action/src/pages/NewEvent.js
new file mode 100644
index 0000000000..fd3a22eef9
--- /dev/null
+++ b/code/28-reusing-action/src/pages/NewEvent.js
@@ -0,0 +1,8 @@
+import EventForm from '../components/EventForm';
+
+function NewEventPage() {
+ return ;
+}
+
+export default NewEventPage;
+
diff --git a/code/28-reusing-action/src/pages/Root.js b/code/28-reusing-action/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/28-reusing-action/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/29-prepared-newsletter-code/package.json b/code/29-prepared-newsletter-code/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/29-prepared-newsletter-code/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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/29-prepared-newsletter-code/public/favicon.ico b/code/29-prepared-newsletter-code/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/29-prepared-newsletter-code/public/favicon.ico differ
diff --git a/code/29-prepared-newsletter-code/public/index.html b/code/29-prepared-newsletter-code/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/29-prepared-newsletter-code/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/29-prepared-newsletter-code/public/logo192.png b/code/29-prepared-newsletter-code/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/29-prepared-newsletter-code/public/logo192.png differ
diff --git a/code/29-prepared-newsletter-code/public/logo512.png b/code/29-prepared-newsletter-code/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/29-prepared-newsletter-code/public/logo512.png differ
diff --git a/code/29-prepared-newsletter-code/public/manifest.json b/code/29-prepared-newsletter-code/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/29-prepared-newsletter-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/29-prepared-newsletter-code/public/robots.txt b/code/29-prepared-newsletter-code/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/29-prepared-newsletter-code/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/29-prepared-newsletter-code/src/App.js b/code/29-prepared-newsletter-code/src/App.js
new file mode 100644
index 0000000000..c6bc1eede2
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/App.js
@@ -0,0 +1,70 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+ action as deleteEventAction,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+import { action as manipulateEventAction } from './components/EventForm';
+import NewsletterPage, { action as newsletterAction } from './pages/Newsletter';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ id: 'event-detail',
+ loader: eventDetailLoader,
+ children: [
+ {
+ index: true,
+ element: ,
+ action: deleteEventAction,
+ },
+ {
+ path: 'edit',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ {
+ path: 'new',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ {
+ path: 'newsletter',
+ element: ,
+ action: newsletterAction,
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/29-prepared-newsletter-code/src/components/EventForm.js b/code/29-prepared-newsletter-code/src/components/EventForm.js
new file mode 100644
index 0000000000..4798c3404b
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/EventForm.js
@@ -0,0 +1,122 @@
+import {
+ Form,
+ useNavigate,
+ useNavigation,
+ useActionData,
+ json,
+ redirect
+} from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const data = useActionData();
+ const navigate = useNavigate();
+ const navigation = useNavigation();
+
+ const isSubmitting = navigation.state === 'submitting';
+
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
+
+export async function action({ request, params }) {
+ const method = request.method;
+ const data = await request.formData();
+
+ const eventData = {
+ title: data.get('title'),
+ image: data.get('image'),
+ date: data.get('date'),
+ description: data.get('description'),
+ };
+
+ let url = 'http://localhost:8080/events';
+
+ if (method === 'PATCH') {
+ const eventId = params.eventId;
+ url = 'http://localhost:8080/events/' + eventId;
+ }
+
+ const response = await fetch(url, {
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(eventData),
+ });
+
+ if (response.status === 422) {
+ return response;
+ }
+
+ if (!response.ok) {
+ throw json({ message: 'Could not save event.' }, { status: 500 });
+ }
+
+ return redirect('/events');
+}
+
diff --git a/code/29-prepared-newsletter-code/src/components/EventForm.module.css b/code/29-prepared-newsletter-code/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/29-prepared-newsletter-code/src/components/EventItem.js b/code/29-prepared-newsletter-code/src/components/EventItem.js
new file mode 100644
index 0000000000..f23bb496ee
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/EventItem.js
@@ -0,0 +1,30 @@
+import { Link, useSubmit } from 'react-router-dom';
+
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ const submit = useSubmit();
+
+ function startDeleteHandler() {
+ const proceed = window.confirm('Are you sure?');
+
+ if (proceed) {
+ submit(null, { method: 'delete' });
+ }
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/29-prepared-newsletter-code/src/components/EventItem.module.css b/code/29-prepared-newsletter-code/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/29-prepared-newsletter-code/src/components/EventsList.js b/code/29-prepared-newsletter-code/src/components/EventsList.js
new file mode 100644
index 0000000000..0488cb0087
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/EventsList.js
@@ -0,0 +1,29 @@
+// import { useLoaderData } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/29-prepared-newsletter-code/src/components/EventsList.module.css b/code/29-prepared-newsletter-code/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/29-prepared-newsletter-code/src/components/EventsNavigation.js b/code/29-prepared-newsletter-code/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/29-prepared-newsletter-code/src/components/EventsNavigation.module.css b/code/29-prepared-newsletter-code/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/29-prepared-newsletter-code/src/components/MainNavigation.js b/code/29-prepared-newsletter-code/src/components/MainNavigation.js
new file mode 100644
index 0000000000..57fe43c2dd
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/MainNavigation.js
@@ -0,0 +1,49 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+import NewsletterSignup from './NewsletterSignup';
+
+function MainNavigation() {
+ return (
+
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/29-prepared-newsletter-code/src/components/MainNavigation.module.css b/code/29-prepared-newsletter-code/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/29-prepared-newsletter-code/src/components/NewsletterSignup.js b/code/29-prepared-newsletter-code/src/components/NewsletterSignup.js
new file mode 100644
index 0000000000..3346e0262c
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/NewsletterSignup.js
@@ -0,0 +1,16 @@
+import classes from './NewsletterSignup.module.css';
+
+function NewsletterSignup() {
+ return (
+
+ );
+}
+
+export default NewsletterSignup;
diff --git a/code/29-prepared-newsletter-code/src/components/NewsletterSignup.module.css b/code/29-prepared-newsletter-code/src/components/NewsletterSignup.module.css
new file mode 100644
index 0000000000..035837e3c4
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/NewsletterSignup.module.css
@@ -0,0 +1,18 @@
+.newsletter input,
+.newsletter button {
+ font: inherit;
+ padding: 0.25rem 0.75rem;
+ border-radius: 0;
+ border: none;
+}
+
+.newsletter button {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ cursor: pointer;
+}
+
+.newsletter button:hover {
+ background-color: var(--color-primary-300);
+ color: var(--color-gray-800);
+}
diff --git a/code/29-prepared-newsletter-code/src/components/PageContent.js b/code/29-prepared-newsletter-code/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/29-prepared-newsletter-code/src/components/PageContent.module.css b/code/29-prepared-newsletter-code/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/29-prepared-newsletter-code/src/index.css b/code/29-prepared-newsletter-code/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/29-prepared-newsletter-code/src/index.js b/code/29-prepared-newsletter-code/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/29-prepared-newsletter-code/src/pages/EditEvent.js b/code/29-prepared-newsletter-code/src/pages/EditEvent.js
new file mode 100644
index 0000000000..a56d343b72
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/pages/EditEvent.js
@@ -0,0 +1,11 @@
+import { useRouteLoaderData } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function EditEventPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EditEventPage;
diff --git a/code/29-prepared-newsletter-code/src/pages/Error.js b/code/29-prepared-newsletter-code/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/29-prepared-newsletter-code/src/pages/EventDetail.js b/code/29-prepared-newsletter-code/src/pages/EventDetail.js
new file mode 100644
index 0000000000..65cecde564
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/pages/EventDetail.js
@@ -0,0 +1,45 @@
+import { useRouteLoaderData, json, redirect } from 'react-router-dom';
+
+import EventItem from '../components/EventItem';
+
+function EventDetailPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EventDetailPage;
+
+export async function loader({ request, params }) {
+ const id = params.eventId;
+
+ const response = await fetch('http://localhost:8080/events/' + id);
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not fetch details for selected event.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
+
+export async function action({ params, request }) {
+ const eventId = params.eventId;
+ const response = await fetch('http://localhost:8080/events/' + eventId, {
+ method: request.method,
+ });
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not delete event.' },
+ {
+ status: 500,
+ }
+ );
+ }
+ return redirect('/events');
+}
diff --git a/code/29-prepared-newsletter-code/src/pages/Events.js b/code/29-prepared-newsletter-code/src/pages/Events.js
new file mode 100644
index 0000000000..ce4bceb5e2
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/pages/Events.js
@@ -0,0 +1,35 @@
+import { useLoaderData, json } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+
+ // if (data.isError) {
+ // return {data.message}
;
+ // }
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/29-prepared-newsletter-code/src/pages/EventsRoot.js b/code/29-prepared-newsletter-code/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/29-prepared-newsletter-code/src/pages/Home.js b/code/29-prepared-newsletter-code/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/29-prepared-newsletter-code/src/pages/NewEvent.js b/code/29-prepared-newsletter-code/src/pages/NewEvent.js
new file mode 100644
index 0000000000..fd3a22eef9
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/pages/NewEvent.js
@@ -0,0 +1,8 @@
+import EventForm from '../components/EventForm';
+
+function NewEventPage() {
+ return ;
+}
+
+export default NewEventPage;
+
diff --git a/code/29-prepared-newsletter-code/src/pages/Newsletter.js b/code/29-prepared-newsletter-code/src/pages/Newsletter.js
new file mode 100644
index 0000000000..e92e1675f8
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/pages/Newsletter.js
@@ -0,0 +1,21 @@
+import NewsletterSignup from '../components/NewsletterSignup';
+import PageContent from '../components/PageContent';
+
+function NewsletterPage() {
+ return (
+
+
+
+ );
+}
+
+export default NewsletterPage;
+
+export async function action({ request }) {
+ const data = await request.formData();
+ const email = data.get('email');
+
+ // send to backend newsletter server ...
+ console.log(email);
+ return { message: 'Signup successful!' };
+}
diff --git a/code/29-prepared-newsletter-code/src/pages/Root.js b/code/29-prepared-newsletter-code/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/29-prepared-newsletter-code/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/30-usefetcher/package.json b/code/30-usefetcher/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/30-usefetcher/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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/30-usefetcher/public/favicon.ico b/code/30-usefetcher/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/30-usefetcher/public/favicon.ico differ
diff --git a/code/30-usefetcher/public/index.html b/code/30-usefetcher/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/30-usefetcher/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/30-usefetcher/public/logo192.png b/code/30-usefetcher/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/30-usefetcher/public/logo192.png differ
diff --git a/code/30-usefetcher/public/logo512.png b/code/30-usefetcher/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/30-usefetcher/public/logo512.png differ
diff --git a/code/30-usefetcher/public/manifest.json b/code/30-usefetcher/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/30-usefetcher/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/30-usefetcher/public/robots.txt b/code/30-usefetcher/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/30-usefetcher/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/30-usefetcher/src/App.js b/code/30-usefetcher/src/App.js
new file mode 100644
index 0000000000..c6bc1eede2
--- /dev/null
+++ b/code/30-usefetcher/src/App.js
@@ -0,0 +1,70 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+ action as deleteEventAction,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+import { action as manipulateEventAction } from './components/EventForm';
+import NewsletterPage, { action as newsletterAction } from './pages/Newsletter';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ id: 'event-detail',
+ loader: eventDetailLoader,
+ children: [
+ {
+ index: true,
+ element: ,
+ action: deleteEventAction,
+ },
+ {
+ path: 'edit',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ {
+ path: 'new',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ {
+ path: 'newsletter',
+ element: ,
+ action: newsletterAction,
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/30-usefetcher/src/components/EventForm.js b/code/30-usefetcher/src/components/EventForm.js
new file mode 100644
index 0000000000..4798c3404b
--- /dev/null
+++ b/code/30-usefetcher/src/components/EventForm.js
@@ -0,0 +1,122 @@
+import {
+ Form,
+ useNavigate,
+ useNavigation,
+ useActionData,
+ json,
+ redirect
+} from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const data = useActionData();
+ const navigate = useNavigate();
+ const navigation = useNavigation();
+
+ const isSubmitting = navigation.state === 'submitting';
+
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
+
+export async function action({ request, params }) {
+ const method = request.method;
+ const data = await request.formData();
+
+ const eventData = {
+ title: data.get('title'),
+ image: data.get('image'),
+ date: data.get('date'),
+ description: data.get('description'),
+ };
+
+ let url = 'http://localhost:8080/events';
+
+ if (method === 'PATCH') {
+ const eventId = params.eventId;
+ url = 'http://localhost:8080/events/' + eventId;
+ }
+
+ const response = await fetch(url, {
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(eventData),
+ });
+
+ if (response.status === 422) {
+ return response;
+ }
+
+ if (!response.ok) {
+ throw json({ message: 'Could not save event.' }, { status: 500 });
+ }
+
+ return redirect('/events');
+}
+
diff --git a/code/30-usefetcher/src/components/EventForm.module.css b/code/30-usefetcher/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/30-usefetcher/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/30-usefetcher/src/components/EventItem.js b/code/30-usefetcher/src/components/EventItem.js
new file mode 100644
index 0000000000..f23bb496ee
--- /dev/null
+++ b/code/30-usefetcher/src/components/EventItem.js
@@ -0,0 +1,30 @@
+import { Link, useSubmit } from 'react-router-dom';
+
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ const submit = useSubmit();
+
+ function startDeleteHandler() {
+ const proceed = window.confirm('Are you sure?');
+
+ if (proceed) {
+ submit(null, { method: 'delete' });
+ }
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/30-usefetcher/src/components/EventItem.module.css b/code/30-usefetcher/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/30-usefetcher/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/30-usefetcher/src/components/EventsList.js b/code/30-usefetcher/src/components/EventsList.js
new file mode 100644
index 0000000000..0488cb0087
--- /dev/null
+++ b/code/30-usefetcher/src/components/EventsList.js
@@ -0,0 +1,29 @@
+// import { useLoaderData } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/30-usefetcher/src/components/EventsList.module.css b/code/30-usefetcher/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/30-usefetcher/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/30-usefetcher/src/components/EventsNavigation.js b/code/30-usefetcher/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/30-usefetcher/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/30-usefetcher/src/components/EventsNavigation.module.css b/code/30-usefetcher/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/30-usefetcher/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/30-usefetcher/src/components/MainNavigation.js b/code/30-usefetcher/src/components/MainNavigation.js
new file mode 100644
index 0000000000..57fe43c2dd
--- /dev/null
+++ b/code/30-usefetcher/src/components/MainNavigation.js
@@ -0,0 +1,49 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+import NewsletterSignup from './NewsletterSignup';
+
+function MainNavigation() {
+ return (
+
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/30-usefetcher/src/components/MainNavigation.module.css b/code/30-usefetcher/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/30-usefetcher/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/30-usefetcher/src/components/NewsletterSignup.js b/code/30-usefetcher/src/components/NewsletterSignup.js
new file mode 100644
index 0000000000..095ada39a5
--- /dev/null
+++ b/code/30-usefetcher/src/components/NewsletterSignup.js
@@ -0,0 +1,32 @@
+import { useEffect } from 'react';
+import { useFetcher } from 'react-router-dom';
+
+import classes from './NewsletterSignup.module.css';
+
+function NewsletterSignup() {
+ const fetcher = useFetcher();
+ const { data, state } = fetcher;
+
+ useEffect(() => {
+ if (state === 'idle' && data && data.message) {
+ window.alert(data.message);
+ }
+ }, [data, state]);
+
+ return (
+
+
+
+
+ );
+}
+
+export default NewsletterSignup;
diff --git a/code/30-usefetcher/src/components/NewsletterSignup.module.css b/code/30-usefetcher/src/components/NewsletterSignup.module.css
new file mode 100644
index 0000000000..035837e3c4
--- /dev/null
+++ b/code/30-usefetcher/src/components/NewsletterSignup.module.css
@@ -0,0 +1,18 @@
+.newsletter input,
+.newsletter button {
+ font: inherit;
+ padding: 0.25rem 0.75rem;
+ border-radius: 0;
+ border: none;
+}
+
+.newsletter button {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ cursor: pointer;
+}
+
+.newsletter button:hover {
+ background-color: var(--color-primary-300);
+ color: var(--color-gray-800);
+}
diff --git a/code/30-usefetcher/src/components/PageContent.js b/code/30-usefetcher/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/30-usefetcher/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/30-usefetcher/src/components/PageContent.module.css b/code/30-usefetcher/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/30-usefetcher/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/30-usefetcher/src/index.css b/code/30-usefetcher/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/30-usefetcher/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/30-usefetcher/src/index.js b/code/30-usefetcher/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/30-usefetcher/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/30-usefetcher/src/pages/EditEvent.js b/code/30-usefetcher/src/pages/EditEvent.js
new file mode 100644
index 0000000000..a56d343b72
--- /dev/null
+++ b/code/30-usefetcher/src/pages/EditEvent.js
@@ -0,0 +1,11 @@
+import { useRouteLoaderData } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function EditEventPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EditEventPage;
diff --git a/code/30-usefetcher/src/pages/Error.js b/code/30-usefetcher/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/30-usefetcher/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/30-usefetcher/src/pages/EventDetail.js b/code/30-usefetcher/src/pages/EventDetail.js
new file mode 100644
index 0000000000..65cecde564
--- /dev/null
+++ b/code/30-usefetcher/src/pages/EventDetail.js
@@ -0,0 +1,45 @@
+import { useRouteLoaderData, json, redirect } from 'react-router-dom';
+
+import EventItem from '../components/EventItem';
+
+function EventDetailPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EventDetailPage;
+
+export async function loader({ request, params }) {
+ const id = params.eventId;
+
+ const response = await fetch('http://localhost:8080/events/' + id);
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not fetch details for selected event.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
+
+export async function action({ params, request }) {
+ const eventId = params.eventId;
+ const response = await fetch('http://localhost:8080/events/' + eventId, {
+ method: request.method,
+ });
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not delete event.' },
+ {
+ status: 500,
+ }
+ );
+ }
+ return redirect('/events');
+}
diff --git a/code/30-usefetcher/src/pages/Events.js b/code/30-usefetcher/src/pages/Events.js
new file mode 100644
index 0000000000..ce4bceb5e2
--- /dev/null
+++ b/code/30-usefetcher/src/pages/Events.js
@@ -0,0 +1,35 @@
+import { useLoaderData, json } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const data = useLoaderData();
+
+ // if (data.isError) {
+ // return {data.message}
;
+ // }
+ const events = data.events;
+
+ return ;
+}
+
+export default EventsPage;
+
+export async function loader() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ return response;
+ }
+}
diff --git a/code/30-usefetcher/src/pages/EventsRoot.js b/code/30-usefetcher/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/30-usefetcher/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/30-usefetcher/src/pages/Home.js b/code/30-usefetcher/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/30-usefetcher/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/30-usefetcher/src/pages/NewEvent.js b/code/30-usefetcher/src/pages/NewEvent.js
new file mode 100644
index 0000000000..fd3a22eef9
--- /dev/null
+++ b/code/30-usefetcher/src/pages/NewEvent.js
@@ -0,0 +1,8 @@
+import EventForm from '../components/EventForm';
+
+function NewEventPage() {
+ return ;
+}
+
+export default NewEventPage;
+
diff --git a/code/30-usefetcher/src/pages/Newsletter.js b/code/30-usefetcher/src/pages/Newsletter.js
new file mode 100644
index 0000000000..e92e1675f8
--- /dev/null
+++ b/code/30-usefetcher/src/pages/Newsletter.js
@@ -0,0 +1,21 @@
+import NewsletterSignup from '../components/NewsletterSignup';
+import PageContent from '../components/PageContent';
+
+function NewsletterPage() {
+ return (
+
+
+
+ );
+}
+
+export default NewsletterPage;
+
+export async function action({ request }) {
+ const data = await request.formData();
+ const email = data.get('email');
+
+ // send to backend newsletter server ...
+ console.log(email);
+ return { message: 'Signup successful!' };
+}
diff --git a/code/30-usefetcher/src/pages/Root.js b/code/30-usefetcher/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/30-usefetcher/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/31-defer-introduction/package.json b/code/31-defer-introduction/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/31-defer-introduction/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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/31-defer-introduction/public/favicon.ico b/code/31-defer-introduction/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/31-defer-introduction/public/favicon.ico differ
diff --git a/code/31-defer-introduction/public/index.html b/code/31-defer-introduction/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/31-defer-introduction/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/31-defer-introduction/public/logo192.png b/code/31-defer-introduction/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/31-defer-introduction/public/logo192.png differ
diff --git a/code/31-defer-introduction/public/logo512.png b/code/31-defer-introduction/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/31-defer-introduction/public/logo512.png differ
diff --git a/code/31-defer-introduction/public/manifest.json b/code/31-defer-introduction/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/31-defer-introduction/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/31-defer-introduction/public/robots.txt b/code/31-defer-introduction/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/31-defer-introduction/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/31-defer-introduction/src/App.js b/code/31-defer-introduction/src/App.js
new file mode 100644
index 0000000000..c6bc1eede2
--- /dev/null
+++ b/code/31-defer-introduction/src/App.js
@@ -0,0 +1,70 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+ action as deleteEventAction,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+import { action as manipulateEventAction } from './components/EventForm';
+import NewsletterPage, { action as newsletterAction } from './pages/Newsletter';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ id: 'event-detail',
+ loader: eventDetailLoader,
+ children: [
+ {
+ index: true,
+ element: ,
+ action: deleteEventAction,
+ },
+ {
+ path: 'edit',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ {
+ path: 'new',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ {
+ path: 'newsletter',
+ element: ,
+ action: newsletterAction,
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/31-defer-introduction/src/components/EventForm.js b/code/31-defer-introduction/src/components/EventForm.js
new file mode 100644
index 0000000000..4798c3404b
--- /dev/null
+++ b/code/31-defer-introduction/src/components/EventForm.js
@@ -0,0 +1,122 @@
+import {
+ Form,
+ useNavigate,
+ useNavigation,
+ useActionData,
+ json,
+ redirect
+} from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const data = useActionData();
+ const navigate = useNavigate();
+ const navigation = useNavigation();
+
+ const isSubmitting = navigation.state === 'submitting';
+
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
+
+export async function action({ request, params }) {
+ const method = request.method;
+ const data = await request.formData();
+
+ const eventData = {
+ title: data.get('title'),
+ image: data.get('image'),
+ date: data.get('date'),
+ description: data.get('description'),
+ };
+
+ let url = 'http://localhost:8080/events';
+
+ if (method === 'PATCH') {
+ const eventId = params.eventId;
+ url = 'http://localhost:8080/events/' + eventId;
+ }
+
+ const response = await fetch(url, {
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(eventData),
+ });
+
+ if (response.status === 422) {
+ return response;
+ }
+
+ if (!response.ok) {
+ throw json({ message: 'Could not save event.' }, { status: 500 });
+ }
+
+ return redirect('/events');
+}
+
diff --git a/code/31-defer-introduction/src/components/EventForm.module.css b/code/31-defer-introduction/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/31-defer-introduction/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/31-defer-introduction/src/components/EventItem.js b/code/31-defer-introduction/src/components/EventItem.js
new file mode 100644
index 0000000000..f23bb496ee
--- /dev/null
+++ b/code/31-defer-introduction/src/components/EventItem.js
@@ -0,0 +1,30 @@
+import { Link, useSubmit } from 'react-router-dom';
+
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ const submit = useSubmit();
+
+ function startDeleteHandler() {
+ const proceed = window.confirm('Are you sure?');
+
+ if (proceed) {
+ submit(null, { method: 'delete' });
+ }
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/31-defer-introduction/src/components/EventItem.module.css b/code/31-defer-introduction/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/31-defer-introduction/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/31-defer-introduction/src/components/EventsList.js b/code/31-defer-introduction/src/components/EventsList.js
new file mode 100644
index 0000000000..b9ae25c013
--- /dev/null
+++ b/code/31-defer-introduction/src/components/EventsList.js
@@ -0,0 +1,29 @@
+// import { useLoaderData } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/31-defer-introduction/src/components/EventsList.module.css b/code/31-defer-introduction/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/31-defer-introduction/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/31-defer-introduction/src/components/EventsNavigation.js b/code/31-defer-introduction/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/31-defer-introduction/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/31-defer-introduction/src/components/EventsNavigation.module.css b/code/31-defer-introduction/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/31-defer-introduction/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/31-defer-introduction/src/components/MainNavigation.js b/code/31-defer-introduction/src/components/MainNavigation.js
new file mode 100644
index 0000000000..57fe43c2dd
--- /dev/null
+++ b/code/31-defer-introduction/src/components/MainNavigation.js
@@ -0,0 +1,49 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+import NewsletterSignup from './NewsletterSignup';
+
+function MainNavigation() {
+ return (
+
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/31-defer-introduction/src/components/MainNavigation.module.css b/code/31-defer-introduction/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/31-defer-introduction/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/31-defer-introduction/src/components/NewsletterSignup.js b/code/31-defer-introduction/src/components/NewsletterSignup.js
new file mode 100644
index 0000000000..095ada39a5
--- /dev/null
+++ b/code/31-defer-introduction/src/components/NewsletterSignup.js
@@ -0,0 +1,32 @@
+import { useEffect } from 'react';
+import { useFetcher } from 'react-router-dom';
+
+import classes from './NewsletterSignup.module.css';
+
+function NewsletterSignup() {
+ const fetcher = useFetcher();
+ const { data, state } = fetcher;
+
+ useEffect(() => {
+ if (state === 'idle' && data && data.message) {
+ window.alert(data.message);
+ }
+ }, [data, state]);
+
+ return (
+
+
+
+
+ );
+}
+
+export default NewsletterSignup;
diff --git a/code/31-defer-introduction/src/components/NewsletterSignup.module.css b/code/31-defer-introduction/src/components/NewsletterSignup.module.css
new file mode 100644
index 0000000000..035837e3c4
--- /dev/null
+++ b/code/31-defer-introduction/src/components/NewsletterSignup.module.css
@@ -0,0 +1,18 @@
+.newsletter input,
+.newsletter button {
+ font: inherit;
+ padding: 0.25rem 0.75rem;
+ border-radius: 0;
+ border: none;
+}
+
+.newsletter button {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ cursor: pointer;
+}
+
+.newsletter button:hover {
+ background-color: var(--color-primary-300);
+ color: var(--color-gray-800);
+}
diff --git a/code/31-defer-introduction/src/components/PageContent.js b/code/31-defer-introduction/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/31-defer-introduction/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/31-defer-introduction/src/components/PageContent.module.css b/code/31-defer-introduction/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/31-defer-introduction/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/31-defer-introduction/src/index.css b/code/31-defer-introduction/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/31-defer-introduction/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/31-defer-introduction/src/index.js b/code/31-defer-introduction/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/31-defer-introduction/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/31-defer-introduction/src/pages/EditEvent.js b/code/31-defer-introduction/src/pages/EditEvent.js
new file mode 100644
index 0000000000..a56d343b72
--- /dev/null
+++ b/code/31-defer-introduction/src/pages/EditEvent.js
@@ -0,0 +1,11 @@
+import { useRouteLoaderData } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function EditEventPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EditEventPage;
diff --git a/code/31-defer-introduction/src/pages/Error.js b/code/31-defer-introduction/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/31-defer-introduction/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/31-defer-introduction/src/pages/EventDetail.js b/code/31-defer-introduction/src/pages/EventDetail.js
new file mode 100644
index 0000000000..00036f2890
--- /dev/null
+++ b/code/31-defer-introduction/src/pages/EventDetail.js
@@ -0,0 +1,94 @@
+import { Suspense } from 'react';
+import {
+ useRouteLoaderData,
+ json,
+ redirect,
+ defer,
+ Await,
+} from 'react-router-dom';
+
+import EventItem from '../components/EventItem';
+import EventsList from '../components/EventsList';
+
+function EventDetailPage() {
+ const { event, events } = useRouteLoaderData('event-detail');
+
+ return (
+ <>
+ Loading...}>
+
+ {(loadedEvent) => }
+
+
+ Loading...}>
+
+ {(loadedEvents) => }
+
+
+ >
+ );
+}
+
+export default EventDetailPage;
+
+async function loadEvent(id) {
+ const response = await fetch('http://localhost:8080/events/' + id);
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not fetch details for selected event.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ const resData = await response.json();
+ return resData.event;
+ }
+}
+
+async function loadEvents() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ const resData = await response.json();
+ return resData.events;
+ }
+}
+
+export async function loader({ request, params }) {
+ const id = params.eventId;
+
+ return defer({
+ event: await loadEvent(id),
+ events: loadEvents(),
+ });
+}
+
+export async function action({ params, request }) {
+ const eventId = params.eventId;
+ const response = await fetch('http://localhost:8080/events/' + eventId, {
+ method: request.method,
+ });
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not delete event.' },
+ {
+ status: 500,
+ }
+ );
+ }
+ return redirect('/events');
+}
diff --git a/code/31-defer-introduction/src/pages/Events.js b/code/31-defer-introduction/src/pages/Events.js
new file mode 100644
index 0000000000..8a72baad7d
--- /dev/null
+++ b/code/31-defer-introduction/src/pages/Events.js
@@ -0,0 +1,44 @@
+import { Suspense } from 'react';
+import { useLoaderData, json, defer, Await } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const { events } = useLoaderData();
+
+ return (
+ Loading...}>
+
+ {(loadedEvents) => }
+
+
+ );
+}
+
+export default EventsPage;
+
+async function loadEvents() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ const resData = await response.json();
+ return resData.events;
+ }
+}
+
+export function loader() {
+ return defer({
+ events: loadEvents(),
+ });
+}
diff --git a/code/31-defer-introduction/src/pages/EventsRoot.js b/code/31-defer-introduction/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/31-defer-introduction/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/31-defer-introduction/src/pages/Home.js b/code/31-defer-introduction/src/pages/Home.js
new file mode 100644
index 0000000000..1694a59519
--- /dev/null
+++ b/code/31-defer-introduction/src/pages/Home.js
@@ -0,0 +1,5 @@
+function HomePage() {
+ return HomePage
;
+}
+
+export default HomePage;
diff --git a/code/31-defer-introduction/src/pages/NewEvent.js b/code/31-defer-introduction/src/pages/NewEvent.js
new file mode 100644
index 0000000000..fd3a22eef9
--- /dev/null
+++ b/code/31-defer-introduction/src/pages/NewEvent.js
@@ -0,0 +1,8 @@
+import EventForm from '../components/EventForm';
+
+function NewEventPage() {
+ return ;
+}
+
+export default NewEventPage;
+
diff --git a/code/31-defer-introduction/src/pages/Newsletter.js b/code/31-defer-introduction/src/pages/Newsletter.js
new file mode 100644
index 0000000000..e92e1675f8
--- /dev/null
+++ b/code/31-defer-introduction/src/pages/Newsletter.js
@@ -0,0 +1,21 @@
+import NewsletterSignup from '../components/NewsletterSignup';
+import PageContent from '../components/PageContent';
+
+function NewsletterPage() {
+ return (
+
+
+
+ );
+}
+
+export default NewsletterPage;
+
+export async function action({ request }) {
+ const data = await request.formData();
+ const email = data.get('email');
+
+ // send to backend newsletter server ...
+ console.log(email);
+ return { message: 'Signup successful!' };
+}
diff --git a/code/31-defer-introduction/src/pages/Root.js b/code/31-defer-introduction/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/31-defer-introduction/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/code/32-finished/backend/app.js b/code/32-finished/backend/app.js
new file mode 100644
index 0000000000..be62c0f3cd
--- /dev/null
+++ b/code/32-finished/backend/app.js
@@ -0,0 +1,24 @@
+const bodyParser = require('body-parser');
+const express = require('express');
+
+const eventRoutes = require('./routes/events');
+
+const app = express();
+
+app.use(bodyParser.json());
+app.use((req, res, next) => {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PATCH,DELETE');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
+ next();
+});
+
+app.use('/events', eventRoutes);
+
+app.use((error, req, res, next) => {
+ const status = error.status || 500;
+ const message = error.message || 'Something went wrong.';
+ res.status(status).json({ message: message });
+});
+
+app.listen(8080);
diff --git a/code/32-finished/backend/data/event.js b/code/32-finished/backend/data/event.js
new file mode 100644
index 0000000000..3a5db5ab92
--- /dev/null
+++ b/code/32-finished/backend/data/event.js
@@ -0,0 +1,70 @@
+const fs = require('node:fs/promises');
+
+const { v4: generateId } = require('uuid');
+
+const { NotFoundError } = require('../util/errors');
+
+async function readData() {
+ const data = await fs.readFile('events.json', 'utf8');
+ return JSON.parse(data);
+}
+
+async function writeData(data) {
+ await fs.writeFile('events.json', JSON.stringify(data));
+}
+
+async function getAll() {
+ const storedData = await readData();
+ if (!storedData.events) {
+ throw new NotFoundError('Could not find any events.');
+ }
+ return storedData.events;
+}
+
+async function get(id) {
+ const storedData = await readData();
+ if (!storedData.events || storedData.events.length === 0) {
+ throw new NotFoundError('Could not find any events.');
+ }
+
+ const event = storedData.events.find((ev) => ev.id === id);
+ if (!event) {
+ throw new NotFoundError('Could not find event for id ' + id);
+ }
+
+ return event;
+}
+
+async function add(data) {
+ const storedData = await readData();
+ storedData.events.unshift({ ...data, id: generateId() });
+ await writeData(storedData);
+}
+
+async function replace(id, data) {
+ const storedData = await readData();
+ if (!storedData.events || storedData.events.length === 0) {
+ throw new NotFoundError('Could not find any events.');
+ }
+
+ const index = storedData.events.findIndex((ev) => ev.id === id);
+ if (index < 0) {
+ throw new NotFoundError('Could not find event for id ' + id);
+ }
+
+ storedData.events[index] = { ...data, id };
+
+ await writeData(storedData);
+}
+
+async function remove(id) {
+ const storedData = await readData();
+ const updatedData = storedData.events.filter((ev) => ev.id !== id);
+ await writeData({events: updatedData});
+}
+
+exports.getAll = getAll;
+exports.get = get;
+exports.add = add;
+exports.replace = replace;
+exports.remove = remove;
diff --git a/code/32-finished/backend/events.json b/code/32-finished/backend/events.json
new file mode 100644
index 0000000000..3f458bc3c2
--- /dev/null
+++ b/code/32-finished/backend/events.json
@@ -0,0 +1,11 @@
+{
+ "events": [
+ {
+ "id": "e1",
+ "title": "A dummy event",
+ "date": "2023-02-22",
+ "image": "https://blog.hubspot.de/hubfs/Germany/Blog_images/Optimize_Marketing%20Events%20DACH%202021.jpg",
+ "description": "Join this amazing event and connect with fellow developers."
+ }
+ ]
+}
diff --git a/code/32-finished/backend/package.json b/code/32-finished/backend/package.json
new file mode 100644
index 0000000000..b46c8d32d8
--- /dev/null
+++ b/code/32-finished/backend/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "backend-api",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "start": "node app.js"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "body-parser": "^1.20.0",
+ "express": "^4.18.1",
+ "uuid": "^9.0.0"
+ }
+}
diff --git a/code/32-finished/backend/routes/events.js b/code/32-finished/backend/routes/events.js
new file mode 100644
index 0000000000..d8dbf375fc
--- /dev/null
+++ b/code/32-finished/backend/routes/events.js
@@ -0,0 +1,111 @@
+const express = require('express');
+
+const { getAll, get, add, replace, remove } = require('../data/event');
+const {
+ isValidText,
+ isValidDate,
+ isValidImageUrl,
+} = require('../util/validation');
+
+const router = express.Router();
+
+router.get('/', async (req, res, next) => {
+ try {
+ const events = await getAll();
+ res.json({ events: events });
+ } catch (error) {
+ next(error);
+ }
+});
+
+router.get('/:id', async (req, res, next) => {
+ try {
+ const event = await get(req.params.id);
+ res.json({ event: event });
+ } catch (error) {
+ next(error);
+ }
+});
+
+router.post('/', async (req, res, next) => {
+ const data = req.body;
+
+ let errors = {};
+
+ if (!isValidText(data.title)) {
+ errors.title = 'Invalid title.';
+ }
+
+ if (!isValidText(data.description)) {
+ errors.description = 'Invalid description.';
+ }
+
+ if (!isValidDate(data.date)) {
+ errors.date = 'Invalid date.';
+ }
+
+ if (!isValidImageUrl(data.image)) {
+ errors.image = 'Invalid image.';
+ }
+
+ if (Object.keys(errors).length > 0) {
+ return res.status(422).json({
+ message: 'Adding the event failed due to validation errors.',
+ errors,
+ });
+ }
+
+ try {
+ await add(data);
+ res.status(201).json({ message: 'Event saved.', event: data });
+ } catch (error) {
+ next(error);
+ }
+});
+
+router.patch('/:id', async (req, res, next) => {
+ const data = req.body;
+
+ let errors = {};
+
+ if (!isValidText(data.title)) {
+ errors.title = 'Invalid title.';
+ }
+
+ if (!isValidText(data.description)) {
+ errors.description = 'Invalid description.';
+ }
+
+ if (!isValidDate(data.date)) {
+ errors.date = 'Invalid date.';
+ }
+
+ if (!isValidImageUrl(data.image)) {
+ errors.image = 'Invalid image.';
+ }
+
+ if (Object.keys(errors).length > 0) {
+ return res.status(422).json({
+ message: 'Updating the event failed due to validation errors.',
+ errors,
+ });
+ }
+
+ try {
+ await replace(req.params.id, data);
+ res.json({ message: 'Event updated.', event: data });
+ } catch (error) {
+ next(error);
+ }
+});
+
+router.delete('/:id', async (req, res, next) => {
+ try {
+ await remove(req.params.id);
+ res.json({ message: 'Event deleted.' });
+ } catch (error) {
+ next(error);
+ }
+});
+
+module.exports = router;
diff --git a/code/32-finished/backend/util/errors.js b/code/32-finished/backend/util/errors.js
new file mode 100644
index 0000000000..db9cf9d805
--- /dev/null
+++ b/code/32-finished/backend/util/errors.js
@@ -0,0 +1,8 @@
+class NotFoundError {
+ constructor(message) {
+ this.message = message;
+ this.status = 404;
+ }
+}
+
+exports.NotFoundError = NotFoundError;
\ No newline at end of file
diff --git a/code/32-finished/backend/util/validation.js b/code/32-finished/backend/util/validation.js
new file mode 100644
index 0000000000..c2ddbabd9f
--- /dev/null
+++ b/code/32-finished/backend/util/validation.js
@@ -0,0 +1,16 @@
+function isValidText(value) {
+ return value && value.trim().length > 0;
+}
+
+function isValidDate(value) {
+ const date = new Date(value);
+ return value && date !== 'Invalid Date';
+}
+
+function isValidImageUrl(value) {
+ return value && value.startsWith('http');
+}
+
+exports.isValidText = isValidText;
+exports.isValidDate = isValidDate;
+exports.isValidImageUrl = isValidImageUrl;
diff --git a/code/32-finished/frontend/package.json b/code/32-finished/frontend/package.json
new file mode 100644
index 0000000000..eafe171a6b
--- /dev/null
+++ b/code/32-finished/frontend/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "react-complete-guide",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.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/32-finished/frontend/public/favicon.ico b/code/32-finished/frontend/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/code/32-finished/frontend/public/favicon.ico differ
diff --git a/code/32-finished/frontend/public/index.html b/code/32-finished/frontend/public/index.html
new file mode 100644
index 0000000000..aa069f27cb
--- /dev/null
+++ b/code/32-finished/frontend/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/code/32-finished/frontend/public/logo192.png b/code/32-finished/frontend/public/logo192.png
new file mode 100644
index 0000000000..fc44b0a379
Binary files /dev/null and b/code/32-finished/frontend/public/logo192.png differ
diff --git a/code/32-finished/frontend/public/logo512.png b/code/32-finished/frontend/public/logo512.png
new file mode 100644
index 0000000000..a4e47a6545
Binary files /dev/null and b/code/32-finished/frontend/public/logo512.png differ
diff --git a/code/32-finished/frontend/public/manifest.json b/code/32-finished/frontend/public/manifest.json
new file mode 100644
index 0000000000..080d6c77ac
--- /dev/null
+++ b/code/32-finished/frontend/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/32-finished/frontend/public/robots.txt b/code/32-finished/frontend/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/code/32-finished/frontend/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/code/32-finished/frontend/src/App.js b/code/32-finished/frontend/src/App.js
new file mode 100644
index 0000000000..c6bc1eede2
--- /dev/null
+++ b/code/32-finished/frontend/src/App.js
@@ -0,0 +1,70 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+ action as deleteEventAction,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+import { action as manipulateEventAction } from './components/EventForm';
+import NewsletterPage, { action as newsletterAction } from './pages/Newsletter';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ id: 'event-detail',
+ loader: eventDetailLoader,
+ children: [
+ {
+ index: true,
+ element: ,
+ action: deleteEventAction,
+ },
+ {
+ path: 'edit',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ {
+ path: 'new',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ {
+ path: 'newsletter',
+ element: ,
+ action: newsletterAction,
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/code/32-finished/frontend/src/components/EventForm.js b/code/32-finished/frontend/src/components/EventForm.js
new file mode 100644
index 0000000000..4798c3404b
--- /dev/null
+++ b/code/32-finished/frontend/src/components/EventForm.js
@@ -0,0 +1,122 @@
+import {
+ Form,
+ useNavigate,
+ useNavigation,
+ useActionData,
+ json,
+ redirect
+} from 'react-router-dom';
+
+import classes from './EventForm.module.css';
+
+function EventForm({ method, event }) {
+ const data = useActionData();
+ const navigate = useNavigate();
+ const navigation = useNavigation();
+
+ const isSubmitting = navigation.state === 'submitting';
+
+ function cancelHandler() {
+ navigate('..');
+ }
+
+ return (
+
+ );
+}
+
+export default EventForm;
+
+export async function action({ request, params }) {
+ const method = request.method;
+ const data = await request.formData();
+
+ const eventData = {
+ title: data.get('title'),
+ image: data.get('image'),
+ date: data.get('date'),
+ description: data.get('description'),
+ };
+
+ let url = 'http://localhost:8080/events';
+
+ if (method === 'PATCH') {
+ const eventId = params.eventId;
+ url = 'http://localhost:8080/events/' + eventId;
+ }
+
+ const response = await fetch(url, {
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(eventData),
+ });
+
+ if (response.status === 422) {
+ return response;
+ }
+
+ if (!response.ok) {
+ throw json({ message: 'Could not save event.' }, { status: 500 });
+ }
+
+ return redirect('/events');
+}
+
diff --git a/code/32-finished/frontend/src/components/EventForm.module.css b/code/32-finished/frontend/src/components/EventForm.module.css
new file mode 100644
index 0000000000..01931bd559
--- /dev/null
+++ b/code/32-finished/frontend/src/components/EventForm.module.css
@@ -0,0 +1,46 @@
+.form {
+ max-width: 40rem;
+ margin: 2rem auto;
+}
+
+.form label,
+.form input,
+.form textarea {
+ display: block;
+ width: 100%;
+}
+
+.form input,
+.form textarea {
+ font: inherit;
+ padding: 0.25rem;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.actions button {
+ font: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ background-color: var(--color-gray-300);
+ color: var(--color-gray-800);
+ border: none;
+}
+
+.actions button[type='button'] {
+ background-color: transparent;
+ color: var(--color-gray-300);
+}
+
+.actions button:hover {
+ background-color: var(--color-primary-300);
+}
+
+.actions button[type='button']:hover {
+ background-color: var(--color-gray-800);
+}
diff --git a/code/32-finished/frontend/src/components/EventItem.js b/code/32-finished/frontend/src/components/EventItem.js
new file mode 100644
index 0000000000..f23bb496ee
--- /dev/null
+++ b/code/32-finished/frontend/src/components/EventItem.js
@@ -0,0 +1,30 @@
+import { Link, useSubmit } from 'react-router-dom';
+
+import classes from './EventItem.module.css';
+
+function EventItem({ event }) {
+ const submit = useSubmit();
+
+ function startDeleteHandler() {
+ const proceed = window.confirm('Are you sure?');
+
+ if (proceed) {
+ submit(null, { method: 'delete' });
+ }
+ }
+
+ return (
+
+
+ {event.title}
+
+ {event.description}
+
+
+ );
+}
+
+export default EventItem;
diff --git a/code/32-finished/frontend/src/components/EventItem.module.css b/code/32-finished/frontend/src/components/EventItem.module.css
new file mode 100644
index 0000000000..f1b15f486d
--- /dev/null
+++ b/code/32-finished/frontend/src/components/EventItem.module.css
@@ -0,0 +1,32 @@
+.event {
+ max-width: 50rem;
+ margin: 2rem auto;
+ text-align: center;
+}
+
+.event img {
+ width: 30rem;
+ border-radius: 4px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+}
+
+.actions a,
+.actions button {
+ padding: 0.25rem 1rem;
+ text-decoration: none;
+ font: inherit;
+ cursor: pointer;
+}
+
+.actions button {
+ background-color: transparent;
+ border: none;
+ color: var(--color-primary-800);
+}
diff --git a/code/32-finished/frontend/src/components/EventsList.js b/code/32-finished/frontend/src/components/EventsList.js
new file mode 100644
index 0000000000..b9ae25c013
--- /dev/null
+++ b/code/32-finished/frontend/src/components/EventsList.js
@@ -0,0 +1,29 @@
+// import { useLoaderData } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+
+import classes from './EventsList.module.css';
+
+function EventsList({events}) {
+ // const events = useLoaderData();
+
+ return (
+
+ );
+}
+
+export default EventsList;
diff --git a/code/32-finished/frontend/src/components/EventsList.module.css b/code/32-finished/frontend/src/components/EventsList.module.css
new file mode 100644
index 0000000000..2dbb173f99
--- /dev/null
+++ b/code/32-finished/frontend/src/components/EventsList.module.css
@@ -0,0 +1,36 @@
+.events {
+ margin: 2rem auto;
+ max-width: 40rem;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.item a {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ background-color: var(--color-gray-800);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.item a:hover {
+ transform: scale(1.02);
+ background-color: var(--color-gray-700);
+}
+
+.item img {
+ width: 33%;
+ object-fit: cover;
+}
+
+.content {
+ padding: 1rem;
+}
+.item h2 {
+ margin: 0 0 1rem 0;
+}
diff --git a/code/32-finished/frontend/src/components/EventsNavigation.js b/code/32-finished/frontend/src/components/EventsNavigation.js
new file mode 100644
index 0000000000..02cad548af
--- /dev/null
+++ b/code/32-finished/frontend/src/components/EventsNavigation.js
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './EventsNavigation.module.css';
+
+function EventsNavigation() {
+ return (
+
+
+
+ );
+}
+
+export default EventsNavigation;
diff --git a/code/32-finished/frontend/src/components/EventsNavigation.module.css b/code/32-finished/frontend/src/components/EventsNavigation.module.css
new file mode 100644
index 0000000000..13ceb7c7c8
--- /dev/null
+++ b/code/32-finished/frontend/src/components/EventsNavigation.module.css
@@ -0,0 +1,24 @@
+.header {
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ background-color: var(--color-gray-500);
+ color: var(--color-gray-900);
+ padding: 0.5rem 1.5rem;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.list a:hover,
+.list a:active,
+.list a.active {
+ background-color: var(--color-primary-600);
+}
diff --git a/code/32-finished/frontend/src/components/MainNavigation.js b/code/32-finished/frontend/src/components/MainNavigation.js
new file mode 100644
index 0000000000..57fe43c2dd
--- /dev/null
+++ b/code/32-finished/frontend/src/components/MainNavigation.js
@@ -0,0 +1,49 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+import NewsletterSignup from './NewsletterSignup';
+
+function MainNavigation() {
+ return (
+
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/code/32-finished/frontend/src/components/MainNavigation.module.css b/code/32-finished/frontend/src/components/MainNavigation.module.css
new file mode 100644
index 0000000000..c4dc654c3e
--- /dev/null
+++ b/code/32-finished/frontend/src/components/MainNavigation.module.css
@@ -0,0 +1,22 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+}
diff --git a/code/32-finished/frontend/src/components/NewsletterSignup.js b/code/32-finished/frontend/src/components/NewsletterSignup.js
new file mode 100644
index 0000000000..095ada39a5
--- /dev/null
+++ b/code/32-finished/frontend/src/components/NewsletterSignup.js
@@ -0,0 +1,32 @@
+import { useEffect } from 'react';
+import { useFetcher } from 'react-router-dom';
+
+import classes from './NewsletterSignup.module.css';
+
+function NewsletterSignup() {
+ const fetcher = useFetcher();
+ const { data, state } = fetcher;
+
+ useEffect(() => {
+ if (state === 'idle' && data && data.message) {
+ window.alert(data.message);
+ }
+ }, [data, state]);
+
+ return (
+
+
+
+
+ );
+}
+
+export default NewsletterSignup;
diff --git a/code/32-finished/frontend/src/components/NewsletterSignup.module.css b/code/32-finished/frontend/src/components/NewsletterSignup.module.css
new file mode 100644
index 0000000000..035837e3c4
--- /dev/null
+++ b/code/32-finished/frontend/src/components/NewsletterSignup.module.css
@@ -0,0 +1,18 @@
+.newsletter input,
+.newsletter button {
+ font: inherit;
+ padding: 0.25rem 0.75rem;
+ border-radius: 0;
+ border: none;
+}
+
+.newsletter button {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ cursor: pointer;
+}
+
+.newsletter button:hover {
+ background-color: var(--color-primary-300);
+ color: var(--color-gray-800);
+}
diff --git a/code/32-finished/frontend/src/components/PageContent.js b/code/32-finished/frontend/src/components/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/code/32-finished/frontend/src/components/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/code/32-finished/frontend/src/components/PageContent.module.css b/code/32-finished/frontend/src/components/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/code/32-finished/frontend/src/components/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/code/32-finished/frontend/src/index.css b/code/32-finished/frontend/src/index.css
new file mode 100644
index 0000000000..70bfa7fb2a
--- /dev/null
+++ b/code/32-finished/frontend/src/index.css
@@ -0,0 +1,50 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ --color-gray-100: #f4f3f1;
+ --color-gray-200: #dddbd8;
+ --color-gray-300: #ccc9c6;
+ --color-gray-400: #aeaba7;
+ --color-gray-500: #8a8784;
+ --color-gray-600: #656360;
+ --color-gray-700: #4b4a47;
+ --color-gray-800: #31302e;
+ --color-gray-900: #1f1d1b;
+
+ --color-primary-100: #fcf3e1;
+ --color-primary-200: #fceccd;
+ --color-primary-300: #fae1af;
+ --color-primary-400: #fbd997;
+ --color-primary-500: #ffd37c;
+ --color-primary-600: #f9c762;
+ --color-primary-700: #fbc14d;
+ --color-primary-800: #fab833;
+ --color-primary-900: #f6ad1b;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
diff --git a/code/32-finished/frontend/src/index.js b/code/32-finished/frontend/src/index.js
new file mode 100644
index 0000000000..09de846062
--- /dev/null
+++ b/code/32-finished/frontend/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+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/32-finished/frontend/src/pages/EditEvent.js b/code/32-finished/frontend/src/pages/EditEvent.js
new file mode 100644
index 0000000000..a56d343b72
--- /dev/null
+++ b/code/32-finished/frontend/src/pages/EditEvent.js
@@ -0,0 +1,11 @@
+import { useRouteLoaderData } from 'react-router-dom';
+
+import EventForm from '../components/EventForm';
+
+function EditEventPage() {
+ const data = useRouteLoaderData('event-detail');
+
+ return ;
+}
+
+export default EditEventPage;
diff --git a/code/32-finished/frontend/src/pages/Error.js b/code/32-finished/frontend/src/pages/Error.js
new file mode 100644
index 0000000000..9cbc56f8f2
--- /dev/null
+++ b/code/32-finished/frontend/src/pages/Error.js
@@ -0,0 +1,31 @@
+import { useRouteError } from 'react-router-dom';
+import MainNavigation from '../components/MainNavigation';
+
+import PageContent from '../components/PageContent';
+
+function ErrorPage() {
+ const error = useRouteError();
+
+ let title = 'An error occurred!';
+ let message = 'Something went wrong!';
+
+ if (error.status === 500) {
+ message = error.data.message;
+ }
+
+ if (error.status === 404) {
+ title = 'Not found!';
+ message = 'Could not find resource or page.';
+ }
+
+ return (
+ <>
+
+
+ {message}
+
+ >
+ );
+}
+
+export default ErrorPage;
diff --git a/code/32-finished/frontend/src/pages/EventDetail.js b/code/32-finished/frontend/src/pages/EventDetail.js
new file mode 100644
index 0000000000..00036f2890
--- /dev/null
+++ b/code/32-finished/frontend/src/pages/EventDetail.js
@@ -0,0 +1,94 @@
+import { Suspense } from 'react';
+import {
+ useRouteLoaderData,
+ json,
+ redirect,
+ defer,
+ Await,
+} from 'react-router-dom';
+
+import EventItem from '../components/EventItem';
+import EventsList from '../components/EventsList';
+
+function EventDetailPage() {
+ const { event, events } = useRouteLoaderData('event-detail');
+
+ return (
+ <>
+ Loading...}>
+
+ {(loadedEvent) => }
+
+
+ Loading...}>
+
+ {(loadedEvents) => }
+
+
+ >
+ );
+}
+
+export default EventDetailPage;
+
+async function loadEvent(id) {
+ const response = await fetch('http://localhost:8080/events/' + id);
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not fetch details for selected event.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ const resData = await response.json();
+ return resData.event;
+ }
+}
+
+async function loadEvents() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ const resData = await response.json();
+ return resData.events;
+ }
+}
+
+export async function loader({ request, params }) {
+ const id = params.eventId;
+
+ return defer({
+ event: await loadEvent(id),
+ events: loadEvents(),
+ });
+}
+
+export async function action({ params, request }) {
+ const eventId = params.eventId;
+ const response = await fetch('http://localhost:8080/events/' + eventId, {
+ method: request.method,
+ });
+
+ if (!response.ok) {
+ throw json(
+ { message: 'Could not delete event.' },
+ {
+ status: 500,
+ }
+ );
+ }
+ return redirect('/events');
+}
diff --git a/code/32-finished/frontend/src/pages/Events.js b/code/32-finished/frontend/src/pages/Events.js
new file mode 100644
index 0000000000..8a72baad7d
--- /dev/null
+++ b/code/32-finished/frontend/src/pages/Events.js
@@ -0,0 +1,44 @@
+import { Suspense } from 'react';
+import { useLoaderData, json, defer, Await } from 'react-router-dom';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const { events } = useLoaderData();
+
+ return (
+ Loading...}>
+
+ {(loadedEvents) => }
+
+
+ );
+}
+
+export default EventsPage;
+
+async function loadEvents() {
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ // return { isError: true, message: 'Could not fetch events.' };
+ // throw new Response(JSON.stringify({ message: 'Could not fetch events.' }), {
+ // status: 500,
+ // });
+ throw json(
+ { message: 'Could not fetch events.' },
+ {
+ status: 500,
+ }
+ );
+ } else {
+ const resData = await response.json();
+ return resData.events;
+ }
+}
+
+export function loader() {
+ return defer({
+ events: loadEvents(),
+ });
+}
diff --git a/code/32-finished/frontend/src/pages/EventsRoot.js b/code/32-finished/frontend/src/pages/EventsRoot.js
new file mode 100644
index 0000000000..67fcbcc02f
--- /dev/null
+++ b/code/32-finished/frontend/src/pages/EventsRoot.js
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+
+import EventsNavigation from '../components/EventsNavigation';
+
+function EventsRootLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default EventsRootLayout;
diff --git a/code/32-finished/frontend/src/pages/Home.js b/code/32-finished/frontend/src/pages/Home.js
new file mode 100644
index 0000000000..96224767fc
--- /dev/null
+++ b/code/32-finished/frontend/src/pages/Home.js
@@ -0,0 +1,11 @@
+import PageContent from '../components/PageContent';
+
+function HomePage() {
+ return (
+
+ Browse all our amazing events!
+
+ );
+}
+
+export default HomePage;
diff --git a/code/32-finished/frontend/src/pages/NewEvent.js b/code/32-finished/frontend/src/pages/NewEvent.js
new file mode 100644
index 0000000000..fd3a22eef9
--- /dev/null
+++ b/code/32-finished/frontend/src/pages/NewEvent.js
@@ -0,0 +1,8 @@
+import EventForm from '../components/EventForm';
+
+function NewEventPage() {
+ return ;
+}
+
+export default NewEventPage;
+
diff --git a/code/32-finished/frontend/src/pages/Newsletter.js b/code/32-finished/frontend/src/pages/Newsletter.js
new file mode 100644
index 0000000000..e92e1675f8
--- /dev/null
+++ b/code/32-finished/frontend/src/pages/Newsletter.js
@@ -0,0 +1,21 @@
+import NewsletterSignup from '../components/NewsletterSignup';
+import PageContent from '../components/PageContent';
+
+function NewsletterPage() {
+ return (
+
+
+
+ );
+}
+
+export default NewsletterPage;
+
+export async function action({ request }) {
+ const data = await request.formData();
+ const email = data.get('email');
+
+ // send to backend newsletter server ...
+ console.log(email);
+ return { message: 'Signup successful!' };
+}
diff --git a/code/32-finished/frontend/src/pages/Root.js b/code/32-finished/frontend/src/pages/Root.js
new file mode 100644
index 0000000000..7854396414
--- /dev/null
+++ b/code/32-finished/frontend/src/pages/Root.js
@@ -0,0 +1,19 @@
+import { Outlet, useNavigation } from 'react-router-dom';
+
+import MainNavigation from '../components/MainNavigation';
+
+function RootLayout() {
+ // const navigation = useNavigation();
+
+ return (
+ <>
+
+
+ {/* {navigation.state === 'loading' && Loading...
} */}
+
+
+ >
+ );
+}
+
+export default RootLayout;
diff --git a/extra-files/01-starting-project.zip b/extra-files/01-starting-project.zip
new file mode 100644
index 0000000000..fc4cbc84ac
Binary files /dev/null and b/extra-files/01-starting-project.zip differ
diff --git a/extra-files/12-adv-starting-project.zip b/extra-files/12-adv-starting-project.zip
new file mode 100644
index 0000000000..3603f37ee6
Binary files /dev/null and b/extra-files/12-adv-starting-project.zip differ
diff --git a/extra-files/App.js b/extra-files/App.js
new file mode 100644
index 0000000000..c6bc1eede2
--- /dev/null
+++ b/extra-files/App.js
@@ -0,0 +1,70 @@
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+
+import EditEventPage from './pages/EditEvent';
+import ErrorPage from './pages/Error';
+import EventDetailPage, {
+ loader as eventDetailLoader,
+ action as deleteEventAction,
+} from './pages/EventDetail';
+import EventsPage, { loader as eventsLoader } from './pages/Events';
+import EventsRootLayout from './pages/EventsRoot';
+import HomePage from './pages/Home';
+import NewEventPage from './pages/NewEvent';
+import RootLayout from './pages/Root';
+import { action as manipulateEventAction } from './components/EventForm';
+import NewsletterPage, { action as newsletterAction } from './pages/Newsletter';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: 'events',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: eventsLoader,
+ },
+ {
+ path: ':eventId',
+ id: 'event-detail',
+ loader: eventDetailLoader,
+ children: [
+ {
+ index: true,
+ element: ,
+ action: deleteEventAction,
+ },
+ {
+ path: 'edit',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ {
+ path: 'new',
+ element: ,
+ action: manipulateEventAction,
+ },
+ ],
+ },
+ {
+ path: 'newsletter',
+ element: ,
+ action: newsletterAction,
+ },
+ ],
+ },
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/extra-files/Events.js b/extra-files/Events.js
new file mode 100644
index 0000000000..d8eaf25c05
--- /dev/null
+++ b/extra-files/Events.js
@@ -0,0 +1,37 @@
+import { useEffect, useState } from 'react';
+
+import EventsList from '../components/EventsList';
+
+function EventsPage() {
+ const [isLoading, setIsLoading] = useState(false);
+ const [fetchedEvents, setFetchedEvents] = useState();
+ const [error, setError] = useState();
+
+ useEffect(() => {
+ async function fetchEvents() {
+ setIsLoading(true);
+ const response = await fetch('http://localhost:8080/events');
+
+ if (!response.ok) {
+ setError('Fetching events failed.');
+ } else {
+ const resData = await response.json();
+ setFetchedEvents(resData.events);
+ }
+ setIsLoading(false);
+ }
+
+ fetchEvents();
+ }, []);
+ return (
+ <>
+
+ {isLoading &&
Loading...
}
+ {error &&
{error}
}
+
+ {!isLoading && fetchedEvents && }
+ >
+ );
+}
+
+export default EventsPage;
diff --git a/extra-files/MainNavigation.js b/extra-files/MainNavigation.js
new file mode 100644
index 0000000000..57fe43c2dd
--- /dev/null
+++ b/extra-files/MainNavigation.js
@@ -0,0 +1,49 @@
+import { NavLink } from 'react-router-dom';
+
+import classes from './MainNavigation.module.css';
+import NewsletterSignup from './NewsletterSignup';
+
+function MainNavigation() {
+ return (
+
+
+
+
+ );
+}
+
+export default MainNavigation;
diff --git a/extra-files/MainNavigation.module.css b/extra-files/MainNavigation.module.css
new file mode 100644
index 0000000000..cb0d5689ea
--- /dev/null
+++ b/extra-files/MainNavigation.module.css
@@ -0,0 +1,23 @@
+.header {
+ max-width: 60rem;
+ margin: auto;
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+}
+
+.list {
+ display: flex;
+ gap: 1rem;
+}
+
+.list a {
+ text-decoration: none;
+ color: var(--color-primary-400);
+}
+
+.list a:hover,
+.list a.active {
+ color: var(--color-primary-800);
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/extra-files/Newsletter.js b/extra-files/Newsletter.js
new file mode 100644
index 0000000000..e92e1675f8
--- /dev/null
+++ b/extra-files/Newsletter.js
@@ -0,0 +1,21 @@
+import NewsletterSignup from '../components/NewsletterSignup';
+import PageContent from '../components/PageContent';
+
+function NewsletterPage() {
+ return (
+
+
+
+ );
+}
+
+export default NewsletterPage;
+
+export async function action({ request }) {
+ const data = await request.formData();
+ const email = data.get('email');
+
+ // send to backend newsletter server ...
+ console.log(email);
+ return { message: 'Signup successful!' };
+}
diff --git a/extra-files/NewsletterSignup.js b/extra-files/NewsletterSignup.js
new file mode 100644
index 0000000000..1a22296f3a
--- /dev/null
+++ b/extra-files/NewsletterSignup.js
@@ -0,0 +1,17 @@
+import classes from './NewsletterSignup.module.css';
+
+function NewsletterSignup() {
+ return (
+
+ );
+}
+
+export default NewsletterSignup;
diff --git a/extra-files/NewsletterSignup.module.css b/extra-files/NewsletterSignup.module.css
new file mode 100644
index 0000000000..035837e3c4
--- /dev/null
+++ b/extra-files/NewsletterSignup.module.css
@@ -0,0 +1,18 @@
+.newsletter input,
+.newsletter button {
+ font: inherit;
+ padding: 0.25rem 0.75rem;
+ border-radius: 0;
+ border: none;
+}
+
+.newsletter button {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ cursor: pointer;
+}
+
+.newsletter button:hover {
+ background-color: var(--color-primary-300);
+ color: var(--color-gray-800);
+}
diff --git a/extra-files/PageContent.js b/extra-files/PageContent.js
new file mode 100644
index 0000000000..e0f0675198
--- /dev/null
+++ b/extra-files/PageContent.js
@@ -0,0 +1,12 @@
+import classes from './PageContent.module.css';
+
+function PageContent({ title, children }) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+export default PageContent;
diff --git a/extra-files/PageContent.module.css b/extra-files/PageContent.module.css
new file mode 100644
index 0000000000..96e913d842
--- /dev/null
+++ b/extra-files/PageContent.module.css
@@ -0,0 +1,3 @@
+.content {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/slides/slides.pdf b/slides/slides.pdf
new file mode 100644
index 0000000000..62735d5424
Binary files /dev/null and b/slides/slides.pdf differ