├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── screenshots ├── Screenshot 2020-06-19 at 2.27.52 AM.png ├── Screenshot 2020-06-19 at 2.28.27 AM.png ├── Screenshot 2020-06-19 at 2.28.38 AM.png ├── Screenshot 2020-06-19 at 2.29.07 AM.png ├── Screenshot 2020-06-19 at 2.29.34 AM.png └── Screenshot 2020-06-19 at 3.04.45 AM.png ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── components │ ├── bill │ │ ├── Bill.css │ │ ├── Bill.tsx │ │ ├── EditBill.tsx │ │ └── NewBill.tsx │ ├── chart │ │ ├── TimeSeriesChart.css │ │ └── TimeSeriesChart.tsx │ ├── common │ │ ├── Common.css │ │ └── SelectDropdown.tsx │ ├── expense │ │ ├── TotalExpense.css │ │ └── TotalExpense.tsx │ ├── header │ │ ├── Header.css │ │ └── Header.tsx │ ├── history │ │ ├── History.css │ │ └── History.tsx │ ├── index.tsx │ └── transaction │ │ ├── Transaction.css │ │ └── Transaction.tsx ├── context │ ├── AppReducer.tsx │ └── GlobalState.tsx ├── helpers │ ├── reducerHelper.ts │ ├── transactions.js │ └── utils.ts ├── index.css ├── index.tsx ├── logo.svg ├── models │ ├── IDailyExpense.ts │ ├── IExpense.ts │ ├── IMonthlyExpense.ts │ └── ITransaction.ts ├── react-app-env.d.ts ├── serviceWorker.ts └── setupTests.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A React bill manager to simplify the task of managing and paying bills. 2 | 3 | Features: 4 | 5 | 1. Add, remove and edit bills (hover on each bill). 6 | 2. Filter bills by category and month. 7 | 3. Display monthly or entire year time series of expenditure. (Currently only supports 2020, due to assignment time constraints) 8 | 4. Highlight bills to be paid based on a budget such that no more bills can be added. 9 | 10 | Installation steps: 11 | 12 | 1. Clone the repository : git clone https://github.com/ash-neo/bill-manager.git 13 | 2. Cd into the repository : cd bill-manager 14 | 3. Install all dependencies : yarn or npm install 15 | 4. Start the server : yarn start or npm run start 16 | 5. Go to : http://localhost:3000/ 17 | 6. To disable mock data set ENABLE_MOCK_DATA = false in src/context/GlobalState.tsx 18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bills", 3 | "version": "0.1.0", 4 | "private": true, 5 | "author": "Ashish", 6 | "dependencies": { 7 | "@material-ui/core": "^4.10.2", 8 | "@material-ui/lab": "^4.0.0-alpha.56", 9 | "@testing-library/jest-dom": "^4.2.4", 10 | "@testing-library/react": "^9.3.2", 11 | "@testing-library/user-event": "^7.1.2", 12 | "@types/jest": "^24.0.0", 13 | "@types/node": "^12.0.0", 14 | "@types/react": "^16.9.0", 15 | "@types/react-dom": "^16.9.0", 16 | "chart.js": "^2.9.3", 17 | "material-ui": "^0.20.2", 18 | "pondjs": "^0.9.0", 19 | "react": "^16.13.1", 20 | "react-chartjs-2": "^2.9.0", 21 | "react-dom": "^16.13.1", 22 | "react-linechart": "^1.1.12", 23 | "react-scripts": "3.4.1", 24 | "react-timeseries-charts": "^0.16.1", 25 | "typescript": "~3.7.2" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-dsa/bill-manager/b979d63b0acf7527042f229389a34508c7e0a3ad/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-dsa/bill-manager/b979d63b0acf7527042f229389a34508c7e0a3ad/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-dsa/bill-manager/b979d63b0acf7527042f229389a34508c7e0a3ad/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /screenshots/Screenshot 2020-06-19 at 2.27.52 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-dsa/bill-manager/b979d63b0acf7527042f229389a34508c7e0a3ad/screenshots/Screenshot 2020-06-19 at 2.27.52 AM.png -------------------------------------------------------------------------------- /screenshots/Screenshot 2020-06-19 at 2.28.27 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-dsa/bill-manager/b979d63b0acf7527042f229389a34508c7e0a3ad/screenshots/Screenshot 2020-06-19 at 2.28.27 AM.png -------------------------------------------------------------------------------- /screenshots/Screenshot 2020-06-19 at 2.28.38 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-dsa/bill-manager/b979d63b0acf7527042f229389a34508c7e0a3ad/screenshots/Screenshot 2020-06-19 at 2.28.38 AM.png -------------------------------------------------------------------------------- /screenshots/Screenshot 2020-06-19 at 2.29.07 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-dsa/bill-manager/b979d63b0acf7527042f229389a34508c7e0a3ad/screenshots/Screenshot 2020-06-19 at 2.29.07 AM.png -------------------------------------------------------------------------------- /screenshots/Screenshot 2020-06-19 at 2.29.34 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-dsa/bill-manager/b979d63b0acf7527042f229389a34508c7e0a3ad/screenshots/Screenshot 2020-06-19 at 2.29.34 AM.png -------------------------------------------------------------------------------- /screenshots/Screenshot 2020-06-19 at 3.04.45 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-dsa/bill-manager/b979d63b0acf7527042f229389a34508c7e0a3ad/screenshots/Screenshot 2020-06-19 at 3.04.45 AM.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Lato&display=swap"); 2 | 3 | :root { 4 | --box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | } 10 | 11 | body { 12 | background-color: #f7f7f7; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | min-height: 100vh; 18 | margin: 0; 19 | font-family: "Lato", sans-serif; 20 | } 21 | 22 | .container { 23 | margin: 30px auto; 24 | width: 350px; 25 | left: 0; 26 | position: absolute; 27 | } 28 | 29 | h1 { 30 | letter-spacing: 1px; 31 | margin: 0; 32 | } 33 | 34 | h3 { 35 | border-bottom: 1px solid #bbb; 36 | padding-bottom: 10px; 37 | margin: 40px 0 10px; 38 | } 39 | 40 | h4 { 41 | margin: 0; 42 | text-transform: uppercase; 43 | } 44 | 45 | .inc-exp-container { 46 | background-color: #fff; 47 | box-shadow: var(--box-shadow); 48 | padding: 20px; 49 | display: flex; 50 | justify-content: space-between; 51 | } 52 | 53 | .inc-exp-container > div { 54 | flex: 1; 55 | text-align: center; 56 | } 57 | 58 | label { 59 | display: inline-block; 60 | margin: 10px 0; 61 | } 62 | 63 | input[type="text"], 64 | input[type="number"] { 65 | border: 1px solid #dedede; 66 | border-radius: 2px; 67 | display: block; 68 | font-size: 16px; 69 | padding: 10px; 70 | width: 100%; 71 | } 72 | 73 | @media (max-width: 320px) { 74 | .container { 75 | width: 300px; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/react"; 2 | import React from "react"; 3 | import App from "./App"; 4 | 5 | test("renders learn react link", () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import { Header, History, NewBill, TotalExpense } from "./components"; 4 | import { GlobalProvider } from "./context/GlobalState"; 5 | 6 | function App() { 7 | return ( 8 | 9 |
10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /src/components/bill/Bill.css: -------------------------------------------------------------------------------- 1 | .new-bill { 2 | position: absolute; 3 | left: 0; 4 | width: 30%; 5 | top: 0; 6 | padding-top: 5%; 7 | padding-left: 2%; 8 | } 9 | 10 | .btn { 11 | cursor: pointer; 12 | background-color: green; 13 | box-shadow: var(--box-shadow); 14 | color: #fff; 15 | border: 0; 16 | display: block; 17 | font-size: 16px; 18 | margin: 10px 0 30px; 19 | padding: 10px; 20 | width: 100%; 21 | } 22 | 23 | .btn:focus, 24 | .delete-btn:focus, 25 | .edit-btn:focus { 26 | outline: 0; 27 | } 28 | input[type="number"].bill-amount { 29 | border: 1px solid #dedede; 30 | border-radius: 2px; 31 | display: flex; 32 | font-size: 17px; 33 | padding: 10px; 34 | width: 100%; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/bill/Bill.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { GlobalContext } from "../../context/GlobalState"; 3 | import "./Bill.css"; 4 | 5 | export const Bill = ({ 6 | onSubmit, 7 | category, 8 | setCategory, 9 | description, 10 | setDescription, 11 | amount, 12 | setAmount, 13 | billDate, 14 | setBillDate, 15 | defaultTransaction, 16 | }) => { 17 | const { 18 | discardTransaction, 19 | setDefaultPresent, 20 | }: { 21 | discardTransaction: () => void; 22 | setDefaultPresent: (State: any) => void; 23 | } = useContext(GlobalContext); 24 | const resetState = () => { 25 | setCategory(""); 26 | setDescription(""); 27 | setAmount(""); 28 | setBillDate(""); 29 | }; 30 | return ( 31 |
32 |

33 | {defaultTransaction 34 | ? defaultTransaction[0] 35 | ? "Edit Bill" 36 | : "New Bill" 37 | : "New Bill"} 38 |

39 |
40 |
41 | 42 | { 47 | setCategory(e.target.value); 48 | }} 49 | placeholder="Enter Category..." 50 | /> 51 |
52 |
53 | 54 | { 59 | setDescription(e.target.value); 60 | }} 61 | placeholder="Enter Value..." 62 | /> 63 |
64 |
65 | 68 | { 75 | setAmount(e.target.valueAsNumber); 76 | }} 77 | placeholder="Enter amount..." 78 | /> 79 |
80 |
81 |
Date
82 | { 87 | setBillDate(e.target.value); 88 | }} 89 | /> 90 |
91 | 104 | {category || description || amount || billDate ? ( 105 | 115 | ) : null} 116 |
117 |
118 | ); 119 | }; 120 | -------------------------------------------------------------------------------- /src/components/bill/EditBill.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext, useEffect, useState } from "react"; 2 | import { GlobalContext } from "../../context/GlobalState"; 3 | import { ITransaction } from "../../models/ITransaction"; 4 | import { Bill } from "./Bill"; 5 | import "./Bill.css"; 6 | export const EditBill = () => { 7 | const { 8 | addTransaction, 9 | defaultTransaction, 10 | deleteTransaction, 11 | }: { 12 | addTransaction: (State: any) => void; 13 | defaultTransaction: Map; 14 | transactions: Map; 15 | deleteTransaction: (State: any) => void; 16 | } = useContext(GlobalContext); 17 | 18 | const stateInitializer = useCallback( 19 | (parameter) => { 20 | if (defaultTransaction && defaultTransaction[0]) { 21 | if (defaultTransaction[0][parameter]) { 22 | return defaultTransaction[0][parameter]; 23 | } 24 | } 25 | return ""; 26 | }, 27 | [defaultTransaction] 28 | ); 29 | 30 | const [category, setCategory] = useState(stateInitializer("category")); 31 | const [description, setDescription] = useState( 32 | stateInitializer("description") 33 | ); 34 | const [billDate, setBillDate] = useState(stateInitializer("date")); 35 | const [amount, setAmount] = useState(stateInitializer("amount")); 36 | 37 | useEffect(() => { 38 | setCategory(stateInitializer("category")); 39 | setDescription(stateInitializer("description")); 40 | setBillDate(stateInitializer("date")); 41 | setAmount(stateInitializer("amount")); 42 | }, [stateInitializer, defaultTransaction]); 43 | 44 | const onSubmit = (e) => { 45 | e.preventDefault(); 46 | const newTransaction = { 47 | id: defaultTransaction[0].id, 48 | description: description, 49 | amount: +amount, 50 | category: category, 51 | date: billDate, 52 | }; 53 | deleteTransaction(newTransaction.id); 54 | addTransaction(newTransaction); 55 | resetState(); 56 | }; 57 | const resetState = () => { 58 | setCategory(""); 59 | setDescription(""); 60 | setAmount(""); 61 | setBillDate(""); 62 | }; 63 | if (!defaultTransaction || !defaultTransaction[0]) { 64 | return null; 65 | } 66 | 67 | return ( 68 | 80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /src/components/bill/NewBill.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { GlobalContext } from "../../context/GlobalState"; 3 | import { ITransaction } from "../../models/ITransaction"; 4 | import { Bill } from "./Bill"; 5 | import "./Bill.css"; 6 | import { EditBill } from "./EditBill"; 7 | 8 | export const NewBill = () => { 9 | const { 10 | addTransaction, 11 | defaultTransaction, 12 | transactions, 13 | deleteTransaction, 14 | }: { 15 | addTransaction: (State: any) => void; 16 | defaultTransaction: Map; 17 | transactions: Map; 18 | deleteTransaction: (State: any) => void; 19 | } = useContext(GlobalContext); 20 | 21 | const [category, setCategory] = useState(""); 22 | const [description, setDescription] = useState(""); 23 | const [billDate, setBillDate] = useState(""); 24 | const [amount, setAmount] = useState(undefined); 25 | 26 | const generateId = () => { 27 | let randomId = Math.floor(Math.random() * 1000); 28 | while (transactions.has(randomId)) { 29 | randomId = Math.floor(Math.random() * 1000); 30 | } 31 | return randomId; 32 | }; 33 | 34 | const onSubmit = (e) => { 35 | e.preventDefault(); 36 | 37 | const newTransaction = { 38 | id: generateId(), 39 | description: description, 40 | amount: +amount, 41 | category: category, 42 | date: billDate, 43 | select: false, 44 | }; 45 | deleteTransaction(newTransaction.id); 46 | addTransaction(newTransaction); 47 | resetState(); 48 | }; 49 | const resetState = () => { 50 | setCategory(""); 51 | setDescription(""); 52 | setAmount(""); 53 | setBillDate(""); 54 | }; 55 | if (defaultTransaction && defaultTransaction[0]) { 56 | return ; 57 | } 58 | 59 | return ( 60 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/chart/TimeSeriesChart.css: -------------------------------------------------------------------------------- 1 | .drop-down { 2 | display: "flex"; 3 | flex-direction: column; 4 | width: 33%; 5 | padding-top: 2%; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/chart/TimeSeriesChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { Line } from "react-chartjs-2"; 3 | import { GlobalContext } from "../../context/GlobalState"; 4 | import { 5 | computeCalendar, 6 | monthNameToNumber, 7 | months, 8 | } from "../../helpers/utils"; 9 | import { ITransaction } from "../../models/ITransaction"; 10 | import { SelectDropdown } from "../common/SelectDropdown"; 11 | import "./TimeSeriesChart.css"; 12 | export const TimeSeriesChart = () => { 13 | const { 14 | transactions, 15 | }: { 16 | transactions: Map; 17 | } = useContext(GlobalContext); 18 | const myCalendar = computeCalendar(); 19 | const [listMonth, setListMonth] = useState("Select all"); 20 | transactions.forEach((transaction) => { 21 | const tempDates = transaction.date.toString().split("-"); 22 | const tempMonth = myCalendar.get(Number(tempDates[1])); 23 | const tempDay = tempMonth.monthlyExpense.get(Number(tempDates[2])); 24 | tempDay.amount += transaction.amount; 25 | tempMonth.amount += transaction.amount; 26 | tempMonth.monthlyExpense.set(Number(tempDates[2]), tempDay); 27 | myCalendar.set(Number(tempDates[1]), tempMonth); 28 | }); 29 | 30 | let expenseArray = []; 31 | if (listMonth === "Select all") { 32 | myCalendar.forEach((entry) => { 33 | expenseArray.push(entry.amount); 34 | }); 35 | } else { 36 | const currentMonth = myCalendar.get(Number(monthNameToNumber[listMonth])); 37 | currentMonth.monthlyExpense.forEach((entry) => { 38 | expenseArray.push(entry.amount); 39 | }); 40 | } 41 | let labelArray = []; 42 | 43 | if (listMonth === "Select all") { 44 | labelArray = months.map((monthObj) => { 45 | return monthObj.name; 46 | }); 47 | } else { 48 | const currentMonth = myCalendar.get(Number(monthNameToNumber[listMonth])); 49 | let N = currentMonth.monthlyExpense.size; 50 | labelArray = Array.from({ length: N }, (v, k) => k + 1); 51 | } 52 | const dataSet = { 53 | labels: labelArray, 54 | datasets: [ 55 | { 56 | label: "Expenditure", 57 | fill: true, 58 | lineTension: 0.1, 59 | backgroundColor: "rgba(75,192,192,0.4)", 60 | borderColor: "rgba(75,192,192,1)", 61 | borderCapStyle: "butt", 62 | borderDash: [], 63 | borderDashOffset: 0.0, 64 | borderJoinStyle: "miter", 65 | pointBorderColor: "rgba(75,192,192,1)", 66 | pointBackgroundColor: "#fff", 67 | pointBorderWidth: 1, 68 | pointHoverRadius: 7, 69 | pointHoverBackgroundColor: "rgba(75,192,192,1)", 70 | pointHoverBorderColor: "rgba(220,220,220,1)", 71 | pointHoverBorderWidth: 2, 72 | pointRadius: 1, 73 | pointHitRadius: 10, 74 | data: expenseArray, 75 | }, 76 | ], 77 | }; 78 | 79 | return ( 80 |
81 |
85 | 90 |
91 | 92 | 93 |
94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/components/common/Common.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-dsa/bill-manager/b979d63b0acf7527042f229389a34508c7e0a3ad/src/components/common/Common.css -------------------------------------------------------------------------------- /src/components/common/SelectDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export const SelectDropdown = ({ dropdowns, stateUpdater, dropDownparam }) => { 3 | return ( 4 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/expense/TotalExpense.css: -------------------------------------------------------------------------------- 1 | .total-expenses { 2 | position: absolute; 3 | right: 0; 4 | width: 33%; 5 | top: 0; 6 | padding-top: 10%; 7 | padding-right: 2%; 8 | } 9 | 10 | .money { 11 | font-size: 20px; 12 | letter-spacing: 1px; 13 | margin: 5px 0; 14 | } 15 | 16 | .money.minus { 17 | color: #c0392b; 18 | } 19 | 20 | .inc-exp-container { 21 | background-color: #fff; 22 | box-shadow: var(--box-shadow); 23 | padding: 20px; 24 | display: flex; 25 | justify-content: space-between; 26 | } 27 | 28 | .inc-exp-container > div { 29 | flex: 1; 30 | text-align: center; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/expense/TotalExpense.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { GlobalContext } from "../../context/GlobalState"; 3 | import { ITransaction } from "../../models/ITransaction"; 4 | import { TimeSeriesChart } from "../chart/TimeSeriesChart"; 5 | import "./TotalExpense.css"; 6 | export const TotalExpense = () => { 7 | const { 8 | transactions, 9 | }: { transactions: Map } = useContext(GlobalContext); 10 | let expense = 0; 11 | transactions.forEach((transaction) => (expense += transaction.amount)); 12 | 13 | return ( 14 |
15 |
16 |
17 |

Total

18 |

₹{expense}

19 |
20 |
21 | 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/header/Header.css: -------------------------------------------------------------------------------- 1 | .header { 2 | position: absolute; 3 | top: 0; 4 | left: 45%; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Header.css"; 3 | export const Header = () => { 4 | return

Bill Manager

; 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/history/History.css: -------------------------------------------------------------------------------- 1 | .history { 2 | position: absolute; 3 | left: 33%; 4 | width: 30%; 5 | top: 0; 6 | padding-top: 5%; 7 | } 8 | .list { 9 | list-style-type: none; 10 | padding: 0; 11 | margin-bottom: 40px; 12 | } 13 | 14 | .list li { 15 | background-color: #fff; 16 | box-shadow: var(--box-shadow); 17 | color: #333; 18 | display: flex; 19 | justify-content: space-between; 20 | position: relative; 21 | padding: 10px; 22 | margin: 10px 0; 23 | } 24 | .list li:hover .delete-btn { 25 | opacity: 1; 26 | } 27 | 28 | .list li:hover .edit-btn { 29 | opacity: 1; 30 | } 31 | input[type="number"].budget-amount { 32 | border: 1px solid #dedede; 33 | border-radius: 2px; 34 | display: flex; 35 | font-size: 17px; 36 | padding: 10px 10px 10px 5px; 37 | width: 103%; 38 | } 39 | select { 40 | border: 1px solid #dedede; 41 | border-radius: 2px; 42 | display: flex; 43 | font-size: 16px; 44 | width: 90%; 45 | padding: 10px 10px 10px 0px; 46 | } 47 | 48 | .drop-down { 49 | display: "flex"; 50 | flex-direction: column; 51 | width: 33%; 52 | } 53 | -------------------------------------------------------------------------------- /src/components/history/History.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { GlobalContext } from "../../context/GlobalState"; 3 | import { monthNameToNumber, months } from "../../helpers/utils"; 4 | import { ITransaction } from "../../models/ITransaction"; 5 | import { SelectDropdown } from "../common/SelectDropdown"; 6 | import { Transaction } from "../transaction/Transaction"; 7 | import "./History.css"; 8 | export const History = () => { 9 | const { 10 | transactions, 11 | }: { 12 | transactions: Map; 13 | } = useContext(GlobalContext); 14 | const [listCategory, setListCategory] = useState("Select all"); 15 | const [listMonth, setListMonth] = useState("Select all"); 16 | const [amount, setAmount] = useState(undefined); 17 | 18 | let transactionsArray = Array.from(transactions.values()).map( 19 | (transaction) => { 20 | return { ...transaction, selected: false }; 21 | } 22 | ); 23 | 24 | const getCategories = () => { 25 | const categorySet = new Set(); 26 | transactionsArray.forEach((value) => categorySet.add(value.category)); 27 | return Array.from(categorySet.values()).map((value) => { 28 | return { category: value }; 29 | }); 30 | }; 31 | 32 | const filterByParams = () => { 33 | transactionsArray = transactionsArray.sort((a, b) => 34 | a.amount > b.amount ? -1 : a.amount < b.amount ? 1 : 0 35 | ); 36 | transactionsArray = transactionsArray.filter((transaction) => { 37 | if (listCategory === "Select all") { 38 | return transaction; 39 | } else if (transaction.category === listCategory) { 40 | return transaction; 41 | } else { 42 | return null; 43 | } 44 | }); 45 | transactionsArray = transactionsArray.filter((transaction) => { 46 | if (listMonth === "Select all") { 47 | return transaction; 48 | } else if ( 49 | transaction.date.toString().split("-")[1] === 50 | monthNameToNumber[listMonth] 51 | ) { 52 | return transaction; 53 | } else { 54 | return null; 55 | } 56 | }); 57 | 58 | let currentAmount = Number(amount); 59 | transactionsArray = transactionsArray.map((transaction) => { 60 | if (currentAmount - transaction.amount >= 0) { 61 | currentAmount -= transaction.amount; 62 | transaction.selected = true; 63 | } 64 | return transaction; 65 | }); 66 | 67 | return transactionsArray; 68 | }; 69 | 70 | return ( 71 |
72 |

History

73 |
74 |
78 | 79 | 84 |
85 |
89 | 90 | 95 |
96 |
100 |
101 | 102 | { 109 | setAmount(e.target.valueAsNumber); 110 | }} 111 | placeholder="Enter amount..." 112 | /> 113 |
114 |
115 |
116 |
    117 | {filterByParams().map((transaction) => ( 118 | 119 | ))} 120 |
121 |
122 | ); 123 | }; 124 | -------------------------------------------------------------------------------- /src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { Bill } from "./bill/Bill"; 2 | export { EditBill } from "./bill/EditBill"; 3 | export { NewBill } from "./bill/NewBill"; 4 | export { TimeSeriesChart } from "./chart/TimeSeriesChart"; 5 | export { TotalExpense } from "./expense/TotalExpense"; 6 | export { Header } from "./header/Header"; 7 | export { History } from "./history/History"; 8 | export { Transaction } from "./transaction/Transaction"; 9 | -------------------------------------------------------------------------------- /src/components/transaction/Transaction.css: -------------------------------------------------------------------------------- 1 | .btn:focus, 2 | .delete-btn:focus, 3 | .edit-btn:focus { 4 | outline: 0; 5 | } 6 | .delete-btn { 7 | cursor: pointer; 8 | background-color: #e74c3c; 9 | border: 0; 10 | color: #fff; 11 | font-size: 20px; 12 | line-height: 20px; 13 | padding: 2px 5px; 14 | position: absolute; 15 | top: 50%; 16 | left: -1%; 17 | transform: translate(-100%, -50%); 18 | opacity: 0; 19 | transition: opacity 0.3s ease; 20 | } 21 | .list li:hover .delete-btn { 22 | opacity: 1; 23 | } 24 | .show-ellipsis { 25 | overflow: hidden; 26 | text-overflow: ellipsis; 27 | white-space: nowrap; 28 | width: 25%; 29 | } 30 | 31 | .edit-btn { 32 | cursor: pointer; 33 | background-color: green; 34 | border: 0; 35 | color: #fff; 36 | font-size: 20px; 37 | line-height: 20px; 38 | padding: 2px 5px; 39 | position: absolute; 40 | top: 50%; 41 | right: -11%; 42 | transform: translate(-100%, -50%); 43 | opacity: 0; 44 | transition: opacity 0.3s ease; 45 | } 46 | 47 | .list li:hover .edit-btn { 48 | opacity: 1; 49 | } 50 | -------------------------------------------------------------------------------- /src/components/transaction/Transaction.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { GlobalContext } from "../../context/GlobalState"; 3 | import { ITransaction } from "../../models/ITransaction"; 4 | import "./Transaction.css"; 5 | export const Transaction = ({ transaction }: { transaction: ITransaction }) => { 6 | const { 7 | deleteTransaction, 8 | editTransaction, 9 | setDefaultPresent, 10 | }: { 11 | deleteTransaction: (State: any) => void; 12 | editTransaction: (State: any) => void; 13 | setDefaultPresent: (State: any) => void; 14 | } = useContext(GlobalContext); 15 | 16 | return ( 17 |
  • 18 | 26 |
    {transaction.description}
    27 |
    {transaction.category}
    28 |
    29 | {transaction.date.toString().split("-").reverse().join("-")} 30 |
    31 | 32 | 33 | ₹{Math.abs(transaction.amount)} 34 | 35 | 36 | 45 |
  • 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/context/AppReducer.tsx: -------------------------------------------------------------------------------- 1 | import { addElement, deleteElement } from "../helpers/reducerHelper"; 2 | 3 | export default (state, action) => { 4 | switch (action.type) { 5 | case "DELETE_TRANSACTION": 6 | return { 7 | ...state, 8 | defaultTransaction: null, 9 | transactions: deleteElement(state.transactions, action.payload), 10 | }; 11 | case "EDIT_TRANSACTION": 12 | return { 13 | ...state, 14 | defaultTransaction: [state.transactions.get(action.payload)], 15 | }; 16 | case "ADD_TRANSACTION": 17 | return { 18 | ...state, 19 | defaultTransaction: null, 20 | transactions: addElement(state.transactions, action.payload), 21 | }; 22 | case "DISCARD_TRANSACTION": 23 | return { 24 | ...state, 25 | defaultTransaction: null, 26 | }; 27 | default: 28 | return state; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/context/GlobalState.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useReducer, useState } from "react"; 2 | import { mockData } from "../helpers/transactions"; 3 | import { ITransaction } from "../models/ITransaction"; 4 | import AppReducer from "./AppReducer"; 5 | 6 | const ENABLE_MOCK_DATA = true; 7 | 8 | //@ts-ignore 9 | const mocksData: Map = mockData; 10 | // Initial state 11 | const initialState = { 12 | transactions: new Map( 13 | ENABLE_MOCK_DATA ? mocksData : null 14 | ), 15 | defaultTransaction: new Map(), 16 | deleteTransaction: (State: any) => {}, 17 | addTransaction: (State: any) => {}, 18 | editTransaction: (State: any) => {}, 19 | discardTransaction: () => {}, 20 | defaultPresent: false, 21 | setDefaultPresent: (State: any) => {}, 22 | }; 23 | 24 | export const GlobalContext = createContext(initialState); 25 | 26 | // Provider component 27 | export const GlobalProvider = ({ children }) => { 28 | const [state, dispatch] = useReducer(AppReducer, initialState); 29 | const [defaultPresent, setDefaultPresent] = useState(false); 30 | // Actions 31 | const deleteTransaction = (id: any) => { 32 | dispatch({ 33 | type: "DELETE_TRANSACTION", 34 | payload: id, 35 | }); 36 | }; 37 | 38 | const addTransaction = (transaction: ITransaction) => { 39 | dispatch({ 40 | type: "ADD_TRANSACTION", 41 | payload: transaction, 42 | }); 43 | }; 44 | 45 | const editTransaction = (id: any) => { 46 | dispatch({ 47 | type: "EDIT_TRANSACTION", 48 | payload: id, 49 | }); 50 | }; 51 | const discardTransaction = () => { 52 | dispatch({ 53 | type: "DISCARD_TRANSACTION", 54 | }); 55 | }; 56 | 57 | return ( 58 | 70 | {children} 71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/helpers/reducerHelper.ts: -------------------------------------------------------------------------------- 1 | export const deleteElement = (transactions, id) => { 2 | transactions.delete(id); 3 | return new Map(transactions); 4 | }; 5 | 6 | export const addElement = (transactions, transaction) => { 7 | transactions.set(transaction.id, transaction); 8 | return new Map(transactions); 9 | }; 10 | -------------------------------------------------------------------------------- /src/helpers/transactions.js: -------------------------------------------------------------------------------- 1 | export const mockData = [ 2 | [ 3 | 940, 4 | { 5 | id: 940, 6 | description: "Rajma Chawal", 7 | amount: 300, 8 | category: "Food", 9 | date: "2020-01-17", 10 | }, 11 | ], 12 | [ 13 | 941, 14 | { 15 | id: 941, 16 | description: "Dal Fry", 17 | amount: 300, 18 | category: "Food", 19 | date: "2020-01-18", 20 | }, 21 | ], 22 | [ 23 | 942, 24 | { 25 | id: 942, 26 | description: "Masala Dosa", 27 | amount: 300, 28 | category: "Food", 29 | date: "2020-01-18", 30 | }, 31 | ], 32 | [ 33 | 943, 34 | { 35 | id: 943, 36 | description: "Veg Kebab", 37 | amount: 300, 38 | category: "Food", 39 | date: "2020-01-18", 40 | }, 41 | ], 42 | [ 43 | 944, 44 | { 45 | id: 944, 46 | description: "Chicken Kebab", 47 | amount: 300, 48 | category: "Food", 49 | date: "2020-01-18", 50 | }, 51 | ], 52 | [ 53 | 776, 54 | { 55 | id: 776, 56 | description: "Chicken", 57 | amount: 500, 58 | category: "Food", 59 | date: "2020-04-02", 60 | }, 61 | ], 62 | [ 63 | 777, 64 | { 65 | id: 777, 66 | description: "Malai Chicken", 67 | amount: 500, 68 | category: "Food", 69 | date: "2020-04-06", 70 | }, 71 | ], 72 | [ 73 | 714, 74 | { 75 | id: 714, 76 | description: "Electricity", 77 | amount: 1700, 78 | category: "Bill", 79 | date: "2020-06-01", 80 | }, 81 | ], 82 | [ 83 | 715, 84 | { 85 | id: 715, 86 | description: "Electricity", 87 | amount: 1900, 88 | category: "Bill", 89 | date: "2020-07-01", 90 | }, 91 | ], 92 | [ 93 | 701, 94 | { 95 | id: 701, 96 | description: "Water", 97 | amount: 1700, 98 | category: "Bill", 99 | date: "2020-03-11", 100 | }, 101 | ], 102 | [ 103 | 702, 104 | { 105 | id: 702, 106 | description: "Water", 107 | amount: 1700, 108 | category: "Bill", 109 | date: "2020-03-11", 110 | }, 111 | ], 112 | [ 113 | 703, 114 | { 115 | id: 703, 116 | description: "Water", 117 | amount: 1500, 118 | category: "Bill", 119 | date: "2020-04-11", 120 | }, 121 | ], 122 | [ 123 | 704, 124 | { 125 | id: 704, 126 | description: "Water", 127 | amount: 1600, 128 | category: "Bill", 129 | date: "2020-05-11", 130 | }, 131 | ], 132 | [ 133 | 705, 134 | { 135 | id: 705, 136 | description: "Wine", 137 | amount: 1600, 138 | category: "Drinks", 139 | date: "2020-05-11", 140 | }, 141 | ], 142 | [ 143 | 706, 144 | { 145 | id: 706, 146 | description: "Juice", 147 | amount: 1600, 148 | category: "Drinks", 149 | date: "2020-05-11", 150 | }, 151 | ], 152 | [ 153 | 707, 154 | { 155 | id: 707, 156 | description: "Coconut Water", 157 | amount: 1600, 158 | category: "Drinks", 159 | date: "2020-05-11", 160 | }, 161 | ], 162 | [ 163 | 708, 164 | { 165 | id: 708, 166 | description: "Bottled Water", 167 | amount: 1600, 168 | category: "Drinks", 169 | date: "2020-05-11", 170 | }, 171 | ], 172 | [ 173 | 940, 174 | { 175 | id: 940, 176 | description: "Rajma Chawal", 177 | amount: 300, 178 | category: "Food", 179 | date: "2020-01-17", 180 | }, 181 | ], 182 | [ 183 | 948, 184 | { 185 | id: 948, 186 | description: "Dal Fry", 187 | amount: 300, 188 | category: "Food", 189 | date: "2020-02-18", 190 | }, 191 | ], 192 | [ 193 | 947, 194 | { 195 | id: 947, 196 | description: "Masala Dosa", 197 | amount: 300, 198 | category: "Food", 199 | date: "2020-01-19", 200 | }, 201 | ], 202 | [ 203 | 946, 204 | { 205 | id: 946, 206 | description: "Veg Kebab", 207 | amount: 300, 208 | category: "Food", 209 | date: "2020-06-08", 210 | }, 211 | ], 212 | [ 213 | 945, 214 | { 215 | id: 945, 216 | description: "Chicken Kebab", 217 | amount: 300, 218 | category: "Food", 219 | date: "2020-03-18", 220 | }, 221 | ], 222 | ]; 223 | -------------------------------------------------------------------------------- /src/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import { IDailyExpense } from "../models/IDailyExpense"; 2 | import { IMonthlyExpense } from "../models/IMonthlyExpense"; 3 | 4 | export const monthNameToNumber = { 5 | January: "01", 6 | February: "02", 7 | March: "03", 8 | April: "04", 9 | May: "05", 10 | June: "06", 11 | July: "07", 12 | August: "08", 13 | September: "09", 14 | October: "10", 15 | November: "11", 16 | December: "12", 17 | }; 18 | 19 | export const monthNumberToName = { 20 | "01": "January", 21 | "02": "February", 22 | "03": "March", 23 | "04": "April", 24 | "05": "May", 25 | "06": "June", 26 | "07": "July", 27 | "08": "August", 28 | "09": "September", 29 | "10": "October", 30 | "11": "November", 31 | "12": "December", 32 | }; 33 | 34 | export const dayNumberToName = { 35 | "1": "Monday", 36 | "2": "Tuesday", 37 | "3": "Wednesday", 38 | "4": "Thursday", 39 | "5": "Friday", 40 | "6": "Saturday", 41 | "0": "Sunday", 42 | }; 43 | 44 | export const daysInMonth = (month, year) => { 45 | return new Date(year, month, 0).getDate(); 46 | }; 47 | 48 | export const computeCalendar = () => { 49 | const yearlyExpense = new Map(); 50 | for (let i = 1; i <= 12; i++) { 51 | let monthlyExpense: IMonthlyExpense = { 52 | amount: 0, 53 | monthlyExpense: new Map(), 54 | name: "", 55 | }; 56 | const monthsDays = daysInMonth(i, 2020); 57 | for (let j = 1; j <= monthsDays; j++) { 58 | let currentDay = new Date("2020-" + i + "-" + j); 59 | let dailyExpense: IDailyExpense = { 60 | amount: 0, 61 | dayName: dayNumberToName[currentDay.getDay()], 62 | dayNumber: currentDay.getDay(), 63 | }; 64 | monthlyExpense.amount += dailyExpense.amount; 65 | monthlyExpense.monthlyExpense.set(j, dailyExpense); 66 | } 67 | let currentDay = new Date("2020-" + i + "-" + 1); 68 | let currentMonth = (currentDay.getMonth() + 1).toString(); 69 | if (i < 10) { 70 | currentMonth = "0" + currentMonth; 71 | } 72 | monthlyExpense.name = monthNumberToName[currentMonth]; 73 | yearlyExpense.set(i, monthlyExpense); 74 | } 75 | return yearlyExpense; 76 | }; 77 | 78 | export const months = [ 79 | { name: "January" }, 80 | { name: "February" }, 81 | { name: "March" }, 82 | { name: "April" }, 83 | { name: "May" }, 84 | { name: "June" }, 85 | { name: "July" }, 86 | { name: "August" }, 87 | { name: "September" }, 88 | { name: "October" }, 89 | { name: "November" }, 90 | { name: "December" }, 91 | ]; 92 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/models/IDailyExpense.ts: -------------------------------------------------------------------------------- 1 | export interface IDailyExpense { 2 | dayName: string; 3 | dayNumber: number; 4 | amount: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/models/IExpense.ts: -------------------------------------------------------------------------------- 1 | export interface IExpense { 2 | amount: number; 3 | } 4 | -------------------------------------------------------------------------------- /src/models/IMonthlyExpense.ts: -------------------------------------------------------------------------------- 1 | import { IDailyExpense } from "./IDailyExpense"; 2 | 3 | export interface IMonthlyExpense { 4 | monthlyExpense: Map; 5 | amount: number; 6 | name: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/models/ITransaction.ts: -------------------------------------------------------------------------------- 1 | export interface ITransaction { 2 | id: number; 3 | description: string; 4 | category: string; 5 | amount: number; 6 | date: Date; 7 | selected?: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"] 19 | } 20 | --------------------------------------------------------------------------------