diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index a11777c..0000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/index.html b/public/index.html index aa069f2..cb019b0 100644 --- a/public/index.html +++ b/public/index.html @@ -2,42 +2,17 @@ - - - - - + React App
- diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a..0000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a6..0000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 080d6c7..0000000 --- a/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "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/src/App.js b/src/App.js index 3784575..534cd30 100644 --- a/src/App.js +++ b/src/App.js @@ -1,25 +1,45 @@ -import logo from './logo.svg'; -import './App.css'; +import React, {useState} from "react"; +import Costs from "./components/Costs/Costs"; +import NewCost from "./components/NewCost/NewCost"; + +const INITIAL_COSTS = [ + { + id: 'c1', + date: new Date(2021, 9, 19), + description: "Refrigerator", + amount: 999.99 + }, + { + id: 'c2', + date: new Date(2023, 8, 20), + description: "MacBook", + amount: 1250 + }, + { + id: 'c3', + date: new Date(2023, 4, 10), + description: "Keyboard", + amount: 50 + } + ]; + +const App = () => { + + const [costs, setCosts] = useState(INITIAL_COSTS); + + const addCostHandler = (cost) => { + setCosts((prevCosts) => { + return [cost, ...prevCosts]; + }); + }; -function App() { return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
+
+ +
); } + export default App; diff --git a/src/App.test.js b/src/App.test.js index 1f03afe..15a55d4 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -1,8 +1,8 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; +// import { render, screen } from '@testing-library/react'; +// import App from './App'; -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); +// test('renders learn react link', () => { +// render(); +// const linkElement = screen.getByText(/learn react/i); +// expect(linkElement).toBeInTheDocument(); +// }); diff --git a/src/components/Costs/CostDate.css b/src/components/Costs/CostDate.css new file mode 100644 index 0000000..e8c739d --- /dev/null +++ b/src/components/Costs/CostDate.css @@ -0,0 +1,26 @@ +.cost-date{ + display: flex; + flex-direction: column; + width: 5.5rem; + height: 5.5rem; + border: 1px solid rgb(222, 237, 248); + background-color: rgb(16, 39, 243); + color: white; + border-radius: 12px; + align-items: center; + justify-content: center +} + +.cost-date__month { + font-size: 0.75rem; + font-weight: bold; +} + +.cost-date__year { + font-size: 0.75rem; +} + +.cost-date__day{ + font-size: 0.75rem; + font-weight: bold; +} \ No newline at end of file diff --git a/src/components/Costs/CostDate.js b/src/components/Costs/CostDate.js new file mode 100644 index 0000000..bdfc84d --- /dev/null +++ b/src/components/Costs/CostDate.js @@ -0,0 +1,17 @@ +import './CostDate.css'; + +const CostDate = (props) => { + const month = props.date.toLocaleString("ua-UA", { month: "long" }); + const year = props.date.getFullYear(); + const day = props.date.toLocaleString("ua-UA", { day: "2-digit" }); + + return ( +
+
{month}
+
{year}
+
{day}
+
+ ); +} + +export default CostDate; diff --git a/src/components/Costs/CostItem.css b/src/components/Costs/CostItem.css new file mode 100644 index 0000000..ec7952a --- /dev/null +++ b/src/components/Costs/CostItem.css @@ -0,0 +1,67 @@ +.cost-item { + max-width: 700px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem; + margin: 1rem auto; + background-color: #ec8383; + +} + +.cost-item__date { + width: 30%; + font-size: 1rem; + color: rgb(173, 234, 157); + text-align: left; +} + +.cost-item__description { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: flex-end; + flex-flow: column-reverse; + justify-content: flex-start; + flex: 1; +} + +.cost-item h2 { + color: aqua; + font-size: 1rem; + flex: 1; + margin: 0 1rem; + color: white; + text-align: center; + +} + +.cost-item__price { + font-size: 1rem; + font-weight: bold; + color: #c6ddf6; + background-color: rgb(16, 39, 243); + border: 1px solid rgb(222, 237, 248); + padding: 0.5rem; + border-radius: 10px; + +} + +@media (min-width: 580px) { + .cost-item__description { + flex-direction: row; + align-items: center; + justify-content: flex-start; + flex: 1; + } + + .cost-item__description h2 { + font-size: 1.25rem; + } + + .cost-item__price { + font-size: 1.25rem; + padding: 0.5rem 1.5rem; + } +} \ No newline at end of file diff --git a/src/components/Costs/CostItem.js b/src/components/Costs/CostItem.js new file mode 100644 index 0000000..bf1d7bd --- /dev/null +++ b/src/components/Costs/CostItem.js @@ -0,0 +1,19 @@ +import "./CostItem.css"; +import CostDate from "./CostDate"; +import Card from "../UI/Card"; + +const CostItem = (props) => { + return ( +
  • + + +
    +

    {props.description}

    +
    ${props.amount}
    +
    +
    +
  • + ); +} + +export default CostItem; diff --git a/src/components/Costs/CostList.css b/src/components/Costs/CostList.css new file mode 100644 index 0000000..d5155f7 --- /dev/null +++ b/src/components/Costs/CostList.css @@ -0,0 +1,9 @@ +.cost-list { + list-style: none; + padding: 0; +} + +.cost-list__fallback { + color: #f1552d; + text-align: center; +} \ No newline at end of file diff --git a/src/components/Costs/CostList.js b/src/components/Costs/CostList.js new file mode 100644 index 0000000..cd4dec0 --- /dev/null +++ b/src/components/Costs/CostList.js @@ -0,0 +1,22 @@ +import CostItem from "./CostItem"; +import './CostList.css'; + +const CostList = (props) => { + + if (props.costs.length === 0) { + return

    There are no expenses in this year.

    + } + + return
      + {props.costs.map((cost) => ( + + ))}; +
    +} + +export default CostList; \ No newline at end of file diff --git a/src/components/Costs/Costs.css b/src/components/Costs/Costs.css new file mode 100644 index 0000000..56c3aa0 --- /dev/null +++ b/src/components/Costs/Costs.css @@ -0,0 +1,9 @@ +.costs { + padding: 1rem; + background-color: rgb(143, 218, 245); + margin: 2rem auto; + width: 70rem; + max-width: 90%; + border-radius: 10px; + box-shadow: 0 1px 7px reba(0 0 0 0.25); +} \ No newline at end of file diff --git a/src/components/Costs/Costs.js b/src/components/Costs/Costs.js new file mode 100644 index 0000000..4f3c0d2 --- /dev/null +++ b/src/components/Costs/Costs.js @@ -0,0 +1,32 @@ +import "./Costs.css"; +import Card from "../UI/Card"; +import CostsFilter from "./CostsFilter"; +import React, { useState } from "react"; +import CostList from "./CostList"; +import CostsDiagram from "./CostsDiagram"; + +const Costs = (props) => { + const [selectedYear, setSelectedYear] = useState("2020"); + + const yearChangeHandler = (year) => { + setSelectedYear(year); + }; + + const filteredCosts = props.costs.filter((cost) => { + return cost.date.getFullYear().toString() === selectedYear; + }); + + + + return ( +
    + + + + + +
    + ); +}; + +export default Costs; diff --git a/src/components/Costs/CostsDiagram.js b/src/components/Costs/CostsDiagram.js new file mode 100644 index 0000000..639e697 --- /dev/null +++ b/src/components/Costs/CostsDiagram.js @@ -0,0 +1,28 @@ +import Diagram from "../Diagram/Diagram" + +const CostsDiagram = (props) => { + + const diagramDataSets = [ + { label: 'Jan', value: 0 }, + { label: 'Feb', value: 0 }, + { label: 'Mar', value: 0 }, + { label: 'Apr', value: 0 }, + { label: 'May', value: 0 }, + { label: 'Jun', value: 0 }, + { label: 'Jul', value: 0 }, + { label: 'Aug', value: 0 }, + { label: 'Sep', value: 0 }, + { label: 'Oct', value: 0 }, + { label: 'Nov', value: 0 }, + { label: 'Dec', value: 0 }, + ]; + + for (const cost of props.costs) { + const costMonth = cost.date.getMonth(); + diagramDataSets[costMonth].value += cost.amount; + } + + return +} + +export default CostsDiagram; \ No newline at end of file diff --git a/src/components/Costs/CostsFilter.css b/src/components/Costs/CostsFilter.css new file mode 100644 index 0000000..08ac7e3 --- /dev/null +++ b/src/components/Costs/CostsFilter.css @@ -0,0 +1,24 @@ +.costs-filter { + color: rgb(11, 0, 2); + padding: 0 1rem; +} + +.costs-filter__control { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + margin: 1rem 0; +} + +.costs-filter label { + font-weight: bold; + margin-bottom: 0.5rem; +} + +.costs-filter select { + font: inherit; + padding: 0.5rem 3rem; + font-weight: bold; + border-radius: 6px +} \ No newline at end of file diff --git a/src/components/Costs/CostsFilter.js b/src/components/Costs/CostsFilter.js new file mode 100644 index 0000000..df159b4 --- /dev/null +++ b/src/components/Costs/CostsFilter.js @@ -0,0 +1,24 @@ +import './CostsFilter.css'; + +const CostsFilter = (props) => { + const yearChangeHandler = (event) => { + props.onChangeYear(event.target.value); + } + + return ( +
    +
    + + +
    +
    + ); +}; + +export default CostsFilter; \ No newline at end of file diff --git a/src/components/Diagram/DagramBar.css b/src/components/Diagram/DagramBar.css new file mode 100644 index 0000000..c8e31aa --- /dev/null +++ b/src/components/Diagram/DagramBar.css @@ -0,0 +1,33 @@ +.diagram-bar { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +.diagram-bar__inner { + height: 100%; + width: 100%; + border: 1px solid grey; + border-radius: 12px; + background-color: #fefae1; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: flex-end; +} + +.diagram-bar__fill { + background-color: #5478ee; + width: 100%; + transition: all 0.3s ease-out; +} + +.diagram-bar__label { + font-weight: bold; + font-size: 0.5rem; + /* display: flex; + justify-content: center; */ + text-align: center; + +} \ No newline at end of file diff --git a/src/components/Diagram/Diagram.css b/src/components/Diagram/Diagram.css new file mode 100644 index 0000000..87efc06 --- /dev/null +++ b/src/components/Diagram/Diagram.css @@ -0,0 +1,9 @@ +.diagram{ + padding: 1rem; + border-radius: 12px; + background-color: rgb(247, 192, 110); + text-align: center; + display: flex; + justify-content: space-around; + height: 10rem; +} diff --git a/src/components/Diagram/Diagram.js b/src/components/Diagram/Diagram.js new file mode 100644 index 0000000..a22b810 --- /dev/null +++ b/src/components/Diagram/Diagram.js @@ -0,0 +1,23 @@ +import DiagramBar from "./DiagramBar"; +import "./Diagram.css"; + +const Diagram = props => { + const dataSetsValues = props.dataSets.map((dataSet) => dataSet.value); + + const maxMonthCosts = Math.max(...dataSetsValues); + + return ( +
    + {props.dataSets.map((dataSet) => ( + + ))} +
    + ); +}; + +export default Diagram; diff --git a/src/components/Diagram/DiagramBar.js b/src/components/Diagram/DiagramBar.js new file mode 100644 index 0000000..04baf66 --- /dev/null +++ b/src/components/Diagram/DiagramBar.js @@ -0,0 +1,22 @@ +import "./DagramBar.css"; + +const DiagramBar = props => { + let barFillHeight = "0%"; + + if (props.maxValue > 0) { + barFillHeight = Math.round(props.value / props.maxValue * 100) + "%"; + } + + return ( +
    +
    +
    +
    +
    + {props.label} +
    +
    + ); +}; + +export default DiagramBar; diff --git a/src/components/NewCost/CostForm.css b/src/components/NewCost/CostForm.css new file mode 100644 index 0000000..c7c6ac6 --- /dev/null +++ b/src/components/NewCost/CostForm.css @@ -0,0 +1,49 @@ +.new-cost__control { + display: block; + margin-bottom: 1rem; +} + +.new-cost__control label { + font-weight: bold; + margin-bottom: 0.5rem; + display: block; +} + +.new-cost__control input { + font: inherit; + padding: 0.5rem; + border-radius: 5px; + border: 1px solid white; + width: 20rem; + max-width: 100%; +} + +.new-cost__actions { + text-align: left; +} + +@media (min-width: 580px) { + .new-cost__control { + display: block; + margin-bottom: 1rem; + } + + .new-cost__control label { + font-weight: bold; + margin-bottom: 0.5rem; + display: block; + } + + .new-cost__control input { + font: inherit; + padding: 0.5rem; + border-radius: 5px; + border: 1px solid white; + width: 20rem; + max-width: 100%; + } + + .new-cost__actions { + text-align: left; + } +} diff --git a/src/components/NewCost/CostForm.js b/src/components/NewCost/CostForm.js new file mode 100644 index 0000000..2c176b0 --- /dev/null +++ b/src/components/NewCost/CostForm.js @@ -0,0 +1,94 @@ +import React, {useState} from 'react'; +import './CostForm.css'; + + +const CostForm = (props) => { + const [inputName, setInputName] = useState(''); + const [inputAmount, setInputAmount] = useState(''); + const [inputDate, setInputDate] = useState(''); + + + + // const [userInput, setUserInput] = useState({ + // name: '', + // amount: '', + // date: '' + // }) + + const nameChangeHandler = (event) => { + setInputName(event.target.value) + // setUserInput({ + // ...userInput, + // name: event.target.value + // }) + // setUserInput((previousState) => { + // return { + // ...userInput, + // name: event.target.value + // } + // }) + } + + const amountChangeHandler = (event) => { + setInputAmount(event.target.value) + // setUserInput({ + // ...userInput, + // amount: event.target.value + // }) + } + + const dateChangeHandler = (event) => { + setInputDate(event.target.value) + // setUserInput({ + // ...userInput, + // date: event.target.value + // }) + + } + + const SubmitHandler = (event) => { + event.preventDefault(); + + const costData = { + description: inputName, + amount: inputAmount, + date: new Date(inputDate) + }; + + props.onSaveCostData(costData); + setInputName(''); + setInputAmount(''); + setInputDate(''); +} + + return ( +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + ); +} + +export default CostForm; diff --git a/src/components/NewCost/NewCost.css b/src/components/NewCost/NewCost.css new file mode 100644 index 0000000..5d715e8 --- /dev/null +++ b/src/components/NewCost/NewCost.css @@ -0,0 +1,28 @@ +.new-cost { + background-color: rgb(244, 109, 109); + padding: 1rem; + margin: 2rem auto; + width: 70rem; + max-width: 95%; + border-radius: 10px; + text-align: center; + box-shadow: 0 1px 7px rgba(0, 0, 0, 0.25); +} + +.new-cost button{ + font: inherit; + cursor: pointer; + padding: 1rem 2rem; + border: 1px solid rgb(109, 67, 67); + background-color: rgb(244, 187, 169); + color: rgb(76, 127, 244); + border-radius: 10px; + margin-right: 3px; +} + +.new-cost button:hover, +.new-cost button:active { + background-color: rgb(34, 76, 105); + border-color: bisque; +} + diff --git a/src/components/NewCost/NewCost.js b/src/components/NewCost/NewCost.js new file mode 100644 index 0000000..249d849 --- /dev/null +++ b/src/components/NewCost/NewCost.js @@ -0,0 +1,38 @@ +import React, { useState } from "react"; +import CostForm from "./CostForm"; +import "./NewCost.css"; + +const NewCost = props => { + const [isFormVisisble, setIsFormVisible] = useState(false); + + const saveCostDataHandler = inputCostData => { + const costData = { + ...inputCostData, + id: Math.random().toString() + }; + props.onAddCost(costData); + setIsFormVisible(false); + }; + + const inputCostDataHandler = () => { + setIsFormVisible(true); + }; + + const cancelCostHandler = () => { + setIsFormVisible(false); + }; + + return ( +
    + {!isFormVisisble && + } + {isFormVisisble && + } +
    + ); +}; + +export default NewCost; diff --git a/src/components/UI/Card.css b/src/components/UI/Card.css new file mode 100644 index 0000000..084ee0f --- /dev/null +++ b/src/components/UI/Card.css @@ -0,0 +1,4 @@ +.card{ + border-radius: 10px; + box-shadow: 0 1px 7px reba(0 0 0 0.25); +} \ No newline at end of file diff --git a/src/components/UI/Card.js b/src/components/UI/Card.js new file mode 100644 index 0000000..b79b1e9 --- /dev/null +++ b/src/components/UI/Card.js @@ -0,0 +1,8 @@ +import './Card.css'; + +const Card = (props) => { + const classes = 'card ' + props.className; + return
    {props.children}
    ; +} + +export default Card; diff --git a/src/index.css b/src/index.css index ec2585e..2de6086 100644 --- a/src/index.css +++ b/src/index.css @@ -1,5 +1,6 @@ body { - margin: 0; + margin: 10px; + background-color: bisque; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; diff --git a/src/index.js b/src/index.js index d563c0f..3606d2c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,17 +1,10 @@ -import React from 'react'; + import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; -import reportWebVitals from './reportWebVitals'; -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render( - - - -); -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); +ReactDOM.createRoot(document.getElementById('root')) + .render( + +) diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 9dfc1c0..0000000 --- a/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js deleted file mode 100644 index 5253d3a..0000000 --- a/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/src/setupTests.js b/src/setupTests.js deleted file mode 100644 index 8f2609b..0000000 --- a/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom';