diff --git a/code/01-starting-project/package.json b/code/01-starting-project/package.json new file mode 100644 index 0000000000..6c1b63a022 --- /dev/null +++ b/code/01-starting-project/package.json @@ -0,0 +1,38 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/01-starting-project/public/favicon.ico b/code/01-starting-project/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/01-starting-project/public/favicon.ico differ diff --git a/code/01-starting-project/public/index.html b/code/01-starting-project/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/01-starting-project/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/code/01-starting-project/public/logo192.png b/code/01-starting-project/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/01-starting-project/public/logo192.png differ diff --git a/code/01-starting-project/public/logo512.png b/code/01-starting-project/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/01-starting-project/public/logo512.png differ diff --git a/code/01-starting-project/public/manifest.json b/code/01-starting-project/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/01-starting-project/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/01-starting-project/public/robots.txt b/code/01-starting-project/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/01-starting-project/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/01-starting-project/src/App.js b/code/01-starting-project/src/App.js new file mode 100644 index 0000000000..4a194e6079 --- /dev/null +++ b/code/01-starting-project/src/App.js @@ -0,0 +1,9 @@ +function App() { + return ( +
+

Let's get started!

+
+ ); +} + +export default App; diff --git a/code/01-starting-project/src/index.css b/code/01-starting-project/src/index.css new file mode 100644 index 0000000000..72399cc5c6 --- /dev/null +++ b/code/01-starting-project/src/index.css @@ -0,0 +1,15 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #3f3f3f; +} + diff --git a/code/01-starting-project/src/index.js b/code/01-starting-project/src/index.js new file mode 100644 index 0000000000..778ec1ba20 --- /dev/null +++ b/code/01-starting-project/src/index.js @@ -0,0 +1,7 @@ +import ReactDOM from 'react-dom/client'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/code/02-defining-and-using-routes/package.json b/code/02-defining-and-using-routes/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/02-defining-and-using-routes/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/02-defining-and-using-routes/public/favicon.ico b/code/02-defining-and-using-routes/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/02-defining-and-using-routes/public/favicon.ico differ diff --git a/code/02-defining-and-using-routes/public/index.html b/code/02-defining-and-using-routes/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/02-defining-and-using-routes/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/code/02-defining-and-using-routes/public/logo192.png b/code/02-defining-and-using-routes/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/02-defining-and-using-routes/public/logo192.png differ diff --git a/code/02-defining-and-using-routes/public/logo512.png b/code/02-defining-and-using-routes/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/02-defining-and-using-routes/public/logo512.png differ diff --git a/code/02-defining-and-using-routes/public/manifest.json b/code/02-defining-and-using-routes/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/02-defining-and-using-routes/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/02-defining-and-using-routes/public/robots.txt b/code/02-defining-and-using-routes/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/02-defining-and-using-routes/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/02-defining-and-using-routes/src/App.js b/code/02-defining-and-using-routes/src/App.js new file mode 100644 index 0000000000..86364d8498 --- /dev/null +++ b/code/02-defining-and-using-routes/src/App.js @@ -0,0 +1,22 @@ +import { Route } from 'react-router-dom'; + +import Welcome from './pages/Welcome'; +import Products from './pages/Products'; + +function App() { + return ( +
+ + + + + + +
+ ); +} + +export default App; + +// our-domain.com/welcome => Welcome Component +// our-domain.com/products => Products Component \ No newline at end of file diff --git a/code/02-defining-and-using-routes/src/index.css b/code/02-defining-and-using-routes/src/index.css new file mode 100644 index 0000000000..72399cc5c6 --- /dev/null +++ b/code/02-defining-and-using-routes/src/index.css @@ -0,0 +1,15 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #3f3f3f; +} + diff --git a/code/02-defining-and-using-routes/src/index.js b/code/02-defining-and-using-routes/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/02-defining-and-using-routes/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/02-defining-and-using-routes/src/pages/Products.js b/code/02-defining-and-using-routes/src/pages/Products.js new file mode 100644 index 0000000000..5755202240 --- /dev/null +++ b/code/02-defining-and-using-routes/src/pages/Products.js @@ -0,0 +1,5 @@ +const Products = () => { + return

The Products Page

; +}; + +export default Products; \ No newline at end of file diff --git a/code/02-defining-and-using-routes/src/pages/Welcome.js b/code/02-defining-and-using-routes/src/pages/Welcome.js new file mode 100644 index 0000000000..1453342bae --- /dev/null +++ b/code/02-defining-and-using-routes/src/pages/Welcome.js @@ -0,0 +1,5 @@ +const Welcome = () => { + return

The Welcome Page

; +}; + +export default Welcome; \ No newline at end of file diff --git a/code/03-working-with-links/package.json b/code/03-working-with-links/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/03-working-with-links/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/03-working-with-links/public/favicon.ico b/code/03-working-with-links/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/03-working-with-links/public/favicon.ico differ diff --git a/code/03-working-with-links/public/index.html b/code/03-working-with-links/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/03-working-with-links/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/code/03-working-with-links/public/logo192.png b/code/03-working-with-links/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/03-working-with-links/public/logo192.png differ diff --git a/code/03-working-with-links/public/logo512.png b/code/03-working-with-links/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/03-working-with-links/public/logo512.png differ diff --git a/code/03-working-with-links/public/manifest.json b/code/03-working-with-links/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/03-working-with-links/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/03-working-with-links/public/robots.txt b/code/03-working-with-links/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/03-working-with-links/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/03-working-with-links/src/App.js b/code/03-working-with-links/src/App.js new file mode 100644 index 0000000000..52f9031155 --- /dev/null +++ b/code/03-working-with-links/src/App.js @@ -0,0 +1,26 @@ +import { Route } from 'react-router-dom'; + +import Welcome from './pages/Welcome'; +import Products from './pages/Products'; +import MainHeader from './components/MainHeader'; + +function App() { + return ( +
+ +
+ + + + + + +
+
+ ); +} + +export default App; + +// our-domain.com/welcome => Welcome Component +// our-domain.com/products => Products Component diff --git a/code/03-working-with-links/src/components/MainHeader.js b/code/03-working-with-links/src/components/MainHeader.js new file mode 100644 index 0000000000..66befe0eee --- /dev/null +++ b/code/03-working-with-links/src/components/MainHeader.js @@ -0,0 +1,22 @@ +import { Link } from 'react-router-dom'; + +import classes from './MainHeader.module.css'; + +const MainHeader = () => { + return ( +
+ +
+ ); +}; + +export default MainHeader; diff --git a/code/03-working-with-links/src/components/MainHeader.module.css b/code/03-working-with-links/src/components/MainHeader.module.css new file mode 100644 index 0000000000..ea62a274cf --- /dev/null +++ b/code/03-working-with-links/src/components/MainHeader.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + background-color: #044599; + padding: 0 10%; +} + +.header nav { + height: 100%; +} + +.header ul { + height: 100%; + list-style: none; + display: flex; + padding: 0; + margin: 0; + align-items: center; + justify-content: center; +} + +.header li { + margin: 0 1rem; + width: 5rem; +} + +.header a { + color: white; + text-decoration: none; +} + +.header a:hover, +.header a:active { + color: #95bcf0; + padding-bottom: 0.25rem; + border-bottom: 4px solid #95bcf0; +} \ No newline at end of file diff --git a/code/03-working-with-links/src/index.css b/code/03-working-with-links/src/index.css new file mode 100644 index 0000000000..44a860502a --- /dev/null +++ b/code/03-working-with-links/src/index.css @@ -0,0 +1,26 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #e0e9f5; +} + +main { + margin-top: 7rem; + text-align: center; +} + +h1, +h2, +h3, +p { + color: #042b5f; +} diff --git a/code/03-working-with-links/src/index.js b/code/03-working-with-links/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/03-working-with-links/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/03-working-with-links/src/pages/Products.js b/code/03-working-with-links/src/pages/Products.js new file mode 100644 index 0000000000..5755202240 --- /dev/null +++ b/code/03-working-with-links/src/pages/Products.js @@ -0,0 +1,5 @@ +const Products = () => { + return

The Products Page

