├── .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 |
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 |
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 |
--------------------------------------------------------------------------------