Merge pull request #221 from juliamuiruri4/main
added total shopping cost & quantity functionality
This commit is contained in:
Коммит
cbd422e605
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -13,9 +13,11 @@
|
|||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"graphql": "^16.6.0",
|
||||
"nth-check": "^2.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-loading-icons": "^1.1.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 256 KiB После Ширина: | Высота: | Размер: 305 KiB |
|
@ -1,21 +1,19 @@
|
|||
<!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" />
|
||||
<!--
|
||||
|
||||
<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" />
|
||||
<!--
|
||||
<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.
|
||||
|
@ -24,12 +22,13 @@
|
|||
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>
|
||||
<!--
|
||||
<title>Smart Shopping</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.
|
||||
|
||||
|
@ -39,5 +38,6 @@
|
|||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -2,32 +2,45 @@ import './App.css';
|
|||
import CreateItem from './components/CreateItem';
|
||||
import ItemList from './components/ItemList';
|
||||
import PriceTag from './components/PriceTag';
|
||||
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
|
||||
import { Container, Grid, Box } from '@mui/material';
|
||||
import Typography from '@mui/joy/Typography';
|
||||
import React, { useState } from 'react';
|
||||
import { totalItems, totalCost } from './utils/Utils'
|
||||
import Loading from './components/Loading';
|
||||
|
||||
// Apollo Client setup for GraphQL API calls to the backend server
|
||||
const client = new ApolloClient({
|
||||
uri: '/data-api/graphql',
|
||||
cache: new InMemoryCache({
|
||||
addTypename: false
|
||||
})
|
||||
});
|
||||
|
||||
function App() {
|
||||
const [items, setItems] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('/data-api/api/Item');
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
const data = await response.json();
|
||||
setItems(data.value);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<Container maxWidth="lg" sx={{ padding:5 }}>
|
||||
<Container maxWidth="lg" sx={{ padding: 5 }}>
|
||||
{loading && <Loading />}
|
||||
<Typography level="h1" color="info" variant='outlined'>Smart Shopping Planner</Typography>
|
||||
<Grid item xs={12} md={6} sx={{ display: 'grid', gridTemplateColumns: '1fr 2fr 1fr', gap: '20px', paddingTop: '20px' }}>
|
||||
{ <CreateItem />}
|
||||
{ <ItemList />}
|
||||
<Box>
|
||||
{ <PriceTag />}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} sx={{ display: 'grid', gridTemplateColumns: '1fr 2fr 1fr', gap: '20px', paddingTop: '20px' }}>
|
||||
{<CreateItem fetchData={fetchData} />}
|
||||
{<ItemList fetchData={fetchData} items={items} />}
|
||||
<Box>
|
||||
{<PriceTag itemsNo={totalItems(items)} totalCost={totalCost(items)} />}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Container>
|
||||
</ApolloProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
|
@ -7,159 +7,98 @@ import TextField from '@mui/material/TextField';
|
|||
import CardContent from '@mui/material/CardContent';
|
||||
import Box from '@mui/material/Box';
|
||||
import { MenuItem, Button } from '@mui/material';
|
||||
import { categories } from '../utils/Utils'
|
||||
import Loading from './Loading';
|
||||
|
||||
const categories = [
|
||||
{
|
||||
value: 'Utensils',
|
||||
label: 'Utensils',
|
||||
},
|
||||
{
|
||||
value: 'Furniture',
|
||||
label: 'Furniture',
|
||||
},
|
||||
{
|
||||
value: 'Kitchen Wear',
|
||||
label: 'Kitchen Wear',
|
||||
},
|
||||
{
|
||||
value: 'Bathroom Wear',
|
||||
label: 'Bathroom Wear',
|
||||
},
|
||||
{
|
||||
value: 'Food Items',
|
||||
label: 'Food Items',
|
||||
},
|
||||
{
|
||||
value: 'Office Wear',
|
||||
label: 'Office Wear',
|
||||
},
|
||||
];
|
||||
|
||||
function CreateItem({ refetch }) {
|
||||
function CreateItem({ fetchData }) {
|
||||
const [category, setCategory] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [quantity, setQuantity] = useState('');
|
||||
const [unitPrice, setUnitPrice] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
||||
const createItemRequest = async () => {
|
||||
try {
|
||||
const response = await fetch('/data-api/rest/Item', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-MS-API-ROLE' : 'admin',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
category,
|
||||
name,
|
||||
quantity,
|
||||
description
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok){
|
||||
} else {
|
||||
throw new Error(data.error.message);
|
||||
if (category === '' || name === '' || quantity === '' || unitPrice === '' || description === '') { // check if all fields are filled
|
||||
alert('Please fill in all fields');
|
||||
return;
|
||||
} else {
|
||||
setLoading(true) // start the loading
|
||||
try {
|
||||
const response = await fetch('/data-api/api/Item/', { // fetch data from the API
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-MS-API-ROLE': 'admin' },
|
||||
body: JSON.stringify({ category, name, quantity, description, unitPrice }) // send the data to the API
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
fetchData(); // refresh the list
|
||||
} else {
|
||||
throw new Error(data.error.message);
|
||||
}
|
||||
setLoading(false) // stop the loading
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setLoading(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
await createItemRequest();
|
||||
refetch();
|
||||
event.preventDefault(); // prevent the default behavior of the form
|
||||
await createItemRequest(); // call the createItemRequest function
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
sx={{
|
||||
'& .MuiTextField-root': { m: 1 },
|
||||
}}
|
||||
action={
|
||||
<IconButton aria-label="settings">
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
}
|
||||
{loading && <Loading />}
|
||||
<CardHeader sx={{ '& .MuiTextField-root': { m: 1 }, }}
|
||||
action={<IconButton aria-label="settings"><MoreVertIcon /></IconButton>}
|
||||
title="Add Item"
|
||||
/>
|
||||
<CardContent>
|
||||
<Box
|
||||
component="form"
|
||||
sx={{
|
||||
'& .MuiTextField-root': { m: 1 },
|
||||
}}
|
||||
noValidate
|
||||
autoComplete="on"
|
||||
>
|
||||
<div>
|
||||
<TextField
|
||||
id="category"
|
||||
select
|
||||
label="Select"
|
||||
defaultValue="EUR"
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
<Box component="form" sx={{ '& .MuiTextField-root': { m: 1 }, }} noValidate autoComplete="on">
|
||||
<TextField id="category" select label="Select" value={category} onChange={(e) => setCategory(e.target.value)}
|
||||
helperText="Please select your category"
|
||||
>
|
||||
required >
|
||||
{categories.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
id="name"
|
||||
label="Item Name"
|
||||
placeholder="New Item Name"
|
||||
multiline
|
||||
helperText="Please type in a new item name"
|
||||
</TextField>
|
||||
<TextField id="name" label="Item name" placeholder="New Item Name" multiline helperText="Please type in a new item name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
id="description"
|
||||
label="Item description"
|
||||
placeholder="Item Description"
|
||||
multiline
|
||||
required
|
||||
/>
|
||||
<TextField id="description" label="Item description" placeholder="Item Description" multiline
|
||||
helperText="Please type in a a short description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
id="quantity"
|
||||
label="Quantity"
|
||||
type='number'
|
||||
placeholder="New Quantity"
|
||||
multiline
|
||||
id="unitPrice" label="Unit price in $" type='number' placeholder="Unit Price" multiline
|
||||
helperText="Please add unit price"
|
||||
value={unitPrice}
|
||||
onChange={(e) => setUnitPrice(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<TextField id="quantity" label="Quantity" type='number' placeholder="New Quantity" multiline
|
||||
helperText="Please add quantity"
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
required
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={handleSubmit}
|
||||
size="small"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ p: 2 }}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button type="submit" onClick={handleSubmit} size="small" variant="contained" color="primary" sx={{ p: 2 }}>Add</Button>
|
||||
</CardContent>
|
||||
</Card >
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,58 +8,51 @@ import TableHead from '@mui/material/TableHead';
|
|||
import TableRow from '@mui/material/TableRow';
|
||||
import Card from '@mui/material/Card';
|
||||
import { Button } from '@mui/material';
|
||||
import Loading from './Loading';
|
||||
|
||||
const ItemList = () => {
|
||||
const [items, setItems] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetchData = async (disableLoadState) => {
|
||||
if(!disableLoadState) setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch('/data-api/rest/Item');
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
const data = await response.json();
|
||||
setItems(data.value);
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
const ItemList = ({ fetchData, items }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const deleteItem = async (id) => {
|
||||
setLoading(true) // start the loading
|
||||
try {
|
||||
const response = await fetch(`/data-api/rest/Item/id/${id}`, {
|
||||
const response = await fetch('/data-api/api/Item/id/${id}', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-MS-API-ROLE' : 'admin',
|
||||
'X-MS-API-ROLE': 'admin',
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
await fetchData(true);
|
||||
} catch (error) { }
|
||||
await fetchData(true); // refresh the list
|
||||
setLoading(false) // stop the loading
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setLoading(false) // stop the loading
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
setLoading(true) // start the loading
|
||||
fetchData(); // fetch the data once the page loads[componentDidMount]
|
||||
setLoading(false)
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='item-list'>
|
||||
{loading && <Loading />}
|
||||
<Card>
|
||||
<TableContainer component={Paper}>
|
||||
<Table aria-label="simple table">
|
||||
<Table aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Category</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Quantity</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell>Unit Price</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
@ -74,6 +67,7 @@ const ItemList = () => {
|
|||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell>{item.quantity}</TableCell>
|
||||
<TableCell>{item.description}</TableCell>
|
||||
<TableCell>${item.unitPrice}</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="contained" color="error" onClick={() => deleteItem(item.id)}>Delete</Button>
|
||||
</TableCell>
|
||||
|
@ -83,7 +77,7 @@ const ItemList = () => {
|
|||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
.Loading-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 50px;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { ThreeDots } from 'react-loading-icons'
|
||||
import './Loading.css'
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className='Loading-container'>
|
||||
<ThreeDots className='loading' stroke="#fff" fill="#fff" />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -9,11 +9,11 @@ import BookmarkAdd from '@mui/icons-material/BookmarkAddOutlined';
|
|||
|
||||
var today = new Date();
|
||||
var dd = String(today.getDate()).padStart(2, '0');
|
||||
var mm = String(today.getMonth() +1).padStart(2, '0');
|
||||
var mm = String(today.getMonth() + 1).padStart(2, '0');
|
||||
var yyyy = today.getFullYear();
|
||||
today = mm + '/' + dd + '/' + yyyy;
|
||||
|
||||
export default function PriceTag() {
|
||||
export default function PriceTag({ itemsNo, totalCost }) {
|
||||
return (
|
||||
<Card variant="outlined" sx={{ width: 320 }}>
|
||||
<div>
|
||||
|
@ -42,13 +42,13 @@ export default function PriceTag() {
|
|||
<div>
|
||||
<Typography level="body3">Total price:</Typography>
|
||||
<Typography fontSize="lg" fontWeight="lg">
|
||||
$--
|
||||
${totalCost}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography level="body3">Total items:</Typography>
|
||||
<Typography fontSize="lg" fontWeight="lg">
|
||||
--
|
||||
{itemsNo}
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
|
|
|
@ -6,9 +6,9 @@ import reportWebVitals from './reportWebVitals';
|
|||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
// <React.StrictMode>
|
||||
<App />
|
||||
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
|
||||
// total number of items
|
||||
export const totalItems = (items) => {
|
||||
return items.reduce((total, item) => {
|
||||
return total + item.quantity;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// total cost of all items
|
||||
export const totalCost = (items) => {
|
||||
return items.reduce((total, item) => {
|
||||
return total + (item.quantity * item.unitPrice);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
//all categories to be used in the select input in the UI
|
||||
export const categories = [
|
||||
{
|
||||
value: 'Utensils',
|
||||
label: 'Utensils',
|
||||
},
|
||||
{
|
||||
value: 'Furniture',
|
||||
label: 'Furniture',
|
||||
},
|
||||
{
|
||||
value: 'Kitchen Wear',
|
||||
label: 'Kitchen Wear',
|
||||
},
|
||||
{
|
||||
value: 'Bathroom Wear',
|
||||
label: 'Bathroom Wear',
|
||||
},
|
||||
{
|
||||
value: 'Food Items',
|
||||
label: 'Food Items',
|
||||
},
|
||||
{
|
||||
value: 'Office Wear',
|
||||
label: 'Office Wear',
|
||||
},
|
||||
];
|
||||
|
Загрузка…
Ссылка в новой задаче