├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── _redirects
└── index.html
└── src
├── components
├── ExpenseCard.jsx
├── Main.jsx
├── Modal.jsx
├── NavBar.jsx
├── RadioGroup.jsx
├── Select.jsx
└── Tabs.jsx
├── index.js
├── pages
├── Dashboard.jsx
├── Home.jsx
├── Login.jsx
└── SignUp.jsx
├── queries
├── AddExpense.js
├── CurrentUser.js
├── DeleteExpense.js
├── LoginMutation.js
├── SignUpMutation.js
├── UpdateUser.js
└── UsersExpenses.js
└── styles
├── base
├── grid.scss
├── reset.scss
└── typography.scss
├── components
├── button.scss
├── modal.scss
└── radiogroup.scss
├── helpers
├── mixins.scss
└── variables.scss
├── index.scss
└── layout
├── account.scss
├── addexpense.scss
├── form.scss
├── insights.scss
├── main.scss
├── navbar.scss
├── select.scss
└── tabs.scss
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | ["import", {
4 | "libraryName": "antd",
5 | "libraryDirectory": "es",
6 | "style": "css"
7 | }]
8 | ]
9 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | debug.log
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Praveen Bisht
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Expense App - Front End
2 |
3 | Links - [Demo](https://expense-app.netlify.com/) | [Backend](https://github.com/prvnbist/expense-app-backend)
4 |
5 | Status - Online
6 |
7 | 
8 |
9 | #### Tech Stack
10 | - ReactJs
11 | - Apollo Client
12 |
13 | #### Getting Started
14 | - `git clone https://github.com/prvnbist/expense-app-frontend.git`
15 | - `npm install` to install all the required packages.
16 | - Go into the `.env` file and replace the value of `REACT_APP_SERVER_URL` with the url where your server is running.
17 | - `npm start` to run the server locally on `http://localhost:3000`
18 | - `npm run build` to build the project for production.
19 | ---
20 | *Happy Coding!*
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "expense-app-frontend",
3 | "version": "1.0.0",
4 | "homepage": ".",
5 | "dependencies": {
6 | "apollo-boost": "^0.4.0",
7 | "chart.js": "^2.8.0",
8 | "dotenv": "^8.0.0",
9 | "formik": "^1.5.7",
10 | "graphql": "^14.3.1",
11 | "graphql-tag": "^2.10.1",
12 | "node-sass": "^4.12.0",
13 | "react": "^16.8.6",
14 | "react-apollo": "^2.5.6",
15 | "react-chartjs-2": "^2.7.6",
16 | "react-dom": "^16.8.6",
17 | "react-router-dom": "^5.0.0",
18 | "react-scripts": "3.0.1",
19 | "yup": "^0.27.0"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": "react-app"
29 | },
30 | "browserslist": [
31 | ">0.2%",
32 | "not dead",
33 | "not ie <= 11",
34 | "not op_mini all"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Expense Manager
9 |
10 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/ExpenseCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Mutation} from 'react-apollo';
3 | import DELETE_EXPENSE_MUTATION from '../queries/DeleteExpense';
4 |
5 | const Expense = props => {
6 | return (
7 |
15 |
16 |
17 | {props.item.spentOn.length > 30
18 | ? this
19 | .props
20 | .item
21 | .spentOn
22 | .slice(0, 20) + '...'
23 | : props.item.spentOn}
24 |
25 |
30 | {props.item.type === "plus"
37 | ? '+'
38 | : '-'} {parseInt(props.item.amount).toLocaleString("en-IN", {
39 | style: 'currency',
40 | currency: "INR"
41 | })}
42 | ['usersExpenses']}>
48 | {mutation => }
51 |
52 |
53 |
54 |
55 |
56 | category
57 | {props.item.category}
58 |
59 |
60 | access_time
61 |
62 | {new Date(Number(props.item.createdAt))
63 | .toTimeString()
64 | .slice(0, 5)}
65 |
66 |
67 |
68 | event
69 | {new Date(Number(props.item.createdAt))
70 | .toDateString()
71 | .replace(' 2018', '')}
72 |
73 |
74 |
75 |
76 | description
77 | {props.item.description
78 | ? props.item.description
79 | : 'No description.'}
80 |
81 |
82 |
83 | );
84 | }
85 |
86 | export default Expense;
--------------------------------------------------------------------------------
/src/components/Main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Query, Mutation} from 'react-apollo';
3 | import {Formik} from 'formik';
4 | import * as Yup from 'yup';
5 | import {Pie, Line} from "react-chartjs-2";
6 |
7 | import ADD_EXPENSE_MUTATION from '../queries/AddExpense';
8 | import USERS_EXPENSES from '../queries/UsersExpenses';
9 |
10 | import Tabs from './Tabs';
11 | import Expense from './ExpenseCard';
12 | import Select from './Select';
13 | import RadioGroup from './RadioGroup';
14 | import Modal from './Modal';
15 |
16 | const Main = () => {
17 | const [filters,
18 | setFilters] = React.useState({search: "", category: "", type: ""});
19 | const [modal,
20 | showModal] = React.useState(false);
21 | const selected = option => setFilters({
22 | ...filters,
23 | category: option
24 | });
25 | const typeSelected = option => setFilters({
26 | ...filters,
27 | type: option
28 | });
29 | const closeModal = value => showModal(value);
30 |
31 | const categories = [
32 | "Mortgage",
33 | "Rent",
34 | "Property Taxes",
35 | "House/Tenant Insurance",
36 | "Utility bills",
37 | "Lease/Car Loan Payment",
38 | "Vehicle Insurance",
39 | "Life/Disability/Extended Health",
40 | "Bank Fees",
41 | "Debt Payments",
42 | "Salary",
43 | "Electronics",
44 | "Groceries",
45 | "Personal Care",
46 | "Fuel/Public Transportation",
47 | "Parking",
48 | "Clothing & Shoes",
49 | "Daycare",
50 | "Work Lunches & Snacks",
51 | "Eating Out",
52 | "Entertainment",
53 | "Tobacco/Alcohol",
54 | "Lottery",
55 | "Babysitting",
56 | "Sports & Recreation",
57 | "Hair Care/Salon Services",
58 | "Magazines/Newspapers/Books",
59 | "Children’s Lessons & Activities",
60 | "Furniture"
61 | ];
62 | const radioOptions = [
63 | {
64 | value: "plus",
65 | text: "Earned"
66 | }, {
67 | value: "minus",
68 | text: "Spent"
69 | }
70 | ];
71 | const expenseSchema = Yup
72 | .object()
73 | .shape({
74 | spentOn: Yup
75 | .string()
76 | .required(),
77 | amount: Yup
78 | .number()
79 | .typeError()
80 | .required(),
81 | description: Yup
82 | .string()
83 | .max(80)
84 | });
85 | return (
86 |
87 | {modal && (
88 |
89 | ['usersExpenses']}>
92 | {addExpense => (
93 | {
103 | addExpense({
104 | variables: {
105 | spentOn: values.spentOn,
106 | category: values.category,
107 | amount: values.amount,
108 | type: values.type,
109 | description: values.description
110 | }
111 | });
112 | setSubmitting(false);
113 | }}>
114 | {({
115 | values,
116 | errors,
117 | touched,
118 | handleChange,
119 | handleBlur,
120 | handleSubmit,
121 | isSubmitting
122 | }) => (
123 |
124 |
208 |
209 | )}
210 |
211 | )}
212 |
213 |
214 | )}
215 |
216 |
217 |
218 |
222 |
223 | setFilters({
230 | ...filters,
231 | search: e.target.value
232 | })}/>
233 |
236 |
237 |
243 |
250 |
251 |
258 | {({loading, error, data}) => {
259 | if (loading)
260 | return
268 |

271 |
;
272 | if (error)
273 | return `Error! ${error.message}`;
274 | return
275 | {
276 | data.usersExpenses.length === 0
277 | ? (
278 |
291 | )
292 | : (
293 | {data
294 | .usersExpenses
295 | .map((item, index) => )}
296 | )
297 | }
298 | ;
299 | }}
300 |
301 |
302 |
303 |
304 |
305 | {({client, loading, error, data: {
306 | usersExpenses
307 | }}) => {
308 | if (loading)
309 | return
310 | Loading...
311 |
;
312 | if (error)
313 | return `Error! ${error.message}`;
314 | const totalSpent = usersExpenses
315 | .filter(expense => expense.type === "minus")
316 | .reduce((total, expense) => Number(expense.amount) + total, 0);
317 | const totalExpenses = usersExpenses.length;
318 | return (
319 |
320 |
321 |
322 |
323 | {parseInt(totalSpent).toLocaleString("en-IN", {
324 | style: 'currency',
325 | currency: "INR"
326 | })}
327 | rupees spent so far
328 |
329 |
330 |
331 |
332 |
333 | {totalExpenses}
334 | expenses so far
335 |
336 |
337 |
338 | )
339 | }}
340 |
341 |
342 |
343 |
344 | {({client, loading, error, data: {
345 | usersExpenses
346 | }}) => {
347 | if (loading)
348 | return
349 | Loading...
350 |
;
351 | if (error)
352 | return `Error! ${error.message}`;
353 | let categories = [...new Set(usersExpenses.filter(item => item.type === "minus").map(item => item.category))];
354 | let amount = [];
355 | let catAmount = 0;
356 | for (let i in categories) {
357 | for (let j in usersExpenses) {
358 | if (usersExpenses[j].category === categories[i]) {
359 | catAmount += Number(usersExpenses[j].amount);
360 | }
361 | }
362 | amount.push(catAmount);
363 | catAmount = 0;
364 | }
365 | let obj = [];
366 | for (let i = 0; i < categories.length; i++) {
367 | obj.push({category: categories[i], amount: amount[i]});
368 | }
369 | let sortedData = obj.sort((a, b) => b.amount - a.amount).slice(0, 5);
370 | const data = {
371 | labels: sortedData.map(i => i.category),
372 | datasets: [
373 | {
374 | data: sortedData.map(i => i.amount),
375 | backgroundColor: [
376 | "#E84C3D", "#3598DB", "#1BBC9B", "#F1C40F", "#34495E"
377 | ],
378 | hoverBackgroundColor: ["#E84C3D", "#3598DB", "#1BBC9B", "#F1C40F", "#34495E"]
379 | }
380 | ]
381 | };
382 |
383 | const getMonth = data => new Intl
384 | .DateTimeFormat("en-US", {month: "long"})
385 | .format(new Date(Number(data)));
386 | const getYear = data => new Intl
387 | .DateTimeFormat("en-US", {year: "numeric"})
388 | .format(new Date(Number(data)));
389 |
390 | let months = [...new Set(usersExpenses.filter(item => item.type === "minus").map(i => `${getMonth(i.createdAt)} ${getYear(i.createdAt)}`))].reverse();
391 |
392 | let amount1 = [];
393 | let catAmount1 = 0;
394 | let filterRaw = usersExpenses.filter(item => item.type === "minus");
395 | for (let i in months) {
396 | for (let j in filterRaw) {
397 | let month = `${getMonth(filterRaw[j].createdAt)}`;
398 | let year = `${getYear(filterRaw[j].createdAt)}`;
399 |
400 | if (months[i].includes(month) && months[i].includes(year)) {
401 | catAmount1 += Number(filterRaw[j].amount);
402 | }
403 | }
404 | amount1.push(catAmount1);
405 | catAmount1 = 0;
406 | }
407 | const data1 = {
408 | labels: months,
409 | datasets: [
410 | {
411 | data: amount1,
412 | fill: false,
413 | lineTension: 0.1,
414 | backgroundColor: "rgba(75,192,192,0.4)",
415 | borderColor: "rgba(75,192,192,1)",
416 | borderCapStyle: "butt",
417 | borderDash: [],
418 | borderDashOffset: 0.0,
419 | borderJoinStyle: "miter",
420 | pointBorderColor: "rgba(75,192,192,1)",
421 | pointBackgroundColor: "#fff",
422 | pointBorderWidth: 1,
423 | pointHoverRadius: 5,
424 | pointHoverBackgroundColor: "rgba(75,192,192,1)",
425 | pointHoverBorderColor: "rgba(220,220,220,1)",
426 | pointHoverBorderWidth: 2,
427 | pointRadius: 1,
428 | pointHitRadius: 10
429 | }
430 | ]
431 | };
432 |
433 | return (
434 |
435 |
436 |
437 |
438 |
454 |
455 |
456 |
457 |
458 |
459 |
464 |
465 |
466 |
467 | )
468 | }}
469 |
470 |
471 |
472 |
473 |
474 | );
475 | }
476 |
477 | export default Main;
--------------------------------------------------------------------------------
/src/components/Modal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const ModalMain = ({children}) => {children};
4 | const ModalFooter = ({children}) => ;
5 |
6 | class Modal extends React.Component {
7 | static Main = ModalMain;
8 | static Footer = ModalFooter;
9 | render() {
10 | return (
11 |
12 |
13 |
14 | {this.props.title}
15 | this.props.closeModal(false)}>close
16 |
17 | {this.props.children}
18 |
19 |
20 | );
21 | }
22 | }
23 |
24 | export default Modal;
--------------------------------------------------------------------------------
/src/components/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router-dom';
3 | import {Query, Mutation} from 'react-apollo';
4 | import {Formik} from 'formik';
5 | import * as Yup from 'yup';
6 |
7 | import CURRENT_USER from '../queries/CurrentUser';
8 | import UPDATE_USER_MUTATION from '../queries/UpdateUser';
9 |
10 | import Modal from './Modal';
11 |
12 | const NavBar = (props) => {
13 | const [passwordVisibility,
14 | setPasswordVisibility] = React.useState(false);
15 | const [isFormUpdated,
16 | setIsFormUpdated] = React.useState(false);
17 | const [modal,
18 | showModal] = React.useState(false);
19 | const logOut = () => {
20 | localStorage.removeItem('access_token');
21 | }
22 | const showPassword = e => {
23 | if (!passwordVisibility) {
24 | e.target.innerText = 'visibility';
25 | document
26 | .getElementById('password-input')
27 | .type = "text";
28 | setPasswordVisibility(!passwordVisibility);
29 | return;
30 | }
31 | e.target.innerText = 'visibility_off';
32 | document
33 | .getElementById('password-input')
34 | .type = "password";
35 | setPasswordVisibility(!passwordVisibility);
36 | }
37 | const closeModal = value => showModal(value);
38 | const SignupSchema = Yup
39 | .object()
40 | .shape({
41 | name: Yup
42 | .string()
43 | .min(4, 'Name is too short!')
44 | .max(50, 'Name is too long!'),
45 | email: Yup
46 | .string()
47 | .email('Please enter a valid email!'),
48 | username: Yup
49 | .string()
50 | .min(4, 'Username is too short!')
51 | .max(50, 'Username is too long!')
52 | .matches(/^[a-zA-Z0-9-_]+$/, "Username must have _alpha_numerics"),
53 | password: Yup
54 | .string()
55 | .min(8, 'Password is too short!')
56 | .max(20, 'Password is too long!')
57 | .matches(/(?=^.{8,20}$)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?!.*\s)[0-9a-zA-Z!@#$%^&*()]*$/, "Password must have atleast one lowercase letter, one uppercase letter, one digit" +
58 | " and one special character.")
59 | });
60 | return (
61 |
266 | )
267 | }
268 |
269 | export default NavBar;
--------------------------------------------------------------------------------
/src/components/RadioGroup.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const RadioGroup = props => {
4 | const [checkedOption, setcheckedOption] = React.useState("");
5 | return (
6 |
7 | {props.options.map((option, index) => (
8 |
9 | {
16 | setcheckedOption(e.target.value);
17 | props.selected(e.target.value);
18 | }}
19 | onClick={() => {
20 | props.selected("");
21 | return checkedOption === option.value
22 | ? setcheckedOption("")
23 | : null;
24 | }}
25 | />
26 |
27 |
28 | ))}
29 |
30 | );
31 | };
32 |
33 | export default RadioGroup;
--------------------------------------------------------------------------------
/src/components/Select.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Select = props => {
4 | const [visibility,
5 | setVisibility] = React.useState(false);
6 | const [selectedOption,
7 | setSelectedOption] = React.useState("");
8 | const [search,
9 | setSearch] = React.useState("");
10 | return (
11 | {
14 | setVisibility(!visibility);
15 | setSearch("");
16 | e.currentTarget.children[0].children[1].innerHTML = visibility
17 | ? "arrow_drop_down"
18 | : "arrow_drop_up";
19 | }}>
20 |
21 |
30 | {selectedOption === ""
31 | ? props.placeholder
32 | : selectedOption.length <= 20
33 | ? selectedOption
34 | : `${selectedOption.slice(0, 20)}...`}
35 |
36 | arrow_drop_down
37 |
38 | {visibility && (
39 |
40 |
41 | e.stopPropagation()}
46 | onChange={e => setSearch(e.target.value)}/>
47 |
48 |
49 | - {
52 | setSelectedOption("");
53 | props.selected("")
54 | }}>
55 | {props.placeholder}
56 |
57 | {props
58 | .options
59 | .filter(option => option.toLowerCase().includes(search.toLowerCase()))
60 | .map((option, index) => (
61 | - {
67 | setSelectedOption(option);
68 | props.selected(option);
69 | }}>
70 | {option}
71 |
72 | ))}
73 |
74 |
75 | )}
76 |
77 | );
78 | };
79 |
80 | export default Select;
--------------------------------------------------------------------------------
/src/components/Tabs.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Tab = props => {
4 | const onClick = () => {
5 | const {label, onClick} = props;
6 | onClick(label);
7 | };
8 |
9 | let className = "tab-list-item";
10 |
11 | if (props.activeTab === props.label) {
12 | className += " tab-list-active";
13 | }
14 | return (
15 |
16 | {props.label}
17 |
18 | );
19 | };
20 |
21 | const Tabs = props => {
22 | const [activeTab,
23 | setActiveTab] = React.useState(props.children[0].props.label);
24 | const onClickTabItem = tab => {
25 | setActiveTab(tab);
26 | };
27 | return (
28 |
29 |
30 |
35 |
36 |
37 | {props
38 | .children
39 | .map(child => {
40 | const {label} = child.props;
41 | return ();
42 | })}
43 |
44 |
45 |
46 |
47 |
48 |
49 | {props
50 | .children
51 | .map(child => {
52 | if (child.props.label !== activeTab)
53 | return undefined;
54 | return child.props.children;
55 | })}
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default Tabs;
64 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import ReactDOM from "react-dom";
3 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
4 |
5 | // Apollo Imports
6 | import { ApolloProvider } from "react-apollo";
7 | import {
8 | ApolloClient,
9 | ApolloLink,
10 | InMemoryCache,
11 | HttpLink
12 | } from "apollo-boost";
13 |
14 | // Components
15 | import Home from "./pages/Home.jsx";
16 | import Dashboard from "./pages/Dashboard.jsx";
17 |
18 | // Styles
19 | import "./styles/index.scss";
20 | require("dotenv").config();
21 | // Keys
22 | const httpLink = new HttpLink({ uri: process.env.REACT_APP_SERVER_URL });
23 |
24 | // Middleware to set the headers
25 | const authLink = new ApolloLink((operation, forward) => {
26 | if (localStorage.getItem("access_token") !== undefined) {
27 | const token = localStorage.getItem("access_token");
28 | operation.setContext({
29 | headers: {
30 | authorization: token ? `Bearer ${token}` : ""
31 | }
32 | });
33 | return forward(operation);
34 | }
35 | });
36 |
37 | const client = new ApolloClient({
38 | link: authLink.concat(httpLink),
39 | cache: new InMemoryCache(),
40 | fetchOptions: {
41 | credentials: "include"
42 | },
43 | onError: ({ networkError }) => {
44 | if (networkError) console.log("Network Error", networkError);
45 | }
46 | });
47 |
48 | class App extends Component {
49 | render() {
50 | return (
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | }
64 | }
65 |
66 | ReactDOM.render(, document.getElementById("root"));
67 |
--------------------------------------------------------------------------------
/src/pages/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | // import Header from '../components/Header';
4 | import Main from '../components/Main';
5 | import NavBar from '../components/NavBar';
6 |
7 | export default class Dashboard extends Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
14 | )
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import NavBar from "../components/NavBar";
4 | import Tabs from "../components/Tabs";
5 | import Login from "./Login";
6 | import SignUp from "./SignUp";
7 |
8 | export default class Home extends Component {
9 | render() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Mutation } from "react-apollo";
3 | import { Formik } from "formik";
4 |
5 | import LOGIN_MUTATION from "../queries/LoginMutation";
6 |
7 | class Login extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | show: false,
12 | buttonText: "Login",
13 | authError: null
14 | };
15 | }
16 | showPassword = e => {
17 | if (!this.state.show) {
18 | e.target.innerText = "visibility";
19 | document.getElementById("password-input").type = "text";
20 | this.setState({
21 | show: !this.state.show
22 | });
23 | return;
24 | }
25 | e.target.innerText = "visibility_off";
26 | document.getElementById("password-input").type = "password";
27 | this.setState({
28 | show: !this.state.show
29 | });
30 | };
31 | submitForm = ({ login }) => {
32 | localStorage.clear();
33 | localStorage.setItem("access_token", login);
34 | this.props.history.push("/dashboard");
35 | };
36 | render() {
37 | return (
38 |
39 |
Login to view your expenses.
40 |
43 | this.setState({
44 | authError: err.message.split(":")[1].trim(),
45 | buttonText: "Login"
46 | })
47 | }
48 | onCompleted={data => this.submitForm(data)}
49 | >
50 | {loginMutation => (
51 | {
57 | this.setState({
58 | authError: null
59 | });
60 | let errors = {};
61 | if (values.username === "") {
62 | errors.username = "Please fill in your username";
63 | }
64 | if (values.password === "") {
65 | errors.password = "Please fill in your password";
66 | }
67 | return errors;
68 | }}
69 | onSubmit={(values, { setSubmitting }) => {
70 | this.setState({ buttonText: "Logging In..." });
71 | setTimeout(() => {
72 | loginMutation({
73 | variables: {
74 | username: values.username,
75 | password: values.password
76 | }
77 | });
78 | setSubmitting(false);
79 | }, 400);
80 | }}
81 | >
82 | {({
83 | values,
84 | errors,
85 | touched,
86 | handleChange,
87 | handleBlur,
88 | handleSubmit,
89 | isSubmitting
90 | }) => (
91 |
151 | )}
152 |
153 | )}
154 |
155 |
156 | );
157 | }
158 | }
159 |
160 | export default Login;
161 |
--------------------------------------------------------------------------------
/src/pages/SignUp.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Mutation } from "react-apollo";
3 | import { Formik } from "formik";
4 | import * as Yup from "yup";
5 |
6 | import SIGNUP_MUTATION from "../queries/SignUpMutation";
7 | export default class SignUp extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | show: false,
12 | buttonText: "Sign Up",
13 | authError: null
14 | };
15 | }
16 | showPassword = e => {
17 | if (!this.state.show) {
18 | e.target.innerText = "visibility";
19 | document.getElementById("password-input").type = "text";
20 | this.setState({
21 | show: !this.state.show
22 | });
23 | return;
24 | }
25 | e.target.innerText = "visibility_off";
26 | document.getElementById("password-input").type = "password";
27 | this.setState({
28 | show: !this.state.show
29 | });
30 | };
31 | submitForm = ({ signup }) => {
32 | localStorage.clear();
33 | localStorage.setItem("access_token", signup);
34 | this.props.history.push("/dashboard");
35 | };
36 | render() {
37 | const SignupSchema = Yup.object().shape({
38 | name: Yup.string()
39 | .min(4, "Name is too short!")
40 | .max(50, "Name is too long!")
41 | .required("Name is required!"),
42 | email: Yup.string()
43 | .email("Please enter a valid email!")
44 | .required("Email is required!"),
45 | username: Yup.string()
46 | .min(4, "Username is too short!")
47 | .max(50, "Username is too long!")
48 | .matches(/^[a-zA-Z0-9-_]+$/, "Username must have _alpha_numerics")
49 | .required("Username is required!"),
50 | password: Yup.string()
51 | .min(8, "Password is too short!")
52 | .max(20, "Password is too long!")
53 | .matches(
54 | /(?=^.{8,20}$)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?!.*\s)[0-9a-zA-Z!@#$%^&*()]*$/,
55 | "Password must have atleast one lowercase letter, one uppercase letter, one digit" +
56 | " and one special character."
57 | )
58 | .required("Password is required!")
59 | });
60 | return (
61 |
62 |
63 | Register to start managing your expenses.
64 |
65 |
{
68 | const errors = err.message
69 | .split(",")
70 | .map(i => i.split(":"))
71 | .map(i => i.reverse()[0])
72 | .map(i => i.trim());
73 | this.setState({
74 | authError: errors,
75 | buttonText: "Sign Up"
76 | });
77 | }}
78 | onCompleted={data => this.submitForm(data)}
79 | >
80 | {signUpMutation => (
81 |
90 | this.setState({
91 | authError: null
92 | }) || SignupSchema
93 | }
94 | onSubmit={(values, { setSubmitting }) => {
95 | this.setState({ buttonText: "Signing Up..." });
96 | setTimeout(() => {
97 | signUpMutation({
98 | variables: {
99 | username: values.username,
100 | password: values.password,
101 | name: values.name,
102 | email: values.email,
103 | gender: values.gender
104 | }
105 | });
106 | setSubmitting(false);
107 | }, 400);
108 | }}
109 | >
110 | {({
111 | values,
112 | errors,
113 | touched,
114 | handleChange,
115 | handleBlur,
116 | handleSubmit,
117 | isSubmitting
118 | }) => (
119 |
235 | )}
236 |
237 | )}
238 |
239 |
240 | );
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/src/queries/AddExpense.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const ADD_EXPENSE_MUTATION = gql `
4 | mutation AddExpense($spentOn: String!, $category: String! $amount: String!, $description: String!,$type:String!){
5 | addExpense(spentOn:$spentOn,category:$category,amount:$amount,description:$description, type:$type) {
6 | spentOn
7 | }
8 | }
9 | `
10 |
11 | export default ADD_EXPENSE_MUTATION;
--------------------------------------------------------------------------------
/src/queries/CurrentUser.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const CURRENT_USER = gql `
4 | query currentUser {
5 | me {
6 | id
7 | name
8 | username
9 | email
10 | gender
11 | balance
12 | }
13 | }
14 | `;
15 |
16 | export default CURRENT_USER;
--------------------------------------------------------------------------------
/src/queries/DeleteExpense.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const DELETE_EXPENSE_MUTATION = gql `
4 | mutation DeleteExpense($id: String!){
5 | deleteExpense(id: $id) {
6 | spentOn
7 | }
8 | }
9 | `
10 |
11 | export default DELETE_EXPENSE_MUTATION;
--------------------------------------------------------------------------------
/src/queries/LoginMutation.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const LOGIN_MUTATION = gql `
4 | mutation LoginMutation($username: String!, $password: String!) {
5 | login(username: $username, password: $password)
6 | }
7 | `
8 |
9 | export default LOGIN_MUTATION;
--------------------------------------------------------------------------------
/src/queries/SignUpMutation.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const SIGNUP_MUTATION = gql `
4 | mutation SignUpMutation($username: String!, $password: String!,$name:String!,$email:String!,$gender: String!) {
5 | signup(username: $username, password: $password, email: $email, name: $name, gender: $gender)
6 | }
7 | `
8 |
9 | export default SIGNUP_MUTATION;
--------------------------------------------------------------------------------
/src/queries/UpdateUser.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const UPDATE_USER_MUTATION = gql `
4 | mutation UpdateUser($name: String, $email: String, $username: String, $password: String) {
5 | updateUser(name: $name, email: $email, username: $username, password: $password) {
6 | name,
7 | username,
8 | email
9 | }
10 | }
11 | `
12 |
13 | export default UPDATE_USER_MUTATION;
--------------------------------------------------------------------------------
/src/queries/UsersExpenses.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const USERS_EXPENSES = gql `
4 | query usersExpenses($category: String, $search: String, $type: String) {
5 | usersExpenses(category: $category, search: $search, type: $type) {
6 | id
7 | spentOn
8 | amount
9 | type
10 | description
11 | category
12 | createdAt
13 | }
14 | }
15 | `;
16 |
17 | export default USERS_EXPENSES;
--------------------------------------------------------------------------------
/src/styles/base/grid.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | @include object(1180px,calc(100% - 40px),null,null);
3 | margin: 0 auto;
4 | }
5 |
6 | .container-fluid {
7 | @include object(100%,null,null,null);
8 | }
9 |
10 | .full-height {
11 | height:100vh;
12 | }
13 |
14 | .row {
15 | display: -ms-grid;
16 | display: grid;
17 | grid-gap: 20px;
18 | margin-bottom: 20px;
19 | }
20 |
21 | .span-8-4 {
22 | -ms-grid-columns: 2fr 1fr;
23 | grid-template-columns: 2fr 1fr;
24 | }
25 |
26 | .span-2-10 {
27 | -ms-grid-columns: 1fr 5fr;
28 | grid-template-columns: 1fr 5fr;
29 | }
30 |
31 | .span-3-9 {
32 | -ms-grid-columns: 1fr 3fr;
33 | grid-template-columns: 1fr 3fr;
34 | }
35 |
36 | .span-4-8 {
37 | -ms-grid-columns: 1fr 2fr;
38 | grid-template-columns: 1fr 2fr;
39 | }
40 |
41 | .span-12 {
42 | -ms-grid-columns: 1fr;
43 | grid-template-columns: 1fr;
44 | }
--------------------------------------------------------------------------------
/src/styles/base/reset.scss:
--------------------------------------------------------------------------------
1 | a {
2 | text-decoration: none;
3 | cursor: pointer;
4 | }
5 |
6 | button, input, select {
7 | cursor: pointer;
8 | &:focus {
9 | outline:none;
10 | }
11 | }
--------------------------------------------------------------------------------
/src/styles/base/typography.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Overpass:300,400,600,700,800,900&display=swap');
--------------------------------------------------------------------------------
/src/styles/components/button.scss:
--------------------------------------------------------------------------------
1 | .btn {
2 | @include object(null,null,40px,null);
3 | border-radius: 4px;
4 | padding: 0 16px;
5 | font-size: 16px;
6 | text-align: center;
7 | line-height: 42px;
8 | color: #FFFFFF;
9 | cursor: pointer;
10 | }
11 |
12 | .btn__primary {
13 | background: #0052CC;
14 | }
15 | .btn__success {
16 | background: #6FCF97;
17 | }
18 |
19 | .btn__outline {
20 | background: #fff;
21 | color: #041A2A;
22 | border: 1px solid #DCDCDC;
23 | }
24 |
25 | .btn__overlay__dark {
26 | background: rgba(255, 255, 255, 0.3);
27 | }
28 |
29 | .btn__small {
30 | height: 32px;
31 | padding: 0 8px;
32 | font-weight: 300;
33 | font-size: 14px;
34 | line-height: 34px;
35 | }
36 |
37 | .btn__icon {
38 | display: -webkit-box;
39 | display: -ms-flexbox;
40 | display: flex;
41 | -webkit-box-align: center;
42 | -ms-flex-align: center;
43 | align-items: center;
44 | i {
45 | margin-right: 8px;
46 | }
47 | span {
48 | -webkit-transform: translateY(2px);
49 | -ms-transform: translateY(2px);
50 | transform: translateY(2px);
51 | }
52 | }
--------------------------------------------------------------------------------
/src/styles/components/modal.scss:
--------------------------------------------------------------------------------
1 | .modal__bg {
2 | background: rgba(#000, 0.3);
3 | height: 100vh;
4 | width: 100vw;
5 | max-width: 100%;
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | z-index: 1000;
10 | @include flex(center,center);
11 | }
12 |
13 | .modal__card {
14 | height: auto;
15 | background: #fff;
16 | border-radius: 4px;
17 | padding: 0 16px;
18 | overflow-y: auto;
19 | header {
20 | height: 48px;
21 | @include flex(center, space-between);
22 | border-bottom: 1px solid #DCDCDC;
23 | i {
24 | -webkit-transform: translateX(4px);
25 | -ms-transform: translateX(4px);
26 | transform: translateX(4px);
27 | cursor: pointer;
28 | }
29 | }
30 | main {
31 | &::-webkit-scrollbar-track {
32 | background-color: #F5F5F5;
33 | }
34 |
35 | &::-webkit-scrollbar {
36 | width: 6px;
37 | background-color: #F5F5F5;
38 | }
39 |
40 | &::-webkit-scrollbar-thumb {
41 | background-color: #1B2C44;
42 | }
43 | }
44 | footer {
45 | height: 56px;
46 | width: 100%;
47 | @include flex(center, null);
48 | border-top: 1px solid #DCDCDC;
49 | }
50 | }
--------------------------------------------------------------------------------
/src/styles/components/radiogroup.scss:
--------------------------------------------------------------------------------
1 | .radio__group__component {
2 | height: 40px;
3 | width: auto;
4 | border-radius: 4px;
5 | border: 1px solid #DCDCDC;
6 | background: #fff;
7 | display: -webkit-box;
8 | display: -ms-flexbox;
9 | display: flex;
10 | -webkit-box-align: center;
11 | -ms-flex-align: center;
12 | align-items: center;
13 | overflow: hidden;
14 | .radio__option {
15 | text-align: center;
16 | position: relative;
17 | &:first-child {
18 | label {
19 | border-left: none;
20 | }
21 | }
22 | label {
23 | height: 40px;
24 | line-height: 42px;
25 | padding: 0 16px;
26 | display: inline-block;
27 | border-left: 1px solid #DCDCDC;
28 | }
29 | input {
30 | position: absolute;
31 | visibility: hidden;
32 | &:checked + label {
33 | background: #0066dc;
34 | border-right: 1px solid #0066dc;
35 | color: #fff;
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/styles/helpers/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin object($max-width,$width, $height, $bg){
2 | max-width:$max-width;
3 | width: $width;
4 | height: $height;
5 | background: $bg;
6 | }
7 |
8 | @mixin flex($ai,$jc){
9 | display:-webkit-box;
10 | display:-ms-flexbox;
11 | display:flex;
12 | -webkit-box-align: $ai;
13 | -ms-flex-align: $ai;
14 | align-items: $ai;
15 | -webkit-box-pack: $jc;
16 | -ms-flex-pack: $jc;
17 | justify-content: $jc;
18 | }
--------------------------------------------------------------------------------
/src/styles/helpers/variables.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --font: 'Overpass', sans-serif;
3 |
4 | --vertical-grid-size: 8;
5 |
6 | --margin-1: calc(var(--vertical-grid-size) * 1px);
7 | --margin-2: calc(var(--vertical-grid-size) * 2px);
8 | --margin-3: calc(var(--vertical-grid-size) * 3px);
9 | --margin-4: calc(var(--vertical-grid-size) * 4px);
10 | --margin-5: calc(var(--vertical-grid-size) * 5px);
11 | --margin-6: calc(var(--vertical-grid-size) * 6px);
12 |
13 | --padding-1: calc(var(--vertical-grid-size) * 1px);
14 | --padding-2: calc(var(--vertical-grid-size) * 2px);
15 | --padding-3: calc(var(--vertical-grid-size) * 3px);
16 | --padding-4: calc(var(--vertical-grid-size) * 4px);
17 | }
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import './helpers/variables.scss';
2 | @import './helpers/mixins.scss';
3 | @import './base/typography.scss';
4 | @import './base/reset.scss';
5 | @import './base/grid.scss';
6 | @import './components/button.scss';
7 | @import './components/radiogroup.scss';
8 | @import './components/modal.scss';
9 | @import './layout/navbar.scss';
10 | @import './layout/main.scss';
11 | @import './layout/form.scss';
12 | @import './layout/tabs.scss';
13 | @import './layout/select.scss';
14 | @import './layout/addexpense.scss';
15 | @import './layout/account.scss';
16 | @import './layout/insights.scss';
17 |
18 | * {
19 | margin: 0;
20 | padding: 0;
21 | -webkit-box-sizing: border-box;
22 | box-sizing: border-box;
23 | -webkit-font-smoothing: antialiased;
24 | -moz-osx-font-smoothing: grayscale;
25 | font-family: var(--font);
26 | }
27 |
28 | body {
29 | background: #F9F9F9;
30 | }
31 |
32 | #home {
33 | background-image: url('https://res.cloudinary.com/prvnbist/image/upload/v1561219822/Expense%20App/expense_bg.svg');
34 | background-position: bottom;
35 | background-repeat: no-repeat;
36 | background-size: contain;
37 | height: 100vh;
38 | width: 100vw;
39 | overflow: hidden;
40 | }
41 |
42 | #logo {
43 | font-weight: 400;
44 | line-height: normal;
45 | font-size: 18px;
46 |
47 | a {
48 | color: #FFFFFF;
49 | @include flex(center, null);
50 | }
51 |
52 | img {
53 | width: 30px;
54 | margin-right: 20px;
55 | }
56 | }
57 |
58 | .error-message {
59 | padding-top: 8px;
60 | color: red;
61 | display: block;
62 | }
63 |
64 | @media screen and (max-width:576px) {
65 | .login {
66 | height: calc(100vh - 149px);
67 | }
68 | #home {
69 | padding-bottom: 16px;
70 | overflow-y: auto;
71 | overflow-x: hidden;
72 | }
73 | #logo {
74 | a {
75 | font-size: 16px;
76 | }
77 | }
78 | .top-nav #logo {
79 | width: 30px;
80 | color: transparent;
81 | }
82 | .expense-card {
83 | height: auto;
84 | }
85 | .mid-row {
86 | -webkit-box-orient: vertical;
87 | -webkit-box-direction: normal;
88 | -ms-flex-direction: column;
89 | flex-direction: column;
90 | .category,
91 | .time {
92 | margin-bottom: 16px;
93 | }
94 | }
95 | .top-row {
96 | .spentOn {
97 | font-size: 18px;
98 | }
99 | .amount {
100 | font-size: 16px;
101 | }
102 | .deleteExpense {
103 | i {
104 | font-size: 20px;
105 | }
106 | }
107 | }
108 | #expense-popup-card {
109 | height: 100%;
110 | overflow-y: scroll;
111 | div:nth-child(2) {
112 | -ms-grid-columns: 1fr;
113 | grid-template-columns: 1fr;
114 | }
115 | }
116 | }
117 |
118 | .page-heading {
119 | font-style: normal;
120 | font-weight: 600;
121 | font-size: 24px;
122 | line-height: 37px;
123 | display: -webkit-box;
124 | display: -ms-flexbox;
125 | display: flex;
126 | -webkit-box-align: center;
127 | -ms-flex-align: center;
128 | align-items: center;
129 | color: #1B2C44;
130 | margin-bottom: 24px;
131 | }
--------------------------------------------------------------------------------
/src/styles/layout/account.scss:
--------------------------------------------------------------------------------
1 | .account__modal {
2 | main {
3 | margin-bottom: 16px;
4 | .field {
5 | height: 40px;
6 | width: 320px;
7 | label {
8 | width: 40px;
9 | }
10 | input {
11 | padding-left: 14px;
12 | font-size: 14px;
13 | height: inherit;
14 | }
15 | }
16 | }
17 | .container {
18 | width:100%;
19 | }
20 | }
--------------------------------------------------------------------------------
/src/styles/layout/addexpense.scss:
--------------------------------------------------------------------------------
1 | .addexpense__row {
2 | display: -ms-grid;
3 | display: grid;
4 | grid-auto-flow: column;
5 | grid-gap: 16px;
6 | margin-bottom: 16px;
7 | width: 360px;
8 | &:first-child {
9 | margin-top: 16px;
10 | }
11 | &:last-child {
12 | margin-bottom: 16px;
13 | }
14 | }
15 |
16 | .addexpense__column {
17 | display: -webkit-box;
18 | display: -ms-flexbox;
19 | display: flex;
20 | -webkit-box-orient: vertical;
21 | -webkit-box-direction: normal;
22 | -ms-flex-direction: column;
23 | flex-direction: column;
24 | width:100%;
25 | label {
26 | font-size: 14px;
27 | line-height: 25px;
28 | font-weight: 600;
29 | color: #1B2C44;
30 | }
31 | input {
32 | height: 32px;
33 | padding-left: 8px;
34 | border: 1px solid #DCDCDC;
35 | border-radius: 4px;
36 | }
37 | select {
38 | height: 32px;
39 | padding-left: 8px;
40 | border: 1px solid #DCDCDC;
41 | border-radius: 4px;
42 | }
43 | textarea {
44 | min-height: 60px;
45 | padding-top: 8px;
46 | padding-left: 8px;
47 | border: 1px solid #DCDCDC;
48 | border-radius: 4px;
49 | }
50 | }
51 |
52 |
53 | @media screen and (max-width: 567px) {
54 | .modal__card {
55 | height: 100%;
56 | width: 100%;
57 | .addexpense__row {
58 | width: 100%;
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/src/styles/layout/form.scss:
--------------------------------------------------------------------------------
1 | @import '../helpers/mixins.scss';
2 |
3 | input,select,button {
4 | height:48px;
5 | border:none;
6 | background: transparent;
7 | &:focus + label {
8 | border-right: 1px solid #0066dc;
9 | i {
10 | color: #0066dc;
11 | }
12 | }
13 | }
14 |
15 | .login,
16 | .signup {
17 | .btn {
18 | margin-top: 16px;
19 | }
20 | }
21 |
22 | .field {
23 | @include object(320px,100%,48px,#FFFFFF);
24 | border-radius: 4px;
25 | margin-top: 16px;
26 | display: -webkit-box;
27 | display: -ms-flexbox;
28 | display: flex;
29 | -webkit-box-align: center;
30 | -ms-flex-align: center;
31 | align-items: center;
32 | -webkit-box-orient: horizontal;
33 | -webkit-box-direction: reverse;
34 | -ms-flex-direction: row-reverse;
35 | flex-direction: row-reverse;
36 | border: 1px solid #DCDCDC;
37 |
38 | &:focus-within {
39 | border: 1px solid #0066dc;
40 | }
41 | label {
42 | @include object(null,56px,32px,null);
43 | @include flex(center,center);
44 | border-right: 1px solid #a9a0a087;
45 | i {
46 | color: #A9A0A0;
47 | font-size: 21px;
48 | }
49 | }
50 | input {
51 | padding-left: 20px;
52 | font-weight: 400;
53 | font-size: 16px;
54 | color: #0066DC;
55 | width:100%;
56 | &::-webkit-input-placeholder {
57 | color: #A9A0A0;
58 | }
59 | &:-ms-input-placeholder {
60 | color: #A9A0A0;
61 | }
62 | &::-ms-input-placeholder {
63 | color: #A9A0A0;
64 | }
65 | &::placeholder {
66 | color: #A9A0A0;
67 | }
68 | }
69 | select {
70 | padding: 0 20px;
71 | width: 100%;
72 | -webkit-appearance: none;
73 | background: transparent;
74 | background-image: url("data:image/svg+xml;utf8,");
75 | background-repeat: no-repeat;
76 | background-position-x: 92%;
77 | background-position-y: 12px;
78 | font-weight: 400;
79 | line-height: normal;
80 | font-size: 16px;
81 | color: #0066DC;
82 | }
83 | }
84 |
85 | .select-field {
86 | width:150px;
87 | }
88 |
89 | @media screen and (max-width:567px) {
90 | .form-center {
91 | width: 100%;
92 | padding: 0 20px;
93 | }
94 | }
--------------------------------------------------------------------------------
/src/styles/layout/insights.scss:
--------------------------------------------------------------------------------
1 | #top__row {
2 | display: -ms-grid;
3 | display: grid;
4 | -ms-grid-columns: 1fr 16px 1fr;
5 | grid-template-columns: 1fr 1fr;
6 | grid-gap: 16px;
7 | grid-auto-rows: 200px;
8 | width: 100%;
9 | }
10 |
11 | .stats__card {
12 | border-radius: 4px;
13 | &:nth-child(1) {
14 | background: -webkit-linear-gradient(317.82deg, #6A11CB 0%, #2575FC 100%);
15 | background: -o-linear-gradient(317.82deg, #6A11CB 0%, #2575FC 100%);
16 | background: linear-gradient(132.18deg, #6A11CB 0%, #2575FC 100%);
17 | }
18 | &:nth-child(2) {
19 | background: -webkit-linear-gradient(317.82deg, #C471F5 0%, #FA71CD 100%);
20 | background: -o-linear-gradient(317.82deg, #C471F5 0%, #FA71CD 100%);
21 | background: linear-gradient(132.18deg, #C471F5 0%, #FA71CD 100%);
22 | }
23 | &:nth-child(3) {
24 | background: -webkit-linear-gradient(317.82deg, #4FACFE 0%, #00F2FE 100%);
25 | background: -o-linear-gradient(317.82deg, #4FACFE 0%, #00F2FE 100%);
26 | background: linear-gradient(132.18deg, #4FACFE 0%, #00F2FE 100%);
27 | }
28 | header {
29 | height: 48px;
30 | line-height: 50px;
31 | border-bottom: 1px solid rgba(255, 255, 255, 0.3);
32 | padding: 0 16px;
33 | color: #fff;
34 | text-transform: uppercase;
35 | }
36 | main {
37 | padding: 0 16px;
38 | height: calc(100% - 48px);
39 | color: #fff;
40 | display: -webkit-box;
41 | display: -ms-flexbox;
42 | display: flex;
43 | -webkit-box-align: start;
44 | -ms-flex-align: start;
45 | align-items: flex-start;
46 | -webkit-box-pack: center;
47 | -ms-flex-pack: center;
48 | justify-content: center;
49 | -webkit-box-orient: vertical;
50 | -webkit-box-direction: normal;
51 | -ms-flex-direction: column;
52 | flex-direction: column;
53 | .stats__number {
54 | font-size: 48px;
55 | font-weight: bold;
56 | }
57 | }
58 | }
59 |
60 | #middle__row {
61 | display: -ms-grid;
62 | display: grid;
63 | -ms-grid-columns: 2fr 16px 1fr;
64 | grid-template-columns: 2fr 1fr;
65 | grid-gap: 16px;
66 | width: 100%;
67 | margin-top: 16px;
68 | }
69 |
70 | #most__spenton, #monthly__stats {
71 | border-radius: 4px;
72 | border: 1px solid #DCDCDC;
73 | background: #fff;
74 | header {
75 | height: 48px;
76 | line-height: 50px;
77 | border-bottom: 1px solid #DCDCDC;
78 | padding: 0 16px;
79 | color: #9D8C8C;
80 | text-transform: uppercase;
81 | }
82 | main {
83 | padding: 16px;
84 | min-height: calc(100% - 48px);
85 | color: #fff;
86 | }
87 | }
88 |
89 | #monthly__stats {
90 | canvas {
91 | width:100% !important;
92 | height: 100% !important;
93 | }
94 | }
95 |
96 | @media screen and (max-width: 767px) {
97 | #top__row {
98 | -ms-grid-columns: 1fr;
99 | grid-template-columns: 1fr;
100 | }
101 | #middle__row {
102 | -ms-grid-columns: 1fr;
103 | grid-template-columns: 1fr;
104 | margin-bottom: 24px;
105 | canvas {
106 | width: 100% !important;
107 | height: 350px !important;
108 | }
109 | }
110 | }
--------------------------------------------------------------------------------
/src/styles/layout/main.scss:
--------------------------------------------------------------------------------
1 | @import '../helpers/mixins.scss';
2 |
3 | main {
4 | img {
5 | width:5%;
6 | }
7 | }
8 |
9 | .expense__options {
10 | width:100%;
11 | margin-bottom: 16px;
12 | display: -webkit-box;
13 | display: -ms-flexbox;
14 | display: flex;
15 | -webkit-box-pack: justify;
16 | -ms-flex-pack: justify;
17 | justify-content: space-between;
18 | .field {
19 | width: 260px;
20 | height: 40px;
21 | margin-right: 24px;
22 | margin-top: 0;
23 | label {
24 | width: 40px;
25 | height: 24px;
26 | }
27 | input {
28 | padding-left: 12px;
29 | -webkit-transform: translateY(2px);
30 | -ms-transform: translateY(2px);
31 | transform: translateY(2px);
32 | }
33 | }
34 | .select__component {
35 | width: 260px;
36 | margin-right: 24px;
37 | }
38 | .btn__icon {
39 | margin-top: 0;
40 | height: 40px;
41 | padding: 0 12px;
42 | span {
43 | -webkit-transform: translateY(1px);
44 | -ms-transform: translateY(1px);
45 | transform: translateY(1px);
46 | }
47 | }
48 | }
49 |
50 | .expenses-list {
51 | width: 100%;
52 | display: -ms-grid;
53 | display: grid;
54 | -ms-grid-columns: 1fr 16px 1fr;
55 | grid-template-columns: 1fr 1fr;
56 | grid-gap:16px;
57 | padding-bottom: 16px;
58 | }
59 |
60 | .expense-card {
61 | background: #fff;
62 | min-height:232px;
63 | border: 1px solid rgba(212, 212, 212, 0.6);
64 | border-radius: 4px;
65 | padding-bottom: 20px;
66 | }
67 |
68 | .top-row {
69 | @include flex(center, space-between);
70 | margin-bottom: 20px;
71 | height: 56px;
72 | padding: 0 20px;
73 | border-bottom: 1px solid rgba(212, 212, 212, 0.6);
74 | .spentOn {
75 | font-style: normal;
76 | font-weight: 450;
77 | line-height: normal;
78 | font-size: 24px;
79 | color: #041A2A;
80 | }
81 | .amount {
82 | font-style: normal;
83 | font-weight: 450;
84 | line-height: normal;
85 | font-size: 20px;
86 | text-align: right;
87 | }
88 | }
89 |
90 | .mid-row {
91 | display:-webkit-box;
92 | display:-ms-flexbox;
93 | display:flex;
94 | -ms-flex-wrap:wrap;
95 | flex-wrap:wrap;
96 | margin-bottom: 8px;
97 | padding: 0 20px;
98 | .category,
99 | .time,
100 | .date {
101 | font-style: normal;
102 | font-weight: 400;
103 | line-height: normal;
104 | font-size: 16px;
105 | color: #041A2A;
106 | opacity: 0.4;
107 | margin-right: 20px;
108 | margin-bottom: 16px;
109 | @include flex(center,null);
110 | i{
111 | padding-right: 15px;
112 | font-size:21px;
113 | }
114 | span {
115 | -webkit-transform: translateY(2px);
116 | -ms-transform: translateY(2px);
117 | transform: translateY(2px);
118 | }
119 | }
120 | }
121 |
122 | .description {
123 | font-style: normal;
124 | font-weight: 400;
125 | line-height: normal;
126 | font-size: 16px;
127 | color: #041A2A;
128 | opacity: 0.4;
129 | margin-right: 40px;
130 | padding: 0 20px;
131 | @include flex(center,null);
132 | i{
133 | padding-right: 15px;
134 | font-size: 21px;
135 | }
136 | span {
137 | -webkit-transform: translateY(2px);
138 | -ms-transform: translateY(2px);
139 | transform: translateY(2px);
140 | }
141 | }
142 |
143 | .expense__action {
144 | margin-left: 16px;
145 | height: 32px;
146 | width: 32px;
147 | border: 1px solid #DCDCDC;
148 | border-radius: 4px;
149 | @include flex(center,center);
150 | -webkit-transition: 0.3s ease-in-out;
151 | -o-transition: 0.3s ease-in-out;
152 | transition: 0.3s ease-in-out;
153 | &:hover {
154 | background: #EC1A1A;
155 | i {
156 | color:#fff;
157 | opacity: 1;
158 | }
159 | }
160 | i {
161 | font-style: normal;
162 | font-weight: 400;
163 | line-height: normal;
164 | font-size: 21px;
165 | color: #041A2A;
166 | opacity: 0.4;
167 | }
168 | }
169 |
170 | @media screen and (max-width:567px) {
171 | .expense__options {
172 | &>div {
173 | .field, .select__component, .radio__group__component {
174 | max-width:100%;
175 | width: 100%;
176 | margin-right: 0;
177 | }
178 | .radio__group__component {
179 | display: -ms-grid;
180 | display: grid;
181 | grid-auto-flow: column;
182 | -ms-grid-columns: 1fr 1fr;
183 | grid-template-columns: 1fr 1fr;
184 | .radio__option {
185 | label {
186 | padding: 0;
187 | width: 100%;
188 | }
189 | }
190 | }
191 | }
192 | }
193 | }
194 | @media screen and (max-width:768px) {
195 | .expenses-list {
196 | -ms-grid-columns: 1fr;
197 | grid-template-columns: 1fr;
198 | }
199 | }
200 |
201 | @media screen and (max-width:980px) {
202 | .expense__options {
203 | -ms-flex-wrap: wrap;
204 | flex-wrap: wrap;
205 | &>div {
206 | -ms-flex-wrap: wrap;
207 | flex-wrap: wrap;
208 | .field, .select__component, .radio__group__component {
209 | margin-bottom: 16px;
210 | }
211 | }
212 | }
213 | }
--------------------------------------------------------------------------------
/src/styles/layout/navbar.scss:
--------------------------------------------------------------------------------
1 | @import '../helpers/mixins.scss';
2 |
3 | nav {
4 | height:60px;
5 | background: #0052CC;
6 | position: relative;
7 | & > .container {
8 | height:60px;
9 | position: relative;
10 | z-index: 10;
11 | @include flex(center,space-between);
12 | #logo {
13 | a {
14 | @include flex(center,null);
15 | img {
16 | width:30px;
17 | margin-right: 20px;
18 | }
19 | }
20 | }
21 | }
22 | #user-info-actions {
23 | display: -webkit-box;
24 | display: -ms-flexbox;
25 | display: flex;
26 | }
27 | .user__image {
28 | cursor: pointer;
29 | height: 40px;
30 | width: 40px;
31 | margin-right: 16px;
32 | border-radius: 50%;
33 | background: rgba(255,255,255,0.3);
34 | @include flex(center,center);
35 | color: #fff;
36 | }
37 | }
38 |
39 | @media screen and (max-width: 567px) {
40 | nav > .container #logo a {
41 | color: #0052CC;
42 | }
43 | }
--------------------------------------------------------------------------------
/src/styles/layout/select.scss:
--------------------------------------------------------------------------------
1 |
2 | .select__component {
3 | width: 240px;
4 | height: 40px;
5 | padding: 0 16px;
6 | border-radius: 6px;
7 | background: #fff;
8 | border: 1px solid rgb(243, 220, 220);
9 | position: relative;
10 | cursor: pointer;
11 | .selected-option {
12 | height: inherit;
13 | display: -webkit-box;
14 | display: -ms-flexbox;
15 | display: flex;
16 | -webkit-box-align: center;
17 | -ms-flex-align: center;
18 | align-items: center;
19 | -webkit-box-pack: justify;
20 | -ms-flex-pack: justify;
21 | justify-content: space-between;
22 | span {
23 | font-size: 16px;
24 | }
25 | i {
26 | -webkit-transform: translateX(43%);
27 | -ms-transform: translateX(43%);
28 | transform: translateX(43%);
29 | }
30 | }
31 |
32 | .options {
33 | z-index: 1000;
34 | position: absolute;
35 | background: #fff;
36 | top: 44px;
37 | left: 0;
38 | width: 260px;
39 | max-height: 249px;
40 | overflow-y: scroll;
41 | padding: 0 8px 8px;
42 | border-radius: 6px;
43 | border: 1px solid rgb(243, 220, 220);
44 | li {
45 | height: 32px;
46 | font-size: 14px;
47 | list-style: none;
48 | line-height: 36px;
49 | padding: 0 10px;
50 | cursor: pointer;
51 | border-radius: 6px;
52 | color: rgb(175, 161, 161);
53 | -webkit-transition: 0.3s linear;
54 | -o-transition: 0.3s linear;
55 | transition: 0.3s linear;
56 | &:hover {
57 | color: #000;
58 | background: rgba(10, 39, 206, 0.082);
59 | }
60 | &.active-option {
61 | color: #000;
62 | background: rgba(10, 39, 206, 0.082);
63 | }
64 | }
65 | }
66 |
67 | .search-options {
68 | padding-top: 8px;
69 | background: #fff;
70 | height: 40px;
71 | width: 208px;
72 | }
73 | input {
74 | width: calc(260px - 32px);
75 | height: 32px;
76 | font-size: 14px;
77 | line-height: 32px;
78 | padding: 0 10px;
79 | margin-bottom: 8px;
80 | border-radius: 6px;
81 | border: 1px solid rgb(243, 220, 220);
82 | &::-webkit-input-placeholder {
83 | color: rgb(175, 161, 161);
84 | }
85 | &:-ms-input-placeholder {
86 | color: rgb(175, 161, 161);
87 | }
88 | &::-ms-input-placeholder {
89 | color: rgb(175, 161, 161);
90 | }
91 | &::placeholder {
92 | color: rgb(175, 161, 161);
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/src/styles/layout/tabs.scss:
--------------------------------------------------------------------------------
1 | .tab-list {
2 | height: auto;
3 | line-height: 48px;
4 | width: 100%;
5 | white-space: nowrap;
6 | overflow-x: auto;
7 | overflow-y: hidden;
8 | }
9 |
10 | .tab-list-item {
11 | display: inline-block;
12 | list-style: none;
13 | font-style: normal;
14 | font-weight: 500;
15 | font-size: 16px;
16 | margin-right: 16px;
17 | position: relative;
18 | color: #A9A0A0;
19 | cursor: pointer;
20 | &.tab-list-active {
21 | color: #1B2C44;
22 | }
23 | &.tab-list-active::before {
24 | content: '';
25 | position: absolute;
26 | width: 100%;
27 | height: 3px;
28 | background: #2158E8;
29 | left: 0;
30 | bottom: 0;
31 | border-top-left-radius: 4px;
32 | border-top-right-radius: 4px;
33 | }
34 | }
35 |
36 | .tab-content {
37 | display: -webkit-box;
38 | display: -ms-flexbox;
39 | display: flex;
40 | -ms-flex-wrap: wrap;
41 | flex-wrap: wrap;
42 | margin-top: 24px;
43 | }
--------------------------------------------------------------------------------