diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..21e6351 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/*.scss diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5cbabe4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.lock +*.log diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..1ff0726 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + plugins: [["babel-plugin-transform-remove-imports", { test: "(?:dhtmlx)" }]], + presets: ["@babel/preset-env", "@babel/preset-react"] +}; diff --git a/enzyme.config.js b/enzyme.config.js new file mode 100644 index 0000000..80076ef --- /dev/null +++ b/enzyme.config.js @@ -0,0 +1,5 @@ +/** Used in jest.config.js */ +import { configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +configure({ adapter: new Adapter() }); \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..7ed7b32 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,42 @@ +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: ["src/**/*.{js,jsx,mjs}"], + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of file extensions your modules use + moduleFileExtensions: ["js", "json", "jsx"], + + // The paths to modules that run some code to configure or set up the testing environment before each test + setupFiles: ["/enzyme.config.js"], + + // The test environment that will be used for testing + testEnvironment: "jsdom", + + // The glob patterns Jest uses to detect test files + testMatch: ["**/__tests__/**/*.js?(x)", "**/?(*.)+(spec|test).js?(x)"], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + testPathIgnorePatterns: ["\\\\node_modules\\\\"], + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + testURL: "http://localhost", + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + transformIgnorePatterns: ["/node_modules/"], + + // Indicates whether each individual test should be reported during the run + verbose: false, + + moduleNameMapper: { + "^.+\\.(css|less|scss)$": "babel-jest", + "^~/(.*)$": "/src/$1" + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..2459033 --- /dev/null +++ b/package.json @@ -0,0 +1,71 @@ +{ + "name": "remotelock", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "webpack serve", + "build": "webpack --mode production", + "test": "jest", + "test:watch": "jest --watchAll", + "test:coverage": "jest --coverage --colors" + }, + "dependencies": { + "autoprefixer": "^10.3.1", + "axios": "^0.21.1", + "bootstrap": "^5.0.2", + "bootstrap-scss": "^5.0.2", + "classnames": "^2.2.5", + "dayjs": "^1.10.6", + "react": "^17.0.2", + "react-bootstrap": "^2.0.0-beta.4", + "react-dom": "^17.0.2", + "react-feather": "^2.0.9", + "react-redux": "7.1.0", + "redux": "4.0.4", + "redux-form": "8.2.6", + "redux-mock-store": "^1.5.4", + "redux-thunk": "^2.3.0" + }, + "devDependencies": { + "@babel/core": "^7.0.0", + "@babel/polyfill": "^7.0.0-beta.51", + "@babel/preset-env": "^7.0.0-beta.51", + "@babel/preset-react": "^7.0.0-beta.51", + "babel-core": "^7.0.0-bridge.0", + "babel-jest": "^23.4.2", + "babel-loader": "^8.2.2", + "babel-plugin-transform-remove-imports": "^1.5.2", + "chai": "^4.1.2", + "cross-env": "^6.0.3", + "css-loader": "^2.1.0", + "deep-freeze": "^0.0.1", + "enzyme": "^3.3.0", + "enzyme-adapter-react-16": "^1.1.1", + "html-webpack-plugin": "^5.3.1", + "husky": "^1.3.1", + "jest": "^23.4.2", + "jsdom": "^11.12.0", + "lint-staged": "^10.0.8", + "node-sass": "4.14.0", + "pretty-quick": "^2.0.1", + "sass-loader": "7.1.0", + "sinon": "^6.1.5", + "style-loader": "^0.23.1", + "url-loader": "^0.5.7", + "webpack": "^5.38.1", + "webpack-cli": "^4.7.2", + "webpack-dev-server": "^3.11.2" + }, + "author": "Programmers.io", + "license": "MIT", + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.{js,ts,tsx,scss}": [ + "pretty-quick --staged" + ] + } +} diff --git a/src/__test__/app.test.js b/src/__test__/app.test.js new file mode 100644 index 0000000..a2b3619 --- /dev/null +++ b/src/__test__/app.test.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { expect } from 'chai'; +import App from '../App'; + +describe('src/app.test.js', () => { + describe('', () => { + it('should render tabs properly', () => { + const wrapper = shallow(); + expect(wrapper.find('Tabs')).to.have.length(1); + }); + }); +}); \ No newline at end of file diff --git a/src/_fonts.scss b/src/_fonts.scss new file mode 100644 index 0000000..747fbf5 --- /dev/null +++ b/src/_fonts.scss @@ -0,0 +1,63 @@ + +@mixin font-open-sans-light { + font: { + family: 'Open Sans', sans-serif; + weight: 300; + } +} + +@mixin font-open-sans-regular { + font: { + family: 'Open Sans', sans-serif; + weight: 400; + } +} + +@mixin font-open-sans-semibold { + font: { + family: 'Open Sans', sans-serif; + weight: 600; + } +} + +@mixin base-font { + @include font-open-sans-regular; + font-size: 13px; + color: $var-colors-grey-ultradark; +} + +@mixin heading-1 { + @include font-open-sans-light; + font-size: 24px; + color: $var-font-color-1; +} + +@mixin heading-2 { + @include font-open-sans-regular; + font-size: 20px; + color: $var-font-color-1; +} + +@mixin heading-3 { + @include font-open-sans-regular; + font-size: 18px; + color: $var-colors-grey-ultradark; +} + +@mixin heading-4 { + @include font-open-sans-semibold; + font-size: 16px; + color: $var-colors-grey-ultradark; +} + +@mixin heading-5 { + @include font-open-sans-regular; + font-size: 15px; + color: $var-colors-grey-ultradark; +} + +@mixin heading-6 { + @include font-open-sans-semibold; + font-size: 13px; + color: $var-colors-grey-ultradark; +} \ No newline at end of file diff --git a/src/_mixins.scss b/src/_mixins.scss new file mode 100644 index 0000000..b7a302f --- /dev/null +++ b/src/_mixins.scss @@ -0,0 +1,10 @@ +@mixin ellipsis() { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +@mixin link-text { + color: $var-colors-blue-primary; + text-decoration: none; +} \ No newline at end of file diff --git a/src/_variables.scss b/src/_variables.scss new file mode 100644 index 0000000..e99479d --- /dev/null +++ b/src/_variables.scss @@ -0,0 +1,56 @@ +$var-colors-red: #ff5555; +$var-colors-white-smoke: #eff0f0; +$var-colors-grey-light: #d7d7d7; +$var-colors-grey-med: #b7b7b7; +$var-colors-grey-med-dark: #808080; +$var-colors-grey-dark: #555; +$var-colors-grey-ultradark: #333333; +$var-colors-blue-primary: #015595; + +$colors-error: #ec3d3d; +$colors-error-light: #f4e9e9; +$colors-info: #00acd7; +$colors-info-light: #e8f0f4; +$colors-warning-light: #f7ebc5; + +$border-radius: 4px; + +$box-shadow-low: 0px 0px 1px rgba(0, 0, 0, 0.25), 0px 1px 2px rgba(0, 0, 0, 0.15), + 0px 1px 4px rgba(0, 0, 0, 0.1); +$inset-box-shadow-low: inset 0px 0px 1px rgba(0, 0, 0, 0.25), inset 0px 1px 2px rgba(0, 0, 0, 0.15), + inset 0px 1px 4px rgba(0, 0, 0, 0.1); + +$box-shadow-medium-low: 0px 0px 1px rgba(0, 0, 0, 0.25), 0px 2px 4px rgba(0, 0, 0, 0.15), + 0px 2px 8px rgba(0, 0, 0, 0.1); + +$box-shadow-medium: 0px 0px 1px rgba(0, 0, 0, 0.25), 0px 4px 8px rgba(0, 0, 0, 0.15), + 0px 4px 16px rgba(0, 0, 0, 0.1); +$inset-box-shadow-medium: inset 0px 0px 1px rgba(0, 0, 0, 0.25), + inset 0px 4px 8px rgba(0, 0, 0, 0.15), inset 0px 4px 16px rgba(0, 0, 0, 0.1); + +$box-shadow-high: 0px 0px 1px rgba(0, 0, 0, 0.25), 0px 8px 16px rgba(0, 0, 0, 0.15), + 0px 8px 32px rgba(0, 0, 0, 0.1); + +$var-font-color-1: $var-colors-blue-primary; +$var-text-light: #6c6d6d; + +// Breakpoints +$breakpoint-hand: 576px; +$breakpoint-lap: 768px; +$breakpoint-desk: 1024px; + +// Avatar sizes +$avatar-size: 2.3rem; + +// Export Colors +// By exporting these, we make the values +// available for use in components +:export { + varColorsRed: $var-colors-red; + varColorsWhiteSmoke: $var-colors-white-smoke; + varColorsGreyLight: $var-colors-grey-light; + varColorsGreyMed: $var-colors-grey-med; + varColorsGreyMedDark: $var-colors-grey-med-dark; + varColorsGreyDark: $var-colors-grey-dark; + varColorsGreyUltradark: $var-colors-grey-ultradark; +} diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..1bda784 --- /dev/null +++ b/src/app.js @@ -0,0 +1,31 @@ +import React from "react"; +import { Container, Tabs, Tab } from "react-bootstrap"; +import { Devices, Users } from "~/components"; +import "bootstrap/dist/css/bootstrap.min.css"; +import "~/theme.scss"; +import "~/global.scss"; +import "~/app.scss"; + +const App = () => { + return ( +
+ + + + + + + + + + +
+ ); +}; + +export default App; diff --git a/src/app.scss b/src/app.scss new file mode 100644 index 0000000..b514b64 --- /dev/null +++ b/src/app.scss @@ -0,0 +1,31 @@ +.page-tabs { + background-color: $var-colors-grey-med; + border-radius: 10px; + padding: 5px; + + .nav-item { + @media screen and (max-width: $breakpoint-lap) { + flex: 1; + } + + .nav-link { + color: #000; + font-weight: 700; + @media screen and (max-width: $breakpoint-lap) { + width: 100%; + } + + &.active { + background-color: white; + color: #000; + } + } + } + + .show { + > .nav-linke { + background-color: white; + color: #000; + } + } +} diff --git a/src/bootstrap.js b/src/bootstrap.js new file mode 100644 index 0000000..9c8bff3 --- /dev/null +++ b/src/bootstrap.js @@ -0,0 +1,15 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { Provider } from "react-redux"; +import store from "~/store"; + +import App from "./App"; + +ReactDOM.render( + + + + + , + document.getElementById("root") +); diff --git a/src/components/deviceCard/DeviceCard.js b/src/components/deviceCard/DeviceCard.js new file mode 100644 index 0000000..16d37a1 --- /dev/null +++ b/src/components/deviceCard/DeviceCard.js @@ -0,0 +1,26 @@ +import React from "react"; +import { Card } from "react-bootstrap"; +import { ToggleStatus } from "~/components"; +import "./DeviceCard.scss"; + +const DeviceCard = props => { + const { id, attributes: { name, model_number, state } = {} } = props.info; + return ( + + +
+
 
+
+

{name}

+

{model_number}

+
+ +
+
+
+
+
+ ); +}; + +export default DeviceCard; diff --git a/src/components/deviceCard/DeviceCard.scss b/src/components/deviceCard/DeviceCard.scss new file mode 100644 index 0000000..6e5b1a6 --- /dev/null +++ b/src/components/deviceCard/DeviceCard.scss @@ -0,0 +1,38 @@ +.card { + margin: 10px 0; + border: 0; + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.13); + width: 100%; + + .icon { + background: $var-colors-grey-light; + width: 80px; + height: 80px; + border-radius: 50%; + border: 1px solid $var-colors-grey-med; + } + + .date { + font-weight: 700; + } + + .card-title { + &.h4 { + font-weight: 700; + } + } + + .card-text { + color: darkgray; + } + + .status { + text-align: right; + font-size: 20px; + margin-top: auto; + + span{ + min-width: 100px; + } + } +} diff --git a/src/components/deviceCard/__test__/DeviceCard.test.js b/src/components/deviceCard/__test__/DeviceCard.test.js new file mode 100644 index 0000000..2791443 --- /dev/null +++ b/src/components/deviceCard/__test__/DeviceCard.test.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { expect } from 'chai'; +import DeviceCard from '../DeviceCard'; + +describe('src/components/deviceCard/__test__/DeviceCard.test.js', () => { + describe('', () => { + it('should render tabs properly', () => { + const wrapper = shallow(); + expect(wrapper.find('Card')).to.have.length(1); + }); + }); +}); \ No newline at end of file diff --git a/src/components/devices/Devices.js b/src/components/devices/Devices.js new file mode 100644 index 0000000..065d1fe --- /dev/null +++ b/src/components/devices/Devices.js @@ -0,0 +1,26 @@ +import React, { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { fetchDevices } from "~/store/actions/deviceActions"; +import { DeviceCard } from "~/components"; + +const Devices = () => { + const dispatch = useDispatch(); + const { data } = useSelector(state => state.devicesInfo); + + useEffect(() => { + dispatch(fetchDevices()); + }, [dispatch]); + + return ( +
+ {data && + data.map(d => ( +
+ +
+ ))} +
+ ); +}; + +export default Devices; diff --git a/src/components/devices/__test__/Devices.test.js b/src/components/devices/__test__/Devices.test.js new file mode 100644 index 0000000..5c6b137 --- /dev/null +++ b/src/components/devices/__test__/Devices.test.js @@ -0,0 +1,27 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { expect } from "chai"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import Devices from "../Devices"; + +describe("src/components/devices/__test__/Devices.test.js", () => { + describe("", () => { + const initialState = {}; + const mockStore = configureStore(); + let store, wrapper; + + beforeEach(() => { + store = mockStore(initialState); + wrapper = shallow( + + + + ); + }); + + it("should render tabs properly", () => { + expect(wrapper.find("Devices")).to.have.length(1); + }); + }); +}); diff --git a/src/components/index.js b/src/components/index.js new file mode 100644 index 0000000..b616825 --- /dev/null +++ b/src/components/index.js @@ -0,0 +1,5 @@ +export { default as DeviceCard } from "./deviceCard/DeviceCard"; +export { default as Devices } from "./devices/Devices"; +export { default as ToggleStatus } from "./toggleStatus/ToggleStatus"; +export { default as UserCard } from "./userCard/UserCard"; +export { default as Users } from "./users/Users"; diff --git a/src/components/toggleStatus/ToggleStatus.js b/src/components/toggleStatus/ToggleStatus.js new file mode 100644 index 0000000..89fa6bc --- /dev/null +++ b/src/components/toggleStatus/ToggleStatus.js @@ -0,0 +1,37 @@ +import React, { useState } from "react"; +import { Lock, Unlock } from "react-feather"; +import "./ToggleStatus.scss"; + +const ToggleCheckBox = ({ id, state }) => { + const boolState = state === "locked" ? true : false; + const [isChecked, setIsChecked] = useState(boolState); + const [status, setStatus] = useState(state); + const updateLockHandler = () => { + const status = isChecked === true ? "unlocked" : "locked"; + setStatus(status); + setIsChecked(!isChecked); + }; + + const icon = isChecked === true ? : ; + + return ( +
+ + + {icon} + {status} + +
+ ); +}; + +export default ToggleCheckBox; diff --git a/src/components/toggleStatus/ToggleStatus.scss b/src/components/toggleStatus/ToggleStatus.scss new file mode 100644 index 0000000..ca6f226 --- /dev/null +++ b/src/components/toggleStatus/ToggleStatus.scss @@ -0,0 +1,85 @@ +.statusContent { + display: flex; + justify-content: space-between; + align-items: center; + + .stateText { + text-transform: capitalize; + display: flex; + align-items: center; + + > svg { + margin-right: 5px; + } + > span{ + font-size: 18px; + } + } + + .greenText { + color: #30b075; + } + + .redText { + color: #d74d4d; + } +} + +.switch { + position: relative; + display: inline-block; + width: 64px; + height: 26px; + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #d74d4d; + -webkit-transition: 0.4s; + transition: 0.4s; + + &:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: 0.4s; + transition: 0.4s; + } + + &.round { + border-radius: 34px; + + &:before { + border-radius: 50%; + } + } + } + + input { + opacity: 0; + width: 0; + height: 0; + } +} + +input:checked + .slider { + background-color: #30b075; +} + +input:focus + .slider { + box-shadow: 0 0 1px #30b075; +} + +input:checked + .slider:before { + -webkit-transform: translateX(36px); + -ms-transform: translateX(36px); + transform: translateX(36px); +} diff --git a/src/components/toggleStatus/__test__/ToggleStatus.test.js b/src/components/toggleStatus/__test__/ToggleStatus.test.js new file mode 100644 index 0000000..b3348f4 --- /dev/null +++ b/src/components/toggleStatus/__test__/ToggleStatus.test.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { expect } from 'chai'; +import ToggleStatus from '../ToggleStatus'; + +describe('src/components/toggleStatus/__test__/ToggleStatus.test.js', () => { + describe('', () => { + it('should render tabs properly', () => { + const wrapper = shallow(); + expect(wrapper.find('input')).to.have.length(1); + }); + }); +}); \ No newline at end of file diff --git a/src/components/userCard/UserCard.js b/src/components/userCard/UserCard.js new file mode 100644 index 0000000..0148825 --- /dev/null +++ b/src/components/userCard/UserCard.js @@ -0,0 +1,48 @@ +import React from "react"; +import { Card, Badge } from "react-bootstrap"; +import dayjs from "dayjs"; + +const UserCard = props => { + const { + attributes: { name, email, status, starts_at = "", ends_at = "" } = {} + } = props.info; + + let badgeVariant, badgeText; + if (starts_at && ends_at) { + badgeVariant = "warning"; + badgeText = "Upcoming"; + } else if (status === "current") { + badgeVariant = "success"; + badgeText = "Active"; + } + + const formattedDate = (date = "") => { + return dayjs(date).format("MMM YY HH:mm"); + }; + + return ( + + +
+
 
+
+

{name}

+

{email}

+ {starts_at && ends_at && ( +

{`${formattedDate( + starts_at + )} - ${formattedDate(ends_at)}`}

+ )} +
+ + {badgeText} + +
+
+
+
+
+ ); +}; + +export default UserCard; diff --git a/src/components/userCard/__test__/UserCard.test.js b/src/components/userCard/__test__/UserCard.test.js new file mode 100644 index 0000000..466714e --- /dev/null +++ b/src/components/userCard/__test__/UserCard.test.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { expect } from 'chai'; +import UserCard from '../UserCard'; + +describe('src/components/userCard/__test__/UserCard.test.js', () => { + describe('', () => { + it('should render tabs properly', () => { + const wrapper = shallow(); + expect(wrapper.find('Card')).to.have.length(1); + }); + }); +}); \ No newline at end of file diff --git a/src/components/users/Users.js b/src/components/users/Users.js new file mode 100644 index 0000000..54f5be1 --- /dev/null +++ b/src/components/users/Users.js @@ -0,0 +1,26 @@ +import React, { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { fetchUsers } from "~/store/actions"; +import { UserCard } from "~/components"; + +const Users = () => { + const dispatch = useDispatch(); + const { users } = useSelector(state => state.usersInfo); + + useEffect(() => { + dispatch(fetchUsers()); + }, [dispatch]); + + return ( +
+ {users && + users.map((user, i) => ( +
+ +
+ ))} +
+ ); +}; + +export default Users; diff --git a/src/components/users/__test__/Users.test.js b/src/components/users/__test__/Users.test.js new file mode 100644 index 0000000..78667c1 --- /dev/null +++ b/src/components/users/__test__/Users.test.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { expect } from 'chai'; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import Users from '../Users'; + +describe('src/components/users/__test__/Users.test.js', () => { + describe('', () => { + const initialState = {}; + const mockStore = configureStore(); + let store, wrapper; + + beforeEach(() => { + store = mockStore(initialState); + wrapper = shallow( + + + + ); + }); + + it("should render tabs properly", () => { + expect(wrapper.find("Users")).to.have.length(1); + }); + }); +}); \ No newline at end of file diff --git a/src/global.scss b/src/global.scss new file mode 100644 index 0000000..3b87807 --- /dev/null +++ b/src/global.scss @@ -0,0 +1,57 @@ +html, +body { + background-color: $var-colors-white-smoke; + @include base-font; + height: 100%; + margin: 0; + -webkit-overflow-scrolling: touch; + + > div:first-child { + // Make the div holding the app full-height + // For some reason #app selector won't work here ¯\_(ツ)_/¯ + height: 100%; + } + + a { + @include link-text; + } + + h1 { + @include heading-1; + } + + h2 { + @include heading-2; + } + + h3 { + @include heading-3; + } + + h4 { + @include heading-4; + } + + h5 { + @include heading-5; + } + + h6 { + @include heading-6; + } + + hr { + color: $var-colors-grey-light; + border-top: 1px solid $var-colors-grey-light; + border-bottom: 0; + } + + strong { + @include font-open-sans-semibold; + color: black; + } +} + +button { + font-size: 1rem !important; +} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..3277271 --- /dev/null +++ b/src/index.html @@ -0,0 +1,11 @@ + + + + + + Remote Lock + + +
+ + diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..7d165aa --- /dev/null +++ b/src/index.js @@ -0,0 +1 @@ +import("./bootstrap"); \ No newline at end of file diff --git a/src/store/actions/deviceActions.js b/src/store/actions/deviceActions.js new file mode 100644 index 0000000..fa085fc --- /dev/null +++ b/src/store/actions/deviceActions.js @@ -0,0 +1,36 @@ +import { + FETCH_DEVICES, + FETCH_DEVICES_SUCCESS, + FETCH_DEVICES_FAILURE +} from "../constants"; +import axios from "axios"; + +export function fetchDevices() { + return dispatch => { + dispatch(request()); + return axios + .get(`http://localhost:4000/api/devices.json`) + .then(response => { + dispatch(success(response.data.data)); + }) + .catch(error => { + dispatch(failure("Error Occurs")); + }); + }; + + function request() { + return { type: FETCH_DEVICES }; + } + function success(payload) { + return { type: FETCH_DEVICES_SUCCESS, payload }; + } + function failure(error) { + return { type: FETCH_DEVICES_FAILURE, error }; + } +} + +export function updateDeviceStatus() { + return dispatch => { + dispatch({ type: FETCH_DEVICES }); + }; +} diff --git a/src/store/actions/index.js b/src/store/actions/index.js new file mode 100644 index 0000000..0d2e588 --- /dev/null +++ b/src/store/actions/index.js @@ -0,0 +1,2 @@ +export * from "./deviceActions"; +export * from "./userActions"; diff --git a/src/store/actions/userActions.js b/src/store/actions/userActions.js new file mode 100644 index 0000000..a1f1de4 --- /dev/null +++ b/src/store/actions/userActions.js @@ -0,0 +1,30 @@ +import { + FETCH_USER_REQUEST, + FETCH_USER_REQUEST_SUCCESS, + FETCH_USER_REQUEST_FAILURE +} from "../constants"; +import axios from "axios"; + +export function fetchUsers() { + return dispatch => { + dispatch(request()); + return axios + .get(`http://localhost:4000/api/users.json`) + .then(response => { + dispatch(success(response.data.data)); + }) + .catch(error => { + dispatch(failure("Error Occurs")); + }); + }; + + function request() { + return { type: FETCH_USER_REQUEST }; + } + function success(payload) { + return { type: FETCH_USER_REQUEST_SUCCESS, payload }; + } + function failure(error) { + return { type: FETCH_USER_REQUEST_FAILURE, error }; + } +} diff --git a/src/store/constants.js b/src/store/constants.js new file mode 100644 index 0000000..2940902 --- /dev/null +++ b/src/store/constants.js @@ -0,0 +1,7 @@ +export const FETCH_DEVICES = "FETCH_DEVICES"; +export const FETCH_DEVICES_SUCCESS = "FETCH_DEVICES_SUCCESS"; +export const FETCH_DEVICES_FAILURE = "FETCH_DEVICES_FAILURE"; +export const FETCH_USER_REQUEST = "FETCH_USER_REQUEST"; +export const FETCH_USER_REQUEST_SUCCESS = "FETCH_USER_REQUEST_SUCCESS"; +export const FETCH_USER_REQUEST_FAILURE = "FETCH_USER_REQUEST_FAILURE"; +export const UPDATE_DEVICE_STATUS = "UPDATE_DEVICE_STATUS"; diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..8a5ca79 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,15 @@ +import { createStore, compose, applyMiddleware } from "redux"; +import thunk from "redux-thunk"; +import rootReducers from "./reducers"; + +const middleWare = []; + +middleWare.push(thunk); + +const store = createStore( + rootReducers, + {}, + compose(applyMiddleware(...middleWare)) +); + +export default store; diff --git a/src/store/reducers/deviceReducer.js b/src/store/reducers/deviceReducer.js new file mode 100644 index 0000000..414f32d --- /dev/null +++ b/src/store/reducers/deviceReducer.js @@ -0,0 +1,38 @@ +import { + FETCH_DEVICES, + FETCH_DEVICES_SUCCESS, + FETCH_DEVICES_FAILURE +} from "../constants"; + +const initialState = { + loading: false, + error: null, + data: null +}; + +const deviceReducer = (state = initialState, action) => { + switch (action.type) { + case FETCH_DEVICES: + return { + ...state, + loading: true + }; + case FETCH_DEVICES_SUCCESS: + return { + ...state, + loading: false, + error: null, + data: action.payload + }; + case FETCH_DEVICES_FAILURE: + return { + ...state, + loading: false, + error: action.error + }; + default: + return state; + } +}; + +export default deviceReducer; diff --git a/src/store/reducers/index.js b/src/store/reducers/index.js new file mode 100644 index 0000000..d655e1a --- /dev/null +++ b/src/store/reducers/index.js @@ -0,0 +1,8 @@ +import { combineReducers } from "redux"; +import deviceReducer from "./deviceReducer"; +import userReducer from "./userReducer"; + +export default combineReducers({ + devicesInfo: deviceReducer, + usersInfo: userReducer +}); diff --git a/src/store/reducers/userReducer.js b/src/store/reducers/userReducer.js new file mode 100644 index 0000000..29b4506 --- /dev/null +++ b/src/store/reducers/userReducer.js @@ -0,0 +1,38 @@ +import { + FETCH_USER_REQUEST, + FETCH_USER_REQUEST_SUCCESS, + FETCH_USER_REQUEST_FAILURE +} from "../constants"; + +const initialState = { + loading: false, + error: null, + users: null +}; + +const userReducer = (state = initialState, action) => { + switch (action.type) { + case FETCH_USER_REQUEST: + return { + ...state, + loading: true + }; + case FETCH_USER_REQUEST_SUCCESS: + return { + ...state, + loading: false, + error: null, + users: action.payload + }; + case FETCH_USER_REQUEST_FAILURE: + return { + ...state, + loading: false, + error: action.error + }; + default: + return state; + } +}; + +export default userReducer; diff --git a/src/theme.scss b/src/theme.scss new file mode 100644 index 0000000..9438288 --- /dev/null +++ b/src/theme.scss @@ -0,0 +1,3 @@ +@import 'src/_fonts.scss'; +@import 'src/_variables.scss'; +@import 'src/_mixins.scss'; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..bdf74c4 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,67 @@ +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const path = require("path"); + +module.exports = { + entry: "./src/index", + mode: "development", + devServer: { + contentBase: path.join(__dirname, "dist"), + port: 3002 + }, + output: { + publicPath: "auto" + }, + module: { + rules: [ + { + test: /\.jsx?$/, + loader: "babel-loader", + exclude: /node_modules/, + options: { + presets: ["@babel/preset-react"] + } + }, + { + test: /\.(css|(s(a|c)ss))$/, + use: [ + { + loader: "style-loader", + options: { + sourceMap: true + } + }, + { + loader: "css-loader", + options: { + sourceMap: true + } + }, + { + loader: "sass-loader", + options: { + sourceMap: true, + data: '@import "./src/theme.scss";', + includePaths: [__dirname] + } + } + ] + }, + { + test: /\.(woff|woff2|eot|ttf|svg|jpg|png)$/, + use: { + loader: "url-loader" + } + } + ] + }, + resolve: { + alias: { + "~": path.resolve(__dirname, "./src") + } + }, + plugins: [ + new HtmlWebpackPlugin({ + template: "./src/index.html" + }) + ] +};