dom and states
This commit is contained in:
parent
3a29298624
commit
f8258a5754
36549
errors/package-lock.json
generated
Normal file
36549
errors/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
errors/package.json
Normal file
34
errors/package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "react-the-complete-guide",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
|
"@testing-library/react": "^9.4.1",
|
||||||
|
"@testing-library/user-event": "^7.2.1",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0",
|
||||||
|
"react-scripts": "4.0.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
errors/public/favicon.ico
Normal file
BIN
errors/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
43
errors/public/index.html
Normal file
43
errors/public/index.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
errors/public/logo192.png
Normal file
BIN
errors/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
errors/public/logo512.png
Normal file
BIN
errors/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
errors/public/manifest.json
Normal file
25
errors/public/manifest.json
Normal file
@ -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"
|
||||||
|
}
|
3
errors/public/robots.txt
Normal file
3
errors/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
14
errors/src/App.css
Normal file
14
errors/src/App.css
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#goals {
|
||||||
|
width: 35rem;
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 3rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#goal-form {
|
||||||
|
width: 30rem;
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 3rem auto;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
48
errors/src/App.js
Normal file
48
errors/src/App.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import CourseGoalList from "./components/CourseGoals/CourseGoalList/CourseGoalList";
|
||||||
|
import CourseInput from "./components/CourseGoals/CourseInput/CourseInput";
|
||||||
|
import "./App.css";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [courseGoals, setCourseGoals] = useState([
|
||||||
|
{ text: "Do all exercises!", id: "g1" },
|
||||||
|
{ text: "Finish the course!", id: "g2" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const addGoalHandler = (enteredText) => {
|
||||||
|
setCourseGoals((prevGoals) => {
|
||||||
|
const updatedGoals = [...prevGoals];
|
||||||
|
updatedGoals.unshift({ text: enteredText, id: "goal1" });
|
||||||
|
return updatedGoals;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteItemHandler = (goalId) => {
|
||||||
|
setCourseGoals((prevGoals) => {
|
||||||
|
const updatedGoals = prevGoals.filter((goal) => goal.id !== goalId);
|
||||||
|
return updatedGoals;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = (
|
||||||
|
<p style={{ textAlign: "center" }}>No goals found. Maybe add one?</p>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (courseGoals.length > 0) {
|
||||||
|
content = (
|
||||||
|
<CourseGoalList items={courseGoals} onDeleteItem={deleteItemHandler} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section id="goal-form">
|
||||||
|
<CourseInput onAddGoal={addGoalHandler} />
|
||||||
|
</section>
|
||||||
|
<section id="goals">{content}</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
@ -0,0 +1,8 @@
|
|||||||
|
.goal-item {
|
||||||
|
margin: 1rem 0;
|
||||||
|
background: #8b005d;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './CourseGoalItem.css';
|
||||||
|
|
||||||
|
const CourseGoalItem = (props) => {
|
||||||
|
const deleteHandler = () => {
|
||||||
|
props.onDelete(props.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="goal-item" onClick={deleteHandler}>
|
||||||
|
{props.children}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CourseGoalItem;
|
@ -0,0 +1,5 @@
|
|||||||
|
.goal-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import CourseGoalItem from '../CourseGoalItem/CourseGoalItem';
|
||||||
|
import './CourseGoalList.css';
|
||||||
|
|
||||||
|
const CourseGoalList = props => {
|
||||||
|
return (
|
||||||
|
<ul className="goal-list">
|
||||||
|
{props.items.map(goal => (
|
||||||
|
<CourseGoalItem
|
||||||
|
key={goal.id}
|
||||||
|
id={goal.id}
|
||||||
|
onDelete={props.onDeleteItem}
|
||||||
|
>
|
||||||
|
{goal.text}
|
||||||
|
</CourseGoalItem>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CourseGoalList;
|
39
errors/src/components/CourseGoals/CourseInput/CourseInput.js
Normal file
39
errors/src/components/CourseGoals/CourseInput/CourseInput.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import Button from '../../UI/Button/Button';
|
||||||
|
import styles from './CourseInput.module.css';
|
||||||
|
|
||||||
|
const CourseInput = (props) => {
|
||||||
|
const [enteredValue, setEnteredValue] = useState('');
|
||||||
|
const [isValid, setIsValid] = useState(true);
|
||||||
|
|
||||||
|
const goalInputChangeHandler = (event) => {
|
||||||
|
if (event.target.value.trim().length > 0) {
|
||||||
|
setIsValid(true);
|
||||||
|
}
|
||||||
|
setEnteredValue(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSubmitHandler = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (enteredValue.trim().length === 0) {
|
||||||
|
setIsValid(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.onAddGoal(enteredValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={formSubmitHandler}>
|
||||||
|
<div
|
||||||
|
className={`${styles['form-control']} ${!isValid && styles.invalid}`}
|
||||||
|
>
|
||||||
|
<label>Course Goal</label>
|
||||||
|
<input type="text" onChange={goalInputChangeHandler} />
|
||||||
|
</div>
|
||||||
|
<Button type="submit">Add Goal</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CourseInput;
|
@ -0,0 +1,33 @@
|
|||||||
|
.form-control {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control label {
|
||||||
|
font-weight: bold;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control input:focus {
|
||||||
|
outline: none;
|
||||||
|
background: #fad0ec;
|
||||||
|
border-color: #8b005d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control.invalid input {
|
||||||
|
border-color: red;
|
||||||
|
background: #ffd7d7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control.invalid label {
|
||||||
|
color: red;
|
||||||
|
}
|
13
errors/src/components/UI/Button/Button.js
Normal file
13
errors/src/components/UI/Button/Button.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './Button.module.css';
|
||||||
|
|
||||||
|
const Button = props => {
|
||||||
|
return (
|
||||||
|
<button type={props.type} className={styles.button} onClick={props.onClick}>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
27
errors/src/components/UI/Button/Button.module.css
Normal file
27
errors/src/components/UI/Button/Button.module.css
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
font: inherit;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
border: 1px solid #8b005d;
|
||||||
|
color: white;
|
||||||
|
background: #8b005d;
|
||||||
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.26);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover,
|
||||||
|
.button:active {
|
||||||
|
background: #ac0e77;
|
||||||
|
border-color: #ac0e77;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.button {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
11
errors/src/index.css
Normal file
11
errors/src/index.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
8
errors/src/index.js
Normal file
8
errors/src/index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(<App />);
|
35023
react-chap9/package-lock.json
generated
Normal file
35023
react-chap9/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
react-chap9/package.json
Normal file
34
react-chap9/package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "react-the-complete-guide",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
|
"@testing-library/react": "^9.4.1",
|
||||||
|
"@testing-library/user-event": "^7.2.1",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0",
|
||||||
|
"react-scripts": "3.4.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
react-chap9/public/favicon.ico
Normal file
BIN
react-chap9/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
45
react-chap9/public/index.html
Normal file
45
react-chap9/public/index.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="backdrop-root"></div>
|
||||||
|
<div id="overlay-root"></div>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
react-chap9/public/logo192.png
Normal file
BIN
react-chap9/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
react-chap9/public/logo512.png
Normal file
BIN
react-chap9/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
react-chap9/public/manifest.json
Normal file
25
react-chap9/public/manifest.json
Normal file
@ -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"
|
||||||
|
}
|
3
react-chap9/public/robots.txt
Normal file
3
react-chap9/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
26
react-chap9/src/App.js
vendored
Normal file
26
react-chap9/src/App.js
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import AddUser from './components/Users/AddUser';
|
||||||
|
import UsersList from './components/Users/UsersList';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [usersList, setUsersList] = useState([]);
|
||||||
|
|
||||||
|
const addUserHandler = (uName, uAge) => {
|
||||||
|
setUsersList((prevUsersList) => {
|
||||||
|
return [
|
||||||
|
...prevUsersList,
|
||||||
|
{ name: uName, age: uAge, id: Math.random().toString() },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AddUser onAddUser={addUserHandler} />
|
||||||
|
<UsersList users={usersList} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
5
react-chap9/src/components/Helpers/Wrapper.js
vendored
Normal file
5
react-chap9/src/components/Helpers/Wrapper.js
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function Wrapper(props) {
|
||||||
|
return props.children
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Wrapper ;
|
17
react-chap9/src/components/UI/Button.js
vendored
Normal file
17
react-chap9/src/components/UI/Button.js
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import classes from './Button.module.css';
|
||||||
|
|
||||||
|
const Button = (props) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classes.button}
|
||||||
|
type={props.type || 'button'}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
18
react-chap9/src/components/UI/Button.module.css
Normal file
18
react-chap9/src/components/UI/Button.module.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.button {
|
||||||
|
font: inherit;
|
||||||
|
border: 1px solid #4f005f;
|
||||||
|
background: #4f005f;
|
||||||
|
color: white;
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover,
|
||||||
|
.button:active {
|
||||||
|
background: #741188;
|
||||||
|
border-color: #741188;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
9
react-chap9/src/components/UI/Card.js
vendored
Normal file
9
react-chap9/src/components/UI/Card.js
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import classes from './Card.module.css';
|
||||||
|
|
||||||
|
const Card = (props) => {
|
||||||
|
return <div className={`${classes.card} ${props.className}`}>{props.children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Card;
|
5
react-chap9/src/components/UI/Card.module.css
Normal file
5
react-chap9/src/components/UI/Card.module.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
47
react-chap9/src/components/UI/ErrorModal.js
vendored
Normal file
47
react-chap9/src/components/UI/ErrorModal.js
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
import Card from "./Card";
|
||||||
|
import Button from "./Button";
|
||||||
|
import classes from "./ErrorModal.module.css";
|
||||||
|
|
||||||
|
const Backdrop = (props) => {
|
||||||
|
return <div className={classes.backdrop} onClick={props.onConfirm} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalOverlay = (props) => {
|
||||||
|
return (
|
||||||
|
<Card className={classes.modal}>
|
||||||
|
<header className={classes.header}>
|
||||||
|
<h2>{props.title}</h2>
|
||||||
|
</header>
|
||||||
|
<div className={classes.content}>
|
||||||
|
<p>{props.message}</p>
|
||||||
|
</div>
|
||||||
|
<footer className={classes.actions}>
|
||||||
|
<Button onClick={props.onConfirm}>Okay</Button>
|
||||||
|
</footer>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ErrorModal = (props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ReactDOM.createPortal(
|
||||||
|
<Backdrop onConfirm={props.onConfirm} />,
|
||||||
|
document.getElementById("backdrop-root")
|
||||||
|
)}
|
||||||
|
{ReactDOM.createPortal(
|
||||||
|
<ModalOverlay
|
||||||
|
onConfirm={props.onConfirm}
|
||||||
|
title={props.title}
|
||||||
|
message={props.message}
|
||||||
|
/>,
|
||||||
|
document.getElementById("overlay-root")
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorModal;
|
45
react-chap9/src/components/UI/ErrorModal.module.css
Normal file
45
react-chap9/src/components/UI/ErrorModal.module.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 10;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 30vh;
|
||||||
|
left: 10%;
|
||||||
|
width: 80%;
|
||||||
|
z-index: 100;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: #4f005f;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.modal {
|
||||||
|
left: calc(50% - 20rem);
|
||||||
|
width: 40rem;
|
||||||
|
}
|
||||||
|
}
|
72
react-chap9/src/components/Users/AddUser.js
vendored
Normal file
72
react-chap9/src/components/Users/AddUser.js
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
|
||||||
|
import Card from '../UI/Card';
|
||||||
|
import Button from '../UI/Button';
|
||||||
|
import ErrorModal from '../UI/ErrorModal';
|
||||||
|
import Wrapper from '../Helpers/Wrapper';
|
||||||
|
import classes from './AddUser.module.css';
|
||||||
|
|
||||||
|
const AddUser = (props) => {
|
||||||
|
const nameInputRef = useRef()
|
||||||
|
const ageInputRef = useRef()
|
||||||
|
|
||||||
|
const [error, setError] = useState();
|
||||||
|
|
||||||
|
const addUserHandler = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const enteredName = nameInputRef.current.value
|
||||||
|
const enteredUserAge = ageInputRef.current.value
|
||||||
|
if (enteredName.trim().length === 0 || enteredUserAge.trim().length === 0) {
|
||||||
|
setError({
|
||||||
|
title: 'Invalid input',
|
||||||
|
message: 'Please enter a valid name and age (non-empty values).',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (+enteredUserAge < 1) {
|
||||||
|
setError({
|
||||||
|
title: 'Invalid age',
|
||||||
|
message: 'Please enter a valid age (> 0).',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.onAddUser(enteredName, enteredUserAge);
|
||||||
|
nameInputRef.current.value = ''
|
||||||
|
ageInputRef.current.value = ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorHandler = () => {
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
{error && (
|
||||||
|
<ErrorModal
|
||||||
|
title={error.title}
|
||||||
|
message={error.message}
|
||||||
|
onConfirm={errorHandler}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Card className={classes.input}>
|
||||||
|
<form onSubmit={addUserHandler}>
|
||||||
|
<label htmlFor="username">Username</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
ref={nameInputRef}
|
||||||
|
/>
|
||||||
|
<label htmlFor="age">Age (Years)</label>
|
||||||
|
<input
|
||||||
|
id="age"
|
||||||
|
type="number"
|
||||||
|
ref={ageInputRef}
|
||||||
|
/>
|
||||||
|
<Button type="submit">Add User</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddUser;
|
27
react-chap9/src/components/Users/AddUser.module.css
Normal file
27
react-chap9/src/components/Users/AddUser.module.css
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
.input {
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 40rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input label {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input input {
|
||||||
|
font: inherit;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 0.15rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4f005f;
|
||||||
|
}
|
||||||
|
|
20
react-chap9/src/components/Users/UsersList.js
vendored
Normal file
20
react-chap9/src/components/Users/UsersList.js
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Card from '../UI/Card';
|
||||||
|
import classes from './UsersList.module.css';
|
||||||
|
|
||||||
|
const UsersList = (props) => {
|
||||||
|
return (
|
||||||
|
<Card className={classes.users}>
|
||||||
|
<ul>
|
||||||
|
{props.users.map((user) => (
|
||||||
|
<li key={user.id}>
|
||||||
|
{user.name} ({user.age} years old)
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UsersList;
|
16
react-chap9/src/components/Users/UsersList.module.css
Normal file
16
react-chap9/src/components/Users/UsersList.module.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.users {
|
||||||
|
margin: 2rem auto;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 40rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users li {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
12
react-chap9/src/index.css
Normal file
12
react-chap9/src/index.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif;
|
||||||
|
background: #1f1f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
8
react-chap9/src/index.js
vendored
Normal file
8
react-chap9/src/index.js
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(<App />);
|
File diff suppressed because one or more lines are too long
@ -17,19 +17,18 @@ function ExpenseForm(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const submitHandler = (event) => {
|
const submitHandler = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
|
|
||||||
const expenseData = {
|
const expenseData = {
|
||||||
title: enteredTitle,
|
title: enteredTitle,
|
||||||
amount: enteredAmount,
|
amount: +enteredAmount,
|
||||||
date: new Date(enteredDate)
|
date: new Date(enteredDate),
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(expenseData)
|
console.log(expenseData)
|
||||||
setEnteredTitle("")
|
setEnteredTitle("")
|
||||||
setEnteredAmount("")
|
setEnteredAmount("")
|
||||||
setEnteredDate("")
|
setEnteredDate("")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -37,7 +36,11 @@ function ExpenseForm(props) {
|
|||||||
<div className="new-expense__controls">
|
<div className="new-expense__controls">
|
||||||
<div className="new-expense__control">
|
<div className="new-expense__control">
|
||||||
<label>Title</label>
|
<label>Title</label>
|
||||||
<input type="text" value={enteredTitle} onChange={titleChangeHandler} />
|
<input
|
||||||
|
type="text"
|
||||||
|
value={enteredTitle}
|
||||||
|
onChange={titleChangeHandler}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="new-expense__control">
|
<div className="new-expense__control">
|
||||||
<label>Amount</label>
|
<label>Amount</label>
|
||||||
@ -45,7 +48,7 @@ function ExpenseForm(props) {
|
|||||||
type="number"
|
type="number"
|
||||||
min="0.01"
|
min="0.01"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={enteredAmount}
|
value={enteredAmount}
|
||||||
onChange={amountChangeHandler}
|
onChange={amountChangeHandler}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -55,12 +58,14 @@ function ExpenseForm(props) {
|
|||||||
type="date"
|
type="date"
|
||||||
min="2022-01-01"
|
min="2022-01-01"
|
||||||
max="2022-12-31"
|
max="2022-12-31"
|
||||||
value={enteredDate}
|
value={enteredDate}
|
||||||
onChange={dateChangeHandler}
|
onChange={dateChangeHandler}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="new-expense__actions">
|
<div className="new-expense__actions">
|
||||||
<button type="button" onClick={props.onCancel}>Cancel</button>
|
<button type="button" onClick={props.onCancel}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
<button type="submit">Add Expense</button>
|
<button type="submit">Add Expense</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user