; +}; + +export default Products; \ No newline at end of file diff --git a/code/03-working-with-links/src/pages/Welcome.js b/code/03-working-with-links/src/pages/Welcome.js new file mode 100644 index 0000000000..1453342bae --- /dev/null +++ b/code/03-working-with-links/src/pages/Welcome.js @@ -0,0 +1,5 @@ +const Welcome = () => { + return

The Welcome Page

; +}; + +export default Welcome; \ No newline at end of file diff --git a/code/04-using-navlinks/package.json b/code/04-using-navlinks/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/04-using-navlinks/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/04-using-navlinks/public/favicon.ico b/code/04-using-navlinks/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/04-using-navlinks/public/favicon.ico differ diff --git a/code/04-using-navlinks/public/index.html b/code/04-using-navlinks/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/04-using-navlinks/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/code/04-using-navlinks/public/logo192.png b/code/04-using-navlinks/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/04-using-navlinks/public/logo192.png differ diff --git a/code/04-using-navlinks/public/logo512.png b/code/04-using-navlinks/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/04-using-navlinks/public/logo512.png differ diff --git a/code/04-using-navlinks/public/manifest.json b/code/04-using-navlinks/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/04-using-navlinks/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/04-using-navlinks/public/robots.txt b/code/04-using-navlinks/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/04-using-navlinks/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/04-using-navlinks/src/App.js b/code/04-using-navlinks/src/App.js new file mode 100644 index 0000000000..52f9031155 --- /dev/null +++ b/code/04-using-navlinks/src/App.js @@ -0,0 +1,26 @@ +import { Route } from 'react-router-dom'; + +import Welcome from './pages/Welcome'; +import Products from './pages/Products'; +import MainHeader from './components/MainHeader'; + +function App() { + return ( +
+ +
+ + + + + + +
+
+ ); +} + +export default App; + +// our-domain.com/welcome => Welcome Component +// our-domain.com/products => Products Component diff --git a/code/04-using-navlinks/src/components/MainHeader.js b/code/04-using-navlinks/src/components/MainHeader.js new file mode 100644 index 0000000000..5f5b28db4e --- /dev/null +++ b/code/04-using-navlinks/src/components/MainHeader.js @@ -0,0 +1,26 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainHeader.module.css'; + +const MainHeader = () => { + return ( +
+ +
+ ); +}; + +export default MainHeader; diff --git a/code/04-using-navlinks/src/components/MainHeader.module.css b/code/04-using-navlinks/src/components/MainHeader.module.css new file mode 100644 index 0000000000..0d792b4c0d --- /dev/null +++ b/code/04-using-navlinks/src/components/MainHeader.module.css @@ -0,0 +1,38 @@ +.header { + width: 100%; + height: 5rem; + background-color: #044599; + padding: 0 10%; +} + +.header nav { + height: 100%; +} + +.header ul { + height: 100%; + list-style: none; + display: flex; + padding: 0; + margin: 0; + align-items: center; + justify-content: center; +} + +.header li { + margin: 0 1rem; + width: 5rem; +} + +.header a { + color: white; + text-decoration: none; +} + +.header a:hover, +.header a:active, +.header a.active { + color: #95bcf0; + padding-bottom: 0.25rem; + border-bottom: 4px solid #95bcf0; +} \ No newline at end of file diff --git a/code/04-using-navlinks/src/index.css b/code/04-using-navlinks/src/index.css new file mode 100644 index 0000000000..44a860502a --- /dev/null +++ b/code/04-using-navlinks/src/index.css @@ -0,0 +1,26 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #e0e9f5; +} + +main { + margin-top: 7rem; + text-align: center; +} + +h1, +h2, +h3, +p { + color: #042b5f; +} diff --git a/code/04-using-navlinks/src/index.js b/code/04-using-navlinks/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/04-using-navlinks/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/04-using-navlinks/src/pages/Products.js b/code/04-using-navlinks/src/pages/Products.js new file mode 100644 index 0000000000..5755202240 --- /dev/null +++ b/code/04-using-navlinks/src/pages/Products.js @@ -0,0 +1,5 @@ +const Products = () => { + return

The Products Page

; +}; + +export default Products; \ No newline at end of file diff --git a/code/04-using-navlinks/src/pages/Welcome.js b/code/04-using-navlinks/src/pages/Welcome.js new file mode 100644 index 0000000000..1453342bae --- /dev/null +++ b/code/04-using-navlinks/src/pages/Welcome.js @@ -0,0 +1,5 @@ +const Welcome = () => { + return

The Welcome Page

; +}; + +export default Welcome; \ No newline at end of file diff --git a/code/05-extracting-route-params/package.json b/code/05-extracting-route-params/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/05-extracting-route-params/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/05-extracting-route-params/public/favicon.ico b/code/05-extracting-route-params/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/05-extracting-route-params/public/favicon.ico differ diff --git a/code/05-extracting-route-params/public/index.html b/code/05-extracting-route-params/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/05-extracting-route-params/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/code/05-extracting-route-params/public/logo192.png b/code/05-extracting-route-params/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/05-extracting-route-params/public/logo192.png differ diff --git a/code/05-extracting-route-params/public/logo512.png b/code/05-extracting-route-params/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/05-extracting-route-params/public/logo512.png differ diff --git a/code/05-extracting-route-params/public/manifest.json b/code/05-extracting-route-params/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/05-extracting-route-params/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/05-extracting-route-params/public/robots.txt b/code/05-extracting-route-params/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/05-extracting-route-params/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/05-extracting-route-params/src/App.js b/code/05-extracting-route-params/src/App.js new file mode 100644 index 0000000000..a6fbf09ba1 --- /dev/null +++ b/code/05-extracting-route-params/src/App.js @@ -0,0 +1,31 @@ +import { Route } from 'react-router-dom'; + +import Welcome from './pages/Welcome'; +import Products from './pages/Products'; +import ProductDetail from './pages/ProductDetail'; +import MainHeader from './components/MainHeader'; + +function App() { + return ( +
+ +
+ + + + + + + + + +
+
+ ); +} + +export default App; + +// our-domain.com/welcome => Welcome Component +// our-domain.com/products => Products Component +// our-domain.com/product-detail/a-book diff --git a/code/05-extracting-route-params/src/components/MainHeader.js b/code/05-extracting-route-params/src/components/MainHeader.js new file mode 100644 index 0000000000..5f5b28db4e --- /dev/null +++ b/code/05-extracting-route-params/src/components/MainHeader.js @@ -0,0 +1,26 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainHeader.module.css'; + +const MainHeader = () => { + return ( +
+ +
+ ); +}; + +export default MainHeader; diff --git a/code/05-extracting-route-params/src/components/MainHeader.module.css b/code/05-extracting-route-params/src/components/MainHeader.module.css new file mode 100644 index 0000000000..0d792b4c0d --- /dev/null +++ b/code/05-extracting-route-params/src/components/MainHeader.module.css @@ -0,0 +1,38 @@ +.header { + width: 100%; + height: 5rem; + background-color: #044599; + padding: 0 10%; +} + +.header nav { + height: 100%; +} + +.header ul { + height: 100%; + list-style: none; + display: flex; + padding: 0; + margin: 0; + align-items: center; + justify-content: center; +} + +.header li { + margin: 0 1rem; + width: 5rem; +} + +.header a { + color: white; + text-decoration: none; +} + +.header a:hover, +.header a:active, +.header a.active { + color: #95bcf0; + padding-bottom: 0.25rem; + border-bottom: 4px solid #95bcf0; +} \ No newline at end of file diff --git a/code/05-extracting-route-params/src/index.css b/code/05-extracting-route-params/src/index.css new file mode 100644 index 0000000000..44a860502a --- /dev/null +++ b/code/05-extracting-route-params/src/index.css @@ -0,0 +1,26 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #e0e9f5; +} + +main { + margin-top: 7rem; + text-align: center; +} + +h1, +h2, +h3, +p { + color: #042b5f; +} diff --git a/code/05-extracting-route-params/src/index.js b/code/05-extracting-route-params/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/05-extracting-route-params/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/05-extracting-route-params/src/pages/ProductDetail.js b/code/05-extracting-route-params/src/pages/ProductDetail.js new file mode 100644 index 0000000000..3923681f8d --- /dev/null +++ b/code/05-extracting-route-params/src/pages/ProductDetail.js @@ -0,0 +1,16 @@ +import { useParams } from 'react-router-dom'; + +const ProductDetail = () => { + const params = useParams(); + + console.log(params.productId); + + return ( +
+

Product Detail

+

{params.productId}

+
+ ); +}; + +export default ProductDetail; diff --git a/code/05-extracting-route-params/src/pages/Products.js b/code/05-extracting-route-params/src/pages/Products.js new file mode 100644 index 0000000000..7212d35cb6 --- /dev/null +++ b/code/05-extracting-route-params/src/pages/Products.js @@ -0,0 +1,14 @@ +const Products = () => { + return ( +
+

The Products Page

+ +
+ ); +}; + +export default Products; diff --git a/code/05-extracting-route-params/src/pages/Welcome.js b/code/05-extracting-route-params/src/pages/Welcome.js new file mode 100644 index 0000000000..1453342bae --- /dev/null +++ b/code/05-extracting-route-params/src/pages/Welcome.js @@ -0,0 +1,5 @@ +const Welcome = () => { + return

The Welcome Page

; +}; + +export default Welcome; \ No newline at end of file diff --git a/code/06-using-switch-and-exact/package.json b/code/06-using-switch-and-exact/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/06-using-switch-and-exact/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/06-using-switch-and-exact/public/favicon.ico b/code/06-using-switch-and-exact/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/06-using-switch-and-exact/public/favicon.ico differ diff --git a/code/06-using-switch-and-exact/public/index.html b/code/06-using-switch-and-exact/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/06-using-switch-and-exact/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/code/06-using-switch-and-exact/public/logo192.png b/code/06-using-switch-and-exact/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/06-using-switch-and-exact/public/logo192.png differ diff --git a/code/06-using-switch-and-exact/public/logo512.png b/code/06-using-switch-and-exact/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/06-using-switch-and-exact/public/logo512.png differ diff --git a/code/06-using-switch-and-exact/public/manifest.json b/code/06-using-switch-and-exact/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/06-using-switch-and-exact/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/06-using-switch-and-exact/public/robots.txt b/code/06-using-switch-and-exact/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/06-using-switch-and-exact/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/06-using-switch-and-exact/src/App.js b/code/06-using-switch-and-exact/src/App.js new file mode 100644 index 0000000000..3b6e49c201 --- /dev/null +++ b/code/06-using-switch-and-exact/src/App.js @@ -0,0 +1,33 @@ +import { Route, Switch } from 'react-router-dom'; + +import Welcome from './pages/Welcome'; +import Products from './pages/Products'; +import ProductDetail from './pages/ProductDetail'; +import MainHeader from './components/MainHeader'; + +function App() { + return ( +
+ +
+ + + + + + + + + + + +
+
+ ); +} + +export default App; + +// our-domain.com/welcome => Welcome Component +// our-domain.com/products => Products Component +// our-domain.com/product-detail/a-book diff --git a/code/06-using-switch-and-exact/src/components/MainHeader.js b/code/06-using-switch-and-exact/src/components/MainHeader.js new file mode 100644 index 0000000000..5f5b28db4e --- /dev/null +++ b/code/06-using-switch-and-exact/src/components/MainHeader.js @@ -0,0 +1,26 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainHeader.module.css'; + +const MainHeader = () => { + return ( +
+ +
+ ); +}; + +export default MainHeader; diff --git a/code/06-using-switch-and-exact/src/components/MainHeader.module.css b/code/06-using-switch-and-exact/src/components/MainHeader.module.css new file mode 100644 index 0000000000..0d792b4c0d --- /dev/null +++ b/code/06-using-switch-and-exact/src/components/MainHeader.module.css @@ -0,0 +1,38 @@ +.header { + width: 100%; + height: 5rem; + background-color: #044599; + padding: 0 10%; +} + +.header nav { + height: 100%; +} + +.header ul { + height: 100%; + list-style: none; + display: flex; + padding: 0; + margin: 0; + align-items: center; + justify-content: center; +} + +.header li { + margin: 0 1rem; + width: 5rem; +} + +.header a { + color: white; + text-decoration: none; +} + +.header a:hover, +.header a:active, +.header a.active { + color: #95bcf0; + padding-bottom: 0.25rem; + border-bottom: 4px solid #95bcf0; +} \ No newline at end of file diff --git a/code/06-using-switch-and-exact/src/index.css b/code/06-using-switch-and-exact/src/index.css new file mode 100644 index 0000000000..44a860502a --- /dev/null +++ b/code/06-using-switch-and-exact/src/index.css @@ -0,0 +1,26 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #e0e9f5; +} + +main { + margin-top: 7rem; + text-align: center; +} + +h1, +h2, +h3, +p { + color: #042b5f; +} diff --git a/code/06-using-switch-and-exact/src/index.js b/code/06-using-switch-and-exact/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/06-using-switch-and-exact/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/06-using-switch-and-exact/src/pages/ProductDetail.js b/code/06-using-switch-and-exact/src/pages/ProductDetail.js new file mode 100644 index 0000000000..3923681f8d --- /dev/null +++ b/code/06-using-switch-and-exact/src/pages/ProductDetail.js @@ -0,0 +1,16 @@ +import { useParams } from 'react-router-dom'; + +const ProductDetail = () => { + const params = useParams(); + + console.log(params.productId); + + return ( +
+

Product Detail

+

{params.productId}

+
+ ); +}; + +export default ProductDetail; diff --git a/code/06-using-switch-and-exact/src/pages/Products.js b/code/06-using-switch-and-exact/src/pages/Products.js new file mode 100644 index 0000000000..834f867bfd --- /dev/null +++ b/code/06-using-switch-and-exact/src/pages/Products.js @@ -0,0 +1,22 @@ +import { Link } from 'react-router-dom'; + +const Products = () => { + return ( +
+

The Products Page

+ +
+ ); +}; + +export default Products; diff --git a/code/06-using-switch-and-exact/src/pages/Welcome.js b/code/06-using-switch-and-exact/src/pages/Welcome.js new file mode 100644 index 0000000000..1453342bae --- /dev/null +++ b/code/06-using-switch-and-exact/src/pages/Welcome.js @@ -0,0 +1,5 @@ +const Welcome = () => { + return

The Welcome Page

; +}; + +export default Welcome; \ No newline at end of file diff --git a/code/07-working-with-nested-routes/package.json b/code/07-working-with-nested-routes/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/07-working-with-nested-routes/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/07-working-with-nested-routes/public/favicon.ico b/code/07-working-with-nested-routes/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/07-working-with-nested-routes/public/favicon.ico differ diff --git a/code/07-working-with-nested-routes/public/index.html b/code/07-working-with-nested-routes/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/07-working-with-nested-routes/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/code/07-working-with-nested-routes/public/logo192.png b/code/07-working-with-nested-routes/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/07-working-with-nested-routes/public/logo192.png differ diff --git a/code/07-working-with-nested-routes/public/logo512.png b/code/07-working-with-nested-routes/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/07-working-with-nested-routes/public/logo512.png differ diff --git a/code/07-working-with-nested-routes/public/manifest.json b/code/07-working-with-nested-routes/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/07-working-with-nested-routes/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/07-working-with-nested-routes/public/robots.txt b/code/07-working-with-nested-routes/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/07-working-with-nested-routes/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/07-working-with-nested-routes/src/App.js b/code/07-working-with-nested-routes/src/App.js new file mode 100644 index 0000000000..3b6e49c201 --- /dev/null +++ b/code/07-working-with-nested-routes/src/App.js @@ -0,0 +1,33 @@ +import { Route, Switch } from 'react-router-dom'; + +import Welcome from './pages/Welcome'; +import Products from './pages/Products'; +import ProductDetail from './pages/ProductDetail'; +import MainHeader from './components/MainHeader'; + +function App() { + return ( +
+ +
+ + + + + + + + + + + +
+
+ ); +} + +export default App; + +// our-domain.com/welcome => Welcome Component +// our-domain.com/products => Products Component +// our-domain.com/product-detail/a-book diff --git a/code/07-working-with-nested-routes/src/components/MainHeader.js b/code/07-working-with-nested-routes/src/components/MainHeader.js new file mode 100644 index 0000000000..5f5b28db4e --- /dev/null +++ b/code/07-working-with-nested-routes/src/components/MainHeader.js @@ -0,0 +1,26 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainHeader.module.css'; + +const MainHeader = () => { + return ( +
+ +
+ ); +}; + +export default MainHeader; diff --git a/code/07-working-with-nested-routes/src/components/MainHeader.module.css b/code/07-working-with-nested-routes/src/components/MainHeader.module.css new file mode 100644 index 0000000000..0d792b4c0d --- /dev/null +++ b/code/07-working-with-nested-routes/src/components/MainHeader.module.css @@ -0,0 +1,38 @@ +.header { + width: 100%; + height: 5rem; + background-color: #044599; + padding: 0 10%; +} + +.header nav { + height: 100%; +} + +.header ul { + height: 100%; + list-style: none; + display: flex; + padding: 0; + margin: 0; + align-items: center; + justify-content: center; +} + +.header li { + margin: 0 1rem; + width: 5rem; +} + +.header a { + color: white; + text-decoration: none; +} + +.header a:hover, +.header a:active, +.header a.active { + color: #95bcf0; + padding-bottom: 0.25rem; + border-bottom: 4px solid #95bcf0; +} \ No newline at end of file diff --git a/code/07-working-with-nested-routes/src/index.css b/code/07-working-with-nested-routes/src/index.css new file mode 100644 index 0000000000..44a860502a --- /dev/null +++ b/code/07-working-with-nested-routes/src/index.css @@ -0,0 +1,26 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #e0e9f5; +} + +main { + margin-top: 7rem; + text-align: center; +} + +h1, +h2, +h3, +p { + color: #042b5f; +} diff --git a/code/07-working-with-nested-routes/src/index.js b/code/07-working-with-nested-routes/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/07-working-with-nested-routes/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/07-working-with-nested-routes/src/pages/ProductDetail.js b/code/07-working-with-nested-routes/src/pages/ProductDetail.js new file mode 100644 index 0000000000..3923681f8d --- /dev/null +++ b/code/07-working-with-nested-routes/src/pages/ProductDetail.js @@ -0,0 +1,16 @@ +import { useParams } from 'react-router-dom'; + +const ProductDetail = () => { + const params = useParams(); + + console.log(params.productId); + + return ( +
+

Product Detail

+

{params.productId}

+
+ ); +}; + +export default ProductDetail; diff --git a/code/07-working-with-nested-routes/src/pages/Products.js b/code/07-working-with-nested-routes/src/pages/Products.js new file mode 100644 index 0000000000..834f867bfd --- /dev/null +++ b/code/07-working-with-nested-routes/src/pages/Products.js @@ -0,0 +1,22 @@ +import { Link } from 'react-router-dom'; + +const Products = () => { + return ( +
+

The Products Page

+ +
+ ); +}; + +export default Products; diff --git a/code/07-working-with-nested-routes/src/pages/Welcome.js b/code/07-working-with-nested-routes/src/pages/Welcome.js new file mode 100644 index 0000000000..c5ac2fa18c --- /dev/null +++ b/code/07-working-with-nested-routes/src/pages/Welcome.js @@ -0,0 +1,14 @@ +import { Route } from 'react-router-dom'; + +const Welcome = () => { + return ( +
+

The Welcome Page

+ +

Welcome, new user!

+
+
+ ); +}; + +export default Welcome; diff --git a/code/08-redirecting-the-user/package.json b/code/08-redirecting-the-user/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/08-redirecting-the-user/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/08-redirecting-the-user/public/favicon.ico b/code/08-redirecting-the-user/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/08-redirecting-the-user/public/favicon.ico differ diff --git a/code/08-redirecting-the-user/public/index.html b/code/08-redirecting-the-user/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/08-redirecting-the-user/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/code/08-redirecting-the-user/public/logo192.png b/code/08-redirecting-the-user/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/08-redirecting-the-user/public/logo192.png differ diff --git a/code/08-redirecting-the-user/public/logo512.png b/code/08-redirecting-the-user/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/08-redirecting-the-user/public/logo512.png differ diff --git a/code/08-redirecting-the-user/public/manifest.json b/code/08-redirecting-the-user/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/08-redirecting-the-user/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/08-redirecting-the-user/public/robots.txt b/code/08-redirecting-the-user/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/08-redirecting-the-user/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/08-redirecting-the-user/src/App.js b/code/08-redirecting-the-user/src/App.js new file mode 100644 index 0000000000..b78259ef9c --- /dev/null +++ b/code/08-redirecting-the-user/src/App.js @@ -0,0 +1,36 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import Welcome from './pages/Welcome'; +import Products from './pages/Products'; +import ProductDetail from './pages/ProductDetail'; +import MainHeader from './components/MainHeader'; + +function App() { + return ( +
+ +
+ + + + + + + + + + + + + + +
+
+ ); +} + +export default App; + +// our-domain.com/welcome => Welcome Component +// our-domain.com/products => Products Component +// our-domain.com/product-detail/a-book diff --git a/code/08-redirecting-the-user/src/components/MainHeader.js b/code/08-redirecting-the-user/src/components/MainHeader.js new file mode 100644 index 0000000000..5f5b28db4e --- /dev/null +++ b/code/08-redirecting-the-user/src/components/MainHeader.js @@ -0,0 +1,26 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainHeader.module.css'; + +const MainHeader = () => { + return ( +
+ +
+ ); +}; + +export default MainHeader; diff --git a/code/08-redirecting-the-user/src/components/MainHeader.module.css b/code/08-redirecting-the-user/src/components/MainHeader.module.css new file mode 100644 index 0000000000..0d792b4c0d --- /dev/null +++ b/code/08-redirecting-the-user/src/components/MainHeader.module.css @@ -0,0 +1,38 @@ +.header { + width: 100%; + height: 5rem; + background-color: #044599; + padding: 0 10%; +} + +.header nav { + height: 100%; +} + +.header ul { + height: 100%; + list-style: none; + display: flex; + padding: 0; + margin: 0; + align-items: center; + justify-content: center; +} + +.header li { + margin: 0 1rem; + width: 5rem; +} + +.header a { + color: white; + text-decoration: none; +} + +.header a:hover, +.header a:active, +.header a.active { + color: #95bcf0; + padding-bottom: 0.25rem; + border-bottom: 4px solid #95bcf0; +} \ No newline at end of file diff --git a/code/08-redirecting-the-user/src/index.css b/code/08-redirecting-the-user/src/index.css new file mode 100644 index 0000000000..44a860502a --- /dev/null +++ b/code/08-redirecting-the-user/src/index.css @@ -0,0 +1,26 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #e0e9f5; +} + +main { + margin-top: 7rem; + text-align: center; +} + +h1, +h2, +h3, +p { + color: #042b5f; +} diff --git a/code/08-redirecting-the-user/src/index.js b/code/08-redirecting-the-user/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/08-redirecting-the-user/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/08-redirecting-the-user/src/pages/ProductDetail.js b/code/08-redirecting-the-user/src/pages/ProductDetail.js new file mode 100644 index 0000000000..3923681f8d --- /dev/null +++ b/code/08-redirecting-the-user/src/pages/ProductDetail.js @@ -0,0 +1,16 @@ +import { useParams } from 'react-router-dom'; + +const ProductDetail = () => { + const params = useParams(); + + console.log(params.productId); + + return ( +
+

Product Detail

+

{params.productId}

+
+ ); +}; + +export default ProductDetail; diff --git a/code/08-redirecting-the-user/src/pages/Products.js b/code/08-redirecting-the-user/src/pages/Products.js new file mode 100644 index 0000000000..834f867bfd --- /dev/null +++ b/code/08-redirecting-the-user/src/pages/Products.js @@ -0,0 +1,22 @@ +import { Link } from 'react-router-dom'; + +const Products = () => { + return ( +
+

The Products Page

+ +
+ ); +}; + +export default Products; diff --git a/code/08-redirecting-the-user/src/pages/Welcome.js b/code/08-redirecting-the-user/src/pages/Welcome.js new file mode 100644 index 0000000000..c5ac2fa18c --- /dev/null +++ b/code/08-redirecting-the-user/src/pages/Welcome.js @@ -0,0 +1,14 @@ +import { Route } from 'react-router-dom'; + +const Welcome = () => { + return ( +
+

The Welcome Page

+ +

Welcome, new user!

+
+
+ ); +}; + +export default Welcome; diff --git a/code/09-time-to-practice-starting-code/package.json b/code/09-time-to-practice-starting-code/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/09-time-to-practice-starting-code/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/09-time-to-practice-starting-code/public/favicon.ico b/code/09-time-to-practice-starting-code/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/09-time-to-practice-starting-code/public/favicon.ico differ diff --git a/code/09-time-to-practice-starting-code/public/index.html b/code/09-time-to-practice-starting-code/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/09-time-to-practice-starting-code/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/code/09-time-to-practice-starting-code/public/logo192.png b/code/09-time-to-practice-starting-code/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/09-time-to-practice-starting-code/public/logo192.png differ diff --git a/code/09-time-to-practice-starting-code/public/logo512.png b/code/09-time-to-practice-starting-code/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/09-time-to-practice-starting-code/public/logo512.png differ diff --git a/code/09-time-to-practice-starting-code/public/manifest.json b/code/09-time-to-practice-starting-code/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/09-time-to-practice-starting-code/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/09-time-to-practice-starting-code/public/robots.txt b/code/09-time-to-practice-starting-code/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/09-time-to-practice-starting-code/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/09-time-to-practice-starting-code/src/App.js b/code/09-time-to-practice-starting-code/src/App.js new file mode 100644 index 0000000000..05d9bcca2e --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/App.js @@ -0,0 +1,9 @@ +function App() { + return ( +
+ +
+ ); +} + +export default App; diff --git a/code/09-time-to-practice-starting-code/src/components/UI/Card.js b/code/09-time-to-practice-starting-code/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
{props.children}
; +}; + +export default Card; diff --git a/code/09-time-to-practice-starting-code/src/components/UI/Card.module.css b/code/09-time-to-practice-starting-code/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.js b/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
; +} + +export default LoadingSpinner; diff --git a/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.module.css b/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.js b/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.module.css b/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/09-time-to-practice-starting-code/src/components/comments/Comments.js b/code/09-time-to-practice-starting-code/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/09-time-to-practice-starting-code/src/components/comments/Comments.module.css b/code/09-time-to-practice-starting-code/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.js b/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( + + ); +}; + +export default CommentsList; diff --git a/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.module.css b/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.js b/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.module.css b/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/09-time-to-practice-starting-code/src/components/layout/Layout.module.css b/code/09-time-to-practice-starting-code/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/09-time-to-practice-starting-code/src/components/layout/MainNavigation.module.css b/code/09-time-to-practice-starting-code/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.js b/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.module.css b/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.js b/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..14a83fa67c --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,14 @@ +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.module.css b/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.js b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..9a49c7fb7c --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.js @@ -0,0 +1,47 @@ +import { useRef } from 'react'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + return ( + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.module.css b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.js b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..06cf8b1221 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.js @@ -0,0 +1,19 @@ +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.module.css b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.js b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..a37943f11c --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.js @@ -0,0 +1,23 @@ +import { Fragment } from 'react'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const QuoteList = (props) => { + return ( + + + + ); +}; + +export default QuoteList; diff --git a/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.module.css b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/09-time-to-practice-starting-code/src/index.css b/code/09-time-to-practice-starting-code/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/09-time-to-practice-starting-code/src/index.js b/code/09-time-to-practice-starting-code/src/index.js new file mode 100644 index 0000000000..778ec1ba20 --- /dev/null +++ b/code/09-time-to-practice-starting-code/src/index.js @@ -0,0 +1,7 @@ +import ReactDOM from 'react-dom/client'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/code/10-practice-redirecting-and-extracting-params/package.json b/code/10-practice-redirecting-and-extracting-params/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/10-practice-redirecting-and-extracting-params/public/favicon.ico b/code/10-practice-redirecting-and-extracting-params/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/10-practice-redirecting-and-extracting-params/public/favicon.ico differ diff --git a/code/10-practice-redirecting-and-extracting-params/public/index.html b/code/10-practice-redirecting-and-extracting-params/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/10-practice-redirecting-and-extracting-params/public/logo192.png b/code/10-practice-redirecting-and-extracting-params/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/10-practice-redirecting-and-extracting-params/public/logo192.png differ diff --git a/code/10-practice-redirecting-and-extracting-params/public/logo512.png b/code/10-practice-redirecting-and-extracting-params/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/10-practice-redirecting-and-extracting-params/public/logo512.png differ diff --git a/code/10-practice-redirecting-and-extracting-params/public/manifest.json b/code/10-practice-redirecting-and-extracting-params/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/10-practice-redirecting-and-extracting-params/public/robots.txt b/code/10-practice-redirecting-and-extracting-params/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/10-practice-redirecting-and-extracting-params/src/App.js b/code/10-practice-redirecting-and-extracting-params/src/App.js new file mode 100644 index 0000000000..82436c9d54 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/App.js @@ -0,0 +1,26 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; + +function App() { + return ( + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.js b/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.js b/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.js b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.js b/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.js b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( + + ); +}; + +export default CommentsList; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.js b/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/layout/Layout.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/layout/MainNavigation.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.js b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.js b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..14a83fa67c --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,14 @@ +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.js b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..9a49c7fb7c --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.js @@ -0,0 +1,47 @@ +import { useRef } from 'react'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + return ( + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.js b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..ee1ccb3ddc --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.js @@ -0,0 +1,21 @@ +import { Link } from 'react-router-dom'; + +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.js b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..a37943f11c --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.js @@ -0,0 +1,23 @@ +import { Fragment } from 'react'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const QuoteList = (props) => { + return ( + +
      + {props.quotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.module.css b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/10-practice-redirecting-and-extracting-params/src/index.css b/code/10-practice-redirecting-and-extracting-params/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/10-practice-redirecting-and-extracting-params/src/index.js b/code/10-practice-redirecting-and-extracting-params/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/10-practice-redirecting-and-extracting-params/src/pages/AllQuotes.js b/code/10-practice-redirecting-and-extracting-params/src/pages/AllQuotes.js new file mode 100644 index 0000000000..0c50d04a6f --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/pages/AllQuotes.js @@ -0,0 +1,5 @@ +const AllQuotes = () => { + return

    All Quotes Page

    +}; + +export default AllQuotes; \ No newline at end of file diff --git a/code/10-practice-redirecting-and-extracting-params/src/pages/NewQuote.js b/code/10-practice-redirecting-and-extracting-params/src/pages/NewQuote.js new file mode 100644 index 0000000000..0b0d779a5c --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/pages/NewQuote.js @@ -0,0 +1,5 @@ +const NewQuote = () => { + return

    New Quote Page

    +}; + +export default NewQuote; \ No newline at end of file diff --git a/code/10-practice-redirecting-and-extracting-params/src/pages/QuoteDetail.js b/code/10-practice-redirecting-and-extracting-params/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..9c5cd4c72e --- /dev/null +++ b/code/10-practice-redirecting-and-extracting-params/src/pages/QuoteDetail.js @@ -0,0 +1,15 @@ +import { Fragment } from 'react'; +import { useParams } from 'react-router-dom'; + +const QuoteDetail = () => { + const params = useParams(); + + return ( + +

    Quote Detail Page

    +

    {params.quoteId}

    +
    + ); +}; + +export default QuoteDetail; diff --git a/code/11-practicing-nested-routes/package.json b/code/11-practicing-nested-routes/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/11-practicing-nested-routes/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/11-practicing-nested-routes/public/favicon.ico b/code/11-practicing-nested-routes/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/11-practicing-nested-routes/public/favicon.ico differ diff --git a/code/11-practicing-nested-routes/public/index.html b/code/11-practicing-nested-routes/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/11-practicing-nested-routes/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/11-practicing-nested-routes/public/logo192.png b/code/11-practicing-nested-routes/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/11-practicing-nested-routes/public/logo192.png differ diff --git a/code/11-practicing-nested-routes/public/logo512.png b/code/11-practicing-nested-routes/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/11-practicing-nested-routes/public/logo512.png differ diff --git a/code/11-practicing-nested-routes/public/manifest.json b/code/11-practicing-nested-routes/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/11-practicing-nested-routes/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/11-practicing-nested-routes/public/robots.txt b/code/11-practicing-nested-routes/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/11-practicing-nested-routes/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/11-practicing-nested-routes/src/App.js b/code/11-practicing-nested-routes/src/App.js new file mode 100644 index 0000000000..82436c9d54 --- /dev/null +++ b/code/11-practicing-nested-routes/src/App.js @@ -0,0 +1,26 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; + +function App() { + return ( + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/11-practicing-nested-routes/src/components/UI/Card.js b/code/11-practicing-nested-routes/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/11-practicing-nested-routes/src/components/UI/Card.module.css b/code/11-practicing-nested-routes/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.js b/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.module.css b/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/11-practicing-nested-routes/src/components/comments/CommentItem.js b/code/11-practicing-nested-routes/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/11-practicing-nested-routes/src/components/comments/CommentItem.module.css b/code/11-practicing-nested-routes/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/11-practicing-nested-routes/src/components/comments/Comments.js b/code/11-practicing-nested-routes/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/11-practicing-nested-routes/src/components/comments/Comments.module.css b/code/11-practicing-nested-routes/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/11-practicing-nested-routes/src/components/comments/CommentsList.js b/code/11-practicing-nested-routes/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( + + ); +}; + +export default CommentsList; diff --git a/code/11-practicing-nested-routes/src/components/comments/CommentsList.module.css b/code/11-practicing-nested-routes/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.js b/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.module.css b/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/11-practicing-nested-routes/src/components/layout/Layout.module.css b/code/11-practicing-nested-routes/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/11-practicing-nested-routes/src/components/layout/MainNavigation.module.css b/code/11-practicing-nested-routes/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.js b/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.module.css b/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.js b/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..14a83fa67c --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,14 @@ +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.module.css b/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.js b/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..9a49c7fb7c --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.js @@ -0,0 +1,47 @@ +import { useRef } from 'react'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + return ( + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.module.css b/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.js b/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..ee1ccb3ddc --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.js @@ -0,0 +1,21 @@ +import { Link } from 'react-router-dom'; + +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.module.css b/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteList.js b/code/11-practicing-nested-routes/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..a37943f11c --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteList.js @@ -0,0 +1,23 @@ +import { Fragment } from 'react'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const QuoteList = (props) => { + return ( + +
      + {props.quotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/11-practicing-nested-routes/src/components/quotes/QuoteList.module.css b/code/11-practicing-nested-routes/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/11-practicing-nested-routes/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/11-practicing-nested-routes/src/index.css b/code/11-practicing-nested-routes/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/11-practicing-nested-routes/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/11-practicing-nested-routes/src/index.js b/code/11-practicing-nested-routes/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/11-practicing-nested-routes/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/11-practicing-nested-routes/src/pages/AllQuotes.js b/code/11-practicing-nested-routes/src/pages/AllQuotes.js new file mode 100644 index 0000000000..0c50d04a6f --- /dev/null +++ b/code/11-practicing-nested-routes/src/pages/AllQuotes.js @@ -0,0 +1,5 @@ +const AllQuotes = () => { + return

    All Quotes Page

    +}; + +export default AllQuotes; \ No newline at end of file diff --git a/code/11-practicing-nested-routes/src/pages/NewQuote.js b/code/11-practicing-nested-routes/src/pages/NewQuote.js new file mode 100644 index 0000000000..0b0d779a5c --- /dev/null +++ b/code/11-practicing-nested-routes/src/pages/NewQuote.js @@ -0,0 +1,5 @@ +const NewQuote = () => { + return

    New Quote Page

    +}; + +export default NewQuote; \ No newline at end of file diff --git a/code/11-practicing-nested-routes/src/pages/QuoteDetail.js b/code/11-practicing-nested-routes/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..5631383a1d --- /dev/null +++ b/code/11-practicing-nested-routes/src/pages/QuoteDetail.js @@ -0,0 +1,20 @@ +import { Fragment } from 'react'; +import { useParams, Route } from 'react-router-dom'; + +import Comments from '../components/comments/Comments'; + +const QuoteDetail = () => { + const params = useParams(); + + return ( + +

    Quote Detail Page

    +

    {params.quoteId}

    + + + +
    + ); +}; + +export default QuoteDetail; diff --git a/code/12-adding-a-layout-wrapper/package.json b/code/12-adding-a-layout-wrapper/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/12-adding-a-layout-wrapper/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/12-adding-a-layout-wrapper/public/favicon.ico b/code/12-adding-a-layout-wrapper/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/12-adding-a-layout-wrapper/public/favicon.ico differ diff --git a/code/12-adding-a-layout-wrapper/public/index.html b/code/12-adding-a-layout-wrapper/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/12-adding-a-layout-wrapper/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/12-adding-a-layout-wrapper/public/logo192.png b/code/12-adding-a-layout-wrapper/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/12-adding-a-layout-wrapper/public/logo192.png differ diff --git a/code/12-adding-a-layout-wrapper/public/logo512.png b/code/12-adding-a-layout-wrapper/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/12-adding-a-layout-wrapper/public/logo512.png differ diff --git a/code/12-adding-a-layout-wrapper/public/manifest.json b/code/12-adding-a-layout-wrapper/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/12-adding-a-layout-wrapper/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/12-adding-a-layout-wrapper/public/robots.txt b/code/12-adding-a-layout-wrapper/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/12-adding-a-layout-wrapper/src/App.js b/code/12-adding-a-layout-wrapper/src/App.js new file mode 100644 index 0000000000..5f90130429 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/App.js @@ -0,0 +1,29 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; +import Layout from './components/layout/Layout'; + +function App() { + return ( + + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/12-adding-a-layout-wrapper/src/components/UI/Card.js b/code/12-adding-a-layout-wrapper/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/12-adding-a-layout-wrapper/src/components/UI/Card.module.css b/code/12-adding-a-layout-wrapper/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.js b/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.module.css b/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.js b/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.module.css b/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/Comments.js b/code/12-adding-a-layout-wrapper/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/Comments.module.css b/code/12-adding-a-layout-wrapper/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.js b/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( + + ); +}; + +export default CommentsList; diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.module.css b/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.js b/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.module.css b/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/12-adding-a-layout-wrapper/src/components/layout/Layout.js b/code/12-adding-a-layout-wrapper/src/components/layout/Layout.js new file mode 100644 index 0000000000..2eff00a625 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/layout/Layout.js @@ -0,0 +1,15 @@ +import { Fragment } from 'react'; + +import classes from './Layout.module.css'; +import MainNavigation from './MainNavigation'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/12-adding-a-layout-wrapper/src/components/layout/Layout.module.css b/code/12-adding-a-layout-wrapper/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.js b/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.js new file mode 100644 index 0000000000..76d790a1c3 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.js @@ -0,0 +1,27 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainNavigation.module.css'; + +const MainNavigation = () => { + return ( +
    +
    Great Quotes
    + +
    + ); +}; + +export default MainNavigation; diff --git a/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.module.css b/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.js b/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.module.css b/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.js b/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..14a83fa67c --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,14 @@ +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.module.css b/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.js b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..9a49c7fb7c --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.js @@ -0,0 +1,47 @@ +import { useRef } from 'react'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + return ( + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.module.css b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.js b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..ee1ccb3ddc --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.js @@ -0,0 +1,21 @@ +import { Link } from 'react-router-dom'; + +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.module.css b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.js b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..a37943f11c --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.js @@ -0,0 +1,23 @@ +import { Fragment } from 'react'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const QuoteList = (props) => { + return ( + +
      + {props.quotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.module.css b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/12-adding-a-layout-wrapper/src/index.css b/code/12-adding-a-layout-wrapper/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/12-adding-a-layout-wrapper/src/index.js b/code/12-adding-a-layout-wrapper/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/12-adding-a-layout-wrapper/src/pages/AllQuotes.js b/code/12-adding-a-layout-wrapper/src/pages/AllQuotes.js new file mode 100644 index 0000000000..0c50d04a6f --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/pages/AllQuotes.js @@ -0,0 +1,5 @@ +const AllQuotes = () => { + return

    All Quotes Page

    +}; + +export default AllQuotes; \ No newline at end of file diff --git a/code/12-adding-a-layout-wrapper/src/pages/NewQuote.js b/code/12-adding-a-layout-wrapper/src/pages/NewQuote.js new file mode 100644 index 0000000000..0b0d779a5c --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/pages/NewQuote.js @@ -0,0 +1,5 @@ +const NewQuote = () => { + return

    New Quote Page

    +}; + +export default NewQuote; \ No newline at end of file diff --git a/code/12-adding-a-layout-wrapper/src/pages/QuoteDetail.js b/code/12-adding-a-layout-wrapper/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..5631383a1d --- /dev/null +++ b/code/12-adding-a-layout-wrapper/src/pages/QuoteDetail.js @@ -0,0 +1,20 @@ +import { Fragment } from 'react'; +import { useParams, Route } from 'react-router-dom'; + +import Comments from '../components/comments/Comments'; + +const QuoteDetail = () => { + const params = useParams(); + + return ( + +

    Quote Detail Page

    +

    {params.quoteId}

    + + + +
    + ); +}; + +export default QuoteDetail; diff --git a/code/13-adding-dummy-data-and-more-content/package.json b/code/13-adding-dummy-data-and-more-content/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/13-adding-dummy-data-and-more-content/public/favicon.ico b/code/13-adding-dummy-data-and-more-content/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/13-adding-dummy-data-and-more-content/public/favicon.ico differ diff --git a/code/13-adding-dummy-data-and-more-content/public/index.html b/code/13-adding-dummy-data-and-more-content/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/13-adding-dummy-data-and-more-content/public/logo192.png b/code/13-adding-dummy-data-and-more-content/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/13-adding-dummy-data-and-more-content/public/logo192.png differ diff --git a/code/13-adding-dummy-data-and-more-content/public/logo512.png b/code/13-adding-dummy-data-and-more-content/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/13-adding-dummy-data-and-more-content/public/logo512.png differ diff --git a/code/13-adding-dummy-data-and-more-content/public/manifest.json b/code/13-adding-dummy-data-and-more-content/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/13-adding-dummy-data-and-more-content/public/robots.txt b/code/13-adding-dummy-data-and-more-content/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/13-adding-dummy-data-and-more-content/src/App.js b/code/13-adding-dummy-data-and-more-content/src/App.js new file mode 100644 index 0000000000..5f90130429 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/App.js @@ -0,0 +1,29 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; +import Layout from './components/layout/Layout'; + +function App() { + return ( + + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.js b/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.module.css b/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.js b/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.module.css b/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.js b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.module.css b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.js b/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.module.css b/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.js b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( + + ); +}; + +export default CommentsList; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.module.css b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.js b/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.module.css b/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.js b/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.js new file mode 100644 index 0000000000..2eff00a625 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.js @@ -0,0 +1,15 @@ +import { Fragment } from 'react'; + +import classes from './Layout.module.css'; +import MainNavigation from './MainNavigation'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.module.css b/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.js b/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.js new file mode 100644 index 0000000000..76d790a1c3 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.js @@ -0,0 +1,27 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainNavigation.module.css'; + +const MainNavigation = () => { + return ( +
    +
    Great Quotes
    + +
    + ); +}; + +export default MainNavigation; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.module.css b/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.js b/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.module.css b/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.js b/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..14a83fa67c --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,14 @@ +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.module.css b/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.js b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..9a49c7fb7c --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.js @@ -0,0 +1,47 @@ +import { useRef } from 'react'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + return ( + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.module.css b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.js b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..06cf8b1221 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.js @@ -0,0 +1,19 @@ +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.module.css b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.js b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..a37943f11c --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.js @@ -0,0 +1,23 @@ +import { Fragment } from 'react'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const QuoteList = (props) => { + return ( + +
      + {props.quotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.module.css b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/13-adding-dummy-data-and-more-content/src/index.css b/code/13-adding-dummy-data-and-more-content/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/13-adding-dummy-data-and-more-content/src/index.js b/code/13-adding-dummy-data-and-more-content/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/13-adding-dummy-data-and-more-content/src/pages/AllQuotes.js b/code/13-adding-dummy-data-and-more-content/src/pages/AllQuotes.js new file mode 100644 index 0000000000..6362e57d3f --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/pages/AllQuotes.js @@ -0,0 +1,12 @@ +import QuoteList from '../components/quotes/QuoteList'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const AllQuotes = () => { + return +}; + +export default AllQuotes; \ No newline at end of file diff --git a/code/13-adding-dummy-data-and-more-content/src/pages/NewQuote.js b/code/13-adding-dummy-data-and-more-content/src/pages/NewQuote.js new file mode 100644 index 0000000000..d5785be1fb --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/pages/NewQuote.js @@ -0,0 +1,11 @@ +import QuoteForm from '../components/quotes/QuoteForm'; + +const NewQuote = () => { + const addQuoteHandler = (quoteData) => { + console.log(quoteData); + }; + + return ; +}; + +export default NewQuote; diff --git a/code/13-adding-dummy-data-and-more-content/src/pages/QuoteDetail.js b/code/13-adding-dummy-data-and-more-content/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..5631383a1d --- /dev/null +++ b/code/13-adding-dummy-data-and-more-content/src/pages/QuoteDetail.js @@ -0,0 +1,20 @@ +import { Fragment } from 'react'; +import { useParams, Route } from 'react-router-dom'; + +import Comments from '../components/comments/Comments'; + +const QuoteDetail = () => { + const params = useParams(); + + return ( + +

    Quote Detail Page

    +

    {params.quoteId}

    + + + +
    + ); +}; + +export default QuoteDetail; diff --git a/code/14-outputting-data-on-details-page/package.json b/code/14-outputting-data-on-details-page/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/14-outputting-data-on-details-page/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/14-outputting-data-on-details-page/public/favicon.ico b/code/14-outputting-data-on-details-page/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/14-outputting-data-on-details-page/public/favicon.ico differ diff --git a/code/14-outputting-data-on-details-page/public/index.html b/code/14-outputting-data-on-details-page/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/14-outputting-data-on-details-page/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/14-outputting-data-on-details-page/public/logo192.png b/code/14-outputting-data-on-details-page/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/14-outputting-data-on-details-page/public/logo192.png differ diff --git a/code/14-outputting-data-on-details-page/public/logo512.png b/code/14-outputting-data-on-details-page/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/14-outputting-data-on-details-page/public/logo512.png differ diff --git a/code/14-outputting-data-on-details-page/public/manifest.json b/code/14-outputting-data-on-details-page/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/14-outputting-data-on-details-page/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/14-outputting-data-on-details-page/public/robots.txt b/code/14-outputting-data-on-details-page/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/14-outputting-data-on-details-page/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/14-outputting-data-on-details-page/src/App.js b/code/14-outputting-data-on-details-page/src/App.js new file mode 100644 index 0000000000..5f90130429 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/App.js @@ -0,0 +1,29 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; +import Layout from './components/layout/Layout'; + +function App() { + return ( + + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/14-outputting-data-on-details-page/src/components/UI/Card.js b/code/14-outputting-data-on-details-page/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/14-outputting-data-on-details-page/src/components/UI/Card.module.css b/code/14-outputting-data-on-details-page/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.js b/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.module.css b/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.js b/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.module.css b/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/14-outputting-data-on-details-page/src/components/comments/Comments.js b/code/14-outputting-data-on-details-page/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/14-outputting-data-on-details-page/src/components/comments/Comments.module.css b/code/14-outputting-data-on-details-page/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.js b/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( +
      + {props.comments.map((comment) => ( + + ))} +
    + ); +}; + +export default CommentsList; diff --git a/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.module.css b/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.js b/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.module.css b/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/14-outputting-data-on-details-page/src/components/layout/Layout.js b/code/14-outputting-data-on-details-page/src/components/layout/Layout.js new file mode 100644 index 0000000000..2eff00a625 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/layout/Layout.js @@ -0,0 +1,15 @@ +import { Fragment } from 'react'; + +import classes from './Layout.module.css'; +import MainNavigation from './MainNavigation'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/14-outputting-data-on-details-page/src/components/layout/Layout.module.css b/code/14-outputting-data-on-details-page/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.js b/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.js new file mode 100644 index 0000000000..76d790a1c3 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.js @@ -0,0 +1,27 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainNavigation.module.css'; + +const MainNavigation = () => { + return ( +
    +
    Great Quotes
    + +
    + ); +}; + +export default MainNavigation; diff --git a/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.module.css b/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.js b/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.module.css b/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.js b/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..14a83fa67c --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,14 @@ +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.module.css b/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.js b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..9a49c7fb7c --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.js @@ -0,0 +1,47 @@ +import { useRef } from 'react'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + return ( + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.module.css b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.js b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..ee1ccb3ddc --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.js @@ -0,0 +1,21 @@ +import { Link } from 'react-router-dom'; + +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.module.css b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.js b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..a37943f11c --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.js @@ -0,0 +1,23 @@ +import { Fragment } from 'react'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const QuoteList = (props) => { + return ( + +
      + {props.quotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.module.css b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/14-outputting-data-on-details-page/src/index.css b/code/14-outputting-data-on-details-page/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/14-outputting-data-on-details-page/src/index.js b/code/14-outputting-data-on-details-page/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/14-outputting-data-on-details-page/src/pages/AllQuotes.js b/code/14-outputting-data-on-details-page/src/pages/AllQuotes.js new file mode 100644 index 0000000000..6362e57d3f --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/pages/AllQuotes.js @@ -0,0 +1,12 @@ +import QuoteList from '../components/quotes/QuoteList'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const AllQuotes = () => { + return +}; + +export default AllQuotes; \ No newline at end of file diff --git a/code/14-outputting-data-on-details-page/src/pages/NewQuote.js b/code/14-outputting-data-on-details-page/src/pages/NewQuote.js new file mode 100644 index 0000000000..d5785be1fb --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/pages/NewQuote.js @@ -0,0 +1,11 @@ +import QuoteForm from '../components/quotes/QuoteForm'; + +const NewQuote = () => { + const addQuoteHandler = (quoteData) => { + console.log(quoteData); + }; + + return ; +}; + +export default NewQuote; diff --git a/code/14-outputting-data-on-details-page/src/pages/QuoteDetail.js b/code/14-outputting-data-on-details-page/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..8381efa2ac --- /dev/null +++ b/code/14-outputting-data-on-details-page/src/pages/QuoteDetail.js @@ -0,0 +1,31 @@ +import { Fragment } from 'react'; +import { useParams, Route } from 'react-router-dom'; + +import HighlightedQuote from '../components/quotes/HighlightedQuote'; +import Comments from '../components/comments/Comments'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const QuoteDetail = () => { + const params = useParams(); + + const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId); + + if (!quote) { + return

    No quote found!

    ; + } + + return ( + + + + + + + ); +}; + +export default QuoteDetail; diff --git a/code/15-adding-a-notfound-page/package.json b/code/15-adding-a-notfound-page/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/15-adding-a-notfound-page/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/15-adding-a-notfound-page/public/favicon.ico b/code/15-adding-a-notfound-page/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/15-adding-a-notfound-page/public/favicon.ico differ diff --git a/code/15-adding-a-notfound-page/public/index.html b/code/15-adding-a-notfound-page/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/15-adding-a-notfound-page/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/15-adding-a-notfound-page/public/logo192.png b/code/15-adding-a-notfound-page/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/15-adding-a-notfound-page/public/logo192.png differ diff --git a/code/15-adding-a-notfound-page/public/logo512.png b/code/15-adding-a-notfound-page/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/15-adding-a-notfound-page/public/logo512.png differ diff --git a/code/15-adding-a-notfound-page/public/manifest.json b/code/15-adding-a-notfound-page/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/15-adding-a-notfound-page/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/15-adding-a-notfound-page/public/robots.txt b/code/15-adding-a-notfound-page/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/15-adding-a-notfound-page/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/15-adding-a-notfound-page/src/App.js b/code/15-adding-a-notfound-page/src/App.js new file mode 100644 index 0000000000..97eb33fc84 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/App.js @@ -0,0 +1,33 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; +import NotFound from './pages/NotFound'; +import Layout from './components/layout/Layout'; + +function App() { + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/15-adding-a-notfound-page/src/components/UI/Card.js b/code/15-adding-a-notfound-page/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/15-adding-a-notfound-page/src/components/UI/Card.module.css b/code/15-adding-a-notfound-page/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.js b/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.module.css b/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/15-adding-a-notfound-page/src/components/comments/CommentItem.js b/code/15-adding-a-notfound-page/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/15-adding-a-notfound-page/src/components/comments/CommentItem.module.css b/code/15-adding-a-notfound-page/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/15-adding-a-notfound-page/src/components/comments/Comments.js b/code/15-adding-a-notfound-page/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/15-adding-a-notfound-page/src/components/comments/Comments.module.css b/code/15-adding-a-notfound-page/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/15-adding-a-notfound-page/src/components/comments/CommentsList.js b/code/15-adding-a-notfound-page/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( +
      + {props.comments.map((comment) => ( + + ))} +
    + ); +}; + +export default CommentsList; diff --git a/code/15-adding-a-notfound-page/src/components/comments/CommentsList.module.css b/code/15-adding-a-notfound-page/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.js b/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.module.css b/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/15-adding-a-notfound-page/src/components/layout/Layout.js b/code/15-adding-a-notfound-page/src/components/layout/Layout.js new file mode 100644 index 0000000000..2eff00a625 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/layout/Layout.js @@ -0,0 +1,15 @@ +import { Fragment } from 'react'; + +import classes from './Layout.module.css'; +import MainNavigation from './MainNavigation'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/15-adding-a-notfound-page/src/components/layout/Layout.module.css b/code/15-adding-a-notfound-page/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.js b/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.js new file mode 100644 index 0000000000..76d790a1c3 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.js @@ -0,0 +1,27 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainNavigation.module.css'; + +const MainNavigation = () => { + return ( +
    +
    Great Quotes
    + +
    + ); +}; + +export default MainNavigation; diff --git a/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.module.css b/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.js b/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.module.css b/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.js b/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..14a83fa67c --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,14 @@ +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.module.css b/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.js b/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..9a49c7fb7c --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.js @@ -0,0 +1,47 @@ +import { useRef } from 'react'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + return ( + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.module.css b/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.js b/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..ee1ccb3ddc --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.js @@ -0,0 +1,21 @@ +import { Link } from 'react-router-dom'; + +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.module.css b/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.js b/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..a37943f11c --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.js @@ -0,0 +1,23 @@ +import { Fragment } from 'react'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const QuoteList = (props) => { + return ( + +
      + {props.quotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.module.css b/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/15-adding-a-notfound-page/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/15-adding-a-notfound-page/src/index.css b/code/15-adding-a-notfound-page/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/15-adding-a-notfound-page/src/index.js b/code/15-adding-a-notfound-page/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/15-adding-a-notfound-page/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/15-adding-a-notfound-page/src/pages/AllQuotes.js b/code/15-adding-a-notfound-page/src/pages/AllQuotes.js new file mode 100644 index 0000000000..6362e57d3f --- /dev/null +++ b/code/15-adding-a-notfound-page/src/pages/AllQuotes.js @@ -0,0 +1,12 @@ +import QuoteList from '../components/quotes/QuoteList'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const AllQuotes = () => { + return +}; + +export default AllQuotes; \ No newline at end of file diff --git a/code/15-adding-a-notfound-page/src/pages/NewQuote.js b/code/15-adding-a-notfound-page/src/pages/NewQuote.js new file mode 100644 index 0000000000..d5785be1fb --- /dev/null +++ b/code/15-adding-a-notfound-page/src/pages/NewQuote.js @@ -0,0 +1,11 @@ +import QuoteForm from '../components/quotes/QuoteForm'; + +const NewQuote = () => { + const addQuoteHandler = (quoteData) => { + console.log(quoteData); + }; + + return ; +}; + +export default NewQuote; diff --git a/code/15-adding-a-notfound-page/src/pages/NotFound.js b/code/15-adding-a-notfound-page/src/pages/NotFound.js new file mode 100644 index 0000000000..bb7c85baac --- /dev/null +++ b/code/15-adding-a-notfound-page/src/pages/NotFound.js @@ -0,0 +1,9 @@ +const NotFound = () => { + return ( +
    +

    Page not found!

    +
    + ); +}; + +export default NotFound; diff --git a/code/15-adding-a-notfound-page/src/pages/QuoteDetail.js b/code/15-adding-a-notfound-page/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..8381efa2ac --- /dev/null +++ b/code/15-adding-a-notfound-page/src/pages/QuoteDetail.js @@ -0,0 +1,31 @@ +import { Fragment } from 'react'; +import { useParams, Route } from 'react-router-dom'; + +import HighlightedQuote from '../components/quotes/HighlightedQuote'; +import Comments from '../components/comments/Comments'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const QuoteDetail = () => { + const params = useParams(); + + const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId); + + if (!quote) { + return

    No quote found!

    ; + } + + return ( + + + + + + + ); +}; + +export default QuoteDetail; diff --git a/code/16-implementing-programmatic-navigation/package.json b/code/16-implementing-programmatic-navigation/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/16-implementing-programmatic-navigation/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/16-implementing-programmatic-navigation/public/favicon.ico b/code/16-implementing-programmatic-navigation/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/16-implementing-programmatic-navigation/public/favicon.ico differ diff --git a/code/16-implementing-programmatic-navigation/public/index.html b/code/16-implementing-programmatic-navigation/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/16-implementing-programmatic-navigation/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/16-implementing-programmatic-navigation/public/logo192.png b/code/16-implementing-programmatic-navigation/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/16-implementing-programmatic-navigation/public/logo192.png differ diff --git a/code/16-implementing-programmatic-navigation/public/logo512.png b/code/16-implementing-programmatic-navigation/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/16-implementing-programmatic-navigation/public/logo512.png differ diff --git a/code/16-implementing-programmatic-navigation/public/manifest.json b/code/16-implementing-programmatic-navigation/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/16-implementing-programmatic-navigation/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/16-implementing-programmatic-navigation/public/robots.txt b/code/16-implementing-programmatic-navigation/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/16-implementing-programmatic-navigation/src/App.js b/code/16-implementing-programmatic-navigation/src/App.js new file mode 100644 index 0000000000..97eb33fc84 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/App.js @@ -0,0 +1,33 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; +import NotFound from './pages/NotFound'; +import Layout from './components/layout/Layout'; + +function App() { + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/16-implementing-programmatic-navigation/src/components/UI/Card.js b/code/16-implementing-programmatic-navigation/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/16-implementing-programmatic-navigation/src/components/UI/Card.module.css b/code/16-implementing-programmatic-navigation/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.js b/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.module.css b/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.js b/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.module.css b/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/Comments.js b/code/16-implementing-programmatic-navigation/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/Comments.module.css b/code/16-implementing-programmatic-navigation/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.js b/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( +
      + {props.comments.map((comment) => ( + + ))} +
    + ); +}; + +export default CommentsList; diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.module.css b/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.js b/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.module.css b/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/16-implementing-programmatic-navigation/src/components/layout/Layout.js b/code/16-implementing-programmatic-navigation/src/components/layout/Layout.js new file mode 100644 index 0000000000..2eff00a625 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/layout/Layout.js @@ -0,0 +1,15 @@ +import { Fragment } from 'react'; + +import classes from './Layout.module.css'; +import MainNavigation from './MainNavigation'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/16-implementing-programmatic-navigation/src/components/layout/Layout.module.css b/code/16-implementing-programmatic-navigation/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.js b/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.js new file mode 100644 index 0000000000..76d790a1c3 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.js @@ -0,0 +1,27 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainNavigation.module.css'; + +const MainNavigation = () => { + return ( +
    +
    Great Quotes
    + +
    + ); +}; + +export default MainNavigation; diff --git a/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.module.css b/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.js b/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.module.css b/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.js b/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..14a83fa67c --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,14 @@ +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.module.css b/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.js b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..9a49c7fb7c --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.js @@ -0,0 +1,47 @@ +import { useRef } from 'react'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + return ( + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.module.css b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.js b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..ee1ccb3ddc --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.js @@ -0,0 +1,21 @@ +import { Link } from 'react-router-dom'; + +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.module.css b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.js b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..a37943f11c --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.js @@ -0,0 +1,23 @@ +import { Fragment } from 'react'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const QuoteList = (props) => { + return ( + +
      + {props.quotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.module.css b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/16-implementing-programmatic-navigation/src/index.css b/code/16-implementing-programmatic-navigation/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/16-implementing-programmatic-navigation/src/index.js b/code/16-implementing-programmatic-navigation/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/16-implementing-programmatic-navigation/src/pages/AllQuotes.js b/code/16-implementing-programmatic-navigation/src/pages/AllQuotes.js new file mode 100644 index 0000000000..6362e57d3f --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/pages/AllQuotes.js @@ -0,0 +1,12 @@ +import QuoteList from '../components/quotes/QuoteList'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const AllQuotes = () => { + return +}; + +export default AllQuotes; \ No newline at end of file diff --git a/code/16-implementing-programmatic-navigation/src/pages/NewQuote.js b/code/16-implementing-programmatic-navigation/src/pages/NewQuote.js new file mode 100644 index 0000000000..2bf8efb2d2 --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/pages/NewQuote.js @@ -0,0 +1,17 @@ +import { useHistory } from 'react-router-dom'; + +import QuoteForm from '../components/quotes/QuoteForm'; + +const NewQuote = () => { + const history = useHistory(); + + const addQuoteHandler = (quoteData) => { + console.log(quoteData); + + history.push('/quotes'); + }; + + return ; +}; + +export default NewQuote; diff --git a/code/16-implementing-programmatic-navigation/src/pages/NotFound.js b/code/16-implementing-programmatic-navigation/src/pages/NotFound.js new file mode 100644 index 0000000000..bb7c85baac --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/pages/NotFound.js @@ -0,0 +1,9 @@ +const NotFound = () => { + return ( +
    +

    Page not found!

    +
    + ); +}; + +export default NotFound; diff --git a/code/16-implementing-programmatic-navigation/src/pages/QuoteDetail.js b/code/16-implementing-programmatic-navigation/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..8381efa2ac --- /dev/null +++ b/code/16-implementing-programmatic-navigation/src/pages/QuoteDetail.js @@ -0,0 +1,31 @@ +import { Fragment } from 'react'; +import { useParams, Route } from 'react-router-dom'; + +import HighlightedQuote from '../components/quotes/HighlightedQuote'; +import Comments from '../components/comments/Comments'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const QuoteDetail = () => { + const params = useParams(); + + const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId); + + if (!quote) { + return

    No quote found!

    ; + } + + return ( + + + + + + + ); +}; + +export default QuoteDetail; diff --git a/code/17-preventing-unwanted-route-transitions/package.json b/code/17-preventing-unwanted-route-transitions/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/17-preventing-unwanted-route-transitions/public/favicon.ico b/code/17-preventing-unwanted-route-transitions/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/17-preventing-unwanted-route-transitions/public/favicon.ico differ diff --git a/code/17-preventing-unwanted-route-transitions/public/index.html b/code/17-preventing-unwanted-route-transitions/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/17-preventing-unwanted-route-transitions/public/logo192.png b/code/17-preventing-unwanted-route-transitions/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/17-preventing-unwanted-route-transitions/public/logo192.png differ diff --git a/code/17-preventing-unwanted-route-transitions/public/logo512.png b/code/17-preventing-unwanted-route-transitions/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/17-preventing-unwanted-route-transitions/public/logo512.png differ diff --git a/code/17-preventing-unwanted-route-transitions/public/manifest.json b/code/17-preventing-unwanted-route-transitions/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/17-preventing-unwanted-route-transitions/public/robots.txt b/code/17-preventing-unwanted-route-transitions/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/17-preventing-unwanted-route-transitions/src/App.js b/code/17-preventing-unwanted-route-transitions/src/App.js new file mode 100644 index 0000000000..97eb33fc84 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/App.js @@ -0,0 +1,33 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; +import NotFound from './pages/NotFound'; +import Layout from './components/layout/Layout'; + +function App() { + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.js b/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.module.css b/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.js b/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.module.css b/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.js b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.module.css b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.js b/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.module.css b/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.js b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( +
      + {props.comments.map((comment) => ( + + ))} +
    + ); +}; + +export default CommentsList; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.module.css b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.js b/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.module.css b/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.js b/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.js new file mode 100644 index 0000000000..2eff00a625 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.js @@ -0,0 +1,15 @@ +import { Fragment } from 'react'; + +import classes from './Layout.module.css'; +import MainNavigation from './MainNavigation'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.module.css b/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.js b/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.js new file mode 100644 index 0000000000..76d790a1c3 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.js @@ -0,0 +1,27 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainNavigation.module.css'; + +const MainNavigation = () => { + return ( +
    +
    Great Quotes
    + +
    + ); +}; + +export default MainNavigation; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.module.css b/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.js b/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.module.css b/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.js b/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..14a83fa67c --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,14 @@ +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.module.css b/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.js b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..c50916ab91 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.js @@ -0,0 +1,70 @@ +import { Fragment, useRef, useState } from 'react'; +import { Prompt } from 'react-router-dom'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const [isEntering, setIsEntering] = useState(false); + + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + const finishEnteringHandler = () => { + setIsEntering(false); + }; + + const formFocusedHandler = () => { + setIsEntering(true); + }; + + return ( + + + 'Are you sure you want to leave? All your entered data will be lost!' + } + /> + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.module.css b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.js b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..ee1ccb3ddc --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.js @@ -0,0 +1,21 @@ +import { Link } from 'react-router-dom'; + +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.module.css b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.js b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..a37943f11c --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.js @@ -0,0 +1,23 @@ +import { Fragment } from 'react'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const QuoteList = (props) => { + return ( + +
      + {props.quotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.module.css b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/17-preventing-unwanted-route-transitions/src/index.css b/code/17-preventing-unwanted-route-transitions/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/17-preventing-unwanted-route-transitions/src/index.js b/code/17-preventing-unwanted-route-transitions/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/17-preventing-unwanted-route-transitions/src/pages/AllQuotes.js b/code/17-preventing-unwanted-route-transitions/src/pages/AllQuotes.js new file mode 100644 index 0000000000..6362e57d3f --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/pages/AllQuotes.js @@ -0,0 +1,12 @@ +import QuoteList from '../components/quotes/QuoteList'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const AllQuotes = () => { + return +}; + +export default AllQuotes; \ No newline at end of file diff --git a/code/17-preventing-unwanted-route-transitions/src/pages/NewQuote.js b/code/17-preventing-unwanted-route-transitions/src/pages/NewQuote.js new file mode 100644 index 0000000000..2bf8efb2d2 --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/pages/NewQuote.js @@ -0,0 +1,17 @@ +import { useHistory } from 'react-router-dom'; + +import QuoteForm from '../components/quotes/QuoteForm'; + +const NewQuote = () => { + const history = useHistory(); + + const addQuoteHandler = (quoteData) => { + console.log(quoteData); + + history.push('/quotes'); + }; + + return ; +}; + +export default NewQuote; diff --git a/code/17-preventing-unwanted-route-transitions/src/pages/NotFound.js b/code/17-preventing-unwanted-route-transitions/src/pages/NotFound.js new file mode 100644 index 0000000000..bb7c85baac --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/pages/NotFound.js @@ -0,0 +1,9 @@ +const NotFound = () => { + return ( +
    +

    Page not found!

    +
    + ); +}; + +export default NotFound; diff --git a/code/17-preventing-unwanted-route-transitions/src/pages/QuoteDetail.js b/code/17-preventing-unwanted-route-transitions/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..8381efa2ac --- /dev/null +++ b/code/17-preventing-unwanted-route-transitions/src/pages/QuoteDetail.js @@ -0,0 +1,31 @@ +import { Fragment } from 'react'; +import { useParams, Route } from 'react-router-dom'; + +import HighlightedQuote from '../components/quotes/HighlightedQuote'; +import Comments from '../components/comments/Comments'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const QuoteDetail = () => { + const params = useParams(); + + const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId); + + if (!quote) { + return

    No quote found!

    ; + } + + return ( + + + + + + + ); +}; + +export default QuoteDetail; diff --git a/code/18-working-with-query-params/package.json b/code/18-working-with-query-params/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/18-working-with-query-params/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/18-working-with-query-params/public/favicon.ico b/code/18-working-with-query-params/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/18-working-with-query-params/public/favicon.ico differ diff --git a/code/18-working-with-query-params/public/index.html b/code/18-working-with-query-params/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/18-working-with-query-params/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/18-working-with-query-params/public/logo192.png b/code/18-working-with-query-params/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/18-working-with-query-params/public/logo192.png differ diff --git a/code/18-working-with-query-params/public/logo512.png b/code/18-working-with-query-params/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/18-working-with-query-params/public/logo512.png differ diff --git a/code/18-working-with-query-params/public/manifest.json b/code/18-working-with-query-params/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/18-working-with-query-params/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/18-working-with-query-params/public/robots.txt b/code/18-working-with-query-params/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/18-working-with-query-params/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/18-working-with-query-params/src/App.js b/code/18-working-with-query-params/src/App.js new file mode 100644 index 0000000000..97eb33fc84 --- /dev/null +++ b/code/18-working-with-query-params/src/App.js @@ -0,0 +1,33 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; +import NotFound from './pages/NotFound'; +import Layout from './components/layout/Layout'; + +function App() { + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/18-working-with-query-params/src/components/UI/Card.js b/code/18-working-with-query-params/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/18-working-with-query-params/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/18-working-with-query-params/src/components/UI/Card.module.css b/code/18-working-with-query-params/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/18-working-with-query-params/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/18-working-with-query-params/src/components/UI/LoadingSpinner.js b/code/18-working-with-query-params/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/18-working-with-query-params/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/18-working-with-query-params/src/components/UI/LoadingSpinner.module.css b/code/18-working-with-query-params/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/18-working-with-query-params/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/18-working-with-query-params/src/components/comments/CommentItem.js b/code/18-working-with-query-params/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/18-working-with-query-params/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/18-working-with-query-params/src/components/comments/CommentItem.module.css b/code/18-working-with-query-params/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/18-working-with-query-params/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/18-working-with-query-params/src/components/comments/Comments.js b/code/18-working-with-query-params/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/18-working-with-query-params/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/18-working-with-query-params/src/components/comments/Comments.module.css b/code/18-working-with-query-params/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/18-working-with-query-params/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/18-working-with-query-params/src/components/comments/CommentsList.js b/code/18-working-with-query-params/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/18-working-with-query-params/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( +
      + {props.comments.map((comment) => ( + + ))} +
    + ); +}; + +export default CommentsList; diff --git a/code/18-working-with-query-params/src/components/comments/CommentsList.module.css b/code/18-working-with-query-params/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/18-working-with-query-params/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/18-working-with-query-params/src/components/comments/NewCommentForm.js b/code/18-working-with-query-params/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/18-working-with-query-params/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/18-working-with-query-params/src/components/comments/NewCommentForm.module.css b/code/18-working-with-query-params/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/18-working-with-query-params/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/18-working-with-query-params/src/components/layout/Layout.js b/code/18-working-with-query-params/src/components/layout/Layout.js new file mode 100644 index 0000000000..2eff00a625 --- /dev/null +++ b/code/18-working-with-query-params/src/components/layout/Layout.js @@ -0,0 +1,15 @@ +import { Fragment } from 'react'; + +import classes from './Layout.module.css'; +import MainNavigation from './MainNavigation'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/18-working-with-query-params/src/components/layout/Layout.module.css b/code/18-working-with-query-params/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/18-working-with-query-params/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/18-working-with-query-params/src/components/layout/MainNavigation.js b/code/18-working-with-query-params/src/components/layout/MainNavigation.js new file mode 100644 index 0000000000..76d790a1c3 --- /dev/null +++ b/code/18-working-with-query-params/src/components/layout/MainNavigation.js @@ -0,0 +1,27 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainNavigation.module.css'; + +const MainNavigation = () => { + return ( +
    +
    Great Quotes
    + +
    + ); +}; + +export default MainNavigation; diff --git a/code/18-working-with-query-params/src/components/layout/MainNavigation.module.css b/code/18-working-with-query-params/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/18-working-with-query-params/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.js b/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.module.css b/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/18-working-with-query-params/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.js b/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..14a83fa67c --- /dev/null +++ b/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,14 @@ +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.module.css b/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/18-working-with-query-params/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteForm.js b/code/18-working-with-query-params/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..c50916ab91 --- /dev/null +++ b/code/18-working-with-query-params/src/components/quotes/QuoteForm.js @@ -0,0 +1,70 @@ +import { Fragment, useRef, useState } from 'react'; +import { Prompt } from 'react-router-dom'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const [isEntering, setIsEntering] = useState(false); + + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + const finishEnteringHandler = () => { + setIsEntering(false); + }; + + const formFocusedHandler = () => { + setIsEntering(true); + }; + + return ( + + + 'Are you sure you want to leave? All your entered data will be lost!' + } + /> + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteForm.module.css b/code/18-working-with-query-params/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/18-working-with-query-params/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteItem.js b/code/18-working-with-query-params/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..ee1ccb3ddc --- /dev/null +++ b/code/18-working-with-query-params/src/components/quotes/QuoteItem.js @@ -0,0 +1,21 @@ +import { Link } from 'react-router-dom'; + +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteItem.module.css b/code/18-working-with-query-params/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/18-working-with-query-params/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteList.js b/code/18-working-with-query-params/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..d320fbdf83 --- /dev/null +++ b/code/18-working-with-query-params/src/components/quotes/QuoteList.js @@ -0,0 +1,52 @@ +import { Fragment } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const sortQuotes = (quotes, ascending) => { + return quotes.sort((quoteA, quoteB) => { + if (ascending) { + return quoteA.id > quoteB.id ? 1 : -1; + } else { + return quoteA.id < quoteB.id ? 1 : -1; + } + }); +}; + +const QuoteList = (props) => { + const history = useHistory(); + const location = useLocation(); + + const queryParams = new URLSearchParams(location.search); + + const isSortingAscending = queryParams.get('sort') === 'asc'; + + const sortedQuotes = sortQuotes(props.quotes, isSortingAscending); + + const changeSortingHandler = () => { + history.push('/quotes?sort=' + (isSortingAscending ? 'desc' : 'asc')); + }; + + return ( + +
    + +
    +
      + {sortedQuotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/18-working-with-query-params/src/components/quotes/QuoteList.module.css b/code/18-working-with-query-params/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/18-working-with-query-params/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/18-working-with-query-params/src/index.css b/code/18-working-with-query-params/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/18-working-with-query-params/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/18-working-with-query-params/src/index.js b/code/18-working-with-query-params/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/18-working-with-query-params/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/18-working-with-query-params/src/pages/AllQuotes.js b/code/18-working-with-query-params/src/pages/AllQuotes.js new file mode 100644 index 0000000000..6362e57d3f --- /dev/null +++ b/code/18-working-with-query-params/src/pages/AllQuotes.js @@ -0,0 +1,12 @@ +import QuoteList from '../components/quotes/QuoteList'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const AllQuotes = () => { + return +}; + +export default AllQuotes; \ No newline at end of file diff --git a/code/18-working-with-query-params/src/pages/NewQuote.js b/code/18-working-with-query-params/src/pages/NewQuote.js new file mode 100644 index 0000000000..2bf8efb2d2 --- /dev/null +++ b/code/18-working-with-query-params/src/pages/NewQuote.js @@ -0,0 +1,17 @@ +import { useHistory } from 'react-router-dom'; + +import QuoteForm from '../components/quotes/QuoteForm'; + +const NewQuote = () => { + const history = useHistory(); + + const addQuoteHandler = (quoteData) => { + console.log(quoteData); + + history.push('/quotes'); + }; + + return ; +}; + +export default NewQuote; diff --git a/code/18-working-with-query-params/src/pages/NotFound.js b/code/18-working-with-query-params/src/pages/NotFound.js new file mode 100644 index 0000000000..bb7c85baac --- /dev/null +++ b/code/18-working-with-query-params/src/pages/NotFound.js @@ -0,0 +1,9 @@ +const NotFound = () => { + return ( +
    +

    Page not found!

    +
    + ); +}; + +export default NotFound; diff --git a/code/18-working-with-query-params/src/pages/QuoteDetail.js b/code/18-working-with-query-params/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..8381efa2ac --- /dev/null +++ b/code/18-working-with-query-params/src/pages/QuoteDetail.js @@ -0,0 +1,31 @@ +import { Fragment } from 'react'; +import { useParams, Route } from 'react-router-dom'; + +import HighlightedQuote from '../components/quotes/HighlightedQuote'; +import Comments from '../components/comments/Comments'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const QuoteDetail = () => { + const params = useParams(); + + const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId); + + if (!quote) { + return

    No quote found!

    ; + } + + return ( + + + + + + + ); +}; + +export default QuoteDetail; diff --git a/code/19-writing-more-flexible-routing-code/package.json b/code/19-writing-more-flexible-routing-code/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/19-writing-more-flexible-routing-code/public/favicon.ico b/code/19-writing-more-flexible-routing-code/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/19-writing-more-flexible-routing-code/public/favicon.ico differ diff --git a/code/19-writing-more-flexible-routing-code/public/index.html b/code/19-writing-more-flexible-routing-code/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/19-writing-more-flexible-routing-code/public/logo192.png b/code/19-writing-more-flexible-routing-code/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/19-writing-more-flexible-routing-code/public/logo192.png differ diff --git a/code/19-writing-more-flexible-routing-code/public/logo512.png b/code/19-writing-more-flexible-routing-code/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/19-writing-more-flexible-routing-code/public/logo512.png differ diff --git a/code/19-writing-more-flexible-routing-code/public/manifest.json b/code/19-writing-more-flexible-routing-code/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/19-writing-more-flexible-routing-code/public/robots.txt b/code/19-writing-more-flexible-routing-code/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/19-writing-more-flexible-routing-code/src/App.js b/code/19-writing-more-flexible-routing-code/src/App.js new file mode 100644 index 0000000000..97eb33fc84 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/App.js @@ -0,0 +1,33 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; +import NotFound from './pages/NotFound'; +import Layout from './components/layout/Layout'; + +function App() { + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/19-writing-more-flexible-routing-code/src/components/UI/Card.js b/code/19-writing-more-flexible-routing-code/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/19-writing-more-flexible-routing-code/src/components/UI/Card.module.css b/code/19-writing-more-flexible-routing-code/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.js b/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.module.css b/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.js b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.module.css b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.js b/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.module.css b/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.js b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( +
      + {props.comments.map((comment) => ( + + ))} +
    + ); +}; + +export default CommentsList; diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.module.css b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.js b/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.module.css b/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.js b/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.js new file mode 100644 index 0000000000..2eff00a625 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.js @@ -0,0 +1,15 @@ +import { Fragment } from 'react'; + +import classes from './Layout.module.css'; +import MainNavigation from './MainNavigation'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.module.css b/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.js b/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.js new file mode 100644 index 0000000000..76d790a1c3 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.js @@ -0,0 +1,27 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainNavigation.module.css'; + +const MainNavigation = () => { + return ( +
    +
    Great Quotes
    + +
    + ); +}; + +export default MainNavigation; diff --git a/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.module.css b/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.js b/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.module.css b/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.js b/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..14a83fa67c --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,14 @@ +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.module.css b/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.js b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..c50916ab91 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.js @@ -0,0 +1,70 @@ +import { Fragment, useRef, useState } from 'react'; +import { Prompt } from 'react-router-dom'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const [isEntering, setIsEntering] = useState(false); + + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + const finishEnteringHandler = () => { + setIsEntering(false); + }; + + const formFocusedHandler = () => { + setIsEntering(true); + }; + + return ( + + + 'Are you sure you want to leave? All your entered data will be lost!' + } + /> + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.module.css b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.js b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..ee1ccb3ddc --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.js @@ -0,0 +1,21 @@ +import { Link } from 'react-router-dom'; + +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.module.css b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.js b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..05cab9d98b --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.js @@ -0,0 +1,55 @@ +import { Fragment } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const sortQuotes = (quotes, ascending) => { + return quotes.sort((quoteA, quoteB) => { + if (ascending) { + return quoteA.id > quoteB.id ? 1 : -1; + } else { + return quoteA.id < quoteB.id ? 1 : -1; + } + }); +}; + +const QuoteList = (props) => { + const history = useHistory(); + const location = useLocation(); + + const queryParams = new URLSearchParams(location.search); + + const isSortingAscending = queryParams.get('sort') === 'asc'; + + const sortedQuotes = sortQuotes(props.quotes, isSortingAscending); + + const changeSortingHandler = () => { + history.push({ + pathname: location.pathname, + search: `?sort=${(isSortingAscending ? 'desc' : 'asc')}` + }); + }; + + return ( + +
    + +
    +
      + {sortedQuotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.module.css b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/19-writing-more-flexible-routing-code/src/index.css b/code/19-writing-more-flexible-routing-code/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/19-writing-more-flexible-routing-code/src/index.js b/code/19-writing-more-flexible-routing-code/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/19-writing-more-flexible-routing-code/src/pages/AllQuotes.js b/code/19-writing-more-flexible-routing-code/src/pages/AllQuotes.js new file mode 100644 index 0000000000..6362e57d3f --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/pages/AllQuotes.js @@ -0,0 +1,12 @@ +import QuoteList from '../components/quotes/QuoteList'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const AllQuotes = () => { + return +}; + +export default AllQuotes; \ No newline at end of file diff --git a/code/19-writing-more-flexible-routing-code/src/pages/NewQuote.js b/code/19-writing-more-flexible-routing-code/src/pages/NewQuote.js new file mode 100644 index 0000000000..2bf8efb2d2 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/pages/NewQuote.js @@ -0,0 +1,17 @@ +import { useHistory } from 'react-router-dom'; + +import QuoteForm from '../components/quotes/QuoteForm'; + +const NewQuote = () => { + const history = useHistory(); + + const addQuoteHandler = (quoteData) => { + console.log(quoteData); + + history.push('/quotes'); + }; + + return ; +}; + +export default NewQuote; diff --git a/code/19-writing-more-flexible-routing-code/src/pages/NotFound.js b/code/19-writing-more-flexible-routing-code/src/pages/NotFound.js new file mode 100644 index 0000000000..bb7c85baac --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/pages/NotFound.js @@ -0,0 +1,9 @@ +const NotFound = () => { + return ( +
    +

    Page not found!

    +
    + ); +}; + +export default NotFound; diff --git a/code/19-writing-more-flexible-routing-code/src/pages/QuoteDetail.js b/code/19-writing-more-flexible-routing-code/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..db5ef5d861 --- /dev/null +++ b/code/19-writing-more-flexible-routing-code/src/pages/QuoteDetail.js @@ -0,0 +1,39 @@ +import { Fragment } from 'react'; +import { useParams, Route, Link, useRouteMatch } from 'react-router-dom'; + +import HighlightedQuote from '../components/quotes/HighlightedQuote'; +import Comments from '../components/comments/Comments'; + +const DUMMY_QUOTES = [ + { id: 'q1', author: 'Max', text: 'Learning React is fun!' }, + { id: 'q2', author: 'Maximilian', text: 'Learning React is great!' }, +]; + +const QuoteDetail = () => { + const match = useRouteMatch(); + const params = useParams(); + + const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId); + + if (!quote) { + return

    No quote found!

    ; + } + + return ( + + + +
    + + Load Comments + +
    +
    + + + +
    + ); +}; + +export default QuoteDetail; diff --git a/code/20-sending-getting-quote-data/package.json b/code/20-sending-getting-quote-data/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/20-sending-getting-quote-data/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/20-sending-getting-quote-data/public/favicon.ico b/code/20-sending-getting-quote-data/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/20-sending-getting-quote-data/public/favicon.ico differ diff --git a/code/20-sending-getting-quote-data/public/index.html b/code/20-sending-getting-quote-data/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/20-sending-getting-quote-data/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/20-sending-getting-quote-data/public/logo192.png b/code/20-sending-getting-quote-data/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/20-sending-getting-quote-data/public/logo192.png differ diff --git a/code/20-sending-getting-quote-data/public/logo512.png b/code/20-sending-getting-quote-data/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/20-sending-getting-quote-data/public/logo512.png differ diff --git a/code/20-sending-getting-quote-data/public/manifest.json b/code/20-sending-getting-quote-data/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/20-sending-getting-quote-data/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/20-sending-getting-quote-data/public/robots.txt b/code/20-sending-getting-quote-data/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/20-sending-getting-quote-data/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/20-sending-getting-quote-data/src/App.js b/code/20-sending-getting-quote-data/src/App.js new file mode 100644 index 0000000000..97eb33fc84 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/App.js @@ -0,0 +1,33 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; +import NotFound from './pages/NotFound'; +import Layout from './components/layout/Layout'; + +function App() { + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/20-sending-getting-quote-data/src/components/UI/Card.js b/code/20-sending-getting-quote-data/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/20-sending-getting-quote-data/src/components/UI/Card.module.css b/code/20-sending-getting-quote-data/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.js b/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.module.css b/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/20-sending-getting-quote-data/src/components/comments/CommentItem.js b/code/20-sending-getting-quote-data/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/20-sending-getting-quote-data/src/components/comments/CommentItem.module.css b/code/20-sending-getting-quote-data/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/20-sending-getting-quote-data/src/components/comments/Comments.js b/code/20-sending-getting-quote-data/src/components/comments/Comments.js new file mode 100644 index 0000000000..6dbd006177 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/comments/Comments.js @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && } +

    Comments...

    +
    + ); +}; + +export default Comments; diff --git a/code/20-sending-getting-quote-data/src/components/comments/Comments.module.css b/code/20-sending-getting-quote-data/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/20-sending-getting-quote-data/src/components/comments/CommentsList.js b/code/20-sending-getting-quote-data/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( +
      + {props.comments.map((comment) => ( + + ))} +
    + ); +}; + +export default CommentsList; diff --git a/code/20-sending-getting-quote-data/src/components/comments/CommentsList.module.css b/code/20-sending-getting-quote-data/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.js b/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..2950b1e837 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.js @@ -0,0 +1,29 @@ +import { useRef } from 'react'; + +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const submitFormHandler = (event) => { + event.preventDefault(); + + // optional: Could validate here + + // send comment to server + }; + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.module.css b/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/20-sending-getting-quote-data/src/components/layout/Layout.js b/code/20-sending-getting-quote-data/src/components/layout/Layout.js new file mode 100644 index 0000000000..2eff00a625 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/layout/Layout.js @@ -0,0 +1,15 @@ +import { Fragment } from 'react'; + +import classes from './Layout.module.css'; +import MainNavigation from './MainNavigation'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/20-sending-getting-quote-data/src/components/layout/Layout.module.css b/code/20-sending-getting-quote-data/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.js b/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.js new file mode 100644 index 0000000000..76d790a1c3 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.js @@ -0,0 +1,27 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainNavigation.module.css'; + +const MainNavigation = () => { + return ( +
    +
    Great Quotes
    + +
    + ); +}; + +export default MainNavigation; diff --git a/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.module.css b/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.js b/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.module.css b/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.js b/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..8642a10db4 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,16 @@ +import { Link } from 'react-router-dom'; + +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.module.css b/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.js b/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..c50916ab91 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.js @@ -0,0 +1,70 @@ +import { Fragment, useRef, useState } from 'react'; +import { Prompt } from 'react-router-dom'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const [isEntering, setIsEntering] = useState(false); + + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + const finishEnteringHandler = () => { + setIsEntering(false); + }; + + const formFocusedHandler = () => { + setIsEntering(true); + }; + + return ( + + + 'Are you sure you want to leave? All your entered data will be lost!' + } + /> + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.module.css b/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.js b/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..ee1ccb3ddc --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.js @@ -0,0 +1,21 @@ +import { Link } from 'react-router-dom'; + +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.module.css b/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.js b/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..05cab9d98b --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.js @@ -0,0 +1,55 @@ +import { Fragment } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const sortQuotes = (quotes, ascending) => { + return quotes.sort((quoteA, quoteB) => { + if (ascending) { + return quoteA.id > quoteB.id ? 1 : -1; + } else { + return quoteA.id < quoteB.id ? 1 : -1; + } + }); +}; + +const QuoteList = (props) => { + const history = useHistory(); + const location = useLocation(); + + const queryParams = new URLSearchParams(location.search); + + const isSortingAscending = queryParams.get('sort') === 'asc'; + + const sortedQuotes = sortQuotes(props.quotes, isSortingAscending); + + const changeSortingHandler = () => { + history.push({ + pathname: location.pathname, + search: `?sort=${(isSortingAscending ? 'desc' : 'asc')}` + }); + }; + + return ( + +
    + +
    +
      + {sortedQuotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.module.css b/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/20-sending-getting-quote-data/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/20-sending-getting-quote-data/src/hooks/use-http.js b/code/20-sending-getting-quote-data/src/hooks/use-http.js new file mode 100644 index 0000000000..b7c56bc76a --- /dev/null +++ b/code/20-sending-getting-quote-data/src/hooks/use-http.js @@ -0,0 +1,60 @@ +import { useReducer, useCallback } from 'react'; + +function httpReducer(state, action) { + if (action.type === 'SEND') { + return { + data: null, + error: null, + status: 'pending', + }; + } + + if (action.type === 'SUCCESS') { + return { + data: action.responseData, + error: null, + status: 'completed', + }; + } + + if (action.type === 'ERROR') { + return { + data: null, + error: action.errorMessage, + status: 'completed', + }; + } + + return state; +} + +function useHttp(requestFunction, startWithPending = false) { + const [httpState, dispatch] = useReducer(httpReducer, { + status: startWithPending ? 'pending' : null, + data: null, + error: null, + }); + + const sendRequest = useCallback( + async function (requestData) { + dispatch({ type: 'SEND' }); + try { + const responseData = await requestFunction(requestData); + dispatch({ type: 'SUCCESS', responseData }); + } catch (error) { + dispatch({ + type: 'ERROR', + errorMessage: error.message || 'Something went wrong!', + }); + } + }, + [requestFunction] + ); + + return { + sendRequest, + ...httpState, + }; +} + +export default useHttp; diff --git a/code/20-sending-getting-quote-data/src/index.css b/code/20-sending-getting-quote-data/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/20-sending-getting-quote-data/src/index.js b/code/20-sending-getting-quote-data/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/20-sending-getting-quote-data/src/lib/api.js b/code/20-sending-getting-quote-data/src/lib/api.js new file mode 100644 index 0000000000..17d2b1a359 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/lib/api.js @@ -0,0 +1,96 @@ +const FIREBASE_DOMAIN = 'https://react-prep-default-rtdb.firebaseio.com'; + +export async function getAllQuotes() { + const response = await fetch(`${FIREBASE_DOMAIN}/quotes.json`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Could not fetch quotes.'); + } + + const transformedQuotes = []; + + for (const key in data) { + const quoteObj = { + id: key, + ...data[key], + }; + + transformedQuotes.push(quoteObj); + } + + return transformedQuotes; +} + +export async function getSingleQuote(quoteId) { + const response = await fetch(`${FIREBASE_DOMAIN}/quotes/${quoteId}.json`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Could not fetch quote.'); + } + + const loadedQuote = { + id: quoteId, + ...data, + }; + + return loadedQuote; +} + +export async function addQuote(quoteData) { + const response = await fetch(`${FIREBASE_DOMAIN}/quotes.json`, { + method: 'POST', + body: JSON.stringify(quoteData), + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Could not create quote.'); + } + + return null; +} + +export async function addComment(commentData, quoteId) { + const response = await fetch(`${FIREBASE_DOMAIN}/comments/${quoteId}.json`, { + method: 'POST', + body: JSON.stringify(commentData), + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Could not add comment.'); + } + + return { commentId: data.name }; +} + +export async function getAllComments(quoteId) { + const response = await fetch(`${FIREBASE_DOMAIN}/comments/${quoteId}.json`); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Could not get comments.'); + } + + const transformedComments = []; + + for (const key in data) { + const commentObj = { + id: key, + ...data[key], + }; + + transformedComments.push(commentObj); + } + + return transformedComments; +} diff --git a/code/20-sending-getting-quote-data/src/pages/AllQuotes.js b/code/20-sending-getting-quote-data/src/pages/AllQuotes.js new file mode 100644 index 0000000000..ffd486ebc9 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/pages/AllQuotes.js @@ -0,0 +1,38 @@ +import { useEffect } from 'react'; + +import QuoteList from '../components/quotes/QuoteList'; +import LoadingSpinner from '../components/UI/LoadingSpinner'; +import NoQuotesFound from '../components/quotes/NoQuotesFound'; +import useHttp from '../hooks/use-http'; +import { getAllQuotes } from '../lib/api'; + +const AllQuotes = () => { + const { sendRequest, status, data: loadedQuotes, error } = useHttp( + getAllQuotes, + true + ); + + useEffect(() => { + sendRequest(); + }, [sendRequest]); + + if (status === 'pending') { + return ( +
    + +
    + ); + } + + if (error) { + return

    {error}

    ; + } + + if (status === 'completed' && (!loadedQuotes || loadedQuotes.length === 0)) { + return ; + } + + return ; +}; + +export default AllQuotes; diff --git a/code/20-sending-getting-quote-data/src/pages/NewQuote.js b/code/20-sending-getting-quote-data/src/pages/NewQuote.js new file mode 100644 index 0000000000..b571e25788 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/pages/NewQuote.js @@ -0,0 +1,25 @@ +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + +import QuoteForm from '../components/quotes/QuoteForm'; +import useHttp from '../hooks/use-http'; +import { addQuote } from '../lib/api'; + +const NewQuote = () => { + const { sendRequest, status } = useHttp(addQuote); + const history = useHistory(); + + useEffect(() => { + if (status === 'completed') { + history.push('/quotes'); + } + }, [status, history]); + + const addQuoteHandler = (quoteData) => { + sendRequest(quoteData); + }; + + return ; +}; + +export default NewQuote; diff --git a/code/20-sending-getting-quote-data/src/pages/NotFound.js b/code/20-sending-getting-quote-data/src/pages/NotFound.js new file mode 100644 index 0000000000..bb7c85baac --- /dev/null +++ b/code/20-sending-getting-quote-data/src/pages/NotFound.js @@ -0,0 +1,9 @@ +const NotFound = () => { + return ( +
    +

    Page not found!

    +
    + ); +}; + +export default NotFound; diff --git a/code/20-sending-getting-quote-data/src/pages/QuoteDetail.js b/code/20-sending-getting-quote-data/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..3992b44738 --- /dev/null +++ b/code/20-sending-getting-quote-data/src/pages/QuoteDetail.js @@ -0,0 +1,58 @@ +import { Fragment, useEffect } from 'react'; +import { useParams, Route, Link, useRouteMatch } from 'react-router-dom'; + +import HighlightedQuote from '../components/quotes/HighlightedQuote'; +import Comments from '../components/comments/Comments'; +import useHttp from '../hooks/use-http'; +import { getSingleQuote } from '../lib/api'; +import LoadingSpinner from '../components/UI/LoadingSpinner'; + +const QuoteDetail = () => { + const match = useRouteMatch(); + const params = useParams(); + + const { quoteId } = params; + + const { sendRequest, status, data: loadedQuote, error } = useHttp( + getSingleQuote, + true + ); + + useEffect(() => { + sendRequest(quoteId); + }, [sendRequest, quoteId]); + + if (status === 'pending') { + return ( +
    + +
    + ); + } + + if (error) { + return

    {error}

    ; + } + + if (!loadedQuote.text) { + return

    No quote found!

    ; + } + + return ( + + + +
    + + Load Comments + +
    +
    + + + +
    + ); +}; + +export default QuoteDetail; diff --git a/code/21-finished/package.json b/code/21-finished/package.json new file mode 100644 index 0000000000..c0645e836b --- /dev/null +++ b/code/21-finished/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/21-finished/public/favicon.ico b/code/21-finished/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/21-finished/public/favicon.ico differ diff --git a/code/21-finished/public/index.html b/code/21-finished/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/21-finished/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/21-finished/public/logo192.png b/code/21-finished/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/21-finished/public/logo192.png differ diff --git a/code/21-finished/public/logo512.png b/code/21-finished/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/21-finished/public/logo512.png differ diff --git a/code/21-finished/public/manifest.json b/code/21-finished/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/21-finished/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/21-finished/public/robots.txt b/code/21-finished/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/21-finished/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/21-finished/src/App.js b/code/21-finished/src/App.js new file mode 100644 index 0000000000..97eb33fc84 --- /dev/null +++ b/code/21-finished/src/App.js @@ -0,0 +1,33 @@ +import { Route, Switch, Redirect } from 'react-router-dom'; + +import AllQuotes from './pages/AllQuotes'; +import QuoteDetail from './pages/QuoteDetail'; +import NewQuote from './pages/NewQuote'; +import NotFound from './pages/NotFound'; +import Layout from './components/layout/Layout'; + +function App() { + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/code/21-finished/src/components/UI/Card.js b/code/21-finished/src/components/UI/Card.js new file mode 100644 index 0000000000..03c3af7db2 --- /dev/null +++ b/code/21-finished/src/components/UI/Card.js @@ -0,0 +1,7 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return
    {props.children}
    ; +}; + +export default Card; diff --git a/code/21-finished/src/components/UI/Card.module.css b/code/21-finished/src/components/UI/Card.module.css new file mode 100644 index 0000000000..dad43b15fb --- /dev/null +++ b/code/21-finished/src/components/UI/Card.module.css @@ -0,0 +1,7 @@ +.card { + padding: 1rem; + margin: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: white; +} diff --git a/code/21-finished/src/components/UI/LoadingSpinner.js b/code/21-finished/src/components/UI/LoadingSpinner.js new file mode 100644 index 0000000000..4b88a52d45 --- /dev/null +++ b/code/21-finished/src/components/UI/LoadingSpinner.js @@ -0,0 +1,7 @@ +import classes from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return
    ; +} + +export default LoadingSpinner; diff --git a/code/21-finished/src/components/UI/LoadingSpinner.module.css b/code/21-finished/src/components/UI/LoadingSpinner.module.css new file mode 100644 index 0000000000..7b38ef62f1 --- /dev/null +++ b/code/21-finished/src/components/UI/LoadingSpinner.module.css @@ -0,0 +1,24 @@ +.spinner { + display: inline-block; + width: 80px; + height: 80px; +} +.spinner:after { + content: ' '; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid teal; + border-color: teal transparent teal transparent; + animation: spinner 1.2s linear infinite; +} +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/code/21-finished/src/components/comments/CommentItem.js b/code/21-finished/src/components/comments/CommentItem.js new file mode 100644 index 0000000000..448654f269 --- /dev/null +++ b/code/21-finished/src/components/comments/CommentItem.js @@ -0,0 +1,11 @@ +import classes from './CommentItem.module.css'; + +const CommentItem = (props) => { + return ( +
  • +

    {props.text}

    +
  • + ); +}; + +export default CommentItem; diff --git a/code/21-finished/src/components/comments/CommentItem.module.css b/code/21-finished/src/components/comments/CommentItem.module.css new file mode 100644 index 0000000000..21b1bef872 --- /dev/null +++ b/code/21-finished/src/components/comments/CommentItem.module.css @@ -0,0 +1,7 @@ +.item { + margin: 1rem 0; + color: #4a5555; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid teal;; +} \ No newline at end of file diff --git a/code/21-finished/src/components/comments/Comments.js b/code/21-finished/src/components/comments/Comments.js new file mode 100644 index 0000000000..99b9ae2eab --- /dev/null +++ b/code/21-finished/src/components/comments/Comments.js @@ -0,0 +1,71 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useParams } from 'react-router-dom'; + +import classes from './Comments.module.css'; +import NewCommentForm from './NewCommentForm'; +import useHttp from '../../hooks/use-http'; +import { getAllComments } from '../../lib/api'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import CommentsList from './CommentsList'; + +const Comments = () => { + const [isAddingComment, setIsAddingComment] = useState(false); + const params = useParams(); + + const { quoteId } = params; + + const { sendRequest, status, data: loadedComments } = useHttp(getAllComments); + + useEffect(() => { + sendRequest(quoteId); + }, [quoteId, sendRequest]); + + const startAddCommentHandler = () => { + setIsAddingComment(true); + }; + + const addedCommentHandler = useCallback(() => { + sendRequest(quoteId); + }, [sendRequest, quoteId]); + + let comments; + + if (status === 'pending') { + comments = ( +
    + +
    + ); + } + + if (status === 'completed' && loadedComments && loadedComments.length > 0) { + comments = ; + } + + if ( + status === 'completed' && + (!loadedComments || loadedComments.length === 0) + ) { + comments =

    No comments were added yet!

    ; + } + + return ( +
    +

    User Comments

    + {!isAddingComment && ( + + )} + {isAddingComment && ( + + )} + {comments} +
    + ); +}; + +export default Comments; diff --git a/code/21-finished/src/components/comments/Comments.module.css b/code/21-finished/src/components/comments/Comments.module.css new file mode 100644 index 0000000000..0fad756422 --- /dev/null +++ b/code/21-finished/src/components/comments/Comments.module.css @@ -0,0 +1,7 @@ +.comments { + text-align: center; +} + +.comments > button { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/code/21-finished/src/components/comments/CommentsList.js b/code/21-finished/src/components/comments/CommentsList.js new file mode 100644 index 0000000000..5e800a22e9 --- /dev/null +++ b/code/21-finished/src/components/comments/CommentsList.js @@ -0,0 +1,14 @@ +import CommentItem from './CommentItem'; +import classes from './CommentsList.module.css'; + +const CommentsList = (props) => { + return ( +
      + {props.comments.map((comment) => ( + + ))} +
    + ); +}; + +export default CommentsList; diff --git a/code/21-finished/src/components/comments/CommentsList.module.css b/code/21-finished/src/components/comments/CommentsList.module.css new file mode 100644 index 0000000000..6b7aaac226 --- /dev/null +++ b/code/21-finished/src/components/comments/CommentsList.module.css @@ -0,0 +1,5 @@ +.comments { + list-style: none; + margin: 2.5rem 0; + padding: 0; +} \ No newline at end of file diff --git a/code/21-finished/src/components/comments/NewCommentForm.js b/code/21-finished/src/components/comments/NewCommentForm.js new file mode 100644 index 0000000000..821f8c25a0 --- /dev/null +++ b/code/21-finished/src/components/comments/NewCommentForm.js @@ -0,0 +1,49 @@ +import { useRef, useEffect } from 'react'; + +import useHttp from '../../hooks/use-http'; +import { addComment } from '../../lib/api'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './NewCommentForm.module.css'; + +const NewCommentForm = (props) => { + const commentTextRef = useRef(); + + const { sendRequest, status, error } = useHttp(addComment); + + const { onAddedComment } = props; + + useEffect(() => { + if (status === 'completed' && !error) { + onAddedComment(); + } + }, [status, error, onAddedComment]); + + const submitFormHandler = (event) => { + event.preventDefault(); + + const enteredText = commentTextRef.current.value; + + // optional: Could validate here + + sendRequest({ commentData: { text: enteredText }, quoteId: props.quoteId }); + }; + + return ( +
    + {status === 'pending' && ( +
    + +
    + )} +
    + + +
    +
    + +
    +
    + ); +}; + +export default NewCommentForm; diff --git a/code/21-finished/src/components/comments/NewCommentForm.module.css b/code/21-finished/src/components/comments/NewCommentForm.module.css new file mode 100644 index 0000000000..3b2565652d --- /dev/null +++ b/code/21-finished/src/components/comments/NewCommentForm.module.css @@ -0,0 +1,45 @@ +.form { + margin-top: 1rem; + position: relative; + text-align: center; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/21-finished/src/components/layout/Layout.js b/code/21-finished/src/components/layout/Layout.js new file mode 100644 index 0000000000..2eff00a625 --- /dev/null +++ b/code/21-finished/src/components/layout/Layout.js @@ -0,0 +1,15 @@ +import { Fragment } from 'react'; + +import classes from './Layout.module.css'; +import MainNavigation from './MainNavigation'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/21-finished/src/components/layout/Layout.module.css b/code/21-finished/src/components/layout/Layout.module.css new file mode 100644 index 0000000000..eb13837358 --- /dev/null +++ b/code/21-finished/src/components/layout/Layout.module.css @@ -0,0 +1,5 @@ +.main { + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/21-finished/src/components/layout/MainNavigation.js b/code/21-finished/src/components/layout/MainNavigation.js new file mode 100644 index 0000000000..76d790a1c3 --- /dev/null +++ b/code/21-finished/src/components/layout/MainNavigation.js @@ -0,0 +1,27 @@ +import { NavLink } from 'react-router-dom'; + +import classes from './MainNavigation.module.css'; + +const MainNavigation = () => { + return ( +
    +
    Great Quotes
    + +
    + ); +}; + +export default MainNavigation; diff --git a/code/21-finished/src/components/layout/MainNavigation.module.css b/code/21-finished/src/components/layout/MainNavigation.module.css new file mode 100644 index 0000000000..be9d206679 --- /dev/null +++ b/code/21-finished/src/components/layout/MainNavigation.module.css @@ -0,0 +1,37 @@ +.header { + width: 100%; + height: 5rem; + display: flex; + padding: 0 10%; + justify-content: space-between; + align-items: center; + background-color: #008080; +} + +.logo { + font-size: 2rem; + color: white; +} + +.nav ul { + list-style: none; + display: flex; + margin: 0; + padding: 0; +} + +.nav li { + margin-left: 1.5rem; + font-size: 1.25rem; +} + +.nav a { + text-decoration: none; + color: #88dfdf; +} + +.nav a:hover, +.nav a:active, +.nav a.active { + color: #e6fcfc; +} diff --git a/code/21-finished/src/components/quotes/HighlightedQuote.js b/code/21-finished/src/components/quotes/HighlightedQuote.js new file mode 100644 index 0000000000..b6d3445c28 --- /dev/null +++ b/code/21-finished/src/components/quotes/HighlightedQuote.js @@ -0,0 +1,12 @@ +import classes from './HighlightedQuote.module.css'; + +const HighlightedQuote = (props) => { + return ( +
    +

    {props.text}

    +
    {props.author}
    +
    + ); +}; + +export default HighlightedQuote; diff --git a/code/21-finished/src/components/quotes/HighlightedQuote.module.css b/code/21-finished/src/components/quotes/HighlightedQuote.module.css new file mode 100644 index 0000000000..466b463010 --- /dev/null +++ b/code/21-finished/src/components/quotes/HighlightedQuote.module.css @@ -0,0 +1,20 @@ +.quote { + background-color: #162b2b; + color: white; + border-radius: 6px; + padding: 3rem; + margin: 3rem auto; + width: 90%; + max-width: 40rem; +} + +.quote p { + font-size: 2.5rem; +} + +.quote figcaption { + font-style: italic; + font-size: 1.5rem; + text-align: right; + color: #a1e0e0; +} \ No newline at end of file diff --git a/code/21-finished/src/components/quotes/NoQuotesFound.js b/code/21-finished/src/components/quotes/NoQuotesFound.js new file mode 100644 index 0000000000..8642a10db4 --- /dev/null +++ b/code/21-finished/src/components/quotes/NoQuotesFound.js @@ -0,0 +1,16 @@ +import { Link } from 'react-router-dom'; + +import classes from './NoQuotesFound.module.css'; + +const NoQuotesFound = () => { + return ( +
    +

    No quotes found!

    + + Add a Quote + +
    + ); +}; + +export default NoQuotesFound; diff --git a/code/21-finished/src/components/quotes/NoQuotesFound.module.css b/code/21-finished/src/components/quotes/NoQuotesFound.module.css new file mode 100644 index 0000000000..0d48b19f9b --- /dev/null +++ b/code/21-finished/src/components/quotes/NoQuotesFound.module.css @@ -0,0 +1,17 @@ +.noquotes { + height: 20rem; + margin: auto; + display: flex; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.noquotes p { + color: #262c2c; + font-size: 3rem; + font-weight: bold; +} + diff --git a/code/21-finished/src/components/quotes/QuoteForm.js b/code/21-finished/src/components/quotes/QuoteForm.js new file mode 100644 index 0000000000..c50916ab91 --- /dev/null +++ b/code/21-finished/src/components/quotes/QuoteForm.js @@ -0,0 +1,70 @@ +import { Fragment, useRef, useState } from 'react'; +import { Prompt } from 'react-router-dom'; + +import Card from '../UI/Card'; +import LoadingSpinner from '../UI/LoadingSpinner'; +import classes from './QuoteForm.module.css'; + +const QuoteForm = (props) => { + const [isEntering, setIsEntering] = useState(false); + + const authorInputRef = useRef(); + const textInputRef = useRef(); + + function submitFormHandler(event) { + event.preventDefault(); + + const enteredAuthor = authorInputRef.current.value; + const enteredText = textInputRef.current.value; + + // optional: Could validate here + + props.onAddQuote({ author: enteredAuthor, text: enteredText }); + } + + const finishEnteringHandler = () => { + setIsEntering(false); + }; + + const formFocusedHandler = () => { + setIsEntering(true); + }; + + return ( + + + 'Are you sure you want to leave? All your entered data will be lost!' + } + /> + +
    + {props.isLoading && ( +
    + +
    + )} + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    + ); +}; + +export default QuoteForm; diff --git a/code/21-finished/src/components/quotes/QuoteForm.module.css b/code/21-finished/src/components/quotes/QuoteForm.module.css new file mode 100644 index 0000000000..ee8d855137 --- /dev/null +++ b/code/21-finished/src/components/quotes/QuoteForm.module.css @@ -0,0 +1,49 @@ +.form { + position: relative; +} + +.loading { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.control { + margin-bottom: 0.5rem; +} + +.control label { + font-weight: bold; + display: block; + margin-bottom: 0.5rem; +} + +.control input, +.control textarea { + font: inherit; + padding: 0.35rem; + border-radius: 4px; + background-color: #f0f0f0; + border: 1px solid #c1d1d1; + display: block; + width: 100%; + font-size: 1.25rem; +} + +.control input:focus, +.control textarea:focus { + background-color: #cbf8f8; + outline-color: teal; +} + +.actions { + text-align: right; +} + +.actions button { + font-size: 1.25rem; +} diff --git a/code/21-finished/src/components/quotes/QuoteItem.js b/code/21-finished/src/components/quotes/QuoteItem.js new file mode 100644 index 0000000000..ee1ccb3ddc --- /dev/null +++ b/code/21-finished/src/components/quotes/QuoteItem.js @@ -0,0 +1,21 @@ +import { Link } from 'react-router-dom'; + +import classes from './QuoteItem.module.css'; + +const QuoteItem = (props) => { + return ( +
  • +
    +
    +

    {props.text}

    +
    +
    {props.author}
    +
    + + View Fullscreen + +
  • + ); +}; + +export default QuoteItem; diff --git a/code/21-finished/src/components/quotes/QuoteItem.module.css b/code/21-finished/src/components/quotes/QuoteItem.module.css new file mode 100644 index 0000000000..74cd09b8b7 --- /dev/null +++ b/code/21-finished/src/components/quotes/QuoteItem.module.css @@ -0,0 +1,37 @@ +.item { + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 6px; + background-color: #c2e7f0; +} + +.item:last-of-type { + border-bottom: none; +} + +.item figure { + margin: 0; + padding: 0; + width: 70%; +} + +.item blockquote { + margin: 0; + text-align: left; + font-size: 1.5rem; + color: #212929; +} + +.item p { + margin: 0; + margin-bottom: 0.25rem; +} + +.item figcaption { + font-style: italic; + color: #566d6d; +} diff --git a/code/21-finished/src/components/quotes/QuoteList.js b/code/21-finished/src/components/quotes/QuoteList.js new file mode 100644 index 0000000000..05cab9d98b --- /dev/null +++ b/code/21-finished/src/components/quotes/QuoteList.js @@ -0,0 +1,55 @@ +import { Fragment } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +import QuoteItem from './QuoteItem'; +import classes from './QuoteList.module.css'; + +const sortQuotes = (quotes, ascending) => { + return quotes.sort((quoteA, quoteB) => { + if (ascending) { + return quoteA.id > quoteB.id ? 1 : -1; + } else { + return quoteA.id < quoteB.id ? 1 : -1; + } + }); +}; + +const QuoteList = (props) => { + const history = useHistory(); + const location = useLocation(); + + const queryParams = new URLSearchParams(location.search); + + const isSortingAscending = queryParams.get('sort') === 'asc'; + + const sortedQuotes = sortQuotes(props.quotes, isSortingAscending); + + const changeSortingHandler = () => { + history.push({ + pathname: location.pathname, + search: `?sort=${(isSortingAscending ? 'desc' : 'asc')}` + }); + }; + + return ( + +
    + +
    +
      + {sortedQuotes.map((quote) => ( + + ))} +
    +
    + ); +}; + +export default QuoteList; diff --git a/code/21-finished/src/components/quotes/QuoteList.module.css b/code/21-finished/src/components/quotes/QuoteList.module.css new file mode 100644 index 0000000000..cfb5fbf9ab --- /dev/null +++ b/code/21-finished/src/components/quotes/QuoteList.module.css @@ -0,0 +1,25 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.sorting { + padding-bottom: 1rem; + border-bottom: 3px solid #b2d4d4; + margin-bottom: 2rem; +} + +.sorting button { + font: inherit; + color: teal; + border: 1px solid teal; + background-color: transparent; + border-radius: 4px; + padding: 0.5rem 1.5rem; + cursor: pointer; +} + +.sorting button:hover { + background-color: #c2fafa; +} \ No newline at end of file diff --git a/code/21-finished/src/hooks.zip b/code/21-finished/src/hooks.zip new file mode 100644 index 0000000000..41fb9a916d Binary files /dev/null and b/code/21-finished/src/hooks.zip differ diff --git a/code/21-finished/src/hooks/use-http.js b/code/21-finished/src/hooks/use-http.js new file mode 100644 index 0000000000..b7c56bc76a --- /dev/null +++ b/code/21-finished/src/hooks/use-http.js @@ -0,0 +1,60 @@ +import { useReducer, useCallback } from 'react'; + +function httpReducer(state, action) { + if (action.type === 'SEND') { + return { + data: null, + error: null, + status: 'pending', + }; + } + + if (action.type === 'SUCCESS') { + return { + data: action.responseData, + error: null, + status: 'completed', + }; + } + + if (action.type === 'ERROR') { + return { + data: null, + error: action.errorMessage, + status: 'completed', + }; + } + + return state; +} + +function useHttp(requestFunction, startWithPending = false) { + const [httpState, dispatch] = useReducer(httpReducer, { + status: startWithPending ? 'pending' : null, + data: null, + error: null, + }); + + const sendRequest = useCallback( + async function (requestData) { + dispatch({ type: 'SEND' }); + try { + const responseData = await requestFunction(requestData); + dispatch({ type: 'SUCCESS', responseData }); + } catch (error) { + dispatch({ + type: 'ERROR', + errorMessage: error.message || 'Something went wrong!', + }); + } + }, + [requestFunction] + ); + + return { + sendRequest, + ...httpState, + }; +} + +export default useHttp; diff --git a/code/21-finished/src/index.css b/code/21-finished/src/index.css new file mode 100644 index 0000000000..039c6d7fe6 --- /dev/null +++ b/code/21-finished/src/index.css @@ -0,0 +1,56 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 0; + background-color: #e7f8f8; +} + +.centered { + margin: 3rem auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.focused { + font-size: 3rem; + font-weight: bold; + color: white; +} + +.btn { + text-decoration: none; + background-color: teal; + color: white; + border-radius: 4px; + padding: 0.75rem 1.5rem; + border: 1px solid teal; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #11acac; + border-color: #11acac; +} + +.btn--flat { + cursor: pointer; + font: inherit; + color: teal; + border: none; + background-color: none; + text-decoration: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; +} + +.btn--flat:hover, +.btn--flat:active { + background-color: teal; + color: white; +} \ No newline at end of file diff --git a/code/21-finished/src/index.js b/code/21-finished/src/index.js new file mode 100644 index 0000000000..c2a6402346 --- /dev/null +++ b/code/21-finished/src/index.js @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/21-finished/src/lib.zip b/code/21-finished/src/lib.zip new file mode 100644 index 0000000000..3cd03daba9 Binary files /dev/null and b/code/21-finished/src/lib.zip differ diff --git a/code/21-finished/src/lib/api.js b/code/21-finished/src/lib/api.js new file mode 100644 index 0000000000..2bc84aac81 --- /dev/null +++ b/code/21-finished/src/lib/api.js @@ -0,0 +1,96 @@ +const FIREBASE_DOMAIN = 'https://react-prep-default-rtdb.firebaseio.com'; + +export async function getAllQuotes() { + const response = await fetch(`${FIREBASE_DOMAIN}/quotes.json`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Could not fetch quotes.'); + } + + const transformedQuotes = []; + + for (const key in data) { + const quoteObj = { + id: key, + ...data[key], + }; + + transformedQuotes.push(quoteObj); + } + + return transformedQuotes; +} + +export async function getSingleQuote(quoteId) { + const response = await fetch(`${FIREBASE_DOMAIN}/quotes/${quoteId}.json`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Could not fetch quote.'); + } + + const loadedQuote = { + id: quoteId, + ...data, + }; + + return loadedQuote; +} + +export async function addQuote(quoteData) { + const response = await fetch(`${FIREBASE_DOMAIN}/quotes.json`, { + method: 'POST', + body: JSON.stringify(quoteData), + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Could not create quote.'); + } + + return null; +} + +export async function addComment(requestData) { + const response = await fetch(`${FIREBASE_DOMAIN}/comments/${requestData.quoteId}.json`, { + method: 'POST', + body: JSON.stringify(requestData.commentData), + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Could not add comment.'); + } + + return { commentId: data.name }; +} + +export async function getAllComments(quoteId) { + const response = await fetch(`${FIREBASE_DOMAIN}/comments/${quoteId}.json`); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Could not get comments.'); + } + + const transformedComments = []; + + for (const key in data) { + const commentObj = { + id: key, + ...data[key], + }; + + transformedComments.push(commentObj); + } + + return transformedComments; +} diff --git a/code/21-finished/src/pages/AllQuotes.js b/code/21-finished/src/pages/AllQuotes.js new file mode 100644 index 0000000000..ffd486ebc9 --- /dev/null +++ b/code/21-finished/src/pages/AllQuotes.js @@ -0,0 +1,38 @@ +import { useEffect } from 'react'; + +import QuoteList from '../components/quotes/QuoteList'; +import LoadingSpinner from '../components/UI/LoadingSpinner'; +import NoQuotesFound from '../components/quotes/NoQuotesFound'; +import useHttp from '../hooks/use-http'; +import { getAllQuotes } from '../lib/api'; + +const AllQuotes = () => { + const { sendRequest, status, data: loadedQuotes, error } = useHttp( + getAllQuotes, + true + ); + + useEffect(() => { + sendRequest(); + }, [sendRequest]); + + if (status === 'pending') { + return ( +
    + +
    + ); + } + + if (error) { + return

    {error}

    ; + } + + if (status === 'completed' && (!loadedQuotes || loadedQuotes.length === 0)) { + return ; + } + + return ; +}; + +export default AllQuotes; diff --git a/code/21-finished/src/pages/NewQuote.js b/code/21-finished/src/pages/NewQuote.js new file mode 100644 index 0000000000..b571e25788 --- /dev/null +++ b/code/21-finished/src/pages/NewQuote.js @@ -0,0 +1,25 @@ +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + +import QuoteForm from '../components/quotes/QuoteForm'; +import useHttp from '../hooks/use-http'; +import { addQuote } from '../lib/api'; + +const NewQuote = () => { + const { sendRequest, status } = useHttp(addQuote); + const history = useHistory(); + + useEffect(() => { + if (status === 'completed') { + history.push('/quotes'); + } + }, [status, history]); + + const addQuoteHandler = (quoteData) => { + sendRequest(quoteData); + }; + + return ; +}; + +export default NewQuote; diff --git a/code/21-finished/src/pages/NotFound.js b/code/21-finished/src/pages/NotFound.js new file mode 100644 index 0000000000..bb7c85baac --- /dev/null +++ b/code/21-finished/src/pages/NotFound.js @@ -0,0 +1,9 @@ +const NotFound = () => { + return ( +
    +

    Page not found!

    +
    + ); +}; + +export default NotFound; diff --git a/code/21-finished/src/pages/QuoteDetail.js b/code/21-finished/src/pages/QuoteDetail.js new file mode 100644 index 0000000000..3992b44738 --- /dev/null +++ b/code/21-finished/src/pages/QuoteDetail.js @@ -0,0 +1,58 @@ +import { Fragment, useEffect } from 'react'; +import { useParams, Route, Link, useRouteMatch } from 'react-router-dom'; + +import HighlightedQuote from '../components/quotes/HighlightedQuote'; +import Comments from '../components/comments/Comments'; +import useHttp from '../hooks/use-http'; +import { getSingleQuote } from '../lib/api'; +import LoadingSpinner from '../components/UI/LoadingSpinner'; + +const QuoteDetail = () => { + const match = useRouteMatch(); + const params = useParams(); + + const { quoteId } = params; + + const { sendRequest, status, data: loadedQuote, error } = useHttp( + getSingleQuote, + true + ); + + useEffect(() => { + sendRequest(quoteId); + }, [sendRequest, quoteId]); + + if (status === 'pending') { + return ( +
    + +
    + ); + } + + if (error) { + return

    {error}

    ; + } + + if (!loadedQuote.text) { + return

    No quote found!

    ; + } + + return ( + + + +
    + + Load Comments + +
    +
    + + + +
    + ); +}; + +export default QuoteDetail; diff --git a/extra-files/MainHeader.module.css b/extra-files/MainHeader.module.css new file mode 100644 index 0000000000..0d792b4c0d --- /dev/null +++ b/extra-files/MainHeader.module.css @@ -0,0 +1,38 @@ +.header { + width: 100%; + height: 5rem; + background-color: #044599; + padding: 0 10%; +} + +.header nav { + height: 100%; +} + +.header ul { + height: 100%; + list-style: none; + display: flex; + padding: 0; + margin: 0; + align-items: center; + justify-content: center; +} + +.header li { + margin: 0 1rem; + width: 5rem; +} + +.header a { + color: white; + text-decoration: none; +} + +.header a:hover, +.header a:active, +.header a.active { + color: #95bcf0; + padding-bottom: 0.25rem; + border-bottom: 4px solid #95bcf0; +} \ No newline at end of file diff --git a/extra-files/hooks.zip b/extra-files/hooks.zip new file mode 100644 index 0000000000..41fb9a916d Binary files /dev/null and b/extra-files/hooks.zip differ diff --git a/extra-files/index.css b/extra-files/index.css new file mode 100644 index 0000000000..44a860502a --- /dev/null +++ b/extra-files/index.css @@ -0,0 +1,26 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #e0e9f5; +} + +main { + margin-top: 7rem; + text-align: center; +} + +h1, +h2, +h3, +p { + color: #042b5f; +} diff --git a/extra-files/lib.zip b/extra-files/lib.zip new file mode 100644 index 0000000000..3cd03daba9 Binary files /dev/null and b/extra-files/lib.zip differ diff --git a/extra-files/sorting.js b/extra-files/sorting.js new file mode 100644 index 0000000000..3423908b6a --- /dev/null +++ b/extra-files/sorting.js @@ -0,0 +1,9 @@ +const sortQuotes = (quotes, ascending) => { + return quotes.sort((quoteA, quoteB) => { + if (ascending) { + return quoteA.id > quoteB.id ? 1 : -1; + } else { + return quoteA.id < quoteB.id ? 1 : -1; + } + }); +}; diff --git a/slides/slides.pdf b/slides/slides.pdf new file mode 100644 index 0000000000..395967faef Binary files /dev/null and b/slides/slides.pdf differ